зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
314f131577
Коммит
a7f75d48d4
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче