//
//  RootCAChallengeResolver.swift
//  RootCAChallengeResolver
//
//  Created by Paul Bates on 1/14/20.
//  Copyright © 2020 Paul Bates. All rights reserved.
//

import Foundation

/// Supports use of self-signed root certificates for TLS, where it may be required to resolve the cert challenge dynamically (i.e.
/// an application does not want to support insecure communcation for anything)
public class RootCAChallengeResolver: NSObject, URLSessionDelegate {
    /// Host address for secured endpoint
    private let host: String

    /// Certifcate to ensure trust with
    private let cert: SecCertificate?

    // MARK: Init

    /// Initializes the resolver for a given host and (optional) cert
    /// Can return `nil` is the cert data provided is not a valid certificate
    ///
    /// - Parameters:
    ///     - host: The host to initialize for
    ///     - cert: Optional certificate data the trust challenge will be verified against
    public init?(host: String, cert: Data? = nil) {
        precondition(!host.isEmpty, "A valid host is required")
        precondition(!(cert?.isEmpty ?? false), "Trust cert must contain data")

        self.host = host
        if let certData = cert, certData.count > 0 {
            guard let certificate = SecCertificateCreateWithData(nil, certData as CFData) else {
                return nil
            }

            self.cert = certificate
        } else {
            self.cert = nil
        }
    }

    // MARK: URLSessionDelegate

    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
                           completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
    {
        let protectionSpace = challenge.protectionSpace
        if // Only support trust challenges
            protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
            // Ensure there is something to trust
            let trust = protectionSpace.serverTrust,
            // Ensure hosts are equivalent
            protectionSpace.host == host
        {
            // If there is no cert to verify, we can trust the connection
            guard let cert = cert else {
                completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
                return
            }

            // Verify the trust against the cert
            SecTrustSetAnchorCertificates(trust, [cert] as CFArray)
            SecTrustSetAnchorCertificatesOnly(trust, false)
            if SecTrustEvaluateWithError(trust, nil) {
                completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
            }
        }

        // Default handling
        completionHandler(.performDefaultHandling, nil)
    }
}
