Bug 1172502 - Add message encription for WebPush. r=mt r=kitcambridge r=keeler r=smaug

--HG--
extra : commitid : FsE5V9w4fej
extra : amend_source : 8b44837b765bd319cadc93a53948264dfbd87ecf
This commit is contained in:
Dragana Damjanovic 2015-09-11 07:51:32 -07:00
Родитель 314f131577
Коммит a7f75d48d4
15 изменённых файлов: 755 добавлений и 131 удалений

Просмотреть файл

@ -17,10 +17,13 @@ interface nsIPrincipal;
* endpoint.
*/
[scriptable, uuid(0bcac389-a3ac-44a4-97fb-b50e41a46146)]
[scriptable, uuid(dc201064-8e5c-4a26-bd37-d1e33558a903)]
interface nsIPushEndpointCallback : nsISupports
{
void onPushEndpoint(in nsresult status, in DOMString endpoint);
void onPushEndpoint(in nsresult status,
in DOMString endpoint,
in uint32_t keyLen,
[array, size_is(keyLen)] in octet key);
};
/**

Просмотреть файл

@ -121,10 +121,22 @@ Push.prototype = {
() => {
fn(that._scope, that._principal, {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPushEndpointCallback]),
onPushEndpoint: function(ok, endpoint) {
onPushEndpoint: function(ok, endpoint, keyLen, key) {
if (ok === Cr.NS_OK) {
if (endpoint) {
let sub = new that._window.PushSubscription(endpoint, that._scope);
let sub;
if (keyLen) {
let publicKey = new ArrayBuffer(keyLen);
let keyView = new Uint8Array(publicKey);
keyView.set(key);
sub = new that._window.PushSubscription(endpoint,
that._scope,
publicKey);
} else {
sub = new that._window.PushSubscription(endpoint,
that._scope,
null);
}
sub.setPrincipal(that._principal);
resolve(sub);
} else {

Просмотреть файл

@ -100,6 +100,19 @@ PushClient.prototype = {
}, null, principal);
},
_deliverPushEndpoint: function(request, registration) {
if (registration.p256dhKey) {
let key = new Uint8Array(registration.p256dhKey);
request.onPushEndpoint(Cr.NS_OK,
registration.pushEndpoint,
key.length,
key);
return;
}
request.onPushEndpoint(Cr.NS_OK, registration.pushEndpoint, 0, null);
},
receiveMessage: function(aMessage) {
let json = aMessage.data;
@ -112,23 +125,23 @@ PushClient.prototype = {
debug("receiveMessage(): " + JSON.stringify(aMessage))
switch (aMessage.name) {
case "PushService:Register:OK":
{
request.onPushEndpoint(Cr.NS_OK, json.pushEndpoint);
this._deliverPushEndpoint(request, json);
break;
}
case "PushService:Register:KO":
request.onPushEndpoint(Cr.NS_ERROR_FAILURE, "");
request.onPushEndpoint(Cr.NS_ERROR_FAILURE, "", 0, null);
break;
case "PushService:Registration:OK":
{
let endpoint = "";
if (json.registration)
endpoint = json.registration.pushEndpoint;
request.onPushEndpoint(Cr.NS_OK, endpoint);
if (!json.registration) {
request.onPushEndpoint(Cr.NS_OK, "", 0, null);
} else {
this._deliverPushEndpoint(request, json.registration);
}
break;
}
case "PushService:Registration:KO":
request.onPushEndpoint(Cr.NS_ERROR_FAILURE, "");
request.onPushEndpoint(Cr.NS_ERROR_FAILURE, "", 0, null);
break;
case "PushService:Unregister:OK":
if (typeof json.result !== "boolean") {

Просмотреть файл

@ -90,13 +90,18 @@ PushSubscription::Unsubscribe(ErrorResult& aRv)
PushSubscription::PushSubscription(nsIGlobalObject* aGlobal,
const nsAString& aEndpoint,
const nsAString& aScope)
: mGlobal(aGlobal), mEndpoint(aEndpoint), mScope(aScope)
const nsAString& aScope,
const nsTArray<uint8_t>& aRawP256dhKey)
: mGlobal(aGlobal)
, mEndpoint(aEndpoint)
, mScope(aScope)
, mRawP256dhKey(aRawP256dhKey)
{
}
PushSubscription::~PushSubscription()
{}
{
}
JSObject*
PushSubscription::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
@ -104,6 +109,20 @@ PushSubscription::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
return PushSubscriptionBinding::Wrap(aCx, this, aGivenProto);
}
void
PushSubscription::GetKey(JSContext* aCx,
PushEncryptionKeyName aType,
JS::MutableHandle<JSObject*> aP256dhKey)
{
if (aType == PushEncryptionKeyName::P256dh && !mRawP256dhKey.IsEmpty()) {
aP256dhKey.set(ArrayBuffer::Create(aCx,
mRawP256dhKey.Length(),
mRawP256dhKey.Elements()));
} else {
aP256dhKey.set(nullptr);
}
}
void
PushSubscription::SetPrincipal(nsIPrincipal* aPrincipal)
{
@ -113,16 +132,34 @@ PushSubscription::SetPrincipal(nsIPrincipal* aPrincipal)
// static
already_AddRefed<PushSubscription>
PushSubscription::Constructor(GlobalObject& aGlobal, const nsAString& aEndpoint, const nsAString& aScope, ErrorResult& aRv)
PushSubscription::Constructor(GlobalObject& aGlobal,
const nsAString& aEndpoint,
const nsAString& aScope,
const Nullable<ArrayBuffer>& aP256dhKey,
ErrorResult& aRv)
{
MOZ_ASSERT(!aEndpoint.IsEmpty());
MOZ_ASSERT(!aScope.IsEmpty());
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
nsRefPtr<PushSubscription> sub = new PushSubscription(global, aEndpoint, aScope);
nsTArray<uint8_t> rawKey;
if (!aP256dhKey.IsNull()) {
const ArrayBuffer& key = aP256dhKey.Value();
key.ComputeLengthAndData();
rawKey.SetLength(key.Length());
rawKey.ReplaceElementsAt(0, key.Length(), key.Data(), key.Length());
}
nsRefPtr<PushSubscription> sub = new PushSubscription(global,
aEndpoint,
aScope,
rawKey);
return sub.forget();
}
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushSubscription, mGlobal, mPrincipal)
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushSubscription)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushSubscription)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushSubscription)
@ -188,8 +225,9 @@ NS_INTERFACE_MAP_END
// WorkerPushSubscription
WorkerPushSubscription::WorkerPushSubscription(const nsAString& aEndpoint,
const nsAString& aScope)
: mEndpoint(aEndpoint), mScope(aScope)
const nsAString& aScope,
const nsTArray<uint8_t>& aRawP256dhKey)
: mEndpoint(aEndpoint), mScope(aScope), mRawP256dhKey(aRawP256dhKey)
{
MOZ_ASSERT(!aScope.IsEmpty());
MOZ_ASSERT(!aEndpoint.IsEmpty());
@ -206,16 +244,45 @@ WorkerPushSubscription::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenP
// static
already_AddRefed<WorkerPushSubscription>
WorkerPushSubscription::Constructor(GlobalObject& aGlobal, const nsAString& aEndpoint, const nsAString& aScope, ErrorResult& aRv)
WorkerPushSubscription::Constructor(GlobalObject& aGlobal,
const nsAString& aEndpoint,
const nsAString& aScope,
const Nullable<ArrayBuffer>& aP256dhKey,
ErrorResult& aRv)
{
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(worker);
worker->AssertIsOnWorkerThread();
nsRefPtr<WorkerPushSubscription> sub = new WorkerPushSubscription(aEndpoint, aScope);
nsTArray<uint8_t> rawKey;
if (!aP256dhKey.IsNull()) {
const ArrayBuffer& key = aP256dhKey.Value();
key.ComputeLengthAndData();
rawKey.SetLength(key.Length());
rawKey.ReplaceElementsAt(0, key.Length(), key.Data(), key.Length());
}
nsRefPtr<WorkerPushSubscription> sub = new WorkerPushSubscription(aEndpoint,
aScope,
rawKey);
return sub.forget();
}
void
WorkerPushSubscription::GetKey(JSContext* aCx,
PushEncryptionKeyName aType,
JS::MutableHandle<JSObject*> aP256dhKey)
{
if (aType == mozilla::dom::PushEncryptionKeyName::P256dh &&
!mRawP256dhKey.IsEmpty()) {
aP256dhKey.set(ArrayBuffer::Create(aCx,
mRawP256dhKey.Length(),
mRawP256dhKey.Elements()));
} else {
aP256dhKey.set(nullptr);
}
}
class UnsubscribeResultRunnable final : public WorkerRunnable
{
public:
@ -371,6 +438,7 @@ WorkerPushSubscription::Unsubscribe(ErrorResult &aRv)
}
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(WorkerPushSubscription)
NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkerPushSubscription)
NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkerPushSubscription)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkerPushSubscription)
@ -397,12 +465,14 @@ public:
GetSubscriptionResultRunnable(PromiseWorkerProxy* aProxy,
nsresult aStatus,
const nsAString& aEndpoint,
const nsAString& aScope)
const nsAString& aScope,
const nsTArray<uint8_t>& aRawP256dhKey)
: WorkerRunnable(aProxy->GetWorkerPrivate(), WorkerThreadModifyBusyCount)
, mProxy(aProxy)
, mStatus(aStatus)
, mEndpoint(aEndpoint)
, mScope(aScope)
, mRawP256dhKey(aRawP256dhKey)
{ }
bool
@ -414,7 +484,7 @@ public:
promise->MaybeResolve(JS::NullHandleValue);
} else {
nsRefPtr<WorkerPushSubscription> sub =
new WorkerPushSubscription(mEndpoint, mScope);
new WorkerPushSubscription(mEndpoint, mScope, mRawP256dhKey);
promise->MaybeResolve(sub);
}
} else {
@ -432,6 +502,7 @@ private:
nsresult mStatus;
nsString mEndpoint;
nsString mScope;
nsTArray<uint8_t> mRawP256dhKey;
};
class GetSubscriptionCallback final : public nsIPushEndpointCallback
@ -446,7 +517,10 @@ public:
{}
NS_IMETHOD
OnPushEndpoint(nsresult aStatus, const nsAString& aEndpoint) override
OnPushEndpoint(nsresult aStatus,
const nsAString& aEndpoint,
uint32_t aKeyLen,
uint8_t* aKey) override
{
AssertIsOnMainThread();
MOZ_ASSERT(mProxy, "OnPushEndpoint() called twice?");
@ -461,8 +535,15 @@ public:
AutoJSAPI jsapi;
jsapi.Init();
nsTArray<uint8_t> rawP256dhKey(aKeyLen);
rawP256dhKey.ReplaceElementsAt(0, aKeyLen, aKey, aKeyLen);
nsRefPtr<GetSubscriptionResultRunnable> r =
new GetSubscriptionResultRunnable(proxy, aStatus, aEndpoint, mScope);
new GetSubscriptionResultRunnable(proxy,
aStatus,
aEndpoint,
mScope,
rawP256dhKey);
r->Dispatch(jsapi.cx());
return NS_OK;
}
@ -502,7 +583,7 @@ public:
nsCOMPtr<nsIPermissionManager> permManager =
mozilla::services::GetPermissionManager();
if (!permManager) {
callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString());
callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
return NS_OK;
}
@ -515,14 +596,14 @@ public:
&permission);
if (NS_WARN_IF(NS_FAILED(rv)) || permission != nsIPermissionManager::ALLOW_ACTION) {
callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString());
callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
return NS_OK;
}
nsCOMPtr<nsIPushClient> client =
do_CreateInstance("@mozilla.org/push/PushClient;1");
if (!client) {
callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString());
callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
return NS_OK;
}
@ -534,7 +615,7 @@ public:
}
if (NS_WARN_IF(NS_FAILED(rv))) {
callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString());
callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
return rv;
}

Просмотреть файл

@ -36,6 +36,7 @@
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/TypedArray.h"
#include "nsCOMPtr.h"
#include "mozilla/nsRefPtr.h"
@ -44,6 +45,8 @@
class nsIGlobalObject;
class nsIPrincipal;
#include "mozilla/dom/PushSubscriptionBinding.h"
namespace mozilla {
namespace dom {
@ -63,7 +66,8 @@ public:
explicit PushSubscription(nsIGlobalObject* aGlobal,
const nsAString& aEndpoint,
const nsAString& aScope);
const nsAString& aScope,
const nsTArray<uint8_t>& aP256dhKey);
JSObject*
WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
@ -80,8 +84,17 @@ public:
aEndpoint = mEndpoint;
}
void
GetKey(JSContext* cx,
PushEncryptionKeyName aType,
JS::MutableHandle<JSObject*> aP256dhKey);
static already_AddRefed<PushSubscription>
Constructor(GlobalObject& aGlobal, const nsAString& aEndpoint, const nsAString& aScope, ErrorResult& aRv);
Constructor(GlobalObject& aGlobal,
const nsAString& aEndpoint,
const nsAString& aScope,
const Nullable<ArrayBuffer>& aP256dhKey,
ErrorResult& aRv);
void
SetPrincipal(nsIPrincipal* aPrincipal);
@ -97,6 +110,7 @@ private:
nsCOMPtr<nsIPrincipal> mPrincipal;
nsString mEndpoint;
nsString mScope;
nsTArray<uint8_t> mRawP256dhKey;
};
class PushManager final : public nsISupports
@ -146,7 +160,8 @@ public:
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WorkerPushSubscription)
explicit WorkerPushSubscription(const nsAString& aEndpoint,
const nsAString& aScope);
const nsAString& aScope,
const nsTArray<uint8_t>& aRawP256dhKey);
nsIGlobalObject*
GetParentObject() const
@ -158,7 +173,11 @@ public:
WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
static already_AddRefed<WorkerPushSubscription>
Constructor(GlobalObject& aGlobal, const nsAString& aEndpoint, const nsAString& aScope, ErrorResult& aRv);
Constructor(GlobalObject& aGlobal,
const nsAString& aEndpoint,
const nsAString& aScope,
const Nullable<ArrayBuffer>& aP256dhKey,
ErrorResult& aRv);
void
GetEndpoint(nsAString& aEndpoint) const
@ -166,6 +185,10 @@ public:
aEndpoint = mEndpoint;
}
void
GetKey(JSContext* cx, PushEncryptionKeyName aType,
JS::MutableHandle<JSObject*> aP256dhKey);
already_AddRefed<Promise>
Unsubscribe(ErrorResult& aRv);
@ -175,6 +198,7 @@ protected:
private:
nsString mEndpoint;
nsString mScope;
nsTArray<uint8_t> mRawP256dhKey;
};
class WorkerPushManager final : public nsISupports

Просмотреть файл

@ -719,6 +719,11 @@ this.PushService = {
.then(record => this._notifySubscriptionChangeObservers(record));
},
updateRecordAndNotifyApp: function(aKeyID, aUpdateFunc) {
return this._db.update(aKeyID, aUpdateFunc)
.then(record => this._notifySubscriptionChangeObservers(record));
},
/**
* Dispatches an incoming message to a service worker, recalculating the
* quota for the associated push registration. If the quota is exceeded,
@ -737,7 +742,7 @@ this.PushService = {
debug("receivedPushMessage()");
let shouldNotify = false;
this.getByKeyID(keyID).then(record => {
return this.getByKeyID(keyID).then(record => {
if (!record) {
throw new Error("No record for key ID " + keyID);
}
@ -761,11 +766,13 @@ this.PushService = {
return newRecord;
});
}).then(record => {
var notified = false;
if (!record) {
return null;
return notified;
}
if (shouldNotify) {
this._notifyApp(record, message);
notified = this._notifyApp(record, message);
}
if (record.isExpired()) {
// Drop the registration in the background. If the user returns to the
@ -775,6 +782,7 @@ this.PushService = {
debug("receivedPushMessage: Unregister error: " + error);
});
}
return notified;
}).catch(error => {
debug("receivedPushMessage: Error notifying app: " + error);
});
@ -785,7 +793,7 @@ this.PushService = {
aPushRecord.originAttributes === undefined) {
debug("notifyApp() something is undefined. Dropping notification: " +
JSON.stringify(aPushRecord) );
return;
return false;
}
debug("notifyApp() " + aPushRecord.scope);
@ -807,7 +815,7 @@ this.PushService = {
// If permission has been revoked, trash the message.
if (!aPushRecord.hasPermission()) {
debug("Does not have permission for push.");
return;
return false;
}
// TODO data.
@ -818,6 +826,7 @@ this.PushService = {
};
this._notifyListeners('push', data);
return true;
},
getByKeyID: function(aKeyID) {

Просмотреть файл

@ -19,6 +19,9 @@ Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
const {PushServiceHttp2Crypto, concatArray} =
Cu.import("resource://gre/modules/PushServiceHttp2Crypto.jsm");
this.EXPORTED_SYMBOLS = ["PushServiceHttp2"];
const prefs = new Preferences("dom.push.");
@ -34,7 +37,7 @@ function debug(s) {
}
const kPUSHHTTP2DB_DB_NAME = "pushHttp2";
const kPUSHHTTP2DB_DB_VERSION = 4; // Change this if the IndexedDB format changes
const kPUSHHTTP2DB_DB_VERSION = 5; // Change this if the IndexedDB format changes
const kPUSHHTTP2DB_STORE_NAME = "pushHttp2";
/**
@ -114,13 +117,12 @@ PushSubscriptionListener.prototype = {
var PushChannelListener = function(pushSubscriptionListener) {
debug("Creating a new push channel listener.");
this._mainListener = pushSubscriptionListener;
this._message = [];
this._ackUri = null;
};
PushChannelListener.prototype = {
_message: null,
_ackUri: null,
onStartRequest: function(aRequest, aContext) {
this._ackUri = aRequest.URI.spec;
},
@ -132,15 +134,13 @@ PushChannelListener.prototype = {
return;
}
let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
let inputStream = Cc["@mozilla.org/binaryinputstream;1"]
.createInstance(Ci.nsIBinaryInputStream);
inputStream.init(aStream);
if (!this._message) {
this._message = inputStream.read(aCount);
} else {
this._message.concat(inputStream.read(aCount));
}
inputStream.setInputStream(aStream);
let chunk = new ArrayBuffer(aCount);
inputStream.readArrayBuffer(aCount, chunk);
this._message.push(chunk);
},
onStopRequest: function(aRequest, aContext, aStatusCode) {
@ -148,13 +148,76 @@ PushChannelListener.prototype = {
if (Components.isSuccessCode(aStatusCode) &&
this._mainListener &&
this._mainListener._pushService) {
var keymap = encryptKeyFieldParser(aRequest);
if (!keymap) {
return;
}
var enc = encryptFieldParser(aRequest);
if (!enc || !enc.keyid) {
return;
}
var dh = keymap[enc.keyid];
var salt = enc.salt;
var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
if (!dh || !salt || isNaN(rs) || (rs <= 1)) {
return;
}
var msg = concatArray(this._message);
this._mainListener._pushService._pushChannelOnStop(this._mainListener.uri,
this._ackUri,
this._message);
msg,
dh,
salt,
rs);
}
}
};
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()
.replace(/^"(.*)"$/, '$1');
}
return m;
};
function encryptKeyFieldParser(aRequest) {
try {
var encryptKeyField = aRequest.getRequestHeader("Encryption-Key");
var params = encryptKeyField.split(',');
return params.reduce((m, p) => {
var pmap = p.split(';').reduce(parseHeaderFieldParams, {});
if (pmap.keyid && pmap.dh) {
m[pmap.keyid] = pmap.dh;
}
return m;
}, {});
} catch(e) {
// getRequestHeader can throw.
return null;
}
}
function encryptFieldParser(aRequest) {
try {
return aRequest.getRequestHeader("Encryption")
.split(',', 1)[0]
.split(';')
.reduce(parseHeaderFieldParams, {});
} catch(e) {
// getRequestHeader can throw.
return null;
}
}
var PushServiceDelete = function(resolve, reject) {
this._resolve = resolve;
this._reject = reject;
@ -188,9 +251,12 @@ PushServiceDelete.prototype = {
}
};
var SubscriptionListener = function(aSubInfo, aServerURI, aPushServiceHttp2) {
var SubscriptionListener = function(aSubInfo, aResolve, aReject,
aServerURI, aPushServiceHttp2) {
debug("Creating a new subscription listener.");
this._subInfo = aSubInfo;
this._resolve = aResolve;
this._reject = aReject;
this._data = '';
this._serverURI = aServerURI;
this._service = aPushServiceHttp2;
@ -221,12 +287,12 @@ SubscriptionListener.prototype = {
// Check if pushService is still active.
if (!this._service.hasmainPushService()) {
this._subInfo.reject({error: "Service deactivated"});
this._reject({error: "Service deactivated"});
return;
}
if (!Components.isSuccessCode(aStatus)) {
this._subInfo.reject({error: "Error status" + aStatus});
this._reject({error: "Error status" + aStatus});
return;
}
@ -236,15 +302,18 @@ SubscriptionListener.prototype = {
if (this._subInfo.retries < prefs.get("http2.maxRetries")) {
this._subInfo.retries++;
var retryAfter = retryAfterParser(aRequest);
setTimeout(this._service.retrySubscription.bind(this._service,
this._subInfo),
retryAfter);
setTimeout(_ => this._reject(
{
retry: true,
subInfo: this._subInfo
}),
retryAfter);
} else {
this._subInfo.reject({error: "Error response code: " + statusCode });
this._reject({error: "Error response code: " + statusCode });
}
return;
} else if (statusCode != 201) {
this._subInfo.reject({error: "Error response code: " + statusCode });
this._reject({error: "Error response code: " + statusCode });
return;
}
@ -252,7 +321,7 @@ SubscriptionListener.prototype = {
try {
subscriptionUri = aRequest.getResponseHeader("location");
} catch (err) {
this._subInfo.reject({error: "Return code 201, but the answer is bogus"});
this._reject({error: "Return code 201, but the answer is bogus"});
return;
}
@ -262,27 +331,27 @@ SubscriptionListener.prototype = {
try {
linkList = aRequest.getResponseHeader("link");
} catch (err) {
this._subInfo.reject({error: "Return code 201, but the answer is bogus"});
this._reject({error: "Return code 201, but the answer is bogus"});
return;
}
var linkParserResult = linkParser(linkList, this._serverURI);
if (linkParserResult.error) {
this._subInfo.reject(linkParserResult);
this._reject(linkParserResult);
return;
}
if (!subscriptionUri) {
this._subInfo.reject({error: "Return code 201, but the answer is bogus," +
" missing subscriptionUri"});
this._reject({error: "Return code 201, but the answer is bogus," +
" missing subscriptionUri"});
return;
}
try {
let uriTry = Services.io.newURI(subscriptionUri, null, null);
} catch (e) {
debug("Invalid URI " + subscriptionUri);
this._subInfo.reject({error: "Return code 201, but URI is bogus. " +
subscriptionUri});
this._reject({error: "Return code 201, but URI is bogus. " +
subscriptionUri});
return;
}
@ -295,7 +364,7 @@ SubscriptionListener.prototype = {
quota: this._subInfo.record.maxQuota,
});
this._subInfo.resolve(reply);
this._resolve(reply);
},
};
@ -456,41 +525,53 @@ this.PushServiceHttp2 = {
_subscribeResource: function(aRecord) {
debug("subscribeResource()");
return new Promise((resolve, reject) => {
this._subscribeResourceInternal({record: aRecord,
resolve,
reject,
retries: 0});
return this._subscribeResourceInternal({
record: aRecord,
retries: 0
})
.then(result => {
this._conns[result.subscriptionUri] = {channel: null,
listener: null,
countUnableToConnect: 0,
lastStartListening: 0,
waitingForAlarm: false};
this._listenForMsgs(result.subscriptionUri);
return result;
});
.then(result =>
PushServiceHttp2Crypto.generateKeys()
.then(exportedKeys => {
result.p256dhPublicKey = exportedKeys[0];
result.p256dhPrivateKey = exportedKeys[1];
this._conns[result.subscriptionUri] = {
channel: null,
listener: null,
countUnableToConnect: 0,
lastStartListening: 0,
waitingForAlarm: false
};
this._listenForMsgs(result.subscriptionUri);
return result;
})
);
},
_subscribeResourceInternal: function(aSubInfo) {
debug("subscribeResource()");
debug("subscribeResourceInternal()");
var listener = new SubscriptionListener(aSubInfo,
this._serverURI,
this);
return new Promise((resolve, reject) => {
var listener = new SubscriptionListener(aSubInfo,
resolve,
reject,
this._serverURI,
this);
var chan = this._makeChannel(this._serverURI.spec);
chan.requestMethod = "POST";
try{
chan.asyncOpen(listener, null);
} catch(e) {
aSubInfo.reject({status: 0, error: "NetworkError"});
}
},
retrySubscription: function(aSubInfo) {
this._subscribeResourceInternal(aSubInfo);
var chan = this._makeChannel(this._serverURI.spec);
chan.requestMethod = "POST";
try {
chan.asyncOpen(listener, null);
} catch(e) {
reject({status: 0, error: "NetworkError"});
}
})
.catch(err => {
if ("retry" in err) {
return this._subscribeResourceInternal(err.subInfo);
} else {
throw err;
}
})
},
_deleteResource: function(aUri) {
@ -640,19 +721,51 @@ this.PushServiceHttp2 = {
for (let i = 0; i < aSubscriptions.length; i++) {
let record = aSubscriptions[i];
if (typeof this._conns[record.subscriptionUri] != "object") {
this._conns[record.subscriptionUri] = {channel: null,
listener: null,
countUnableToConnect: 0,
waitingForAlarm: false};
}
if (!this._conns[record.subscriptionUri].conn) {
this._conns[record.subscriptionUri].waitingForAlarm = false;
this._listenForMsgs(record.subscriptionUri);
if (record.p256dhPublicKey && record.p256dhPrivateKey) {
this._startSingleConnection(record);
} else {
// We do not have a encryption key. so we need to generate it. This
// is only going to happen on db upgrade from version 4 to higher.
PushServiceHttp2Crypto.generateKeys()
.then(exportedKeys => {
if (this._mainPushService) {
return this._mainPushService
.updateRecordAndNotifyApp(record.subscriptionUri, record => {
record.p256dhPublicKey = exportedKeys[0];
record.p256dhPrivateKey = exportedKeys[1];
return record;
});
}
}, error => {
record = null;
if (this._mainPushService) {
this._mainPushService
.dropRegistrationAndNotifyApp(record.subscriptionUri);
}
})
.then(_ => {
if (record) {
this._startSingleConnection(record);
}
});
}
}
},
_startSingleConnection: function(record) {
debug("_startSingleConnection()");
if (typeof this._conns[record.subscriptionUri] != "object") {
this._conns[record.subscriptionUri] = {channel: null,
listener: null,
countUnableToConnect: 0,
waitingForAlarm: false};
}
if (!this._conns[record.subscriptionUri].conn) {
this._conns[record.subscriptionUri].waitingForAlarm = false;
this._listenForMsgs(record.subscriptionUri);
}
},
// Start listening if subscriptions present.
_startConnectionsWaitingForAlarm: function() {
debug("startConnectionsWaitingForAlarm()");
@ -756,19 +869,33 @@ this.PushServiceHttp2 = {
}
},
_pushChannelOnStop: function(aUri, aAckUri, aMessage) {
_pushChannelOnStop: function(aUri, aAckUri, aMessage, dh, salt, rs) {
debug("pushChannelOnStop() ");
this._mainPushService.receivedPushMessage(aUri, aMessage, record => {
// Always update the stored record.
return record;
this._mainPushService.getByKeyID(aUri)
.then(aPushRecord =>
PushServiceHttp2Crypto.decodeMsg(aMessage, aPushRecord.p256dhPrivateKey,
dh, salt, rs)
.then(msg => {
var msgString = '';
for (var i=0; i<msg.length; i++) {
msgString += String.fromCharCode(msg[i]);
}
return this._mainPushService.receivedPushMessage(aUri,
msgString,
record => {
// Always update the stored record.
return record;
});
})
)
.then(_ => this._ackMsgRecv(aAckUri))
.catch(err => {
debug("Error receiving message: " + err);
});
this._ackMsgRecv(aAckUri);
},
onAlarmFired: function() {
// Conditions are arranged in decreasing specificity.
// i.e. when _waitingForPong is true, other conditions are also true.
this._startConnectionsWaitingForAlarm();
},
};
@ -777,6 +904,8 @@ function PushRecordHttp2(record) {
PushRecord.call(this, record);
this.subscriptionUri = record.subscriptionUri;
this.pushReceiptEndpoint = record.pushReceiptEndpoint;
this.p256dhPublicKey = record.p256dhPublicKey;
this.p256dhPrivateKey = record.p256dhPrivateKey;
}
PushRecordHttp2.prototype = Object.create(PushRecord.prototype, {
@ -790,11 +919,13 @@ PushRecordHttp2.prototype = Object.create(PushRecord.prototype, {
PushRecordHttp2.prototype.toRegistration = function() {
let registration = PushRecord.prototype.toRegistration.call(this);
registration.pushReceiptEndpoint = this.pushReceiptEndpoint;
registration.p256dhKey = this.p256dhPublicKey;
return registration;
};
PushRecordHttp2.prototype.toRegister = function() {
let register = PushRecord.prototype.toRegister.call(this);
register.pushReceiptEndpoint = this.pushReceiptEndpoint;
register.p256dhKey = this.p256dhPublicKey;
return register;
};

Просмотреть файл

@ -0,0 +1,189 @@
/* jshint moz: true, esnext: true */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const Cu = Components.utils;
Cu.importGlobalProperties(['crypto']);
this.EXPORTED_SYMBOLS = ['PushServiceHttp2Crypto', 'concatArray'];
var ENCRYPT_INFO = new TextEncoder('utf-8').encode('Content-Encoding: aesgcm128');
var NONCE_INFO = new TextEncoder('utf-8').encode('Content-Encoding: nonce');
function chunkArray(array, size) {
var start = array.byteOffset || 0;
array = array.buffer || array;
var index = 0;
var result = [];
while(index + size <= array.byteLength) {
result.push(new Uint8Array(array, start + index, size));
index += size;
}
if (index < array.byteLength) {
result.push(new Uint8Array(array, start + index));
}
return result;
}
function base64UrlDecode(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/');
// Replace padding if it was stripped by the sender.
// See http://tools.ietf.org/html/rfc4648#section-4
switch (s.length % 4) {
case 0:
break; // No pad chars in this case
case 2:
s += '==';
break; // Two pad chars
case 3:
s += '=';
break; // One pad char
default:
throw new Error('Illegal base64url string!');
}
// With correct padding restored, apply the standard base64 decoder
var decoded = atob(s);
var array = new Uint8Array(new ArrayBuffer(decoded.length));
for (var i = 0; i < decoded.length; i++) {
array[i] = decoded.charCodeAt(i);
}
return array;
}
this.concatArray = function(arrays) {
var size = arrays.reduce((total, a) => total + a.byteLength, 0);
var index = 0;
return arrays.reduce((result, a) => {
result.set(new Uint8Array(a), index);
index += a.byteLength;
return result;
}, new Uint8Array(size));
};
var HMAC_SHA256 = { name: 'HMAC', hash: 'SHA-256' };
function hmac(key) {
this.keyPromise = crypto.subtle.importKey('raw', key, HMAC_SHA256,
false, ['sign']);
}
hmac.prototype.hash = function(input) {
return this.keyPromise.then(k => crypto.subtle.sign('HMAC', k, input));
};
function hkdf(salt, ikm) {
this.prkhPromise = new hmac(salt).hash(ikm)
.then(prk => new hmac(prk));
}
hkdf.prototype.generate = function(info, len) {
var input = concatArray([info, new Uint8Array([1])]);
return this.prkhPromise
.then(prkh => prkh.hash(input))
.then(h => {
if (h.byteLength < len) {
throw new Error('Length is too long');
}
return h.slice(0, len);
});
};
/* generate a 96-bit IV for use in GCM, 48-bits of which are populated */
function generateNonce(base, index) {
if (index >= Math.pow(2, 48)) {
throw new Error('Error generating IV - index is too large.');
}
var nonce = base.slice(0, 12);
nonce = new Uint8Array(nonce);
for (var i = 0; i < 6; ++i) {
nonce[nonce.byteLength - 1 - i] ^= (index / Math.pow(256, i)) & 0xff;
}
return nonce;
}
this.PushServiceHttp2Crypto = {
generateKeys: function() {
return crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256'},
true,
['deriveBits'])
.then(cryptoKey =>
Promise.all([
crypto.subtle.exportKey('raw', cryptoKey.publicKey),
// TODO: change this when bug 1048931 lands.
crypto.subtle.exportKey('jwk', cryptoKey.privateKey)
]));
},
decodeMsg: function(aData, aPrivateKey, aRemotePublicKey, aSalt, aRs) {
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.
if (aData.byteLength % (aRs + 16) === 0) {
return Promise.reject(new Error('Data truncated'));
}
return Promise.all([
crypto.subtle.importKey('raw', base64UrlDecode(aRemotePublicKey),
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveBits']),
crypto.subtle.importKey('jwk', aPrivateKey,
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveBits'])
])
.then(keys =>
crypto.subtle.deriveBits({ name: 'ECDH', public: keys[0] }, keys[1], 256))
.then(rawKey => {
var kdf = new hkdf(base64UrlDecode(aSalt), new Uint8Array(rawKey));
return Promise.all([
kdf.generate(ENCRYPT_INFO, 16)
.then(gcmBits =>
crypto.subtle.importKey('raw', gcmBits, 'AES-GCM', false,
['decrypt'])),
kdf.generate(NONCE_INFO, 12)
])
})
.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]))))
.then(r => concatArray(r));
},
_decodeChunk: function(aSlice, aIndex, aNonce, aKey) {
return crypto.subtle.decrypt({name: 'AES-GCM',
iv: generateNonce(aNonce, aIndex)
},
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);
}
});
}
};

