SwiftGemini.swift (4526B)
1 import Foundation 2 import Network 3 4 public class GeminiRequestor { 5 static let queue = DispatchQueue(label: "gemini", qos: .default) 6 var conn: NWConnection? 7 8 public enum GeminiRequestorError: Error { 9 case invalidResponseHeader 10 } 11 12 public init() {} 13 14 public func request(_ url: URL) async throws -> GeminiResponse { 15 let response: GeminiResponse = try await withCheckedThrowingContinuation({ continuation in 16 requestWithCompletion(url, completion: { result in 17 switch result { 18 case .success(let response): 19 continuation.resume(returning: response) 20 case .failure(let error): 21 continuation.resume(throwing: error) 22 } 23 }) 24 }) 25 return response 26 } 27 28 public func requestWithCompletion(_ url: URL, completion: @escaping (Result<GeminiResponse, Error>) -> Void) { 29 guard url.user == nil && url.password == nil, 30 url.host != nil && url.absoluteString.data(using: .utf8)!.count <= 1024 else { return } 31 32 let host = NWEndpoint.Host(url.host!) 33 let port = NWEndpoint.Port(integerLiteral: NWEndpoint.Port.IntegerLiteralType(url.port ?? 1965)) 34 let opts = NWProtocolTLS.Options() 35 sec_protocol_options_set_min_tls_protocol_version(opts.securityProtocolOptions, .TLSv12) 36 sec_protocol_options_set_verify_block(opts.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in 37 sec_protocol_metadata_copy_peer_public_key(sec_protocol_metadata) 38 sec_protocol_verify_complete(true) 39 }, GeminiRequestor.queue) 40 41 let request = "\(url.absoluteString)\r\n" 42 let data = request.data(using: .utf8) 43 44 let conn = NWConnection(host: host, port: port, using: NWParameters(tls: opts)) 45 self.conn = conn 46 47 conn.start(queue: GeminiRequestor.queue) 48 conn.send(content: data, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed({ (err) in 49 conn.receiveMessage { (data, ctx, isComplete, err) in 50 conn.cancel() 51 completion(self.parseResponse(url: url, data: data!)) 52 } 53 })) 54 } 55 56 func parseResponse(url: URL, data: Data) -> Result<GeminiResponse, Error> { 57 guard let ix = data.firstIndex(of: 13), 58 data[ix+1] == 10, ix < (1024+3) else { return .failure(GeminiRequestorError.invalidResponseHeader) } 59 var header = String(data: data.prefix(upTo: ix), encoding: .utf8)! 60 let status = StatusCode(rawValue: Int(header.components(separatedBy: " ").first!)!)! 61 let meta: MetaHeader 62 var body: GeminiBody? 63 64 header = String(header.dropFirst(3)) 65 switch status { 66 case .Input, .SensitiveInput: meta = MetaHeader.Prompt(header) 67 case .Success: meta = MetaHeader.MIME(header) 68 case .TemporaryRedirect, .PermanentRedirect: meta = MetaHeader.Redirect(URL(string: header)!) 69 case .TemporaryFailure, .ServerUnavailable, .CGIError, 70 .ProxyError, .PermanentFailure, .NotFound, .Gone, 71 .ProxyRequestRefused, .BadRequest: meta = MetaHeader.FailureInfo(header) 72 case .SlowDown: meta = MetaHeader.Seconds(Int(header)!) 73 case .ClientCertNotAuth, .ClientCertNotValid, 74 .ClientCertRequired: meta = MetaHeader.CertInfo(header) 75 } 76 77 if status == StatusCode.Success { 78 body = parseBody(header: meta, data: data) 79 } 80 81 return .success(GeminiResponse( 82 url: url, status: status, 83 header: meta, body: body 84 )) 85 } 86 87 func parseBody(header: MetaHeader, data: Data) -> GeminiBody { 88 let ix = data.firstIndex(of: 13)! + 2 89 let body = data.suffix(from: ix) 90 var gemtext, text: String? 91 92 if case let .MIME(mime) = header, mime.hasPrefix("text/") || mime.isEmpty { 93 if mime.hasPrefix("text/gemini") || mime.isEmpty { 94 gemtext = String(data: body, encoding: .utf8) 95 } 96 text = String(data: body, encoding: .utf8) 97 } 98 99 return GeminiBody(raw: body, text: text, gemtext: gemtext) 100 } 101 } 102 103 public struct GeminiResponse { 104 public let url: URL 105 public let status: StatusCode 106 public let header: MetaHeader 107 public let body: GeminiBody? 108 } 109 110 public struct GeminiBody { 111 public let raw: Data 112 public let text, gemtext: String? 113 }