swiftgemini

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

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 }