URLSessionでSSLエラーが起きた時にSafariと同様にアクセスできるようにする

# 要約
`SecTrust` を使って、SSLエラーへの対処を行います。

# 概要
Safariでブラウジングしていると、サイトによって「接続はプライベートはありません」と表示される場合があります。

![接続はプライベートはありません 2020-08-31 22.58.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/33a4bbc5-b7e9-b2f5-f726-97b5401bf42b.png)
これに対し、「詳細を表示」→「このWebサイトを閲覧」とすることにより、**ユーザーの責任で**サイトを閲覧できます。
これをURLSessionでも行いたいと思います。

## Info.plistの設定
Info.plistに`App Transport Security Settings`の設定をする必要があります。
`Exception Domains` → 目的のドメイン → `NSExceptionAllowsInsecureHTTPLoads` をYESに設定します。
今回の例では、[badssl.com](https://badssl.com)のサブドメインでもテストしたいので、`NSIncludesSubdomains`もYESにしておきます。

```
	<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSExceptionDomains</key>
		<dict>
			<key>badssl.com</key>
			<dict>
				<key>NSExceptionAllowsInsecureHTTPLoads</key>
				<true/>
				<key>NSIncludesSubdomains</key>
				<true/>
			</dict>
		</dict>
	</dict>
```


## URLSessionへの実装
まず、URLSessionはdelegateをつけられるように初期化します。
そして、`URLSessionTaskDelegate` など、目的のデリゲートを設定して、`urlSession(_:didReceive:completionHandler:)`などでハンドリングしてあげます。

### コード


```swift
class ViewController: UIViewController {
    lazy var session: URLSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)

    override func viewDidLoad() {
        super.viewDidLoad()
        let task = session.dataTask(with: URL(string: "https://revoked.badssl.com")!)
        task.resume()
    }
}

extension ViewController: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print(#function,#line,"\(String(describing: error))")
    }
    
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        //サーバー信頼チェック以外はデフォルトの挙動を行う
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
            let serverTrust = challenge.protectionSpace.serverTrust else {
                completionHandler(.performDefaultHandling, nil)
                return
        }

        // Trustを確認する
        SecTrustEvaluateAsyncWithError(serverTrust, DispatchQueue.global()) { (secTrust, trusted, error) in
            if trusted {
                completionHandler(.performDefaultHandling, nil) // 信頼できる場合デフォルトの挙動を行う
            } else {
                // エラーメッセージを表示
                let title:String
                if let error = error {
                    title = error.localizedDescription
                } else {
                    title = "接続はプライベートではありません"
                }
                DispatchQueue.main.async {
                    //アラートを表示して、ユーザーに選択を促す
                    let alert = UIAlertController(title: title,
                                                  message: "それでもサーバに接続しますか?",
                                                  preferredStyle: .alert)
                    alert.addAction(UIAlertAction(title: "接続する", style: .destructive, handler: { (alertAction) in
                        completionHandler(.useCredential, URLCredential(trust: secTrust))
                    }))

                    alert.addAction(UIAlertAction(title: "接続しない", style: .cancel, handler: { (alertAction) in
                        completionHandler(.cancelAuthenticationChallenge, nil)
                    }))

                    self.present(alert, animated: true, completion: nil)
                }
            }
        }
    }
}
```

### コード説明

`urlSession(_:didReceive:completionHandler:)`はベーシック認証時などもコールされるのですが、今回はサーバーの信頼に対する処理だけなので、`NSURLAuthenticationMethodServerTrust`で確認します。
それ以外の場合は特に対処の必要は今回は考慮しないのでリターンをしますが、completionHandlerを必ずコールする必要があるので、`.performDefaultHandling` をコールしておきます。

まず、`SecTrustEvaluateAsyncWithError` にsecTrustを渡して信頼できるかどうかをコールバックでもらいます。
コールバックでは信頼結果が返ってきて、信頼できると判断できる場合はそのまま`.performDefaultHandling`で処理継続できますが、信頼できないとなった場合、アラートを表示してユーザーに判断をしてもらいます。
CFErrorのlocalizedDescriptionにはエラーの原因が入っているので、それをユーザーに提示し、ユーザーが接続可能と判断した場合は`.useCredential`で処理継続できます。
ユーザーが接続しないと判断した場合は`.cancelAuthenticationChallenge`でその接続をキャンセルします。

# アクセス時の画像

## 自己証明書
![自己証明書.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/0347fa14-b780-6ada-5a6c-ce3a4f0f8ae8.png)

## 期限切れ
![期限切れ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/992b0024-1a37-a748-b823-9717e50e771c.png)

## revoked
![revoked.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/9efd39fe-8c36-162c-6da5-75e08dca39c7.png)


# まとめ
つかおう！SecTrust！