iOS + PHPでPush Notificationを実装する

Morning Relayという目覚ましアプリで、iOS + PHPでPush Notificationを実装してみた。公式ドキュメントを読むと複雑で難しそうだが、じっくりやれば大丈夫。サーバー側の実装は公式ドキュメントには実例が載っていないのだが、「apns-php」というPHPのライブラリを使うことでラクにできた。

環境

  1. XCode 4.3
  2. PHPフレームワークCakePHPを使っているが、特にCakePHPに依存している個所はない)
  3. サーバー側のライブラリにapns-phpを使用、ローカルでの作業にMac標準の「キーチェーンアクセス」を使用

概要

  1. 準備
    • App IDを作成する
    • プロビジョニングファイルの作成とローカルへのコピー
    • ローカルでCSR(証明書署名要求: Certificate Signing Request)ファイルを作成、それをAppleのサーバーにアップロードして証明書と鍵を受け取る
    • 自分のサーバーに配置するためのSSL証明書と鍵を作成する(2種類のpemファイルを作成)
  2. iOS側での実装
  3. サーバー側での実装
    • apns-phpをダウンロードする
    • apns-phpのファイルを修正する
    • Apple Push Notification サービス(APNs)にプッシュ

準備

1. App IDを作成する

※ すでにApp IDがあるときは、これらの操作は不要
(1) iOS Dev Center > iOS Provisioning Portalを開く。
(2) ウインドウの左メニューの[App IDs]をクリック
(3) [New App ID] をクリック
(4) Descriptionにアプリ名(App IDを説明する名前)を入力、Bundle Identifierにドメイン名を逆にするなどの一意の値を入力して、[Submit]をクリック。App IDが新規に作成される。

2. プロビジョニングファイルの作成とローカルへのコピー

(1) iOS Provisioning Portalにアクセスして[New Profile]からプロビジョニングファイルを作成する。App IDは、1で作成したものを選択する。
(2) 作成したプロビジョニングファイルをダウンロードして、~/Library/MobileDevice/Provisioning Profilesにコピーする。ファイル名は、MorningRelayDev.mobileprovisionなど、開発用か製品用か区別できるようにしておく。

3. ローカルでCSR(証明書署名要求: Certificate Signing Request)ファイルを作成、それをAppleのサーバーにアップロードして証明書と鍵を受け取る

(1) ローカルでキーチェーンアクセスアプリケーションを起動して、キーチェーンアクセス > 証明書アシスタント > 認証局に証明書を要求…をクリック、ユーザのメールアドレスはAppleに登録したメールアドレスを入力、通称は開発者の名前や会社名などを入力、要求の処理でディスクに保存をチェック、続けるをクリックして、適当な名前を付けてファイルを保存する
(2) iOS Provisioning Portal > App IDsから先ほど作成したアプリのConfigureリンクをクリック、Configure App ID ページで「Enable for Push Notification Services」ボックスにチェックを入れて、まずは開発版のDevelopment Push SSL Certificateの[Configure]をクリック、[Continue]クリックで次に進む。
(3) (1)で作成したキーファイルを設定して、[Generate]をクリックする。なぜかGoogle Chromeでは反応しなかった。Safariを使えば数秒で認証が完了する。
(4) 完了すると「Download & Install Your Apple Push Notification service SSL Certificate」の画面が表示されるので、[Download]ボタンで証明書をダウンロードする。ここで、App IDsの画面で、アプリの「Push Notification」が「Configurable」から「Enabled」になったことを確認する。
(5) ダウンロードした証明書(aps_development.cer)は開発用なので、aps_development_dev.cerなどにリネーム後、ローカルでダブルクリックしてMacのキーチェーンに登録する。
※ 「Development Push SSL Certificate」に続き「Production Push SSL Certificate」も同様に上記の手順を実行する。

