2020-07-15 02:39:38 +00:00
|
|
|
//
|
|
|
|
// GeminiDataTask.swift
|
|
|
|
// GeminiProtocol
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 7/14/20.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import Network
|
|
|
|
|
|
|
|
public class GeminiDataTask {
|
2020-07-22 22:25:56 +00:00
|
|
|
public typealias Completion = (Result<GeminiResponse, Error>) -> Void
|
2020-07-15 02:39:38 +00:00
|
|
|
|
2020-07-22 22:25:56 +00:00
|
|
|
private static let queue = DispatchQueue(label: "GeminiDataTask", qos: .default)
|
2020-07-15 02:39:38 +00:00
|
|
|
|
2020-07-22 22:25:56 +00:00
|
|
|
public let request: GeminiRequest
|
|
|
|
private let completion: Completion
|
|
|
|
private var state = State.unstarted
|
|
|
|
private let connection: NWConnection
|
2021-09-29 00:20:08 +00:00
|
|
|
// todo: remove stupid hack when deployment target is >= iOS 15/macOS 12
|
|
|
|
private var _attribution: Any? = nil
|
2021-10-01 23:25:54 +00:00
|
|
|
#if os(iOS)
|
2021-09-29 00:20:08 +00:00
|
|
|
@available(iOS 15.0, *)
|
|
|
|
public var attribution: NWParameters.Attribution {
|
|
|
|
get {
|
|
|
|
_attribution as? NWParameters.Attribution ?? .developer
|
|
|
|
}
|
|
|
|
set {
|
|
|
|
_attribution = newValue
|
|
|
|
}
|
|
|
|
}
|
2021-10-01 23:25:54 +00:00
|
|
|
#endif
|
2020-07-15 02:39:38 +00:00
|
|
|
|
2020-07-22 22:25:56 +00:00
|
|
|
public init(request: GeminiRequest, completion: @escaping Completion) {
|
2020-07-15 02:39:38 +00:00
|
|
|
self.request = request
|
|
|
|
self.completion = completion
|
2020-07-23 13:18:59 +00:00
|
|
|
let port = request.url.port != nil ? UInt16(request.url.port!) : 1965
|
|
|
|
let endpoint: NWEndpoint = .hostPort(host: NWEndpoint.Host(request.url.host!), port: NWEndpoint.Port(rawValue: port)!)
|
|
|
|
self.connection = NWConnection(to: endpoint, using: .gemini)
|
2020-07-22 22:25:56 +00:00
|
|
|
|
|
|
|
self.connection.stateUpdateHandler = { (newState) in
|
|
|
|
switch newState {
|
|
|
|
case .ready:
|
|
|
|
self.sendRequest()
|
|
|
|
case let .failed(error):
|
2020-07-23 13:18:59 +00:00
|
|
|
self.state = .completed
|
2020-07-22 22:25:56 +00:00
|
|
|
self.connection.cancel()
|
|
|
|
self.completion(.failure(.connectionError(error)))
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2020-07-15 02:39:38 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-07-22 22:25:56 +00:00
|
|
|
public convenience init(url: URL, completion: @escaping Completion) throws {
|
2020-07-15 02:39:38 +00:00
|
|
|
self.init(request: try GeminiRequest(url: url), completion: completion)
|
|
|
|
}
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
self.cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
public func resume() {
|
2020-07-22 22:25:56 +00:00
|
|
|
guard state == .unstarted else { return }
|
2021-10-01 23:25:54 +00:00
|
|
|
|
|
|
|
#if os(iOS)
|
|
|
|
if #available(iOS 15.0, *) {
|
|
|
|
connection.parameters.attribution = attribution
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2020-07-23 13:18:59 +00:00
|
|
|
state = .started
|
2020-07-22 22:25:56 +00:00
|
|
|
connection.start(queue: GeminiDataTask.queue)
|
2020-07-15 02:39:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public func cancel() {
|
2020-07-22 22:25:56 +00:00
|
|
|
guard state != .completed else { return }
|
|
|
|
connection.cancel()
|
|
|
|
state = .completed
|
|
|
|
}
|
|
|
|
|
|
|
|
private func sendRequest() {
|
|
|
|
let message = NWProtocolFramer.Message(geminiRequest: request)
|
|
|
|
let context = NWConnection.ContentContext(identifier: "GeminiRequest", metadata: [message])
|
2020-07-23 13:18:59 +00:00
|
|
|
connection.send(content: nil, contentContext: context, isComplete: true, completion: .contentProcessed({ (error) in
|
|
|
|
if let error = error {
|
|
|
|
self.state = .completed
|
|
|
|
self.connection.cancel()
|
|
|
|
self.completion(.failure(.connectionError(error)))
|
|
|
|
}
|
|
|
|
}))
|
2020-07-22 22:25:56 +00:00
|
|
|
receive()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func receive() {
|
|
|
|
connection.receiveMessage { (data, context, isComplete, error) in
|
|
|
|
if let error = error {
|
|
|
|
self.completion(.failure(.connectionError(error)))
|
|
|
|
} else if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message,
|
|
|
|
let header = message.geminiResponseHeader {
|
2021-06-16 02:24:16 +00:00
|
|
|
guard isComplete else { fatalError() }
|
2020-07-22 22:25:56 +00:00
|
|
|
let response = GeminiResponse(header: header, body: data)
|
|
|
|
self.completion(.success(response))
|
|
|
|
}
|
|
|
|
|
|
|
|
self.connection.cancel()
|
|
|
|
self.state = .completed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension GeminiDataTask {
|
|
|
|
enum State {
|
|
|
|
case unstarted, started, completed
|
2020-07-15 02:39:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public extension GeminiDataTask {
|
|
|
|
enum Error: Swift.Error {
|
|
|
|
case noData
|
|
|
|
case connectionError(NWError)
|
|
|
|
}
|
|
|
|
}
|