11

I have developed a push notification service for my web site. the service worker is:

    'use strict';
    self.addEventListener('push', function (event) {    
    var msg = {};
    if (event.data) {
        msg = event.data.json();
    }    
    let notificationTitle = msg.title;
    const notificationOptions = {
        body: msg.body,//body
        dir:'rtl',//direction
        icon: msg.icon,//image
        data: {
            url: msg.url,//click
        },
    };
    event.waitUntil(
      Promise.all([
        self.registration.showNotification(
          notificationTitle, notificationOptions),      
      ])
    );
    });
    self.addEventListener('notificationclick', function (event) {    
        event.notification.close();

    let clickResponsePromise = Promise.resolve();
    if (event.notification.data && event.notification.data.url) {
        clickResponsePromise = clients.openWindow(event.notification.data.url);
    }
    const fetchOptions = 
        { method: 'post'}; 
    fetch('http://localhost:5333/usrh.ashx?click=true', fetchOptions).
    then(function (response) 
    {
        if (response.status >= 400 && response.status < 500) 
        {         
            throw new Error('Failed to send push message via web push protocol');
        } 
    }).catch((err) => 
    { 
        this.showErrorMessage('Ooops Unable to Send a Click', err); 
    });
});

self.addEventListener('notificationclose', function (event) {
    const fetchOptions = 
        { method: 'post'}; 
    fetch('http://localhost:5333/usrh.ashx?close=true', fetchOptions).
    then(function (response) 
    {
        if (response.status >= 400 && response.status < 500) 
        {         
            throw new Error('Failed to send push message via web push protocol');
        } 
    }).catch((err) => 
    { 
        this.showErrorMessage('Ooops Unable to Send a Click', err); 
    }); 
});
self.addEventListener('pushsubscriptionchange', function () {
    const fetchOptions = {
        method: 'post'
        ,
    };

    fetch('http://localhost:5333/usru.ashx', fetchOptions)
        .then(function (response) {
            if (response.status >= 400 && response.status < 500) {
                console.log('Failed web push response: ', response, response.status);
                throw new Error('Failed to update users.');
            }
        })
        .catch((err) => {
            this.showErrorMessage('Ooops Unable to Send a user', err);
        });
});

I have subscribed the users successfully using the following code:

registerServiceWorker() {
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('http://localhost:5333/service-worker.js')
                .catch((err) => {
                    this.showErrorMessage('Unable to Register SW', 'Sorry this demo requires a service worker to work and it ' + 'failed to install - sorry :(');
                    console.error(err);
                });
        } else {
            this.showErrorMessage('Service Worker Not Supported', 'Sorry this demo requires service worker support in your browser. ' +
                'Please try this demo in Chrome or Firefox Nightly.');
        }
    }

and

