swiftgemini

[ACTIVE] gemini protocol in swift
git clone git://git.figbert.com/swiftgemini.git
Log | Files | Refs | README

commit 968407e61d0a0f19954f9ba7851c437bab4d47d2
parent c89452c9ee339f83122681c7d6b64f52c9892b16
Author: FIGBERT <figbert@figbert.com>
Date:   Sun,  4 Jul 2021 18:44:06 +0300

Add rough networking implementation

It's untested and I wrote it on a plane flight after being awake for
over twenty-four hours straight. I'll probably need to tweak some stuff
before it hits production...

Diffstat:
ASources/SwiftGemini/MetaHeader.swift | 11+++++++++++
ASources/SwiftGemini/StatusCode.swift | 23+++++++++++++++++++++++
MSources/SwiftGemini/SwiftGemini.swift | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 129 insertions(+), 2 deletions(-)

diff --git a/Sources/SwiftGemini/MetaHeader.swift b/Sources/SwiftGemini/MetaHeader.swift @@ -0,0 +1,11 @@ +import Foundation + +enum MetaHeader { + case GemenonDefault + case Prompt(String) + case MIME(String) + case Redirect(URL) + case FailureInfo(String) + case Seconds(Int) + case CertInfo(String) +} diff --git a/Sources/SwiftGemini/StatusCode.swift b/Sources/SwiftGemini/StatusCode.swift @@ -0,0 +1,23 @@ +import Foundation + +enum StatusCode: Int { + case GemenonDefault = 0 + case Input = 10 + case SensitiveInput = 11 + case Success = 20 + case TemporaryRedirect = 30 + case PermanentRedirect = 31 + case TemporaryFailure = 40 + case ServerUnavailable = 41 + case CGIError = 42 + case ProxyError = 43 + case SlowDown = 44 + case PermanentFailure = 50 + case NotFound = 51 + case Gone = 52 + case ProxyRequestRefused = 53 + case BadRequest = 59 + case ClientCertRequired = 60 + case ClientCertNotAuth = 61 + case ClientCertNotValid = 62 +} diff --git a/Sources/SwiftGemini/SwiftGemini.swift b/Sources/SwiftGemini/SwiftGemini.swift @@ -1,3 +1,96 @@ -struct SwiftGemini { - var text = "Hello, World!" +import Foundation +import Network + +struct GeminiRequest { + public let url: URL + public let status: StatusCode + public let meta: MetaHeader + public let raw: Data + public let text: String + + init?(_ site: String) throws { + guard var components = URLComponents(url: URL(string: site)!, resolvingAgainstBaseURL: true) else { return nil } + guard components.scheme == "gemini" else { return nil } + guard components.user!.isEmpty && components.password!.isEmpty else { return nil } + if components.port == nil { components.port = 1965 } + let url = components.url! + if url.absoluteString.data(using: .utf8)!.count > 1024 { return nil } + + let queue = DispatchQueue(label: "gemenon") + let endpoint = NWEndpoint.Host.init(url.host!) + + let options = NWProtocolTLS.Options() + sec_protocol_options_set_verify_block(options.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in + sec_protocol_verify_complete(true) + }, queue) + + let connection = NWConnection( + host: endpoint, + port: .init(rawValue: UInt16(url.port!))!, + using: NWParameters(tls: options) + ) + + var status = StatusCode.GemenonDefault + var meta = MetaHeader.GemenonDefault + var raw = Data() + var text = String() + + // Get status code + connection.receive(minimumIncompleteLength: 2, maximumLength: 2) { (data, contentContext, isComplete, error) in + guard data != nil else { return } + status = StatusCode(rawValue: Int(String(data: data!, encoding: .utf8)!)!)! + } + // Get meta header + connection.receive(minimumIncompleteLength: 0, maximumLength: 1024 + 3) { (data, contentContext, isComplete, error) in + guard data != nil else { return } + let header = String(data: data!, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + switch status { + case .Input, .SensitiveInput: + meta = .Prompt(header) + case .Success: + meta = .MIME(header) + case .TemporaryRedirect, .PermanentRedirect: + meta = .Redirect(URL(string: header)!) + case .TemporaryFailure, .ServerUnavailable, .CGIError, + .ProxyError, .PermanentFailure, .NotFound, + .Gone, .ProxyRequestRefused, .BadRequest: + meta = .FailureInfo(header) + case .SlowDown: + meta = .Seconds(Int(header)!) + case .ClientCertRequired, .ClientCertNotAuth, .ClientCertNotValid: + meta = .CertInfo(header) + case .GemenonDefault: + return + } + } + if status == .Success { + // Get content + connection.receive(minimumIncompleteLength: Int.min, maximumLength: Int.max) { (data, contentContext, isComplete, error) in + guard data != nil else { return } + raw = data! + text = String(data: data!, encoding: .utf8) ?? "" + + if isComplete { + connection.cancel() + } else if let error = error { + print("setupReceive: error \(error.localizedDescription)") + } + } + } + + connection.start(queue: queue) + connection.send(content: "\(url.absoluteString)\r\n".data(using: .utf8)!, completion: .contentProcessed( { error in + print("data sent") + if let error = error { + print("got error: \(error)") + return + } + })) + + self.url = url + self.status = status + self.meta = meta + self.raw = raw + self.text = text + } }