Главная > Apple, Coding > Подготовка Apple Push Notification SSL сертификата

Подготовка Apple Push Notification SSL сертификата

Сегодня хотел бы записать на память один нетривиальный процесс, касающийся внедрения механизма Apple Push Notification Service, который осуществляет рассылку коротких сообщений на устройства пользователей приложений в AppStore. И хотел бы я записать последовательность действий, производимых при создании и установки SSL Push Certificate, без которого не будет работать серверная часть, рассылающая пуши. Также если останется место, напишу, как реализовать самый простецкий push-сервер на php. Сама функция Push Notification очень полезна для оповещения пользователей о новых событиях в системе. Такие монстры, как Skype, Google, WhatsApp используют технологию push, чтобы осуществлять вызов абонентов или уведомлять о новых сообщениях на манер, как это делает стандартное приложение PhoneApp.Есть множество сценариев реализации push-технологии в своем проекте, я остановлюсь на одной, которую не так давно реализовывал в проекте Первая Помощь. Предположим, что у нас есть приложение компании, которое содержит каталоги офисов и каталоги промо-материалов компании (акции, новости). При этом стоит задача оповещать всех подписчиков услуги Push об открытии новых офисов и проведении новых акций. Какая в данном случае оптимальная конфигурация? На мой взгляд, в серверной архитектуре должна быть отдельная сущность, реализующая модель данных для Push, сущность для хранения push-токенов подписчиков услуги, и процедура рассылки, собирающая все готовые к отправке Пуш-сообщения на устройства подписчиков. Эта процедура должна стоять на расписании, большую часть своего времени она будет простаивать, но нагрузку это создает минимальную.
И так, что понадобится?
Про то, что нужно иметь официальный сертификат и быть участником программы iOS Developer, я уж не говорю (если не знаете, попробуйте ознакомиться со следующим материалом:  Лёд тронулся, или «Мой путь в AppStore»).
1. Необходимо иметь созданный App ID (кто не в курсе, как, имеется и на тот случай статейка Публикуем приложение в AppStore)
И так, у нас есть APP ID, нам нужно сконфигурировать SSL-сертификат для Push Notification. Для этого мы открываем наш iOS App на просмотр в своем девелоперском аккаунте.

В секции Push Notification ставим галку, если Push Notification для приложения находится в состоянии Disabled. Теперь мы видим два фрейма с сертификатами Development SSL Certificate и Production SSL Certificate. В чем разница? Девелопмент сертификат нужен в период разработки, продакшн уже после публикации. Важное замечание: тестировать пуш можно только на физическом устройстве — в симуляторе не будет работать.
2. Нажимаем Create Certificate сначала для Development SSL Certificate. Для продакшн сертификата последовательность действий будет 100% аналогичная. Попадаем на страницу «About creating a Certificate Signin Request (CSR)», где дается описание наших дальнейших действий.

 

Необходимо создать запрос на подпись сертификата. Делается это через стандартное приложение OS X, которая называется «Связка ключей» (Keychain). Запускаем ее, и сразу идем в меню Связка ключей -> Ассистент сертификации -> Запросить сертификат у бюро сертификации.

В появившемся окне нужно выбрать опцию «Сохранен на диске», email пользователя (здесь часто рекомендуют вводить емэйл apple-аккаунта, но я не уверен, что это критично), Общее имя — так будет называться ваши личный и открытый ключи шифрования.

Далее нажимаем Продолжить, выбираем место на диске, куда будет сохранен файл запроса. Возвращаемся в developer portal к странице «About creating a Certificate Signin Request (CSR)», нажимаем там Continue, на появившейся странице выбираем наш файл запроса и нажимаем Generate. Немного подумав система выдает ссылку на скачивание сгенерированного сертификата, что мы и делаем.

 

Система предлагает сохранить файл сертификата под именем aps_development.cer. Советую создать отдельную папку, куда сохранять все причастное. Встраивать его в свою связку ключей совсем не обязательно
3. Далее необходимо выгрузить личный ключ. Открываем Связку ключей, находим свой ключ, выбираем его и через меню выбираем Файл -> Экспортировать Объект.

 

Система попросит ввести ключевое слово — это пароль для доступа к приватному ключу, что даже если кто-то завладеет экспортированным ключом, он им не сможет воспользоваться. Запомните эту фразу, обозначим ее как pass1.

Система предложит сохранить ключ в файле с расширением .p12 (пусть будет KeyName.p12). При этом еще попросит ввести пароль к связке ключей.

4. Далее нужно сконвертировать сам сертификат и ключ в формат PEM (связка ключей выгружает объекты в формате DER). Не знаю, чем это обусловлено, только библиотека openssl, с помощью которой мы будем осуществлять взаимодействие с Apple Push Notification Service (apns) в php, гораздо охотнее работает с файлами с формате PEM. Реализация openssl присутствует в большинстве *nix-систем, по крайней мере в OS X и FreeBSD она идет в стандартной комплектации. Открываем окно терминала и делаем следующее.