class PushClient {
    constructor(subscriptionUpdate, appkeys) {
        this._subscriptionUpdate = subscriptionUpdate;
        this._publicApplicationKey = appkeys;
        if (!('serviceWorker' in navigator)) {
            return;
        }
        if (!('PushManager' in window)) {
            return;
        }
        if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
            return;
        }
        navigator.serviceWorker.ready.then(() => {
            this.setUpPushPermission();
        });
    }
    setUpPushPermission() {
        return navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
            return serviceWorkerRegistration.pushManager.getSubscription();
        })
            .then((subscription) => {
                if (!subscription) {
                    return;
                }
                this._subscriptionUpdate(subscription);
            })
            .catch((err) => {
                console.log('setUpPushPermission() ', err);
            });
    }
    subscribeDevice() {
        return new Promise((resolve, reject) => {
            if (Notification.permission === 'denied') {
                sc(3);
                return reject(new Error('Push messages are blocked.'));
            }
            if (Notification.permission === 'granted') {
                sc(3);
                return resolve();
            }
            if (Notification.permission === 'default') {
                Notification.requestPermission((result) => {
                    if (result === 'denied') {
                        sc(0);
                    } else if (result === 'granted') {
                        sc(1);
                    } else {
                        sc(2);
                    }
                    if (result !== 'granted') {
                        reject(new Error('Bad permission result'));
                    }
                    resolve();
                });
            }
        })
            .then(() => {
                return navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
                    return serviceWorkerRegistration.pushManager.subscribe({
                        userVisibleOnly: true
                        , applicationServerKey: this._publicApplicationKey.publicKey
                    ,
                    });
                })
                    .then((subscription) => {
                        this._subscriptionUpdate(subscription);
                        if (subscription) {
                            this.sendPushMessage(subscription);
                        }
                    })
                    .catch((subscriptionErr) => { });
            })
            .catch(() => { });
    }
    toBase64(arrayBuffer, start, end) {
        start = start || 0;
        end = end || arrayBuffer.byteLength;
        const partialBuffer = new Uint8Array(arrayBuffer.slice(start, end));
        return btoa(String.fromCharCode.apply(null, partialBuffer));
    }
    unsubscribeDevice() {
        navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
            return serviceWorkerRegistration.pushManager.getSubscription();
        })
            .then((pushSubscription) => {
                if (!pushSubscription) {
                    this._subscriptionUpdate(null);
                    return;
                }
                return pushSubscription.unsubscribe()
                    .then(function (successful) {
                        if (!successful) {
                            console.error('We were unable to unregister from push');
                        }
                    });
            })
            .then(() => {
                this._subscriptionUpdate(null);
            })
            .catch((err) => {
                console.error('Error thrown while revoking push notifications. ' + 'Most likely because push was never registered', err);
            });
    }
    sendPushMessage(subscription) {
        let payloadPromise = Promise.resolve(null);
        payloadPromise = JSON.parse(JSON.stringify(subscription));
        const vapidPromise = EncryptionHelperFactory.createVapidAuthHeader(this._publicApplicationKey, subscription.endpoint, 'http://localhost:5333/');
        return Promise.all([payloadPromise, vapidPromise, ])
            .then((results) => {
                const payload = results[0];
                const vapidHeaders = results[1];
                let infoFunction = this.getWebPushInfo;
                infoFunction = () => {
                    return this.getWebPushInfo(subscription, payload, vapidHeaders);
                };
                const requestInfo = infoFunction();
                this.sendRequestToProxyServer(requestInfo);
            });
    }
    getWebPushInfo(subscription, payload, vapidHeaders) {
        let body = null;
        const headers = {};
        headers.TTL = 60;
        if (payload) {
            headers.Encryption = `auth=${payload.keys.auth}`;
            headers['Crypto-Key'] = `p256dh=${payload.keys.p256dh}`;
            headers['Content-Encoding'] = 'aesgcm';
        } else {
            headers['Content-Length'] = 0;
        }
        if (vapidHeaders) {
            headers.Authorization = `WebPush ${vapidHeaders.authorization}`;
            if (headers['Crypto-Key']) {
                headers['Crypto-Key'] = `${headers['Crypto-Key']}; ` + `p256ecdsa=${vapidHeaders.p256ecdsa}`;
            } else {
                headers['Crypto-Key'] = `p256ecdsa=${vapidHeaders.p256ecdsa}`;
            }
        }
        const response = {
            headers: headers
            , endpoint: subscription.endpoint
        ,
        };
        if (body) {
            response.body = body;
        }
        return response;
    }
    sendRequestToProxyServer(requestInfo) {
        const fetchOptions = {
            method: 'post'
        ,
        };
        if (requestInfo.body && requestInfo.body instanceof ArrayBuffer) {
            requestInfo.body = this.toBase64(requestInfo.body);
            fetchOptions.body = requestInfo;
        }
        fetchOptions.body = JSON.stringify(requestInfo);
        fetch('http://localhost:5333/usrh.ashx', fetchOptions)
            .then(function (response) {
                if (response.status >= 400 && response.status < 500) {
                    console.log('Failed web push response: ', response, response.status);
                    throw new Error('Failed to send push message via web push protocol');
                }
            })
            .catch((err) => {
                this.showErrorMessage('Ooops Unable to Send a Push', err);
            });
    }
}

All these codes are in javascript. I can successfully recieve user subscription infromarion on my server like:

Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwcxxxxx  
Crypto-Key: p256dh=BBp90dwDWxxxxc1TfdBjFPqxxxxxwjO9fCip-K_Eebmg=; p256ecdsa=BDd3_hVL9fZi9Yboxxxxxxo
endpoint: https://fcm.googleapis.com/fcm/send/cxxxxxxxxxxxxxxJRorOMHKLQ3gtT7
Encryption: auth=9PzQZ1mut99qxxxxxxxxxxyw== 
Content-Encoding: aesgcm

Also I can successfully send a push to this user using the code bellow in C#:

public static async Task<bool> SendNotificationByte(string endpoint, string[] Keys, byte[] userSecret, byte[] data = null,
                                        int ttl = 0, ushort padding = 0, bool randomisePadding = false, string auth="")
        {
            #region send
            HttpRequestMessage Request = new HttpRequestMessage(HttpMethod.Post, endpoint);                
                Request.Headers.TryAddWithoutValidation("Authorization", auth);
            Request.Headers.Add("TTL", ttl.ToString());
            if (data != null && Keys[1] != null && userSecret != null)
            {
                EncryptionResult Package = EncryptMessage(Decode(Keys[1]), userSecret, data, padding, randomisePadding);
                Request.Content = new ByteArrayContent(Package.Payload);
                Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                Request.Content.Headers.ContentLength = Package.Payload.Length;
                Request.Content.Headers.ContentEncoding.Add("aesgcm");
                Request.Headers.Add("Crypto-Key", "dh=" + Encode(Package.PublicKey)+" ;"+Keys[2]+"="+Keys[3]);
                Request.Headers.Add("Encryption", "salt=" + Encode(Package.Salt));
            }
            using (HttpClient HC = new HttpClient())
            {
                HttpResponseMessage res = await HC.SendAsync(Request).ConfigureAwait(false);
                if (res.StatusCode == HttpStatusCode.Created)
                    return true;
                else return false;
            }
            #endregion
        }

