From dfbc19e5e78d87ec735839333a647cae409f53eb Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Sun, 28 Jul 2024 00:02:07 -0600 Subject: swift-format --- Sources/GopherHelpers/GopherHelpers.swift | 390 ++++++------ Sources/swift-gopher/gopherHandler.swift | 704 +++++++++++---------- Sources/swift-gopher/helpers.swift | 14 +- Sources/swift-gopher/server.swift | 150 ++--- Sources/swiftGopherClient/gopherClient.swift | 230 +++---- .../gopherRequestResponseHandler.swift | 188 +++--- .../swiftGopherClientTests.swift | 44 +- 7 files changed, 865 insertions(+), 855 deletions(-) diff --git a/Sources/GopherHelpers/GopherHelpers.swift b/Sources/GopherHelpers/GopherHelpers.swift index 15f14cb..d561d33 100644 --- a/Sources/GopherHelpers/GopherHelpers.swift +++ b/Sources/GopherHelpers/GopherHelpers.swift @@ -43,216 +43,216 @@ import NIOCore */ public enum gopherItemType { - case text - case directory - case nameserver - case error - case binhex - case bindos - case uuencoded - case search - case telnet - case binary - case mirror - case gif - case image - case tn3270Session - case bitmap - case movie - case sound - case doc - case html - case info + case text + case directory + case nameserver + case error + case binhex + case bindos + case uuencoded + case search + case telnet + case binary + case mirror + case gif + case image + case tn3270Session + case bitmap + case movie + case sound + case doc + case html + case info } public struct gopherItem { - public var rawLine: String - public var rawData: ByteBuffer? - public var message: String = "" - public var parsedItemType: gopherItemType = .info - public var host: String = "error.host" - public var port: Int = 1 - public var selector: String = "" - public var valid: Bool = true + public var rawLine: String + public var rawData: ByteBuffer? + public var message: String = "" + public var parsedItemType: gopherItemType = .info + public var host: String = "error.host" + public var port: Int = 1 + public var selector: String = "" + public var valid: Bool = true - public init(rawLine: String) { - self.rawLine = rawLine - } + public init(rawLine: String) { + self.rawLine = rawLine + } } public func getGopherFileType(item: String) -> gopherItemType { - switch item { - case "0": - return .text - case "1": - return .directory - case "2": - return .nameserver - case "3": - return .error - case "4": - return .binhex - case "5": - return .bindos - case "6": - return .uuencoded - case "7": - return .search - case "8": - return .telnet - case "9": - return .binary - case "+": - return .mirror - case "g": - return .gif - case "I": - return .image - case "T": - return .tn3270Session - case ":": - return .bitmap - case ";": - return .movie - case "<": - return .sound - case "d": - return .doc - case "h": - return .html - case "i": - return .info - case "p": - return .image - case "r": - return .doc - case "s": - return .doc - case "P": - return .doc - case "X": - return .doc - default: - return .info - } + switch item { + case "0": + return .text + case "1": + return .directory + case "2": + return .nameserver + case "3": + return .error + case "4": + return .binhex + case "5": + return .bindos + case "6": + return .uuencoded + case "7": + return .search + case "8": + return .telnet + case "9": + return .binary + case "+": + return .mirror + case "g": + return .gif + case "I": + return .image + case "T": + return .tn3270Session + case ":": + return .bitmap + case ";": + return .movie + case "<": + return .sound + case "d": + return .doc + case "h": + return .html + case "i": + return .info + case "p": + return .image + case "r": + return .doc + case "s": + return .doc + case "P": + return .doc + case "X": + return .doc + default: + return .info + } } public func getFileType(fileExtension: String) -> gopherItemType { - switch fileExtension { - case "txt": - return .text - case "md": - return .text - case "html": - return .html - case "pdf": - return .doc - case "png": - return .image - case "gif": - return .gif - case "jpg": - return .image - case "jpeg": - return .image - case "mp3": - return .sound - case "wav": - return .sound - case "mp4": - return .movie - case "mov": - return .movie - case "avi": - return .movie - case "rtf": - return .doc - case "xml": - return .doc - default: - return .binary - } + switch fileExtension { + case "txt": + return .text + case "md": + return .text + case "html": + return .html + case "pdf": + return .doc + case "png": + return .image + case "gif": + return .gif + case "jpg": + return .image + case "jpeg": + return .image + case "mp3": + return .sound + case "wav": + return .sound + case "mp4": + return .movie + case "mov": + return .movie + case "avi": + return .movie + case "rtf": + return .doc + case "xml": + return .doc + default: + return .binary + } } public func fileTypeToGopherItem(fileType: gopherItemType) -> String { - switch fileType { - case .text: - return "0" - case .directory: - return "1" - case .nameserver: - return "2" - case .error: - return "3" - case .binhex: - return "4" - case .bindos: - return "5" - case .uuencoded: - return "6" - case .search: - return "7" - case .telnet: - return "8" - case .binary: - return "9" - case .mirror: - return "+" - case .tn3270Session: - return "T" - case .gif: - return "g" - case .image: - return "I" - case .bitmap: - return "b" - case .movie: - return "M" - case .sound: - return "s" - case .doc: - return "d" - case .html: - return "h" - case .info: - return "i" - // case .png: - // return "p" - // case .rtf: - // return "t" - // case .wavfile: - // return "w" - // case .pdf: - // return "P" - // case .xml: - // return "x" - } + switch fileType { + case .text: + return "0" + case .directory: + return "1" + case .nameserver: + return "2" + case .error: + return "3" + case .binhex: + return "4" + case .bindos: + return "5" + case .uuencoded: + return "6" + case .search: + return "7" + case .telnet: + return "8" + case .binary: + return "9" + case .mirror: + return "+" + case .tn3270Session: + return "T" + case .gif: + return "g" + case .image: + return "I" + case .bitmap: + return "b" + case .movie: + return "M" + case .sound: + return "s" + case .doc: + return "d" + case .html: + return "h" + case .info: + return "i" + // case .png: + // return "p" + // case .rtf: + // return "t" + // case .wavfile: + // return "w" + // case .pdf: + // return "P" + // case .xml: + // return "x" + } } public func itemToImageType(_ item: gopherItem) -> String { - switch item.parsedItemType { - case .text: - return "doc.plaintext" - case .directory: - return "folder" - case .error: - return "exclamationmark.triangle" - case .gif: - return "photo.stack" - case .image: - return "photo" - case .doc: - return "doc.richtext" - case .sound: - return "music.note" - case .bitmap: - return "photo" - case .html: - return "globe" - case .movie: - return "videoprojector" - default: - return "questionmark.square.dashed" - } + switch item.parsedItemType { + case .text: + return "doc.plaintext" + case .directory: + return "folder" + case .error: + return "exclamationmark.triangle" + case .gif: + return "photo.stack" + case .image: + return "photo" + case .doc: + return "doc.richtext" + case .sound: + return "music.note" + case .bitmap: + return "photo" + case .html: + return "globe" + case .movie: + return "videoprojector" + default: + return "questionmark.square.dashed" + } } diff --git a/Sources/swift-gopher/gopherHandler.swift b/Sources/swift-gopher/gopherHandler.swift index 8b7100d..8f55f5d 100644 --- a/Sources/swift-gopher/gopherHandler.swift +++ b/Sources/swift-gopher/gopherHandler.swift @@ -5,409 +5,417 @@ import Logging import NIO final class GopherHandler: ChannelInboundHandler { - typealias InboundIn = ByteBuffer - typealias OutboundOut = ByteBuffer - - let gopherdata_dir: String - let gopherdata_host: String - let gopherdata_port: Int - let logger: Logger - let enableSearch: Bool - let disableGophermap: Bool - - init( - logger: Logger, - gopherdata_dir: String = "./example-gopherdata", gopherdata_host: String = "localhost", - gopherdata_port: Int = 70, enableSearch: Bool = false, - disableGophermap: Bool = false - ) { - self.gopherdata_dir = gopherdata_dir - self.gopherdata_host = gopherdata_host - self.gopherdata_port = gopherdata_port - self.logger = logger - self.enableSearch = enableSearch - self.disableGophermap = disableGophermap - } - - private var buffer = ByteBuffer() - let delChar = Character(UnicodeScalar(127)) - let backspaceChar = Character(UnicodeScalar(8)) - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - var input = self.unwrapInboundIn(data) - buffer.writeBuffer(&input) - - // print(buffer.readableBytes) - - if let requestString = buffer.getString(at: 0, length: buffer.readableBytes) { - if requestString.firstIndex(of: "\r\n") != nil || requestString.firstIndex(of: "\n") != nil - || requestString.firstIndex(of: "\r") != nil - { // May not be necessary to use last two cases - if let remoteAddress = context.remoteAddress { - logger.info( - "Received request from \(remoteAddress) for '\(requestString.replacingOccurrences(of: "\r\n", with: "").replacingOccurrences(of: "\n", with: ""))'" - ) - } else { - logger.warning("Unable to retrieve remote address") - } + typealias InboundIn = ByteBuffer + typealias OutboundOut = ByteBuffer + + let gopherdata_dir: String + let gopherdata_host: String + let gopherdata_port: Int + let logger: Logger + let enableSearch: Bool + let disableGophermap: Bool + + init( + logger: Logger, + gopherdata_dir: String = "./example-gopherdata", gopherdata_host: String = "localhost", + gopherdata_port: Int = 70, enableSearch: Bool = false, + disableGophermap: Bool = false + ) { + self.gopherdata_dir = gopherdata_dir + self.gopherdata_host = gopherdata_host + self.gopherdata_port = gopherdata_port + self.logger = logger + self.enableSearch = enableSearch + self.disableGophermap = disableGophermap + } - var processedRequestString: String = requestString.replacingOccurrences( - of: "\r\0", with: "") - // Check for backspace or delete and process them - if processedRequestString.contains(delChar) - || processedRequestString.contains(backspaceChar) - { - logger.info( - "Request contains delete character (ASCII code 127) or the backsapce character (ASCII code 8), processing delete sequences" - ) - - func processDeleteCharacter(_ input: String, _ asciiCode: Int = 8) -> String { - var result: [Character] = [] - for character in input { - if let asciiValue = character.asciiValue, asciiValue == asciiCode { - if !result.isEmpty { - result.removeLast() + private var buffer = ByteBuffer() + let delChar = Character(UnicodeScalar(127)) + let backspaceChar = Character(UnicodeScalar(8)) + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + var input = self.unwrapInboundIn(data) + buffer.writeBuffer(&input) + + // print(buffer.readableBytes) + + if let requestString = buffer.getString(at: 0, length: buffer.readableBytes) { + if requestString.firstIndex(of: "\r\n") != nil + || requestString.firstIndex(of: "\n") != nil + || requestString.firstIndex(of: "\r") != nil + { // May not be necessary to use last two cases + if let remoteAddress = context.remoteAddress { + logger.info( + "Received request from \(remoteAddress) for '\(requestString.replacingOccurrences(of: "\r\n", with: "").replacingOccurrences(of: "\n", with: ""))'" + ) + } else { + logger.warning("Unable to retrieve remote address") } - } else { - result.append(character) - } - } - return String(result) - } - processedRequestString = processDeleteCharacter(requestString, 127) - processedRequestString = processDeleteCharacter(processedRequestString) // Could just combine in one statement if asciiCode is changed to asciiCodes: [Int] - } + var processedRequestString: String = requestString.replacingOccurrences( + of: "\r\0", with: "") + // Check for backspace or delete and process them + if processedRequestString.contains(delChar) + || processedRequestString.contains(backspaceChar) + { + logger.info( + "Request contains delete character (ASCII code 127) or the backsapce character (ASCII code 8), processing delete sequences" + ) + + func processDeleteCharacter(_ input: String, _ asciiCode: Int = 8) -> String { + var result: [Character] = [] + for character in input { + if let asciiValue = character.asciiValue, asciiValue == asciiCode { + if !result.isEmpty { + result.removeLast() + } + } else { + result.append(character) + } + } + return String(result) + } + + processedRequestString = processDeleteCharacter(requestString, 127) + processedRequestString = processDeleteCharacter(processedRequestString) // Could just combine in one statement if asciiCode is changed to asciiCodes: [Int] + } -// for character in requestString { // Helpful for debugging -// if let scalar = character.unicodeScalars.first, scalar.value < 128 { -// print("\(character): \(scalar.value)") -// } else { -// print("\(character): Not an ASCII character") -// } -// } - - let response = processGopherRequest(processedRequestString) - var outputBuffer: ByteBuffer - switch response { - case .string(let string): - outputBuffer = context.channel.allocator.buffer(string: string) - case .data(let data): - outputBuffer = context.channel.allocator.buffer(bytes: data) - } + // for character in requestString { // Helpful for debugging + // if let scalar = character.unicodeScalars.first, scalar.value < 128 { + // print("\(character): \(scalar.value)") + // } else { + // print("\(character): Not an ASCII character") + // } + // } + + let response = processGopherRequest(processedRequestString) + var outputBuffer: ByteBuffer + switch response { + case .string(let string): + outputBuffer = context.channel.allocator.buffer(string: string) + case .data(let data): + outputBuffer = context.channel.allocator.buffer(bytes: data) + } - context.writeAndFlush(self.wrapOutboundOut(outputBuffer)).whenComplete { _ in - context.close(mode: .all, promise: nil) - } + context.writeAndFlush(self.wrapOutboundOut(outputBuffer)).whenComplete { _ in + context.close(mode: .all, promise: nil) + } - } else { - //print("No CR/LF") - } - } - } - - func requestHandler(path: URL) -> ResponseType { - logger.info("Handling request for '\(path.path)'") - - // Check if path is a directory or a file - let fm = FileManager.default - var isDir: ObjCBool = false - - if fm.fileExists(atPath: path.path, isDirectory: &isDir) { - if isDir.boolValue { - return .string(prepareGopherMenu(path: path)) - } else { - // Check if file is plain text or binary - let fileExtension = path.pathExtension - let fileType = getFileType(fileExtension: fileExtension) - - if fileType == .text || fileType == .html { - do { - let fileContents = try String(contentsOfFile: path.path, encoding: .utf8) - - return .string(fileContents) - } catch { - logger.error("Error reading file: \(path.path) Error: \(error)") - return .string("3Error reading file...\t\terror.host\t1\r\n") - } - } else { - // Handle binary file - do { - let fileContents = try Data(contentsOf: path) - return .data(fileContents) - } catch { - logger.error("Error reading binary file: \(path.path) Error: \(error)") - return .string("3Error reading file...\t\terror.host\t1\r\n") - } + } else { + //print("No CR/LF") + } } - - } - } else { - logger.error("Error reading directory: \(path.path) Directory does not exist.") - return .string("3Error reading file...\t\terror.host\t1\r\n") } - } + func requestHandler(path: URL) -> ResponseType { + logger.info("Handling request for '\(path.path)'") + + // Check if path is a directory or a file + let fm = FileManager.default + var isDir: ObjCBool = false + + if fm.fileExists(atPath: path.path, isDirectory: &isDir) { + if isDir.boolValue { + return .string(prepareGopherMenu(path: path)) + } else { + // Check if file is plain text or binary + let fileExtension = path.pathExtension + let fileType = getFileType(fileExtension: fileExtension) + + if fileType == .text || fileType == .html { + do { + let fileContents = try String(contentsOfFile: path.path, encoding: .utf8) + + return .string(fileContents) + } catch { + logger.error("Error reading file: \(path.path) Error: \(error)") + return .string("3Error reading file...\t\terror.host\t1\r\n") + } + } else { + // Handle binary file + do { + let fileContents = try Data(contentsOf: path) + return .data(fileContents) + } catch { + logger.error("Error reading binary file: \(path.path) Error: \(error)") + return .string("3Error reading file...\t\terror.host\t1\r\n") + } + } - func preparePath(path: String = "/") -> URL { - var sanitizedPath = sanitizeSelectorPath(path: path) - let base_dir = URL(fileURLWithPath: gopherdata_dir) - if base_dir.path.hasSuffix("/") && sanitizedPath.hasPrefix("/") { - sanitizedPath = String(sanitizedPath.dropFirst()) - } + } + } else { + logger.error("Error reading directory: \(path.path) Directory does not exist.") + return .string("3Error reading file...\t\terror.host\t1\r\n") + } - // Now check if there is still a prefix - if sanitizedPath.hasPrefix("/") { - sanitizedPath = String(sanitizedPath.dropFirst()) } - let full_path = base_dir.appendingPathComponent(sanitizedPath) - return full_path - } - - func generateGopherItem( - item_name: String, item_path: URL, item_host: String? = nil, item_port: String? = nil - ) -> String { - let myItemHost = item_host ?? gopherdata_host - let myItemPort = item_port ?? String(gopherdata_port) - let base_path = URL(fileURLWithPath: gopherdata_dir) - var relative_path = item_path.path.replacingOccurrences(of: base_path.path, with: "") - if !relative_path.hasPrefix("/") { - relative_path = "/\(relative_path)" - } - return "\(item_name)\t\(relative_path)\t\(myItemHost)\t\(myItemPort)\r\n" - } + func preparePath(path: String = "/") -> URL { + var sanitizedPath = sanitizeSelectorPath(path: path) + let base_dir = URL(fileURLWithPath: gopherdata_dir) + if base_dir.path.hasSuffix("/") && sanitizedPath.hasPrefix("/") { + sanitizedPath = String(sanitizedPath.dropFirst()) + } - func generateGopherMap(path: URL) -> [String] { - var items: [String] = [] + // Now check if there is still a prefix + if sanitizedPath.hasPrefix("/") { + sanitizedPath = String(sanitizedPath.dropFirst()) + } - var basePath = URL(fileURLWithPath: gopherdata_dir).path - if basePath.hasSuffix("/") { - basePath = String(basePath.dropLast()) + let full_path = base_dir.appendingPathComponent(sanitizedPath) + return full_path } - let fm = FileManager.default - do { - print("Reading directory: \(path.path)") - let itemsInDirectory = try fm.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) - for item in itemsInDirectory { - let isDirectory = try item.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false - let name = item.lastPathComponent - if isDirectory { - items.append(generateGopherItem(item_name: "1\(name)", item_path: item)) - } else { - let fileType = getFileType(fileExtension: item.pathExtension) - let gopherFileType = fileTypeToGopherItem(fileType: fileType) - items.append(generateGopherItem(item_name: "\(gopherFileType)\(name)", item_path: item)) + func generateGopherItem( + item_name: String, item_path: URL, item_host: String? = nil, item_port: String? = nil + ) -> String { + let myItemHost = item_host ?? gopherdata_host + let myItemPort = item_port ?? String(gopherdata_port) + let base_path = URL(fileURLWithPath: gopherdata_dir) + var relative_path = item_path.path.replacingOccurrences(of: base_path.path, with: "") + if !relative_path.hasPrefix("/") { + relative_path = "/\(relative_path)" } - } - } catch { - print("Error reading directory: \(path.path)") + return "\(item_name)\t\(relative_path)\t\(myItemHost)\t\(myItemPort)\r\n" } - return items - } - - func prepareGopherMenu(path: URL = URL(string: "/")!) -> String { - var gopherResponse: [String] = [] - - let fm = FileManager.default - - do { - let gophermap_path = path.appendingPathComponent("gophermap") - if fm.fileExists(atPath: gophermap_path.path) && !disableGophermap { - let gophermap_contents = try String(contentsOfFile: gophermap_path.path, encoding: .utf8) - let gophermap_lines = gophermap_contents.components(separatedBy: "\n") - for originalLine in gophermap_lines { - // Only keep first 80 characters - var line = String(originalLine) //.prefix(80) - if "0123456789+gIT:; 1 { - if line.hasSuffix("\n") { - line = String(line.dropLast()) - } - if line.prefix(1) == "i" { - gopherResponse.append("\(line)\t\terror.host\t1\r\n") - continue - } - - let regex = try! NSRegularExpression(pattern: "\\t+| {2,}") - let nsString = line as NSString - let range = NSRange(location: 0, length: nsString.length) - let matches = regex.matches(in: String(line), range: range) - var lastRangeEnd = 0 - var components = [String]() + func generateGopherMap(path: URL) -> [String] { + var items: [String] = [] - for match in matches { - let range = NSRange( - location: lastRangeEnd, length: match.range.location - lastRangeEnd) - components.append(nsString.substring(with: range)) - lastRangeEnd = match.range.location + match.range.length - } + var basePath = URL(fileURLWithPath: gopherdata_dir).path + if basePath.hasSuffix("/") { + basePath = String(basePath.dropLast()) + } - if lastRangeEnd < nsString.length { - components.append(nsString.substring(from: lastRangeEnd)) + let fm = FileManager.default + do { + print("Reading directory: \(path.path)") + let itemsInDirectory = try fm.contentsOfDirectory( + at: path, includingPropertiesForKeys: nil) + for item in itemsInDirectory { + let isDirectory = + try item.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false + let name = item.lastPathComponent + if isDirectory { + items.append(generateGopherItem(item_name: "1\(name)", item_path: item)) + } else { + let fileType = getFileType(fileExtension: item.pathExtension) + let gopherFileType = fileTypeToGopherItem(fileType: fileType) + items.append( + generateGopherItem(item_name: "\(gopherFileType)\(name)", item_path: item)) + } } + } catch { + print("Error reading directory: \(path.path)") + } + return items + } - if components.count < 3 { - continue + func prepareGopherMenu(path: URL = URL(string: "/")!) -> String { + var gopherResponse: [String] = [] + + let fm = FileManager.default + + do { + let gophermap_path = path.appendingPathComponent("gophermap") + if fm.fileExists(atPath: gophermap_path.path) && !disableGophermap { + let gophermap_contents = try String( + contentsOfFile: gophermap_path.path, encoding: .utf8) + let gophermap_lines = gophermap_contents.components(separatedBy: "\n") + for originalLine in gophermap_lines { + // Only keep first 80 characters + var line = String(originalLine) //.prefix(80) + if "0123456789+gIT:; 1 { + if line.hasSuffix("\n") { + line = String(line.dropLast()) + } + if line.prefix(1) == "i" { + gopherResponse.append("\(line)\t\terror.host\t1\r\n") + continue + } + + let regex = try! NSRegularExpression(pattern: "\\t+| {2,}") + let nsString = line as NSString + let range = NSRange(location: 0, length: nsString.length) + let matches = regex.matches(in: String(line), range: range) + + var lastRangeEnd = 0 + var components = [String]() + + for match in matches { + let range = NSRange( + location: lastRangeEnd, length: match.range.location - lastRangeEnd) + components.append(nsString.substring(with: range)) + lastRangeEnd = match.range.location + match.range.length + } + + if lastRangeEnd < nsString.length { + components.append(nsString.substring(from: lastRangeEnd)) + } + + if components.count < 3 { + continue + } + + let item_name = components[0] + let item_path = components[1] + let item_host = components[2] + let item_port = components.count > 3 ? components[3] : "70" + + let item_line = "\(item_name)\t\(item_path)\t\(item_host)\t\(item_port)\r\n" + gopherResponse.append(item_line) + } else { + line = line.replacingOccurrences(of: "\n", with: "") + gopherResponse.append("i\(line)\t\terror.host\t1\r\n") + } + } + } else { + print("No gophermap found for \(path.path)") + gopherResponse = generateGopherMap(path: path) } + } catch { + logger.error("Error reading directory: \(path.path)") + gopherResponse.append("3Error reading directory...\r\n") + } - let item_name = components[0] - let item_path = components[1] - let item_host = components[2] - let item_port = components.count > 3 ? components[3] : "70" - - let item_line = "\(item_name)\t\(item_path)\t\(item_host)\t\(item_port)\r\n" - gopherResponse.append(item_line) - } else { - line = line.replacingOccurrences(of: "\n", with: "") - gopherResponse.append("i\(line)\t\terror.host\t1\r\n") - } + // Append Search + if enableSearch { + let search_line = "7Search Server\t/search\t\(gopherdata_host)\t\(gopherdata_port)\r\n" + gopherResponse.append(search_line) } - } else { - print("No gophermap found for \(path.path)") - gopherResponse = generateGopherMap(path: path) - } - } catch { - logger.error("Error reading directory: \(path.path)") - gopherResponse.append("3Error reading directory...\r\n") - } - // Append Search - if enableSearch { - let search_line = "7Search Server\t/search\t\(gopherdata_host)\t\(gopherdata_port)\r\n" - gopherResponse.append(search_line) - } + // Append Server Info + gopherResponse.append(buildVersionStringResponse()) - // Append Server Info - gopherResponse.append(buildVersionStringResponse()) + return gopherResponse.joined(separator: "") + } - return gopherResponse.joined(separator: "") - } + // TODO: Refactor + func performSearch(query: String) -> String { + // Really basic search implementation - // TODO: Refactor - func performSearch(query: String) -> String { - // Really basic search implementation + var search_results = [String: String]() - var search_results = [String: String]() + let fm = FileManager.default - let fm = FileManager.default + let base_dir = URL(fileURLWithPath: gopherdata_dir) - let base_dir = URL(fileURLWithPath: gopherdata_dir) + let enumerator = fm.enumerator(at: base_dir, includingPropertiesForKeys: nil) - let enumerator = fm.enumerator(at: base_dir, includingPropertiesForKeys: nil) + while let file = enumerator?.nextObject() as? URL { + let file_name = file.lastPathComponent + let file_path = file.path + let file_type = + try? file.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false + ? "1" : "0" - while let file = enumerator?.nextObject() as? URL { - let file_name = file.lastPathComponent - let file_path = file.path - let file_type = - try? file.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false ? "1" : "0" + if file_type == "0" { + // Check if file name or contents match the query + let file_contents = try? String(contentsOfFile: file_path, encoding: .utf8) + if file_name.lowercased().contains(query) + || (file_contents?.lowercased().contains(query) ?? false) + { + search_results[file_name] = file_path + } + } else { + // Check if directory name matches the query + if file_name.lowercased().contains(query) { + search_results[file_name] = file_path + } + } + } - if file_type == "0" { - // Check if file name or contents match the query - let file_contents = try? String(contentsOfFile: file_path, encoding: .utf8) - if file_name.lowercased().contains(query) - || (file_contents?.lowercased().contains(query) ?? false) - { - search_results[file_name] = file_path + // Prepare Gopher menu with search results + var gopherResponse: [String] = [] + + for (_, file_path) in search_results { + var item_type = + try? URL(fileURLWithPath: file_path).resourceValues(forKeys: [.isDirectoryKey]) + .isDirectory + ?? false ? "1" : "0" + if item_type == "0" { + item_type = fileTypeToGopherItem( + fileType: getFileType( + fileExtension: URL(fileURLWithPath: file_path).pathExtension)) + } + let item_host = gopherdata_host + let item_port = gopherdata_port + let item_path = file_path.replacingOccurrences(of: base_dir.path, with: "") + let item_line = + "\(item_type ?? "0")\(item_path)\t\(item_path)\t\(item_host)\t\(item_port)\r\n" + gopherResponse.append(item_line) } - } else { - // Check if directory name matches the query - if file_name.lowercased().contains(query) { - search_results[file_name] = file_path + + if gopherResponse.count == 0 { + gopherResponse.append("iNo results found for the query \(query)\r\n") } - } - } + gopherResponse.append(buildVersionStringResponse()) + return gopherResponse.joined(separator: "") - // Prepare Gopher menu with search results - var gopherResponse: [String] = [] - - for (_, file_path) in search_results { - var item_type = - try? URL(fileURLWithPath: file_path).resourceValues(forKeys: [.isDirectoryKey]).isDirectory - ?? false ? "1" : "0" - if item_type == "0" { - item_type = fileTypeToGopherItem( - fileType: getFileType(fileExtension: URL(fileURLWithPath: file_path).pathExtension)) - } - let item_host = gopherdata_host - let item_port = gopherdata_port - let item_path = file_path.replacingOccurrences(of: base_dir.path, with: "") - let item_line = - "\(item_type ?? "0")\(item_path)\t\(item_path)\t\(item_host)\t\(item_port)\r\n" - gopherResponse.append(item_line) } - if gopherResponse.count == 0 { - gopherResponse.append("iNo results found for the query \(query)\r\n") - } - gopherResponse.append(buildVersionStringResponse()) - return gopherResponse.joined(separator: "") + func sanitizeSelectorPath(path: String) -> String { + // Replace \r\n with empty string + var sanitizedRequest = path.replacingOccurrences(of: "\r\n", with: "") - } + // Basic escape against directory traversal + while sanitizedRequest.contains("..") { + sanitizedRequest = sanitizedRequest.replacingOccurrences(of: "..", with: "") + } - func sanitizeSelectorPath(path: String) -> String { - // Replace \r\n with empty string - var sanitizedRequest = path.replacingOccurrences(of: "\r\n", with: "") + while sanitizedRequest.contains("//") { + sanitizedRequest = sanitizedRequest.replacingOccurrences(of: "//", with: "/") + } - // Basic escape against directory traversal - while sanitizedRequest.contains("..") { - sanitizedRequest = sanitizedRequest.replacingOccurrences(of: "..", with: "") + return sanitizedRequest } - while sanitizedRequest.contains("//") { - sanitizedRequest = sanitizedRequest.replacingOccurrences(of: "//", with: "/") + func channelReadComplete(context: ChannelHandlerContext) { + context.flush() } - return sanitizedRequest - } - - func channelReadComplete(context: ChannelHandlerContext) { - context.flush() - } + func errorCaught(context: ChannelHandlerContext, error: Error) { + print("Error: \(error)") + logger.error("Error: \(error)") + context.close(promise: nil) + } - func errorCaught(context: ChannelHandlerContext, error: Error) { - print("Error: \(error)") - logger.error("Error: \(error)") - context.close(promise: nil) - } + private func processGopherRequest(_ originalRequest: String) -> ResponseType { + var request = originalRequest - private func processGopherRequest(_ originalRequest: String) -> ResponseType { - var request = originalRequest + // Fix for "Gopher" (iOS) client sending an extra \n + if request.hasSuffix("\n\n") { + request = String(request.dropLast()) + } - // Fix for "Gopher" (iOS) client sending an extra \n - if request.hasSuffix("\n\n") { - request = String(request.dropLast()) - } + if request == "\r\n" { // Empty request + return .string(prepareGopherMenu(path: preparePath())) + } - if request == "\r\n" { // Empty request - return .string(prepareGopherMenu(path: preparePath())) - } + // Check if request is an HTTP url + if request.hasPrefix("URL:") { + let url = String(request.dropFirst(4)) + return .string( + "" + ) + } - // Check if request is an HTTP url - if request.hasPrefix("URL:") { - let url = String(request.dropFirst(4)) - return .string( - "" - ) - } + // Again, fix for the iOS client. Might as well make my own client + if request.hasSuffix("\n") { + request = String(request.dropLast()) + } - // Again, fix for the iOS client. Might as well make my own client - if request.hasSuffix("\n") { - request = String(request.dropLast()) - } + if request.contains("\t") { + if enableSearch { + var searchQuery = request.components(separatedBy: "\t")[1] + searchQuery = searchQuery.replacingOccurrences(of: "\r\n", with: "") + return .string(performSearch(query: searchQuery.lowercased())) + } else { + return .string("3Search is disabled on this server.\r\n") + } + } - if request.contains("\t") { - if enableSearch { - var searchQuery = request.components(separatedBy: "\t")[1] - searchQuery = searchQuery.replacingOccurrences(of: "\r\n", with: "") - return .string(performSearch(query: searchQuery.lowercased())) - } else { - return .string("3Search is disabled on this server.\r\n") - } + //TODO: Potential Bug in Gopher implementation? curl gopher://localhost:8080/new_folder/ does not work but curl gopher://localhost:8080//new_folder/ works (tested with gopher://gopher.meulie.net//EFFector/ as well) + return requestHandler(path: preparePath(path: request)) } - - //TODO: Potential Bug in Gopher implementation? curl gopher://localhost:8080/new_folder/ does not work but curl gopher://localhost:8080//new_folder/ works (tested with gopher://gopher.meulie.net//EFFector/ as well) - return requestHandler(path: preparePath(path: request)) - } } diff --git a/Sources/swift-gopher/helpers.swift b/Sources/swift-gopher/helpers.swift index 1415ad4..e9545cc 100644 --- a/Sources/swift-gopher/helpers.swift +++ b/Sources/swift-gopher/helpers.swift @@ -3,14 +3,14 @@ import Foundation let versionString = "generated and served by swift-gopher/1.1.4" // TODO: Handle automatic versioning func buildVersionStringResponse() -> String { - let repeatedString = "i" + String(repeating: "-", count: 72) + "\t\terror.host\t1\r\n" - let versionResponseString = - "i" + String(repeating: " ", count: 72 - versionString.count) + versionString - + "\t\terror.host\t1\r\n" - return "\(repeatedString)\(versionResponseString)" + let repeatedString = "i" + String(repeating: "-", count: 72) + "\t\terror.host\t1\r\n" + let versionResponseString = + "i" + String(repeating: " ", count: 72 - versionString.count) + versionString + + "\t\terror.host\t1\r\n" + return "\(repeatedString)\(versionResponseString)" } enum ResponseType { - case string(String) - case data(Data) + case string(String) + case data(Data) } diff --git a/Sources/swift-gopher/server.swift b/Sources/swift-gopher/server.swift index 2f4a863..0e1c475 100644 --- a/Sources/swift-gopher/server.swift +++ b/Sources/swift-gopher/server.swift @@ -8,87 +8,87 @@ import NIO @main struct swiftGopher: ParsableCommand { - @Option(name: [.short, .long], help: "Hostname used for generating selectors") - var gopherHostName: String = "localhost" - @Option(name: [.short, .long]) - var host: String = "0.0.0.0" - @Option(name: [.short, .long]) - var port: Int = 8080 - @Option(name: [.customShort("d"), .long], help: "Data directory to map") - var gopherDataDir: String = "./example-gopherdata" - @Flag(help: "Disable full-text search feature") - var disableSearch: Bool = false - @Flag(help: "Disable reading gophermap files to override automatic generation") - var disableGophermap: Bool = false + @Option(name: [.short, .long], help: "Hostname used for generating selectors") + var gopherHostName: String = "localhost" + @Option(name: [.short, .long]) + var host: String = "0.0.0.0" + @Option(name: [.short, .long]) + var port: Int = 8080 + @Option(name: [.customShort("d"), .long], help: "Data directory to map") + var gopherDataDir: String = "./example-gopherdata" + @Flag(help: "Disable full-text search feature") + var disableSearch: Bool = false + @Flag(help: "Disable reading gophermap files to override automatic generation") + var disableGophermap: Bool = false - public mutating func run() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup( - numberOfThreads: System.coreCount - ) + public mutating func run() throws { + let eventLoopGroup = MultiThreadedEventLoopGroup( + numberOfThreads: System.coreCount + ) - defer { - try! eventLoopGroup.syncShutdownGracefully() - } + defer { + try! eventLoopGroup.syncShutdownGracefully() + } - let localGopherDataDir = gopherDataDir - let localGopherHostName = gopherHostName - let localPort = port - let localEnableSearch = !disableSearch - let localDisableGophermap = disableGophermap + let localGopherDataDir = gopherDataDir + let localGopherHostName = gopherHostName + let localPort = port + let localEnableSearch = !disableSearch + let localDisableGophermap = disableGophermap - let logger = Logger(label: "com.navanchauhan.gopher.server") + let logger = Logger(label: "com.navanchauhan.gopher.server") - let serverBootstrap = ServerBootstrap( - group: eventLoopGroup - ) - .serverChannelOption( - ChannelOptions.backlog, - value: 256 - ) - .serverChannelOption( - ChannelOptions.socketOption( - .so_reuseaddr - ), - value: 1 - ) - .childChannelInitializer { channel in - channel.pipeline.addHandlers([ - BackPressureHandler(), - GopherHandler( - logger: logger, - gopherdata_dir: localGopherDataDir, - gopherdata_host: localGopherHostName, - gopherdata_port: localPort, - enableSearch: localEnableSearch, - disableGophermap: localDisableGophermap - ), - ]) - } - .childChannelOption( - ChannelOptions.socketOption( - .so_reuseaddr - ), - value: 1 - ) - .childChannelOption( - ChannelOptions.maxMessagesPerRead, - value: 16 - ) - .childChannelOption( - ChannelOptions.recvAllocator, - value: AdaptiveRecvByteBufferAllocator() - ) + let serverBootstrap = ServerBootstrap( + group: eventLoopGroup + ) + .serverChannelOption( + ChannelOptions.backlog, + value: 256 + ) + .serverChannelOption( + ChannelOptions.socketOption( + .so_reuseaddr + ), + value: 1 + ) + .childChannelInitializer { channel in + channel.pipeline.addHandlers([ + BackPressureHandler(), + GopherHandler( + logger: logger, + gopherdata_dir: localGopherDataDir, + gopherdata_host: localGopherHostName, + gopherdata_port: localPort, + enableSearch: localEnableSearch, + disableGophermap: localDisableGophermap + ), + ]) + } + .childChannelOption( + ChannelOptions.socketOption( + .so_reuseaddr + ), + value: 1 + ) + .childChannelOption( + ChannelOptions.maxMessagesPerRead, + value: 16 + ) + .childChannelOption( + ChannelOptions.recvAllocator, + value: AdaptiveRecvByteBufferAllocator() + ) - let defaultHost = host - let defaultPort = port + let defaultHost = host + let defaultPort = port - let channel = try serverBootstrap.bind( - host: defaultHost, - port: defaultPort - ).wait() + let channel = try serverBootstrap.bind( + host: defaultHost, + port: defaultPort + ).wait() - logger.info("Server started and listening on \(channel.localAddress!)") - try channel.closeFuture.wait() - logger.info("Server closed") - } + logger.info("Server started and listening on \(channel.localAddress!)") + try channel.closeFuture.wait() + logger.info("Server closed") + } } diff --git a/Sources/swiftGopherClient/gopherClient.swift b/Sources/swiftGopherClient/gopherClient.swift index 68cca6c..389daf9 100644 --- a/Sources/swiftGopherClient/gopherClient.swift +++ b/Sources/swiftGopherClient/gopherClient.swift @@ -14,111 +14,113 @@ import NIOTransportServices /// /// It utilizes `NIOTSEventLoopGroup` on iOS/macOS (Not sure why you would run this on watchOS/tvOS but it supports that as well) for network operations, falling back to `MultiThreadedEventLoopGroup` otherwise. public class GopherClient { - private let group: EventLoopGroup + private let group: EventLoopGroup - /// Initializes a new instance of `GopherClient`. - /// - /// It automatically chooses the appropriate `EventLoopGroup` based on the running platform. - public init() { - #if os(Linux) - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - #else - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) { - self.group = NIOTSEventLoopGroup() - } else { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } - #endif - } - - deinit { - try? group.syncShutdownGracefully() - } + /// Initializes a new instance of `GopherClient`. + /// + /// It automatically chooses the appropriate `EventLoopGroup` based on the running platform. + public init() { + #if os(Linux) + self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + #else + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) { + self.group = NIOTSEventLoopGroup() + } else { + self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + } + #endif + } - /// Sends a request to a Gopher server. - /// - /// - Parameters: - /// - host: The host address of the Gopher server. - /// - port: The port of the Gopher server. Defaults to 70. - /// - message: The message to be sent to the server. - /// - completion: A closure that handles the result of the request. - /// - /// The method asynchronously establishes a connection, sends the request, and calls the completion handler with the result. - public func sendRequest( - to host: String, port: Int = 70, message: String, - completion: @escaping (Result<[gopherItem], Error>) -> Void - ) { - #if os(Linux) - let bootstrap = ClientBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler( - GopherRequestResponseHandler(message: message, completion: completion)) - } - bootstrap.connect(host: host, port: port).whenComplete { result in - switch result { - case .success(let channel): - channel.closeFuture.whenComplete { _ in - print("Connection closed") - } - case .failure(let error): - completion(.failure(error)) - } - } - #else + deinit { + try? group.syncShutdownGracefully() + } - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) { - let bootstrap = NIOTSConnectionBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler( - GopherRequestResponseHandler(message: message, completion: completion)) - } - bootstrap.connect(host: host, port: port).whenComplete { result in - switch result { - case .success(let channel): - channel.closeFuture.whenComplete { _ in - print("Connection closed") + /// Sends a request to a Gopher server. + /// + /// - Parameters: + /// - host: The host address of the Gopher server. + /// - port: The port of the Gopher server. Defaults to 70. + /// - message: The message to be sent to the server. + /// - completion: A closure that handles the result of the request. + /// + /// The method asynchronously establishes a connection, sends the request, and calls the completion handler with the result. + public func sendRequest( + to host: String, port: Int = 70, message: String, + completion: @escaping (Result<[gopherItem], Error>) -> Void + ) { + #if os(Linux) + let bootstrap = ClientBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler( + GopherRequestResponseHandler(message: message, completion: completion)) + } + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection closed") + } + case .failure(let error): + completion(.failure(error)) + } } - case .failure(let error): - completion(.failure(error)) - } - } - } else { - let bootstrap = ClientBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler( - GopherRequestResponseHandler(message: message, completion: completion)) - } - bootstrap.connect(host: host, port: port).whenComplete { result in - switch result { - case .success(let channel): - channel.closeFuture.whenComplete { _ in - print("Connection closed") + #else + + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) { + let bootstrap = NIOTSConnectionBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler( + GopherRequestResponseHandler(message: message, completion: completion)) + } + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection closed") + } + case .failure(let error): + completion(.failure(error)) + } + } + } else { + let bootstrap = ClientBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler( + GopherRequestResponseHandler(message: message, completion: completion)) + } + bootstrap.connect(host: host, port: port).whenComplete { result in + switch result { + case .success(let channel): + channel.closeFuture.whenComplete { _ in + print("Connection closed") + } + case .failure(let error): + completion(.failure(error)) + } + } } - case .failure(let error): - completion(.failure(error)) - } - } - } - #endif + #endif + + } - } - @available(iOS 13.0, *) @available(macOS 10.15, *) - public func sendRequest(to host: String, port: Int = 70, message: String) async throws -> [gopherItem] { + public func sendRequest(to host: String, port: Int = 70, message: String) async throws + -> [gopherItem] + { return try await withCheckedThrowingContinuation { continuation in let bootstrap = self.createBootstrap(message: message) { result in continuation.resume(with: result) } - + bootstrap.connect(host: host, port: port).whenComplete { result in switch result { case .success(let channel): channel.closeFuture.whenComplete { _ in - print("Connection Closed") + print("Connection Closed") } case .failure(let error): continuation.resume(throwing: error) @@ -126,7 +128,7 @@ public class GopherClient { } } } - + private func createBootstrap( message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void @@ -134,36 +136,36 @@ public class GopherClient { let handler = GopherRequestResponseHandler(message: message, completion: completion) #if os(Linux) - return ClientBootstrap(group: eventLoopGroup) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler(handler) - } - #else - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) { - return NIOTSConnectionBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler(handler) - } - } else { - return ClientBootstrap(group: group) + return ClientBootstrap(group: eventLoopGroup) .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) .channelInitializer { channel in channel.pipeline.addHandler(handler) } - } + #else + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, visionOS 1.0, *) { + return NIOTSConnectionBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler(handler) + } + } else { + return ClientBootstrap(group: group) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler(handler) + } + } #endif } - /// Shuts down the event loop group, releasing any resources. - /// - /// This method is called during deinitialization to ensure clean shutdown of network resources. - private func shutdownEventLoopGroup() { - do { - try group.syncShutdownGracefully() - } catch { - print("Error shutting down event loop group: \(error)") + /// Shuts down the event loop group, releasing any resources. + /// + /// This method is called during deinitialization to ensure clean shutdown of network resources. + private func shutdownEventLoopGroup() { + do { + try group.syncShutdownGracefully() + } catch { + print("Error shutting down event loop group: \(error)") + } } - } } diff --git a/Sources/swiftGopherClient/gopherRequestResponseHandler.swift b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift index 034770d..10da67e 100644 --- a/Sources/swiftGopherClient/gopherRequestResponseHandler.swift +++ b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift @@ -10,104 +10,104 @@ import GopherHelpers import NIO final class GopherRequestResponseHandler: ChannelInboundHandler { - typealias InboundIn = ByteBuffer - typealias OutboundOut = ByteBuffer - - private var accumulatedData: ByteBuffer - private let message: String - private let completion: (Result<[gopherItem], Error>) -> Void - - init(message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { - self.message = message - self.completion = completion - self.accumulatedData = ByteBuffer() - } - - func channelActive(context: ChannelHandlerContext) { - var buffer = context.channel.allocator.buffer(capacity: message.utf8.count) - buffer.writeString(message) - context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil) - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - var buffer = unwrapInboundIn(data) - accumulatedData.writeBuffer(&buffer) - } - - func channelInactive(context: ChannelHandlerContext) { - if let dataCopy = accumulatedData.getSlice(at: 0, length: accumulatedData.readableBytes) { - parseGopherServerResponse( - response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "", - originalBytes: dataCopy) + typealias InboundIn = ByteBuffer + typealias OutboundOut = ByteBuffer + + private var accumulatedData: ByteBuffer + private let message: String + private let completion: (Result<[gopherItem], Error>) -> Void + + init(message: String, completion: @escaping (Result<[gopherItem], Error>) -> Void) { + self.message = message + self.completion = completion + self.accumulatedData = ByteBuffer() + } + + func channelActive(context: ChannelHandlerContext) { + var buffer = context.channel.allocator.buffer(capacity: message.utf8.count) + buffer.writeString(message) + context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil) } - } - - func errorCaught(context: ChannelHandlerContext, error: Error) { - print("Error: ", error) - context.close(promise: nil) - } - - func createGopherItem(rawLine: String, itemType: gopherItemType = .info, rawData: ByteBuffer) - -> gopherItem - { - var item = gopherItem(rawLine: rawLine) - item.parsedItemType = itemType - item.rawData = rawData - - if rawLine.isEmpty { - item.valid = false - } else { - let components = rawLine.components(separatedBy: "\t") - - // Handle cases where rawLine does not have any itemType in the first character - item.message = String(components[0].dropFirst()) - - if components.indices.contains(1) { - item.selector = String(components[1]) - } - - if components.indices.contains(2) { - item.host = String(components[2]) - } - - if components.indices.contains(3) { - item.port = Int(String(components[3])) ?? 70 - } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + var buffer = unwrapInboundIn(data) + accumulatedData.writeBuffer(&buffer) + } + + func channelInactive(context: ChannelHandlerContext) { + if let dataCopy = accumulatedData.getSlice(at: 0, length: accumulatedData.readableBytes) { + parseGopherServerResponse( + response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "", + originalBytes: dataCopy) + } } - return item - } - - func parseGopherServerResponse(response: String, originalBytes: ByteBuffer) { - var gopherServerResponse: [gopherItem] = [] - - print("parsing") - let carriageReturnCount = response.filter({ $0 == "\r" }).count - let newlineCarriageReturnCount = response.filter({ $0 == "\r\n" }).count - print( - "Carriage Returns: \(carriageReturnCount), Newline + Carriage Returns: \(newlineCarriageReturnCount)" - ) - - if newlineCarriageReturnCount == 0 { - for line in response.split(separator: "\n") { - let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") - let item = createGopherItem( - rawLine: String(line), itemType: lineItemType, rawData: originalBytes) - gopherServerResponse.append(item) - - } - } else { - for line in response.split(separator: "\r\n") { - let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") - let item = createGopherItem( - rawLine: String(line), itemType: lineItemType, rawData: originalBytes) - gopherServerResponse.append(item) - - } + func errorCaught(context: ChannelHandlerContext, error: Error) { + print("Error: ", error) + context.close(promise: nil) } - print("done parsing") + func createGopherItem(rawLine: String, itemType: gopherItemType = .info, rawData: ByteBuffer) + -> gopherItem + { + var item = gopherItem(rawLine: rawLine) + item.parsedItemType = itemType + item.rawData = rawData + + if rawLine.isEmpty { + item.valid = false + } else { + let components = rawLine.components(separatedBy: "\t") - completion(.success(gopherServerResponse)) - } + // Handle cases where rawLine does not have any itemType in the first character + item.message = String(components[0].dropFirst()) + + if components.indices.contains(1) { + item.selector = String(components[1]) + } + + if components.indices.contains(2) { + item.host = String(components[2]) + } + + if components.indices.contains(3) { + item.port = Int(String(components[3])) ?? 70 + } + } + + return item + } + + func parseGopherServerResponse(response: String, originalBytes: ByteBuffer) { + var gopherServerResponse: [gopherItem] = [] + + print("parsing") + let carriageReturnCount = response.filter({ $0 == "\r" }).count + let newlineCarriageReturnCount = response.filter({ $0 == "\r\n" }).count + print( + "Carriage Returns: \(carriageReturnCount), Newline + Carriage Returns: \(newlineCarriageReturnCount)" + ) + + if newlineCarriageReturnCount == 0 { + for line in response.split(separator: "\n") { + let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") + let item = createGopherItem( + rawLine: String(line), itemType: lineItemType, rawData: originalBytes) + gopherServerResponse.append(item) + + } + } else { + for line in response.split(separator: "\r\n") { + let lineItemType = getGopherFileType(item: "\(line.first ?? " ")") + let item = createGopherItem( + rawLine: String(line), itemType: lineItemType, rawData: originalBytes) + gopherServerResponse.append(item) + + } + } + + print("done parsing") + + completion(.success(gopherServerResponse)) + } } diff --git a/Tests/swiftGopherClientTests/swiftGopherClientTests.swift b/Tests/swiftGopherClientTests/swiftGopherClientTests.swift index 47d0f24..707ff32 100644 --- a/Tests/swiftGopherClientTests/swiftGopherClientTests.swift +++ b/Tests/swiftGopherClientTests/swiftGopherClientTests.swift @@ -12,30 +12,30 @@ import XCTest final class GopherClientTests: XCTestCase { - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - func testGopherServerConnection() { - let expectation = XCTestExpectation( - description: "Connect and receive response from Gopher server") - let client = GopherClient() - client.sendRequest(to: "gopher.floodgap.com", message: "\r\n") { result in - switch result { - case .success(_): - expectation.fulfill() - case .failure(let error): - print("Error \(error)") - } + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testGopherServerConnection() { + let expectation = XCTestExpectation( + description: "Connect and receive response from Gopher server") + let client = GopherClient() + client.sendRequest(to: "gopher.floodgap.com", message: "\r\n") { result in + switch result { + case .success(_): + expectation.fulfill() + case .failure(let error): + print("Error \(error)") + } + } + + wait(for: [expectation], timeout: 30) } - wait(for: [expectation], timeout: 30) - } - func testGopherClientAsync() async throws { let client = GopherClient() let reply = try await client.sendRequest(to: "gopher.navan.dev", message: "\r\n") -- cgit v1.2.3