openssl x509 -in aps_development.cer -inform der -out MyAppCert.pem
openssl pkcs12 -nocerts -out ApnsDevKey.pem -in KeyName.p12

при выполнении второй команды консоль попросит ввести ключевую фразу трижды. Первый раз нужно ввести ту ключевую фразу, которую вы вводили при экспорте ключа (pass1). Второй и третий раз — новая ключевая фраза, с которой будет сгенерирован PEM ключ. Будем условно ее называть pass2. Если сделать ее такой же, как pass1 ничего криминального не будет.

Дальше нужно будет склеить эти два файла.

cat MyAppCert.pem ApnsDevKey.pem > apns-dev.pem

и в итоге получаем файл apns-dev.pem, который в дальнейшем и будем эксплуатировать на сервере.
Кстати, тут же можно проверить, что ssl-сертификат выгрузился/сконвертировался корректно. В консоли набираем

openssl s_client -connect gateway.sandbox.push.apple.com:2195  -cert MyAppCert.pem -key ApnsDevKey.pem

если вы тестируете production сертификат, то вместо домена gateway.sandbox.push.apple.com нужно указывать gateway.push.apple.com

Если все ок, консоль выдаст много-много текста.

Далее переносимся на сервер, копируем сертификат apns-dev.pem и создаем рядом с ним php-скрипт, который будет осуществлять непосредственно рассылку Push-сообщений

  function replace_unicode_escape_sequence($match) {
    return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
  }
 
  function send_ios_push($message) {
     $sound = 'default';
     $development = true;
 
     $payload = array();
     $payload["aps"] = array('alert' => $message["body"], 'sound' => $sound);
     $payload = json_encode($payload);
     $payload = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', 'replace_unicode_escape_sequence', $payload);
 
     $apns_url = NULL;
     $apns_cert = NULL;
     $apns_port = 2195;
 
     if($development) {
        $apns_url = 'gateway.sandbox.push.apple.com';
        $apns_cert = 'apns-dev.pem';
     } else {
        $apns_url = 'gateway.push.apple.com';
        $apns_cert = 'apns-dev-prod.pem';
     }
 
     if (file_exists($apns_cert)) 
       echo("cert file exists\n"); 
     else 
       echo("cert file not exists\n");; 
     $success = 0;
     $stream_context = stream_context_create();
     stream_context_set_option($stream_context, 'ssl', 'local_cert', $apns_cert);
     stream_context_set_option($stream_context, 'ssl', 'passphrase', 'pass2');
 
     $apns = stream_socket_client('ssl://' . $apns_url . ':' . $apns_port, $error, $error_string, 2, STREAM_CLIENT_CONNECT, $stream_context);
 
     $device_tokens =array(
        "ae7c44342eceef645f3baf9f7e1eb2daf383f9341535c8dd5ef7276a29068e8d",
        "8d509c28896865f8640f328f30f15721ed40e41593e40a51e77b91c5b6db17d6"
     );
 
     foreach($device_tokens as $device) {
       $apns_message = chr(0) . chr(0) . chr(32) . pack('H*', str_replace(' ', '', $devices)) . chr(0) . chr(strlen($payload)) . $payload;
       if (fwrite($apns, $apns_message)) {
         $success++;
         echo("sent\n");
       } else {
         echo("failed \n");
       }
     }
     echo("fetch done\n"); 
     socket_close($apns);
     fclose($apns);
     return $success;
  }
 
  send_ios_push('Hello world');

Здесь следует обратить внимание на следующие нюансы.
stream_context_set_option($stream_context, ‘ssl’, ‘passphrase’, ‘pass2’);
В этом месте необходимо прописать ключевую фразу pass2 (см. выше). Иначе openssl будет каждый раз просить ввести ее вручную.
$payload = preg_replace_callback(‘/\\\\u([0-9a-f]{4})/i’,
‘replace_unicode_escape_sequence’, $payload);
это делается для того, чтобы увеличить фактическую длину сообщения. Поскольку php функция json_encode преобразует все не-ASCII символы в unicode-сущности, что в 5 раз уменьшает и так не большую максимальную длину Push-сообщения (256 символов).

Конечно вся остальная бизнес-логика может быть другой, например, список push-токенов не в виде константного массива $device_tokens. Этот кусок кода чисто для напоминания принципов работы именно с openssl в php и отправкой push-сообщений.