MEMO:
> 公式ドキュメントにはもう少し詳しく書いてある:CSRの生成時に、キーチェーンアクセスは秘密暗号鍵と公開暗号鍵のペアを生成します。秘密鍵 はデフォルトでログインキーチェーンに組み込まれます。公開鍵は、CA(認証局)に送信され るCSRに含められます。CAから証明書が戻ってくると、その証明書の項目の1つが公開鍵になっています。

4. 自分のサーバーに配置するためのSSL証明書と鍵を作成する(2種類のpemファイルを作成)

(1) キーチェーンアクセスからSSL証明書と鍵を取得する。左側のペインの自分の証明書 > Apple Development IOS Push Services: *** をクリックして展開して、証明書と秘密鍵の両方が表示させる。
(2) 証明書と鍵の両方を選択して、ファイル > 書き出す」を選択、それらを「個人情報交換(.p12)」ファイルとして書き出す。
(3) サーバーで使えるように、以下のコマンドで.p12形式を.pem形式に変換する。AppNameDev.pemファイルが作られる。(「AppNameDev」部分を書き換える。)

$ openssl pkcs12 -in AppNameDev.p12 -out AppNameDev.pem -nodes

(4) Entrust Root Certification Authorityを作成する。キーチェーンアクセス > 左下ペインの「証明書」 > Entrust Root Certification Authority > 右クリック > 「"Entrust Root Certification Authority"を書き出す...」 > ファイル名を「entrust_root_certification_authority.pem」にして保存する。これで、entrust_root_certification_authority.pemファイルが作られる。

iOS側での実装

1. RemoteNotificationを登録する
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    ...

    // アプリケーションが起動するたびに、デバイストークンを要求してそれをプロバイダに渡すことで、
    // プロバイダが最新のデバイストークンを持つことを保証
    [application registerForRemoteNotificationTypes: (UIRemoteNotificationTypeBadge|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeAlert)];
    
    return YES;
}

これで、アプリ初回起動時にRemoteNotificationを許可するかどうかのダイアログが表示されるようになる。「didFinishLaunching」内だと「didFinishLaunchingWithOptions」メソッドが存在すれば呼ばれないので注意。

2. デバイストークン(Device Token)を取得する
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *) devToken
{ 
    NSLog(@"deviceToken: %@", devToken);
    [self sendProviderDeviceToken:devToken]; // 自分のサーバーに送信する
}

- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *) err
{ 
    NSLog(@"Errorinregistration.Error:%@", err); 
}
3. デバイストークンを自分のサーバーに送る
- (void)sendProviderDeviceToken:(NSData *)token
{
    NSMutableData *data = [NSMutableData data];
    [data appendData:[@"device=" dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:token];
    
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://morning-relay.com/push"]];
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];		
    [request setHTTPMethod:@"POST"];
    [request setHTTPBody:data];
    [NSURLConnection connectionWithRequest:request delegate:self];
} 

この例ではNSURLConnectionクラスを使っているが、自分のサーバーにデータをポストするだけなので、どんな方法でもOK。

サーバー側での実装

iOSから送られてきたデバイストークンを受け取る処理は省略。以下は、Apple Push Notificationサービス(APNs)のサーバーにPUSHする処理。

1. apns-phpをダウンロードする

(1) apns-phpというライブラリ(ApnsPHP-r100.zip)をダウンロードする。
(2) ZIPファイルをサーバー(ローカルホストでもOK)に展開して、ブラウザからアクセスできる場所に配置する。
(3) サーバープログラムのルートディレクトリ(CakePHPの場合はapp/webroot)にcertificatesというフォルダを作り、その中に上記で作成した「AppNameDev.pem」と「entrust_root_certification_authority.pem」ファイルを配置する。※ 本番では適切なアクセス権を設定する

2. apns-phpのファイルを修正する

(4) sample_push.phpを以下のように修正する。

