2 |
Mercurius
3 |
A generic web-push service - github
4 |
14 |
22 |
25 |
26 |
In order to use Mercurius, you need to be online.
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/test/testNotifyMultipleMachines.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var nock = require('nock');
4 | var chai = require('chai');
5 | var testUtils = require('./testUtils.js');
6 |
7 | var should = chai.should();
8 |
9 | describe('mercurius (multiple-machines-)notify', function() {
10 | var token;
11 |
12 | before(function() {
13 | return mercurius.ready
14 | .then(() => testUtils.register(mercurius.app, 'machineZ', 'https://localhost:50005'))
15 | .then(gotToken => token = gotToken)
16 | .then(() => testUtils.register(mercurius.app, 'machineZ2', 'https://localhost:50006', null, token));
17 | });
18 |
19 | it('sends notifications to multiple machines of a registered user', function(done) {
20 | nock('https://localhost:50006')
21 | .post('/')
22 | .reply(201);
23 |
24 | nock('https://localhost:50005')
25 | .post('/')
26 | .reply(201);
27 |
28 | request(mercurius.app)
29 | .post('/notify')
30 | .send({
31 | token: token,
32 | client: 'aClient',
33 | payload: 'hello',
34 | })
35 | .expect(200, done);
36 | });
37 |
38 | it('returns `500` if there\'s a failure in sending a notifications to one of the machines of a registered user', function(done) {
39 | nock('https://localhost:50006')
40 | .post('/')
41 | .reply(201);
42 |
43 | nock('https://localhost:50005')
44 | .post('/')
45 | .reply(404);
46 |
47 | request(mercurius.app)
48 | .post('/notify')
49 | .send({
50 | token: token,
51 | client: 'aClient',
52 | payload: 'hello',
53 | })
54 | .expect(500, done);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/testUtils.js:
--------------------------------------------------------------------------------
1 | var request = require('supertest');
2 | var nock = require('nock');
3 | var assert = require('assert');
4 | var crypto = require('crypto');
5 | var urlBase64 = require('urlsafe-base64');
6 | var redis = require('../redis.js');
7 |
8 | var userCurve = crypto.createECDH('prime256v1');
9 |
10 | var userPublicKey = userCurve.generateKeys();
11 | var userPrivateKey = userCurve.getPrivateKey();
12 |
13 | afterEach(function() {
14 | assert(nock.isDone(), 'All requests have been made. Pending: ' + nock.pendingMocks());
15 | });
16 |
17 | before(function() {
18 | return redis.select(5);
19 | });
20 |
21 | after(function() {
22 | return redis.flushdb();
23 | });
24 |
25 | module.exports = {
26 | register: function(app, machine, endpoint, key, existingToken, doNotify) {
27 | if (typeof key === "undefined" || key === null) {
28 | key = urlBase64.encode(userPublicKey);
29 | }
30 |
31 | return new Promise(function(resolve, reject) {
32 | var token;
33 |
34 | request(app)
35 | .post('/register')
36 | .send({
37 | token: existingToken,
38 | machineId: machine,
39 | endpoint: endpoint,
40 | key: key,
41 | })
42 | .expect(function(res) {
43 | token = res.body.token;
44 | })
45 | .end(function() {
46 | if (!doNotify) {
47 | resolve(token);
48 | return;
49 | }
50 |
51 | return request(app)
52 | .post('/notify')
53 | .send({
54 | token: token,
55 | client: 'test',
56 | payload: 'hello',
57 | })
58 | .end(function() {
59 | resolve(token);
60 | });
61 | });
62 | });
63 | },
64 | };
65 |
--------------------------------------------------------------------------------
/test/testUnregister.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var testUtils = require('./testUtils.js');
4 | var redis = require('../redis.js');
5 | var chai = require('chai');
6 |
7 | var should = chai.should();
8 |
9 | describe('mercurius unregister', function() {
10 | var token;
11 |
12 | before(function() {
13 | return mercurius.ready
14 | .then(() => testUtils.register(mercurius.app, 'machine_1', 'https://android.googleapis.com/gcm/send/someSubscriptionID', ''))
15 | .then(gotToken => token = gotToken);
16 | });
17 |
18 | it('returns 404 if bad token provided', function(done) {
19 | request(mercurius.app)
20 | .post('/unregister')
21 | .send({
22 | token: 'notexisting',
23 | })
24 | .expect(404, done);
25 | });
26 |
27 | it('successfully unregisters users', function(done) {
28 | request(mercurius.app)
29 | .post('/unregister')
30 | .send({
31 | token: token,
32 | })
33 | .expect(200, done);
34 | });
35 |
36 | it('replies with 404 when trying to unregister a non registered user', function(done) {
37 | request(mercurius.app)
38 | .post('/unregister')
39 | .send({
40 | token: token,
41 | })
42 | .expect(404, done);
43 | });
44 |
45 | it('replies with 404 on `notify` after a registration has been removed', function(done) {
46 | request(mercurius.app)
47 | .post('/notify')
48 | .send({
49 | token: token,
50 | client: 'aClient',
51 | payload: 'hello',
52 | })
53 | .expect(404, done);
54 | });
55 |
56 | it('deletes all token related data after a registration has been removed', function() {
57 | return redis.exists(token)
58 | .then(function(exists) {
59 | exists.should.equal(0);
60 | return redis.exists(token + ':clients');
61 | })
62 | .then(function(exists) {
63 | exists.should.equal(0);
64 | return redis.exists('machine_1');
65 | })
66 | .then(function(exists) {
67 | exists.should.equal(0);
68 | return redis.exists('machine_1:clients');
69 | })
70 | .then(function(exists) {
71 | exists.should.equal(0);
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/static/sw-push.js:
--------------------------------------------------------------------------------
1 | importScripts('localforage.min.js');
2 |
3 | self.addEventListener('push', function(event) {
4 | function getPayload() {
5 | if (event.data) {
6 | return Promise.resolve(event.data.json());
7 | } else {
8 | return localforage.getItem('token')
9 | .then(function(token) {
10 | if (!token) {
11 | return null;
12 | }
13 |
14 | return fetch('./getPayload/' + token)
15 | .then(function(response) {
16 | return response.json();
17 | });
18 | });
19 | }
20 | }
21 |
22 | event.waitUntil(
23 | getPayload()
24 | .then(function(data) {
25 | var title = data ? data.title : 'Mercurius';
26 | var body = data ? data.body : 'Notification';
27 | if (title === 'unregister') {
28 | return localforage.removeItem('token')
29 | .then(function() {
30 | self.registration.pushManager.getSubscription()
31 | .then(function(subscription) {
32 | subscription.unsubscribe();
33 | });
34 | });
35 | }
36 | return self.registration.showNotification(title, {
37 | body: body,
38 | });
39 | })
40 | );
41 | });
42 |
43 | self.addEventListener('pushsubscriptionchange', function(event) {
44 | event.waitUntil(
45 | localforage.getItem('token')
46 | .then(function(token) {
47 | if (!token) {
48 | return;
49 | }
50 |
51 | localforage.getItem('machineId')
52 | .then(function(machineId) {
53 | return self.registration.pushManager.subscribe({ userVisibleOnly: true })
54 | .then(function(subscription) {
55 | var key = subscription.getKey ? subscription.getKey('p256dh') : '';
56 |
57 | return fetch('./updateRegistration', {
58 | method: 'post',
59 | headers: {
60 | 'Content-type': 'application/json'
61 | },
62 | body: JSON.stringify({
63 | token: token,
64 | machineId: machineId,
65 | endpoint: subscription.endpoint,
66 | key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '',
67 | }),
68 | });
69 | });
70 | });
71 | })
72 | );
73 | });
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mercurius
2 | Cross-platform web push center. Site allows to proxy a POST request to a full featured push notification. Especially useful for services without web presence, originally came out as an IRSSI notification system.
3 |
4 | Secure, as no data is stored except of generated token, machine id with an endpoint and connected client names.
5 |
6 | We're currently running a [publicly available development server](https://mozcurius.herokuapp.com) under Heroku.
7 |
8 | Check [the post on Mozilla's hacks page](https://hacks.mozilla.org/2015/12/web-push-notifications-from-irssi/) to see a real live usecase.
9 |
10 | [](https://travis-ci.org/marco-c/mercurius)
11 | [](https://david-dm.org/marco-c/mercurius)
12 | [](https://david-dm.org/marco-c/mercurius#info=devDependencies)
13 |
14 | ## API
15 |
16 | ### POST /notify
17 | Send a notification to a user.
18 |
19 | The body of the request is a JSON object containing:
20 | - token;
21 | - client - the client sending the notification (e.g. 'Irssi');
22 | - payload;
23 | - *(optional)* TTL (Time-To-Live of the notification).
24 |
25 | The payload is a JSON object containing the parameters of the nofication to be shown to the user:
26 | - title: the title of the notification;
27 | - body: the body of the notification.
28 |
29 | Example:
30 | ```
31 | {
32 | "token": "aToken",
33 | "client": "someClient",
34 | "payload": {
35 | "title": "IRSSI",
36 | "body": "a message"
37 | }
38 | }
39 | ```
40 |
41 | ## VARIABLES
42 |
43 | Mandatory :
44 |
45 | - REDISCLOUD_URL (Ex. redis://localhost:6379)
46 |
47 | Optional
48 | - GCM_API_KEY : Your Google API Key to send notification to Chrome.
49 | - DISABLE_SSL_REDIRECT : Disable the built-in SSL redirection.
50 |
51 |
52 | ## INSTALL
53 |
54 | Install Redis database and set `REDISCLOUD_URL` environment variable to its
55 | host (`redis://localhost:6379`)
56 |
57 | ## DOCKER
58 |
59 | - clone this repo
60 |
61 | ```
62 | cd mercurius && docker build -t="mercurius" .
63 | docker run --publish 4000:4000 -e REDISCLOUD_URL="redis://localhost:6379" -e GCM_API_KEY="" -e DISABLE_SSL_REDIRECT="1" mercurius
64 | ```
--------------------------------------------------------------------------------
/test/testNotify.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var nock = require('nock');
4 | var crypto = require('crypto');
5 | var urlBase64 = require('urlsafe-base64');
6 | var testUtils = require('./testUtils.js');
7 |
8 | var userCurve = crypto.createECDH('prime256v1');
9 |
10 | var userPublicKey = userCurve.generateKeys();
11 | var userPrivateKey = userCurve.getPrivateKey();
12 |
13 | describe('mercurius notify', function() {
14 | var token;
15 |
16 | before(function() {
17 | return mercurius.ready
18 | .then(() => testUtils.register(mercurius.app, 'machine_1', 'https://localhost:50005', urlBase64.encode(userPublicKey)))
19 | .then(gotToken => token = gotToken);
20 | });
21 |
22 | it('replies with 404 on `notify` when a registration doesn\'t exist', function(done) {
23 | request(mercurius.app)
24 | .post('/notify')
25 | .send({
26 | token: 'token_inesistente',
27 | client: 'test',
28 | payload: 'hello',
29 | })
30 | .expect(404, done);
31 | });
32 |
33 | it('replies with 500 on `notify` when there\'s an error with the push service', function(done) {
34 | nock('https://localhost:50005')
35 | .post('/')
36 | .reply(404);
37 |
38 | request(mercurius.app)
39 | .post('/notify')
40 | .send({
41 | token: token,
42 | client: 'test',
43 | payload: 'hello',
44 | })
45 | .expect(500, done);
46 | });
47 |
48 | it('sends a notification to a registered user', function(done) {
49 | nock('https://localhost:50005')
50 | .post('/')
51 | .reply(201);
52 |
53 | request(mercurius.app)
54 | .post('/notify')
55 | .send({
56 | token: token,
57 | client: 'test',
58 | payload: 'hello',
59 | })
60 | .expect(200, done);
61 | });
62 |
63 | it('sends a notification without payload to a registered user', function(done) {
64 | request(mercurius.app)
65 | .post('/notify')
66 | .send({
67 | token: token,
68 | client: 'test',
69 | })
70 | .expect(500, done);
71 | });
72 |
73 | it('sends a notification without client to a registered user', function(done) {
74 | request(mercurius.app)
75 | .post('/notify')
76 | .send({
77 | token: token,
78 | payload: 'hello',
79 | })
80 | .expect(500, done);
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/testGetPayload.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var nock = require('nock');
4 | var testUtils = require('./testUtils.js');
5 |
6 | describe('mercurius getPayload', function() {
7 | var gcmToken, webPushToken;
8 |
9 | before(function() {
10 | return mercurius.ready
11 | .then(() => testUtils.register(mercurius.app, 'machineX', 'https://android.googleapis.com/gcm/send/someSubscriptionID', ''))
12 | .then(token => gcmToken = token)
13 | .then(() => testUtils.register(mercurius.app, 'machineZ', 'https://localhost:50005'))
14 | .then(token => webPushToken = token);
15 | });
16 |
17 | it('replies with 404 if there\'s no payload available', function(done) {
18 | request(mercurius.app)
19 | .get('/getPayload/' + gcmToken)
20 | .send()
21 | .expect(404, done);
22 | });
23 |
24 | it('successfully sends a notification to a GCM endpoint', function(done) {
25 | nock('https://android.googleapis.com/')
26 | .post('/gcm/send')
27 | .reply(200);
28 |
29 | request(mercurius.app)
30 | .post('/notify')
31 | .send({
32 | token: gcmToken,
33 | client: 'aClient',
34 | payload: 'hello',
35 | })
36 | .expect(200, done);
37 | });
38 |
39 | it('replies with the payload encoded in JSON if there\'s a payload available', function(done) {
40 | request(mercurius.app)
41 | .get('/getPayload/' + gcmToken)
42 | .send()
43 | .expect(200)
44 | .expect('"hello"')
45 | .end(done);
46 | });
47 |
48 | it('replies with the payload encoded in JSON (doesn\'t remove the payload)', function(done) {
49 | request(mercurius.app)
50 | .get('/getPayload/' + gcmToken)
51 | .send()
52 | .expect(200)
53 | .expect('"hello"')
54 | .end(done);
55 | });
56 |
57 | it('sends a notification with payload to a registered user', function(done) {
58 | nock('https://localhost:50005')
59 | .post('/')
60 | .reply(201);
61 |
62 | request(mercurius.app)
63 | .post('/notify')
64 | .send({
65 | token: webPushToken,
66 | client: 'aClient',
67 | payload: 'hello',
68 | })
69 | .expect(200, done);
70 | });
71 |
72 | it('replies with 404 on `getPayload` for Web Push-capable endpoints', function(done) {
73 | request(mercurius.app)
74 | .get('/getPayload/' + webPushToken)
75 | .send()
76 | .expect(404, done);
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/test/testUpdateMeta.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var nock = require('nock');
4 | var redis = require('../redis.js');
5 | var testUtils = require('./testUtils.js');
6 |
7 | describe('mercurius updateMeta', function() {
8 | var token;
9 |
10 | before(function() {
11 | return mercurius.ready
12 | .then(() => testUtils.register(mercurius.app, 'machineX', 'https://localhost:50005'))
13 | .then(gotToken => token = gotToken);
14 | });
15 |
16 | it('updates the metadata successfully', function(done) {
17 | nock('https://localhost:50005')
18 | .post('/')
19 | .reply(201);
20 |
21 | request(mercurius.app)
22 | .post('/updateMeta')
23 | .send({
24 | token: token,
25 | machineId: 'machineX',
26 | name: 'newName',
27 | active: false,
28 | })
29 | .expect(200, function() {
30 | request(mercurius.app)
31 | .post('/notify')
32 | .send({
33 | token: token,
34 | client: 'aClient',
35 | payload: 'hello',
36 | })
37 | .expect(200, done);
38 | });
39 | });
40 |
41 | it('returns 404 if the token doesn\'t exist', function(done) {
42 | request(mercurius.app)
43 | .post('/updateMeta')
44 | .send({
45 | token: 'token_inesistente',
46 | machineId: 'machineX',
47 | name: 'newName',
48 | })
49 | .expect(404, done);
50 | });
51 |
52 | it('returns 404 if the machine doesn\'t exist', function(done) {
53 | request(mercurius.app)
54 | .post('/updateMeta')
55 | .send({
56 | token: token,
57 | machineId: 'machine_inesistente',
58 | name: 'newName',
59 | })
60 | .expect(404, done);
61 | });
62 |
63 | it('returns 404 if the machine is in the token set but doesn\'t exist', function(done) {
64 | redis.sadd(token, 'machine_inesistente')
65 | .then(function() {
66 | request(mercurius.app)
67 | .post('/updateMeta')
68 | .send({
69 | token: token,
70 | machineId: 'machine_inesistente',
71 | name: 'newName',
72 | })
73 | .expect(404, done);
74 | });
75 | });
76 |
77 | it('returns 404 if the machine exists but isn\'t in the token set', function(done) {
78 | request(mercurius.app)
79 | .post('/register')
80 | .send({
81 | machineId: 'machine_3',
82 | endpoint: 'https://localhost:50005',
83 | key: 'key',
84 | })
85 | .end(function() {
86 | request(mercurius.app)
87 | .post('/updateMeta')
88 | .send({
89 | token: token,
90 | machineId: 'machine_3',
91 | name: 'newName',
92 | })
93 | .expect(404, done);
94 | });
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/test/testUpdateRegistration.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var nock = require('nock');
4 | var crypto = require('crypto');
5 | var urlBase64 = require('urlsafe-base64');
6 | var redis = require('../redis.js');
7 | var testUtils = require('./testUtils.js');
8 |
9 | var userCurve = crypto.createECDH('prime256v1');
10 |
11 | var userPublicKey = userCurve.generateKeys();
12 | var userPrivateKey = userCurve.getPrivateKey();
13 |
14 | describe('mercurius updateRegistration', function() {
15 | var token;
16 |
17 | before(function() {
18 | return mercurius.ready
19 | .then(() => testUtils.register(mercurius.app, 'machine_1', 'https://localhost:50005', urlBase64.encode(userPublicKey), token))
20 | .then(gotToken => token = gotToken)
21 | .then(() => testUtils.register(mercurius.app, 'machine_2', 'https://localhost:50006', urlBase64.encode(userPublicKey)));
22 | });
23 |
24 | it('updates the registration successfully on `updateRegistration`', function(done) {
25 | nock('https://localhost:50007')
26 | .post('/')
27 | .reply(201);
28 |
29 | request(mercurius.app)
30 | .post('/updateRegistration')
31 | .send({
32 | token: token,
33 | machineId: 'machine_1',
34 | endpoint: 'https://localhost:50007',
35 | key: urlBase64.encode(userPublicKey),
36 | })
37 | .expect(200, function() {
38 | request(mercurius.app)
39 | .post('/notify')
40 | .send({
41 | token: token,
42 | client: 'aClient',
43 | payload: 'hello',
44 | })
45 | .expect(200, done);
46 | });
47 | });
48 |
49 | it('replies with 404 on `updateRegistration` when a registration doesn\'t exist', function(done) {
50 | request(mercurius.app)
51 | .post('/updateRegistration')
52 | .send({
53 | token: 'token_inesistente',
54 | machineId: 'machine_1',
55 | endpoint: 'endpoint',
56 | key: urlBase64.encode(userPublicKey),
57 | })
58 | .expect(404, done);
59 | });
60 |
61 | it('replies with 404 on `updateRegistration` when a registration doesn\'t exist', function(done) {
62 | redis.sadd(token, 'nonexistingmachine')
63 | .then(function() {
64 | request(mercurius.app)
65 | .post('/updateRegistration')
66 | .send({
67 | token: token,
68 | machineId: 'nonexistingmachine',
69 | endpoint: 'endpoint',
70 | key: urlBase64.encode(userPublicKey),
71 | })
72 | .expect(404, done);
73 | });
74 | });
75 |
76 | it('returns 404 on `updateRegistration` if the machine exists but isn\'t in the token set', function(done) {
77 | request(mercurius.app)
78 | .post('/updateRegistration')
79 | .send({
80 | token: token,
81 | machineId: 'machine_2',
82 | endpoint: 'endpoint',
83 | key: urlBase64.encode(userPublicKey),
84 | name: 'newName',
85 | })
86 | .expect(404, done);
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/test/testRedisErrors.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var redis = require('../redis.js');
4 |
5 | describe('mercurius redis errors', function() {
6 | var origCommands = {};
7 |
8 | before(function() {
9 | ['set', 'get', 'del', 'exists', 'sismember', 'hmset', 'hget', 'smembers',
10 | 'sadd', 'hgetall', 'srem', 'select', 'flushdb']
11 | .map(function(name) {
12 | origCommands[name] = redis[name];
13 | redis[name] = function() {
14 | return Promise.reject(new Error('Fake error.'));
15 | };
16 | });
17 | });
18 |
19 | after(function() {
20 | ['set', 'get', 'del', 'exists', 'sismember', 'hmset', 'hget', 'smembers',
21 | 'sadd', 'hgetall', 'srem', 'select', 'flushdb']
22 | .map(function(name) {
23 | redis[name] = origCommands[name];
24 | });
25 | });
26 |
27 | it('/devices fails with 500', function(done) {
28 | request(mercurius.app)
29 | .get('/devices/aToken')
30 | .expect(500, done);
31 | });
32 |
33 | it('/register fails with 500', function(done) {
34 | request(mercurius.app)
35 | .post('/register')
36 | .send()
37 | .expect(500, done);
38 | });
39 |
40 | it('/unregister fails with 500', function(done) {
41 | request(mercurius.app)
42 | .post('/unregister')
43 | .send({
44 | token: 'aToken',
45 | })
46 | .expect(500, done);
47 | });
48 |
49 | it('/unregisterMachine fails with 500', function(done) {
50 | request(mercurius.app)
51 | .post('/unregisterMachine')
52 | .send({
53 | token: 'aToken',
54 | machineId: 'aMachine',
55 | })
56 | .expect(500, done);
57 | });
58 |
59 | it('/updateRegistration fails with 500', function(done) {
60 | request(mercurius.app)
61 | .post('/updateRegistration')
62 | .send({
63 | token: 'aToken',
64 | machineId: 'aMachine',
65 | })
66 | .expect(500, done);
67 | });
68 |
69 | it('/updateMeta fails with 500', function(done) {
70 | request(mercurius.app)
71 | .post('/updateMeta')
72 | .send({
73 | token: 'aToken',
74 | machineId: 'aMachine',
75 | })
76 | .expect(500, done);
77 | });
78 |
79 | it('/notify fails with 500', function(done) {
80 | request(mercurius.app)
81 | .post('/notify')
82 | .send({
83 | token: 'aToken',
84 | })
85 | .expect(500, done);
86 | });
87 |
88 | it('/getPayload fails with 500', function(done) {
89 | request(mercurius.app)
90 | .get('/getPayload/aToken')
91 | .send()
92 | .expect(500, done);
93 | });
94 |
95 | it('/toggleClientNotification fails with 500', function(done) {
96 | request(mercurius.app)
97 | .post('/toggleClientNotification')
98 | .send({
99 | token: 'aToken',
100 | machineId: 'aMachine',
101 | client: 'aClient',
102 | })
103 | .expect(500, done);
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/test/testRegister.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var crypto = require('crypto');
4 | var should = require('chai').should();
5 | var redis = require('../redis.js');
6 |
7 | describe('mercurius register', function() {
8 | var token;
9 |
10 | before(function() {
11 | return mercurius.ready;
12 | });
13 |
14 | var origRandomBytes = crypto.randomBytes;
15 | afterEach(function() {
16 | crypto.randomBytes = origRandomBytes;
17 | });
18 |
19 | it('replies with 500 if there\'s an error while generating the token', function(done) {
20 | crypto.randomBytes = function(len, cb) {
21 | cb(new Error('Fake error.'));
22 | };
23 |
24 | request(mercurius.app)
25 | .post('/register')
26 | .send({
27 | machineId: 'machineX',
28 | endpoint: 'http://localhost:50005',
29 | key: '',
30 | })
31 | .expect(500, done);
32 | });
33 |
34 | it('successfully registers users', function(done) {
35 | request(mercurius.app)
36 | .post('/register')
37 | .send({
38 | machineId: 'machine',
39 | endpoint: 'https://localhost:50008',
40 | key: '',
41 | })
42 | .expect(200)
43 | .expect(function(res) {
44 | res.body.should.be.an('object');
45 | res.body.machines.should.be.an('object');
46 | res.body.token.should.have.length(16);
47 | token = res.body.token;
48 | })
49 | .end(done);
50 | });
51 |
52 | it('successfully registers additional machine', function(done) {
53 | request(mercurius.app)
54 | .post('/register')
55 | .send({
56 | token: token,
57 | machineId: 'machine2',
58 | endpoint: 'endpoint',
59 | key: '',
60 | })
61 | .expect(200)
62 | .expect(function(res) {
63 | res.body.token.should.equal(token);
64 | res.body.machines.machine.endpoint.should.equal('https://localhost:50008');
65 | res.body.machines.machine2.endpoint.should.equal('endpoint');
66 | })
67 | .end(done);
68 | });
69 |
70 | it('successfully registers a machine even if it exists', function(done) {
71 | redis.smembers(token)
72 | .then(function(machines) {
73 | var startlength = machines.length;
74 | request(mercurius.app)
75 | .post('/register')
76 | .send({
77 | token: token,
78 | machineId: 'machine2',
79 | endpoint: 'endpoint2',
80 | key: '',
81 | })
82 | .expect(200)
83 | .expect(function(res) {
84 | res.body.token.should.equal(token);
85 | res.body.machines.machine2.endpoint.should.equal('endpoint2');
86 | Object.keys(res.body.machines).should.have.length(startlength);
87 | })
88 | .end(done);
89 | });
90 | });
91 |
92 | it('returns 404 if bad token provided', function(done) {
93 | request(mercurius.app)
94 | .post('/register')
95 | .send({
96 | token: 'notexisting',
97 | machineId: 'machine of a not existing token',
98 | endpoint: 'endpoint',
99 | key: '',
100 | })
101 | .expect(404, done);
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/static/bwip-support.js:
--------------------------------------------------------------------------------
1 | // Original source is:
2 | // Copyright (c) 2011-2015 Mark Warren
3 | //
4 | // See the LICENSE file in the node_modules/bwip-js directory
5 | // for the extended copyright notice.
6 |
7 | // Dynamically load the encoders.
8 | BWIPJS.load = function(path) {
9 | var script = document.createElement('script');
10 | script.type = 'text/javascript';
11 | script.src = path.replace('bwipp/', '');
12 | document.head.appendChild(script);
13 | };
14 |
15 | BWIPJS.print = function(s) {
16 | };
17 |
18 | // Encapsulate the bitmap interface
19 | // bgcolor is optional, defaults to #fff.
20 | function Bitmap(bgcolor) {
21 | var clr = [0, 0, 0];
22 | var pts = [];
23 | var minx = 0; // min-x
24 | var miny = 0; // min-y
25 | var maxx = 0; // max-x
26 | var maxy = 0; // max-y
27 |
28 | this.color = function(r, g, b) {
29 | clr = [r, g, b];
30 | };
31 |
32 | // a is the alpha-level of the pixel [0 .. 255]
33 | this.set = function(x,y,a) {
34 | x = Math.floor(x);
35 | y = Math.floor(y);
36 |
37 | pts.push([ x, y, clr, a ]);
38 |
39 | if (minx > x) {
40 | minx = x;
41 | }
42 |
43 | if (miny > y) {
44 | miny = y;
45 | }
46 |
47 | if (maxx < x) {
48 | maxx = x;
49 | }
50 |
51 | if (maxy < y) {
52 | maxy = y;
53 | }
54 | };
55 |
56 | this.show = function(cvs, rot) {
57 | if (pts.length === 0) {
58 | cvs.width = 32;
59 | cvs.height = 32;
60 | cvs.getContext('2d').clearRect(0, 0, cvs.width, cvs.height);
61 | cvs.style.display = 'block';
62 | return;
63 | }
64 |
65 | var w, h;
66 | if (rot === 'R' || rot === 'L') {
67 | h = maxx - minx + 1;
68 | w = maxy - miny + 1;
69 | } else {
70 | w = maxx - minx + 1;
71 | h = maxy - miny + 1;
72 | }
73 |
74 | cvs.width = w;
75 | cvs.height = h;
76 |
77 | var ctx = cvs.getContext('2d');
78 | ctx.fillStyle = bgcolor || '#fff';
79 | ctx.fillRect(0, 0, cvs.width, cvs.height);
80 | ctx.fillStyle = '#000';
81 |
82 | var id = ctx.getImageData(0, 0, cvs.width, cvs.height);
83 | var dat = id.data;
84 |
85 | for (var i = 0; i < pts.length; i++) {
86 | // PostScript builds bottom-up, we build top-down.
87 | var x = pts[i][0] - minx;
88 | var y = pts[i][1] - miny;
89 | var c = pts[i][2];
90 | var a = pts[i][3] / 255;
91 |
92 | if (rot === 'N') {
93 | y = h - y - 1; // Invert y
94 | } else if (rot === 'I') {
95 | x = w - x - 1; // Invert x
96 | } else {
97 | y = w - y; // Invert y
98 |
99 | var t;
100 | if (rot === 'L') {
101 | t = y;
102 | y = h - x - 1;
103 | x = t - 1;
104 | } else {
105 | t = x;
106 | x = w - y;
107 | y = t;
108 | }
109 | }
110 |
111 | var idx = (y * id.width + x) * 4;
112 | dat[idx+0] = (dat[idx+0] * (1 - a) + c[0] * a) | 0;
113 | dat[idx+1] = (dat[idx+1] * (1 - a) + c[1] * a) | 0;
114 | dat[idx+2] = (dat[idx+2] * (1 - a) + c[2] * a) | 0;
115 | dat[idx+3] = 255;
116 | }
117 |
118 | ctx.putImageData(id, 0, 0);
119 | cvs.style.display = 'block';
120 | };
121 | }
122 |
--------------------------------------------------------------------------------
/static/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Foundation Icons v 3.0
3 | * Made by ZURB 2013 http://zurb.com/playground/foundation-icon-fonts-3
4 | * MIT License
5 | */
6 |
7 | @font-face {
8 | font-family: "foundation-icons";
9 | src: url("https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.eot");
10 | src: url("https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.eot?#iefix") format("embedded-opentype"),
11 | url("https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.woff") format("woff"),
12 | url("https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.ttf") format("truetype"),
13 | url("https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.svg#fontcustom") format("svg");
14 | font-weight: normal;
15 | font-style: normal;
16 | }
17 |
18 | /*--------------------- Typography ----------------------------*/
19 |
20 | @font-face {
21 | font-family: 'Open Sans';
22 | src: url('https://mozorg.cdn.mozilla.net/media/fonts/OpenSans-Light-webfont.woff') format('woff');
23 | font-weight: 300;
24 | font-style: normal;
25 | }
26 |
27 | @font-face {
28 | font-family: 'Open Sans';
29 | src: url('https://mozorg.cdn.mozilla.net/media/fonts/OpenSans-Semibold-webfont.woff') format('woff');
30 | font-weight: 600;
31 | font-style: normal;
32 | }
33 |
34 | @font-face {
35 | font-family: 'Open Sans';
36 | src: url('https://mozorg.cdn.mozilla.net/media/fonts/OpenSans-LightItalic-webfont.woff') format('woff');
37 | font-weight: 300;
38 | font-style: italic;
39 | }
40 |
41 | @font-face {
42 | font-family: 'Open Sans';
43 | src: url('https://mozorg.cdn.mozilla.net/media/fonts/OpenSans-Regular-webfont.woff') format('woff');
44 | font-weight: 400;
45 | font-style: normal;
46 | }
47 |
48 | @font-face {
49 | font-family: 'Open Sans';
50 | src: url('https://mozorg.cdn.mozilla.net/media/fonts/OpenSans-Bold-webfont.woff') format('woff');
51 | font-weight: bold;
52 | font-style: normal;
53 | }
54 |
55 | @font-face {
56 | font-family: 'Open Sans';
57 | src: url('https://mozorg.cdn.mozilla.net/media/fonts/OpenSans-Italic-webfont.woff') format('woff');
58 | font-weight: normal;
59 | font-style: italic;
60 | }
61 |
62 | /*--------------------- Layout ----------------------------*/
63 | html {
64 | height: 100%;
65 | }
66 |
67 | a {
68 | color: #a87aad;
69 | }
70 |
71 | body {
72 | font-family: "Open Sans","Clear Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
73 | font-size: 14px;
74 | line-height: 18px;
75 | color: #30404f;
76 | margin: 2em; padding: 0;
77 | height:100%;
78 | }
79 |
80 | .tip {
81 | background: #fffea1;
82 | padding: 10px;
83 | border-left: #fc0;
84 | }
85 |
86 | h1 {
87 | color: #78ba91;
88 | line-height: 1;
89 | }
90 |
91 | h2 {
92 | margin: 40px 0 0 0;
93 | color: #c17878;
94 | }
95 |
96 | h3 a {
97 | color: #6b7b95;
98 | }
99 |
100 | #token {
101 | font-size: 14px; line-height: 16px;
102 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace;
103 | margin: 0; padding: 0;
104 | font-weight: bold;
105 | }
106 |
107 | #tokenInput, #tokenLabel {
108 | display: none;
109 | }
110 |
111 | #machines tr.currentMachine td.machine {
112 | font-weight: bold;
113 | padding-left: 2em;
114 | }
115 |
116 | #machines td {
117 | text-align: center;
118 | }
119 |
120 | #machines td.on {
121 | background: #afa;
122 | }
123 |
124 | #machines td.machine {
125 | text-align: left;
126 | min-width: 200px;
127 | }
128 |
--------------------------------------------------------------------------------
/test/testClient.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var chai = require('chai').should();
4 | var nock = require('nock');
5 | var testUtils = require('./testUtils.js');
6 |
7 | describe('mercurius clients support', function() {
8 | var token;
9 |
10 | before(function() {
11 | return mercurius.ready
12 | .then(() => testUtils.register(mercurius.app, 'machineXZ', 'https://localhost:50005'))
13 | .then(gotToken => token = gotToken);
14 | });
15 |
16 | it('sends notifications to a machine from a client', function(done) {
17 | nock('https://localhost:50005')
18 | .post('/')
19 | .reply(201);
20 |
21 | request(mercurius.app)
22 | .post('/notify')
23 | .send({
24 | token: token,
25 | client: 'clientXZ',
26 | payload: 'hello',
27 | })
28 | .expect(200, done);
29 | });
30 |
31 | it('provides information about the client', function(done) {
32 | request(mercurius.app)
33 | .get('/devices/' + token)
34 | .send()
35 | .expect(200)
36 | .expect(function(res) {
37 | res.body.clients.should.have.length(1);
38 | res.body.clients.indexOf('clientXZ').should.equal(0);
39 | res.body.machines.machineXZ.clients.clientXZ.should.equal('1');
40 | })
41 | .end(done);
42 | });
43 |
44 | it('toggles (disables) notifications for a client', function(done) {
45 | request(mercurius.app)
46 | .post('/toggleClientNotification')
47 | .send({
48 | token: token,
49 | machineId: 'machineXZ',
50 | client: 'clientXZ',
51 | payload: 'hello',
52 | })
53 | .expect(200)
54 | .expect(function(res) {
55 | res.body.clients.should.have.length(1);
56 | res.body.clients.indexOf('clientXZ').should.equal(0);
57 | res.body.machines.machineXZ.clients.clientXZ.should.equal('0');
58 | })
59 | .end(done);
60 | });
61 |
62 | it('doesn\'t send notifications to a machine from a disabled client', function(done) {
63 | var req = nock('https://localhost:50005')
64 | .post('/')
65 | .reply(201);
66 |
67 | request(mercurius.app)
68 | .post('/notify')
69 | .send({
70 | token: token,
71 | client: 'clientXZ',
72 | payload: 'hello',
73 | })
74 | .expect(200, function() {
75 | req.isDone().should.equal(false, 'Notification isn\'t sent do a disabled client');
76 | nock.cleanAll();
77 | done();
78 | });
79 | });
80 |
81 | it('toggles (enables) notifications for a client', function(done) {
82 | request(mercurius.app)
83 | .post('/toggleClientNotification')
84 | .send({
85 | token: token,
86 | machineId: 'machineXZ',
87 | client: 'clientXZ',
88 | })
89 | .expect(200)
90 | .expect(function(res) {
91 | res.body.clients.should.have.length(1);
92 | res.body.clients.indexOf('clientXZ').should.equal(0);
93 | res.body.machines.machineXZ.clients.clientXZ.should.equal('1');
94 | })
95 | .end(done);
96 | });
97 |
98 | it('sends notifications to a machine from a re-enabled client', function(done) {
99 | nock('https://localhost:50005')
100 | .post('/')
101 | .reply(201);
102 |
103 | request(mercurius.app)
104 | .post('/notify')
105 | .send({
106 | token: token,
107 | client: 'clientXZ',
108 | payload: 'hello',
109 | })
110 | .expect(200, done);
111 | });
112 |
113 | it('fails to toggle notifications for a client of a non-existing token', function(done) {
114 | request(mercurius.app)
115 | .post('/toggleClientNotification')
116 | .send({
117 | token: 'notexisting',
118 | machineId: 'machineXZ',
119 | client: 'clientXZ',
120 | })
121 | .expect(404, done);
122 | });
123 |
124 | it('fails to toggle notifications for a client of an unregistered token', function(done) {
125 | request(mercurius.app)
126 | .post('/unregister')
127 | .send({
128 | token: token,
129 | })
130 | .expect(200, function() {
131 | request(mercurius.app)
132 | .post('/toggleClientNotification')
133 | .send({
134 | token: token,
135 | machineId: 'machineXZ',
136 | client: 'clientXZ',
137 | })
138 | .expect(404, done);
139 | });
140 | });
141 | });
142 |
--------------------------------------------------------------------------------
/test/testUnregisterMachine.js:
--------------------------------------------------------------------------------
1 | var mercurius = require('../index.js');
2 | var request = require('supertest');
3 | var nock = require('nock');
4 | var testUtils = require('./testUtils.js');
5 | var redis = require('../redis.js');
6 | var chai = require('chai').should();
7 |
8 | describe('mercurius unregisterMachine', function() {
9 | var token;
10 |
11 | before(function() {
12 | nock('https://localhost:50005')
13 | .post('/')
14 | .reply(201);
15 |
16 | nock('https://android.googleapis.com/')
17 | .post('/gcm/send')
18 | .reply(200);
19 |
20 | return mercurius.ready
21 | .then(() => testUtils.register(mercurius.app, 'machine_1', 'https://android.googleapis.com/gcm/send/someSubscriptionID', ''))
22 | .then(gotToken => token = gotToken)
23 | .then(() => testUtils.register(mercurius.app, 'machine_2', 'https://localhost:50005', null, token, true));
24 | });
25 |
26 | it('created the machines properly', function() {
27 | return redis.exists(token)
28 | .then(function(exists) {
29 | exists.should.equal(1);
30 | return redis.exists(token + ':clients');
31 | })
32 | .then(function(exists) {
33 | exists.should.equal(1);
34 | return redis.exists('machine_2:clients');
35 | })
36 | .then(function(exists) {
37 | exists.should.equal(1);
38 | });
39 | });
40 |
41 | it('replies with 404 when trying to unregister a non existing token', function(done) {
42 | request(mercurius.app)
43 | .post('/unregisterMachine')
44 | .send({
45 | token: 'nonexistingtoken',
46 | machineId: 'machine',
47 | })
48 | .expect(404, done);
49 | });
50 |
51 | it('replies with 404 when trying to unregister a non registered machine', function(done) {
52 | request(mercurius.app)
53 | .post('/unregisterMachine')
54 | .send({
55 | token: token,
56 | machineId: 'non-existing-machine',
57 | })
58 | .expect(404, done);
59 | });
60 |
61 | it('unregisterMachine with an unexisting token/machine doesn\'t affect `getPayload`', function(done) {
62 | request(mercurius.app)
63 | .get('/getPayload/' + token)
64 | .send()
65 | .expect(200)
66 | .expect('"hello"')
67 | .end(done);
68 | });
69 |
70 | it('successfully unregisters a GCM endpoint', function(done) {
71 | nock('https://android.googleapis.com/')
72 | .post('/gcm/send')
73 | .reply(200);
74 |
75 | request(mercurius.app)
76 | .post('/unregisterMachine')
77 | .send({
78 | token: token,
79 | machineId: 'machine_1',
80 | })
81 | .expect(200, done);
82 | });
83 |
84 | it('doesn\'t send notifications to unregistered machines but sends them to machines of the same token set of an unregistered machine', function(done) {
85 | var req1 = nock('https://android.googleapis.com/')
86 | .post('/gcm/send')
87 | .reply(200);
88 |
89 | var req2 = nock('https://localhost:50005')
90 | .post('/')
91 | .reply(201);
92 |
93 | request(mercurius.app)
94 | .post('/notify')
95 | .send({
96 | token: token,
97 | client: 'aClient',
98 | payload: 'hello',
99 | })
100 | .expect(200, function() {
101 | req1.isDone().should.equal(false, 'Notification isn\'t sent do an unregistered machine');
102 | req2.isDone().should.equal(true, 'Notification is sent do a machine in the same token set of an unregistered machine');
103 | nock.cleanAll();
104 | done();
105 | });
106 | });
107 |
108 | it('replies with the payload encoded in JSON on `getPayload` if there\'s a payload available', function(done) {
109 | request(mercurius.app)
110 | .get('/getPayload/' + token)
111 | .send()
112 | .expect(200)
113 | .expect(JSON.stringify({
114 | title: 'unregister',
115 | body: 'called from unregisterMachine'
116 | }))
117 | .end(done);
118 | });
119 |
120 | it('hasn\'t deleted a token after unregistering the first machine', function() {
121 | return redis.exists(token)
122 | .then(function(exists) {
123 | exists.should.equal(1);
124 | });
125 | });
126 |
127 | it('successfully unregisters a machine', function(done) {
128 | nock('https://localhost:50005')
129 | .post('/')
130 | .reply(201);
131 |
132 | request(mercurius.app)
133 | .post('/unregisterMachine')
134 | .send({
135 | token: token,
136 | machineId: 'machine_2',
137 | })
138 | .expect(200, done);
139 | });
140 |
141 | it('deletes machine\'s clients object after removing the machine', function() {
142 | return redis.exists('machine_1:clients')
143 | .then(function(exists) {
144 | exists.should.equal(0);
145 | });
146 | });
147 |
148 | it('deletes token after unregistering the last machine', function() {
149 | return redis.exists(token)
150 | .then(function(exists) {
151 | exists.should.equal(0);
152 | return redis.exists(token + ':clients');
153 | })
154 | .then(function(exists) {
155 | exists.should.equal(0);
156 | });
157 | });
158 | });
159 |
--------------------------------------------------------------------------------
/static/index.js:
--------------------------------------------------------------------------------
1 | var machineId;
2 | var machineName;
3 |
4 | // all static DOM elements
5 | var domShowTokenInput = document.getElementById('showTokenInput');
6 | var domTokenInput = document.getElementById('tokenInput');
7 | var domTokenLabel = document.getElementById('tokenLabel');
8 | var domTokenInteractive = document.getElementById('interactive');
9 | var domMachineName = document.getElementById('machineName');
10 | var domToken = document.getElementById('token');
11 | var domTokenBarcode = document.getElementById('tokenBarcode');
12 | var domRegister = document.getElementById('register');
13 | var domUnregister = document.getElementById('unregister');
14 | var domMachines = document.getElementById('machines');
15 | var domShowMachineName = document.getElementById('showMachineName');
16 |
17 | domShowTokenInput.onclick = function() {
18 | domTokenInput.style.display = 'block';
19 | domTokenLabel.style.display = 'block';
20 | this.style.display = 'none';
21 |
22 | Quagga.init({}, function(err) {
23 | if (err) {
24 | console.error(err);
25 | return;
26 | }
27 |
28 | Quagga.start();
29 | });
30 |
31 | Quagga.onDetected(function(result) {
32 | domTokenInput.value = result.codeResult.code;
33 | Quagga.stop();
34 | domTokenInteractive.style.display = 'none';
35 | window.alert('Token detected!');
36 | });
37 | };
38 |
39 | domMachineName.placeholder = window.navigator.userAgent;
40 |
41 | function drawBarcode(text) {
42 | var bw = new BWIPJS();
43 |
44 | bw.bitmap(new Bitmap());
45 |
46 | bw.scale(2, 2);
47 |
48 | bw.push(text);
49 | bw.push({
50 | includetext: true,
51 | });
52 |
53 | bw.call('code128', function(e) {
54 | if (e) {
55 | if (typeof e === 'string') {
56 | console.error(e);
57 | } else if (e.stack) {
58 | console.error(e.message + '\r\n' + e.stack);
59 | } else {
60 | var s = '';
61 | if (e.fileName) {
62 | s += e.fileName + ' ';
63 | }
64 | if (e.lineNumber) {
65 | s += '[line ' + e.lineNumber + '] ';
66 | }
67 | console.error(s + (s ? ': ' : '') + e.message);
68 | }
69 | } else {
70 | bw.bitmap().show(domTokenBarcode, 'N');
71 | }
72 | });
73 | }
74 |
75 | function register() {
76 | localforage.getItem('token')
77 | .then(function(token) {
78 | if (token) {
79 | return;
80 | }
81 |
82 | navigator.serviceWorker.ready
83 | .then(function(registration) {
84 | return registration.pushManager.getSubscription()
85 | .then(function(subscription) {
86 | if (subscription) {
87 | return subscription;
88 | }
89 |
90 | return registration.pushManager.subscribe({ userVisibleOnly: true });
91 | });
92 | })
93 | .then(function(subscription) {
94 | var key = subscription.getKey ? subscription.getKey('p256dh') : '';
95 |
96 | machineName = domMachineName.value;
97 |
98 | fetch('./register', {
99 | method: 'post',
100 | headers: {
101 | 'Content-type': 'application/json'
102 | },
103 | body: JSON.stringify({
104 | endpoint: subscription.endpoint,
105 | key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '',
106 | machineId: machineId,
107 | token: domTokenInput.value,
108 | name: machineName
109 | }),
110 | })
111 | .then(function(response) {
112 | response.json()
113 | .then(function(body) {
114 | var token = body.token;
115 | if (response.ok) {
116 | localforage.setItem('token', token);
117 | localforage.setItem('machineName', machineName);
118 | domShowMachineName.textContent = machineName || machineId;
119 | domToken.textContent = token;
120 | drawBarcode(token);
121 | showSection('unregistrationForm');
122 | showMachines(token, body.machines, body.clients);
123 | } else {
124 | alert('Error: ' + token);
125 | }
126 | });
127 | });
128 | });
129 | });
130 | }
131 |
132 | domRegister.onclick = register;
133 |
134 | var sections = ['registrationForm', 'unregistrationForm', 'unsupported', 'offline'];
135 | function showSection(section) {
136 | for (var index = 0; index < sections.length; index++) {
137 | if (sections[index] === section) {
138 | document.getElementById(section).style.display = 'block';
139 | } else {
140 | document.getElementById(sections[index]).style.display = 'none';
141 | }
142 | }
143 | }
144 |
145 | function forceUnregister() {
146 | domToken.textContent = '';
147 | domTokenBarcode.style.display = 'none';
148 | showSection('registrationForm');
149 | localforage.removeItem('token');
150 | }
151 |
152 | domUnregister.onclick = function() {
153 | unregisterMachine(machineId)
154 | .then(forceUnregister);
155 | };
156 |
157 | function unregisterMachine(mId) {
158 | return localforage.getItem('token')
159 | .then(function(token) {
160 | return fetch('./unregisterMachine', {
161 | method: 'post',
162 | headers: {
163 | 'Content-type': 'application/json'
164 | },
165 | body: JSON.stringify({
166 | token: token,
167 | machineId: mId
168 | }),
169 | });
170 | });
171 | }
172 |
173 | // generate a random string (default: 40)
174 | function makeId(length) {
175 | var arr = new Uint8Array((length || 40) / 2);
176 | window.crypto.getRandomValues(arr);
177 | return [].map.call(arr, function(n) { return n.toString(16); }).join("");
178 | }
179 |
180 | // create DOM Element for a device
181 | function showMachine(token, mId, device, clients) {
182 | var tr = document.createElement('tr');
183 | if (mId === machineId) {
184 | tr.classList.add('currentMachine');
185 | }
186 | var td = document.createElement('td');
187 | var a = document.createElement('a');
188 | td.classList.add('machine');
189 | // unregister current machine only by using a button
190 | if (mId !== machineId) {
191 | a.textContent = '[x]';
192 | a.onclick = function() {
193 | unregisterMachine(mId)
194 | .then(function(response) {
195 | response.json()
196 | .then(function(body) {
197 | showMachines(token, body.machines, body.clients);
198 | });
199 | });
200 | };
201 | td.appendChild(a);
202 | }
203 | td.appendChild(document.createTextNode(' ' + (device.name || mId)));
204 | tr.appendChild(td);
205 | function toggleOnclick(ev) {
206 | toggleMachineClientNotification(token, mId, ev.target.dataset.client)
207 | .then(function(response) {
208 | response.json()
209 | .then(function(body) {
210 | showMachines(token, body.machines, body.clients);
211 | });
212 | });
213 | }
214 | for (var i = 0; i < clients.length; i++) {
215 | td = document.createElement('td');
216 | td.dataset.client = clients[i];
217 | td.onclick = toggleOnclick;
218 | if (device.clients && device.clients[clients[i]] === '0') {
219 | // machine is NOT receiving notifications from this client
220 | td.classList.add('off');
221 | td.textContent = 'off';
222 | } else {
223 | td.classList.add('on');
224 | td.textContent = 'on';
225 | }
226 | tr.appendChild(td);
227 | }
228 | domMachines.appendChild(tr);
229 | }
230 |
231 | // clean machine list and call showMachine on each machine from the list
232 | function showMachines(token, deviceList, clientsList) {
233 | // delete everything in the list
234 | domMachines.innerHTML = "";
235 | // create the list
236 | for (var machineId in deviceList) {
237 | showMachine(token, machineId, deviceList[machineId], clientsList);
238 | }
239 | // create header
240 | var head = document.createElement('thead');
241 | var tr = document.createElement('tr');
242 | tr.appendChild(document.createElement('td'));
243 | for (var i = 0; i < clientsList.length; i++) {
244 | var td = document.createElement('td');
245 | td.textContent = clientsList[i];
246 | tr.appendChild(td);
247 | }
248 | head.appendChild(tr);
249 | domMachines.appendChild(head);
250 | }
251 |
252 | // load machines from the server
253 | function getMachines(token) {
254 | fetch('/devices/' + token)
255 | .then(function(response) {
256 | response.json()
257 | .then(function(body) {
258 | showMachines(token, body.machines, body.clients);
259 | });
260 | });
261 | }
262 |
263 | // toggle client notification per machine
264 | function toggleMachineClientNotification(token, machineId, client) {
265 | return localforage.getItem('token')
266 | .then(function(token) {
267 | return fetch('./toggleClientNotification', {
268 | method: 'post',
269 | headers: {
270 | 'Content-type': 'application/json'
271 | },
272 | body: JSON.stringify({
273 | token: token,
274 | machineId: machineId,
275 | client: client
276 | }),
277 | });
278 | });
279 | }
280 |
281 | function updateFound() {
282 | var installingWorker = this.installing;
283 |
284 | // Wait for the new service worker to be installed before prompting to update.
285 | installingWorker.addEventListener('statechange', function() {
286 | switch (installingWorker.state) {
287 | case 'installed':
288 | // Only show the prompt if there is currently a controller so it is not
289 | // shown on first load.
290 | if (navigator.serviceWorker.controller &&
291 | window.confirm('An updated version of this page is available, would you like to update?')) {
292 | window.location.reload();
293 | return;
294 | }
295 | break;
296 |
297 | case 'redundant':
298 | console.error('The installing service worker became redundant.');
299 | break;
300 | }
301 | });
302 | }
303 |
304 | if (navigator.serviceWorker) {
305 | navigator.serviceWorker.register('offline-worker.js')
306 | .then(function(registration) {
307 | registration.addEventListener('updatefound', updateFound);
308 | });
309 | } else {
310 | showSection('unsupported');
311 | }
312 |
313 | window.addEventListener('online', function() {
314 | localforage.getItem('token')
315 | .then(function(token) {
316 | if (token) {
317 | showSection('unregistrationForm');
318 | } else {
319 | showSection('registrationForm');
320 | }
321 | });
322 | });
323 |
324 | window.addEventListener('offline', function() {
325 | showSection('offline');
326 | });
327 |
328 | window.onload = function() {
329 | if (!navigator.serviceWorker) {
330 | return;
331 | }
332 |
333 | localforage.getItem('machineId')
334 | .then(function(id) {
335 | if (id) {
336 | machineId = id;
337 | } else {
338 | machineId = makeId(20);
339 | localforage.setItem('machineId', machineId);
340 | }
341 | })
342 | .then(function() {
343 | return localforage.getItem('token')
344 | .then(function(token) {
345 | if (token) {
346 | showSection('unregistrationForm');
347 | domToken.textContent = token;
348 | drawBarcode(token);
349 | return localforage.getItem('machineName')
350 | .then(function(mName) {
351 | machineName = mName;
352 | domShowMachineName.textContent = machineName || machineId;
353 | getMachines(token);
354 | });
355 | } else {
356 | showSection('registrationForm');
357 | }
358 | });
359 | })
360 | .then(function() {
361 | if (!navigator.onLine) {
362 | showSection('offline');
363 | }
364 | });
365 | };
366 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | var fs = require('fs');
4 | var crypto = require('crypto');
5 | var express = require('express');
6 | var bodyParser = require('body-parser');
7 | var redis = require('./redis.js');
8 | var webPush = require('web-push');
9 | var exphbs = require('express-handlebars');
10 |
11 | var app = express();
12 | app.engine('handlebars', exphbs({defaultLayout: 'main'}));
13 | app.set('view engine', 'handlebars');
14 |
15 | app.use(bodyParser.json());
16 |
17 | app.use(function(req, res, next) {
18 | var host = req.get('Host');
19 |
20 | if (!process.env.DISABLE_SSL_REDIRECT && host.indexOf('localhost') !== 0 && host.indexOf('127.0.0.1') !== 0) {
21 | // https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security
22 | res.header('Strict-Transport-Security', 'max-age=15768000');
23 | // https://github.com/rangle/force-ssl-heroku/blob/master/force-ssl-heroku.js
24 | if (req.headers['x-forwarded-proto'] !== 'https') {
25 | return res.redirect('https://' + host + req.url);
26 | }
27 | }
28 |
29 | return next();
30 | });
31 |
32 | app.use(function(req, res, next) {
33 | // http://enable-cors.org/server_expressjs.html
34 | res.header('Access-Control-Allow-Origin', '*');
35 | res.header('Access-Control-Allow-Headers', 'Origin, Content-Type, Accept');
36 | next();
37 | });
38 |
39 | if (!fs.existsSync('./dist')) {
40 | throw new Error('Missing `dist` folder, execute `npm run build` first.');
41 | }
42 | app.use(express.static('./dist'));
43 |
44 | // load current data for
45 | app.get('/', function(req, res) {
46 | res.render('index');
47 | });
48 |
49 | // get machines for the token and send them along with the token
50 | function sendMachines(res, token) {
51 | var machines = {};
52 | var clients;
53 |
54 | return redis.smembers(token)
55 | .then(function(ids) {
56 | var promises = ids.map(function(machineId) {
57 | return redis.hgetall(machineId)
58 | .then(function(machine) {
59 | return redis.hgetall(machineId + ':clients')
60 | .then(function(machineClients) {
61 | machine.clients = machineClients;
62 | machines[machineId] = machine;
63 | });
64 | });
65 | });
66 |
67 | promises.push(
68 | redis.smembers(token + ':clients')
69 | .then(function(sclients) {
70 | clients = sclients;
71 | }));
72 |
73 | return Promise.all(promises);
74 | })
75 | .then(() => res.send({
76 | token: token,
77 | machines: machines,
78 | clients: clients,
79 | }));
80 | }
81 |
82 | function randomBytes(len) {
83 | return new Promise(function(resolve, reject) {
84 | crypto.randomBytes(len, function(err, res) {
85 | if (err) {
86 | reject(err);
87 | } else {
88 | resolve(res);
89 | }
90 | });
91 | });
92 | }
93 |
94 | function handleError(res, err) {
95 | if (err && err.message === "Not Found") {
96 | res.sendStatus(404);
97 | } else {
98 | console.error('ERROR: ', err);
99 | res.sendStatus(500);
100 | }
101 | }
102 |
103 | app.get('/devices/:token', function(req, res) {
104 | var token = req.params.token;
105 | redis.exists(token)
106 | .then(function(exists) {
107 | if (!exists) {
108 | throw new Error('Not Found');
109 | }
110 | return sendMachines(res, token);
111 | })
112 | .catch(err => handleError(res, err));
113 | });
114 |
115 | // adds a new machine to a token set
116 | // creates a new token set if needed
117 | app.post('/register', function(req, res) {
118 | // add/update machine in database
119 | var machineId = req.body.machineId;
120 | var token = req.body.token;
121 |
122 | var getTokenPromise;
123 |
124 | if (!req.body.token) {
125 | getTokenPromise = randomBytes(8)
126 | .then(res => token = res.toString('hex'));
127 | } else {
128 | getTokenPromise = redis.exists(token)
129 | .then(function(exists) {
130 | if (!exists) {
131 | console.log('ERROR(?) Attempt to use a non existing token: ' + token);
132 | throw new Error('Not Found');
133 | }
134 | });
135 | }
136 |
137 | getTokenPromise
138 | .then(function() {
139 | return redis.hmset(machineId, {
140 | endpoint: req.body.endpoint,
141 | key: req.body.key,
142 | name: req.body.name,
143 | });
144 | })
145 | .then(() => redis.sismember(token, machineId))
146 | .then(function(isMember) {
147 | // add to the token set only if not there already (multiple
148 | // notifications!)
149 | if (!isMember) {
150 | return redis.sadd(token, machineId);
151 | }
152 | })
153 | .then(() => sendMachines(res, token))
154 | .catch(err => handleError(res, err));
155 | });
156 |
157 | // remove entire token set and all its machines
158 | function deleteToken(token) {
159 | return redis.smembers(token)
160 | .then(function(machines) {
161 | console.log('DEBUG: Deleting token ' + token);
162 | return Promise.all(machines.map(machine => redis.del(machine, machine + ':clients')));
163 | })
164 | .then(() => redis.del(token))
165 | .then(() => redis.del(token + ':clients'));
166 | }
167 |
168 | // unregister token and related data
169 | app.post('/unregister', function(req, res) {
170 | var token = req.body.token;
171 |
172 | redis.exists(token)
173 | .then(function(exists) {
174 | if (!exists) {
175 | throw new Error('Not Found');
176 | }
177 | })
178 | .then(() => deleteToken(token))
179 | .then(() => res.sendStatus(200))
180 | .catch(err => handleError(res, err));
181 | });
182 |
183 | function sendNotification(token, registration, payload, ttl) {
184 | console.log('DEBUG: sending notification to: ' + registration.endpoint);
185 | if (registration.endpoint.indexOf('https://android.googleapis.com/gcm/send') === 0) {
186 | return redis.set(token + '-payload', payload)
187 | .then(webPush.sendNotification(registration.endpoint, ttl));
188 | }
189 |
190 | return webPush.sendNotification(registration.endpoint, ttl, registration.key, payload);
191 | }
192 |
193 | // remove machine hash and its id from token set
194 | app.post('/unregisterMachine', function(req, res) {
195 | var token = req.body.token;
196 | var machineId = req.body.machineId;
197 |
198 | console.log('DEBUG: unregistering machine', machineId);
199 | redis.exists(token)
200 | .then(function(exists) {
201 | if (!exists) {
202 | throw new Error('Not Found');
203 | }
204 | return redis.hgetall(machineId);
205 | })
206 | .then(function(registration) {
207 | if (!registration) {
208 | throw new Error('Not Found');
209 | }
210 | // send notification to an endpoint to unregister itself
211 | var payload = JSON.stringify({
212 | title: 'unregister',
213 | body: 'called from unregisterMachine',
214 | });
215 | return sendNotification(token, registration, payload);
216 | })
217 | .then(() => redis.srem(token, machineId))
218 | .then(() => redis.del(machineId))
219 | .then(() => redis.del(machineId + ':clients'))
220 | .then(() => redis.smembers(token))
221 | .then(function(machines) {
222 | if (machines.length === 0) {
223 | return deleteToken(token);
224 | }
225 | })
226 | .then(() => sendMachines(res, token))
227 | .catch(err => handleError(res, err));
228 | });
229 |
230 |
231 | // used only if registration is expired
232 | // it's needed as happens on request from service worker which doesn't
233 | // have any meta data
234 | app.post('/updateRegistration', function(req, res) {
235 | var token = req.body.token;
236 | var machineId = req.body.machineId;
237 |
238 | redis.sismember(token, machineId)
239 | .then(function(isMember) {
240 | if (!isMember) {
241 | throw new Error('Not Found');
242 | }
243 |
244 | return redis.exists(machineId);
245 | })
246 | .then(function(exists) {
247 | if (!exists) {
248 | throw new Error('Not Found');
249 | }
250 |
251 | return redis.hmset(machineId, {
252 | endpoint: req.body.endpoint,
253 | key: req.body.key,
254 | });
255 | })
256 | .then(() => res.sendStatus(200))
257 | .catch(err => handleError(res, err));
258 | });
259 |
260 | // used only if metadata updated
261 | // this happens on request to update meta data from front-end site
262 | app.post('/updateMeta', function(req, res) {
263 | var token = req.body.token;
264 | var machineId = req.body.machineId;
265 |
266 | redis.sismember(token, machineId)
267 | .then(function(isMember) {
268 | if (!isMember) {
269 | throw new Error('Not Found');
270 | }
271 |
272 | return redis.exists(machineId);
273 | })
274 | .then(function(exists) {
275 | if (!exists) {
276 | throw new Error('Not Found');
277 | }
278 |
279 | return redis.hmset(machineId, {
280 | name: req.body.name,
281 | });
282 | })
283 | .then(() => res.sendStatus(200))
284 | .catch(err => handleError(res, err));
285 | });
286 |
287 | app.post('/notify', function(req, res) {
288 | var token = req.body.token;
289 |
290 | if (!req.body.payload ||
291 | !req.body.client) {
292 | res.sendStatus(500);
293 | return;
294 | }
295 |
296 | redis.smembers(token)
297 | .then(function(machines) {
298 | var client = req.body.client;
299 |
300 | // send notification to all machines assigned to `token`
301 | if (!machines || machines.length === 0) {
302 | throw new Error('Not Found');
303 | }
304 |
305 | // if a machine registers to a different token its active clients would
306 | // need to be added again to the list
307 | redis.sismember(token + ':clients', client)
308 | .then(function(isMember) {
309 | if (!isMember) {
310 | return redis.sadd(token + ':clients', client);
311 | }
312 | });
313 |
314 | // check activity and sendNotification if not specified or "1"
315 | function checkActivity(machine, registration, payload, ttl) {
316 | var machineKey = machine + ':clients';
317 | return redis.hget(machineKey, client)
318 | .then(function(isActive) {
319 | if (isActive === '0') {
320 | // not sending if token:machine client is "0"
321 | return;
322 | }
323 | if (isActive === '1') {
324 | // client is active on this machine
325 | return sendNotification(token, registration, payload, ttl);
326 | }
327 | // adding a client to the machine, '1' by default
328 | return redis.hmset(machineKey, client, 1)
329 | .then(() => sendNotification(token, registration, payload, ttl));
330 | });
331 | }
332 |
333 | var promises = machines.map(function(machine) {
334 | return redis.hgetall(machine)
335 | .then(registration => checkActivity(machine, registration, JSON.stringify(req.body.payload), req.body.ttl));
336 | });
337 |
338 | return Promise.all(promises);
339 | })
340 | .then(() => res.sendStatus(200))
341 | .catch(err => handleError(res, err));
342 | });
343 |
344 | app.get('/getPayload/:token', function(req, res) {
345 | var hash = req.params.token + '-payload';
346 |
347 | redis.get(hash)
348 | .then(function(payload) {
349 | if (!payload) {
350 | throw new Error('Not Found');
351 | }
352 |
353 | res.send(payload);
354 | })
355 | .catch(err => handleError(res, err));
356 | });
357 |
358 | if (!process.env.GCM_API_KEY) {
359 | console.warn('Set the GCM_API_KEY environment variable to support GCM');
360 | }
361 |
362 | app.post('/toggleClientNotification', function(req, res) {
363 | var token = req.body.token;
364 | var machineId = req.body.machineId;
365 | var client = req.body.client;
366 | var machineKey = machineId + ':clients';
367 |
368 | redis.exists(token)
369 | .then(function(exists) {
370 | if (!exists) {
371 | throw new Error('Not Found');
372 | }
373 | })
374 | .then(() => redis.hget(machineKey, client))
375 | .then(active => redis.hmset(machineKey, client, (active === '0' ? '1' : '0')))
376 | .then(() => sendMachines(res, token))
377 | .catch(err => handleError(res, err));
378 | });
379 |
380 | webPush.setGCMAPIKey(process.env.GCM_API_KEY);
381 |
382 | var port = process.env.PORT || 4000;
383 | var ready = new Promise(function(resolve, reject) {
384 | app.listen(port, function(err) {
385 | if (err) {
386 | reject(err);
387 | return;
388 | }
389 |
390 | console.log('Mercurius listening on http://localhost:%d', port);
391 |
392 | resolve();
393 | });
394 | });
395 |
396 | module.exports = {
397 | app: app,
398 | ready: ready,
399 | };
400 |
--------------------------------------------------------------------------------