diff options
author | Navan Chauhan <navanchauhan@gmail.com> | 2023-12-04 23:22:34 -0700 |
---|---|---|
committer | Navan Chauhan <navanchauhan@gmail.com> | 2023-12-04 23:22:34 -0700 |
commit | 3ec3af34e4099e933bb39731036dc50bd2ce14d6 (patch) | |
tree | 94dc0ab8856a81d3f698994d38fd21647afa5bf1 | |
parent | 6f57c8e5ad47ca3046213e11134e4f4b145e5bb8 (diff) |
refactor
-rw-r--r-- | Sources/Helpers.swift | 7 | ||||
-rw-r--r-- | Sources/fileTypes.swift | 133 | ||||
-rw-r--r-- | Sources/gopherHandler.swift (renamed from Sources/main.swift) | 337 | ||||
-rw-r--r-- | Sources/server.swift | 88 |
4 files changed, 312 insertions, 253 deletions
diff --git a/Sources/Helpers.swift b/Sources/Helpers.swift index 26bd024..fecc4ab 100644 --- a/Sources/Helpers.swift +++ b/Sources/Helpers.swift @@ -1,8 +1 @@ -// -// File.swift -// -// -// Created by Navan Chauhan on 12/3/23. -// - import Foundation diff --git a/Sources/fileTypes.swift b/Sources/fileTypes.swift new file mode 100644 index 0000000..221b7b2 --- /dev/null +++ b/Sources/fileTypes.swift @@ -0,0 +1,133 @@ +// +// fileTypes.swift +// +// +// Created by Navan Chauhan on 12/3/23. +// + +import Foundation + +enum ResponseType { + case string(String) + case data(Data) +} + +enum gopherFileType { + case text + case directory + case nameserver + case error + case binhex + case bindos + case uuencoded + case indexSearch + case telnet + case binary + case redundantServer + case tn3270Session + case gif + case image + case bitmap + case movie + case sound + case doc + case html + case message + case png + case rtf + case wavfile + case pdf + case xml +} + +func getFileType(fileExtension: String) -> gopherFileType { + switch fileExtension { + case "txt": + return .text + case "md": + return .text + case "html": + return .html + case "pdf": + return .pdf + case "png": + return .png + case "gif": + return .gif + case "jpg": + return .image + case "jpeg": + return .image + case "mp3": + return .sound + case "wav": + return .wavfile + case "mp4": + return .movie + case "mov": + return .movie + case "avi": + return .movie + case "rtf": + return .rtf + case "xml": + return .xml + default: + return .binary + } +} + +func fileTypeToGopherItem(fileType: gopherFileType) -> 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 .indexSearch: + return "7" + case .telnet: + return "8" + case .binary: + return "9" + case .redundantServer: + 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 .message: + return "i" + case .png: + return "p" + case .rtf: + return "t" + case .wavfile: + return "w" + case .pdf: + return "P" + case .xml: + return "x" + } +} diff --git a/Sources/main.swift b/Sources/gopherHandler.swift index 7776780..2d369bf 100644 --- a/Sources/main.swift +++ b/Sources/gopherHandler.swift @@ -1,211 +1,8 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book - import ArgumentParser import Foundation import Logging import NIO -@main -struct swiftGopher: ParsableCommand { - @Option var gopherHostName: String = "localhost" - @Option var port: Int = 8080 - @Option var gopherDataDir: String = "./example-gopherdata" - @Option var host: String = "0.0.0.0" - - public mutating func run() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup( - numberOfThreads: System.coreCount - ) - - defer { - try! eventLoopGroup.syncShutdownGracefully() - } - - let localGopherDataDir = gopherDataDir - let localGopherHostName = gopherHostName - let localPort = port - - 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 - ), - ]) - } - .childChannelOption( - ChannelOptions.socketOption( - .so_reuseaddr - ), - value: 1 - ) - .childChannelOption( - ChannelOptions.maxMessagesPerRead, - value: 16 - ) - .childChannelOption( - ChannelOptions.recvAllocator, - value: AdaptiveRecvByteBufferAllocator() - ) - - let defaultHost = host - let defaultPort = port - - 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") - } -} - -enum ResponseType { - case string(String) - case data(Data) -} - -enum gopherFileType { - case text - case directory - case nameserver - case error - case binhex - case bindos - case uuencoded - case indexSearch - case telnet - case binary - case redundantServer - case tn3270Session - case gif - case image - case bitmap - case movie - case sound - case doc - case html - case message - case png - case rtf - case wavfile - case pdf - case xml -} - -func getFileType(fileExtension: String) -> gopherFileType { - switch fileExtension { - case "txt": - return .text - case "md": - return .text - case "html": - return .html - case "pdf": - return .pdf - case "png": - return .png - case "gif": - return .gif - case "jpg": - return .image - case "jpeg": - return .image - case "mp3": - return .sound - case "wav": - return .wavfile - case "mp4": - return .movie - case "mov": - return .movie - case "avi": - return .movie - case "rtf": - return .rtf - case "xml": - return .xml - default: - return .binary - } -} - -func fileTypeToGopherItem(fileType: gopherFileType) -> 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 .indexSearch: - return "7" - case .telnet: - return "8" - case .binary: - return "9" - case .redundantServer: - 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 .message: - return "i" - case .png: - return "p" - case .rtf: - return "t" - case .wavfile: - return "w" - case .pdf: - return "P" - case .xml: - return "x" - } -} - final class GopherHandler: ChannelInboundHandler { typealias InboundIn = ByteBuffer typealias OutboundOut = ByteBuffer @@ -214,16 +11,21 @@ final class GopherHandler: ChannelInboundHandler { 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 + 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 } func channelRead(context: ChannelHandlerContext, data: NIOAny) { @@ -234,12 +36,11 @@ final class GopherHandler: ChannelInboundHandler { } if let remoteAddress = context.remoteAddress { - logger.info("Received request from \(remoteAddress) for \(requestString)") + 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") } - let response = processGopherRequest(requestString) var buffer: ByteBuffer @@ -257,6 +58,8 @@ final class GopherHandler: ChannelInboundHandler { } 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 @@ -303,20 +106,66 @@ final class GopherHandler: ChannelInboundHandler { if base_dir.path.hasSuffix("/") && sanitizedPath.hasPrefix("/") { sanitizedPath = String(sanitizedPath.dropFirst()) } + + // 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 generateGopherMap(path: URL) -> [String] { + var items: [String] = [] + + var basePath = URL(fileURLWithPath: gopherdata_dir).path + if basePath.hasSuffix("/") { + basePath = String(basePath.dropLast()) + } + + 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 + } + func prepareGopherMenu(path: URL = URL(string: "/")!) -> String { var gopherResponse: [String] = [] let fm = FileManager.default - let absolute_base_path = URL(fileURLWithPath: gopherdata_dir) - let relative_path = path.path.replacingOccurrences(of: absolute_base_path.path, with: "") do { let gophermap_path = path.appendingPathComponent("gophermap") - if fm.fileExists(atPath: gophermap_path.path) { + 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 { @@ -367,24 +216,8 @@ final class GopherHandler: ChannelInboundHandler { } } } else { - let items = try fm.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) - - for item in items { - let item_name = item.lastPathComponent - var item_type = "" - if try item.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false { - item_type = "1" - } else { - let fileType = getFileType(fileExtension: item.pathExtension) - item_type = fileTypeToGopherItem(fileType: fileType) - } - let item_path = "\(relative_path)\(relative_path.hasSuffix("/") ? "" : "/")\(item_name)" - .replacingOccurrences(of: "//", with: "/") - let item_host = gopherdata_host - let item_port = gopherdata_port - let item_line = "\(item_type)\(item_name)\t\(item_path)\t\(item_host)\t\(item_port)\r\n" - gopherResponse.append(item_line) - } + print("No gophermap found for \(path.path)") + gopherResponse = generateGopherMap(path: path) } } catch { logger.error("Error reading directory: \(path.path)") @@ -392,12 +225,16 @@ final class GopherHandler: ChannelInboundHandler { } // Append Search - let search_line = "7Search Server\t/swiftSearch\t\(gopherdata_host)\t\(gopherdata_port)\r\n" - gopherResponse.append(search_line) + if enableSearch { + let search_line = "7Search Server\t/search\t\(gopherdata_host)\t\(gopherdata_port)\r\n" + gopherResponse.append(search_line) + } + return gopherResponse.joined(separator: "") } + // TODO: Refactor func performSearch(query: String) -> String { // Really basic search implementation @@ -480,26 +317,34 @@ final class GopherHandler: ChannelInboundHandler { context.close(promise: nil) } - private func processGopherRequest(_ request: String) -> ResponseType { - // Implement your logic to handle the Gopher request and return a response - // For example, you can retrieve a document based on the request string - // Return the document or an appropriate response as a string + 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()) + } - // Example response (you should replace this with actual logic) - if request == "\r\n" { + if request == "\r\n" { // Empty request return .string(prepareGopherMenu(path: preparePath())) - } else if !request.contains("\t") { - //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)) - } else if request.contains("\t") { - var searchQuery = request.components(separatedBy: "\t")[1] - searchQuery = searchQuery.replacingOccurrences(of: "\r\n", with: "") - return .string(performSearch(query: searchQuery.lowercased())) } - return - .string( - "Hello from Gopher Server! You requested: \(request), but this request could not be processed.\r\n" - ) + // 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") + } + } + + //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/server.swift b/Sources/server.swift new file mode 100644 index 0000000..44520bb --- /dev/null +++ b/Sources/server.swift @@ -0,0 +1,88 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +import ArgumentParser +import Foundation +import Logging +import NIO + +@main +struct swiftGopher: ParsableCommand { + @Option var gopherHostName: String = "localhost" + @Option var port: Int = 8080 + @Option var gopherDataDir: String = "./example-gopherdata" + @Option var host: String = "0.0.0.0" + @Flag var disableSearch: Bool = false + @Flag var disableGophermap: Bool = false + + public mutating func run() throws { + let eventLoopGroup = MultiThreadedEventLoopGroup( + numberOfThreads: System.coreCount + ) + + defer { + try! eventLoopGroup.syncShutdownGracefully() + } + + let localGopherDataDir = gopherDataDir + let localGopherHostName = gopherHostName + let localPort = port + let localEnableSearch = !disableSearch + let localDisableGophermap = disableGophermap + + 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 defaultHost = host + let defaultPort = port + + 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") + } +} |