aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Package.swift4
-rw-r--r--Sources/swiftGopherClient/gopherClient.swift48
-rw-r--r--Sources/swiftGopherClient/gopherRequestResponseHandler.swift111
-rw-r--r--Sources/swiftGopherClient/gopherTypes.swift122
-rw-r--r--Tests/swiftGopherClientTests/swiftGopherClientTests.swift37
5 files changed, 322 insertions, 0 deletions
diff --git a/Package.swift b/Package.swift
index 4fdc0ea..9cbb057 100644
--- a/Package.swift
+++ b/Package.swift
@@ -33,6 +33,10 @@ let package = Package(
dependencies: [
.product(name: "NIO", package: "swift-nio")
]
+ ),
+ .testTarget(
+ name: "swiftGopherClientTests",
+ dependencies: ["swiftGopherClient"]
)
]
)
diff --git a/Sources/swiftGopherClient/gopherClient.swift b/Sources/swiftGopherClient/gopherClient.swift
new file mode 100644
index 0000000..6b76d94
--- /dev/null
+++ b/Sources/swiftGopherClient/gopherClient.swift
@@ -0,0 +1,48 @@
+//
+// gopherClient.swift
+//
+//
+// Created by Navan Chauhan on 12/12/23.
+//
+
+import Foundation
+import NIO
+
+public class GopherClient {
+ private let group: EventLoopGroup
+
+ public init() {
+ self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
+ }
+
+ deinit {
+ try? group.syncShutdownGracefully()
+ }
+
+ public func sendRequest(to host: String, port: Int = 70, message: String, completion: @escaping (Result<String, Error>) -> Void) {
+ 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))
+ }
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..0cd040a
--- /dev/null
+++ b/Sources/swiftGopherClient/gopherRequestResponseHandler.swift
@@ -0,0 +1,111 @@
+//
+// gopherRequestResponseHandler.swift
+//
+//
+// Created by Navan Chauhan on 12/12/23.
+//
+
+import Foundation
+import NIO
+
+final class GopherRequestResponseHandler: ChannelInboundHandler {
+ typealias InboundIn = ByteBuffer
+ typealias OutboundOut = ByteBuffer
+
+ private var accumulatedData: ByteBuffer
+ private let message: String
+ private let completion: (Result<String, Error>) -> Void
+
+ init(message: String, completion: @escaping (Result<String, 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)
+ if let receivedString = buffer.getString(at: 0, length: buffer.readableBytes) {
+ print("Received from server: \(receivedString)")
+ }
+ //completion(.success(receivedString))
+ //context.close(promise: nil)
+ }
+
+ func channelInactive(context: ChannelHandlerContext) {
+ // Parse GopherServerResponse
+ parseGopherServerResponse(response: accumulatedData.readString(length: accumulatedData.readableBytes) ?? "")
+ //completion(.success(accumulatedData.readString(length: accumulatedData.readableBytes) ?? ""))
+ }
+
+ func errorCaught(context: ChannelHandlerContext, error: Error) {
+ print("Error: ", error)
+ context.close(promise: nil)
+ }
+
+ func createGopherItem(rawLine: String, itemType: gopherItemType = .info) -> gopherItem {
+ var item = gopherItem(rawLine: rawLine)
+ item.parsedItemType = itemType
+
+ 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
+ }
+ }
+
+ return item
+ }
+
+ func parseGopherServerResponse(response: String) {
+ 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)")
+
+ for line in response.split(separator: "\r\n") {
+ let lineItemType = getGopherFileType(item: "\(line.first ?? " ")")
+ let item = createGopherItem(rawLine: String(line), itemType: lineItemType)
+ print(item.message)
+ gopherServerResponse.append(item)
+
+ }
+
+ print("done parsing")
+
+ completion(.success(response))
+ }
+}
+
+struct gopherItem {
+ var rawLine: String
+ var message: String = ""
+ var parsedItemType: gopherItemType = .info
+ var host: String = "error.host"
+ var port: Int = 1
+ var selector: String = ""
+ var valid: Bool = true
+}
+
diff --git a/Sources/swiftGopherClient/gopherTypes.swift b/Sources/swiftGopherClient/gopherTypes.swift
new file mode 100644
index 0000000..2f1c99f
--- /dev/null
+++ b/Sources/swiftGopherClient/gopherTypes.swift
@@ -0,0 +1,122 @@
+//
+// gopherTypes.swift
+//
+//
+// Created by Navan Chauhan on 12/12/23.
+//
+
+import Foundation
+
+/*
+
+ From Wikipedia
+
+ Canonical types
+ 0 Text file
+ 1 Gopher submenu
+ 2 CCSO Nameserver
+ 3 Error code returned by a Gopher server to indicate failure
+ 4 BinHex-encoded file (primarily for Macintosh computers)
+ 5 DOS file
+ 6 uuencoded file
+ 7 Gopher full-text search
+ 8 Telnet
+ 9 Binary file
+ + Mirror or alternate server (for load balancing or in case of primary server downtime)
+ g GIF file
+ I Image file
+ T Telnet 3270
+ gopher+ types
+ : Bitmap image
+ ; Movie file
+ < Sound file
+ Non-canonical types
+ d Doc. Seen used alongside PDF's and .DOC's
+ h HTML file
+ i Informational message, widely used.[25]
+ p image file "(especially the png format)"
+ r document rtf file "rich text Format")
+ s Sound file (especially the WAV format)
+ P document pdf file "Portable Document Format")
+ X document xml file "eXtensive Markup Language" )
+ */
+
+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
+}
+
+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
+ }
+}
diff --git a/Tests/swiftGopherClientTests/swiftGopherClientTests.swift b/Tests/swiftGopherClientTests/swiftGopherClientTests.swift
new file mode 100644
index 0000000..a687a3e
--- /dev/null
+++ b/Tests/swiftGopherClientTests/swiftGopherClientTests.swift
@@ -0,0 +1,37 @@
+//
+// swiftGopherClientTests.swift
+//
+//
+// Created by Navan Chauhan on 12/12/23.
+//
+
+import XCTest
+import NIO
+
+@testable import swiftGopherClient
+
+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)")
+ }
+ }
+
+ wait(for: [expectation], timeout: 30)
+ }
+}