The problem is that after a period of time (about 20 hours or even less), when I want to send a push to this user I got the following errors:

firefox subscription:

{StatusCode: 410, ReasonPhrase: 'Gone', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Access-Control-Allow-Headers: content-encoding,encryption,crypto-key,ttl,encryption-key,content-type,authorization
  Access-Control-Allow-Methods: POST
  Access-Control-Allow-Origin: *
  Access-Control-Expose-Headers: location,www-authenticate
  Connection: keep-alive
  Cache-Control: max-age=86400
  Date: Tue, 21 Feb 2017 08:19:03 GMT
  Server: nginx
  Content-Length: 179
  Content-Type: application/json
}}

chrome subscription:

{StatusCode: 400, ReasonPhrase: 'UnauthorizedRegistration', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  X-Content-Type-Options: nosniff
  X-Frame-Options: SAMEORIGIN
  X-XSS-Protection: 1; mode=block
  Alt-Svc: quic=":443"; ma=2592000; v="35,34"
  Vary: Accept-Encoding
  Transfer-Encoding: chunked
  Accept-Ranges: none
  Cache-Control: max-age=0, private
  Date: Tue, 21 Feb 2017 08:18:35 GMT
  Server: GSE
  Content-Type: text/html; charset=UTF-8
  Expires: Tue, 21 Feb 2017 08:18:35 GMT
}}

I think I missed something, that makes the subscription expires, or have to make the users to resubscribe when their subscription information is changed or expired, but I do not know how?!!

Alireza Mahmoudi
  • 964
  • 8
  • 35

4 Answers4

3

The Problem is solved by sending a push echo notification to the subscribed users to resubscribe them. I have wrote a job in which I send a push echo periodically and resubscribe the users and update their information.

To do so I send an special message called "push echo" using the code bellow:

self.addEventListener('push', function (event) {
lastEventName = 'push';
var msg = {};
if (event.data) {
    msg = event.data.json();
    if (!!msg.isEcho) {
        self.registration.pushManager.getSubscription()
            .then(function (subscription) {
                if (!subscription) {
                } else {
                    subscription.unsubscribe().then(function () {
                        self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array('xxxxxxxxxxxxxxxx') })
                            .then(function (subscription) {
                                resubscription(subscription);
                            });
                    });
                    }
            });
        return;
    }
}
if (!!msg.isEcho)
    return;
let notificationTitle = msg.title;
const notificationOptions = {
    body: msg.body,
    dir: 'rtl',
    icon: msg.icon,
    data: {
        url: msg.url,
        id: msg.id,
        key: msg.key
    },
};
event.waitUntil(
  Promise.all([
    self.registration.showNotification(
      notificationTitle, notificationOptions),
  ])
);

const fetchOptions =
    { method: 'post', mode: 'no-cors' };
fetch('http://example.com', fetchOptions).
    then(function (response) {
        if (response.status >= 400 && response.status < 500) {
            throw new Error('Failed to send push message via web push protocol');
        }
        lastEventName = 'view';
    }).catch((err) => {
        this.showErrorMessage('Ooops Unable to Send a Click', err);
    });
});

In the resubscription method you can unsubscribe and then subscribe the user and update server data.

amirpaia
  • 366
  • 2
  • 9
2

I think the issue is about how you send your applicationServerKey. I just have done an example of what you want to do and I had to send that key encoded with this function:

  function urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/\-/g, '+')
      .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }

so you have to create your subscription object in this way :

  registration.pushManager
    .subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(this._publicApplicationKey.publicKey),
    })

Mainly what I did was follow this tutorial. I have that working example in this github repo. The README file is in Spanish, but I think it can help you.

gabrielperales
  • 6,935
  • 1
  • 20
  • 19
  • 1
    I don't think so! If this was the solution I never could send a push notification. As I mentioned I can send to a user by about 20 hours! After that it seems the subscription is expired! – Alireza Mahmoudi Mar 04 '17 at 10:46
2

I think the problem can be solved by resubscribing the users.

Alireza Mahmoudi
  • 964
  • 8
  • 35
1

Some clues:

The sw needs to be registered and activated, at the time the push event arrives. This mean that you may not clean session, use private browsing mode, clean cache of computer in between.

The push event must come from the same origin.

Walle Cyril
  • 3,087
  • 4
  • 23
  • 55