Недавно понадобилось внедрить push уведомления в свои веб сервисы, поискав инструкции в интернете нашел много чего для GCM, Firebase и т.д. но ни одной подробной или пошаговой инструкции для браузера Safari (на macOS, не знаю или будет работать в Windows). В принципе, и Firebase в Safari спрашивал разрешение на уведомление, и даже попадал в настройки, но это всего лишь пыль в глаза, т.к. ясное дело что никаких уведомлений от Firebase браузер получать не хотел.
Делал я все это по вот этой вот инструкции, тут много полезного, но и много чего не хватает, надо постоянно что-то искать и собирать, поэтому решил написать статью от и до: «Как сделать Push уведомления в браузере Safari на macOS» вдруг кому пригодится!
Инструкция подразумевает что у вас есть аккаунт разработчика Apple. Не знаю нужен ли платный, на бесплатном не пробовал (пользуюсь корпоративным).
Заходим в свой аккаунт разработчика, и здесь регистрируем новый Website Push ID:
Заходим в связку ключей, в меню выбираем «Ассистент сертификации» — «Запросить сертификат у бюро сертификации», Вводим свою почту, имя, и выбираем «Сохранен на диске» и выбираем куда сохранить файл.
Идём снова в аккаунт разработчика, здесь нажимаем на плюсик, тем самым создаём новый сертификат, выбираем пункт «Website Push ID Certificate», выбираем наш сгенерированный Website Push ID, далее, далее, и потом выбираем сгенерированный во втором шаге файл, после этого нам скажут что всё окей, и дадут скачать файл. Естественно качаем его к себе на компьютер, и дважды кликнув на него добавляем его в свою связку ключей.
Когда я делал это впервые я потратил около часа времени, пока понял что от меня нужно, а магия простая: надо экспортировать не только сертификат, а и ключ! Для этого его необходимо «раскрыть» и по очереди экспортировать как ключ так и сертификат, правой кнопкой на сертификат и выбираем «Экспортировать» и то же самое для ключа. Советую сразу назвать сертификат apns-pro-cert.pem и ключ apns-pro-key.pem, это для того чтобы в следующем шаге просто копировать команды и не переписывать имена файлов на свои. Так же при экспорте укажите пароли, и запомните, при конвертации нужно будет их вводить.
Нашел очень классную инструкцию по которой делал конвертацию сертификата в p12 формат (который нужен как для отправки уведомлений, так и для генерации файлов (будет далее)). Нам необходима «Production Phase». Продублирую код тут, а то я попадал на много статей где были указаны ссылки а они оказывались битые.
Внимательно смотрите на шаг 3 (который удаляет пароль) и далее шаг 4 и 5, которые разнятся в зависимости выполняли ли вы шаг 3 или нет, лично я не делал, и оставлял пароль, как делать вам — решайте сами.
Лучше положить их выше папки докрута чтобы к ним не было доступа из веба, нам из всего полученного нужны будут только 2 файла это: apns-pro.pem — для отправки уведомлений, и apns-pro-cert.p12 для генерации пакета для уведомлений (будет далее).
Чтобы браузер разрешил вам получать запросы от вашего сайта необходимо создать правильный пакет файлов, который будет включать в себя иконки, manifest.json, signature, website.json, при первом запросе на уведомления браузер загружает это всё к себе и хранит локально. Для этого делаем такую структуру, кладём это всё в корень нашего сайта.
Уточнение: В файле website.json «urlFormatString»: «awery.workreports.pro/#/app/%@», это ссылка по которой будет переходить пользователь кликая на уведомление, %@ это url-argument который можно будет передавать чтобы пользователь по разным типам уведомлений переходил по разным путям.
Чтобы получить правильные файлы manifest.json и signature необходимо воспользоваться файлом генерации от Apple. Я его немного подправил, удалил лишнего чуть, этот работает как надо.
Кладём его в докрут и вызываем, он в ответ даст нам сразу файл на загрузку, только назвать его надо будет «pushPackage.zip» Сохраните, распакуйте и проверьте создались ли все нужные файлы (а именно signature).
Я делал папку v1, в которую положил нужные файлы, архив, который мы сгенерировали в предыдущем шаге, и файлик index.php для обработки запросов. Необходимо чтобы сайт отвечал на такие запросы:
/v1/pushPackages/{website} должен отдавать архив в ответ
/v1/devices/{device}/registrations/{website} для регистрации и удаления (GET/POST — DELETE)
/v1/log для логирования ошибок
На самом деле нужно сделать рабочим только роут который отдаёт архив в ответ, остальные могут просто отдавать 200 статус, без особой обработки, но можно (и желательно) сделать всё как надо. По роуту /v1/devices/{device}/registrations/{website} в device будет передаваться токен а в website ваш зарегистрированный ID (в моем случае это com.pro.workreports.awery) если запрос POST (или GET) тогда мы регистрируем девайс, если запрос DELETE тогда удаляем. Но будет и другой способ, из JS'a.
По роуту /v1/log вам будут приходить ошибки, если вдруг что-то не так. Мне это в первые разы очень помогало, понимал что я не доделал и чинил это.
У меня на сайте есть и Firebase (для уведомлений в Chrome) и APNS для уведомлений в safari. Чтобы и то и то работало корректно — делаю проверку (приложение на AngularJS):
и функция checkRemotePermission:
И понятное дело функцию sendTokenToServer нет смысла описывать, она на вход принимает токен и тип устройства и сохраняет в базе, с этим, думаю, справитесь.
Скрипт отправки уведомлений на Safari такой же как и на iOS, просто оставлю его здесь:
Тут всё предельно ясно — вылавливаю все токены пользователя с типом «safari» и шлю уведомления по всем токенам.
Пробуем запускать сайт, разрешаем уведомления, *смотрим или токен сохранился*, и пробуем отправлять. Пробуйте отправлять с другого браузера, и Safari должен быть закрыт. Если всё сделали правильно — увидите уведомление! Они красивые, красивее чем из Chrome'a, они выглядят как нативные, и приходят чаще, т.к. у меня и Firebase и Safari то вижу что иногда Safari уведомления есть, а вот из Chrome'a не всегда приходят.
Если вдруг после прочтения статьи остались вопросы или непонимания — пишите, буду по возможности отвечать в комментариях. Буду рад конструктивной критике и замечаниям. На супер продвинутого программиста не претендую, поэтому допускаю что где-то я могу быть не совсем прав. Но по этому алгоритму делал уведомления на 2 своих ресурса и везде они работают.
Делал я все это по вот этой вот инструкции, тут много полезного, но и много чего не хватает, надо постоянно что-то искать и собирать, поэтому решил написать статью от и до: «Как сделать Push уведомления в браузере Safari на macOS» вдруг кому пригодится!
Инструкция подразумевает что у вас есть аккаунт разработчика Apple. Не знаю нужен ли платный, на бесплатном не пробовал (пользуюсь корпоративным).
Вот так выглядели запросы из Firebase
Шаг 1: Регистрируем Website Push ID
Заходим в свой аккаунт разработчика, и здесь регистрируем новый Website Push ID:
Website Push ID
Шаг 2: Запрашиваем сертификат в Связке ключей на MacOS
Заходим в связку ключей, в меню выбираем «Ассистент сертификации» — «Запросить сертификат у бюро сертификации», Вводим свою почту, имя, и выбираем «Сохранен на диске» и выбираем куда сохранить файл.
Запрос сертификата
Шаг 3: Генерируем сертификат
Идём снова в аккаунт разработчика, здесь нажимаем на плюсик, тем самым создаём новый сертификат, выбираем пункт «Website Push ID Certificate», выбираем наш сгенерированный Website Push ID, далее, далее, и потом выбираем сгенерированный во втором шаге файл, после этого нам скажут что всё окей, и дадут скачать файл. Естественно качаем его к себе на компьютер, и дважды кликнув на него добавляем его в свою связку ключей.
Генерация сертификата
Шаг 4: Экспорт ключа и сертификата
Когда я делал это впервые я потратил около часа времени, пока понял что от меня нужно, а магия простая: надо экспортировать не только сертификат, а и ключ! Для этого его необходимо «раскрыть» и по очереди экспортировать как ключ так и сертификат, правой кнопкой на сертификат и выбираем «Экспортировать» и то же самое для ключа. Советую сразу назвать сертификат apns-pro-cert.pem и ключ apns-pro-key.pem, это для того чтобы в следующем шаге просто копировать команды и не переписывать имена файлов на свои. Так же при экспорте укажите пароли, и запомните, при конвертации нужно будет их вводить.
Экспорт ключа и сертификата
Шаг 5: Конвертация сертификата в p12
Нашел очень классную инструкцию по которой делал конвертацию сертификата в p12 формат (который нужен как для отправки уведомлений, так и для генерации файлов (будет далее)). Нам необходима «Production Phase». Продублирую код тут, а то я попадал на много статей где были указаны ссылки а они оказывались битые.
Step 1: Create Certificate .pem from Certificate .p12
Command: openssl pkcs12 -clcerts -nokeys -out apns-pro-cert.pem -in apns-pro-cert.p12
Step 2: Create Key .pem from Key .p12
Command : openssl pkcs12 -nocerts -out apns-pro-key.pem -in apns-pro-key.p12
Step 3: Optional (If you want to remove pass phrase asked in second step)
Command : openssl rsa -in apns-pro-key.pem -out apns-pro-key-noenc.pem
Step 4: Now we have to merge the Key .pem and Certificate .pem to get Production .pem needed for Push Notifications in Production Phase of App
Command : cat apns-pro-cert.pem apns-pro-key-noenc.pem > apns-pro.pem (If 3rd step is performed ) Command : cat apns-pro-cert.pem apns-pro-key.pem > apns-pro.pem (if not)
Step 5: Check certificate validity and connectivity to APNS
Command: openssl s_client -connect gateway.push.apple.com:2195 -cert apns-pro-cert.pem -key apns-pro-key.pem (If 3rd step is not performed )
Command: openssl s_client -connect gateway.push.apple.com:2195 -cert apns-pro-cert.pem -key apns-pro-key-noenc.pem (If performed )
Внимательно смотрите на шаг 3 (который удаляет пароль) и далее шаг 4 и 5, которые разнятся в зависимости выполняли ли вы шаг 3 или нет, лично я не делал, и оставлял пароль, как делать вам — решайте сами.
Конвертация сертификата в p12
Шаг 6: Закидываем сертификаты на сайт
Лучше положить их выше папки докрута чтобы к ним не было доступа из веба, нам из всего полученного нужны будут только 2 файла это: apns-pro.pem — для отправки уведомлений, и apns-pro-cert.p12 для генерации пакета для уведомлений (будет далее).
Шаг 7: Подготавливаем ресурсы
Чтобы браузер разрешил вам получать запросы от вашего сайта необходимо создать правильный пакет файлов, который будет включать в себя иконки, manifest.json, signature, website.json, при первом запросе на уведомления браузер загружает это всё к себе и хранит локально. Для этого делаем такую структуру, кладём это всё в корень нашего сайта.
Уточнение: В файле website.json «urlFormatString»: «awery.workreports.pro/#/app/%@», это ссылка по которой будет переходить пользователь кликая на уведомление, %@ это url-argument который можно будет передавать чтобы пользователь по разным типам уведомлений переходил по разным путям.
Подготавливаем ресурсы
Шаг 8: Генерация архива
Чтобы получить правильные файлы manifest.json и signature необходимо воспользоваться файлом генерации от Apple. Я его немного подправил, удалил лишнего чуть, этот работает как надо.
<?php
// This script creates a valid push package.
// This script assumes that the website.json file and iconset already exist.
// This script creates a manifest and signature, zips the folder, and returns the push package.
// Use this script as an example to generate a push package dynamically.
$certificate_path = "../certs/apns-pro-cert.p12"; // Change this to the path where your certificate is located
$certificate_password = "PASSPHRASE"; // Change this to the certificate's import password
// Convenience function that returns an array of raw files needed to construct the package.
function raw_files() {
return array(
'icon.iconset/icon_16x16.png',
'icon.iconset/icon_16x16@2x.png',
'icon.iconset/icon_32x32.png',
'icon.iconset/icon_32x32@2x.png',
'icon.iconset/icon_128x128.png',
'icon.iconset/icon_128x128@2x.png',
'website.json'
);
}
// Copies the raw push package files to $package_dir.
function copy_raw_push_package_files($package_dir) {
mkdir($package_dir . '/icon.iconset');
foreach (raw_files() as $raw_file) {
copy("pushPackage.raw/$raw_file", "$package_dir/$raw_file");
}
}
// Creates the manifest by calculating the SHA1 hashes for all of the raw files in the package.
function create_manifest($package_dir) {
// Obtain SHA1 hashes of all the files in the push package
$manifest_data = array();
foreach (raw_files() as $raw_file) {
$manifest_data[$raw_file] = sha1(file_get_contents("$package_dir/$raw_file"));
}
file_put_contents("$package_dir/manifest.json", json_encode((object)$manifest_data));
}
// Creates a signature of the manifest using the push notification certificate.
function create_signature($package_dir, $cert_path, $cert_password) {
// Load the push notification certificate
$pkcs12 = file_get_contents($cert_path);
$certs = array();
if(!openssl_pkcs12_read($pkcs12, $certs, $cert_password)) {
exit('Something wrong with certificate. Err 1');
return;
}
$signature_path = "$package_dir/signature";
// Sign the manifest.json file with the private key from the certificate
$cert_data = openssl_x509_read($certs['cert']);
$private_key = openssl_pkey_get_private($certs['pkey'], $cert_password);
openssl_pkcs7_sign("$package_dir/manifest.json", $signature_path, $cert_data, $private_key, array(), PKCS7_BINARY | PKCS7_DETACHED);
// Convert the signature from PEM to DER
$signature_pem = file_get_contents($signature_path);
$matches = array();
if (!preg_match('~Content-Disposition:[^\n]+\s*?([A-Za-z0-9+=/\r\n]+)\s*?-----~', $signature_pem, $matches)) {
exit('Something wrong with certificate. Err 2');
return;
}
$signature_der = base64_decode($matches[1]);
file_put_contents($signature_path, $signature_der);
}
// Zips the directory structure into a push package, and returns the path to the archive.
function package_raw_data($package_dir) {
$zip_path = "$package_dir.zip";
// Package files as a zip file
$zip = new ZipArchive();
if (!$zip->open("$package_dir.zip", ZIPARCHIVE::CREATE)) {
error_log('Could not create ' . $zip_path);
return;
}
$raw_files = raw_files();
$raw_files[] = 'manifest.json';
$raw_files[] = 'signature';
foreach ($raw_files as $raw_file) {
$zip->addFile("$package_dir/$raw_file", $raw_file);
}
$zip->close();
return $zip_path;
}
// Creates the push package, and returns the path to the archive.
function create_push_package() {
global $certificate_path, $certificate_password;
// Create a temporary directory in which to assemble the push package
$package_dir = '/tmp/pushPackage' . time();
if (!mkdir($package_dir)) {
unlink($package_dir);
die;
}
copy_raw_push_package_files($package_dir);
create_manifest($package_dir);
create_signature($package_dir, $certificate_path, $certificate_password);
$package_path = package_raw_data($package_dir);
return $package_path;
}
// MAIN
$package_path = create_push_package();
if (empty($package_path)) {
http_response_code(500);
die;
}
header("Content-type: application/zip");
echo file_get_contents($package_path);
die;
Кладём его в докрут и вызываем, он в ответ даст нам сразу файл на загрузку, только назвать его надо будет «pushPackage.zip» Сохраните, распакуйте и проверьте создались ли все нужные файлы (а именно signature).
Шаг 9: Делаем папку v1 (или настраиваем роутинг)
Я делал папку v1, в которую положил нужные файлы, архив, который мы сгенерировали в предыдущем шаге, и файлик index.php для обработки запросов. Необходимо чтобы сайт отвечал на такие запросы:
/v1/pushPackages/{website} должен отдавать архив в ответ
/v1/devices/{device}/registrations/{website} для регистрации и удаления (GET/POST — DELETE)
/v1/log для логирования ошибок
На самом деле нужно сделать рабочим только роут который отдаёт архив в ответ, остальные могут просто отдавать 200 статус, без особой обработки, но можно (и желательно) сделать всё как надо. По роуту /v1/devices/{device}/registrations/{website} в device будет передаваться токен а в website ваш зарегистрированный ID (в моем случае это com.pro.workreports.awery) если запрос POST (или GET) тогда мы регистрируем девайс, если запрос DELETE тогда удаляем. Но будет и другой способ, из JS'a.
По роуту /v1/log вам будут приходить ошибки, если вдруг что-то не так. Мне это в первые разы очень помогало, понимал что я не доделал и чинил это.
Папка V1
Шаг 10: Запрашиваем токен из JS'a
У меня на сайте есть и Firebase (для уведомлений в Chrome) и APNS для уведомлений в safari. Чтобы и то и то работало корректно — делаю проверку (приложение на AngularJS):
if ('safari' in window && 'pushNotification' in window.safari) {
var permissionData = window.safari.pushNotification.permission('web.com.example.domain');
$scope.checkRemotePermission(permissionData);
}else {
//FIREBASE HERE
}
и функция checkRemotePermission:
$scope.checkRemotePermission = function (permissionData) {
if (permissionData.permission === 'default') {
// This is a new web service URL and its validity is unknown.
window.safari.pushNotification.requestPermission(
'https://awery.workreports.pro', // The web service URL.
'web.pro.workreports.awery', // The Website Push ID.
{}, // Data that you choose to send to your server to help you identify the user.
$scope.checkRemotePermission // The callback function.
);
}
else if (permissionData.permission === 'denied') {
if($rootScope.log)
console.war('User not allowed notifications');
}
else if (permissionData.permission === 'granted') {
// The web service URL is a valid push provider, and the user said yes.
// permissionData.deviceToken is now available to use.
console.log(permissionData.deviceToken, 'YEAH!');
$rootScope.sendTokenToServer(permissionData.deviceToken, 'safari');
}
};
И понятное дело функцию sendTokenToServer нет смысла описывать, она на вход принимает токен и тип устройства и сохраняет в базе, с этим, думаю, справитесь.
Запрос прав
Шаг 11: Отправка уведомлеиня с бекенда:
Скрипт отправки уведомлений на Safari такой же как и на iOS, просто оставлю его здесь:
$sql = "SELECT * FROM `user_devices` WHERE `id_user` = ? AND `type` = ?";
$tokens = $dbh->fetchAll($sql, array($id_user, 'safari'));
$result = false;
if (count($tokens) > 0) {
$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', __DIR__ . '/certs/apns-pro.pem');
stream_context_set_option($ctx, 'ssl', 'passphrase', 'PASSPHAREHERE');
//
$fp = stream_socket_client(
'ssl://gateway.push.apple.com:2195', $err,
$errstr, 60, STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT, $ctx);
if (!$fp)
$log->addError("Failed to connect: $err $errstr");
else {
foreach ($tokens as $token) {
$deviceToken = $token['token'];
// Create the payload body
$payload = json_encode(array(
'aps' => array(
'alert' => array(
'title' => $title,
'body' => $message,
'action' => 'Details'
),
'url-args' => array($route!=''?route:'generalUser/notifications/list')
)
));
$msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload;
$result = fwrite($fp, $msg, strlen($msg));
$log->addInfo('Push SAFARI send successfully; result ' . $result . ' Sent message: "' . $payload . '"; ID user: ' . $id_user);
}
fclose($fp);
}
}
Тут всё предельно ясно — вылавливаю все токены пользователя с типом «safari» и шлю уведомления по всем токенам.
Шаг 12: Тестирование
Пробуем запускать сайт, разрешаем уведомления, *смотрим или токен сохранился*, и пробуем отправлять. Пробуйте отправлять с другого браузера, и Safari должен быть закрыт. Если всё сделали правильно — увидите уведомление! Они красивые, красивее чем из Chrome'a, они выглядят как нативные, и приходят чаще, т.к. у меня и Firebase и Safari то вижу что иногда Safari уведомления есть, а вот из Chrome'a не всегда приходят.
Тестирование
Если вдруг после прочтения статьи остались вопросы или непонимания — пишите, буду по возможности отвечать в комментариях. Буду рад конструктивной критике и замечаниям. На супер продвинутого программиста не претендую, поэтому допускаю что где-то я могу быть не совсем прав. Но по этому алгоритму делал уведомления на 2 своих ресурса и везде они работают.
stgunholy
А на IPhone рабоает?
R4_Serega Автор
Нет, на iPhone ни хром ни сафари не умеют пуши :(