Если все это получилось, нужно будет все то же самое проделать для Production сертификата, прежде чем публиковать приложение.

  1. 6 августа 2013 в 13:00 | #1

    Несколько замечаний:

    1) По отправке уведомлений из php, вы не делаете проверку на ошибки. У Apple есть расширенный протокол, который возвращает код в случае возникновения ошибки. Чем это плохо? Допустим у вас в базе есть токены устройств, и один из них не валидный. При отправке на такое устройство соединение будет закрыто без предупреждения. А вы в цикле продолжаете слать на остальные устройства. Получается, что вы шлете уведомления которые на часть устройств не дойдут и вы не узнаете в какой момент соединение было закрыто. Так же вы не узнаете причину

    2) Отправка уведомлений из php приемлема только если сообщение отправляется на небольшое кол-во устройств. При отправке скажем на 2000 устройств возникают проблемы, например скрипт сваливается по таймауту. Для таких целей лучше использовать отдельный процесс/демон который из базы берет подготовленные сообщения и шлет их. Я для таки целей имею демона написанного на Си. Сообщения формируются и сохраняются в БД из веб приложения, а демон их рассылает. Для рассылки использую сишную либу libcapn (libcapn.org). Кстати над либой есть враппер — нативный php модуль apn (http://pecl.php.net/package/apn), но его я не юзал :)

  2. 6 августа 2013 в 14:14 | #2

    @Serjo:
    Спасибо большущее за комментарий, он реально полезную информацию несет. Особенно важен для меня лично первый пункт, я думал об этом, и даже где-то слышал, что за постоянные попытки рассылки на дохлые токены Apple может забанить аккаунт. Также слышал, что APNS API позволяет получить список активных токенов, который можно использовать, чтоб актуализировать список на своем сервер-сайде. Хотя в архитектуре, которую я использую для APNS список токенов должен быть всегда актуальным, потому что регулируется клиентским приложением: как только клиент узнает, что apns-токен сменился, приложение отправляет на свой сервер запрос обновления токена. Также предусмотрен запрос отключения push для конкретного девайса — этот момент в приложении также легко отлавливается, главное, проверять токен на живучесть в методах AppDelegate на всяких событиях типа applicationWillEnterForeground.
    По поводу второго пункта вашего ответа — на 100% согласен, и тоже практикую всегда данный подход. В бэкенде раздел рассылки push действует таким образом, что сообщения могут находиться в нескольких состояниях: draft, pending_transfer, transferring, failed, dilivered, deleted. Только демон у меня конечно не такой крутой, как у вас, все на том же php, запускается по крону раз в 2 минуты (минимальный квант крона). То есть цикл отправки следующий: черновик — готов к отправке (ждем своей участи по расписанию) — отправка (запустился крон и пометил сообщение, как отправляющееся) — отправлено/не отправлено.
    Я использую, как вы заметили, php-враппер для openSSL.
    Был бы очень признателен, если б вы может дали какую идею, как можно обрабатывать ошибки в данном случае? Мне видится, что нужно читать ответный поток, анализировать контент — сдается мне, там приезжает старый добрый xml. В нем, вероятно, можно найти код ошибки и отреагировать соответствующе.

  3. Влад
    13 октября 2014 в 20:29 | #3

    Великолепно, это то что нужно! спасибо Вам!!

  4. Сергей
    28 января 2015 в 20:25 | #4

    Спасибо за статью. Такой вопрос. Все работает, но только в sandbox’е, а в продакшене ни в какую. Что уже только не перепробовал. В том числе на AdHoc сборке и сборке через TestFlight. Продакшн сертификат подготовил в точности также, как и в варианте с сэндбоксом. Никаких ошибок в ответ не получаю. Причем в тесте через Amazon SNS в продакшене все работает, но в собственной реализации тишина… Не хочется использовать сторонние сервисы. Есть ли возможность как-то получить ответ ошибки от Apple-сервера? Вроде слышал, что есть вариант с расширенным форматом payload, но не нашел подробностей.

  5. Сергей
    28 января 2015 в 22:07 | #5

    Снимаю свой предыдущий вопрос. Пересоздал по новой сертификаты и проблема исчезла.

  6. Андрей
    8 января 2017 в 21:44 | #6

    А можно изначально браться сразу за сертификат? Читал статью на стеке — говорят, что можно просто выбрать сертификат (вместе с закрытым ключом, то есть выбираем два объекта), создаем из них р12 файл, а потом в терминале с помощью одной строки
    openssl pkcs12 -in FILENAME.p12 -out FILENAME.pem -nodes
    делаем нужный нам pem. Все остальное как и у вас, но вроде должно работать.

  7. 10 января 2017 в 15:56 | #7

    не очень понимаю вашу мысль. сертификат ведь генерируется на стороне Apple. вы генерируете у себя запрос на генерацию сертификата, который содержит в себе публичный ключ. для отправки этого запроса вы должны быть зарегистрированы как девелопер, плюс у вас уже должно быть создано приложение (AppId), к которому привязывается push-сертификат.

  8. 7 апреля 2017 в 21:00 | #8

    У меня есть одна задача. Подскажите пожалуйста, реализуема ли она впринципе?
    мне нужно не отправлять пуши, а принимать
    то есть: на мой ios или android девайс приходят пуши, с информацией о типе операции, и сумме операции.
    и мне нужно эту информацию передать http-запросом на определенный url.

  9. 8 апреля 2017 в 00:11 | #9

    Если у вас есть свободное iOS устройство для этих целей, то не вижу решительно никаких проблем. Что у вас вызывает сложность в этом вопросе?

Подписаться на комментарии по RSS