Просмотреть файл

@ -20,6 +20,7 @@ EXTRA_JS_MODULES += [
'PushService.jsm',
'PushServiceChildPreload.jsm',
'PushServiceHttp2.jsm',
'PushServiceHttp2Crypto.jsm',
]
MOCHITEST_MANIFESTS += [

Просмотреть файл

@ -46,6 +46,13 @@ 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.
let db = PushServiceHttp2.newPushDB();
do_register_cleanup(() => {
return db.drop().then(_ => db.close());
@ -58,6 +65,16 @@ add_task(function* test_pushNotifications() {
pushEndpoint: serverURL + '/pushEndpoint1',
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint1',
scope: 'https://example.com/page/1',
p256dhPublicKey: 'BPCd4gNQkjwRah61LpdALdzZKLLnU5UAwDztQ5_h0QsT26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA',
p256dhPrivateKey: {
crv: 'P-256',
d: '1jUPhzVsRkzV0vIzwL4ZEsOlKdNOWm7TmaTfzitJkgM',
ext: true,
key_ops: ["deriveBits"],
kty: "EC",
x: '8J3iA1CSPBFqHrUul0At3NkosudTlQDAPO1Dn-HRCxM',
y: '26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA'
},
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
quota: Infinity,
}, {
@ -65,6 +82,16 @@ add_task(function* test_pushNotifications() {
pushEndpoint: serverURL + '/pushEndpoint2',
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint2',
scope: 'https://example.com/page/2',
p256dhPublicKey: 'BPnWyUo7yMnuMlyKtERuLfWE8a09dtdjHSW2lpC9_BqR5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E',
p256dhPrivateKey: {
crv: 'P-256',
d: 'lFm4nPsUKYgNGBJb5nXXKxl8bspCSp0bAhCYxbveqT4',
ext: true,
key_ops: ["deriveBits"],
kty: 'EC',
x: '-dbJSjvIye4yXIq0RG4t9YTxrT1212MdJbaWkL38GpE',
y: '5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E'
},
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
quota: Infinity,
}, {
@ -72,6 +99,16 @@ add_task(function* test_pushNotifications() {
pushEndpoint: serverURL + '/pushEndpoint3',
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint3',
scope: 'https://example.com/page/3',
p256dhPublicKey: 'BDhUHITSeVrWYybFnb7ylVTCDDLPdQWMpf8gXhcWwvaaJa6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI',
p256dhPrivateKey: {
crv: 'P-256',
d: 'Q1_SE1NySTYzjbqgWwPgrYh7XRg3adqZLkQPsy319G8',
ext: true,
key_ops: ["deriveBits"],
kty: 'EC',
x: 'OFQchNJ5WtZjJsWdvvKVVMIMMs91BYyl_yBeFxbC9po',
y: 'Ja6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI'
},
originAttributes: ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false }),
quota: Infinity,
}];
@ -81,9 +118,27 @@ add_task(function* test_pushNotifications() {
}
let notifyPromise = Promise.all([
promiseObserverNotification('push-notification'),
promiseObserverNotification('push-notification'),
promiseObserverNotification('push-notification')
promiseObserverNotification('push-notification', function(subject, data) {
var notification = subject.QueryInterface(Ci.nsIPushObserverNotification);
if (notification && (data == "https://example.com/page/1")){
equal(subject.data, "Some message", "decoded message is incorrect");
return true;
}
}),
promiseObserverNotification('push-notification', function(subject, data) {
var notification = subject.QueryInterface(Ci.nsIPushObserverNotification);
if (notification && (data == "https://example.com/page/2")){
equal(subject.data, "Some message", "decoded message is incorrect");
return true;
}
}),
promiseObserverNotification('push-notification', function(subject, data) {
var notification = subject.QueryInterface(Ci.nsIPushObserverNotification);
if (notification && (data == "https://example.com/page/3")){
equal(subject.data, "Some message", "decoded message is incorrect");
return true;
}
})
]);
PushService.init({

Просмотреть файл

@ -68,6 +68,16 @@ add_task(function* test1() {
pushEndpoint: serverURL + '/pushEndpoint',
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
scope: 'https://example.com/page',
p256dhPublicKey: 'BPCd4gNQkjwRah61LpdALdzZKLLnU5UAwDztQ5_h0QsT26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA',
p256dhPrivateKey: {
crv: 'P-256',
d: '1jUPhzVsRkzV0vIzwL4ZEsOlKdNOWm7TmaTfzitJkgM',
ext: true,
key_ops: ["deriveBits"],
kty: "EC",
x: '8J3iA1CSPBFqHrUul0At3NkosudTlQDAPO1Dn-HRCxM',
y: '26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA'
},
originAttributes: '',
quota: Infinity,
}];

Просмотреть файл

@ -0,0 +1,80 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/httpd.js");
const {PushDB, PushService, PushServiceHttp2} = serviceExports;
var httpServer = null;
XPCOMUtils.defineLazyGetter(this, "serverPort", function() {
return httpServer.identity.primaryPort;
});
function listenHandler(metadata, response) {
do_check_true(true, "Start listening");
httpServer.stop(do_test_finished);
response.setHeader("Retry-After", "10");
response.setStatusLine(metadata.httpVersion, 500, "Retry");
}
httpServer = new HttpServer();
httpServer.registerPathHandler("/subscriptionNoKey", listenHandler);
httpServer.start(-1);
function run_test() {
do_get_profile();
setPrefs({
'http2.retryInterval': 1000,
'http2.maxRetries': 2
});
disableServiceWorkerEvents(
'https://example.com/page'
);
run_next_test();
}
add_task(function* test1() {
let db = PushServiceHttp2.newPushDB();
do_register_cleanup(_ => {
return db.drop().then(_ => db.close());
});
do_test_pending();
var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
let record = {
subscriptionUri: serverURL + '/subscriptionNoKey',
pushEndpoint: serverURL + '/pushEndpoint',
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
scope: 'https://example.com/page',
originAttributes: '',
quota: Infinity,
};
yield db.put(record);
let notifyPromise = promiseObserverNotification('push-subscription-change',
_ => true);
PushService.init({
serverURI: serverURL + "/subscribe",
service: PushServiceHttp2,
db
});
yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
'Timed out waiting for notifications');
let aRecord = yield db.getByKeyID(serverURL + '/subscriptionNoKey');
ok(aRecord, 'The record should still be there');
ok(aRecord.p256dhPublicKey, 'There should be a public key');
ok(aRecord.p256dhPrivateKey, 'There should be a private key');
});

Просмотреть файл

@ -38,6 +38,7 @@ skip-if = toolkit == 'android'
[test_resubscribe_5xxCode_http2.js]
[test_resubscribe_listening_for_msg_error_http2.js]
[test_register_5xxCode_http2.js]
[test_updateRecordNoEncryptionKeys.js]
[test_register_success_http2.js]
skip-if = !hasNode
run-sequentially = node server exceptions dont replay well

Просмотреть файл

@ -9,11 +9,17 @@
interface Principal;
enum PushEncryptionKeyName
{
"p256dh"
};
[Exposed=(Window,Worker), Func="nsContentUtils::PushEnabled",
ChromeConstructor(DOMString pushEndpoint, DOMString scope)]
ChromeConstructor(DOMString pushEndpoint, DOMString scope, ArrayBuffer? key)]
interface PushSubscription
{
readonly attribute USVString endpoint;
ArrayBuffer? getKey(PushEncryptionKeyName name);
[Throws, UseCounter]
Promise<boolean> unsubscribe();
jsonifier;

Просмотреть файл

@ -487,40 +487,49 @@ function handleRequest(req, res) {
else if (u.pathname ==="/pushNotifications/subscription1") {
pushPushServer1 = res.push(
{ hostname: 'localhost:' + serverPort, port: serverPort,
path : '/pushNotificationsDeliver1', method : 'GET',
headers: {'x-pushed-request': 'true', 'x-foo' : 'bar'}});
{ hostname: 'localhost:' + serverPort, port: serverPort,
path : '/pushNotificationsDeliver1', method : 'GET',
headers: { 'Encryption-Key': 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
'Encryption': 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"'
}
});
pushPushServer1.writeHead(200, {
'content-length' : 2,
'subresource' : '1'
});
pushPushServer1.end('ok');
pushPushServer1.end('370aeb3963f12c4f12bf946bd0a7a9ee7d3eaff8f7aec62b530fc25cfa', 'hex');
return;
}
else if (u.pathname ==="/pushNotifications/subscription2") {
pushPushServer2 = res.push(
{ hostname: 'localhost:' + serverPort, port: serverPort,
path : '/pushNotificationsDeliver3', method : 'GET',
headers: {'x-pushed-request': 'true', 'x-foo' : 'bar'}});
{ hostname: 'localhost:' + serverPort, port: serverPort,
path : '/pushNotificationsDeliver3', method : 'GET',
headers: { 'Encryption-Key': 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
'Encryption': 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"'
}
});
pushPushServer2.writeHead(200, {
'content-length' : 2,
'subresource' : '1'
});
pushPushServer2.end('ok');
pushPushServer2.end('66df5d11daa01e5c802ff97cdf7f39684b5bf7c6418a5cf9b609c6826c04b25e403823607ac514278a7da945', 'hex');
return;
}
else if (u.pathname ==="/pushNotifications/subscription3") {
pushPushServer3 = res.push(
{ hostname: 'localhost:' + serverPort, port: serverPort,
path : '/pushNotificationsDeliver3', method : 'GET',
headers: {'x-pushed-request': 'true', 'x-foo' : 'bar'}});
{ 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'
}
});
pushPushServer3.writeHead(200, {
'content-length' : 2,
'subresource' : '1'
});
pushPushServer3.end('ok');
pushPushServer3.end('2caaeedd9cf1059b80c58b6c6827da8ff7de864ac8bea6d5775892c27c005209cbf9c4de0c3fbcddb9711d74eaeebd33f7275374cb42dd48c07168bc2cc9df63e045ce2d2a2408c66088a40c', 'hex');
return;
}