aboutsummaryrefslogtreecommitdiff
path: root/Sources
diff options
context:
space:
mode:
Diffstat (limited to 'Sources')
-rw-r--r--Sources/GopherHelpers/GopherHelpers.swift390
-rw-r--r--Sources/swift-gopher/gopherHandler.swift704
-rw-r--r--Sources/swift-gopher/helpers.swift14
-rw-r--r--Sources/swift-gopher/server.swift150
-rw-r--r--Sources/swiftGopherClient/gopherClient.swift230
-rw-r--r--Sources/swiftGopherClient/gopherRequestResponseHandler.swift188
6 files changed, 843 insertions, 833 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: "<GopherSequence>").replacingOccurrences(of: "\n", with: "<Linebreak>"))'"
- )
- } 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: "<GopherSequence>").replacingOccurrences(of: "\n", with: "<Linebreak>"))'"
+ )
+ } 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:;<dhprsPXi".contains(line.prefix(1)) && line.count > 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:;<dhprsPXi".contains(line.prefix(1)) && line.count > 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(
+ "<!DOCTYPE html><html><head><meta http-equiv=\"refresh\" content=\"0; url=\(url)\" /></head><body></body></html>"
+ )
+ }
- // Check if request is an HTTP url
- if request.hasPrefix("URL:") {
- let url = String(request.dropFirst(4))
- return .string(
- "<!DOCTYPE html><html><head><meta http-equiv=\"refresh\" content=\"0; url=\(url)\" /></head><body></body></html>"
- )
- }
+ // 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))
+ }
}