commit bc107305ea1fdc81f5b512297a909c162a0b911f
parent acca20f891987f3a95e595bd9afedcb42c774be1
Author: FIGBERT <figbert@figbert.com>
Date: Thu, 21 Jul 2022 19:12:47 -0700
Add primitive but functional implementation
Diffstat:
4 files changed, 79 insertions(+), 89 deletions(-)
diff --git a/Sources/SwiftGemini/MetaHeader.swift b/Sources/SwiftGemini/MetaHeader.swift
@@ -1,7 +1,6 @@
import Foundation
enum MetaHeader {
- case GemenonDefault
case Prompt(String)
case MIME(String)
case Redirect(URL)
diff --git a/Sources/SwiftGemini/StatusCode.swift b/Sources/SwiftGemini/StatusCode.swift
@@ -1,7 +1,6 @@
import Foundation
enum StatusCode: Int {
- case GemenonDefault = 0
case Input = 10
case SensitiveInput = 11
case Success = 20
diff --git a/Sources/SwiftGemini/SwiftGemini.swift b/Sources/SwiftGemini/SwiftGemini.swift
@@ -1,96 +1,87 @@
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
+class GeminiRequest {
+ static let queue = DispatchQueue(label: "gemini", qos: .default)
+ fileprivate var conn: NWConnection?
+ fileprivate var response: Data?
+
+ let url: URL
+ var status: StatusCode?
+ var meta: MetaHeader?
+ var body: Data?
+ var gemtext: String?
+
+ init?(_ url: URL) throws {
+ guard url.scheme == "gemini",
+ url.user == nil && url.password == nil,
+ url.absoluteString.data(using: .utf8)!.count <= 1024 else { return nil }
+ self.url = url
+ request()
+ }
+
+ fileprivate func request() {
+ let host = NWEndpoint.Host(self.url.host!)
+ let port = NWEndpoint.Port(integerLiteral: NWEndpoint.Port.IntegerLiteralType(self.url.port ?? 1965))
+ let opts = NWProtocolTLS.Options()
+ sec_protocol_options_set_tls_min_version(opts.securityProtocolOptions, .tlsProtocol12)
+ sec_protocol_options_set_verify_block(opts.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
+ sec_protocol_metadata_copy_peer_public_key(sec_protocol_metadata)
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
+ }, GeminiRequest.queue)
+
+ let request = "\(self.url.absoluteString)\r\n"
+ let data = request.data(using: .utf8)
+
+ let conn = NWConnection(host: host, port: port, using: NWParameters(tls: opts))
+ self.conn = conn
+
+ conn.start(queue: GeminiRequest.queue)
+ conn.send(content: data, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed({ (err) in
+ conn.receiveMessage { (data, ctx, isComplete, err) in
+ conn.cancel()
+ self.parseResponse(data: data!)
}
}))
-
- self.url = url
+ }
+
+ fileprivate func parseResponse(data: Data) {
+ guard let ix = data.firstIndex(of: 13),
+ data[ix+1] == 10, ix < (1024+3) else { return }
+ var header = String(data: data.prefix(upTo: ix), encoding: .utf8)!
+ let status = StatusCode(rawValue: Int(header.components(separatedBy: " ").first!)!)!
+ let meta: MetaHeader
+
+ header = String(header.dropFirst(3))
+ switch status {
+ case .Input, .SensitiveInput: meta = MetaHeader.Prompt(header)
+ case .Success: meta = MetaHeader.MIME(header)
+ case .TemporaryRedirect, .PermanentRedirect: meta = MetaHeader.Redirect(URL(string: header)!)
+ case .TemporaryFailure, .ServerUnavailable, .CGIError,
+ .ProxyError, .PermanentFailure, .NotFound, .Gone,
+ .ProxyRequestRefused, .BadRequest: meta = MetaHeader.FailureInfo(header)
+ case .SlowDown: meta = MetaHeader.Seconds(Int(header)!)
+ case .ClientCertNotAuth, .ClientCertNotValid,
+ .ClientCertRequired: meta = MetaHeader.CertInfo(header)
+ }
+
self.status = status
self.meta = meta
- self.raw = raw
- self.text = text
+ self.response = data
+
+ if self.status == StatusCode.Success {
+ self.parseBody()
+ }
+ }
+
+ fileprivate func parseBody() {
+ let ix = self.response!.firstIndex(of: 13)! + 2
+ let body = self.response!.suffix(from: ix)
+
+ if case let .MIME(mime) = self.meta, mime.hasPrefix("text/gemini") {
+ self.gemtext = String(data: body, encoding: .utf8)
+ } else {
+ self.body = body
+ }
}
}
diff --git a/Tests/SwiftGeminiTests/SwiftGeminiTests.swift b/Tests/SwiftGeminiTests/SwiftGeminiTests.swift
@@ -6,7 +6,8 @@ final class SwiftGeminiTests: XCTestCase {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
- XCTAssertEqual(SwiftGemini().text, "Hello, World!")
+ let _ = try! GeminiRequest(URL(string: "gemini://figbert.com")!)
+ sleep(1)
}
static var allTests = [