Swift: ネットワークリクエスト（URLSession、Alamofire を使って）、‪ファイルのアップロード‬、SSL証明書の検証を行うためのコード

## この記事の内容

- `NSURLSession` と `Alamofire` を使用してネットワーク `POST` もしくは `GET` リクエストを行う方法について説明します。
- SSL Pinning (SSL証明書の検証を行うためのコード)

サンプルコードが提供されるので、このページをブックマークし、これらのコードをプロジェクトにすばやく追加できます。

## どうやってテストをするのか？

アプリケーションがネットワークリクエストを成功させることができるかどうかをテストするためのデバッグオプションはたくさんあります。プロキシ、またはWebフックを使用できます。ここでは、無料のオンラインWebフックツール `https://webhook.site/` を使用しました。

サイトにアクセスすると、Webフックリンクが表示されます。そのURLへのリクエストを行うことができ、そしてウェブサイトのインターフェイス上でリクエスト内容を見ることができます。

## Alamofire

`Alamofire` はネットワーク用のオープンソースのフレームワークです。

[Github Repo](https://github.com/Alamofire/Alamofire)
[インストールガイド](https://github.com/Alamofire/Alamofire#installation)

### GET / POST

```swift
func sendRequestRequest() {
    
    // httpリクエストヘッダーを追加する
    let headers = [
        "Authorization":"Bearer ABCD-EFGH-123-456",
        "CatName":"NekoNohe",
    ]
    
    let httpHeaders = HTTPHeaders(headers)
    
    // リクエストを実行する
    
    AF.request("https://apple.com/", method: .get, headers: httpHeaders)
        .validate(statusCode: 200..<300)
        //.responseJSON { response in
        .responseString { response in
            
            switch response.result {
                
            case .success(let value):
                debugPrint("HTTP Response Body: \(response.data)")
            
            case .failure(let error):
                print(error.localizedDescription)
            }
            
    }
    
}
```

### ファイルのアップロード (multipartFormData)

```swift
func requestWith(image: UIImage){
    
    let url = "https://webhook.site/xxxxxxxxxxxx"
    
    let headers: HTTPHeaders = [
        "Content-type": "multipart/form-data"
    ]
    
    guard let imageData = image.jpegData(compressionQuality: 1.0) else { return }
    
    AF.upload(multipartFormData: { (multipartFormData) in
        multipartFormData.append(imageData, withName: "image", fileName: "image.jpg", mimeType: "image/jpeg")
    }, to: url, method: .put, headers: headers).responseString { (response) in
        
        switch response.result {
            
        case .success(let result):
            print(result)
            
        case .failure(let error):
            print(error.localizedDescription)
            
            
        }
        
    }
    
}
```

上記の方式を示す引数は使用対象のウェブサイトが要求する方式に変更する必要があります。一部のウェブサイトでは、`POST` 方式が必要となる可能性があります。

### SSL Pinning (証明書の検証)

#### SSLピン留めとは

クライアントからサーバーにネットワーク要求を行う場合、クライアントとサーバーの間に第三者が存在する可能性があります。そして、第三者がリクエストを傍受し、独自の証明書を使ってデータを変更する可能性があります。しかし、SSL証明書の有効性を検証し、それらが実際のサーバーに属していることを確認すると、そういった行為を防ぐことができます。

#### ウェブサイトのSSLサーバ証明書を取得する

Safariブラウザを開き、ウェブサイト（https://apple.com など）にアクセスします。

ロックアイコンをクリックし、ボタンをクリックすると詳細が表示されます。

![image](https://mszmagic.s3-ap-northeast-1.amazonaws.com/qiita_article_sslPinning_20200530.png)

次に、証明書をアイコンからファインダにドラッグします。

![image](https://mszmagic.s3-ap-northeast-1.amazonaws.com/qiita_article_SSLpinning3_20200530.gif)

#### コード

まず、パブリック認証ファイルをプロジェクトに追加する。この例では、ファイル名は `www.apple.com.cer` だ。ファイルがプログラミングのターゲットにリンクされていることを確認すること。

##### `Alamofire session` を用意する。

```swift
var session: Session!

func getSecureSession() -> Session? {
    
    if let filePath = Bundle.main.path(forResource: "www.apple.com", ofType: "cer"),
        let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)),
        let certificate = SecCertificateCreateWithData(nil, data as CFData) {
        
        let manager = ServerTrustManager(evaluators:
            [
                "www.apple.com": PinnedCertificatesTrustEvaluator(certificates: [certificate], acceptSelfSignedCertificates: false, performDefaultValidation: true, validateHost: true)
        ])
        let configuration = URLSessionConfiguration.af.default

        return Session(configuration: configuration, serverTrustManager: manager)
        
    }
    
    return nil
    
}
```

ここに、ドメイン名とポリシーをマッピングする辞書（ディクショナリ）がある。

```swift
"www.apple.com": PinnedCertificatesTrustEvaluator(certificates: [certificate], acceptSelfSignedCertificates: false, performDefaultValidation: true, validateHost: true)
```

##### リクエストする。

```swift
func sendRequestRequest() {
    
    guard let session = getSecureSession() else { return }
    self.session = session
    
    // Perform the request
    
    self.session.request("https://apple.com/", method: .get)
        .validate(statusCode: 200..<300)
        //.responseJSON { response in
        .responseString { response in
            
            switch response.result {
                
            case .success(let value):
                debugPrint("HTTP Response Body: \(response.data)")
            
            case .failure(let error):
                print(error.localizedDescription)
            }
            
    }
    
}
```

そうすると、接続しているドメインが `www.apple.com` なのであればリクエストが通る。

SSL証明を別のウェブサイトの証明書と置き換え、リクエストエラーが発生することを確認してみるといいだろう。

#### 注

SSLサーバ証明書には有効期限があります。

アプリに保存されているSSLサーバ証明書は、たまに更新する必要があります。

## NSURLSession

### POST / GET

```swift
func sendRequest() {
    
    let sessionConfig = URLSessionConfiguration.default
    let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)

    //URLを設定する
    let urlString = "https://webhook.site/xxxxxxxxxxxx"
    guard let URL = URL(string: urlString) else { return }
    
    var request = URLRequest(url: URL)
    
    //リクエストの方式 `(GET, POST)` を設定する
    request.httpMethod = "GET"
    
    //OAuth 2 Bearer トークン
    request.addValue("Bearer ABCD-EFGH-123-456", forHTTPHeaderField: "Authorization")

    //httpリクエストヘッダーを追加する
    request.addValue("NekoNohe", forHTTPHeaderField: "CatName")
    request.addValue("token=NekoNeko", forHTTPHeaderField: "Cookie")

    //httpリクエストボディーを追加する
    let JSONdictionary : [String: Any] = [
                                          "name": "axel",
                                          "favorite_animal": "fox"
                                         ]

    if let postData = try? JSONSerialization.data(withJSONObject: JSONdictionary, options: []) {
        request.httpBody = postData
    }

    //データタスクを定義する
    let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
        if (error == nil),
            let httpResponse = response as? HTTPURLResponse {
            // リクエスト成功
            let statusCode = httpResponse.statusCode
            // TODO: コードをここに追加する
        }
        else {
            print(error?.localizedDescription)
        }
    })
    task.resume()
    session.finishTasksAndInvalidate()
}
```

## その他の

私は以前に `Twitter` にコンテンツを投稿するための `Swift` & `NSURLSession` プログラムを書く方法について記事を書いています。こちらからお読みいただけます。

https://qiita.com/MaShunzhe/items/d7f4ad1df259354d52ae

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">