APNs Provider API(http2)を利用する(iOS)

iOSのAPNsをAPI経由で使う方法。 しかもiOS端末からPush通知を送信する方法。

前回のNode.jsからAPNsを使う方法の派生ネタ。 APNs Provider APIはhttp2とクライアント証明書に対応さえしていればPushを送れるので、 それならiOS端末からでも良けるよね?って試してみた。

環境構築

iOSならiOS9からhttp2に対応しているので、証明書の準備のみ必要。

証明書の準備方法は、以前のPerfect APNs編の手順でapns.p12を書き出せばOK。 .pemの作成やCAルート証明書は不要。

実装

ポイントはhttpsのクライアント認証を実装すること。 それができれば後はPOST形式でAPIを呼び出すだけなので簡単(APIについては前回記事参照)

クライアント認証

NSURLSessionでクライアント認証を実装するには、NSURLSessionDelegateURLSession(_:didReceiveChallenge:completionHandler:)を実装する。

サーバからクライアント認証が要求されると、このデリゲートメソッドが呼ばれるので、 クライアント証明書を読み込んでNSURLCredentialにして渡してあげればOK。

注意点は、他の認証(通常のSSL/TLS認証とかBasic認証)時も全て呼び出されるので、その実装を忘れないこと!

以上を踏まえると、実装はこんな感じ

func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    switch challenge.protectionSpace.authenticationMethod {
    // 通常のhttpsのSSL/TLS認証
    case NSURLAuthenticationMethodServerTrust:
      // デフォルトの動作をさせる
      completionHandler(.PerformDefaultHandling, nil)
    // httpsのクライアント認証
    case NSURLAuthenticationMethodClientCertificate:
      // clientCredential(あらかじめクライアント証明書から生成した認証情報)を
      // 利用して認証をかける
      completionHandler(.UseCredential, clientCredential)
    // その他の認証
    default:
      completionHandler(.PerformDefaultHandling, nil)
    }
}

クライアント証明書の読み込み

クライアントの証明書はp12形式を利用する。 証明書からはSecPKCS12Importを使って認証情報を取り出し、NSURLCredentialを生成する。

// アプリにバンドルされているクライアント証明書(apns.p12)
guard let url = NSBundle.mainBundle().
  URLForResource("apns", withExtension: "p12") else { return }
guard let p12data = NSData(contentsOfURL: p12URL) else { return }

let passphrase = "0000"     // 証明書のパスフレーズ
let options = [kSecImportExportPassphrase as String : passphrase]

var items: CFArray?
guard SecPKCS12Import(p12data, options, &items) == errSecSuccess
  else { return }
guard let cfarr = items else { return }
guard let certEntry = (cfarr as Array).first as? [String: AnyObject]
  else { return }

let identity = certEntry["identity"] as! SecIdentity
let certificates = certEntry["chain"] as? [AnyObject]
let clientCredential = NSURLCredential(identity: identity,
                                       certificates: certificates,
                                       persistence: .ForSession)

今回はサンプルなのでクライアント証明書はアプリにバンドルしているが、通常はキーチェーンにいれておくべき。

なお、クライアント証明書の中身はPush送信用の一つだけが入っている前提。

小ネタなのが、certEntry["identity"] as! SecIdentityという部分。 AnyObjectからSecIdentityへの変換は常に成功するのでas?にはできないみたい。 詳細は公式フォーラムを参照。

APNsの送信

// デバイストークン
let deviceToken = "00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0"
// 通知内容
let payload = "{\"aps\":{\"alert\":\"Hello!\"}}"

// 開発環境向けURL
guard let url = NSURL(string: "https://api.development.push.apple.com/3/device/")
  else { return }
let request = NSMutableURLRequest(URL: url.URLByAppendingPathComponent(deviceToken))
request.HTTPMethod = "POST"
request.HTTPBody = payload.dataUsingEncoding(NSUTF8StringEncoding)

let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
session.dataTaskWithRequest(request).resume()

送信時のポイントは、デリゲートを指定しておくことと、completionHandler形式のメソッドを使わないこと。 使ってしまうとデリゲートが呼び出されなくなり、クライアント認証が通らなくなる。

感想

今回は本当にネタ。多分使い道はないと思う。。。

開発環境

ソース

クライアント証明書を上書きして使う必要があるので注意!

こちら