...
// Instanciate a new ApnsPHP_Push object
$push = new ApnsPHP_Push(
	ApnsPHP_Abstract::ENVIRONMENT_SANDBOX,
        // 修正前
	// 'server_certificates_bundle_sandbox.pem',
        // 修正後
	'certificates/AppNameDev.pem'
);
...
// Set the Root Certificate Autority to verify the Apple remote peer
// 修正前
// $push->setRootCertificationAuthority('entrust_root_certification_authority.pem');
// 修正後
$push->setRootCertificationAuthority('certificates/entrust_root_certification_authority.pem');
... 
// Instantiate a new Message with a single recipient
// 修正前
// $message = new ApnsPHP_Message('9e1791742ce6658f0d04128ec05ae4ed9858e534ff6ae6e8e18c68ded547ba0b');
// 修正後
$message = new ApnsPHP_Message('iOSで取得したDeviceTokenを記入。NSLogで出力したものから半角スペースを除くだけでOK。');
3. Apple Push Notification サービス(APNs)にプッシュ

(1) ブラウザでsample_push.phpにアクセスする
これでOKと思いきや、ブラウザでsample_push.phpにアクセスすると、以下のエラーが表示される。

Mon, 27 Aug 2012 08:22:13 +0200 ApnsPHP[4102]: INFO: Trying ssl://gateway.sandbox.push.apple.com:2195... Mon, 27 Aug 2012 08:22:14 +0200 ApnsPHP[4102]: ERROR: Unable to connect to 'ssl://gateway.sandbox.push.apple.com:2195': (0) Mon, 27 Aug 2012 08:22:14 +0200 ApnsPHP[4102]: INFO: Retry to connect (1/3)... Mon, 27 Aug 2012 08:22:15 +0200 ApnsPHP[4102]: INFO: Trying ssl://gateway.sandbox.push.apple.com:2195... Mon, 27 Aug 2012 08:22:15 +0200 ApnsPHP[4102]: ERROR: Unable to connect to 'ssl://gateway.sandbox.push.apple.com:2195': (0) Mon, 27 Aug 2012 08:22:15 +0200 ApnsPHP[4102]: INFO: Retry to connect (2/3)... Mon, 27 Aug 2012 08:22:16 +0200 ApnsPHP[4102]: INFO: Trying ssl://gateway.sandbox.push.apple.com:2195... Mon, 27 Aug 2012 08:22:17 +0200 ApnsPHP[4102]: ERROR: Unable to connect to 'ssl://gateway.sandbox.push.apple.com:2195': (0) Mon, 27 Aug 2012 08:22:17 +0200 ApnsPHP[4102]: INFO: Retry to connect (3/3)... Mon, 27 Aug 2012 08:22:18 +0200 ApnsPHP[4102]: INFO: Trying ssl://gateway.sandbox.push.apple.com:2195...

Unable to connect to 'ssl://gateway.sandbox.push.apple.com:2195': (0)

回避方法は、Stack Over Flowのページにあった。

// Abstract.php
$streamContext = stream_context_create(array('ssl' => array(
    // この行をコメントアウト。なぜこうすれば動くかは不明。
    // 'verify_peer' => isset($this->_sRootCertificationAuthorityFile),
    'cafile' => $this->_sRootCertificationAuthorityFile,
    'local_cert' => $this->_sProviderCertificateFile
)));

(2) リトライ!
これで、再度ブラウザからアクセスすると、以下のようなデバッグメッセージが表示されて...

Mon, 27 Aug 2012 08:59:35 +0200 ApnsPHP[6999]: INFO: Trying ssl://gateway.sandbox.push.apple.com:2195... Mon, 27 Aug 2012 08:59:35 +0200 ApnsPHP[6999]: INFO: Connected to ssl://gateway.sandbox.push.apple.com:2195. Mon, 27 Aug 2012 08:59:35 +0200 ApnsPHP[6999]: INFO: Sending messages queue, run #1: 1 message(s) left in queue. Mon, 27 Aug 2012 08:59:35 +0200 ApnsPHP[6999]: STATUS: Sending message ID 1 [custom identifier: Message-Badge-3] (1/3): 177 bytes. Mon, 27 Aug 2012 08:59:36 +0200 ApnsPHP[6999]: INFO: Disconnected.

無事Remote Notificationが送られてきた!