--- a/dom/push/PushCrypto.jsm
+++ b/dom/push/PushCrypto.jsm
@@ -9,17 +9,25 @@ const Cu = Components.utils;
Cu.importGlobalProperties(['crypto']);
this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
'getCryptoParams',
'base64UrlDecode'];
var UTF8 = new TextEncoder('utf-8');
-var ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
+
+// Legacy encryption scheme.
+var AESGCM_128_ENCODING = 'aesgcm128';
+var AESGCM_128_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
+
+// New encryption scheme.
+var AESGCM_ENCODING = 'aesgcm';
+var AESGCM_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm');
+
var NONCE_INFO = UTF8.encode('Content-Encoding: nonce');
var AUTH_INFO = UTF8.encode('Content-Encoding: auth\0'); // note nul-terminus
var P256DH_INFO = UTF8.encode('P-256\0');
var ECDH_KEY = { name: 'ECDH', namedCurve: 'P-256' };
// A default keyid with a name that won't conflict with a real keyid.
var DEFAULT_KEYID = '';
function getEncryptionKeyParams(encryptKeyField) {
@@ -47,19 +55,25 @@ function getEncryptionParams(encryptFiel
return p.split(';').reduce(parseHeaderFieldParams, {});
}
this.getCryptoParams = function(headers) {
if (!headers) {
return null;
}
+ var encoding = headers.encoding;
+ if (encoding != AESGCM_ENCODING && encoding != AESGCM_128_ENCODING) {
+ return null;
+ }
+
var requiresAuthenticationSecret = true;
var keymap = getEncryptionKeyParams(headers.crypto_key);
- if (!keymap) {
+ if (!keymap && encoding == AESGCM_128_ENCODING) {
+ // Only allow the older `aesgcm128` scheme to omit the secret.
requiresAuthenticationSecret = false;
keymap = getEncryptionKeyParams(headers.encryption_key);
if (!keymap) {
return null;
}
}
var enc = getEncryptionParams(headers.encryption);
if (!enc) {
@@ -67,17 +81,18 @@ this.getCryptoParams = function(headers)
}
var dh = keymap[enc.keyid || DEFAULT_KEYID];
var salt = enc.salt;
var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
if (!dh || !salt || isNaN(rs) || (rs <= 1)) {
return null;
}
- return {dh, salt, rs, auth: requiresAuthenticationSecret};
+ var padSize = encoding == AESGCM_128_ENCODING ? 1 : 2;
+ return {dh, salt, rs, auth: requiresAuthenticationSecret, padSize};
}
var parseHeaderFieldParams = (m, v) => {
var i = v.indexOf('=');
if (i >= 0) {
// A quoted string with internal quotes is invalid for all the possible
// values of this header field.
m[v.substring(0, i).trim()] = v.substring(i + 1).trim()
@@ -190,18 +205,18 @@ this.PushCrypto = {
return crypto.subtle.generateKey(ECDH_KEY, true, ['deriveBits'])
.then(cryptoKey =>
Promise.all([
crypto.subtle.exportKey('raw', cryptoKey.publicKey),
crypto.subtle.exportKey('jwk', cryptoKey.privateKey)
]));
},
- decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey,
- aSalt, aRs, aAuthenticationSecret) {
+ decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey, aSalt, aRs,
+ aAuthenticationSecret, aPadSize) {
if (aData.byteLength === 0) {
// Zero length messages will be passed as null.
return Promise.resolve(null);
}
// The last chunk of data must be less than aRs, if it is not return an
// error.
@@ -214,31 +229,34 @@ this.PushCrypto = {
crypto.subtle.importKey('raw', senderKey, ECDH_KEY,
false, ['deriveBits']),
crypto.subtle.importKey('jwk', aPrivateKey, ECDH_KEY,
false, ['deriveBits'])
])
.then(([appServerKey, subscriptionPrivateKey]) =>
crypto.subtle.deriveBits({ name: 'ECDH', public: appServerKey },
subscriptionPrivateKey, 256))
- .then(ikm => this._deriveKeyAndNonce(new Uint8Array(ikm),
+ .then(ikm => this._deriveKeyAndNonce(aPadSize,
+ new Uint8Array(ikm),
base64UrlDecode(aSalt),
aPublicKey,
senderKey,
aAuthenticationSecret))
.then(r =>
// AEAD_AES_128_GCM expands ciphertext to be 16 octets longer.
Promise.all(chunkArray(aData, aRs + 16).map((slice, index) =>
- this._decodeChunk(slice, index, r[1], r[0]))))
+ this._decodeChunk(aPadSize, slice, index, r[1], r[0]))))
.then(r => concatArray(r));
},
- _deriveKeyAndNonce(ikm, salt, receiverKey, senderKey, authenticationSecret) {
+ _deriveKeyAndNonce(padSize, ikm, salt, receiverKey, senderKey,
+ authenticationSecret) {
var kdfPromise;
var context;
+ var encryptInfo;
// The authenticationSecret, when present, is mixed with the ikm using HKDF.
// This is its primary purpose. However, since the authentication secret
// was added at the same time that the info string was changed, we also use
// its presence to change how the final info string is calculated:
//
// 1. When there is no authenticationSecret, the context string is simply
// "Content-Encoding: <blah>". This corresponds to old, deprecated versions
// of the content encoding. This should eventually be removed: bug 1230038.
@@ -255,48 +273,69 @@ this.PushCrypto = {
// We also use the presence of the authentication secret to indicate that
// we have extra context to add to the info parameter.
context = concatArray([
new Uint8Array([0]), P256DH_INFO,
this._encodeLength(receiverKey), receiverKey,
this._encodeLength(senderKey), senderKey
]);
+ // Finally, we use the pad size to infer the content encoding.
+ encryptInfo = padSize == 2 ? AESGCM_ENCRYPT_INFO :
+ AESGCM_128_ENCRYPT_INFO;
} else {
kdfPromise = Promise.resolve(new hkdf(salt, ikm));
context = new Uint8Array(0);
+ if (padSize == 2) {
+ throw new Error("aesgcm encoding requires an authentication secret");
+ }
+ encryptInfo = AESGCM_128_ENCRYPT_INFO;
}
return kdfPromise.then(kdf => Promise.all([
- kdf.extract(concatArray([ENCRYPT_INFO, context]), 16)
+ kdf.extract(concatArray([encryptInfo, context]), 16)
.then(gcmBits => crypto.subtle.importKey('raw', gcmBits, 'AES-GCM', false,
['decrypt'])),
kdf.extract(concatArray([NONCE_INFO, context]), 12)
]));
},
_encodeLength(buffer) {
return new Uint8Array([0, buffer.byteLength]);
},
- _decodeChunk(aSlice, aIndex, aNonce, aKey) {
+ _decodeChunk(aPadSize, aSlice, aIndex, aNonce, aKey) {
let params = {
name: 'AES-GCM',
iv: generateNonce(aNonce, aIndex)
};
return crypto.subtle.decrypt(params, aKey, aSlice)
- .then(decoded => {
- decoded = new Uint8Array(decoded);
- if (decoded.length == 0) {
- return Promise.reject(new Error('Decoded array is too short!'));
- } else if (decoded[0] > decoded.length) {
- return Promise.reject(new Error ('Padding is wrong!'));
- } else {
- // All padded bytes must be zero except the first one.
- for (var i = 1; i <= decoded[0]; i++) {
- if (decoded[i] != 0) {
- return Promise.reject(new Error('Padding is wrong!'));
- }
- }
- return decoded.slice(decoded[0] + 1);
- }
- });
- }
+ .then(decoded => this._unpadChunk(aPadSize, new Uint8Array(decoded)));
+ },
+
+ /**
+ * Removes padding from a decrypted chunk.
+ *
+ * @param {Number} padSize The size of the padding length prepended to each
+ * chunk. For `aesgcm`, the padding length is expressed as a 16-bit unsigned
+ * big endian integer. For `aesgcm128`, the padding is an 8-bit integer.
+ * @param {Uint8Array} decoded The decrypted, padded chunk.
+ * @returns {Uint8Array} The chunk with padding removed.
+ */
+ _unpadChunk(padSize, decoded) {
+ if (padSize < 1 || padSize > 2) {
+ throw new Error('Unsupported pad size');
+ }
+ if (decoded.length < padSize) {
+ throw new Error('Decoded array is too short!');
+ }
+ var pad = padSize == 2 ? (decoded[0] << 8) | decoded[1] : decoded[0];
+ if (pad > decoded.length) {
+ throw new Error ('Padding is wrong!');
+ }
+ // All padded bytes must be zero except the first one.
+ for (var i = padSize; i <= pad; i++) {
+ if (decoded[i] !== 0) {
+ throw new Error('Padding is wrong!');
+ }
+ }
+ return decoded.slice(pad + padSize);
+ },
};
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -806,17 +806,18 @@ this.PushService = {
if (cryptoParams) {
decodedPromise = PushCrypto.decodeMsg(
message,
record.p256dhPrivateKey,
record.p256dhPublicKey,
cryptoParams.dh,
cryptoParams.salt,
cryptoParams.rs,
- cryptoParams.auth ? record.authenticationSecret : null
+ cryptoParams.auth ? record.authenticationSecret : null,
+ cryptoParams.padSize
);
} else {
decodedPromise = Promise.resolve(null);
}
return decodedPromise.then(message => {
if (shouldNotify) {
notified = this._notifyApp(record, message);
}
--- a/dom/push/PushServiceAndroidGCM.jsm
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -109,16 +109,17 @@ this.PushServiceAndroidGCM = {
let message = null;
let cryptoParams = null;
if (data.message && data.enc && (data.enckey || data.cryptokey)) {
let headers = {
encryption_key: data.enckey,
crypto_key: data.cryptokey,
encryption: data.enc,
+ encoding: data.con,
};
cryptoParams = getCryptoParams(headers);
// Ciphertext is (urlsafe) Base 64 encoded.
message = base64UrlDecode(data.message);
}
console.debug("Delivering message to main PushService:", message, cryptoParams);
this._mainPushService.receivedPushMessage(
--- a/dom/push/PushServiceHttp2.jsm
+++ b/dom/push/PushServiceHttp2.jsm
@@ -150,16 +150,17 @@ PushChannelListener.prototype = {
aStatusCode);
if (Components.isSuccessCode(aStatusCode) &&
this._mainListener &&
this._mainListener._pushService) {
let headers = {
encryption_key: getHeaderField(aRequest, "Encryption-Key"),
crypto_key: getHeaderField(aRequest, "Crypto-Key"),
encryption: getHeaderField(aRequest, "Encryption"),
+ encoding: getHeaderField(aRequest, "Content-Encoding"),
};
let cryptoParams = getCryptoParams(headers);
let msg = concatArray(this._message);
this._mainListener._pushService._pushChannelOnStop(this._mainListener.uri,
this._ackUri,
msg,
cryptoParams);
--- a/dom/push/test/xpcshell/test_notification_data.js
+++ b/dom/push/test/xpcshell/test_notification_data.js
@@ -122,47 +122,50 @@ add_task(function* test_notification_ack
let allTestData = [
{
channelID: 'subscription1',
version: 'v1',
send: {
headers: {
encryption_key: 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
encryption: 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"',
+ encoding: 'aesgcm128',
},
data: 'NwrrOWPxLE8Sv5Rr0Kep7n0-r_j3rsYrUw_CXPo',
version: 'v1',
},
receive: {
scope: 'https://example.com/page/1',
data: 'Some message'
}
},
{
channelID: 'subscription2',
version: 'v2',
send: {
headers: {
encryption_key: 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
encryption: 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"',
+ encoding: 'aesgcm128',
},
data: 'Zt9dEdqgHlyAL_l83385aEtb98ZBilz5tgnGgmwEsl5AOCNgesUUJ4p9qUU',
},
receive: {
scope: 'https://example.com/page/2',
data: 'Some message'
}
},
{
channelID: 'subscription3',
version: 'v3',
send: {
headers: {
encryption_key: 'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"',
encryption: 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24',
+ encoding: 'aesgcm128',
},
data: 'LKru3ZzxBZuAxYtsaCfaj_fehkrIvqbVd1iSwnwAUgnL-cTeDD-83blxHXTq7r0z9ydTdMtC3UjAcWi8LMnfY-BFzi0qJAjGYIikDA',
},
receive: {
scope: 'https://example.com/page/3',
data: 'Some message'
}
},
@@ -170,48 +173,68 @@ add_task(function* test_notification_ack
// header field. No padding or record size changes.
{
channelID: 'subscription1',
version: 'v4',
send: {
headers: {
crypto_key: 'keyid=v4;dh="BHqG01j7rOfp12BEDzxWXxlCaU4cdOx2DZAwCt3QuzEsnXN9lCna9QmZCkVpXsx7sAlaEmtl_VfF1lHlFS7XWcA"',
encryption: 'keyid="v4";salt="X5-iy5rzhm4naNmMHdSYJw"',
+ encoding: 'aesgcm128',
},
data: '7YlxyNlZsNX4UNknHxzTqFrcrzz58W95uXBa0iY',
},
receive: {
scope: 'https://example.com/page/1',
data: 'Some message'
}
},
+ // A message encoded with `aesgcm` (2 bytes of padding).
+ {
+ channelID: 'subscription1',
+ version: 'v5',
+ send: {
+ headers: {
+ crypto_key: 'dh="BMh_vsnqu79ZZkMTYkxl4gWDLdPSGE72Lr4w2hksSFW398xCMJszjzdblAWXyhSwakRNEU_GopAm4UGzyMVR83w"',
+ encryption: 'salt="C14Wb7rQTlXzrgcPHtaUzw"',
+ encoding: 'aesgcm',
+ },
+ data: 'pus4kUaBWzraH34M-d_oN8e0LPpF_X6acx695AMXovDe',
+ },
+ receive: {
+ scope: 'https://example.com/page/1',
+ data: 'Another message'
+ }
+ },
// A message with 17 bytes of padding and rs of 24
{
channelID: 'subscription2',
version: 'v5',
send: {
headers: {
crypto_key: 'keyid="v5"; dh="BJhyKIH5P30YUKn1bolj_LMnael1-KZT_aGXgD2CRspBfv9gcUhVAmpxToZrw7QQEKl9K83b3zcqNY6G_dFhEsI"',
encryption: 'keyid=v5;salt="bLmqCy550eK1Ao41tD7orA";rs=24',
+ encoding: 'aesgcm128',
},
data: 'SQDlDg1ftLkM_ruZlmyB2bk9L78HYtkcbA-y4-uAxwL-G4KtOA-J-A_rJ007Vi6NUkQe9K4kSZeIBrIUpmGv',
},
receive: {
scope: 'https://example.com/page/2',
data: 'Some message'
}
},
// A message without key identifiers.
{
channelID: 'subscription3',
version: 'v6',
send: {
headers: {
crypto_key: 'dh="BEgnDmVw9Gcn1fWA5t53Jtpsgfewk_pzsjSc_PBPpPmROWGQA2v8ESrSsQgosNXx0o-uMMhi9tDAUeks3380kd8"',
encryption: 'salt=T9DM8bNxuMHRVTn4LzkJDQ',
+ encoding: 'aesgcm128',
},
data: '7KUCi0dBBJbWmsYTqEqhFrgTv4ZOo_BmQRQ_2kY',
},
receive: {
scope: 'https://example.com/page/3',
data: 'Some message'
}
},
--- a/dom/push/test/xpcshell/test_notification_http2.js
+++ b/dom/push/test/xpcshell/test_notification_http2.js
@@ -1,16 +1,17 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
Cu.import("resource://gre/modules/Services.jsm");
const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+const {base64UrlDecode} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
var prefs;
var tlsProfile;
var pushEnabled;
var pushConnectionEnabled;
var serverPort = -1;
@@ -51,16 +52,18 @@ function run_test() {
add_task(function* test_pushNotifications() {
// /pushNotifications/subscription1 will send a message with no rs and padding
// length 1.
// /pushNotifications/subscription2 will send a message with no rs and padding
// length 16.
// /pushNotifications/subscription3 will send a message with rs equal 24 and
// padding length 16.
+ // /pushNotifications/subscription4 will send a message with no rs and padding
+ // length 256.
let db = PushServiceHttp2.newPushDB();
do_register_cleanup(() => {
return db.drop().then(_ => db.close());
});
var serverURL = "https://localhost:" + serverPort;
@@ -116,16 +119,36 @@ add_task(function* test_pushNotification
kty: 'EC',
x: 'OFQchNJ5WtZjJsWdvvKVVMIMMs91BYyl_yBeFxbC9po',
y: 'Ja6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI'
},
originAttributes: ChromeUtils.originAttributesToSuffix(
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
quota: Infinity,
systemRecord: true,
+ }, {
+ subscriptionUri: serverURL + '/pushNotifications/subscription4',
+ pushEndpoint: serverURL + '/pushEndpoint4',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint4',
+ scope: 'https://example.com/page/4',
+ p256dhPublicKey: base64UrlDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'),
+ p256dhPrivateKey: {
+ crv: 'P-256',
+ d: 'fWi7tZaX0Pk6WnLrjQ3kiRq_g5XStL5pdH4pllNCqXw',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: 'EC',
+ x: 'Ry8PORYKtS2NT_DKAv3yxtAJCtbWVj2Ku2AaeUJzgHQ',
+ y: 'JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'
+ },
+ authenticationSecret: base64UrlDecode('cwDVC1iwAn8E37mkR3tMSg'),
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ systemRecord: true,
}];
for (let record of records) {
yield db.put(record);
}
let notifyPromise = Promise.all([
promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
@@ -143,17 +166,24 @@ add_task(function* test_pushNotification
}
}),
promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
var message = subject.QueryInterface(Ci.nsIPushMessage);
if (message && (data == "https://example.com/page/3")){
equal(message.text(), "Some message", "decoded message is incorrect");
return true;
}
- })
+ }),
+ promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
+ var message = subject.QueryInterface(Ci.nsIPushMessage);
+ if (message && (data == "https://example.com/page/4")){
+ equal(message.text(), "Yet another message", "decoded message is incorrect");
+ return true;
+ }
+ }),
]);
PushService.init({
serverURI: serverURL,
db
});
yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
--- a/testing/xpcshell/moz-http2/moz-http2.js
+++ b/testing/xpcshell/moz-http2/moz-http2.js
@@ -199,17 +199,17 @@ function handleRequest(req, res) {
// the headers to have something illegal in them
Compressor.prototype.compress = originalCompressHeaders;
var u = url.parse(req.url);
var content = getHttpContent(u.pathname);
var push, push1, push1a, push2, push3;
// PushService tests.
- var pushPushServer1, pushPushServer2, pushPushServer3;
+ var pushPushServer1, pushPushServer2, pushPushServer3, pushPushServer4;
if (req.httpVersionMajor === 2) {
res.setHeader('X-Connection-Http2', 'yes');
res.setHeader('X-Http2-StreamId', '' + req.stream.id);
} else {
res.setHeader('X-Connection-Http2', 'no');
}
@@ -603,59 +603,79 @@ function handleRequest(req, res) {
return;
}
else if (u.pathname ==="/pushNotifications/subscription1") {
pushPushServer1 = res.push(
{ hostname: 'localhost:' + serverPort, port: serverPort,
path : '/pushNotificationsDeliver1', method : 'GET',
headers: { 'Encryption-Key': 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
- 'Encryption': 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"'
+ 'Encryption': 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"',
+ 'Content-Encoding': 'aesgcm128',
}
});
pushPushServer1.writeHead(200, {
'subresource' : '1'
});
pushPushServer1.end('370aeb3963f12c4f12bf946bd0a7a9ee7d3eaff8f7aec62b530fc25cfa', 'hex');
return;
}
else if (u.pathname ==="/pushNotifications/subscription2") {
pushPushServer2 = res.push(
{ hostname: 'localhost:' + serverPort, port: serverPort,
path : '/pushNotificationsDeliver3', method : 'GET',
headers: { 'Encryption-Key': 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
- 'Encryption': 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"'
+ 'Encryption': 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"',
+ 'Content-Encoding': 'aesgcm128',
}
});
pushPushServer2.writeHead(200, {
'subresource' : '1'
});
pushPushServer2.end('66df5d11daa01e5c802ff97cdf7f39684b5bf7c6418a5cf9b609c6826c04b25e403823607ac514278a7da945', 'hex');
return;
}
else if (u.pathname ==="/pushNotifications/subscription3") {
pushPushServer3 = res.push(
{ hostname: 'localhost:' + serverPort, port: serverPort,
path : '/pushNotificationsDeliver3', method : 'GET',
headers: { 'Encryption-Key': 'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"',
- 'Encryption': 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24'
+ 'Encryption': 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24',
+ 'Content-Encoding': 'aesgcm128',
}
});
pushPushServer3.writeHead(200, {
'subresource' : '1'
});
pushPushServer3.end('2caaeedd9cf1059b80c58b6c6827da8ff7de864ac8bea6d5775892c27c005209cbf9c4de0c3fbcddb9711d74eaeebd33f7275374cb42dd48c07168bc2cc9df63e045ce2d2a2408c66088a40c', 'hex');
return;
}
+ else if (u.pathname == "/pushNotifications/subscription4") {
+ pushPushServer4 = res.push(
+ { hostname: 'localhost:' + serverPort, port: serverPort,
+ path : '/pushNotificationsDeliver4', method : 'GET',
+ headers: { 'Crypto-Key': 'keyid="notification4";dh="BJScXUUTcs7D8jJWI1AOxSgAKkF7e56ay4Lek52TqDlWo1yGd5czaxFWfsuP4j7XNWgGYm60-LKpSUMlptxPFVQ"',
+ 'Encryption': 'keyid="notification4"; salt="sn9p2QqF3V6KBclda8vx7w"',
+ 'Content-Encoding': 'aesgcm',
+ }
+ });
+ pushPushServer4.writeHead(200, {
+ 'subresource' : '1'
+ });
+
+ pushPushServer4.end('9eba7ba6192544a39bd9e9b58e702d0748f1776b27f6616cdc55d29ed5a015a6db8f2dd82cd5751a14315546194ff1c18458ab91eb36c9760ccb042670001fd9964557a079553c3591ee131ceb259389cfffab3ab873f873caa6a72e87d262b8684c3260e5940b992234deebf57a9ff3a8775742f3cbcb152d249725a28326717e19cce8506813a155eff5df9bdba9e3ae8801d3cc2b7e7f2f1b6896e63d1fdda6f85df704b1a34db7b2dd63eba11ede154300a318c6f83c41a3d32356a196e36bc905b99195fd91ae4ff3f545c42d17f1fdc1d5bd2bf7516d0765e3a859fffac84f46160b79cedda589f74c25357cf6988cd8ba83867ebd86e4579c9d3b00a712c77fcea3b663007076e21f9819423faa830c2176ff1001c1690f34be26229a191a938517', 'hex');
+ return;
+ }
+
else if ((u.pathname === "/pushNotificationsDeliver1") ||
(u.pathname === "/pushNotificationsDeliver2") ||
(u.pathname === "/pushNotificationsDeliver3")) {
res.writeHead(410, "GONE");
res.end("");
return;
}