mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-03 13:57:31 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
284
dom/push/Push.js
Normal file
284
dom/push/Push.js
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
/* 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 Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "console", () => {
|
||||
let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
|
||||
return new ConsoleAPI({
|
||||
maxLogLevelPref: "dom.push.loglevel",
|
||||
prefix: "Push",
|
||||
});
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "PushService",
|
||||
"@mozilla.org/push/Service;1", "nsIPushService");
|
||||
|
||||
const PUSH_CID = Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}");
|
||||
|
||||
/**
|
||||
* The Push component runs in the child process and exposes the Push API
|
||||
* to the web application. The PushService running in the parent process is the
|
||||
* one actually performing all operations.
|
||||
*/
|
||||
function Push() {
|
||||
console.debug("Push()");
|
||||
}
|
||||
|
||||
Push.prototype = {
|
||||
__proto__: DOMRequestIpcHelper.prototype,
|
||||
|
||||
contractID: "@mozilla.org/push/PushManager;1",
|
||||
|
||||
classID : PUSH_CID,
|
||||
|
||||
QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer,
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsIObserver]),
|
||||
|
||||
init: function(win) {
|
||||
console.debug("init()");
|
||||
|
||||
this._window = win;
|
||||
|
||||
this.initDOMRequestHelper(win);
|
||||
|
||||
this._principal = win.document.nodePrincipal;
|
||||
},
|
||||
|
||||
__init: function(scope) {
|
||||
this._scope = scope;
|
||||
},
|
||||
|
||||
askPermission: function () {
|
||||
console.debug("askPermission()");
|
||||
|
||||
return this.createPromise((resolve, reject) => {
|
||||
let permissionDenied = () => {
|
||||
reject(new this._window.DOMException(
|
||||
"User denied permission to use the Push API.",
|
||||
"NotAllowedError"
|
||||
));
|
||||
};
|
||||
|
||||
let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
|
||||
try {
|
||||
permission = this._testPermission();
|
||||
} catch (e) {
|
||||
permissionDenied();
|
||||
return;
|
||||
}
|
||||
|
||||
if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
|
||||
resolve();
|
||||
} else if (permission == Ci.nsIPermissionManager.DENY_ACTION) {
|
||||
permissionDenied();
|
||||
} else {
|
||||
this._requestPermission(resolve, permissionDenied);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
subscribe: function(options) {
|
||||
console.debug("subscribe()", this._scope);
|
||||
|
||||
let histogram = Services.telemetry.getHistogramById("PUSH_API_USED");
|
||||
histogram.add(true);
|
||||
return this.askPermission().then(() =>
|
||||
this.createPromise((resolve, reject) => {
|
||||
let callback = new PushSubscriptionCallback(this, resolve, reject);
|
||||
|
||||
if (!options || !options.applicationServerKey) {
|
||||
PushService.subscribe(this._scope, this._principal, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
let appServerKey = options.applicationServerKey;
|
||||
let keyView = new this._window.Uint8Array(ArrayBuffer.isView(appServerKey) ?
|
||||
appServerKey.buffer : appServerKey);
|
||||
if (keyView.byteLength === 0) {
|
||||
callback._rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
|
||||
return;
|
||||
}
|
||||
PushService.subscribeWithKey(this._scope, this._principal,
|
||||
appServerKey.length, appServerKey,
|
||||
callback);
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
getSubscription: function() {
|
||||
console.debug("getSubscription()", this._scope);
|
||||
|
||||
return this.createPromise((resolve, reject) => {
|
||||
let callback = new PushSubscriptionCallback(this, resolve, reject);
|
||||
PushService.getSubscription(this._scope, this._principal, callback);
|
||||
});
|
||||
},
|
||||
|
||||
permissionState: function() {
|
||||
console.debug("permissionState()", this._scope);
|
||||
|
||||
return this.createPromise((resolve, reject) => {
|
||||
let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
|
||||
|
||||
try {
|
||||
permission = this._testPermission();
|
||||
} catch(e) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
let pushPermissionStatus = "prompt";
|
||||
if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
|
||||
pushPermissionStatus = "granted";
|
||||
} else if (permission == Ci.nsIPermissionManager.DENY_ACTION) {
|
||||
pushPermissionStatus = "denied";
|
||||
}
|
||||
resolve(pushPermissionStatus);
|
||||
});
|
||||
},
|
||||
|
||||
_testPermission: function() {
|
||||
let permission = Services.perms.testExactPermissionFromPrincipal(
|
||||
this._principal, "desktop-notification");
|
||||
if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
|
||||
return permission;
|
||||
}
|
||||
try {
|
||||
if (Services.prefs.getBoolPref("dom.push.testing.ignorePermission")) {
|
||||
permission = Ci.nsIPermissionManager.ALLOW_ACTION;
|
||||
}
|
||||
} catch (e) {}
|
||||
return permission;
|
||||
},
|
||||
|
||||
_requestPermission: function(allowCallback, cancelCallback) {
|
||||
// Create an array with a single nsIContentPermissionType element.
|
||||
let type = {
|
||||
type: "desktop-notification",
|
||||
access: null,
|
||||
options: [],
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionType]),
|
||||
};
|
||||
let typeArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
|
||||
typeArray.appendElement(type, false);
|
||||
|
||||
// create a nsIContentPermissionRequest
|
||||
let request = {
|
||||
types: typeArray,
|
||||
principal: this._principal,
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionRequest]),
|
||||
allow: function() {
|
||||
let histogram = Services.telemetry.getHistogramById("PUSH_API_PERMISSION_GRANTED");
|
||||
histogram.add();
|
||||
allowCallback();
|
||||
},
|
||||
cancel: function() {
|
||||
let histogram = Services.telemetry.getHistogramById("PUSH_API_PERMISSION_DENIED");
|
||||
histogram.add();
|
||||
cancelCallback();
|
||||
},
|
||||
window: this._window,
|
||||
};
|
||||
|
||||
let histogram = Services.telemetry.getHistogramById("PUSH_API_PERMISSION_REQUESTED");
|
||||
histogram.add(1);
|
||||
// Using askPermission from nsIDOMWindowUtils that takes care of the
|
||||
// remoting if needed.
|
||||
let windowUtils = this._window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
windowUtils.askPermission(request);
|
||||
},
|
||||
};
|
||||
|
||||
function PushSubscriptionCallback(pushManager, resolve, reject) {
|
||||
this.pushManager = pushManager;
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
}
|
||||
|
||||
PushSubscriptionCallback.prototype = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPushSubscriptionCallback]),
|
||||
|
||||
onPushSubscription: function(ok, subscription) {
|
||||
let {pushManager} = this;
|
||||
if (!Components.isSuccessCode(ok)) {
|
||||
this._rejectWithError(ok);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
this.resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let p256dhKey = this._getKey(subscription, "p256dh");
|
||||
let authSecret = this._getKey(subscription, "auth");
|
||||
let options = {
|
||||
endpoint: subscription.endpoint,
|
||||
scope: pushManager._scope,
|
||||
p256dhKey: p256dhKey,
|
||||
authSecret: authSecret,
|
||||
};
|
||||
let appServerKey = this._getKey(subscription, "appServer");
|
||||
if (appServerKey) {
|
||||
// Avoid passing null keys to work around bug 1256449.
|
||||
options.appServerKey = appServerKey;
|
||||
}
|
||||
let sub = new pushManager._window.PushSubscription(options);
|
||||
this.resolve(sub);
|
||||
},
|
||||
|
||||
_getKey: function(subscription, name) {
|
||||
let outKeyLen = {};
|
||||
let rawKey = Cu.cloneInto(subscription.getKey(name, outKeyLen),
|
||||
this.pushManager._window);
|
||||
if (!outKeyLen.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let key = new this.pushManager._window.ArrayBuffer(outKeyLen.value);
|
||||
let keyView = new this.pushManager._window.Uint8Array(key);
|
||||
keyView.set(rawKey);
|
||||
return key;
|
||||
},
|
||||
|
||||
_rejectWithError: function(result) {
|
||||
let error;
|
||||
switch (result) {
|
||||
case Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR:
|
||||
error = new this.pushManager._window.DOMException(
|
||||
"Invalid raw ECDSA P-256 public key.",
|
||||
"InvalidAccessError"
|
||||
);
|
||||
break;
|
||||
|
||||
case Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR:
|
||||
error = new this.pushManager._window.DOMException(
|
||||
"A subscription with a different application server key already exists.",
|
||||
"InvalidStateError"
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
error = new this.pushManager._window.DOMException(
|
||||
"Error retrieving push subscription.",
|
||||
"AbortError"
|
||||
);
|
||||
}
|
||||
this.reject(error);
|
||||
},
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Push]);
|
||||
11
dom/push/Push.manifest
Normal file
11
dom/push/Push.manifest
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# DOM API
|
||||
component {cde1d019-fad8-4044-b141-65fb4fb7a245} Push.js
|
||||
contract @mozilla.org/push/PushManager;1 {cde1d019-fad8-4044-b141-65fb4fb7a245}
|
||||
|
||||
# XPCOM components.
|
||||
component {daaa8d73-677e-4233-8acd-2c404bd01658} PushComponents.js
|
||||
contract @mozilla.org/push/Service;1 {daaa8d73-677e-4233-8acd-2c404bd01658}
|
||||
category app-startup PushServiceParent @mozilla.org/push/Service;1
|
||||
|
||||
# For immediate loading of PushService instead of delayed loading.
|
||||
category android-push-service PushServiceParent @mozilla.org/push/Service;1
|
||||
558
dom/push/PushComponents.js
Normal file
558
dom/push/PushComponents.js
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* This file exports XPCOM components for C++ and chrome JavaScript callers to
|
||||
* interact with the Push service.
|
||||
*/
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
||||
|
||||
// The default Push service implementation.
|
||||
XPCOMUtils.defineLazyGetter(this, "PushService", function() {
|
||||
const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
|
||||
{});
|
||||
PushService.init();
|
||||
return PushService;
|
||||
});
|
||||
|
||||
// Observer notification topics for push messages and subscription status
|
||||
// changes. These are duplicated and used in `nsIPushNotifier`. They're exposed
|
||||
// on `nsIPushService` so that JS callers only need to import this service.
|
||||
const OBSERVER_TOPIC_PUSH = "push-message";
|
||||
const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
|
||||
const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified";
|
||||
|
||||
/**
|
||||
* `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
|
||||
* implement the `nsIPushService` interface. This interface provides calls
|
||||
* similar to the Push DOM API, but does not require service workers.
|
||||
*
|
||||
* Push service methods may be called from the parent or content process. The
|
||||
* parent process implementation loads `PushService.jsm` at app startup, and
|
||||
* calls its methods directly. The content implementation forwards calls to
|
||||
* the parent Push service via IPC.
|
||||
*
|
||||
* The implementations share a class and contract ID.
|
||||
*/
|
||||
function PushServiceBase() {
|
||||
this.wrappedJSObject = this;
|
||||
this._addListeners();
|
||||
}
|
||||
|
||||
PushServiceBase.prototype = {
|
||||
classID: Components.ID("{daaa8d73-677e-4233-8acd-2c404bd01658}"),
|
||||
contractID: "@mozilla.org/push/Service;1",
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsIPushService,
|
||||
Ci.nsIPushQuotaManager,
|
||||
Ci.nsIPushErrorReporter,
|
||||
]),
|
||||
|
||||
pushTopic: OBSERVER_TOPIC_PUSH,
|
||||
subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
|
||||
subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
|
||||
|
||||
_handleReady() {},
|
||||
|
||||
_addListeners() {
|
||||
for (let message of this._messages) {
|
||||
this._mm.addMessageListener(message, this);
|
||||
}
|
||||
},
|
||||
|
||||
_isValidMessage(message) {
|
||||
return this._messages.includes(message.name);
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
if (topic === "app-startup") {
|
||||
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
|
||||
return;
|
||||
}
|
||||
if (topic === "sessionstore-windows-restored") {
|
||||
Services.obs.removeObserver(this, "sessionstore-windows-restored");
|
||||
this._handleReady();
|
||||
return;
|
||||
}
|
||||
if (topic === "android-push-service") {
|
||||
// Load PushService immediately.
|
||||
this._handleReady();
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
_deliverSubscription(request, props) {
|
||||
if (!props) {
|
||||
request.onPushSubscription(Cr.NS_OK, null);
|
||||
return;
|
||||
}
|
||||
request.onPushSubscription(Cr.NS_OK, new PushSubscription(props));
|
||||
},
|
||||
|
||||
_deliverSubscriptionError(request, error) {
|
||||
let result = typeof error.result == "number" ?
|
||||
error.result : Cr.NS_ERROR_FAILURE;
|
||||
request.onPushSubscription(result, null);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The parent process implementation of `nsIPushService`. This version loads
|
||||
* `PushService.jsm` at startup and calls its methods directly. It also
|
||||
* receives and responds to requests from the content process.
|
||||
*/
|
||||
function PushServiceParent() {
|
||||
PushServiceBase.call(this);
|
||||
}
|
||||
|
||||
PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(PushServiceParent.prototype, "_mm",
|
||||
"@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
|
||||
|
||||
Object.assign(PushServiceParent.prototype, {
|
||||
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceParent),
|
||||
|
||||
_messages: [
|
||||
"Push:Register",
|
||||
"Push:Registration",
|
||||
"Push:Unregister",
|
||||
"Push:Clear",
|
||||
"Push:NotificationForOriginShown",
|
||||
"Push:NotificationForOriginClosed",
|
||||
"Push:ReportError",
|
||||
],
|
||||
|
||||
// nsIPushService methods
|
||||
|
||||
subscribe(scope, principal, callback) {
|
||||
this.subscribeWithKey(scope, principal, 0, null, callback);
|
||||
},
|
||||
|
||||
subscribeWithKey(scope, principal, keyLen, key, callback) {
|
||||
this._handleRequest("Push:Register", principal, {
|
||||
scope: scope,
|
||||
appServerKey: key,
|
||||
}).then(result => {
|
||||
this._deliverSubscription(callback, result);
|
||||
}, error => {
|
||||
this._deliverSubscriptionError(callback, error);
|
||||
}).catch(Cu.reportError);
|
||||
},
|
||||
|
||||
unsubscribe(scope, principal, callback) {
|
||||
this._handleRequest("Push:Unregister", principal, {
|
||||
scope: scope,
|
||||
}).then(result => {
|
||||
callback.onUnsubscribe(Cr.NS_OK, result);
|
||||
}, error => {
|
||||
callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
|
||||
}).catch(Cu.reportError);
|
||||
},
|
||||
|
||||
getSubscription(scope, principal, callback) {
|
||||
return this._handleRequest("Push:Registration", principal, {
|
||||
scope: scope,
|
||||
}).then(result => {
|
||||
this._deliverSubscription(callback, result);
|
||||
}, error => {
|
||||
this._deliverSubscriptionError(callback, error);
|
||||
}).catch(Cu.reportError);
|
||||
},
|
||||
|
||||
clearForDomain(domain, callback) {
|
||||
return this._handleRequest("Push:Clear", null, {
|
||||
domain: domain,
|
||||
}).then(result => {
|
||||
callback.onClear(Cr.NS_OK);
|
||||
}, error => {
|
||||
callback.onClear(Cr.NS_ERROR_FAILURE);
|
||||
}).catch(Cu.reportError);
|
||||
},
|
||||
|
||||
// nsIPushQuotaManager methods
|
||||
|
||||
notificationForOriginShown(origin) {
|
||||
this.service.notificationForOriginShown(origin);
|
||||
},
|
||||
|
||||
notificationForOriginClosed(origin) {
|
||||
this.service.notificationForOriginClosed(origin);
|
||||
},
|
||||
|
||||
// nsIPushErrorReporter methods
|
||||
|
||||
reportDeliveryError(messageId, reason) {
|
||||
this.service.reportDeliveryError(messageId, reason);
|
||||
},
|
||||
|
||||
receiveMessage(message) {
|
||||
if (!this._isValidMessage(message)) {
|
||||
return;
|
||||
}
|
||||
let {name, principal, target, data} = message;
|
||||
if (name === "Push:NotificationForOriginShown") {
|
||||
this.notificationForOriginShown(data);
|
||||
return;
|
||||
}
|
||||
if (name === "Push:NotificationForOriginClosed") {
|
||||
this.notificationForOriginClosed(data);
|
||||
return;
|
||||
}
|
||||
if (!target.assertPermission("push")) {
|
||||
return;
|
||||
}
|
||||
if (name === "Push:ReportError") {
|
||||
this.reportDeliveryError(data.messageId, data.reason);
|
||||
return;
|
||||
}
|
||||
let sender = target.QueryInterface(Ci.nsIMessageSender);
|
||||
return this._handleRequest(name, principal, data).then(result => {
|
||||
sender.sendAsyncMessage(this._getResponseName(name, "OK"), {
|
||||
requestID: data.requestID,
|
||||
result: result
|
||||
});
|
||||
}, error => {
|
||||
sender.sendAsyncMessage(this._getResponseName(name, "KO"), {
|
||||
requestID: data.requestID,
|
||||
result: error.result,
|
||||
});
|
||||
}).catch(Cu.reportError);
|
||||
},
|
||||
|
||||
_handleReady() {
|
||||
this.service.init();
|
||||
},
|
||||
|
||||
_toPageRecord(principal, data) {
|
||||
if (!data.scope) {
|
||||
throw new Error("Invalid page record: missing scope");
|
||||
}
|
||||
if (!principal) {
|
||||
throw new Error("Invalid page record: missing principal");
|
||||
}
|
||||
if (principal.isNullPrincipal || principal.isExpandedPrincipal) {
|
||||
throw new Error("Invalid page record: unsupported principal");
|
||||
}
|
||||
|
||||
// System subscriptions can only be created by chrome callers, and are
|
||||
// exempt from the background message quota and permission checks. They
|
||||
// also do not fire service worker events.
|
||||
data.systemRecord = principal.isSystemPrincipal;
|
||||
|
||||
data.originAttributes =
|
||||
ChromeUtils.originAttributesToSuffix(principal.originAttributes);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
_handleRequest(name, principal, data) {
|
||||
if (name == "Push:Clear") {
|
||||
return this.service.clear(data);
|
||||
}
|
||||
|
||||
let pageRecord;
|
||||
try {
|
||||
pageRecord = this._toPageRecord(principal, data);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
if (name === "Push:Register") {
|
||||
return this.service.register(pageRecord);
|
||||
}
|
||||
if (name === "Push:Registration") {
|
||||
return this.service.registration(pageRecord);
|
||||
}
|
||||
if (name === "Push:Unregister") {
|
||||
return this.service.unregister(pageRecord);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error("Invalid request: unknown name"));
|
||||
},
|
||||
|
||||
_getResponseName(requestName, suffix) {
|
||||
let name = requestName.slice("Push:".length);
|
||||
return "PushService:" + name + ":" + suffix;
|
||||
},
|
||||
|
||||
// Methods used for mocking in tests.
|
||||
|
||||
replaceServiceBackend(options) {
|
||||
return this.service.changeTestServer(options.serverURI, options);
|
||||
},
|
||||
|
||||
restoreServiceBackend() {
|
||||
var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
|
||||
return this.service.changeTestServer(defaultServerURL);
|
||||
},
|
||||
});
|
||||
|
||||
// Used to replace the implementation with a mock.
|
||||
Object.defineProperty(PushServiceParent.prototype, "service", {
|
||||
get() {
|
||||
return this._service || PushService;
|
||||
},
|
||||
set(impl) {
|
||||
this._service = impl;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The content process implementation of `nsIPushService`. This version
|
||||
* uses the child message manager to forward calls to the parent process.
|
||||
* The parent Push service instance handles the request, and responds with a
|
||||
* message containing the result.
|
||||
*/
|
||||
function PushServiceContent() {
|
||||
PushServiceBase.apply(this, arguments);
|
||||
this._requests = new Map();
|
||||
this._requestId = 0;
|
||||
}
|
||||
|
||||
PushServiceContent.prototype = Object.create(PushServiceBase.prototype);
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(PushServiceContent.prototype,
|
||||
"_mm", "@mozilla.org/childprocessmessagemanager;1",
|
||||
"nsISyncMessageSender");
|
||||
|
||||
Object.assign(PushServiceContent.prototype, {
|
||||
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceContent),
|
||||
|
||||
_messages: [
|
||||
"PushService:Register:OK",
|
||||
"PushService:Register:KO",
|
||||
"PushService:Registration:OK",
|
||||
"PushService:Registration:KO",
|
||||
"PushService:Unregister:OK",
|
||||
"PushService:Unregister:KO",
|
||||
"PushService:Clear:OK",
|
||||
"PushService:Clear:KO",
|
||||
],
|
||||
|
||||
// nsIPushService methods
|
||||
|
||||
subscribe(scope, principal, callback) {
|
||||
this.subscribeWithKey(scope, principal, 0, null, callback);
|
||||
},
|
||||
|
||||
subscribeWithKey(scope, principal, keyLen, key, callback) {
|
||||
let requestId = this._addRequest(callback);
|
||||
this._mm.sendAsyncMessage("Push:Register", {
|
||||
scope: scope,
|
||||
appServerKey: key,
|
||||
requestID: requestId,
|
||||
}, null, principal);
|
||||
},
|
||||
|
||||
unsubscribe(scope, principal, callback) {
|
||||
let requestId = this._addRequest(callback);
|
||||
this._mm.sendAsyncMessage("Push:Unregister", {
|
||||
scope: scope,
|
||||
requestID: requestId,
|
||||
}, null, principal);
|
||||
},
|
||||
|
||||
getSubscription(scope, principal, callback) {
|
||||
let requestId = this._addRequest(callback);
|
||||
this._mm.sendAsyncMessage("Push:Registration", {
|
||||
scope: scope,
|
||||
requestID: requestId,
|
||||
}, null, principal);
|
||||
},
|
||||
|
||||
clearForDomain(domain, callback) {
|
||||
let requestId = this._addRequest(callback);
|
||||
this._mm.sendAsyncMessage("Push:Clear", {
|
||||
domain: domain,
|
||||
requestID: requestId,
|
||||
});
|
||||
},
|
||||
|
||||
// nsIPushQuotaManager methods
|
||||
|
||||
notificationForOriginShown(origin) {
|
||||
this._mm.sendAsyncMessage("Push:NotificationForOriginShown", origin);
|
||||
},
|
||||
|
||||
notificationForOriginClosed(origin) {
|
||||
this._mm.sendAsyncMessage("Push:NotificationForOriginClosed", origin);
|
||||
},
|
||||
|
||||
// nsIPushErrorReporter methods
|
||||
|
||||
reportDeliveryError(messageId, reason) {
|
||||
this._mm.sendAsyncMessage("Push:ReportError", {
|
||||
messageId: messageId,
|
||||
reason: reason,
|
||||
});
|
||||
},
|
||||
|
||||
_addRequest(data) {
|
||||
let id = ++this._requestId;
|
||||
this._requests.set(id, data);
|
||||
return id;
|
||||
},
|
||||
|
||||
_takeRequest(requestId) {
|
||||
let d = this._requests.get(requestId);
|
||||
this._requests.delete(requestId);
|
||||
return d;
|
||||
},
|
||||
|
||||
receiveMessage(message) {
|
||||
if (!this._isValidMessage(message)) {
|
||||
return;
|
||||
}
|
||||
let {name, data} = message;
|
||||
let request = this._takeRequest(data.requestID);
|
||||
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "PushService:Register:OK":
|
||||
case "PushService:Registration:OK":
|
||||
this._deliverSubscription(request, data.result);
|
||||
break;
|
||||
|
||||
case "PushService:Register:KO":
|
||||
case "PushService:Registration:KO":
|
||||
this._deliverSubscriptionError(request, data);
|
||||
break;
|
||||
|
||||
case "PushService:Unregister:OK":
|
||||
if (typeof data.result === "boolean") {
|
||||
request.onUnsubscribe(Cr.NS_OK, data.result);
|
||||
} else {
|
||||
request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
|
||||
}
|
||||
break;
|
||||
|
||||
case "PushService:Unregister:KO":
|
||||
request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
|
||||
break;
|
||||
|
||||
case "PushService:Clear:OK":
|
||||
request.onClear(Cr.NS_OK);
|
||||
break;
|
||||
|
||||
case "PushService:Clear:KO":
|
||||
request.onClear(Cr.NS_ERROR_FAILURE);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** `PushSubscription` instances are passed to all subscription callbacks. */
|
||||
function PushSubscription(props) {
|
||||
this._props = props;
|
||||
}
|
||||
|
||||
PushSubscription.prototype = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPushSubscription]),
|
||||
|
||||
/** The URL for sending messages to this subscription. */
|
||||
get endpoint() {
|
||||
return this._props.endpoint;
|
||||
},
|
||||
|
||||
/** The last time a message was sent to this subscription. */
|
||||
get lastPush() {
|
||||
return this._props.lastPush;
|
||||
},
|
||||
|
||||
/** The total number of messages sent to this subscription. */
|
||||
get pushCount() {
|
||||
return this._props.pushCount;
|
||||
},
|
||||
|
||||
/** The number of remaining background messages that can be sent to this
|
||||
* subscription, or -1 of the subscription is exempt from the quota.
|
||||
*/
|
||||
get quota() {
|
||||
return this._props.quota;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates whether this subscription was created with the system principal.
|
||||
* System subscriptions are exempt from the background message quota and
|
||||
* permission checks.
|
||||
*/
|
||||
get isSystemSubscription() {
|
||||
return !!this._props.systemRecord;
|
||||
},
|
||||
|
||||
/** The private key used to decrypt incoming push messages, in JWK format */
|
||||
get p256dhPrivateKey() {
|
||||
return this._props.p256dhPrivateKey;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates whether this subscription is subject to the background message
|
||||
* quota.
|
||||
*/
|
||||
quotaApplies() {
|
||||
return this.quota >= 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates whether this subscription exceeded the background message quota,
|
||||
* or the user revoked the notification permission. The caller must request a
|
||||
* new subscription to continue receiving push messages.
|
||||
*/
|
||||
isExpired() {
|
||||
return this.quota === 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a key for encrypting messages sent to this subscription. JS
|
||||
* callers receive the key buffer as a return value, while C++ callers
|
||||
* receive the key size and buffer as out parameters.
|
||||
*/
|
||||
getKey(name, outKeyLen) {
|
||||
switch (name) {
|
||||
case "p256dh":
|
||||
return this._getRawKey(this._props.p256dhKey, outKeyLen);
|
||||
|
||||
case "auth":
|
||||
return this._getRawKey(this._props.authenticationSecret, outKeyLen);
|
||||
|
||||
case "appServer":
|
||||
return this._getRawKey(this._props.appServerKey, outKeyLen);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
_getRawKey(key, outKeyLen) {
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
let rawKey = new Uint8Array(key);
|
||||
if (outKeyLen) {
|
||||
outKeyLen.value = rawKey.length;
|
||||
}
|
||||
return rawKey;
|
||||
},
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
|
||||
// Export the correct implementation depending on whether we're running in
|
||||
// the parent or content process.
|
||||
isParent ? PushServiceParent : PushServiceContent,
|
||||
]);
|
||||
454
dom/push/PushCrypto.jsm
Normal file
454
dom/push/PushCrypto.jsm
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
/* 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.import('resource://gre/modules/Services.jsm');
|
||||
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, 'gDOMBundle', () =>
|
||||
Services.strings.createBundle('chrome://global/locale/dom/dom.properties'));
|
||||
|
||||
Cu.importGlobalProperties(['crypto']);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray'];
|
||||
|
||||
var UTF8 = new TextEncoder('utf-8');
|
||||
|
||||
// Legacy encryption scheme (draft-thomson-http-encryption-02).
|
||||
var AESGCM128_ENCODING = 'aesgcm128';
|
||||
var AESGCM128_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
|
||||
|
||||
// New encryption scheme (draft-ietf-httpbis-encryption-encoding-01).
|
||||
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' };
|
||||
var ECDSA_KEY = { name: 'ECDSA', namedCurve: 'P-256' };
|
||||
// A default keyid with a name that won't conflict with a real keyid.
|
||||
var DEFAULT_KEYID = '';
|
||||
|
||||
/** Localized error property names. */
|
||||
|
||||
// `Encryption` header missing or malformed.
|
||||
const BAD_ENCRYPTION_HEADER = 'PushMessageBadEncryptionHeader';
|
||||
// `Crypto-Key` or legacy `Encryption-Key` header missing.
|
||||
const BAD_CRYPTO_KEY_HEADER = 'PushMessageBadCryptoKeyHeader';
|
||||
const BAD_ENCRYPTION_KEY_HEADER = 'PushMessageBadEncryptionKeyHeader';
|
||||
// `Content-Encoding` header missing or contains unsupported encoding.
|
||||
const BAD_ENCODING_HEADER = 'PushMessageBadEncodingHeader';
|
||||
// `dh` parameter of `Crypto-Key` header missing or not base64url-encoded.
|
||||
const BAD_DH_PARAM = 'PushMessageBadSenderKey';
|
||||
// `salt` parameter of `Encryption` header missing or not base64url-encoded.
|
||||
const BAD_SALT_PARAM = 'PushMessageBadSalt';
|
||||
// `rs` parameter of `Encryption` header not a number or less than pad size.
|
||||
const BAD_RS_PARAM = 'PushMessageBadRecordSize';
|
||||
// Invalid or insufficient padding for encrypted chunk.
|
||||
const BAD_PADDING = 'PushMessageBadPaddingError';
|
||||
// Generic crypto error.
|
||||
const BAD_CRYPTO = 'PushMessageBadCryptoError';
|
||||
|
||||
class CryptoError extends Error {
|
||||
/**
|
||||
* Creates an error object indicating an incoming push message could not be
|
||||
* decrypted.
|
||||
*
|
||||
* @param {String} message A human-readable error message. This is only for
|
||||
* internal module logging, and doesn't need to be localized.
|
||||
* @param {String} property The localized property name from `dom.properties`.
|
||||
* @param {String...} params Substitutions to insert into the localized
|
||||
* string.
|
||||
*/
|
||||
constructor(message, property, ...params) {
|
||||
super(message);
|
||||
this.isCryptoError = true;
|
||||
this.property = property;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a localized string for reporting decryption errors to the Web
|
||||
* Console.
|
||||
*
|
||||
* @param {String} scope The scope of the service worker receiving the
|
||||
* message, prepended to any other substitutions in the string.
|
||||
* @returns {String} The localized string.
|
||||
*/
|
||||
format(scope) {
|
||||
let params = [scope, ...this.params].map(String);
|
||||
return gDOMBundle.formatStringFromName(this.property, params,
|
||||
params.length);
|
||||
}
|
||||
}
|
||||
|
||||
function getEncryptionKeyParams(encryptKeyField) {
|
||||
if (!encryptKeyField) {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (!m[DEFAULT_KEYID] && pmap.dh) {
|
||||
m[DEFAULT_KEYID] = pmap.dh;
|
||||
}
|
||||
return m;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getEncryptionParams(encryptField) {
|
||||
if (!encryptField) {
|
||||
throw new CryptoError('Missing encryption header',
|
||||
BAD_ENCRYPTION_HEADER);
|
||||
}
|
||||
var p = encryptField.split(',', 1)[0];
|
||||
if (!p) {
|
||||
throw new CryptoError('Encryption header missing params',
|
||||
BAD_ENCRYPTION_HEADER);
|
||||
}
|
||||
return p.split(';').reduce(parseHeaderFieldParams, {});
|
||||
}
|
||||
|
||||
function getCryptoParams(headers) {
|
||||
if (!headers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var keymap;
|
||||
var padSize;
|
||||
if (!headers.encoding) {
|
||||
throw new CryptoError('Missing Content-Encoding header',
|
||||
BAD_ENCODING_HEADER);
|
||||
}
|
||||
if (headers.encoding == AESGCM_ENCODING) {
|
||||
// aesgcm uses the Crypto-Key header, 2 bytes for the pad length, and an
|
||||
// authentication secret.
|
||||
// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-01
|
||||
keymap = getEncryptionKeyParams(headers.crypto_key);
|
||||
if (!keymap) {
|
||||
throw new CryptoError('Missing Crypto-Key header',
|
||||
BAD_CRYPTO_KEY_HEADER);
|
||||
}
|
||||
padSize = 2;
|
||||
} else if (headers.encoding == AESGCM128_ENCODING) {
|
||||
// aesgcm128 uses Encryption-Key, 1 byte for the pad length, and no secret.
|
||||
// https://tools.ietf.org/html/draft-thomson-http-encryption-02
|
||||
keymap = getEncryptionKeyParams(headers.encryption_key);
|
||||
if (!keymap) {
|
||||
throw new CryptoError('Missing Encryption-Key header',
|
||||
BAD_ENCRYPTION_KEY_HEADER);
|
||||
}
|
||||
padSize = 1;
|
||||
} else {
|
||||
throw new CryptoError('Unsupported Content-Encoding: ' + headers.encoding,
|
||||
BAD_ENCODING_HEADER);
|
||||
}
|
||||
|
||||
var enc = getEncryptionParams(headers.encryption);
|
||||
var dh = keymap[enc.keyid || DEFAULT_KEYID];
|
||||
if (!dh) {
|
||||
throw new CryptoError('Missing dh parameter', BAD_DH_PARAM);
|
||||
}
|
||||
var salt = enc.salt;
|
||||
if (!salt) {
|
||||
throw new CryptoError('Missing salt parameter', BAD_SALT_PARAM);
|
||||
}
|
||||
var rs = enc.rs ? parseInt(enc.rs, 10) : 4096;
|
||||
if (isNaN(rs)) {
|
||||
throw new CryptoError('rs parameter must be a number', BAD_RS_PARAM);
|
||||
}
|
||||
if (rs <= padSize) {
|
||||
throw new CryptoError('rs parameter must be at least ' + padSize,
|
||||
BAD_RS_PARAM, padSize);
|
||||
}
|
||||
return {dh, salt, rs, padSize};
|
||||
}
|
||||
|
||||
// Decodes an unpadded, base64url-encoded string.
|
||||
function base64URLDecode(string) {
|
||||
try {
|
||||
return ChromeUtils.base64URLDecode(string, {
|
||||
// draft-ietf-httpbis-encryption-encoding-01 prohibits padding.
|
||||
padding: 'reject',
|
||||
});
|
||||
} catch (ex) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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.extract = 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 CryptoError('HKDF length is too long', BAD_CRYPTO);
|
||||
}
|
||||
return h.slice(0, len);
|
||||
});
|
||||
};
|
||||
|
||||
/* generate a 96-bit nonce for use in GCM, 48-bits of which are populated */
|
||||
function generateNonce(base, index) {
|
||||
if (index >= Math.pow(2, 48)) {
|
||||
throw new CryptoError('Nonce index is too large', BAD_CRYPTO);
|
||||
}
|
||||
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.PushCrypto = {
|
||||
|
||||
generateAuthenticationSecret() {
|
||||
return crypto.getRandomValues(new Uint8Array(16));
|
||||
},
|
||||
|
||||
validateAppServerKey(key) {
|
||||
return crypto.subtle.importKey('raw', key, ECDSA_KEY,
|
||||
true, ['verify'])
|
||||
.then(_ => key);
|
||||
},
|
||||
|
||||
generateKeys() {
|
||||
return crypto.subtle.generateKey(ECDH_KEY, true, ['deriveBits'])
|
||||
.then(cryptoKey =>
|
||||
Promise.all([
|
||||
crypto.subtle.exportKey('raw', cryptoKey.publicKey),
|
||||
crypto.subtle.exportKey('jwk', cryptoKey.privateKey)
|
||||
]));
|
||||
},
|
||||
|
||||
/**
|
||||
* Decrypts a push message.
|
||||
*
|
||||
* @param {JsonWebKey} privateKey The ECDH private key of the subscription
|
||||
* receiving the message, in JWK form.
|
||||
* @param {BufferSource} publicKey The ECDH public key of the subscription
|
||||
* receiving the message, in raw form.
|
||||
* @param {BufferSource} authenticationSecret The 16-byte shared
|
||||
* authentication secret of the subscription receiving the message.
|
||||
* @param {Object} headers The encryption headers passed to `getCryptoParams`.
|
||||
* @param {BufferSource} ciphertext The encrypted message data.
|
||||
* @returns {Promise} Resolves with a `Uint8Array` containing the decrypted
|
||||
* message data. Rejects with a `CryptoError` if decryption fails.
|
||||
*/
|
||||
decrypt(privateKey, publicKey, authenticationSecret, headers, ciphertext) {
|
||||
return Promise.resolve().then(_ => {
|
||||
let cryptoParams = getCryptoParams(headers);
|
||||
if (!cryptoParams) {
|
||||
return null;
|
||||
}
|
||||
return this._decodeMsg(ciphertext, privateKey, publicKey,
|
||||
cryptoParams.dh, cryptoParams.salt,
|
||||
cryptoParams.rs, authenticationSecret,
|
||||
cryptoParams.padSize);
|
||||
}).catch(error => {
|
||||
if (error.isCryptoError) {
|
||||
throw error;
|
||||
}
|
||||
// Web Crypto returns an unhelpful "operation failed for an
|
||||
// operation-specific reason" error if decryption fails. We don't have
|
||||
// context about what went wrong, so we throw a generic error instead.
|
||||
throw new CryptoError('Bad encryption', BAD_CRYPTO);
|
||||
});
|
||||
},
|
||||
|
||||
_decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey, aSalt, aRs,
|
||||
aAuthenticationSecret, aPadSize) {
|
||||
|
||||
if (aData.byteLength === 0) {
|
||||
// Zero length messages will be passed as null.
|
||||
return null;
|
||||
}
|
||||
|
||||
// The last chunk of data must be less than aRs, if it is not return an
|
||||
// error.
|
||||
if (aData.byteLength % (aRs + 16) === 0) {
|
||||
throw new CryptoError('Encrypted data truncated', BAD_CRYPTO);
|
||||
}
|
||||
|
||||
let senderKey = base64URLDecode(aSenderPublicKey);
|
||||
if (!senderKey) {
|
||||
throw new CryptoError('dh parameter is not base64url-encoded',
|
||||
BAD_DH_PARAM);
|
||||
}
|
||||
|
||||
let salt = base64URLDecode(aSalt);
|
||||
if (!salt) {
|
||||
throw new CryptoError('salt parameter is not base64url-encoded',
|
||||
BAD_SALT_PARAM);
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
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(aPadSize,
|
||||
new Uint8Array(ikm),
|
||||
salt,
|
||||
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(aPadSize, slice, index, r[1], r[0]))))
|
||||
.then(r => concatArray(r));
|
||||
},
|
||||
|
||||
_deriveKeyAndNonce(padSize, ikm, salt, receiverKey, senderKey,
|
||||
authenticationSecret) {
|
||||
var kdfPromise;
|
||||
var context;
|
||||
var encryptInfo;
|
||||
// The size of the padding determines which key derivation we use.
|
||||
//
|
||||
// 1. If the pad size is 1, we assume "aesgcm128". This scheme ignores the
|
||||
// authenticationSecret, and uses "Content-Encoding: <blah>" for the
|
||||
// context string. It should eventually be removed: bug 1230038.
|
||||
//
|
||||
// 2. If the pad size is 2, we assume "aesgcm", and mix the
|
||||
// authenticationSecret with the ikm using HKDF. The context string is:
|
||||
// "Content-Encoding: <blah>\0P-256\0" then the length and value of both the
|
||||
// receiver key and sender key.
|
||||
if (padSize == 2) {
|
||||
// Since we are using an authentication secret, we need to run an extra
|
||||
// round of HKDF with the authentication secret as salt.
|
||||
var authKdf = new hkdf(authenticationSecret, ikm);
|
||||
kdfPromise = authKdf.extract(AUTH_INFO, 32)
|
||||
.then(ikm2 => new hkdf(salt, ikm2));
|
||||
|
||||
// aesgcm requires extra context for the info parameter.
|
||||
context = concatArray([
|
||||
new Uint8Array([0]), P256DH_INFO,
|
||||
this._encodeLength(receiverKey), receiverKey,
|
||||
this._encodeLength(senderKey), senderKey
|
||||
]);
|
||||
encryptInfo = AESGCM_ENCRYPT_INFO;
|
||||
} else {
|
||||
kdfPromise = Promise.resolve(new hkdf(salt, ikm));
|
||||
context = new Uint8Array(0);
|
||||
encryptInfo = AESGCM128_ENCRYPT_INFO;
|
||||
}
|
||||
return kdfPromise.then(kdf => Promise.all([
|
||||
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(aPadSize, aSlice, aIndex, aNonce, aKey) {
|
||||
let params = {
|
||||
name: 'AES-GCM',
|
||||
iv: generateNonce(aNonce, aIndex)
|
||||
};
|
||||
return crypto.subtle.decrypt(params, aKey, aSlice)
|
||||
.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 CryptoError('Unsupported pad size', BAD_CRYPTO);
|
||||
}
|
||||
if (decoded.length < padSize) {
|
||||
throw new CryptoError('Decoded array is too short!', BAD_PADDING);
|
||||
}
|
||||
var pad = decoded[0];
|
||||
if (padSize == 2) {
|
||||
pad = (pad << 8) | decoded[1];
|
||||
}
|
||||
if (pad > decoded.length) {
|
||||
throw new CryptoError('Padding is wrong!', BAD_PADDING);
|
||||
}
|
||||
// All padded bytes must be zero except the first one.
|
||||
for (var i = padSize; i <= pad; i++) {
|
||||
if (decoded[i] !== 0) {
|
||||
throw new CryptoError('Padding is wrong!', BAD_PADDING);
|
||||
}
|
||||
}
|
||||
return decoded.slice(pad + padSize);
|
||||
},
|
||||
};
|
||||
440
dom/push/PushDB.jsm
Normal file
440
dom/push/PushDB.jsm
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
/* 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.import("resource://gre/modules/IndexedDBHelper.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.importGlobalProperties(["indexedDB"]);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PushDB"];
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "console", () => {
|
||||
let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
|
||||
return new ConsoleAPI({
|
||||
maxLogLevelPref: "dom.push.loglevel",
|
||||
prefix: "PushDB",
|
||||
});
|
||||
});
|
||||
|
||||
this.PushDB = function PushDB(dbName, dbVersion, dbStoreName, keyPath, model) {
|
||||
console.debug("PushDB()");
|
||||
this._dbStoreName = dbStoreName;
|
||||
this._keyPath = keyPath;
|
||||
this._model = model;
|
||||
|
||||
// set the indexeddb database
|
||||
this.initDBHelper(dbName, dbVersion,
|
||||
[dbStoreName]);
|
||||
};
|
||||
|
||||
this.PushDB.prototype = {
|
||||
__proto__: IndexedDBHelper.prototype,
|
||||
|
||||
toPushRecord: function(record) {
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
return new this._model(record);
|
||||
},
|
||||
|
||||
isValidRecord: function(record) {
|
||||
return record && typeof record.scope == "string" &&
|
||||
typeof record.originAttributes == "string" &&
|
||||
record.quota >= 0 &&
|
||||
typeof record[this._keyPath] == "string";
|
||||
},
|
||||
|
||||
upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
|
||||
if (aOldVersion <= 3) {
|
||||
//XXXnsm We haven't shipped Push during this upgrade, so I'm just going to throw old
|
||||
//registrations away without even informing the app.
|
||||
if (aDb.objectStoreNames.contains(this._dbStoreName)) {
|
||||
aDb.deleteObjectStore(this._dbStoreName);
|
||||
}
|
||||
|
||||
let objectStore = aDb.createObjectStore(this._dbStoreName,
|
||||
{ keyPath: this._keyPath });
|
||||
|
||||
// index to fetch records based on endpoints. used by unregister
|
||||
objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
|
||||
|
||||
// index to fetch records by identifiers.
|
||||
// In the current security model, the originAttributes distinguish between
|
||||
// different 'apps' on the same origin. Since ServiceWorkers are
|
||||
// same-origin to the scope they are registered for, the attributes and
|
||||
// scope are enough to reconstruct a valid principal.
|
||||
objectStore.createIndex("identifiers", ["scope", "originAttributes"], { unique: true });
|
||||
objectStore.createIndex("originAttributes", "originAttributes", { unique: false });
|
||||
}
|
||||
|
||||
if (aOldVersion < 4) {
|
||||
let objectStore = aTransaction.objectStore(this._dbStoreName);
|
||||
|
||||
// index to fetch active and expired registrations.
|
||||
objectStore.createIndex("quota", "quota", { unique: false });
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* @param aRecord
|
||||
* The record to be added.
|
||||
*/
|
||||
|
||||
put: function(aRecord) {
|
||||
console.debug("put()", aRecord);
|
||||
if (!this.isValidRecord(aRecord)) {
|
||||
return Promise.reject(new TypeError(
|
||||
"Scope, originAttributes, and quota are required! " +
|
||||
JSON.stringify(aRecord)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
aStore.put(aRecord).onsuccess = aEvent => {
|
||||
console.debug("put: Request successful. Updated record",
|
||||
aEvent.target.result);
|
||||
aTxn.result = this.toPushRecord(aRecord);
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* @param aKeyID
|
||||
* The ID of record to be deleted.
|
||||
*/
|
||||
delete: function(aKeyID) {
|
||||
console.debug("delete()");
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
console.debug("delete: Removing record", aKeyID);
|
||||
aStore.get(aKeyID).onsuccess = event => {
|
||||
aTxn.result = this.toPushRecord(event.target.result);
|
||||
aStore.delete(aKeyID);
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
// testFn(record) is called with a database record and should return true if
|
||||
// that record should be deleted.
|
||||
clearIf: function(testFn) {
|
||||
console.debug("clearIf()");
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
aStore.openCursor().onsuccess = event => {
|
||||
let cursor = event.target.result;
|
||||
if (cursor) {
|
||||
let record = this.toPushRecord(cursor.value);
|
||||
if (testFn(record)) {
|
||||
let deleteRequest = cursor.delete();
|
||||
deleteRequest.onerror = e => {
|
||||
console.error("clearIf: Error removing record",
|
||||
record.keyID, e);
|
||||
}
|
||||
}
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
getByPushEndpoint: function(aPushEndpoint) {
|
||||
console.debug("getByPushEndpoint()");
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let index = aStore.index("pushEndpoint");
|
||||
index.get(aPushEndpoint).onsuccess = aEvent => {
|
||||
let record = this.toPushRecord(aEvent.target.result);
|
||||
console.debug("getByPushEndpoint: Got record", record);
|
||||
aTxn.result = record;
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
getByKeyID: function(aKeyID) {
|
||||
console.debug("getByKeyID()");
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
aStore.get(aKeyID).onsuccess = aEvent => {
|
||||
let record = this.toPushRecord(aEvent.target.result);
|
||||
console.debug("getByKeyID: Got record", record);
|
||||
aTxn.result = record;
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterates over all records associated with an origin.
|
||||
*
|
||||
* @param {String} origin The origin, matched as a prefix against the scope.
|
||||
* @param {String} originAttributes Additional origin attributes. Requires
|
||||
* an exact match.
|
||||
* @param {Function} callback A function with the signature `(record,
|
||||
* cursor)`, called for each record. `record` is the registration, and
|
||||
* `cursor` is an `IDBCursor`.
|
||||
* @returns {Promise} Resolves once all records have been processed.
|
||||
*/
|
||||
forEachOrigin: function(origin, originAttributes, callback) {
|
||||
console.debug("forEachOrigin()");
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let index = aStore.index("identifiers");
|
||||
let range = IDBKeyRange.bound(
|
||||
[origin, originAttributes],
|
||||
[origin + "\x7f", originAttributes]
|
||||
);
|
||||
index.openCursor(range).onsuccess = event => {
|
||||
let cursor = event.target.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
callback(this.toPushRecord(cursor.value), cursor);
|
||||
cursor.continue();
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
// Perform a unique match against { scope, originAttributes }
|
||||
getByIdentifiers: function(aPageRecord) {
|
||||
console.debug("getByIdentifiers()", aPageRecord);
|
||||
if (!aPageRecord.scope || aPageRecord.originAttributes == undefined) {
|
||||
console.error("getByIdentifiers: Scope and originAttributes are required",
|
||||
aPageRecord);
|
||||
return Promise.reject(new TypeError("Invalid page record"));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let index = aStore.index("identifiers");
|
||||
let request = index.get(IDBKeyRange.only([aPageRecord.scope, aPageRecord.originAttributes]));
|
||||
request.onsuccess = aEvent => {
|
||||
aTxn.result = this.toPushRecord(aEvent.target.result);
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_getAllByKey: function(aKeyName, aKeyValue) {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let index = aStore.index(aKeyName);
|
||||
// It seems ok to use getAll here, since unlike contacts or other
|
||||
// high storage APIs, we don't expect more than a handful of
|
||||
// registrations per domain, and usually only one.
|
||||
let getAllReq = index.mozGetAll(aKeyValue);
|
||||
getAllReq.onsuccess = aEvent => {
|
||||
aTxn.result = aEvent.target.result.map(
|
||||
record => this.toPushRecord(record));
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
// aOriginAttributes must be a string!
|
||||
getAllByOriginAttributes: function(aOriginAttributes) {
|
||||
if (typeof aOriginAttributes !== "string") {
|
||||
return Promise.reject("Expected string!");
|
||||
}
|
||||
return this._getAllByKey("originAttributes", aOriginAttributes);
|
||||
},
|
||||
|
||||
getAllKeyIDs: function() {
|
||||
console.debug("getAllKeyIDs()");
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = undefined;
|
||||
aStore.mozGetAll().onsuccess = event => {
|
||||
aTxn.result = event.target.result.map(
|
||||
record => this.toPushRecord(record));
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_getAllByPushQuota: function(range) {
|
||||
console.debug("getAllByPushQuota()");
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aTxn.result = [];
|
||||
|
||||
let index = aStore.index("quota");
|
||||
index.openCursor(range).onsuccess = event => {
|
||||
let cursor = event.target.result;
|
||||
if (cursor) {
|
||||
aTxn.result.push(this.toPushRecord(cursor.value));
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
getAllUnexpired: function() {
|
||||
console.debug("getAllUnexpired()");
|
||||
return this._getAllByPushQuota(IDBKeyRange.lowerBound(1));
|
||||
},
|
||||
|
||||
getAllExpired: function() {
|
||||
console.debug("getAllExpired()");
|
||||
return this._getAllByPushQuota(IDBKeyRange.only(0));
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an existing push registration.
|
||||
*
|
||||
* @param {String} aKeyID The registration ID.
|
||||
* @param {Function} aUpdateFunc A function that receives the existing
|
||||
* registration record as its argument, and returns a new record.
|
||||
* @returns {Promise} A promise resolved with either the updated record.
|
||||
* Rejects if the record does not exist, or the function returns an invalid
|
||||
* record.
|
||||
*/
|
||||
update: function(aKeyID, aUpdateFunc) {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
this._dbStoreName,
|
||||
(aTxn, aStore) => {
|
||||
aStore.get(aKeyID).onsuccess = aEvent => {
|
||||
aTxn.result = undefined;
|
||||
|
||||
let record = aEvent.target.result;
|
||||
if (!record) {
|
||||
throw new Error("Record " + aKeyID + " does not exist");
|
||||
}
|
||||
let newRecord = aUpdateFunc(this.toPushRecord(record));
|
||||
if (!this.isValidRecord(newRecord)) {
|
||||
console.error("update: Ignoring invalid update",
|
||||
aKeyID, newRecord);
|
||||
throw new Error("Invalid update for record " + aKeyID);
|
||||
}
|
||||
function putRecord() {
|
||||
let req = aStore.put(newRecord);
|
||||
req.onsuccess = aEvent => {
|
||||
console.debug("update: Update successful", aKeyID, newRecord);
|
||||
aTxn.result = newRecord;
|
||||
};
|
||||
}
|
||||
if (aKeyID === newRecord.keyID) {
|
||||
putRecord();
|
||||
} else {
|
||||
// If we changed the primary key, delete the old record to avoid
|
||||
// unique constraint errors.
|
||||
aStore.delete(aKeyID).onsuccess = putRecord;
|
||||
}
|
||||
};
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
drop: function() {
|
||||
console.debug("drop()");
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
this._dbStoreName,
|
||||
function txnCb(aTxn, aStore) {
|
||||
aStore.clear();
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
600
dom/push/PushManager.cpp
Normal file
600
dom/push/PushManager.cpp
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
#include "mozilla/dom/PushManager.h"
|
||||
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/Unused.h"
|
||||
#include "mozilla/dom/PushManagerBinding.h"
|
||||
#include "mozilla/dom/PushSubscription.h"
|
||||
#include "mozilla/dom/PushSubscriptionOptionsBinding.h"
|
||||
#include "mozilla/dom/PushUtil.h"
|
||||
|
||||
#include "mozilla/dom/Promise.h"
|
||||
#include "mozilla/dom/PromiseWorkerProxy.h"
|
||||
|
||||
#include "nsIGlobalObject.h"
|
||||
#include "nsIPermissionManager.h"
|
||||
#include "nsIPrincipal.h"
|
||||
#include "nsIPushService.h"
|
||||
|
||||
#include "nsComponentManagerUtils.h"
|
||||
#include "nsContentUtils.h"
|
||||
|
||||
#include "WorkerRunnable.h"
|
||||
#include "WorkerPrivate.h"
|
||||
#include "WorkerScope.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
using namespace workers;
|
||||
|
||||
namespace {
|
||||
|
||||
nsresult
|
||||
GetPermissionState(nsIPrincipal* aPrincipal,
|
||||
PushPermissionState& aState)
|
||||
{
|
||||
nsCOMPtr<nsIPermissionManager> permManager =
|
||||
mozilla::services::GetPermissionManager();
|
||||
|
||||
if (!permManager) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
|
||||
nsresult rv = permManager->TestExactPermissionFromPrincipal(
|
||||
aPrincipal,
|
||||
"desktop-notification",
|
||||
&permission);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
if (permission == nsIPermissionManager::ALLOW_ACTION ||
|
||||
Preferences::GetBool("dom.push.testing.ignorePermission", false)) {
|
||||
aState = PushPermissionState::Granted;
|
||||
} else if (permission == nsIPermissionManager::DENY_ACTION) {
|
||||
aState = PushPermissionState::Denied;
|
||||
} else {
|
||||
aState = PushPermissionState::Prompt;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// A helper class that frees an `nsIPushSubscription` key buffer when it
|
||||
// goes out of scope.
|
||||
class MOZ_RAII AutoFreeKeyBuffer final
|
||||
{
|
||||
uint8_t** mKeyBuffer;
|
||||
|
||||
public:
|
||||
explicit AutoFreeKeyBuffer(uint8_t** aKeyBuffer)
|
||||
: mKeyBuffer(aKeyBuffer)
|
||||
{
|
||||
MOZ_ASSERT(mKeyBuffer);
|
||||
}
|
||||
|
||||
~AutoFreeKeyBuffer()
|
||||
{
|
||||
NS_Free(*mKeyBuffer);
|
||||
}
|
||||
};
|
||||
|
||||
// Copies a subscription key buffer into an array.
|
||||
nsresult
|
||||
CopySubscriptionKeyToArray(nsIPushSubscription* aSubscription,
|
||||
const nsAString& aKeyName,
|
||||
nsTArray<uint8_t>& aKey)
|
||||
{
|
||||
uint8_t* keyBuffer = nullptr;
|
||||
AutoFreeKeyBuffer autoFree(&keyBuffer);
|
||||
|
||||
uint32_t keyLen;
|
||||
nsresult rv = aSubscription->GetKey(aKeyName, &keyLen, &keyBuffer);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
if (!aKey.SetCapacity(keyLen, fallible) ||
|
||||
!aKey.InsertElementsAt(0, keyBuffer, keyLen, fallible)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
GetSubscriptionParams(nsIPushSubscription* aSubscription,
|
||||
nsAString& aEndpoint,
|
||||
nsTArray<uint8_t>& aRawP256dhKey,
|
||||
nsTArray<uint8_t>& aAuthSecret,
|
||||
nsTArray<uint8_t>& aAppServerKey)
|
||||
{
|
||||
if (!aSubscription) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult rv = aSubscription->GetEndpoint(aEndpoint);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = CopySubscriptionKeyToArray(aSubscription, NS_LITERAL_STRING("p256dh"),
|
||||
aRawP256dhKey);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
rv = CopySubscriptionKeyToArray(aSubscription, NS_LITERAL_STRING("auth"),
|
||||
aAuthSecret);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
rv = CopySubscriptionKeyToArray(aSubscription, NS_LITERAL_STRING("appServer"),
|
||||
aAppServerKey);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
class GetSubscriptionResultRunnable final : public WorkerRunnable
|
||||
{
|
||||
public:
|
||||
GetSubscriptionResultRunnable(WorkerPrivate* aWorkerPrivate,
|
||||
already_AddRefed<PromiseWorkerProxy>&& aProxy,
|
||||
nsresult aStatus,
|
||||
const nsAString& aEndpoint,
|
||||
const nsAString& aScope,
|
||||
nsTArray<uint8_t>&& aRawP256dhKey,
|
||||
nsTArray<uint8_t>&& aAuthSecret,
|
||||
nsTArray<uint8_t>&& aAppServerKey)
|
||||
: WorkerRunnable(aWorkerPrivate)
|
||||
, mProxy(Move(aProxy))
|
||||
, mStatus(aStatus)
|
||||
, mEndpoint(aEndpoint)
|
||||
, mScope(aScope)
|
||||
, mRawP256dhKey(Move(aRawP256dhKey))
|
||||
, mAuthSecret(Move(aAuthSecret))
|
||||
, mAppServerKey(Move(aAppServerKey))
|
||||
{ }
|
||||
|
||||
bool
|
||||
WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
|
||||
{
|
||||
RefPtr<Promise> promise = mProxy->WorkerPromise();
|
||||
if (NS_SUCCEEDED(mStatus)) {
|
||||
if (mEndpoint.IsEmpty()) {
|
||||
promise->MaybeResolve(JS::NullHandleValue);
|
||||
} else {
|
||||
RefPtr<PushSubscription> sub =
|
||||
new PushSubscription(nullptr, mEndpoint, mScope,
|
||||
Move(mRawP256dhKey), Move(mAuthSecret),
|
||||
Move(mAppServerKey));
|
||||
promise->MaybeResolve(sub);
|
||||
}
|
||||
} else if (NS_ERROR_GET_MODULE(mStatus) == NS_ERROR_MODULE_DOM_PUSH ) {
|
||||
promise->MaybeReject(mStatus);
|
||||
} else {
|
||||
promise->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
|
||||
}
|
||||
|
||||
mProxy->CleanUp();
|
||||
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
~GetSubscriptionResultRunnable()
|
||||
{}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> mProxy;
|
||||
nsresult mStatus;
|
||||
nsString mEndpoint;
|
||||
nsString mScope;
|
||||
nsTArray<uint8_t> mRawP256dhKey;
|
||||
nsTArray<uint8_t> mAuthSecret;
|
||||
nsTArray<uint8_t> mAppServerKey;
|
||||
};
|
||||
|
||||
class GetSubscriptionCallback final : public nsIPushSubscriptionCallback
|
||||
{
|
||||
public:
|
||||
NS_DECL_ISUPPORTS
|
||||
|
||||
explicit GetSubscriptionCallback(PromiseWorkerProxy* aProxy,
|
||||
const nsAString& aScope)
|
||||
: mProxy(aProxy)
|
||||
, mScope(aScope)
|
||||
{}
|
||||
|
||||
NS_IMETHOD
|
||||
OnPushSubscription(nsresult aStatus,
|
||||
nsIPushSubscription* aSubscription) override
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
MOZ_ASSERT(mProxy, "OnPushSubscription() called twice?");
|
||||
|
||||
MutexAutoLock lock(mProxy->Lock());
|
||||
if (mProxy->CleanedUp()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsAutoString endpoint;
|
||||
nsTArray<uint8_t> rawP256dhKey, authSecret, appServerKey;
|
||||
if (NS_SUCCEEDED(aStatus)) {
|
||||
aStatus = GetSubscriptionParams(aSubscription, endpoint, rawP256dhKey,
|
||||
authSecret, appServerKey);
|
||||
}
|
||||
|
||||
WorkerPrivate* worker = mProxy->GetWorkerPrivate();
|
||||
RefPtr<GetSubscriptionResultRunnable> r =
|
||||
new GetSubscriptionResultRunnable(worker,
|
||||
mProxy.forget(),
|
||||
aStatus,
|
||||
endpoint,
|
||||
mScope,
|
||||
Move(rawP256dhKey),
|
||||
Move(authSecret),
|
||||
Move(appServerKey));
|
||||
MOZ_ALWAYS_TRUE(r->Dispatch());
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Convenience method for use in this file.
|
||||
void
|
||||
OnPushSubscriptionError(nsresult aStatus)
|
||||
{
|
||||
Unused << NS_WARN_IF(NS_FAILED(
|
||||
OnPushSubscription(aStatus, nullptr)));
|
||||
}
|
||||
|
||||
protected:
|
||||
~GetSubscriptionCallback()
|
||||
{}
|
||||
|
||||
private:
|
||||
RefPtr<PromiseWorkerProxy> mProxy;
|
||||
nsString mScope;
|
||||
};
|
||||
|
||||
NS_IMPL_ISUPPORTS(GetSubscriptionCallback, nsIPushSubscriptionCallback)
|
||||
|
||||
class GetSubscriptionRunnable final : public Runnable
|
||||
{
|
||||
public:
|
||||
GetSubscriptionRunnable(PromiseWorkerProxy* aProxy,
|
||||
const nsAString& aScope,
|
||||
PushManager::SubscriptionAction aAction,
|
||||
nsTArray<uint8_t>&& aAppServerKey)
|
||||
: mProxy(aProxy)
|
||||
, mScope(aScope)
|
||||
, mAction(aAction)
|
||||
, mAppServerKey(Move(aAppServerKey))
|
||||
{}
|
||||
|
||||
NS_IMETHOD
|
||||
Run() override
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
|
||||
nsCOMPtr<nsIPrincipal> principal;
|
||||
|
||||
{
|
||||
// Bug 1228723: If permission is revoked or an error occurs, the
|
||||
// subscription callback will be called synchronously. This causes
|
||||
// `GetSubscriptionCallback::OnPushSubscription` to deadlock when
|
||||
// it tries to acquire the lock.
|
||||
MutexAutoLock lock(mProxy->Lock());
|
||||
if (mProxy->CleanedUp()) {
|
||||
return NS_OK;
|
||||
}
|
||||
principal = mProxy->GetWorkerPrivate()->GetPrincipal();
|
||||
}
|
||||
|
||||
MOZ_ASSERT(principal);
|
||||
|
||||
RefPtr<GetSubscriptionCallback> callback = new GetSubscriptionCallback(mProxy, mScope);
|
||||
|
||||
PushPermissionState state;
|
||||
nsresult rv = GetPermissionState(principal, state);
|
||||
if (NS_FAILED(rv)) {
|
||||
callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (state != PushPermissionState::Granted) {
|
||||
if (mAction == PushManager::GetSubscriptionAction) {
|
||||
callback->OnPushSubscriptionError(NS_OK);
|
||||
return NS_OK;
|
||||
}
|
||||
callback->OnPushSubscriptionError(NS_ERROR_DOM_PUSH_DENIED_ERR);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIPushService> service =
|
||||
do_GetService("@mozilla.org/push/Service;1");
|
||||
if (NS_WARN_IF(!service)) {
|
||||
callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (mAction == PushManager::SubscribeAction) {
|
||||
if (mAppServerKey.IsEmpty()) {
|
||||
rv = service->Subscribe(mScope, principal, callback);
|
||||
} else {
|
||||
rv = service->SubscribeWithKey(mScope, principal,
|
||||
mAppServerKey.Length(),
|
||||
mAppServerKey.Elements(), callback);
|
||||
}
|
||||
} else {
|
||||
MOZ_ASSERT(mAction == PushManager::GetSubscriptionAction);
|
||||
rv = service->GetSubscription(mScope, principal, callback);
|
||||
}
|
||||
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
~GetSubscriptionRunnable()
|
||||
{}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> mProxy;
|
||||
nsString mScope;
|
||||
PushManager::SubscriptionAction mAction;
|
||||
nsTArray<uint8_t> mAppServerKey;
|
||||
};
|
||||
|
||||
class PermissionResultRunnable final : public WorkerRunnable
|
||||
{
|
||||
public:
|
||||
PermissionResultRunnable(PromiseWorkerProxy *aProxy,
|
||||
nsresult aStatus,
|
||||
PushPermissionState aState)
|
||||
: WorkerRunnable(aProxy->GetWorkerPrivate())
|
||||
, mProxy(aProxy)
|
||||
, mStatus(aStatus)
|
||||
, mState(aState)
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
}
|
||||
|
||||
bool
|
||||
WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
|
||||
{
|
||||
MOZ_ASSERT(aWorkerPrivate);
|
||||
aWorkerPrivate->AssertIsOnWorkerThread();
|
||||
|
||||
RefPtr<Promise> promise = mProxy->WorkerPromise();
|
||||
if (NS_SUCCEEDED(mStatus)) {
|
||||
promise->MaybeResolve(mState);
|
||||
} else {
|
||||
promise->MaybeReject(aCx, JS::UndefinedHandleValue);
|
||||
}
|
||||
|
||||
mProxy->CleanUp();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
~PermissionResultRunnable()
|
||||
{}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> mProxy;
|
||||
nsresult mStatus;
|
||||
PushPermissionState mState;
|
||||
};
|
||||
|
||||
class PermissionStateRunnable final : public Runnable
|
||||
{
|
||||
public:
|
||||
explicit PermissionStateRunnable(PromiseWorkerProxy* aProxy)
|
||||
: mProxy(aProxy)
|
||||
{}
|
||||
|
||||
NS_IMETHOD
|
||||
Run() override
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
MutexAutoLock lock(mProxy->Lock());
|
||||
if (mProxy->CleanedUp()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
PushPermissionState state;
|
||||
nsresult rv = GetPermissionState(
|
||||
mProxy->GetWorkerPrivate()->GetPrincipal(),
|
||||
state
|
||||
);
|
||||
|
||||
RefPtr<PermissionResultRunnable> r =
|
||||
new PermissionResultRunnable(mProxy, rv, state);
|
||||
MOZ_ALWAYS_TRUE(r->Dispatch());
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
~PermissionStateRunnable()
|
||||
{}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> mProxy;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
PushManager::PushManager(nsIGlobalObject* aGlobal, PushManagerImpl* aImpl)
|
||||
: mGlobal(aGlobal)
|
||||
, mImpl(aImpl)
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
MOZ_ASSERT(aImpl);
|
||||
}
|
||||
|
||||
PushManager::PushManager(const nsAString& aScope)
|
||||
: mScope(aScope)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
// There's only one global on a worker, so we don't need to pass a global
|
||||
// object to the constructor.
|
||||
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
|
||||
MOZ_ASSERT(worker);
|
||||
worker->AssertIsOnWorkerThread();
|
||||
#endif
|
||||
}
|
||||
|
||||
PushManager::~PushManager()
|
||||
{}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushManager, mGlobal, mImpl)
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushManager)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushManager)
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushManager)
|
||||
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
||||
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
JSObject*
|
||||
PushManager::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return PushManagerBinding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
// static
|
||||
already_AddRefed<PushManager>
|
||||
PushManager::Constructor(GlobalObject& aGlobal,
|
||||
const nsAString& aScope,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (!NS_IsMainThread()) {
|
||||
RefPtr<PushManager> ret = new PushManager(aScope);
|
||||
return ret.forget();
|
||||
}
|
||||
|
||||
RefPtr<PushManagerImpl> impl = PushManagerImpl::Constructor(aGlobal,
|
||||
aGlobal.Context(),
|
||||
aScope, aRv);
|
||||
if (aRv.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
RefPtr<PushManager> ret = new PushManager(global, impl);
|
||||
|
||||
return ret.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PushManager::Subscribe(const PushSubscriptionOptionsInit& aOptions,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (mImpl) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
return mImpl->Subscribe(aOptions, aRv);
|
||||
}
|
||||
|
||||
return PerformSubscriptionActionFromWorker(SubscribeAction, aOptions, aRv);
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PushManager::GetSubscription(ErrorResult& aRv)
|
||||
{
|
||||
if (mImpl) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
return mImpl->GetSubscription(aRv);
|
||||
}
|
||||
|
||||
return PerformSubscriptionActionFromWorker(GetSubscriptionAction, aRv);
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PushManager::PermissionState(const PushSubscriptionOptionsInit& aOptions,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (mImpl) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
return mImpl->PermissionState(aOptions, aRv);
|
||||
}
|
||||
|
||||
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
|
||||
MOZ_ASSERT(worker);
|
||||
worker->AssertIsOnWorkerThread();
|
||||
|
||||
nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope();
|
||||
RefPtr<Promise> p = Promise::Create(global, aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
|
||||
if (!proxy) {
|
||||
p->MaybeReject(worker->GetJSContext(), JS::UndefinedHandleValue);
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
RefPtr<PermissionStateRunnable> r =
|
||||
new PermissionStateRunnable(proxy);
|
||||
NS_DispatchToMainThread(r);
|
||||
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PushManager::PerformSubscriptionActionFromWorker(SubscriptionAction aAction,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
PushSubscriptionOptionsInit options;
|
||||
return PerformSubscriptionActionFromWorker(aAction, options, aRv);
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PushManager::PerformSubscriptionActionFromWorker(SubscriptionAction aAction,
|
||||
const PushSubscriptionOptionsInit& aOptions,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
|
||||
MOZ_ASSERT(worker);
|
||||
worker->AssertIsOnWorkerThread();
|
||||
|
||||
nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope();
|
||||
RefPtr<Promise> p = Promise::Create(global, aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
|
||||
if (!proxy) {
|
||||
p->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> appServerKey;
|
||||
if (!aOptions.mApplicationServerKey.IsNull()) {
|
||||
const OwningArrayBufferViewOrArrayBuffer& bufferSource =
|
||||
aOptions.mApplicationServerKey.Value();
|
||||
if (!PushUtil::CopyBufferSourceToArray(bufferSource, appServerKey) ||
|
||||
appServerKey.IsEmpty()) {
|
||||
p->MaybeReject(NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
|
||||
return p.forget();
|
||||
}
|
||||
}
|
||||
|
||||
RefPtr<GetSubscriptionRunnable> r =
|
||||
new GetSubscriptionRunnable(proxy, mScope, aAction, Move(appServerKey));
|
||||
MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));
|
||||
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
117
dom/push/PushManager.h
Normal file
117
dom/push/PushManager.h
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* PushManager and PushSubscription are exposed on the main and worker threads.
|
||||
* The main thread version is implemented in Push.js. The JS implementation
|
||||
* makes it easier to use certain APIs like the permission prompt and Promises.
|
||||
*
|
||||
* Unfortunately, JS-implemented WebIDL is not supported off the main thread.
|
||||
* To work around this, we use a chain of runnables to query the JS-implemented
|
||||
* nsIPushService component for subscription information, and return the
|
||||
* results to the worker. We don't have to deal with permission prompts, since
|
||||
* we just reject calls if the principal does not have permission.
|
||||
*
|
||||
* On the main thread, PushManager wraps a JS-implemented PushManagerImpl
|
||||
* instance. The C++ wrapper is necessary because our bindings code cannot
|
||||
* accomodate "JS-implemented on the main thread, C++ on the worker" bindings.
|
||||
*
|
||||
* PushSubscription is in C++ on both threads since it isn't particularly
|
||||
* verbose to implement in C++ compared to JS.
|
||||
*/
|
||||
|
||||
#ifndef mozilla_dom_PushManager_h
|
||||
#define mozilla_dom_PushManager_h
|
||||
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
#include "mozilla/AlreadyAddRefed.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "mozilla/dom/BindingDeclarations.h"
|
||||
#include "mozilla/dom/TypedArray.h"
|
||||
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsContentUtils.h" // Required for nsContentUtils::PushEnabled
|
||||
#include "mozilla/RefPtr.h"
|
||||
|
||||
class nsIGlobalObject;
|
||||
class nsIPrincipal;
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
namespace workers {
|
||||
class WorkerPrivate;
|
||||
}
|
||||
|
||||
class Promise;
|
||||
class PushManagerImpl;
|
||||
struct PushSubscriptionOptionsInit;
|
||||
|
||||
class PushManager final : public nsISupports
|
||||
, public nsWrapperCache
|
||||
{
|
||||
public:
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PushManager)
|
||||
|
||||
enum SubscriptionAction {
|
||||
SubscribeAction,
|
||||
GetSubscriptionAction,
|
||||
};
|
||||
|
||||
// The main thread constructor.
|
||||
PushManager(nsIGlobalObject* aGlobal, PushManagerImpl* aImpl);
|
||||
|
||||
// The worker thread constructor.
|
||||
explicit PushManager(const nsAString& aScope);
|
||||
|
||||
nsIGlobalObject*
|
||||
GetParentObject() const
|
||||
{
|
||||
return mGlobal;
|
||||
}
|
||||
|
||||
JSObject*
|
||||
WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
static already_AddRefed<PushManager>
|
||||
Constructor(GlobalObject& aGlobal, const nsAString& aScope,
|
||||
ErrorResult& aRv);
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PerformSubscriptionActionFromWorker(SubscriptionAction aAction,
|
||||
ErrorResult& aRv);
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PerformSubscriptionActionFromWorker(SubscriptionAction aAction,
|
||||
const PushSubscriptionOptionsInit& aOptions,
|
||||
ErrorResult& aRv);
|
||||
|
||||
already_AddRefed<Promise>
|
||||
Subscribe(const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv);
|
||||
|
||||
already_AddRefed<Promise>
|
||||
GetSubscription(ErrorResult& aRv);
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PermissionState(const PushSubscriptionOptionsInit& aOptions,
|
||||
ErrorResult& aRv);
|
||||
|
||||
private:
|
||||
~PushManager();
|
||||
|
||||
// The following are only set and accessed on the main thread.
|
||||
nsCOMPtr<nsIGlobalObject> mGlobal;
|
||||
RefPtr<PushManagerImpl> mImpl;
|
||||
|
||||
// Only used on the worker thread.
|
||||
nsString mScope;
|
||||
};
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_PushManager_h
|
||||
550
dom/push/PushNotifier.cpp
Normal file
550
dom/push/PushNotifier.cpp
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
/* 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/. */
|
||||
|
||||
#include "PushNotifier.h"
|
||||
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsICategoryManager.h"
|
||||
#include "nsIXULRuntime.h"
|
||||
#include "nsNetUtil.h"
|
||||
#include "nsXPCOM.h"
|
||||
#include "ServiceWorkerManager.h"
|
||||
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/Unused.h"
|
||||
|
||||
#include "mozilla/dom/BodyUtil.h"
|
||||
#include "mozilla/dom/ContentChild.h"
|
||||
#include "mozilla/dom/ContentParent.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
using workers::AssertIsOnMainThread;
|
||||
using workers::ServiceWorkerManager;
|
||||
|
||||
PushNotifier::PushNotifier()
|
||||
{}
|
||||
|
||||
PushNotifier::~PushNotifier()
|
||||
{}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_0(PushNotifier)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushNotifier)
|
||||
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPushNotifier)
|
||||
NS_INTERFACE_MAP_ENTRY(nsIPushNotifier)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushNotifier)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushNotifier)
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushNotifier::NotifyPushWithData(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal,
|
||||
const nsAString& aMessageId,
|
||||
uint32_t aDataLen, uint8_t* aData)
|
||||
{
|
||||
NS_ENSURE_ARG(aPrincipal);
|
||||
nsTArray<uint8_t> data;
|
||||
if (!data.SetCapacity(aDataLen, fallible)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (!data.InsertElementsAt(0, aData, aDataLen, fallible)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
PushMessageDispatcher dispatcher(aScope, aPrincipal, aMessageId, Some(data));
|
||||
return Dispatch(dispatcher);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushNotifier::NotifyPush(const nsACString& aScope, nsIPrincipal* aPrincipal,
|
||||
const nsAString& aMessageId)
|
||||
{
|
||||
NS_ENSURE_ARG(aPrincipal);
|
||||
PushMessageDispatcher dispatcher(aScope, aPrincipal, aMessageId, Nothing());
|
||||
return Dispatch(dispatcher);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushNotifier::NotifySubscriptionChange(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal)
|
||||
{
|
||||
NS_ENSURE_ARG(aPrincipal);
|
||||
PushSubscriptionChangeDispatcher dispatcher(aScope, aPrincipal);
|
||||
return Dispatch(dispatcher);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushNotifier::NotifySubscriptionModified(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal)
|
||||
{
|
||||
NS_ENSURE_ARG(aPrincipal);
|
||||
PushSubscriptionModifiedDispatcher dispatcher(aScope, aPrincipal);
|
||||
return Dispatch(dispatcher);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushNotifier::NotifyError(const nsACString& aScope, nsIPrincipal* aPrincipal,
|
||||
const nsAString& aMessage, uint32_t aFlags)
|
||||
{
|
||||
NS_ENSURE_ARG(aPrincipal);
|
||||
PushErrorDispatcher dispatcher(aScope, aPrincipal, aMessage, aFlags);
|
||||
return Dispatch(dispatcher);
|
||||
}
|
||||
|
||||
nsresult
|
||||
PushNotifier::Dispatch(PushDispatcher& aDispatcher)
|
||||
{
|
||||
if (XRE_IsParentProcess()) {
|
||||
// Always notify XPCOM observers in the parent process.
|
||||
Unused << NS_WARN_IF(NS_FAILED(aDispatcher.NotifyObservers()));
|
||||
|
||||
nsTArray<ContentParent*> contentActors;
|
||||
ContentParent::GetAll(contentActors);
|
||||
if (!contentActors.IsEmpty()) {
|
||||
// At least one content process is active, so e10s must be enabled.
|
||||
// Broadcast a message to notify observers and service workers.
|
||||
for (uint32_t i = 0; i < contentActors.Length(); ++i) {
|
||||
Unused << NS_WARN_IF(!aDispatcher.SendToChild(contentActors[i]));
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (BrowserTabsRemoteAutostart()) {
|
||||
// e10s is enabled, but no content processes are active.
|
||||
return aDispatcher.HandleNoChildProcesses();
|
||||
}
|
||||
|
||||
// e10s is disabled; notify workers in the parent.
|
||||
return aDispatcher.NotifyWorkers();
|
||||
}
|
||||
|
||||
// Otherwise, we're in the content process, so e10s must be enabled. Notify
|
||||
// observers and workers, then send a message to notify observers in the
|
||||
// parent.
|
||||
MOZ_ASSERT(XRE_IsContentProcess());
|
||||
|
||||
nsresult rv = aDispatcher.NotifyObserversAndWorkers();
|
||||
|
||||
ContentChild* parentActor = ContentChild::GetSingleton();
|
||||
if (!NS_WARN_IF(!parentActor)) {
|
||||
Unused << NS_WARN_IF(!aDispatcher.SendToParent(parentActor));
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
PushData::PushData(const nsTArray<uint8_t>& aData)
|
||||
: mData(aData)
|
||||
{}
|
||||
|
||||
PushData::~PushData()
|
||||
{}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_0(PushData)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushData)
|
||||
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPushData)
|
||||
NS_INTERFACE_MAP_ENTRY(nsIPushData)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushData)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushData)
|
||||
|
||||
nsresult
|
||||
PushData::EnsureDecodedText()
|
||||
{
|
||||
if (mData.IsEmpty() || !mDecodedText.IsEmpty()) {
|
||||
return NS_OK;
|
||||
}
|
||||
nsresult rv = BodyUtil::ConsumeText(
|
||||
mData.Length(),
|
||||
reinterpret_cast<uint8_t*>(mData.Elements()),
|
||||
mDecodedText
|
||||
);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
mDecodedText.Truncate();
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushData::Text(nsAString& aText)
|
||||
{
|
||||
nsresult rv = EnsureDecodedText();
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
aText = mDecodedText;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushData::Json(JSContext* aCx,
|
||||
JS::MutableHandle<JS::Value> aResult)
|
||||
{
|
||||
nsresult rv = EnsureDecodedText();
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
ErrorResult error;
|
||||
BodyUtil::ConsumeJson(aCx, aResult, mDecodedText, error);
|
||||
return error.StealNSResult();
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushData::Binary(uint32_t* aDataLen, uint8_t** aData)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aDataLen);
|
||||
NS_ENSURE_ARG_POINTER(aData);
|
||||
|
||||
*aData = nullptr;
|
||||
if (mData.IsEmpty()) {
|
||||
*aDataLen = 0;
|
||||
return NS_OK;
|
||||
}
|
||||
uint32_t length = mData.Length();
|
||||
uint8_t* data = static_cast<uint8_t*>(NS_Alloc(length * sizeof(uint8_t)));
|
||||
if (!data) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
memcpy(data, mData.Elements(), length * sizeof(uint8_t));
|
||||
*aDataLen = length;
|
||||
*aData = data;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
PushMessage::PushMessage(nsIPrincipal* aPrincipal, nsIPushData* aData)
|
||||
: mPrincipal(aPrincipal)
|
||||
, mData(aData)
|
||||
{}
|
||||
|
||||
PushMessage::~PushMessage()
|
||||
{}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION(PushMessage, mPrincipal, mData)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushMessage)
|
||||
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPushMessage)
|
||||
NS_INTERFACE_MAP_ENTRY(nsIPushMessage)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushMessage)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushMessage)
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushMessage::GetPrincipal(nsIPrincipal** aPrincipal)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aPrincipal);
|
||||
|
||||
nsCOMPtr<nsIPrincipal> principal = mPrincipal;
|
||||
principal.forget(aPrincipal);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
PushMessage::GetData(nsIPushData** aData)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aData);
|
||||
|
||||
nsCOMPtr<nsIPushData> data = mData;
|
||||
data.forget(aData);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
PushDispatcher::PushDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal)
|
||||
: mScope(aScope)
|
||||
, mPrincipal(aPrincipal)
|
||||
{}
|
||||
|
||||
PushDispatcher::~PushDispatcher()
|
||||
{}
|
||||
|
||||
nsresult
|
||||
PushDispatcher::HandleNoChildProcesses()
|
||||
{
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
PushDispatcher::NotifyObserversAndWorkers()
|
||||
{
|
||||
Unused << NS_WARN_IF(NS_FAILED(NotifyObservers()));
|
||||
return NotifyWorkers();
|
||||
}
|
||||
|
||||
bool
|
||||
PushDispatcher::ShouldNotifyWorkers()
|
||||
{
|
||||
if (NS_WARN_IF(!mPrincipal)) {
|
||||
return false;
|
||||
}
|
||||
// System subscriptions use observer notifications instead of service worker
|
||||
// events. The `testing.notifyWorkers` pref disables worker events for
|
||||
// non-system subscriptions.
|
||||
return !nsContentUtils::IsSystemPrincipal(mPrincipal) &&
|
||||
Preferences::GetBool("dom.push.testing.notifyWorkers", true);
|
||||
}
|
||||
|
||||
nsresult
|
||||
PushDispatcher::DoNotifyObservers(nsISupports *aSubject, const char *aTopic,
|
||||
const nsACString& aScope)
|
||||
{
|
||||
nsCOMPtr<nsIObserverService> obsService =
|
||||
mozilla::services::GetObserverService();
|
||||
if (!obsService) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
// If there's a service for this push category, make sure it is alive.
|
||||
nsCOMPtr<nsICategoryManager> catMan =
|
||||
do_GetService(NS_CATEGORYMANAGER_CONTRACTID);
|
||||
if (catMan) {
|
||||
nsXPIDLCString contractId;
|
||||
nsresult rv = catMan->GetCategoryEntry("push",
|
||||
mScope.BeginReading(),
|
||||
getter_Copies(contractId));
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
// Ensure the service is created - we don't need to do anything with
|
||||
// it though - we assume the service constructor attaches a listener.
|
||||
nsCOMPtr<nsISupports> service = do_GetService(contractId);
|
||||
}
|
||||
}
|
||||
return obsService->NotifyObservers(aSubject, aTopic,
|
||||
NS_ConvertUTF8toUTF16(mScope).get());
|
||||
}
|
||||
|
||||
PushMessageDispatcher::PushMessageDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal,
|
||||
const nsAString& aMessageId,
|
||||
const Maybe<nsTArray<uint8_t>>& aData)
|
||||
: PushDispatcher(aScope, aPrincipal)
|
||||
, mMessageId(aMessageId)
|
||||
, mData(aData)
|
||||
{}
|
||||
|
||||
PushMessageDispatcher::~PushMessageDispatcher()
|
||||
{}
|
||||
|
||||
nsresult
|
||||
PushMessageDispatcher::NotifyObservers()
|
||||
{
|
||||
nsCOMPtr<nsIPushData> data;
|
||||
if (mData) {
|
||||
data = new PushData(mData.ref());
|
||||
}
|
||||
nsCOMPtr<nsIPushMessage> message = new PushMessage(mPrincipal, data);
|
||||
return DoNotifyObservers(message, OBSERVER_TOPIC_PUSH, mScope);
|
||||
}
|
||||
|
||||
nsresult
|
||||
PushMessageDispatcher::NotifyWorkers()
|
||||
{
|
||||
if (!ShouldNotifyWorkers()) {
|
||||
return NS_OK;
|
||||
}
|
||||
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
|
||||
if (!swm) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
nsAutoCString originSuffix;
|
||||
nsresult rv = mPrincipal->GetOriginSuffix(originSuffix);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
return swm->SendPushEvent(originSuffix, mScope, mMessageId, mData);
|
||||
}
|
||||
|
||||
bool
|
||||
PushMessageDispatcher::SendToParent(ContentChild* aParentActor)
|
||||
{
|
||||
if (mData) {
|
||||
return aParentActor->SendNotifyPushObserversWithData(mScope,
|
||||
IPC::Principal(mPrincipal),
|
||||
mMessageId,
|
||||
mData.ref());
|
||||
}
|
||||
return aParentActor->SendNotifyPushObservers(mScope,
|
||||
IPC::Principal(mPrincipal),
|
||||
mMessageId);
|
||||
}
|
||||
|
||||
bool
|
||||
PushMessageDispatcher::SendToChild(ContentParent* aContentActor)
|
||||
{
|
||||
if (mData) {
|
||||
return aContentActor->SendPushWithData(mScope, IPC::Principal(mPrincipal),
|
||||
mMessageId, mData.ref());
|
||||
}
|
||||
return aContentActor->SendPush(mScope, IPC::Principal(mPrincipal),
|
||||
mMessageId);
|
||||
}
|
||||
|
||||
PushSubscriptionChangeDispatcher::PushSubscriptionChangeDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal)
|
||||
: PushDispatcher(aScope, aPrincipal)
|
||||
{}
|
||||
|
||||
PushSubscriptionChangeDispatcher::~PushSubscriptionChangeDispatcher()
|
||||
{}
|
||||
|
||||
nsresult
|
||||
PushSubscriptionChangeDispatcher::NotifyObservers()
|
||||
{
|
||||
return DoNotifyObservers(mPrincipal, OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
|
||||
mScope);
|
||||
}
|
||||
|
||||
nsresult
|
||||
PushSubscriptionChangeDispatcher::NotifyWorkers()
|
||||
{
|
||||
if (!ShouldNotifyWorkers()) {
|
||||
return NS_OK;
|
||||
}
|
||||
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
|
||||
if (!swm) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
nsAutoCString originSuffix;
|
||||
nsresult rv = mPrincipal->GetOriginSuffix(originSuffix);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
return swm->SendPushSubscriptionChangeEvent(originSuffix, mScope);
|
||||
}
|
||||
|
||||
bool
|
||||
PushSubscriptionChangeDispatcher::SendToParent(ContentChild* aParentActor)
|
||||
{
|
||||
return aParentActor->SendNotifyPushSubscriptionChangeObservers(mScope,
|
||||
IPC::Principal(mPrincipal));
|
||||
}
|
||||
|
||||
bool
|
||||
PushSubscriptionChangeDispatcher::SendToChild(ContentParent* aContentActor)
|
||||
{
|
||||
return aContentActor->SendPushSubscriptionChange(mScope,
|
||||
IPC::Principal(mPrincipal));
|
||||
}
|
||||
|
||||
PushSubscriptionModifiedDispatcher::PushSubscriptionModifiedDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal)
|
||||
: PushDispatcher(aScope, aPrincipal)
|
||||
{}
|
||||
|
||||
PushSubscriptionModifiedDispatcher::~PushSubscriptionModifiedDispatcher()
|
||||
{}
|
||||
|
||||
nsresult
|
||||
PushSubscriptionModifiedDispatcher::NotifyObservers()
|
||||
{
|
||||
return DoNotifyObservers(mPrincipal, OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
|
||||
mScope);
|
||||
}
|
||||
|
||||
nsresult
|
||||
PushSubscriptionModifiedDispatcher::NotifyWorkers()
|
||||
{
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool
|
||||
PushSubscriptionModifiedDispatcher::SendToParent(ContentChild* aParentActor)
|
||||
{
|
||||
return aParentActor->SendNotifyPushSubscriptionModifiedObservers(mScope,
|
||||
IPC::Principal(mPrincipal));
|
||||
}
|
||||
|
||||
bool
|
||||
PushSubscriptionModifiedDispatcher::SendToChild(ContentParent* aContentActor)
|
||||
{
|
||||
return aContentActor->SendNotifyPushSubscriptionModifiedObservers(mScope,
|
||||
IPC::Principal(mPrincipal));
|
||||
}
|
||||
|
||||
PushErrorDispatcher::PushErrorDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal,
|
||||
const nsAString& aMessage,
|
||||
uint32_t aFlags)
|
||||
: PushDispatcher(aScope, aPrincipal)
|
||||
, mMessage(aMessage)
|
||||
, mFlags(aFlags)
|
||||
{}
|
||||
|
||||
PushErrorDispatcher::~PushErrorDispatcher()
|
||||
{}
|
||||
|
||||
nsresult
|
||||
PushErrorDispatcher::NotifyObservers()
|
||||
{
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
PushErrorDispatcher::NotifyWorkers()
|
||||
{
|
||||
if (!ShouldNotifyWorkers()) {
|
||||
// For system subscriptions, log the error directly to the browser console.
|
||||
return nsContentUtils::ReportToConsoleNonLocalized(mMessage,
|
||||
mFlags,
|
||||
NS_LITERAL_CSTRING("Push"),
|
||||
nullptr, /* aDocument */
|
||||
nullptr, /* aURI */
|
||||
EmptyString(), /* aLine */
|
||||
0, /* aLineNumber */
|
||||
0, /* aColumnNumber */
|
||||
nsContentUtils::eOMIT_LOCATION);
|
||||
}
|
||||
// For service worker subscriptions, report the error to all clients.
|
||||
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
|
||||
if (swm) {
|
||||
swm->ReportToAllClients(mScope,
|
||||
mMessage,
|
||||
NS_ConvertUTF8toUTF16(mScope), /* aFilename */
|
||||
EmptyString(), /* aLine */
|
||||
0, /* aLineNumber */
|
||||
0, /* aColumnNumber */
|
||||
mFlags);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool
|
||||
PushErrorDispatcher::SendToParent(ContentChild*)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
PushErrorDispatcher::SendToChild(ContentParent* aContentActor)
|
||||
{
|
||||
return aContentActor->SendPushError(mScope, IPC::Principal(mPrincipal),
|
||||
mMessage, mFlags);
|
||||
}
|
||||
|
||||
nsresult
|
||||
PushErrorDispatcher::HandleNoChildProcesses()
|
||||
{
|
||||
// Report to the console if no content processes are active.
|
||||
nsCOMPtr<nsIURI> scopeURI;
|
||||
nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), mScope);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
return nsContentUtils::ReportToConsoleNonLocalized(mMessage,
|
||||
mFlags,
|
||||
NS_LITERAL_CSTRING("Push"),
|
||||
nullptr, /* aDocument */
|
||||
scopeURI, /* aURI */
|
||||
EmptyString(), /* aLine */
|
||||
0, /* aLineNumber */
|
||||
0, /* aColumnNumber */
|
||||
nsContentUtils::eOMIT_LOCATION);
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
203
dom/push/PushNotifier.h
Normal file
203
dom/push/PushNotifier.h
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_dom_PushNotifier_h
|
||||
#define mozilla_dom_PushNotifier_h
|
||||
|
||||
#include "nsIPushNotifier.h"
|
||||
|
||||
#include "nsCycleCollectionParticipant.h"
|
||||
#include "nsIPrincipal.h"
|
||||
#include "nsString.h"
|
||||
|
||||
#include "mozilla/Maybe.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
class ContentChild;
|
||||
class ContentParent;
|
||||
|
||||
/**
|
||||
* `PushDispatcher` is a base class used to forward observer notifications and
|
||||
* service worker events to the correct process.
|
||||
*/
|
||||
class MOZ_STACK_CLASS PushDispatcher
|
||||
{
|
||||
public:
|
||||
// Fires an XPCOM observer notification. This method may be called from both
|
||||
// processes.
|
||||
virtual nsresult NotifyObservers() = 0;
|
||||
|
||||
// Fires a service worker event. This method is called from the content
|
||||
// process if e10s is enabled, or the parent otherwise.
|
||||
virtual nsresult NotifyWorkers() = 0;
|
||||
|
||||
// A convenience method that calls `NotifyObservers` and `NotifyWorkers`.
|
||||
nsresult NotifyObserversAndWorkers();
|
||||
|
||||
// Sends an IPDL message to fire an observer notification in the parent
|
||||
// process. This method is only called from the content process, and only
|
||||
// if e10s is enabled.
|
||||
virtual bool SendToParent(ContentChild* aParentActor) = 0;
|
||||
|
||||
// Sends an IPDL message to fire an observer notification and a service worker
|
||||
// event in the content process. This method is only called from the parent,
|
||||
// and only if e10s is enabled.
|
||||
virtual bool SendToChild(ContentParent* aContentActor) = 0;
|
||||
|
||||
// An optional method, called from the parent if e10s is enabled and there
|
||||
// are no active content processes. The default behavior is a no-op.
|
||||
virtual nsresult HandleNoChildProcesses();
|
||||
|
||||
protected:
|
||||
PushDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal);
|
||||
|
||||
virtual ~PushDispatcher();
|
||||
|
||||
bool ShouldNotifyWorkers();
|
||||
nsresult DoNotifyObservers(nsISupports *aSubject, const char *aTopic,
|
||||
const nsACString& aScope);
|
||||
|
||||
const nsCString mScope;
|
||||
nsCOMPtr<nsIPrincipal> mPrincipal;
|
||||
};
|
||||
|
||||
/**
|
||||
* `PushNotifier` implements the `nsIPushNotifier` interface. This service
|
||||
* broadcasts XPCOM observer notifications for incoming push messages, then
|
||||
* forwards incoming push messages to service workers.
|
||||
*
|
||||
* All scriptable methods on this interface may be called from the parent or
|
||||
* content process. Observer notifications are broadcasted to both processes.
|
||||
*/
|
||||
class PushNotifier final : public nsIPushNotifier
|
||||
{
|
||||
public:
|
||||
PushNotifier();
|
||||
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(PushNotifier, nsIPushNotifier)
|
||||
NS_DECL_NSIPUSHNOTIFIER
|
||||
|
||||
private:
|
||||
~PushNotifier();
|
||||
|
||||
nsresult Dispatch(PushDispatcher& aDispatcher);
|
||||
};
|
||||
|
||||
/**
|
||||
* `PushData` provides methods for retrieving push message data in different
|
||||
* formats. This class is similar to the `PushMessageData` WebIDL interface.
|
||||
*/
|
||||
class PushData final : public nsIPushData
|
||||
{
|
||||
public:
|
||||
explicit PushData(const nsTArray<uint8_t>& aData);
|
||||
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(PushData, nsIPushData)
|
||||
NS_DECL_NSIPUSHDATA
|
||||
|
||||
private:
|
||||
~PushData();
|
||||
|
||||
nsresult EnsureDecodedText();
|
||||
|
||||
nsTArray<uint8_t> mData;
|
||||
nsString mDecodedText;
|
||||
};
|
||||
|
||||
/**
|
||||
* `PushMessage` exposes the subscription principal and data for a push
|
||||
* message. Each `push-message` observer receives an instance of this class
|
||||
* as the subject.
|
||||
*/
|
||||
class PushMessage final : public nsIPushMessage
|
||||
{
|
||||
public:
|
||||
PushMessage(nsIPrincipal* aPrincipal, nsIPushData* aData);
|
||||
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(PushMessage, nsIPushMessage)
|
||||
NS_DECL_NSIPUSHMESSAGE
|
||||
|
||||
private:
|
||||
~PushMessage();
|
||||
|
||||
nsCOMPtr<nsIPrincipal> mPrincipal;
|
||||
nsCOMPtr<nsIPushData> mData;
|
||||
};
|
||||
|
||||
class PushMessageDispatcher final : public PushDispatcher
|
||||
{
|
||||
public:
|
||||
PushMessageDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal,
|
||||
const nsAString& aMessageId,
|
||||
const Maybe<nsTArray<uint8_t>>& aData);
|
||||
~PushMessageDispatcher();
|
||||
|
||||
nsresult NotifyObservers() override;
|
||||
nsresult NotifyWorkers() override;
|
||||
bool SendToParent(ContentChild* aParentActor) override;
|
||||
bool SendToChild(ContentParent* aContentActor) override;
|
||||
|
||||
private:
|
||||
const nsString mMessageId;
|
||||
const Maybe<nsTArray<uint8_t>> mData;
|
||||
};
|
||||
|
||||
class PushSubscriptionChangeDispatcher final : public PushDispatcher
|
||||
{
|
||||
public:
|
||||
PushSubscriptionChangeDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal);
|
||||
~PushSubscriptionChangeDispatcher();
|
||||
|
||||
nsresult NotifyObservers() override;
|
||||
nsresult NotifyWorkers() override;
|
||||
bool SendToParent(ContentChild* aParentActor) override;
|
||||
bool SendToChild(ContentParent* aContentActor) override;
|
||||
};
|
||||
|
||||
class PushSubscriptionModifiedDispatcher : public PushDispatcher
|
||||
{
|
||||
public:
|
||||
PushSubscriptionModifiedDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal);
|
||||
~PushSubscriptionModifiedDispatcher();
|
||||
|
||||
nsresult NotifyObservers() override;
|
||||
nsresult NotifyWorkers() override;
|
||||
bool SendToParent(ContentChild* aParentActor) override;
|
||||
bool SendToChild(ContentParent* aContentActor) override;
|
||||
};
|
||||
|
||||
class PushErrorDispatcher final : public PushDispatcher
|
||||
{
|
||||
public:
|
||||
PushErrorDispatcher(const nsACString& aScope,
|
||||
nsIPrincipal* aPrincipal,
|
||||
const nsAString& aMessage,
|
||||
uint32_t aFlags);
|
||||
~PushErrorDispatcher();
|
||||
|
||||
nsresult NotifyObservers() override;
|
||||
nsresult NotifyWorkers() override;
|
||||
bool SendToParent(ContentChild* aParentActor) override;
|
||||
bool SendToChild(ContentParent* aContentActor) override;
|
||||
|
||||
private:
|
||||
nsresult HandleNoChildProcesses() override;
|
||||
|
||||
const nsString mMessage;
|
||||
uint32_t mFlags;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_PushNotifier_h
|
||||
318
dom/push/PushRecord.jsm
Normal file
318
dom/push/PushRecord.jsm
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
/* 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 Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
|
||||
"resource://gre/modules/Messaging.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PushRecord"];
|
||||
|
||||
const prefs = new Preferences("dom.push.");
|
||||
|
||||
/**
|
||||
* The push subscription record, stored in IndexedDB.
|
||||
*/
|
||||
function PushRecord(props) {
|
||||
this.pushEndpoint = props.pushEndpoint;
|
||||
this.scope = props.scope;
|
||||
this.originAttributes = props.originAttributes;
|
||||
this.pushCount = props.pushCount || 0;
|
||||
this.lastPush = props.lastPush || 0;
|
||||
this.p256dhPublicKey = props.p256dhPublicKey;
|
||||
this.p256dhPrivateKey = props.p256dhPrivateKey;
|
||||
this.authenticationSecret = props.authenticationSecret;
|
||||
this.systemRecord = !!props.systemRecord;
|
||||
this.appServerKey = props.appServerKey;
|
||||
this.recentMessageIDs = props.recentMessageIDs;
|
||||
this.setQuota(props.quota);
|
||||
this.ctime = (typeof props.ctime === "number") ? props.ctime : 0;
|
||||
}
|
||||
|
||||
PushRecord.prototype = {
|
||||
setQuota(suggestedQuota) {
|
||||
if (this.quotaApplies()) {
|
||||
let quota = +suggestedQuota;
|
||||
this.quota = quota >= 0 ? quota : prefs.get("maxQuotaPerSubscription");
|
||||
} else {
|
||||
this.quota = Infinity;
|
||||
}
|
||||
},
|
||||
|
||||
resetQuota() {
|
||||
this.quota = this.quotaApplies() ?
|
||||
prefs.get("maxQuotaPerSubscription") : Infinity;
|
||||
},
|
||||
|
||||
updateQuota(lastVisit) {
|
||||
if (this.isExpired() || !this.quotaApplies()) {
|
||||
// Ignore updates if the registration is already expired, or isn't
|
||||
// subject to quota.
|
||||
return;
|
||||
}
|
||||
if (lastVisit < 0) {
|
||||
// If the user cleared their history, but retained the push permission,
|
||||
// mark the registration as expired.
|
||||
this.quota = 0;
|
||||
return;
|
||||
}
|
||||
if (lastVisit > this.lastPush) {
|
||||
// If the user visited the site since the last time we received a
|
||||
// notification, reset the quota. `Math.max(0, ...)` ensures the
|
||||
// last visit date isn't in the future.
|
||||
let daysElapsed =
|
||||
Math.max(0, (Date.now() - lastVisit) / 24 / 60 / 60 / 1000);
|
||||
this.quota = Math.min(
|
||||
Math.round(8 * Math.pow(daysElapsed, -0.8)),
|
||||
prefs.get("maxQuotaPerSubscription")
|
||||
);
|
||||
Services.telemetry.getHistogramById("PUSH_API_QUOTA_RESET_TO").add(this.quota);
|
||||
}
|
||||
},
|
||||
|
||||
receivedPush(lastVisit) {
|
||||
this.updateQuota(lastVisit);
|
||||
this.pushCount++;
|
||||
this.lastPush = Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Records a message ID sent to this push registration. We track the last few
|
||||
* messages sent to each registration to avoid firing duplicate events for
|
||||
* unacknowledged messages.
|
||||
*/
|
||||
noteRecentMessageID(id) {
|
||||
if (this.recentMessageIDs) {
|
||||
this.recentMessageIDs.unshift(id);
|
||||
} else {
|
||||
this.recentMessageIDs = [id];
|
||||
}
|
||||
// Drop older message IDs from the end of the list.
|
||||
let maxRecentMessageIDs = Math.min(
|
||||
this.recentMessageIDs.length,
|
||||
Math.max(prefs.get("maxRecentMessageIDsPerSubscription"), 0)
|
||||
);
|
||||
this.recentMessageIDs.length = maxRecentMessageIDs || 0;
|
||||
},
|
||||
|
||||
hasRecentMessageID(id) {
|
||||
return this.recentMessageIDs && this.recentMessageIDs.includes(id);
|
||||
},
|
||||
|
||||
reduceQuota() {
|
||||
if (!this.quotaApplies()) {
|
||||
return;
|
||||
}
|
||||
this.quota = Math.max(this.quota - 1, 0);
|
||||
// We check for ctime > 0 to skip older records that did not have ctime.
|
||||
if (this.isExpired() && this.ctime > 0) {
|
||||
let duration = Date.now() - this.ctime;
|
||||
Services.telemetry.getHistogramById("PUSH_API_QUOTA_EXPIRATION_TIME").add(duration / 1000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Queries the Places database for the last time a user visited the site
|
||||
* associated with a push registration.
|
||||
*
|
||||
* @returns {Promise} A promise resolved with either the last time the user
|
||||
* visited the site, or `-Infinity` if the site is not in the user's history.
|
||||
* The time is expressed in milliseconds since Epoch.
|
||||
*/
|
||||
getLastVisit: Task.async(function* () {
|
||||
if (!this.quotaApplies() || this.isTabOpen()) {
|
||||
// If the registration isn't subject to quota, or the user already
|
||||
// has the site open, skip expensive database queries.
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
if (AppConstants.MOZ_ANDROID_HISTORY) {
|
||||
let result = yield Messaging.sendRequestForResult({
|
||||
type: "History:GetPrePathLastVisitedTimeMilliseconds",
|
||||
prePath: this.uri.prePath,
|
||||
});
|
||||
return result == 0 ? -Infinity : result;
|
||||
}
|
||||
|
||||
// Places History transition types that can fire a
|
||||
// `pushsubscriptionchange` event when the user visits a site with expired push
|
||||
// registrations. Visits only count if the user sees the origin in the address
|
||||
// bar. This excludes embedded resources, downloads, and framed links.
|
||||
const QUOTA_REFRESH_TRANSITIONS_SQL = [
|
||||
Ci.nsINavHistoryService.TRANSITION_LINK,
|
||||
Ci.nsINavHistoryService.TRANSITION_TYPED,
|
||||
Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
|
||||
Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
|
||||
Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY
|
||||
].join(",");
|
||||
|
||||
let db = yield PlacesUtils.promiseDBConnection();
|
||||
// We're using a custom query instead of `nsINavHistoryQueryOptions`
|
||||
// because the latter doesn't expose a way to filter by transition type:
|
||||
// `setTransitions` performs a logical "and," but we want an "or." We
|
||||
// also avoid an unneeded left join on `moz_favicons`, and an `ORDER BY`
|
||||
// clause that emits a suboptimal index warning.
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT MAX(visit_date) AS lastVisit
|
||||
FROM moz_places p
|
||||
JOIN moz_historyvisits ON p.id = place_id
|
||||
WHERE rev_host = get_unreversed_host(:host || '.') || '.'
|
||||
AND url BETWEEN :prePath AND :prePath || X'FFFF'
|
||||
AND visit_type IN (${QUOTA_REFRESH_TRANSITIONS_SQL})
|
||||
`,
|
||||
{
|
||||
// Restrict the query to all pages for this origin.
|
||||
host: this.uri.host,
|
||||
prePath: this.uri.prePath,
|
||||
}
|
||||
);
|
||||
|
||||
if (!rows.length) {
|
||||
return -Infinity;
|
||||
}
|
||||
// Places records times in microseconds.
|
||||
let lastVisit = rows[0].getResultByName("lastVisit");
|
||||
|
||||
return lastVisit / 1000;
|
||||
}),
|
||||
|
||||
isTabOpen() {
|
||||
let windows = Services.wm.getEnumerator("navigator:browser");
|
||||
while (windows.hasMoreElements()) {
|
||||
let window = windows.getNext();
|
||||
if (window.closed || PrivateBrowsingUtils.isWindowPrivate(window)) {
|
||||
continue;
|
||||
}
|
||||
// `gBrowser` on Desktop; `BrowserApp` on Fennec.
|
||||
let tabs = window.gBrowser ? window.gBrowser.tabContainer.children :
|
||||
window.BrowserApp.tabs;
|
||||
for (let tab of tabs) {
|
||||
// `linkedBrowser` on Desktop; `browser` on Fennec.
|
||||
let tabURI = (tab.linkedBrowser || tab.browser).currentURI;
|
||||
if (tabURI.prePath == this.uri.prePath) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates whether the registration can deliver push messages to its
|
||||
* associated service worker. System subscriptions are exempt from the
|
||||
* permission check.
|
||||
*/
|
||||
hasPermission() {
|
||||
if (this.systemRecord || prefs.get("testing.ignorePermission")) {
|
||||
return true;
|
||||
}
|
||||
let permission = Services.perms.testExactPermissionFromPrincipal(
|
||||
this.principal, "desktop-notification");
|
||||
return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
|
||||
},
|
||||
|
||||
quotaChanged() {
|
||||
if (!this.hasPermission()) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return this.getLastVisit()
|
||||
.then(lastVisit => lastVisit > this.lastPush);
|
||||
},
|
||||
|
||||
quotaApplies() {
|
||||
return !this.systemRecord;
|
||||
},
|
||||
|
||||
isExpired() {
|
||||
return this.quota === 0;
|
||||
},
|
||||
|
||||
matchesOriginAttributes(pattern) {
|
||||
if (this.systemRecord) {
|
||||
return false;
|
||||
}
|
||||
return ChromeUtils.originAttributesMatchPattern(
|
||||
this.principal.originAttributes, pattern);
|
||||
},
|
||||
|
||||
hasAuthenticationSecret() {
|
||||
return !!this.authenticationSecret &&
|
||||
this.authenticationSecret.byteLength == 16;
|
||||
},
|
||||
|
||||
matchesAppServerKey(key) {
|
||||
if (!this.appServerKey) {
|
||||
return !key;
|
||||
}
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
return this.appServerKey.length === key.length &&
|
||||
this.appServerKey.every((value, index) => value === key[index]);
|
||||
},
|
||||
|
||||
toSubscription() {
|
||||
return {
|
||||
endpoint: this.pushEndpoint,
|
||||
lastPush: this.lastPush,
|
||||
pushCount: this.pushCount,
|
||||
p256dhKey: this.p256dhPublicKey,
|
||||
p256dhPrivateKey: this.p256dhPrivateKey,
|
||||
authenticationSecret: this.authenticationSecret,
|
||||
appServerKey: this.appServerKey,
|
||||
quota: this.quotaApplies() ? this.quota : -1,
|
||||
systemRecord: this.systemRecord,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Define lazy getters for the principal and scope URI. IndexedDB can't store
|
||||
// `nsIPrincipal` objects, so we keep them in a private weak map.
|
||||
var principals = new WeakMap();
|
||||
Object.defineProperties(PushRecord.prototype, {
|
||||
principal: {
|
||||
get() {
|
||||
if (this.systemRecord) {
|
||||
return Services.scriptSecurityManager.getSystemPrincipal();
|
||||
}
|
||||
let principal = principals.get(this);
|
||||
if (!principal) {
|
||||
let uri = Services.io.newURI(this.scope, null, null);
|
||||
// Allow tests to omit origin attributes.
|
||||
let originSuffix = this.originAttributes || "";
|
||||
let originAttributes =
|
||||
principal = Services.scriptSecurityManager.createCodebasePrincipal(uri,
|
||||
ChromeUtils.createOriginAttributesFromOrigin(originSuffix));
|
||||
principals.set(this, principal);
|
||||
}
|
||||
return principal;
|
||||
},
|
||||
configurable: true,
|
||||
},
|
||||
|
||||
uri: {
|
||||
get() {
|
||||
return this.principal.URI;
|
||||
},
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
1365
dom/push/PushService.jsm
Normal file
1365
dom/push/PushService.jsm
Normal file
File diff suppressed because it is too large
Load diff
275
dom/push/PushServiceAndroidGCM.jsm
Normal file
275
dom/push/PushServiceAndroidGCM.jsm
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
/* 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 Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
||||
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
|
||||
const {PushCrypto} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||
Cu.import("resource://gre/modules/Messaging.jsm"); /*global: Messaging */
|
||||
Cu.import("resource://gre/modules/Services.jsm"); /*global: Services */
|
||||
Cu.import("resource://gre/modules/Preferences.jsm"); /*global: Preferences */
|
||||
Cu.import("resource://gre/modules/Promise.jsm"); /*global: Promise */
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global: XPCOMUtils */
|
||||
|
||||
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("Push");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "console", () => {
|
||||
let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
|
||||
return new ConsoleAPI({
|
||||
dump: Log.i,
|
||||
maxLogLevelPref: "dom.push.loglevel",
|
||||
prefix: "PushServiceAndroidGCM",
|
||||
});
|
||||
});
|
||||
|
||||
const kPUSHANDROIDGCMDB_DB_NAME = "pushAndroidGCM";
|
||||
const kPUSHANDROIDGCMDB_DB_VERSION = 5; // Change this if the IndexedDB format changes
|
||||
const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM";
|
||||
|
||||
const FXA_PUSH_SCOPE = "chrome://fxa-push";
|
||||
|
||||
const prefs = new Preferences("dom.push.");
|
||||
|
||||
/**
|
||||
* The implementation of WebPush push backed by Android's GCM
|
||||
* delivery.
|
||||
*/
|
||||
this.PushServiceAndroidGCM = {
|
||||
_mainPushService: null,
|
||||
_serverURI: null,
|
||||
|
||||
newPushDB: function() {
|
||||
return new PushDB(kPUSHANDROIDGCMDB_DB_NAME,
|
||||
kPUSHANDROIDGCMDB_DB_VERSION,
|
||||
kPUSHANDROIDGCMDB_STORE_NAME,
|
||||
"channelID",
|
||||
PushRecordAndroidGCM);
|
||||
},
|
||||
|
||||
validServerURI: function(serverURI) {
|
||||
if (!serverURI) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (serverURI.scheme == "https") {
|
||||
return true;
|
||||
}
|
||||
if (serverURI.scheme == "http") {
|
||||
// Allow insecure server URLs for development and testing.
|
||||
return !!prefs.get("testing.allowInsecureServerURL");
|
||||
}
|
||||
console.info("Unsupported Android GCM dom.push.serverURL scheme", serverURI.scheme);
|
||||
return false;
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "nsPref:changed":
|
||||
if (data == "dom.push.debug") {
|
||||
// Reconfigure.
|
||||
let debug = !!prefs.get("debug");
|
||||
console.info("Debug parameter changed; updating configuration with new debug", debug);
|
||||
this._configure(this._serverURI, debug);
|
||||
}
|
||||
break;
|
||||
case "PushServiceAndroidGCM:ReceivedPushMessage":
|
||||
this._onPushMessageReceived(data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_onPushMessageReceived(data) {
|
||||
// TODO: Use Messaging.jsm for this.
|
||||
if (this._mainPushService == null) {
|
||||
// Shouldn't ever happen, but let's be careful.
|
||||
console.error("No main PushService! Dropping message.");
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
console.error("No data from Java! Dropping message.");
|
||||
return;
|
||||
}
|
||||
data = JSON.parse(data);
|
||||
console.debug("ReceivedPushMessage with data", data);
|
||||
|
||||
let { headers, message } = this._messageAndHeaders(data);
|
||||
|
||||
console.debug("Delivering message to main PushService:", message, headers);
|
||||
this._mainPushService.receivedPushMessage(
|
||||
data.channelID, "", headers, message, (record) => {
|
||||
// Always update the stored record.
|
||||
return record;
|
||||
});
|
||||
},
|
||||
|
||||
_messageAndHeaders(data) {
|
||||
// Default is no data (and no encryption).
|
||||
let message = null;
|
||||
let headers = null;
|
||||
|
||||
if (data.message && data.enc && (data.enckey || data.cryptokey)) {
|
||||
headers = {
|
||||
encryption_key: data.enckey,
|
||||
crypto_key: data.cryptokey,
|
||||
encryption: data.enc,
|
||||
encoding: data.con,
|
||||
};
|
||||
// Ciphertext is (urlsafe) Base 64 encoded.
|
||||
message = ChromeUtils.base64URLDecode(data.message, {
|
||||
// The Push server may append padding.
|
||||
padding: "ignore",
|
||||
});
|
||||
}
|
||||
return { headers, message };
|
||||
},
|
||||
|
||||
_configure: function(serverURL, debug) {
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:Configure",
|
||||
endpoint: serverURL.spec,
|
||||
debug: debug,
|
||||
});
|
||||
},
|
||||
|
||||
init: function(options, mainPushService, serverURL) {
|
||||
console.debug("init()");
|
||||
this._mainPushService = mainPushService;
|
||||
this._serverURI = serverURL;
|
||||
|
||||
prefs.observe("debug", this);
|
||||
Services.obs.addObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage", false);
|
||||
|
||||
return this._configure(serverURL, !!prefs.get("debug")).then(() => {
|
||||
Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:Initialized"
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
console.debug("uninit()");
|
||||
Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:Uninitialized"
|
||||
});
|
||||
|
||||
this._mainPushService = null;
|
||||
Services.obs.removeObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage");
|
||||
prefs.ignore("debug", this);
|
||||
},
|
||||
|
||||
onAlarmFired: function() {
|
||||
// No action required.
|
||||
},
|
||||
|
||||
connect: function(records) {
|
||||
console.debug("connect:", records);
|
||||
// It's possible for the registration or subscriptions backing the
|
||||
// PushService to not be registered with the underlying AndroidPushService.
|
||||
// Expire those that are unrecognized.
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:DumpSubscriptions",
|
||||
})
|
||||
.then(subscriptions => {
|
||||
console.debug("connect:", subscriptions);
|
||||
// subscriptions maps chid => subscription data.
|
||||
return Promise.all(records.map(record => {
|
||||
if (subscriptions.hasOwnProperty(record.keyID)) {
|
||||
console.debug("connect:", "hasOwnProperty", record.keyID);
|
||||
return Promise.resolve();
|
||||
}
|
||||
console.debug("connect:", "!hasOwnProperty", record.keyID);
|
||||
// Subscription is known to PushService.jsm but not to AndroidPushService. Drop it.
|
||||
return this._mainPushService.dropRegistrationAndNotifyApp(record.keyID)
|
||||
.catch(error => {
|
||||
console.error("connect: Error dropping registration", record.keyID, error);
|
||||
});
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
isConnected: function() {
|
||||
return this._mainPushService != null;
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
console.debug("disconnect");
|
||||
},
|
||||
|
||||
register: function(record) {
|
||||
console.debug("register:", record);
|
||||
let ctime = Date.now();
|
||||
let appServerKey = record.appServerKey ?
|
||||
ChromeUtils.base64URLEncode(record.appServerKey, {
|
||||
// The Push server requires padding.
|
||||
pad: true,
|
||||
}) : null;
|
||||
let message = {
|
||||
type: "PushServiceAndroidGCM:SubscribeChannel",
|
||||
appServerKey: appServerKey,
|
||||
}
|
||||
if (record.scope == FXA_PUSH_SCOPE) {
|
||||
message.service = "fxa";
|
||||
}
|
||||
// Caller handles errors.
|
||||
return Messaging.sendRequestForResult(message)
|
||||
.then(data => {
|
||||
console.debug("Got data:", data);
|
||||
return PushCrypto.generateKeys()
|
||||
.then(exportedKeys =>
|
||||
new PushRecordAndroidGCM({
|
||||
// Straight from autopush.
|
||||
channelID: data.channelID,
|
||||
pushEndpoint: data.endpoint,
|
||||
// Common to all PushRecord implementations.
|
||||
scope: record.scope,
|
||||
originAttributes: record.originAttributes,
|
||||
ctime: ctime,
|
||||
systemRecord: record.systemRecord,
|
||||
// Cryptography!
|
||||
p256dhPublicKey: exportedKeys[0],
|
||||
p256dhPrivateKey: exportedKeys[1],
|
||||
authenticationSecret: PushCrypto.generateAuthenticationSecret(),
|
||||
appServerKey: record.appServerKey,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
unregister: function(record) {
|
||||
console.debug("unregister: ", record);
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:UnsubscribeChannel",
|
||||
channelID: record.keyID,
|
||||
});
|
||||
},
|
||||
|
||||
reportDeliveryError: function(messageID, reason) {
|
||||
console.warn("reportDeliveryError: Ignoring message delivery error",
|
||||
messageID, reason);
|
||||
},
|
||||
};
|
||||
|
||||
function PushRecordAndroidGCM(record) {
|
||||
PushRecord.call(this, record);
|
||||
this.channelID = record.channelID;
|
||||
}
|
||||
|
||||
PushRecordAndroidGCM.prototype = Object.create(PushRecord.prototype, {
|
||||
keyID: {
|
||||
get() {
|
||||
return this.channelID;
|
||||
},
|
||||
},
|
||||
});
|
||||
820
dom/push/PushServiceHttp2.jsm
Normal file
820
dom/push/PushServiceHttp2.jsm
Normal file
|
|
@ -0,0 +1,820 @@
|
|||
/* 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 Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
||||
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
const {
|
||||
PushCrypto,
|
||||
concatArray,
|
||||
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PushServiceHttp2"];
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "console", () => {
|
||||
let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
|
||||
return new ConsoleAPI({
|
||||
maxLogLevelPref: "dom.push.loglevel",
|
||||
prefix: "PushServiceHttp2",
|
||||
});
|
||||
});
|
||||
|
||||
const prefs = new Preferences("dom.push.");
|
||||
|
||||
const kPUSHHTTP2DB_DB_NAME = "pushHttp2";
|
||||
const kPUSHHTTP2DB_DB_VERSION = 5; // Change this if the IndexedDB format changes
|
||||
const kPUSHHTTP2DB_STORE_NAME = "pushHttp2";
|
||||
|
||||
/**
|
||||
* A proxy between the PushService and connections listening for incoming push
|
||||
* messages. The PushService can silence messages from the connections by
|
||||
* setting PushSubscriptionListener._pushService to null. This is required
|
||||
* because it can happen that there is an outstanding push message that will
|
||||
* be send on OnStopRequest but the PushService may not be interested in these.
|
||||
* It's easier to stop listening than to have checks at specific points.
|
||||
*/
|
||||
var PushSubscriptionListener = function(pushService, uri) {
|
||||
console.debug("PushSubscriptionListener()");
|
||||
this._pushService = pushService;
|
||||
this.uri = uri;
|
||||
};
|
||||
|
||||
PushSubscriptionListener.prototype = {
|
||||
|
||||
QueryInterface: function (aIID) {
|
||||
if (aIID.equals(Ci.nsIHttpPushListener) ||
|
||||
aIID.equals(Ci.nsIStreamListener)) {
|
||||
return this;
|
||||
}
|
||||
throw Components.results.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
getInterface: function(aIID) {
|
||||
return this.QueryInterface(aIID);
|
||||
},
|
||||
|
||||
onStartRequest: function(aRequest, aContext) {
|
||||
console.debug("PushSubscriptionListener: onStartRequest()");
|
||||
// We do not do anything here.
|
||||
},
|
||||
|
||||
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
|
||||
console.debug("PushSubscriptionListener: onDataAvailable()");
|
||||
// Nobody should send data, but just to be sure, otherwise necko will
|
||||
// complain.
|
||||
if (aCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
|
||||
.createInstance(Ci.nsIScriptableInputStream);
|
||||
|
||||
inputStream.init(aStream);
|
||||
var data = inputStream.read(aCount);
|
||||
},
|
||||
|
||||
onStopRequest: function(aRequest, aContext, aStatusCode) {
|
||||
console.debug("PushSubscriptionListener: onStopRequest()");
|
||||
if (!this._pushService) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pushService.connOnStop(aRequest,
|
||||
Components.isSuccessCode(aStatusCode),
|
||||
this.uri);
|
||||
},
|
||||
|
||||
onPush: function(associatedChannel, pushChannel) {
|
||||
console.debug("PushSubscriptionListener: onPush()");
|
||||
var pushChannelListener = new PushChannelListener(this);
|
||||
pushChannel.asyncOpen2(pushChannelListener);
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
this._pushService = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The listener for pushed messages. The message data is collected in
|
||||
* OnDataAvailable and send to the app in OnStopRequest.
|
||||
*/
|
||||
var PushChannelListener = function(pushSubscriptionListener) {
|
||||
console.debug("PushChannelListener()");
|
||||
this._mainListener = pushSubscriptionListener;
|
||||
this._message = [];
|
||||
this._ackUri = null;
|
||||
};
|
||||
|
||||
PushChannelListener.prototype = {
|
||||
|
||||
onStartRequest: function(aRequest, aContext) {
|
||||
this._ackUri = aRequest.URI.spec;
|
||||
},
|
||||
|
||||
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
|
||||
console.debug("PushChannelListener: onDataAvailable()");
|
||||
|
||||
if (aCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let inputStream = Cc["@mozilla.org/binaryinputstream;1"]
|
||||
.createInstance(Ci.nsIBinaryInputStream);
|
||||
|
||||
inputStream.setInputStream(aStream);
|
||||
let chunk = new ArrayBuffer(aCount);
|
||||
inputStream.readArrayBuffer(aCount, chunk);
|
||||
this._message.push(chunk);
|
||||
},
|
||||
|
||||
onStopRequest: function(aRequest, aContext, aStatusCode) {
|
||||
console.debug("PushChannelListener: onStopRequest()", "status code",
|
||||
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 msg = concatArray(this._message);
|
||||
|
||||
this._mainListener._pushService._pushChannelOnStop(this._mainListener.uri,
|
||||
this._ackUri,
|
||||
headers,
|
||||
msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function getHeaderField(aRequest, name) {
|
||||
try {
|
||||
return aRequest.getRequestHeader(name);
|
||||
} catch(e) {
|
||||
// getRequestHeader can throw.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var PushServiceDelete = function(resolve, reject) {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
};
|
||||
|
||||
PushServiceDelete.prototype = {
|
||||
|
||||
onStartRequest: function(aRequest, aContext) {},
|
||||
|
||||
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
|
||||
// Nobody should send data, but just to be sure, otherwise necko will
|
||||
// complain.
|
||||
if (aCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
|
||||
.createInstance(Ci.nsIScriptableInputStream);
|
||||
|
||||
inputStream.init(aStream);
|
||||
var data = inputStream.read(aCount);
|
||||
},
|
||||
|
||||
onStopRequest: function(aRequest, aContext, aStatusCode) {
|
||||
|
||||
if (Components.isSuccessCode(aStatusCode)) {
|
||||
this._resolve();
|
||||
} else {
|
||||
this._reject(new Error("Error removing subscription: " + aStatusCode));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var SubscriptionListener = function(aSubInfo, aResolve, aReject,
|
||||
aServerURI, aPushServiceHttp2) {
|
||||
console.debug("SubscriptionListener()");
|
||||
this._subInfo = aSubInfo;
|
||||
this._resolve = aResolve;
|
||||
this._reject = aReject;
|
||||
this._data = '';
|
||||
this._serverURI = aServerURI;
|
||||
this._service = aPushServiceHttp2;
|
||||
this._ctime = Date.now();
|
||||
this._retryTimeoutID = null;
|
||||
};
|
||||
|
||||
SubscriptionListener.prototype = {
|
||||
|
||||
onStartRequest: function(aRequest, aContext) {},
|
||||
|
||||
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
|
||||
console.debug("SubscriptionListener: onDataAvailable()");
|
||||
|
||||
// We do not expect any data, but necko will complain if we do not consume
|
||||
// it.
|
||||
if (aCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
|
||||
.createInstance(Ci.nsIScriptableInputStream);
|
||||
|
||||
inputStream.init(aStream);
|
||||
this._data.concat(inputStream.read(aCount));
|
||||
},
|
||||
|
||||
onStopRequest: function(aRequest, aContext, aStatus) {
|
||||
console.debug("SubscriptionListener: onStopRequest()");
|
||||
|
||||
// Check if pushService is still active.
|
||||
if (!this._service.hasmainPushService()) {
|
||||
this._reject(new Error("Push service unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Components.isSuccessCode(aStatus)) {
|
||||
this._reject(new Error("Error listening for messages: " + aStatus));
|
||||
return;
|
||||
}
|
||||
|
||||
var statusCode = aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus;
|
||||
|
||||
if (Math.floor(statusCode / 100) == 5) {
|
||||
if (this._subInfo.retries < prefs.get("http2.maxRetries")) {
|
||||
this._subInfo.retries++;
|
||||
var retryAfter = retryAfterParser(aRequest);
|
||||
this._retryTimeoutID = setTimeout(_ =>
|
||||
{
|
||||
this._reject(
|
||||
{
|
||||
retry: true,
|
||||
subInfo: this._subInfo
|
||||
});
|
||||
this._service.removeListenerPendingRetry(this);
|
||||
this._retryTimeoutID = null;
|
||||
}, retryAfter);
|
||||
this._service.addListenerPendingRetry(this);
|
||||
} else {
|
||||
this._reject(new Error("Unexpected server response: " + statusCode));
|
||||
}
|
||||
return;
|
||||
} else if (statusCode != 201) {
|
||||
this._reject(new Error("Unexpected server response: " + statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
var subscriptionUri;
|
||||
try {
|
||||
subscriptionUri = aRequest.getResponseHeader("location");
|
||||
} catch (err) {
|
||||
this._reject(new Error("Missing Location header"));
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug("onStopRequest: subscriptionUri", subscriptionUri);
|
||||
|
||||
var linkList;
|
||||
try {
|
||||
linkList = aRequest.getResponseHeader("link");
|
||||
} catch (err) {
|
||||
this._reject(new Error("Missing Link header"));
|
||||
return;
|
||||
}
|
||||
|
||||
var linkParserResult;
|
||||
try {
|
||||
linkParserResult = linkParser(linkList, this._serverURI);
|
||||
} catch (e) {
|
||||
this._reject(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subscriptionUri) {
|
||||
this._reject(new Error("Invalid Location header"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let uriTry = Services.io.newURI(subscriptionUri, null, null);
|
||||
} catch (e) {
|
||||
console.error("onStopRequest: Invalid subscription URI",
|
||||
subscriptionUri);
|
||||
this._reject(new Error("Invalid subscription endpoint: " +
|
||||
subscriptionUri));
|
||||
return;
|
||||
}
|
||||
|
||||
let reply = new PushRecordHttp2({
|
||||
subscriptionUri: subscriptionUri,
|
||||
pushEndpoint: linkParserResult.pushEndpoint,
|
||||
pushReceiptEndpoint: linkParserResult.pushReceiptEndpoint,
|
||||
scope: this._subInfo.record.scope,
|
||||
originAttributes: this._subInfo.record.originAttributes,
|
||||
systemRecord: this._subInfo.record.systemRecord,
|
||||
appServerKey: this._subInfo.record.appServerKey,
|
||||
ctime: Date.now(),
|
||||
});
|
||||
|
||||
Services.telemetry.getHistogramById("PUSH_API_SUBSCRIBE_HTTP2_TIME").add(Date.now() - this._ctime);
|
||||
this._resolve(reply);
|
||||
},
|
||||
|
||||
abortRetry: function() {
|
||||
if (this._retryTimeoutID != null) {
|
||||
clearTimeout(this._retryTimeoutID);
|
||||
this._retryTimeoutID = null;
|
||||
} else {
|
||||
console.debug("SubscriptionListener.abortRetry: aborting non-existent retry?");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function retryAfterParser(aRequest) {
|
||||
var retryAfter = 0;
|
||||
try {
|
||||
var retryField = aRequest.getResponseHeader("retry-after");
|
||||
if (isNaN(retryField)) {
|
||||
retryAfter = Date.parse(retryField) - (new Date().getTime());
|
||||
} else {
|
||||
retryAfter = parseInt(retryField, 10) * 1000;
|
||||
}
|
||||
retryAfter = (retryAfter > 0) ? retryAfter : 0;
|
||||
} catch(e) {}
|
||||
|
||||
return retryAfter;
|
||||
}
|
||||
|
||||
function linkParser(linkHeader, serverURI) {
|
||||
|
||||
var linkList = linkHeader.split(',');
|
||||
if ((linkList.length < 1)) {
|
||||
throw new Error("Invalid Link header");
|
||||
}
|
||||
|
||||
var pushEndpoint;
|
||||
var pushReceiptEndpoint;
|
||||
|
||||
linkList.forEach(link => {
|
||||
var linkElems = link.split(';');
|
||||
|
||||
if (linkElems.length == 2) {
|
||||
if (linkElems[1].trim() === 'rel="urn:ietf:params:push"') {
|
||||
pushEndpoint = linkElems[0].substring(linkElems[0].indexOf('<') + 1,
|
||||
linkElems[0].indexOf('>'));
|
||||
|
||||
} else if (linkElems[1].trim() === 'rel="urn:ietf:params:push:receipt"') {
|
||||
pushReceiptEndpoint = linkElems[0].substring(linkElems[0].indexOf('<') + 1,
|
||||
linkElems[0].indexOf('>'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.debug("linkParser: pushEndpoint", pushEndpoint);
|
||||
console.debug("linkParser: pushReceiptEndpoint", pushReceiptEndpoint);
|
||||
// Missing pushReceiptEndpoint is allowed.
|
||||
if (!pushEndpoint) {
|
||||
throw new Error("Missing push endpoint");
|
||||
}
|
||||
|
||||
var pushURI = Services.io.newURI(pushEndpoint, null, serverURI);
|
||||
var pushReceiptURI;
|
||||
if (pushReceiptEndpoint) {
|
||||
pushReceiptURI = Services.io.newURI(pushReceiptEndpoint, null,
|
||||
serverURI);
|
||||
}
|
||||
|
||||
return {
|
||||
pushEndpoint: pushURI.spec,
|
||||
pushReceiptEndpoint: (pushReceiptURI) ? pushReceiptURI.spec : "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation of the WebPush.
|
||||
*/
|
||||
this.PushServiceHttp2 = {
|
||||
_mainPushService: null,
|
||||
_serverURI: null,
|
||||
|
||||
// Keep information about all connections, e.g. the channel, listener...
|
||||
_conns: {},
|
||||
_started: false,
|
||||
|
||||
// Set of SubscriptionListeners that are pending a subscription retry attempt.
|
||||
_listenersPendingRetry: new Set(),
|
||||
|
||||
newPushDB: function() {
|
||||
return new PushDB(kPUSHHTTP2DB_DB_NAME,
|
||||
kPUSHHTTP2DB_DB_VERSION,
|
||||
kPUSHHTTP2DB_STORE_NAME,
|
||||
"subscriptionUri",
|
||||
PushRecordHttp2);
|
||||
},
|
||||
|
||||
hasmainPushService: function() {
|
||||
return this._mainPushService !== null;
|
||||
},
|
||||
|
||||
validServerURI: function(serverURI) {
|
||||
if (serverURI.scheme == "http") {
|
||||
return !!prefs.get("testing.allowInsecureServerURL");
|
||||
}
|
||||
return serverURI.scheme == "https";
|
||||
},
|
||||
|
||||
connect: function(subscriptions) {
|
||||
this.startConnections(subscriptions);
|
||||
},
|
||||
|
||||
isConnected: function() {
|
||||
return this._mainPushService != null;
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
this._shutdownConnections(false);
|
||||
},
|
||||
|
||||
_makeChannel: function(aUri) {
|
||||
var chan = NetUtil.newChannel({uri: aUri, loadUsingSystemPrincipal: true})
|
||||
.QueryInterface(Ci.nsIHttpChannel);
|
||||
|
||||
var loadGroup = Cc["@mozilla.org/network/load-group;1"]
|
||||
.createInstance(Ci.nsILoadGroup);
|
||||
chan.loadGroup = loadGroup;
|
||||
return chan;
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribe new resource.
|
||||
*/
|
||||
register: function(aRecord) {
|
||||
console.debug("subscribeResource()");
|
||||
|
||||
return this._subscribeResourceInternal({
|
||||
record: aRecord,
|
||||
retries: 0
|
||||
})
|
||||
.then(result =>
|
||||
PushCrypto.generateKeys()
|
||||
.then(([publicKey, privateKey]) => {
|
||||
result.p256dhPublicKey = publicKey;
|
||||
result.p256dhPrivateKey = privateKey;
|
||||
result.authenticationSecret = PushCrypto.generateAuthenticationSecret();
|
||||
this._conns[result.subscriptionUri] = {
|
||||
channel: null,
|
||||
listener: null,
|
||||
countUnableToConnect: 0,
|
||||
lastStartListening: 0,
|
||||
retryTimerID: 0,
|
||||
};
|
||||
this._listenForMsgs(result.subscriptionUri);
|
||||
return result;
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_subscribeResourceInternal: function(aSubInfo) {
|
||||
console.debug("subscribeResourceInternal()");
|
||||
|
||||
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";
|
||||
chan.asyncOpen2(listener);
|
||||
})
|
||||
.catch(err => {
|
||||
if ("retry" in err) {
|
||||
return this._subscribeResourceInternal(err.subInfo);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
_deleteResource: function(aUri) {
|
||||
|
||||
return new Promise((resolve,reject) => {
|
||||
var chan = this._makeChannel(aUri);
|
||||
chan.requestMethod = "DELETE";
|
||||
chan.asyncOpen2(new PushServiceDelete(resolve, reject));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Unsubscribe the resource with a subscription uri aSubscriptionUri.
|
||||
* We can't do anything about it if it fails, so we don't listen for response.
|
||||
*/
|
||||
_unsubscribeResource: function(aSubscriptionUri) {
|
||||
console.debug("unsubscribeResource()");
|
||||
|
||||
return this._deleteResource(aSubscriptionUri);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start listening for messages.
|
||||
*/
|
||||
_listenForMsgs: function(aSubscriptionUri) {
|
||||
console.debug("listenForMsgs()", aSubscriptionUri);
|
||||
if (!this._conns[aSubscriptionUri]) {
|
||||
console.warn("listenForMsgs: We do not have this subscription",
|
||||
aSubscriptionUri);
|
||||
return;
|
||||
}
|
||||
|
||||
var chan = this._makeChannel(aSubscriptionUri);
|
||||
var conn = {};
|
||||
conn.channel = chan;
|
||||
var listener = new PushSubscriptionListener(this, aSubscriptionUri);
|
||||
conn.listener = listener;
|
||||
|
||||
chan.notificationCallbacks = listener;
|
||||
|
||||
try {
|
||||
chan.asyncOpen2(listener);
|
||||
} catch (e) {
|
||||
console.error("listenForMsgs: Error connecting to push server.",
|
||||
"asyncOpen2 failed", e);
|
||||
conn.listener.disconnect();
|
||||
chan.cancel(Cr.NS_ERROR_ABORT);
|
||||
this._retryAfterBackoff(aSubscriptionUri, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
this._conns[aSubscriptionUri].lastStartListening = Date.now();
|
||||
this._conns[aSubscriptionUri].channel = conn.channel;
|
||||
this._conns[aSubscriptionUri].listener = conn.listener;
|
||||
|
||||
},
|
||||
|
||||
_ackMsgRecv: function(aAckUri) {
|
||||
console.debug("ackMsgRecv()", aAckUri);
|
||||
return this._deleteResource(aAckUri);
|
||||
},
|
||||
|
||||
init: function(aOptions, aMainPushService, aServerURL) {
|
||||
console.debug("init()");
|
||||
this._mainPushService = aMainPushService;
|
||||
this._serverURI = aServerURL;
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
_retryAfterBackoff: function(aSubscriptionUri, retryAfter) {
|
||||
console.debug("retryAfterBackoff()");
|
||||
|
||||
var resetRetryCount = prefs.get("http2.reset_retry_count_after_ms");
|
||||
// If it was running for some time, reset retry counter.
|
||||
if ((Date.now() - this._conns[aSubscriptionUri].lastStartListening) >
|
||||
resetRetryCount) {
|
||||
this._conns[aSubscriptionUri].countUnableToConnect = 0;
|
||||
}
|
||||
|
||||
let maxRetries = prefs.get("http2.maxRetries");
|
||||
if (this._conns[aSubscriptionUri].countUnableToConnect >= maxRetries) {
|
||||
this._shutdownSubscription(aSubscriptionUri);
|
||||
this._resubscribe(aSubscriptionUri);
|
||||
return;
|
||||
}
|
||||
|
||||
if (retryAfter !== -1) {
|
||||
// This is a 5xx response.
|
||||
this._conns[aSubscriptionUri].countUnableToConnect++;
|
||||
this._conns[aSubscriptionUri].retryTimerID =
|
||||
setTimeout(_ => this._listenForMsgs(aSubscriptionUri), retryAfter);
|
||||
return;
|
||||
}
|
||||
|
||||
retryAfter = prefs.get("http2.retryInterval") *
|
||||
Math.pow(2, this._conns[aSubscriptionUri].countUnableToConnect);
|
||||
|
||||
retryAfter = retryAfter * (0.8 + Math.random() * 0.4); // add +/-20%.
|
||||
|
||||
this._conns[aSubscriptionUri].countUnableToConnect++;
|
||||
this._conns[aSubscriptionUri].retryTimerID =
|
||||
setTimeout(_ => this._listenForMsgs(aSubscriptionUri), retryAfter);
|
||||
|
||||
console.debug("retryAfterBackoff: Retry in", retryAfter);
|
||||
},
|
||||
|
||||
// Close connections.
|
||||
_shutdownConnections: function(deleteInfo) {
|
||||
console.debug("shutdownConnections()");
|
||||
|
||||
for (let subscriptionUri in this._conns) {
|
||||
if (this._conns[subscriptionUri]) {
|
||||
if (this._conns[subscriptionUri].listener) {
|
||||
this._conns[subscriptionUri].listener._pushService = null;
|
||||
}
|
||||
|
||||
if (this._conns[subscriptionUri].channel) {
|
||||
try {
|
||||
this._conns[subscriptionUri].channel.cancel(Cr.NS_ERROR_ABORT);
|
||||
} catch (e) {}
|
||||
}
|
||||
this._conns[subscriptionUri].listener = null;
|
||||
this._conns[subscriptionUri].channel = null;
|
||||
|
||||
if (this._conns[subscriptionUri].retryTimerID > 0) {
|
||||
clearTimeout(this._conns[subscriptionUri].retryTimerID);
|
||||
}
|
||||
|
||||
if (deleteInfo) {
|
||||
delete this._conns[subscriptionUri];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Start listening if subscriptions present.
|
||||
startConnections: function(aSubscriptions) {
|
||||
console.debug("startConnections()", aSubscriptions.length);
|
||||
|
||||
for (let i = 0; i < aSubscriptions.length; i++) {
|
||||
let record = aSubscriptions[i];
|
||||
this._mainPushService.ensureCrypto(record).then(record => {
|
||||
this._startSingleConnection(record);
|
||||
}, error => {
|
||||
console.error("startConnections: Error updating record",
|
||||
record.keyID, error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_startSingleConnection: function(record) {
|
||||
console.debug("_startSingleConnection()");
|
||||
if (typeof this._conns[record.subscriptionUri] != "object") {
|
||||
this._conns[record.subscriptionUri] = {channel: null,
|
||||
listener: null,
|
||||
countUnableToConnect: 0,
|
||||
retryTimerID: 0};
|
||||
}
|
||||
if (!this._conns[record.subscriptionUri].conn) {
|
||||
this._listenForMsgs(record.subscriptionUri);
|
||||
}
|
||||
},
|
||||
|
||||
// Close connection and notify apps that subscription are gone.
|
||||
_shutdownSubscription: function(aSubscriptionUri) {
|
||||
console.debug("shutdownSubscriptions()");
|
||||
|
||||
if (typeof this._conns[aSubscriptionUri] == "object") {
|
||||
if (this._conns[aSubscriptionUri].listener) {
|
||||
this._conns[aSubscriptionUri].listener._pushService = null;
|
||||
}
|
||||
|
||||
if (this._conns[aSubscriptionUri].channel) {
|
||||
try {
|
||||
this._conns[aSubscriptionUri].channel.cancel(Cr.NS_ERROR_ABORT);
|
||||
} catch (e) {}
|
||||
}
|
||||
delete this._conns[aSubscriptionUri];
|
||||
}
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
console.debug("uninit()");
|
||||
this._abortPendingSubscriptionRetries();
|
||||
this._shutdownConnections(true);
|
||||
this._mainPushService = null;
|
||||
},
|
||||
|
||||
_abortPendingSubscriptionRetries: function() {
|
||||
this._listenersPendingRetry.forEach((listener) => listener.abortRetry());
|
||||
this._listenersPendingRetry.clear();
|
||||
},
|
||||
|
||||
unregister: function(aRecord) {
|
||||
this._shutdownSubscription(aRecord.subscriptionUri);
|
||||
return this._unsubscribeResource(aRecord.subscriptionUri);
|
||||
},
|
||||
|
||||
reportDeliveryError: function(messageID, reason) {
|
||||
console.warn("reportDeliveryError: Ignoring message delivery error",
|
||||
messageID, reason);
|
||||
},
|
||||
|
||||
/** Push server has deleted subscription.
|
||||
* Re-subscribe - if it succeeds send update db record and send
|
||||
* pushsubscriptionchange,
|
||||
* - on error delete record and send pushsubscriptionchange
|
||||
* TODO: maybe pushsubscriptionerror will be included.
|
||||
*/
|
||||
_resubscribe: function(aSubscriptionUri) {
|
||||
this._mainPushService.getByKeyID(aSubscriptionUri)
|
||||
.then(record => this.register(record)
|
||||
.then(recordNew => {
|
||||
if (this._mainPushService) {
|
||||
this._mainPushService
|
||||
.updateRegistrationAndNotifyApp(aSubscriptionUri, recordNew)
|
||||
.catch(Cu.reportError);
|
||||
}
|
||||
}, error => {
|
||||
if (this._mainPushService) {
|
||||
this._mainPushService
|
||||
.dropRegistrationAndNotifyApp(aSubscriptionUri)
|
||||
.catch(Cu.reportError);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
connOnStop: function(aRequest, aSuccess,
|
||||
aSubscriptionUri) {
|
||||
console.debug("connOnStop() succeeded", aSuccess);
|
||||
|
||||
var conn = this._conns[aSubscriptionUri];
|
||||
if (!conn) {
|
||||
// there is no connection description that means that we closed
|
||||
// connection, so do nothing. But we should have already deleted
|
||||
// the listener.
|
||||
return;
|
||||
}
|
||||
|
||||
conn.channel = null;
|
||||
conn.listener = null;
|
||||
|
||||
if (!aSuccess) {
|
||||
this._retryAfterBackoff(aSubscriptionUri, -1);
|
||||
|
||||
} else if (Math.floor(aRequest.responseStatus / 100) == 5) {
|
||||
var retryAfter = retryAfterParser(aRequest);
|
||||
this._retryAfterBackoff(aSubscriptionUri, retryAfter);
|
||||
|
||||
} else if (Math.floor(aRequest.responseStatus / 100) == 4) {
|
||||
this._shutdownSubscription(aSubscriptionUri);
|
||||
this._resubscribe(aSubscriptionUri);
|
||||
} else if (Math.floor(aRequest.responseStatus / 100) == 2) { // This should be 204
|
||||
setTimeout(_ => this._listenForMsgs(aSubscriptionUri), 0);
|
||||
} else {
|
||||
this._retryAfterBackoff(aSubscriptionUri, -1);
|
||||
}
|
||||
},
|
||||
|
||||
addListenerPendingRetry: function(aListener) {
|
||||
this._listenersPendingRetry.add(aListener);
|
||||
},
|
||||
|
||||
removeListenerPendingRetry: function(aListener) {
|
||||
if (!this._listenersPendingRetry.remove(aListener)) {
|
||||
console.debug("removeListenerPendingRetry: listener not in list?");
|
||||
}
|
||||
},
|
||||
|
||||
_pushChannelOnStop: function(aUri, aAckUri, aHeaders, aMessage) {
|
||||
console.debug("pushChannelOnStop()");
|
||||
|
||||
this._mainPushService.receivedPushMessage(
|
||||
aUri, "", aHeaders, aMessage, record => {
|
||||
// Always update the stored record.
|
||||
return record;
|
||||
}
|
||||
)
|
||||
.then(_ => this._ackMsgRecv(aAckUri))
|
||||
.catch(err => {
|
||||
console.error("pushChannelOnStop: Error receiving message",
|
||||
err);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function PushRecordHttp2(record) {
|
||||
PushRecord.call(this, record);
|
||||
this.subscriptionUri = record.subscriptionUri;
|
||||
this.pushReceiptEndpoint = record.pushReceiptEndpoint;
|
||||
}
|
||||
|
||||
PushRecordHttp2.prototype = Object.create(PushRecord.prototype, {
|
||||
keyID: {
|
||||
get() {
|
||||
return this.subscriptionUri;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
PushRecordHttp2.prototype.toSubscription = function() {
|
||||
let subscription = PushRecord.prototype.toSubscription.call(this);
|
||||
subscription.pushReceiptEndpoint = this.pushReceiptEndpoint;
|
||||
return subscription;
|
||||
};
|
||||
1145
dom/push/PushServiceWebSocket.jsm
Normal file
1145
dom/push/PushServiceWebSocket.jsm
Normal file
File diff suppressed because it is too large
Load diff
398
dom/push/PushSubscription.cpp
Normal file
398
dom/push/PushSubscription.cpp
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
/* 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/. */
|
||||
|
||||
#include "mozilla/dom/PushSubscription.h"
|
||||
|
||||
#include "nsIPushService.h"
|
||||
#include "nsIScriptObjectPrincipal.h"
|
||||
|
||||
#include "mozilla/Base64.h"
|
||||
#include "mozilla/Unused.h"
|
||||
|
||||
#include "mozilla/dom/Promise.h"
|
||||
#include "mozilla/dom/PromiseWorkerProxy.h"
|
||||
#include "mozilla/dom/PushSubscriptionOptions.h"
|
||||
#include "mozilla/dom/PushUtil.h"
|
||||
#include "mozilla/dom/WorkerPrivate.h"
|
||||
#include "mozilla/dom/WorkerScope.h"
|
||||
#include "mozilla/dom/workers/Workers.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
using namespace workers;
|
||||
|
||||
namespace {
|
||||
|
||||
class UnsubscribeResultCallback final : public nsIUnsubscribeResultCallback
|
||||
{
|
||||
public:
|
||||
NS_DECL_ISUPPORTS
|
||||
|
||||
explicit UnsubscribeResultCallback(Promise* aPromise)
|
||||
: mPromise(aPromise)
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
}
|
||||
|
||||
NS_IMETHOD
|
||||
OnUnsubscribe(nsresult aStatus, bool aSuccess) override
|
||||
{
|
||||
if (NS_SUCCEEDED(aStatus)) {
|
||||
mPromise->MaybeResolve(aSuccess);
|
||||
} else {
|
||||
mPromise->MaybeReject(NS_ERROR_DOM_PUSH_SERVICE_UNREACHABLE);
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
~UnsubscribeResultCallback()
|
||||
{}
|
||||
|
||||
RefPtr<Promise> mPromise;
|
||||
};
|
||||
|
||||
NS_IMPL_ISUPPORTS(UnsubscribeResultCallback, nsIUnsubscribeResultCallback)
|
||||
|
||||
class UnsubscribeResultRunnable final : public WorkerRunnable
|
||||
{
|
||||
public:
|
||||
UnsubscribeResultRunnable(WorkerPrivate* aWorkerPrivate,
|
||||
already_AddRefed<PromiseWorkerProxy>&& aProxy,
|
||||
nsresult aStatus,
|
||||
bool aSuccess)
|
||||
: WorkerRunnable(aWorkerPrivate)
|
||||
, mProxy(Move(aProxy))
|
||||
, mStatus(aStatus)
|
||||
, mSuccess(aSuccess)
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
}
|
||||
|
||||
bool
|
||||
WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
|
||||
{
|
||||
MOZ_ASSERT(aWorkerPrivate);
|
||||
aWorkerPrivate->AssertIsOnWorkerThread();
|
||||
|
||||
RefPtr<Promise> promise = mProxy->WorkerPromise();
|
||||
if (NS_SUCCEEDED(mStatus)) {
|
||||
promise->MaybeResolve(mSuccess);
|
||||
} else {
|
||||
promise->MaybeReject(NS_ERROR_DOM_PUSH_SERVICE_UNREACHABLE);
|
||||
}
|
||||
|
||||
mProxy->CleanUp();
|
||||
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
~UnsubscribeResultRunnable()
|
||||
{}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> mProxy;
|
||||
nsresult mStatus;
|
||||
bool mSuccess;
|
||||
};
|
||||
|
||||
class WorkerUnsubscribeResultCallback final : public nsIUnsubscribeResultCallback
|
||||
{
|
||||
public:
|
||||
NS_DECL_ISUPPORTS
|
||||
|
||||
explicit WorkerUnsubscribeResultCallback(PromiseWorkerProxy* aProxy)
|
||||
: mProxy(aProxy)
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
}
|
||||
|
||||
NS_IMETHOD
|
||||
OnUnsubscribe(nsresult aStatus, bool aSuccess) override
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
MOZ_ASSERT(mProxy, "OnUnsubscribe() called twice?");
|
||||
|
||||
MutexAutoLock lock(mProxy->Lock());
|
||||
if (mProxy->CleanedUp()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
WorkerPrivate* worker = mProxy->GetWorkerPrivate();
|
||||
RefPtr<UnsubscribeResultRunnable> r =
|
||||
new UnsubscribeResultRunnable(worker, mProxy.forget(), aStatus, aSuccess);
|
||||
MOZ_ALWAYS_TRUE(r->Dispatch());
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
~WorkerUnsubscribeResultCallback()
|
||||
{
|
||||
}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> mProxy;
|
||||
};
|
||||
|
||||
NS_IMPL_ISUPPORTS(WorkerUnsubscribeResultCallback, nsIUnsubscribeResultCallback)
|
||||
|
||||
class UnsubscribeRunnable final : public Runnable
|
||||
{
|
||||
public:
|
||||
UnsubscribeRunnable(PromiseWorkerProxy* aProxy,
|
||||
const nsAString& aScope)
|
||||
: mProxy(aProxy)
|
||||
, mScope(aScope)
|
||||
{
|
||||
MOZ_ASSERT(aProxy);
|
||||
MOZ_ASSERT(!aScope.IsEmpty());
|
||||
}
|
||||
|
||||
NS_IMETHOD
|
||||
Run() override
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
|
||||
nsCOMPtr<nsIPrincipal> principal;
|
||||
|
||||
{
|
||||
MutexAutoLock lock(mProxy->Lock());
|
||||
if (mProxy->CleanedUp()) {
|
||||
return NS_OK;
|
||||
}
|
||||
principal = mProxy->GetWorkerPrivate()->GetPrincipal();
|
||||
}
|
||||
|
||||
MOZ_ASSERT(principal);
|
||||
|
||||
RefPtr<WorkerUnsubscribeResultCallback> callback =
|
||||
new WorkerUnsubscribeResultCallback(mProxy);
|
||||
|
||||
nsCOMPtr<nsIPushService> service =
|
||||
do_GetService("@mozilla.org/push/Service;1");
|
||||
if (NS_WARN_IF(!service)) {
|
||||
callback->OnUnsubscribe(NS_ERROR_FAILURE, false);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (NS_WARN_IF(NS_FAILED(service->Unsubscribe(mScope, principal, callback)))) {
|
||||
callback->OnUnsubscribe(NS_ERROR_FAILURE, false);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
~UnsubscribeRunnable()
|
||||
{}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> mProxy;
|
||||
nsString mScope;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
PushSubscription::PushSubscription(nsIGlobalObject* aGlobal,
|
||||
const nsAString& aEndpoint,
|
||||
const nsAString& aScope,
|
||||
nsTArray<uint8_t>&& aRawP256dhKey,
|
||||
nsTArray<uint8_t>&& aAuthSecret,
|
||||
nsTArray<uint8_t>&& aAppServerKey)
|
||||
: mEndpoint(aEndpoint)
|
||||
, mScope(aScope)
|
||||
, mRawP256dhKey(Move(aRawP256dhKey))
|
||||
, mAuthSecret(Move(aAuthSecret))
|
||||
{
|
||||
if (NS_IsMainThread()) {
|
||||
mGlobal = aGlobal;
|
||||
} else {
|
||||
#ifdef DEBUG
|
||||
// There's only one global on a worker, so we don't need to pass a global
|
||||
// object to the constructor.
|
||||
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
|
||||
MOZ_ASSERT(worker);
|
||||
worker->AssertIsOnWorkerThread();
|
||||
#endif
|
||||
}
|
||||
mOptions = new PushSubscriptionOptions(mGlobal, Move(aAppServerKey));
|
||||
}
|
||||
|
||||
PushSubscription::~PushSubscription()
|
||||
{}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushSubscription, mGlobal, mOptions)
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushSubscription)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushSubscription)
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushSubscription)
|
||||
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
||||
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
JSObject*
|
||||
PushSubscription::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return PushSubscriptionBinding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
// static
|
||||
already_AddRefed<PushSubscription>
|
||||
PushSubscription::Constructor(GlobalObject& aGlobal,
|
||||
const PushSubscriptionInit& aInitDict,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
|
||||
nsTArray<uint8_t> rawKey;
|
||||
if (aInitDict.mP256dhKey.WasPassed() &&
|
||||
!aInitDict.mP256dhKey.Value().IsNull() &&
|
||||
!PushUtil::CopyArrayBufferToArray(aInitDict.mP256dhKey.Value().Value(),
|
||||
rawKey)) {
|
||||
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> authSecret;
|
||||
if (aInitDict.mAuthSecret.WasPassed() &&
|
||||
!aInitDict.mAuthSecret.Value().IsNull() &&
|
||||
!PushUtil::CopyArrayBufferToArray(aInitDict.mAuthSecret.Value().Value(),
|
||||
authSecret)) {
|
||||
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> appServerKey;
|
||||
if (aInitDict.mAppServerKey.WasPassed() &&
|
||||
!aInitDict.mAppServerKey.Value().IsNull()) {
|
||||
const OwningArrayBufferViewOrArrayBuffer& bufferSource =
|
||||
aInitDict.mAppServerKey.Value().Value();
|
||||
if (!PushUtil::CopyBufferSourceToArray(bufferSource, appServerKey)) {
|
||||
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
RefPtr<PushSubscription> sub = new PushSubscription(global,
|
||||
aInitDict.mEndpoint,
|
||||
aInitDict.mScope,
|
||||
Move(rawKey),
|
||||
Move(authSecret),
|
||||
Move(appServerKey));
|
||||
|
||||
return sub.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PushSubscription::Unsubscribe(ErrorResult& aRv)
|
||||
{
|
||||
if (!NS_IsMainThread()) {
|
||||
RefPtr<Promise> p = UnsubscribeFromWorker(aRv);
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
MOZ_ASSERT(mGlobal);
|
||||
|
||||
nsCOMPtr<nsIPushService> service =
|
||||
do_GetService("@mozilla.org/push/Service;1");
|
||||
if (NS_WARN_IF(!service)) {
|
||||
aRv.Throw(NS_ERROR_FAILURE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(mGlobal);
|
||||
if (!sop) {
|
||||
aRv.Throw(NS_ERROR_FAILURE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<Promise> p = Promise::Create(mGlobal, aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<UnsubscribeResultCallback> callback =
|
||||
new UnsubscribeResultCallback(p);
|
||||
Unused << NS_WARN_IF(NS_FAILED(
|
||||
service->Unsubscribe(mScope, sop->GetPrincipal(), callback)));
|
||||
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
void
|
||||
PushSubscription::GetKey(JSContext* aCx,
|
||||
PushEncryptionKeyName aType,
|
||||
JS::MutableHandle<JSObject*> aKey,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (aType == PushEncryptionKeyName::P256dh) {
|
||||
PushUtil::CopyArrayToArrayBuffer(aCx, mRawP256dhKey, aKey, aRv);
|
||||
} else if (aType == PushEncryptionKeyName::Auth) {
|
||||
PushUtil::CopyArrayToArrayBuffer(aCx, mAuthSecret, aKey, aRv);
|
||||
} else {
|
||||
aKey.set(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PushSubscription::ToJSON(PushSubscriptionJSON& aJSON, ErrorResult& aRv)
|
||||
{
|
||||
aJSON.mEndpoint.Construct();
|
||||
aJSON.mEndpoint.Value() = mEndpoint;
|
||||
|
||||
aJSON.mKeys.mP256dh.Construct();
|
||||
nsresult rv = Base64URLEncode(mRawP256dhKey.Length(),
|
||||
mRawP256dhKey.Elements(),
|
||||
Base64URLEncodePaddingPolicy::Omit,
|
||||
aJSON.mKeys.mP256dh.Value());
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
aRv.Throw(rv);
|
||||
return;
|
||||
}
|
||||
|
||||
aJSON.mKeys.mAuth.Construct();
|
||||
rv = Base64URLEncode(mAuthSecret.Length(), mAuthSecret.Elements(),
|
||||
Base64URLEncodePaddingPolicy::Omit,
|
||||
aJSON.mKeys.mAuth.Value());
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
aRv.Throw(rv);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
already_AddRefed<PushSubscriptionOptions>
|
||||
PushSubscription::Options()
|
||||
{
|
||||
RefPtr<PushSubscriptionOptions> options = mOptions;
|
||||
return options.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
PushSubscription::UnsubscribeFromWorker(ErrorResult& aRv)
|
||||
{
|
||||
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
|
||||
MOZ_ASSERT(worker);
|
||||
worker->AssertIsOnWorkerThread();
|
||||
|
||||
nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope();
|
||||
RefPtr<Promise> p = Promise::Create(global, aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
|
||||
if (!proxy) {
|
||||
p->MaybeReject(NS_ERROR_DOM_PUSH_SERVICE_UNREACHABLE);
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
RefPtr<UnsubscribeRunnable> r =
|
||||
new UnsubscribeRunnable(proxy, mScope);
|
||||
MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));
|
||||
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
99
dom/push/PushSubscription.h
Normal file
99
dom/push/PushSubscription.h
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_dom_PushSubscription_h
|
||||
#define mozilla_dom_PushSubscription_h
|
||||
|
||||
#include "jsapi.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsWrapperCache.h"
|
||||
#include "nsContentUtils.h" // Required for nsContentUtils::PushEnabled
|
||||
|
||||
#include "mozilla/AlreadyAddRefed.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
|
||||
#include "mozilla/dom/BindingDeclarations.h"
|
||||
#include "mozilla/dom/PushSubscriptionBinding.h"
|
||||
#include "mozilla/dom/PushSubscriptionOptionsBinding.h"
|
||||
#include "mozilla/dom/TypedArray.h"
|
||||
|
||||
class nsIGlobalObject;
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
namespace workers {
|
||||
class WorkerPrivate;
|
||||
}
|
||||
|
||||
class Promise;
|
||||
|
||||
class PushSubscription final : public nsISupports
|
||||
, public nsWrapperCache
|
||||
{
|
||||
public:
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PushSubscription)
|
||||
|
||||
PushSubscription(nsIGlobalObject* aGlobal,
|
||||
const nsAString& aEndpoint,
|
||||
const nsAString& aScope,
|
||||
nsTArray<uint8_t>&& aP256dhKey,
|
||||
nsTArray<uint8_t>&& aAuthSecret,
|
||||
nsTArray<uint8_t>&& aAppServerKey);
|
||||
|
||||
JSObject*
|
||||
WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
nsIGlobalObject*
|
||||
GetParentObject() const
|
||||
{
|
||||
return mGlobal;
|
||||
}
|
||||
|
||||
void
|
||||
GetEndpoint(nsAString& aEndpoint) const
|
||||
{
|
||||
aEndpoint = mEndpoint;
|
||||
}
|
||||
|
||||
void
|
||||
GetKey(JSContext* cx,
|
||||
PushEncryptionKeyName aType,
|
||||
JS::MutableHandle<JSObject*> aKey,
|
||||
ErrorResult& aRv);
|
||||
|
||||
static already_AddRefed<PushSubscription>
|
||||
Constructor(GlobalObject& aGlobal,
|
||||
const PushSubscriptionInit& aInitDict,
|
||||
ErrorResult& aRv);
|
||||
|
||||
already_AddRefed<Promise>
|
||||
Unsubscribe(ErrorResult& aRv);
|
||||
|
||||
void
|
||||
ToJSON(PushSubscriptionJSON& aJSON, ErrorResult& aRv);
|
||||
|
||||
already_AddRefed<PushSubscriptionOptions>
|
||||
Options();
|
||||
|
||||
private:
|
||||
~PushSubscription();
|
||||
|
||||
already_AddRefed<Promise>
|
||||
UnsubscribeFromWorker(ErrorResult& aRv);
|
||||
|
||||
nsString mEndpoint;
|
||||
nsString mScope;
|
||||
nsTArray<uint8_t> mRawP256dhKey;
|
||||
nsTArray<uint8_t> mAuthSecret;
|
||||
nsCOMPtr<nsIGlobalObject> mGlobal;
|
||||
RefPtr<PushSubscriptionOptions> mOptions;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_PushSubscription_h
|
||||
79
dom/push/PushSubscriptionOptions.cpp
Normal file
79
dom/push/PushSubscriptionOptions.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/* 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/. */
|
||||
|
||||
#include "mozilla/dom/PushSubscriptionOptions.h"
|
||||
|
||||
#include "mozilla/dom/PushSubscriptionOptionsBinding.h"
|
||||
#include "mozilla/HoldDropJSObjects.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
PushSubscriptionOptions::PushSubscriptionOptions(nsIGlobalObject* aGlobal,
|
||||
nsTArray<uint8_t>&& aRawAppServerKey)
|
||||
: mGlobal(aGlobal)
|
||||
, mRawAppServerKey(Move(aRawAppServerKey))
|
||||
, mAppServerKey(nullptr)
|
||||
{
|
||||
// There's only one global on a worker, so we don't need to pass a global
|
||||
// object to the constructor.
|
||||
MOZ_ASSERT_IF(NS_IsMainThread(), mGlobal);
|
||||
mozilla::HoldJSObjects(this);
|
||||
}
|
||||
|
||||
PushSubscriptionOptions::~PushSubscriptionOptions()
|
||||
{
|
||||
mAppServerKey = nullptr;
|
||||
mozilla::DropJSObjects(this);
|
||||
}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_CLASS(PushSubscriptionOptions)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PushSubscriptionOptions)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
|
||||
tmp->mAppServerKey = nullptr;
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PushSubscriptionOptions)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(PushSubscriptionOptions)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAppServerKey)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushSubscriptionOptions)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushSubscriptionOptions)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushSubscriptionOptions)
|
||||
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
||||
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
JSObject*
|
||||
PushSubscriptionOptions::WrapObject(JSContext* aCx,
|
||||
JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
return PushSubscriptionOptionsBinding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
void
|
||||
PushSubscriptionOptions::GetApplicationServerKey(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aKey,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (!mRawAppServerKey.IsEmpty() && !mAppServerKey) {
|
||||
JS::Rooted<JSObject*> appServerKey(aCx);
|
||||
PushUtil::CopyArrayToArrayBuffer(aCx, mRawAppServerKey, &appServerKey, aRv);
|
||||
if (aRv.Failed()) {
|
||||
return;
|
||||
}
|
||||
MOZ_ASSERT(appServerKey);
|
||||
mAppServerKey = appServerKey;
|
||||
}
|
||||
aKey.set(mAppServerKey);
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
56
dom/push/PushSubscriptionOptions.h
Normal file
56
dom/push/PushSubscriptionOptions.h
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_dom_PushSubscriptionOptions_h
|
||||
#define mozilla_dom_PushSubscriptionOptions_h
|
||||
|
||||
#include "nsCycleCollectionParticipant.h"
|
||||
#include "nsContentUtils.h" // Required for nsContentUtils::PushEnabled
|
||||
#include "nsTArray.h"
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
class nsIGlobalObject;
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
class ErrorResult;
|
||||
|
||||
namespace dom {
|
||||
|
||||
class PushSubscriptionOptions final : public nsISupports
|
||||
, public nsWrapperCache
|
||||
{
|
||||
public:
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PushSubscriptionOptions)
|
||||
|
||||
PushSubscriptionOptions(nsIGlobalObject* aGlobal,
|
||||
nsTArray<uint8_t>&& aRawAppServerKey);
|
||||
|
||||
nsIGlobalObject*
|
||||
GetParentObject() const
|
||||
{
|
||||
return mGlobal;
|
||||
}
|
||||
|
||||
JSObject*
|
||||
WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
void
|
||||
GetApplicationServerKey(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aKey,
|
||||
ErrorResult& aRv);
|
||||
|
||||
private:
|
||||
~PushSubscriptionOptions();
|
||||
|
||||
nsCOMPtr<nsIGlobalObject> mGlobal;
|
||||
nsTArray<uint8_t> mRawAppServerKey;
|
||||
JS::Heap<JSObject*> mAppServerKey;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_PushSubscriptionOptions_h
|
||||
64
dom/push/PushUtil.cpp
Normal file
64
dom/push/PushUtil.cpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/* 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/. */
|
||||
|
||||
#include "mozilla/dom/PushUtil.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
/* static */ bool
|
||||
PushUtil::CopyArrayBufferToArray(const ArrayBuffer& aBuffer,
|
||||
nsTArray<uint8_t>& aArray)
|
||||
{
|
||||
MOZ_ASSERT(aArray.IsEmpty());
|
||||
aBuffer.ComputeLengthAndData();
|
||||
return aArray.SetCapacity(aBuffer.Length(), fallible) &&
|
||||
aArray.InsertElementsAt(0, aBuffer.Data(), aBuffer.Length(), fallible);
|
||||
}
|
||||
|
||||
/* static */ bool
|
||||
PushUtil::CopyArrayBufferViewToArray(const ArrayBufferView& aView,
|
||||
nsTArray<uint8_t>& aArray)
|
||||
{
|
||||
MOZ_ASSERT(aArray.IsEmpty());
|
||||
aView.ComputeLengthAndData();
|
||||
return aArray.SetCapacity(aView.Length(), fallible) &&
|
||||
aArray.InsertElementsAt(0, aView.Data(), aView.Length(), fallible);
|
||||
}
|
||||
|
||||
/* static */ bool
|
||||
PushUtil::CopyBufferSourceToArray(
|
||||
const OwningArrayBufferViewOrArrayBuffer& aSource, nsTArray<uint8_t>& aArray)
|
||||
{
|
||||
if (aSource.IsArrayBuffer()) {
|
||||
return CopyArrayBufferToArray(aSource.GetAsArrayBuffer(), aArray);
|
||||
}
|
||||
if (aSource.IsArrayBufferView()) {
|
||||
return CopyArrayBufferViewToArray(aSource.GetAsArrayBufferView(), aArray);
|
||||
}
|
||||
MOZ_CRASH("Uninitialized union: expected buffer or view");
|
||||
}
|
||||
|
||||
/* static */ void
|
||||
PushUtil::CopyArrayToArrayBuffer(JSContext* aCx,
|
||||
const nsTArray<uint8_t>& aArray,
|
||||
JS::MutableHandle<JSObject*> aValue,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (aArray.IsEmpty()) {
|
||||
aValue.set(nullptr);
|
||||
return;
|
||||
}
|
||||
JS::Rooted<JSObject*> buffer(aCx, ArrayBuffer::Create(aCx,
|
||||
aArray.Length(),
|
||||
aArray.Elements()));
|
||||
if (NS_WARN_IF(!buffer)) {
|
||||
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
||||
return;
|
||||
}
|
||||
aValue.set(buffer);
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
43
dom/push/PushUtil.h
Normal file
43
dom/push/PushUtil.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_dom_PushUtil_h
|
||||
#define mozilla_dom_PushUtil_h
|
||||
|
||||
#include "nsTArray.h"
|
||||
|
||||
#include "mozilla/dom/TypedArray.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
class OwningArrayBufferViewOrArrayBuffer;
|
||||
|
||||
class PushUtil final
|
||||
{
|
||||
private:
|
||||
PushUtil() = delete;
|
||||
|
||||
public:
|
||||
static bool
|
||||
CopyArrayBufferToArray(const ArrayBuffer& aBuffer,
|
||||
nsTArray<uint8_t>& aArray);
|
||||
|
||||
static bool
|
||||
CopyArrayBufferViewToArray(const ArrayBufferView& aView,
|
||||
nsTArray<uint8_t>& aArray);
|
||||
|
||||
static bool
|
||||
CopyBufferSourceToArray(const OwningArrayBufferViewOrArrayBuffer& aSource,
|
||||
nsTArray<uint8_t>& aArray);
|
||||
|
||||
static void
|
||||
CopyArrayToArrayBuffer(JSContext* aCx, const nsTArray<uint8_t>& aArray,
|
||||
JS::MutableHandle<JSObject*> aValue, ErrorResult& aRv);
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_dom_PushUtil_h
|
||||
65
dom/push/moz.build
Normal file
65
dom/push/moz.build
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'Push.js',
|
||||
'Push.manifest',
|
||||
'PushComponents.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'PushCrypto.jsm',
|
||||
'PushDB.jsm',
|
||||
'PushRecord.jsm',
|
||||
'PushService.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
|
||||
# Everything but Fennec.
|
||||
EXTRA_JS_MODULES += [
|
||||
'PushServiceHttp2.jsm',
|
||||
'PushServiceWebSocket.jsm',
|
||||
]
|
||||
else:
|
||||
# Fennec only.
|
||||
EXTRA_JS_MODULES += [
|
||||
'PushServiceAndroidGCM.jsm',
|
||||
]
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
'test/mochitest.ini',
|
||||
]
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += [
|
||||
'test/xpcshell/xpcshell.ini',
|
||||
]
|
||||
|
||||
EXPORTS.mozilla.dom += [
|
||||
'PushManager.h',
|
||||
'PushNotifier.h',
|
||||
'PushSubscription.h',
|
||||
'PushSubscriptionOptions.h',
|
||||
'PushUtil.h',
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
'PushManager.cpp',
|
||||
'PushNotifier.cpp',
|
||||
'PushSubscription.cpp',
|
||||
'PushSubscriptionOptions.cpp',
|
||||
'PushUtil.cpp',
|
||||
]
|
||||
|
||||
TEST_DIRS += ['test/xpcshell']
|
||||
|
||||
include('/ipc/chromium/chromium-config.mozbuild')
|
||||
|
||||
LOCAL_INCLUDES += [
|
||||
'../base',
|
||||
'../ipc',
|
||||
'../workers',
|
||||
]
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
10
dom/push/test/error_worker.js
Normal file
10
dom/push/test/error_worker.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
this.onpush = function(event) {
|
||||
var request = event.data.json();
|
||||
if (request.type == "exception") {
|
||||
throw new Error("Uncaught exception");
|
||||
}
|
||||
if (request.type == "rejection") {
|
||||
event.waitUntil(Promise.reject(
|
||||
new Error("Unhandled rejection")));
|
||||
}
|
||||
};
|
||||
24
dom/push/test/frame.html
Normal file
24
dom/push/test/frame.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
<script>
|
||||
|
||||
function waitOnWorkerMessage(type) {
|
||||
return new Promise(function(res, rej) {
|
||||
function onMessage(e) {
|
||||
if (e.data.type == type) {
|
||||
navigator.serviceWorker.removeEventListener("message", onMessage);
|
||||
(e.data.okay == "yes" ? res : rej)(e.data);
|
||||
}
|
||||
}
|
||||
navigator.serviceWorker.addEventListener("message", onMessage);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
85
dom/push/test/lifetime_worker.js
Normal file
85
dom/push/test/lifetime_worker.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
var state = "from_scope";
|
||||
var resolvePromiseCallback;
|
||||
|
||||
onfetch = function(event) {
|
||||
if (event.request.url.indexOf("lifetime_frame.html") >= 0) {
|
||||
event.respondWith(new Response("iframe_lifetime"));
|
||||
return;
|
||||
}
|
||||
|
||||
var currentState = state;
|
||||
event.waitUntil(
|
||||
clients.matchAll()
|
||||
.then(clients => {
|
||||
clients.forEach(client => {
|
||||
client.postMessage({type: "fetch", state: currentState});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (event.request.url.indexOf("update") >= 0) {
|
||||
state = "update";
|
||||
} else if (event.request.url.indexOf("wait") >= 0) {
|
||||
event.respondWith(new Promise(function(res, rej) {
|
||||
if (resolvePromiseCallback) {
|
||||
dump("ERROR: service worker was already waiting on a promise.\n");
|
||||
}
|
||||
resolvePromiseCallback = function() {
|
||||
res(new Response("resolve_respondWithPromise"));
|
||||
};
|
||||
}));
|
||||
state = "wait";
|
||||
} else if (event.request.url.indexOf("release") >= 0) {
|
||||
state = "release";
|
||||
resolvePromise();
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePromise() {
|
||||
if (resolvePromiseCallback === undefined || resolvePromiseCallback == null) {
|
||||
dump("ERROR: wait promise was not set.\n");
|
||||
return;
|
||||
}
|
||||
resolvePromiseCallback();
|
||||
resolvePromiseCallback = null;
|
||||
}
|
||||
|
||||
onmessage = function(event) {
|
||||
var lastState = state;
|
||||
state = event.data;
|
||||
if (state === 'wait') {
|
||||
event.waitUntil(new Promise(function(res, rej) {
|
||||
if (resolvePromiseCallback) {
|
||||
dump("ERROR: service worker was already waiting on a promise.\n");
|
||||
}
|
||||
resolvePromiseCallback = res;
|
||||
}));
|
||||
} else if (state === 'release') {
|
||||
resolvePromise();
|
||||
}
|
||||
event.source.postMessage({type: "message", state: lastState});
|
||||
}
|
||||
|
||||
onpush = function(event) {
|
||||
var pushResolve;
|
||||
event.waitUntil(new Promise(function(resolve) {
|
||||
pushResolve = resolve;
|
||||
}));
|
||||
|
||||
// FIXME(catalinb): push message carry no data. So we assume the only
|
||||
// push message we get is "wait"
|
||||
clients.matchAll().then(function(client) {
|
||||
if (client.length == 0) {
|
||||
dump("ERROR: no clients to send the response to.\n");
|
||||
}
|
||||
|
||||
client[0].postMessage({type: "push", state: state});
|
||||
|
||||
state = "wait";
|
||||
if (resolvePromiseCallback) {
|
||||
dump("ERROR: service worker was already waiting on a promise.\n");
|
||||
} else {
|
||||
resolvePromiseCallback = pushResolve;
|
||||
}
|
||||
});
|
||||
}
|
||||
24
dom/push/test/mochitest.ini
Normal file
24
dom/push/test/mochitest.ini
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[DEFAULT]
|
||||
skip-if = os == "android"
|
||||
support-files =
|
||||
worker.js
|
||||
frame.html
|
||||
webpush.js
|
||||
lifetime_worker.js
|
||||
test_utils.js
|
||||
mockpushserviceparent.js
|
||||
error_worker.js
|
||||
|
||||
[test_has_permissions.html]
|
||||
[test_permissions.html]
|
||||
[test_register.html]
|
||||
[test_register_key.html]
|
||||
[test_multiple_register.html]
|
||||
[test_multiple_register_during_service_activation.html]
|
||||
[test_unregister.html]
|
||||
[test_multiple_register_different_scope.html]
|
||||
[test_subscription_change.html]
|
||||
[test_data.html]
|
||||
[test_try_registering_offline_disabled.html]
|
||||
[test_serviceworker_lifetime.html]
|
||||
[test_error_reporting.html]
|
||||
168
dom/push/test/mockpushserviceparent.js
Normal file
168
dom/push/test/mockpushserviceparent.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
/**
|
||||
* Defers one or more callbacks until the next turn of the event loop. Multiple
|
||||
* callbacks are executed in order.
|
||||
*
|
||||
* @param {Function[]} callbacks The callbacks to execute. One callback will be
|
||||
* executed per tick.
|
||||
*/
|
||||
function waterfall(...callbacks) {
|
||||
callbacks.reduce((promise, callback) => promise.then(() => {
|
||||
callback();
|
||||
}), Promise.resolve()).catch(Cu.reportError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal implementation of a mock WebSocket connect to be used with
|
||||
* PushService. Forwards and receive messages from the implementation
|
||||
* that lives in the content process.
|
||||
*/
|
||||
function MockWebSocketParent(originalURI) {
|
||||
this._originalURI = originalURI;
|
||||
}
|
||||
|
||||
MockWebSocketParent.prototype = {
|
||||
_originalURI: null,
|
||||
|
||||
_listener: null,
|
||||
_context: null,
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsISupports,
|
||||
Ci.nsIWebSocketChannel
|
||||
]),
|
||||
|
||||
get originalURI() {
|
||||
return this._originalURI;
|
||||
},
|
||||
|
||||
asyncOpen(uri, origin, windowId, listener, context) {
|
||||
this._listener = listener;
|
||||
this._context = context;
|
||||
waterfall(() => this._listener.onStart(this._context));
|
||||
},
|
||||
|
||||
sendMsg(msg) {
|
||||
sendAsyncMessage("socket-client-msg", msg);
|
||||
},
|
||||
|
||||
close() {
|
||||
waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
|
||||
},
|
||||
|
||||
serverSendMsg(msg) {
|
||||
waterfall(() => this._listener.onMessageAvailable(this._context, msg),
|
||||
() => this._listener.onAcknowledge(this._context, 0));
|
||||
},
|
||||
};
|
||||
|
||||
var pushService = Cc["@mozilla.org/push/Service;1"].
|
||||
getService(Ci.nsIPushService).
|
||||
wrappedJSObject;
|
||||
|
||||
var mockSocket;
|
||||
var serverMsgs = [];
|
||||
|
||||
addMessageListener("socket-setup", function () {
|
||||
pushService.replaceServiceBackend({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
mockSocket = new MockWebSocketParent(uri);
|
||||
while (serverMsgs.length > 0) {
|
||||
let msg = serverMsgs.shift();
|
||||
mockSocket.serverSendMsg(msg);
|
||||
}
|
||||
return mockSocket;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
addMessageListener("socket-teardown", function (msg) {
|
||||
pushService.restoreServiceBackend().then(_ => {
|
||||
serverMsgs.length = 0;
|
||||
if (mockSocket) {
|
||||
mockSocket.close();
|
||||
mockSocket = null;
|
||||
}
|
||||
sendAsyncMessage("socket-server-teardown");
|
||||
}).catch(error => {
|
||||
Cu.reportError(`Error restoring service backend: ${error}`);
|
||||
})
|
||||
});
|
||||
|
||||
addMessageListener("socket-server-msg", function (msg) {
|
||||
if (mockSocket) {
|
||||
mockSocket.serverSendMsg(msg);
|
||||
} else {
|
||||
serverMsgs.push(msg);
|
||||
}
|
||||
});
|
||||
|
||||
var MockService = {
|
||||
requestID: 1,
|
||||
resolvers: new Map(),
|
||||
|
||||
sendRequest(name, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let id = this.requestID++;
|
||||
this.resolvers.set(id, { resolve, reject });
|
||||
sendAsyncMessage("service-request", {
|
||||
name: name,
|
||||
id: id,
|
||||
params: params,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleResponse(response) {
|
||||
if (!this.resolvers.has(response.id)) {
|
||||
Cu.reportError(`Unexpected response for request ${response.id}`);
|
||||
return;
|
||||
}
|
||||
let resolver = this.resolvers.get(response.id);
|
||||
this.resolvers.delete(response.id);
|
||||
if (response.error) {
|
||||
resolver.reject(response.error);
|
||||
} else {
|
||||
resolver.resolve(response.result);
|
||||
}
|
||||
},
|
||||
|
||||
init() {},
|
||||
|
||||
register(pageRecord) {
|
||||
return this.sendRequest("register", pageRecord);
|
||||
},
|
||||
|
||||
registration(pageRecord) {
|
||||
return this.sendRequest("registration", pageRecord);
|
||||
},
|
||||
|
||||
unregister(pageRecord) {
|
||||
return this.sendRequest("unregister", pageRecord);
|
||||
},
|
||||
|
||||
reportDeliveryError(messageId, reason) {
|
||||
sendAsyncMessage("service-delivery-error", {
|
||||
messageId: messageId,
|
||||
reason: reason,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
addMessageListener("service-replace", function () {
|
||||
pushService.service = MockService;
|
||||
});
|
||||
|
||||
addMessageListener("service-restore", function () {
|
||||
pushService.service = null;
|
||||
});
|
||||
|
||||
addMessageListener("service-response", function (response) {
|
||||
MockService.handleResponse(response);
|
||||
});
|
||||
218
dom/push/test/test_data.html
Normal file
218
dom/push/test/test_data.html
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1185544: Add data delivery to the WebSocket backend.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1185544</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/webpush.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1185544">Mozilla Bug 1185544</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
var userAgentID = "ac44402c-85fc-41e4-a0d0-483316d15351";
|
||||
var channelID = null;
|
||||
|
||||
var mockSocket = new MockWebSocket();
|
||||
mockSocket.onRegister = function(request) {
|
||||
channelID = request.channelID;
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: "register",
|
||||
uaid: userAgentID,
|
||||
channelID,
|
||||
status: 200,
|
||||
pushEndpoint: "https://example.com/endpoint/1"
|
||||
}));
|
||||
};
|
||||
|
||||
var registration;
|
||||
add_task(function* start() {
|
||||
yield setupPrefsAndMockSocket(mockSocket);
|
||||
yield setPushPermission(true);
|
||||
|
||||
var url = "worker.js" + "?" + (Math.random());
|
||||
registration = yield navigator.serviceWorker.register(url, {scope: "."});
|
||||
yield waitForActive(registration);
|
||||
});
|
||||
|
||||
var controlledFrame;
|
||||
add_task(function* createControlledIFrame() {
|
||||
controlledFrame = yield injectControlledFrame();
|
||||
});
|
||||
|
||||
var pushSubscription;
|
||||
add_task(function* subscribe() {
|
||||
pushSubscription = yield registration.pushManager.subscribe();
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
add_task(function* compareJSONSubscription() {
|
||||
var json = pushSubscription.toJSON();
|
||||
is(json.endpoint, pushSubscription.endpoint, "Wrong endpoint");
|
||||
|
||||
["p256dh", "auth"].forEach(keyName => {
|
||||
isDeeply(
|
||||
base64UrlDecode(json.keys[keyName]),
|
||||
new Uint8Array(pushSubscription.getKey(keyName)),
|
||||
"Mismatched Base64-encoded key: " + keyName
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* comparePublicKey() {
|
||||
var data = yield sendRequestToWorker({ type: "publicKey" });
|
||||
var p256dhKey = new Uint8Array(pushSubscription.getKey("p256dh"));
|
||||
is(p256dhKey.length, 65, "Key share should be 65 octets");
|
||||
isDeeply(
|
||||
p256dhKey,
|
||||
new Uint8Array(data.p256dh),
|
||||
"Mismatched key share"
|
||||
);
|
||||
var authSecret = new Uint8Array(pushSubscription.getKey("auth"));
|
||||
ok(authSecret.length, 16, "Auth secret should be 16 octets");
|
||||
isDeeply(
|
||||
authSecret,
|
||||
new Uint8Array(data.auth),
|
||||
"Mismatched auth secret"
|
||||
);
|
||||
});
|
||||
|
||||
var version = 0;
|
||||
function sendEncryptedMsg(pushSubscription, message) {
|
||||
return webPushEncrypt(pushSubscription, message)
|
||||
.then((encryptedData) => {
|
||||
mockSocket.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
version: version++,
|
||||
channelID: channelID,
|
||||
data: encryptedData.data,
|
||||
headers: {
|
||||
encryption: encryptedData.encryption,
|
||||
encryption_key: encryptedData.encryption_key,
|
||||
encoding: encryptedData.encoding
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function waitForMessage(pushSubscription, message) {
|
||||
return Promise.all([
|
||||
controlledFrame.waitOnWorkerMessage("finished"),
|
||||
sendEncryptedMsg(pushSubscription, message),
|
||||
]).then(([message]) => message);
|
||||
}
|
||||
|
||||
add_task(function* sendPushMessageFromPage() {
|
||||
var typedArray = new Uint8Array([226, 130, 40, 240, 40, 140, 188]);
|
||||
var json = { hello: "world" };
|
||||
|
||||
var message = yield waitForMessage(pushSubscription, "Text message from page");
|
||||
is(message.data.text, "Text message from page", "Wrong text message data");
|
||||
|
||||
message = yield waitForMessage(
|
||||
pushSubscription,
|
||||
typedArray
|
||||
);
|
||||
isDeeply(new Uint8Array(message.data.arrayBuffer), typedArray,
|
||||
"Wrong array buffer message data");
|
||||
|
||||
message = yield waitForMessage(
|
||||
pushSubscription,
|
||||
JSON.stringify(json)
|
||||
);
|
||||
ok(message.data.json.ok, "Unexpected error parsing JSON");
|
||||
isDeeply(message.data.json.value, json, "Wrong JSON message data");
|
||||
|
||||
message = yield waitForMessage(
|
||||
pushSubscription,
|
||||
""
|
||||
);
|
||||
ok(message, "Should include data for empty messages");
|
||||
is(message.data.text, "", "Wrong text for empty message");
|
||||
is(message.data.arrayBuffer.byteLength, 0, "Wrong buffer length for empty message");
|
||||
ok(!message.data.json.ok, "Expected JSON parse error for empty message");
|
||||
|
||||
message = yield waitForMessage(
|
||||
pushSubscription,
|
||||
new Uint8Array([0x48, 0x69, 0x21, 0x20, 0xf0, 0x9f, 0x91, 0x80])
|
||||
);
|
||||
is(message.data.text, "Hi! \ud83d\udc40", "Wrong text for message with emoji");
|
||||
var text = yield new Promise((resolve, reject) => {
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = event => {
|
||||
if (reader.error) {
|
||||
reject(reader.error);
|
||||
} else {
|
||||
resolve(reader.result);
|
||||
}
|
||||
};
|
||||
reader.readAsText(message.data.blob);
|
||||
});
|
||||
is(text, "Hi! \ud83d\udc40", "Wrong blob data for message with emoji");
|
||||
|
||||
var finishedPromise = controlledFrame.waitOnWorkerMessage("finished");
|
||||
// Send a blank message.
|
||||
mockSocket.serverSendMsg(JSON.stringify({
|
||||
messageType: "notification",
|
||||
version: "vDummy",
|
||||
channelID: channelID
|
||||
}));
|
||||
|
||||
var message = yield finishedPromise;
|
||||
ok(!message.data, "Should exclude data for blank messages");
|
||||
});
|
||||
|
||||
add_task(function* unsubscribe() {
|
||||
controlledFrame.remove();
|
||||
yield pushSubscription.unsubscribe();
|
||||
});
|
||||
|
||||
add_task(function* unregister() {
|
||||
yield registration.unregister();
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
130
dom/push/test/test_error_reporting.html
Normal file
130
dom/push/test/test_error_reporting.html
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1246341: Report message delivery failures to the Push server.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1246341</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1246341">Mozilla Bug 1246341</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
var pushNotifier = SpecialPowers.Cc["@mozilla.org/push/Notifier;1"]
|
||||
.getService(SpecialPowers.Ci.nsIPushNotifier);
|
||||
|
||||
var reporters = new Map();
|
||||
|
||||
var registration;
|
||||
add_task(function* start() {
|
||||
yield setupPrefsAndReplaceService({
|
||||
reportDeliveryError(messageId, reason) {
|
||||
ok(reporters.has(messageId),
|
||||
'Unexpected error reported for message ' + messageId);
|
||||
var resolve = reporters.get(messageId);
|
||||
reporters.delete(messageId);
|
||||
resolve(reason);
|
||||
},
|
||||
});
|
||||
yield setPushPermission(true);
|
||||
|
||||
var url = "error_worker.js" + "?" + (Math.random());
|
||||
registration = yield navigator.serviceWorker.register(url, {scope: "."});
|
||||
yield waitForActive(registration);
|
||||
});
|
||||
|
||||
var controlledFrame;
|
||||
add_task(function* createControlledIFrame() {
|
||||
controlledFrame = yield injectControlledFrame();
|
||||
});
|
||||
|
||||
var idCounter = 1;
|
||||
function waitForDeliveryError(request) {
|
||||
return new Promise(resolve => {
|
||||
var data = new TextEncoder("utf-8").encode(JSON.stringify(request));
|
||||
var principal = SpecialPowers.wrap(document).nodePrincipal;
|
||||
|
||||
let messageId = "message-" + (idCounter++);
|
||||
reporters.set(messageId, resolve);
|
||||
pushNotifier.notifyPushWithData(registration.scope, principal, messageId,
|
||||
data.length, data);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* reportDeliveryErrors() {
|
||||
var reason = yield waitForDeliveryError({ type: "exception" });
|
||||
is(reason, SpecialPowers.Ci.nsIPushErrorReporter.DELIVERY_UNCAUGHT_EXCEPTION,
|
||||
"Should report uncaught exceptions");
|
||||
|
||||
reason = yield waitForDeliveryError({ type: "rejection" });
|
||||
is(reason, SpecialPowers.Ci.nsIPushErrorReporter.DELIVERY_UNHANDLED_REJECTION,
|
||||
"Should report unhandled rejections");
|
||||
});
|
||||
|
||||
add_task(function* reportDecryptionError() {
|
||||
var message = yield new Promise(resolve => {
|
||||
var consoleService = SpecialPowers.Cc["@mozilla.org/consoleservice;1"]
|
||||
.getService(SpecialPowers.Ci.nsIConsoleService);
|
||||
|
||||
var listener = SpecialPowers.wrapCallbackObject({
|
||||
QueryInterface(iid) {
|
||||
if (!SpecialPowers.Ci.nsISupports.equals(iid) &&
|
||||
!SpecialPowers.Ci.nsIConsoleListener.equals(iid)) {
|
||||
throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
observe(message) {
|
||||
let error = message;
|
||||
try {
|
||||
error.QueryInterface(SpecialPowers.Ci.nsIScriptError);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
if (message.innerWindowID == controlledFrame.innerWindowId()) {
|
||||
consoleService.unregisterListener(listener);
|
||||
resolve(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
consoleService.registerListener(listener);
|
||||
|
||||
var principal = SpecialPowers.wrap(document).nodePrincipal;
|
||||
pushNotifier.notifyError(registration.scope, principal, "Push error",
|
||||
SpecialPowers.Ci.nsIScriptError.errorFlag);
|
||||
});
|
||||
|
||||
is(message.sourceName, registration.scope,
|
||||
"Should use the qualified scope URL as the source");
|
||||
is(message.errorMessage, "Push error",
|
||||
"Should report the given error string");
|
||||
});
|
||||
|
||||
add_task(function* unsubscribe() {
|
||||
controlledFrame.remove();
|
||||
});
|
||||
|
||||
add_task(function* unregister() {
|
||||
yield registration.unregister();
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
84
dom/push/test/test_has_permissions.html
Normal file
84
dom/push/test/test_has_permissions.html
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1038811: Push tests.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1038811</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1038811">Mozilla Bug 1038811</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
function debug(str) {
|
||||
// console.log(str + "\n");
|
||||
}
|
||||
|
||||
function start() {
|
||||
return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
|
||||
.then((swr) => {
|
||||
registration = swr;
|
||||
return waitForActive(registration);
|
||||
});
|
||||
}
|
||||
|
||||
function unregister() {
|
||||
return registration.unregister().then(function(result) {
|
||||
ok(result, "Unregister should return true.");
|
||||
}, function(e) {
|
||||
dump("Unregistering the SW failed with " + e + "\n");
|
||||
});
|
||||
}
|
||||
|
||||
function hasPermission(swr) {
|
||||
var p = new Promise(function(res, rej) {
|
||||
swr.pushManager.permissionState().then(
|
||||
function(state) {
|
||||
debug("state: " + state);
|
||||
ok(["granted", "denied", "prompt"].indexOf(state) >= 0, "permissionState() returned a valid state.");
|
||||
res(swr);
|
||||
}, function(error) {
|
||||
ok(false, "permissionState() failed.");
|
||||
res(swr);
|
||||
}
|
||||
);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
start()
|
||||
.then(hasPermission)
|
||||
.then(unregister)
|
||||
.catch(function(e) {
|
||||
ok(false, "Some test failed with error " + e);
|
||||
}).then(SimpleTest.finish);
|
||||
}
|
||||
|
||||
SpecialPowers.addPermission("desktop-notification", false, document);
|
||||
SpecialPowers.pushPrefEnv({"set": [
|
||||
["dom.push.enabled", true],
|
||||
["dom.push.connection.enabled", true],
|
||||
["dom.serviceWorkers.exemptFromPerDomainMax", true],
|
||||
["dom.serviceWorkers.enabled", true],
|
||||
["dom.serviceWorkers.testing.enabled", true]
|
||||
]}, runTest);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
130
dom/push/test/test_multiple_register.html
Normal file
130
dom/push/test/test_multiple_register.html
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1038811: Push tests.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1038811</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1038811">Mozilla Bug 1038811</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
function debug(str) {
|
||||
// console.log(str + "\n");
|
||||
}
|
||||
|
||||
function start() {
|
||||
return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
|
||||
.then((swr) => {
|
||||
registration = swr
|
||||
return waitForActive(registration);
|
||||
});
|
||||
}
|
||||
|
||||
function unregister() {
|
||||
return registration.unregister().then(function(result) {
|
||||
ok(result, "Unregister should return true.");
|
||||
}, function(e) {
|
||||
dump("Unregistering the SW failed with " + e + "\n");
|
||||
});
|
||||
}
|
||||
|
||||
function setupPushNotification(swr) {
|
||||
var p = new Promise(function(res, rej) {
|
||||
swr.pushManager.subscribe().then(
|
||||
function(pushSubscription) {
|
||||
ok(true, "successful registered for push notification");
|
||||
res({swr: swr, pushSubscription: pushSubscription});
|
||||
}, function(error) {
|
||||
ok(false, "could not register for push notification");
|
||||
res(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
function setupSecondEndpoint(result) {
|
||||
var p = new Promise(function(res, rej) {
|
||||
result.swr.pushManager.subscribe().then(
|
||||
function(pushSubscription) {
|
||||
ok(result.pushSubscription.endpoint == pushSubscription.endpoint, "setupSecondEndpoint - Got the same endpoint back.");
|
||||
res(result);
|
||||
}, function(error) {
|
||||
ok(false, "could not register for push notification");
|
||||
res(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
function getEndpointExpectNull(swr) {
|
||||
var p = new Promise(function(res, rej) {
|
||||
swr.pushManager.getSubscription().then(
|
||||
function(pushSubscription) {
|
||||
ok(pushSubscription == null, "getEndpoint should return null when app not subscribed.");
|
||||
res(swr);
|
||||
}, function(error) {
|
||||
ok(false, "could not register for push notification");
|
||||
res(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
function getEndpoint(result) {
|
||||
var p = new Promise(function(res, rej) {
|
||||
result.swr.pushManager.getSubscription().then(
|
||||
function(pushSubscription) {
|
||||
ok(result.pushSubscription.endpoint == pushSubscription.endpoint, "getEndpoint - Got the same endpoint back.");
|
||||
|
||||
res(pushSubscription);
|
||||
}, function(error) {
|
||||
ok(false, "could not register for push notification");
|
||||
res(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
function unregisterPushNotification(pushSubscription) {
|
||||
return pushSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
start()
|
||||
.then(getEndpointExpectNull)
|
||||
.then(setupPushNotification)
|
||||
.then(setupSecondEndpoint)
|
||||
.then(getEndpoint)
|
||||
.then(unregisterPushNotification)
|
||||
.then(unregister)
|
||||
.catch(function(e) {
|
||||
ok(false, "Some test failed with error " + e);
|
||||
}).then(SimpleTest.finish);
|
||||
}
|
||||
|
||||
setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
|
||||
SpecialPowers.addPermission("desktop-notification", true, document);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
123
dom/push/test/test_multiple_register_different_scope.html
Normal file
123
dom/push/test/test_multiple_register_different_scope.html
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1150812: Test registering for two different scopes.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1150812</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1150812">Mozilla Bug 1150812</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
var scopeA = "./a/";
|
||||
var scopeB = "./b/";
|
||||
|
||||
function debug(str) {
|
||||
// console.log(str + "\n");
|
||||
}
|
||||
|
||||
function registerServiceWorker(scope) {
|
||||
return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: scope})
|
||||
.then(swr => waitForActive(swr));
|
||||
}
|
||||
|
||||
function unregister(swr) {
|
||||
return swr.unregister()
|
||||
.then(result => {
|
||||
ok(result, "Unregister should return true.");
|
||||
}, err => {
|
||||
ok(false,"Unregistering the SW failed with " + err + "\n");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function subscribe(swr) {
|
||||
return swr.pushManager.subscribe()
|
||||
.then(sub => {
|
||||
ok(true, "successful registered for push notification");
|
||||
return sub;
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setupMultipleSubscriptions(swr1, swr2) {
|
||||
return Promise.all([
|
||||
subscribe(swr1),
|
||||
subscribe(swr2)
|
||||
]).then(a => {
|
||||
ok(a[0].endpoint != a[1].endpoint, "setupMultipleSubscriptions - Got different endpoints.");
|
||||
return a;
|
||||
});
|
||||
}
|
||||
|
||||
function getEndpointExpectNull(swr) {
|
||||
return swr.pushManager.getSubscription()
|
||||
.then(pushSubscription => {
|
||||
ok(pushSubscription == null, "getEndpoint should return null when app not subscribed.");
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function getEndpoint(swr, results) {
|
||||
return swr.pushManager.getSubscription()
|
||||
.then(sub => {
|
||||
ok((results[0].endpoint == sub.endpoint) ||
|
||||
(results[1].endpoint == sub.endpoint), "getEndpoint - Got the same endpoint back.");
|
||||
return results;
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function unsubscribe(result) {
|
||||
return result[0].unsubscribe()
|
||||
.then(_ => result[1].unsubscribe());
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
registerServiceWorker(scopeA)
|
||||
.then(swrA =>
|
||||
registerServiceWorker(scopeB)
|
||||
.then(swrB =>
|
||||
getEndpointExpectNull(swrA)
|
||||
.then(_ => getEndpointExpectNull(swrB))
|
||||
.then(_ => setupMultipleSubscriptions(swrA, swrB))
|
||||
.then(results => getEndpoint(swrA, results))
|
||||
.then(results => getEndpoint(swrB, results))
|
||||
.then(results => unsubscribe(results))
|
||||
.then(_ => unregister(swrA))
|
||||
.then(_ => unregister(swrB))
|
||||
)
|
||||
)
|
||||
.catch(err => {
|
||||
ok(false, "Some test failed with error " + err);
|
||||
}).then(SimpleTest.finish);
|
||||
}
|
||||
|
||||
setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
|
||||
SpecialPowers.addPermission("desktop-notification", true, document);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1150812: If service is in activating or no connection state it can not send
|
||||
request immediately, but the requests are queued. This test test the case of
|
||||
multiple subscription for the same scope during activation.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1150812</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1150812">Mozilla Bug 1150812</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
function debug(str) {
|
||||
// console.log(str + "\n");
|
||||
}
|
||||
|
||||
function registerServiceWorker() {
|
||||
return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."});
|
||||
}
|
||||
|
||||
function unregister(swr) {
|
||||
return swr.unregister()
|
||||
.then(result => {
|
||||
ok(result, "Unregister should return true.");
|
||||
}, err => {
|
||||
dump("Unregistering the SW failed with " + err + "\n");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function subscribe(swr) {
|
||||
return swr.pushManager.subscribe()
|
||||
.then(sub => {
|
||||
ok(true, "successful registered for push notification");
|
||||
return sub;
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function setupMultipleSubscriptions(swr) {
|
||||
// We need to do this to restart service so that a queue will be formed.
|
||||
let promiseTeardown = teardownMockPushSocket();
|
||||
setupMockPushSocket(new MockWebSocket());
|
||||
|
||||
var pushSubscription;
|
||||
return Promise.all([
|
||||
subscribe(swr),
|
||||
subscribe(swr)
|
||||
]).then(a => {
|
||||
ok(a[0].endpoint == a[1].endpoint, "setupMultipleSubscriptions - Got the same endpoint back.");
|
||||
pushSubscription = a[0];
|
||||
return promiseTeardown;
|
||||
}).then(_ => {
|
||||
return pushSubscription;
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function getEndpointExpectNull(swr) {
|
||||
return swr.pushManager.getSubscription()
|
||||
.then(pushSubscription => {
|
||||
ok(pushSubscription == null, "getEndpoint should return null when app not subscribed.");
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function unsubscribe(sub) {
|
||||
return sub.unsubscribe();
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
registerServiceWorker()
|
||||
.then(swr =>
|
||||
getEndpointExpectNull(swr)
|
||||
.then(_ => setupMultipleSubscriptions(swr))
|
||||
.then(sub => unsubscribe(sub))
|
||||
.then(_ => unregister(swr))
|
||||
)
|
||||
.catch(err => {
|
||||
ok(false, "Some test failed with error " + err);
|
||||
}).then(SimpleTest.finish);
|
||||
}
|
||||
|
||||
setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
|
||||
SpecialPowers.addPermission("desktop-notification", true, document);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
106
dom/push/test/test_permissions.html
Normal file
106
dom/push/test/test_permissions.html
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1038811: Push tests.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1038811</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1038811">Mozilla Bug 1038811</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
function debug(str) {
|
||||
// console.log(str + "\n");
|
||||
}
|
||||
|
||||
var registration;
|
||||
add_task(function* start() {
|
||||
yield setupPrefsAndMockSocket(new MockWebSocket());
|
||||
yield setPushPermission(false);
|
||||
|
||||
var url = "worker.js" + "?" + Math.random();
|
||||
registration = yield navigator.serviceWorker.register(url, {scope: "."});
|
||||
yield waitForActive(registration);
|
||||
});
|
||||
|
||||
add_task(function* denySubscribe() {
|
||||
try {
|
||||
yield registration.pushManager.subscribe();
|
||||
ok(false, "subscribe() should fail because no permission for push");
|
||||
} catch (error) {
|
||||
ok(error instanceof DOMException, "Wrong exception type");
|
||||
is(error.name, "NotAllowedError", "Wrong exception name");
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* denySubscribeInWorker() {
|
||||
// If permission is revoked, `getSubscription()` should return `null`, and
|
||||
// `subscribe()` should reject immediately. Calling these from the worker
|
||||
// should not deadlock the main thread (see bug 1228723).
|
||||
var errorInfo = yield sendRequestToWorker({
|
||||
type: "denySubscribe",
|
||||
});
|
||||
ok(errorInfo.isDOMException, "Wrong exception type");
|
||||
is(errorInfo.name, "NotAllowedError", "Wrong exception name");
|
||||
});
|
||||
|
||||
add_task(function* getEndpoint() {
|
||||
var pushSubscription = yield registration.pushManager.getSubscription();
|
||||
is(pushSubscription, null, "getSubscription() should return null because no permission for push");
|
||||
});
|
||||
|
||||
add_task(function* checkPermissionState() {
|
||||
var permissionManager = SpecialPowers.Ci.nsIPermissionManager;
|
||||
var tests = [{
|
||||
action: permissionManager.ALLOW_ACTION,
|
||||
state: "granted",
|
||||
}, {
|
||||
action: permissionManager.DENY_ACTION,
|
||||
state: "denied",
|
||||
}, {
|
||||
action: permissionManager.PROMPT_ACTION,
|
||||
state: "prompt",
|
||||
}, {
|
||||
action: permissionManager.UNKNOWN_ACTION,
|
||||
state: "prompt",
|
||||
}];
|
||||
for (var test of tests) {
|
||||
yield setPushPermission(test.action);
|
||||
var state = yield registration.pushManager.permissionState();
|
||||
is(state, test.state, JSON.stringify(test));
|
||||
try {
|
||||
yield SpecialPowers.pushPrefEnv({ set: [
|
||||
["dom.push.testing.ignorePermission", true]] });
|
||||
state = yield registration.pushManager.permissionState();
|
||||
is(state, "granted", `Should ignore ${
|
||||
test.action} if the override pref is set`);
|
||||
} finally {
|
||||
yield SpecialPowers.flushPrefEnv();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* unregister() {
|
||||
var result = yield registration.unregister();
|
||||
ok(result, "Unregister should return true.");
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
109
dom/push/test/test_register.html
Normal file
109
dom/push/test/test_register.html
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1038811: Push tests.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1038811</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1038811">Mozilla Bug 1038811</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
function debug(str) {
|
||||
// console.log(str + "\n");
|
||||
}
|
||||
|
||||
var mockSocket = new MockWebSocket();
|
||||
|
||||
var channelID = null;
|
||||
|
||||
mockSocket.onRegister = function(request) {
|
||||
channelID = request.channelID;
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: "register",
|
||||
uaid: "c69e2014-9e15-438d-b253-d79cc2df60a8",
|
||||
channelID,
|
||||
status: 200,
|
||||
pushEndpoint: "https://example.com/endpoint/1"
|
||||
}));
|
||||
};
|
||||
|
||||
var registration;
|
||||
add_task(function* start() {
|
||||
yield setupPrefsAndMockSocket(mockSocket);
|
||||
yield setPushPermission(true);
|
||||
|
||||
var url = "worker.js" + "?" + (Math.random());
|
||||
registration = yield navigator.serviceWorker.register(url, {scope: "."});
|
||||
yield waitForActive(registration);
|
||||
});
|
||||
|
||||
var controlledFrame;
|
||||
add_task(function* createControlledIFrame() {
|
||||
controlledFrame = yield injectControlledFrame();
|
||||
});
|
||||
|
||||
add_task(function* checkPermissionState() {
|
||||
var state = yield registration.pushManager.permissionState();
|
||||
is(state, "granted", "permissionState() should resolve to granted.");
|
||||
});
|
||||
|
||||
var pushSubscription;
|
||||
add_task(function* subscribe() {
|
||||
pushSubscription = yield registration.pushManager.subscribe();
|
||||
is(pushSubscription.options.applicationServerKey, null,
|
||||
"Subscription should not have an app server key");
|
||||
});
|
||||
|
||||
add_task(function* resubscribe() {
|
||||
var data = yield sendRequestToWorker({
|
||||
type: "resubscribe",
|
||||
endpoint: pushSubscription.endpoint,
|
||||
});
|
||||
pushSubscription = yield registration.pushManager.getSubscription();
|
||||
is(data.endpoint, pushSubscription.endpoint,
|
||||
"Subscription endpoints should match after resubscribing in worker");
|
||||
});
|
||||
|
||||
add_task(function* waitForPushNotification() {
|
||||
var finishedPromise = controlledFrame.waitOnWorkerMessage("finished");
|
||||
|
||||
// Send a blank message.
|
||||
mockSocket.serverSendMsg(JSON.stringify({
|
||||
messageType: "notification",
|
||||
version: "vDummy",
|
||||
channelID: channelID
|
||||
}));
|
||||
|
||||
yield finishedPromise;
|
||||
});
|
||||
|
||||
add_task(function* unsubscribe() {
|
||||
controlledFrame.remove();
|
||||
yield pushSubscription.unsubscribe();
|
||||
});
|
||||
|
||||
add_task(function* unregister() {
|
||||
var result = yield registration.unregister();
|
||||
ok(result, "Unregister should return true.");
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
210
dom/push/test/test_register_key.html
Normal file
210
dom/push/test/test_register_key.html
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1247685: Implement `applicationServerKey` for subscription association.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1247685</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1247685">Mozilla Bug 1247685</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
var isTestingMismatchedKey = false;
|
||||
var subscriptions = 0;
|
||||
var testKey; // Generated in `start`.
|
||||
|
||||
function generateKey() {
|
||||
return crypto.subtle.generateKey({
|
||||
name: "ECDSA",
|
||||
namedCurve: "P-256",
|
||||
}, true, ["sign", "verify"]).then(cryptoKey =>
|
||||
crypto.subtle.exportKey("raw", cryptoKey.publicKey)
|
||||
).then(publicKey => new Uint8Array(publicKey));
|
||||
}
|
||||
|
||||
var registration;
|
||||
add_task(function* start() {
|
||||
yield setupPrefsAndReplaceService({
|
||||
register(pageRecord) {
|
||||
ok(pageRecord.appServerKey.length > 0,
|
||||
"App server key should not be empty");
|
||||
if (pageRecord.appServerKey.length != 65) {
|
||||
throw { result:
|
||||
SpecialPowers.Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR };
|
||||
}
|
||||
if (isTestingMismatchedKey) {
|
||||
throw { result:
|
||||
SpecialPowers.Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR };
|
||||
}
|
||||
return {
|
||||
endpoint: "https://example.com/push/" + (++subscriptions),
|
||||
appServerKey: pageRecord.appServerKey,
|
||||
};
|
||||
},
|
||||
|
||||
registration(pageRecord) {
|
||||
return {
|
||||
endpoint: "https://example.com/push/subWithKey",
|
||||
appServerKey: testKey,
|
||||
};
|
||||
},
|
||||
});
|
||||
yield setPushPermission(true);
|
||||
testKey = yield generateKey();
|
||||
|
||||
var url = "worker.js" + "?" + (Math.random());
|
||||
registration = yield navigator.serviceWorker.register(url, {scope: "."});
|
||||
yield waitForActive(registration);
|
||||
});
|
||||
|
||||
var controlledFrame;
|
||||
add_task(function* createControlledIFrame() {
|
||||
controlledFrame = yield injectControlledFrame();
|
||||
});
|
||||
|
||||
add_task(function* emptyKey() {
|
||||
try {
|
||||
yield registration.pushManager.subscribe({
|
||||
applicationServerKey: new ArrayBuffer(0),
|
||||
});
|
||||
ok(false, "Should reject for empty app server keys");
|
||||
} catch (error) {
|
||||
ok(error instanceof DOMException,
|
||||
"Wrong exception type for empty key");
|
||||
is(error.name, "InvalidAccessError",
|
||||
"Wrong exception name for empty key");
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* invalidKey() {
|
||||
try {
|
||||
yield registration.pushManager.subscribe({
|
||||
applicationServerKey: new Uint8Array([0]),
|
||||
});
|
||||
ok(false, "Should reject for invalid app server keys");
|
||||
} catch (error) {
|
||||
ok(error instanceof DOMException,
|
||||
"Wrong exception type for invalid key");
|
||||
is(error.name, "InvalidAccessError",
|
||||
"Wrong exception name for invalid key");
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* validKey() {
|
||||
var pushSubscription = yield registration.pushManager.subscribe({
|
||||
applicationServerKey: yield generateKey(),
|
||||
});
|
||||
is(pushSubscription.endpoint, "https://example.com/push/1",
|
||||
"Wrong endpoint for subscription with key");
|
||||
is(pushSubscription.options.applicationServerKey,
|
||||
pushSubscription.options.applicationServerKey,
|
||||
"App server key getter should return the same object");
|
||||
});
|
||||
|
||||
add_task(function* retrieveKey() {
|
||||
var pushSubscription = yield registration.pushManager.getSubscription();
|
||||
is(pushSubscription.endpoint, "https://example.com/push/subWithKey",
|
||||
"Got wrong endpoint for subscription with key");
|
||||
isDeeply(
|
||||
new Uint8Array(pushSubscription.options.applicationServerKey),
|
||||
testKey,
|
||||
"Got wrong app server key"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(function* mismatchedKey() {
|
||||
isTestingMismatchedKey = true;
|
||||
try {
|
||||
yield registration.pushManager.subscribe({
|
||||
applicationServerKey: yield generateKey(),
|
||||
});
|
||||
ok(false, "Should reject for mismatched app server keys");
|
||||
} catch (error) {
|
||||
ok(error instanceof DOMException,
|
||||
"Wrong exception type for mismatched key");
|
||||
is(error.name, "InvalidStateError",
|
||||
"Wrong exception name for mismatched key");
|
||||
} finally {
|
||||
isTestingMismatchedKey = false;
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* emptyKeyInWorker() {
|
||||
var errorInfo = yield sendRequestToWorker({
|
||||
type: "subscribeWithKey",
|
||||
key: new ArrayBuffer(0),
|
||||
});
|
||||
ok(errorInfo.isDOMException,
|
||||
"Wrong exception type in worker for empty key");
|
||||
is(errorInfo.name, "InvalidAccessError",
|
||||
"Wrong exception name in worker for empty key");
|
||||
});
|
||||
|
||||
add_task(function* invalidKeyInWorker() {
|
||||
var errorInfo = yield sendRequestToWorker({
|
||||
type: "subscribeWithKey",
|
||||
key: new Uint8Array([1]),
|
||||
});
|
||||
ok(errorInfo.isDOMException,
|
||||
"Wrong exception type in worker for invalid key");
|
||||
is(errorInfo.name, "InvalidAccessError",
|
||||
"Wrong exception name in worker for invalid key");
|
||||
});
|
||||
|
||||
add_task(function* validKeyInWorker() {
|
||||
var key = yield generateKey();
|
||||
var data = yield sendRequestToWorker({
|
||||
type: "subscribeWithKey",
|
||||
key: key,
|
||||
});
|
||||
is(data.endpoint, "https://example.com/push/2",
|
||||
"Wrong endpoint for subscription with key created in worker");
|
||||
isDeeply(new Uint8Array(data.key), key,
|
||||
"Wrong app server key for subscription created in worker");
|
||||
});
|
||||
|
||||
add_task(function* mismatchedKeyInWorker() {
|
||||
isTestingMismatchedKey = true;
|
||||
try {
|
||||
var errorInfo = yield sendRequestToWorker({
|
||||
type: "subscribeWithKey",
|
||||
key: yield generateKey(),
|
||||
});
|
||||
ok(errorInfo.isDOMException,
|
||||
"Wrong exception type in worker for mismatched key");
|
||||
is(errorInfo.name, "InvalidStateError",
|
||||
"Wrong exception name in worker for mismatched key");
|
||||
} finally {
|
||||
isTestingMismatchedKey = false;
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* unsubscribe() {
|
||||
is(subscriptions, 2, "Wrong subscription count");
|
||||
controlledFrame.remove();
|
||||
});
|
||||
|
||||
add_task(function* unregister() {
|
||||
yield registration.unregister();
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
362
dom/push/test/test_serviceworker_lifetime.html
Normal file
362
dom/push/test/test_serviceworker_lifetime.html
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the lifetime management of service workers. We keep this test in
|
||||
dom/push/tests to pass the external network check when connecting to
|
||||
the mozilla push service.
|
||||
|
||||
How this test works:
|
||||
- the service worker maintains a state variable and a promise used for
|
||||
extending its lifetime. Note that the terminating the worker will reset
|
||||
these variables to their default values.
|
||||
- we send 3 types of requests to the service worker:
|
||||
|update|, |wait| and |release|. All three requests will cause the sw to update
|
||||
its state to the new value and reply with a message containing
|
||||
its previous state. Furthermore, |wait| will set a waitUntil or a respondWith
|
||||
promise that's not resolved until the next |release| message.
|
||||
- Each subtest will use a combination of values for the timeouts and check
|
||||
if the service worker is in the correct state as we send it different
|
||||
events.
|
||||
- We also wait and assert for service worker termination using an event dispatched
|
||||
through nsIObserverService.
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1188545</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188545">Mozilla Bug 118845</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
function start() {
|
||||
return navigator.serviceWorker.register("lifetime_worker.js", {scope: "./"})
|
||||
.then((swr) => ({registration: swr}));
|
||||
}
|
||||
|
||||
function waitForActiveServiceWorker(ctx) {
|
||||
return waitForActive(ctx.registration).then(function(result) {
|
||||
ok(ctx.registration.active, "Service Worker is active");
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
|
||||
function unregister(ctx) {
|
||||
return ctx.registration.unregister().then(function(result) {
|
||||
ok(result, "Unregister should return true.");
|
||||
}, function(e) {
|
||||
dump("Unregistering the SW failed with " + e + "\n");
|
||||
});
|
||||
}
|
||||
|
||||
function registerPushNotification(ctx) {
|
||||
var p = new Promise(function(res, rej) {
|
||||
ctx.registration.pushManager.subscribe().then(
|
||||
function(pushSubscription) {
|
||||
ok(true, "successful registered for push notification");
|
||||
ctx.subscription = pushSubscription;
|
||||
res(ctx);
|
||||
}, function(error) {
|
||||
ok(false, "could not register for push notification");
|
||||
res(ctx);
|
||||
});
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
var mockSocket = new MockWebSocket();
|
||||
var endpoint = "https://example.com/endpoint/1";
|
||||
var channelID = null;
|
||||
mockSocket.onRegister = function(request) {
|
||||
channelID = request.channelID;
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: "register",
|
||||
uaid: "fa8f2e4b-5ddc-4408-b1e3-5f25a02abff0",
|
||||
channelID,
|
||||
status: 200,
|
||||
pushEndpoint: endpoint
|
||||
}));
|
||||
};
|
||||
|
||||
function sendPushToPushServer(pushEndpoint) {
|
||||
is(pushEndpoint, endpoint, "Got unexpected endpoint");
|
||||
mockSocket.serverSendMsg(JSON.stringify({
|
||||
messageType: "notification",
|
||||
version: "vDummy",
|
||||
channelID
|
||||
}));
|
||||
}
|
||||
|
||||
function unregisterPushNotification(ctx) {
|
||||
return ctx.subscription.unsubscribe().then(function(result) {
|
||||
ok(result, "unsubscribe should succeed.");
|
||||
ctx.subscription = null;
|
||||
return ctx;
|
||||
});
|
||||
}
|
||||
|
||||
function createIframe(ctx) {
|
||||
var p = new Promise(function(res, rej) {
|
||||
var iframe = document.createElement('iframe');
|
||||
// This file doesn't exist, the service worker will give us an empty
|
||||
// document.
|
||||
iframe.src = "http://mochi.test:8888/tests/dom/push/test/lifetime_frame.html";
|
||||
|
||||
iframe.onload = function() {
|
||||
ctx.iframe = iframe;
|
||||
res(ctx);
|
||||
}
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
function closeIframe(ctx) {
|
||||
ctx.iframe.parentNode.removeChild(ctx.iframe);
|
||||
return new Promise(function(res, rej) {
|
||||
// XXXcatalinb: give the worker more time to "notice" it stopped
|
||||
// controlling documents
|
||||
ctx.iframe = null;
|
||||
setTimeout(res, 0);
|
||||
}).then(() => ctx);
|
||||
}
|
||||
|
||||
function waitAndCheckMessage(contentWindow, expected) {
|
||||
function checkMessage(expected, resolve, event) {
|
||||
ok(event.data.type == expected.type, "Received correct message type: " + expected.type);
|
||||
ok(event.data.state == expected.state, "Service worker is in the correct state: " + expected.state);
|
||||
this.navigator.serviceWorker.onmessage = null;
|
||||
resolve();
|
||||
}
|
||||
return new Promise(function(res, rej) {
|
||||
contentWindow.navigator.serviceWorker.onmessage =
|
||||
checkMessage.bind(contentWindow, expected, res);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchEvent(ctx, expected_state, new_state) {
|
||||
var expected = { type: "fetch", state: expected_state };
|
||||
var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
|
||||
ctx.iframe.contentWindow.fetch(new_state);
|
||||
return p;
|
||||
}
|
||||
|
||||
function pushEvent(ctx, expected_state, new_state) {
|
||||
var expected = {type: "push", state: expected_state};
|
||||
var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
|
||||
sendPushToPushServer(ctx.subscription.endpoint);
|
||||
return p;
|
||||
}
|
||||
|
||||
function messageEventIframe(ctx, expected_state, new_state) {
|
||||
var expected = {type: "message", state: expected_state};
|
||||
var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
|
||||
ctx.iframe.contentWindow.navigator.serviceWorker.controller.postMessage(new_state);
|
||||
return p;
|
||||
}
|
||||
|
||||
function messageEvent(ctx, expected_state, new_state) {
|
||||
var expected = {type: "message", state: expected_state};
|
||||
var p = waitAndCheckMessage(window, expected);
|
||||
ctx.registration.active.postMessage(new_state);
|
||||
return p;
|
||||
}
|
||||
|
||||
function checkStateAndUpdate(eventFunction, expected_state, new_state) {
|
||||
return function(ctx) {
|
||||
return eventFunction(ctx, expected_state, new_state)
|
||||
.then(() => ctx);
|
||||
}
|
||||
}
|
||||
|
||||
function setShutdownObserver(expectingEvent) {
|
||||
info("Setting shutdown observer: expectingEvent=" + expectingEvent);
|
||||
return function(ctx) {
|
||||
cancelShutdownObserver(ctx);
|
||||
|
||||
ctx.observer_promise = new Promise(function(res, rej) {
|
||||
ctx.observer = {
|
||||
observe: function(subject, topic, data) {
|
||||
ok((topic == "service-worker-shutdown") && expectingEvent, "Service worker was terminated.");
|
||||
this.remove(ctx);
|
||||
},
|
||||
remove: function(ctx) {
|
||||
SpecialPowers.removeObserver(this, "service-worker-shutdown");
|
||||
ctx.observer = null;
|
||||
res(ctx);
|
||||
}
|
||||
}
|
||||
SpecialPowers.addObserver(ctx.observer, "service-worker-shutdown", false);
|
||||
});
|
||||
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
||||
function waitOnShutdownObserver(ctx) {
|
||||
info("Waiting on worker to shutdown.");
|
||||
return ctx.observer_promise;
|
||||
}
|
||||
|
||||
function cancelShutdownObserver(ctx) {
|
||||
if (ctx.observer) {
|
||||
ctx.observer.remove(ctx);
|
||||
}
|
||||
return ctx.observer_promise;
|
||||
}
|
||||
|
||||
function subTest(test) {
|
||||
return function(ctx) {
|
||||
return new Promise(function(res, rej) {
|
||||
function run() {
|
||||
test.steps(ctx).catch(function(e) {
|
||||
ok(false, "Some test failed with error: " + e);
|
||||
}).then((ctx) => res(ctx));
|
||||
}
|
||||
|
||||
SpecialPowers.pushPrefEnv({"set" : test.prefs}, run);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var test1 = {
|
||||
prefs: [
|
||||
["dom.serviceWorkers.idle_timeout", 0],
|
||||
["dom.serviceWorkers.idle_extended_timeout", 2999999]
|
||||
],
|
||||
// Test that service workers are terminated after the grace period expires
|
||||
// when there are no pending waitUntil or respondWith promises.
|
||||
steps: function(ctx) {
|
||||
// Test with fetch events and respondWith promises
|
||||
return createIframe(ctx)
|
||||
.then(setShutdownObserver(true))
|
||||
.then(checkStateAndUpdate(fetchEvent, "from_scope", "update"))
|
||||
.then(waitOnShutdownObserver)
|
||||
.then(setShutdownObserver(false))
|
||||
.then(checkStateAndUpdate(fetchEvent, "from_scope", "wait"))
|
||||
.then(checkStateAndUpdate(fetchEvent, "wait", "update"))
|
||||
.then(checkStateAndUpdate(fetchEvent, "update", "update"))
|
||||
.then(setShutdownObserver(true))
|
||||
// The service worker should be terminated when the promise is resolved.
|
||||
.then(checkStateAndUpdate(fetchEvent, "update", "release"))
|
||||
.then(waitOnShutdownObserver)
|
||||
.then(setShutdownObserver(false))
|
||||
.then(closeIframe)
|
||||
.then(cancelShutdownObserver)
|
||||
|
||||
// Test with push events and message events
|
||||
.then(setShutdownObserver(true))
|
||||
.then(createIframe)
|
||||
// Make sure we are shutdown before entering our "no shutdown" sequence
|
||||
// to avoid races.
|
||||
.then(waitOnShutdownObserver)
|
||||
.then(setShutdownObserver(false))
|
||||
.then(checkStateAndUpdate(pushEvent, "from_scope", "wait"))
|
||||
.then(checkStateAndUpdate(messageEventIframe, "wait", "update"))
|
||||
.then(checkStateAndUpdate(messageEventIframe, "update", "update"))
|
||||
.then(setShutdownObserver(true))
|
||||
.then(checkStateAndUpdate(messageEventIframe, "update", "release"))
|
||||
.then(waitOnShutdownObserver)
|
||||
.then(closeIframe)
|
||||
}
|
||||
}
|
||||
|
||||
var test2 = {
|
||||
prefs: [
|
||||
["dom.serviceWorkers.idle_timeout", 0],
|
||||
["dom.serviceWorkers.idle_extended_timeout", 2999999]
|
||||
],
|
||||
steps: function(ctx) {
|
||||
// Older versions used to terminate workers when the last controlled
|
||||
// window was closed. This should no longer happen, though. Verify
|
||||
// the new behavior.
|
||||
setShutdownObserver(true)(ctx);
|
||||
return createIframe(ctx)
|
||||
// Make sure we are shutdown before entering our "no shutdown" sequence
|
||||
// to avoid races.
|
||||
.then(waitOnShutdownObserver)
|
||||
.then(setShutdownObserver(false))
|
||||
.then(checkStateAndUpdate(fetchEvent, "from_scope", "wait"))
|
||||
.then(closeIframe)
|
||||
.then(setShutdownObserver(true))
|
||||
.then(checkStateAndUpdate(messageEvent, "wait", "release"))
|
||||
.then(waitOnShutdownObserver)
|
||||
|
||||
// Push workers were exempt from the old rule and should continue to
|
||||
// survive past the closing of the last controlled window.
|
||||
.then(setShutdownObserver(true))
|
||||
.then(createIframe)
|
||||
// Make sure we are shutdown before entering our "no shutdown" sequence
|
||||
// to avoid races.
|
||||
.then(waitOnShutdownObserver)
|
||||
.then(setShutdownObserver(false))
|
||||
.then(checkStateAndUpdate(pushEvent, "from_scope", "wait"))
|
||||
.then(closeIframe)
|
||||
.then(setShutdownObserver(true))
|
||||
.then(checkStateAndUpdate(messageEvent, "wait", "release"))
|
||||
.then(waitOnShutdownObserver)
|
||||
}
|
||||
};
|
||||
|
||||
var test3 = {
|
||||
prefs: [
|
||||
["dom.serviceWorkers.idle_timeout", 2999999],
|
||||
["dom.serviceWorkers.idle_extended_timeout", 0]
|
||||
],
|
||||
steps: function(ctx) {
|
||||
// set the grace period to 0 and dispatch a message which will reset
|
||||
// the internal sw timer to the new value.
|
||||
var test3_1 = {
|
||||
prefs: [
|
||||
["dom.serviceWorkers.idle_timeout", 0],
|
||||
["dom.serviceWorkers.idle_extended_timeout", 0]
|
||||
],
|
||||
steps: function(ctx) {
|
||||
return new Promise(function(res, rej) {
|
||||
ctx.iframe.contentWindow.fetch("update");
|
||||
res(ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Test that service worker is closed when the extended timeout expired
|
||||
return createIframe(ctx)
|
||||
.then(setShutdownObserver(false))
|
||||
.then(checkStateAndUpdate(messageEvent, "from_scope", "update"))
|
||||
.then(checkStateAndUpdate(messageEventIframe, "update", "update"))
|
||||
.then(checkStateAndUpdate(fetchEvent, "update", "wait"))
|
||||
.then(setShutdownObserver(true))
|
||||
.then(subTest(test3_1)) // This should cause the internal timer to expire.
|
||||
.then(waitOnShutdownObserver)
|
||||
.then(closeIframe)
|
||||
}
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
start()
|
||||
.then(waitForActiveServiceWorker)
|
||||
.then(registerPushNotification)
|
||||
.then(subTest(test1))
|
||||
.then(subTest(test2))
|
||||
.then(subTest(test3))
|
||||
.then(unregisterPushNotification)
|
||||
.then(unregister)
|
||||
.catch(function(e) {
|
||||
ok(false, "Some test failed with error " + e)
|
||||
}).then(SimpleTest.finish);
|
||||
}
|
||||
|
||||
setupPrefsAndMockSocket(mockSocket).then(_ => runTest());
|
||||
SpecialPowers.addPermission('desktop-notification', true, document);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
69
dom/push/test/test_subscription_change.html
Normal file
69
dom/push/test/test_subscription_change.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1205109: Make `pushsubscriptionchange` extendable.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1205109</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205109">Mozilla Bug 1205109</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
var registration;
|
||||
add_task(function* start() {
|
||||
yield setupPrefsAndMockSocket(new MockWebSocket());
|
||||
yield setPushPermission(true);
|
||||
|
||||
var url = "worker.js" + "?" + (Math.random());
|
||||
registration = yield navigator.serviceWorker.register(url, {scope: "."});
|
||||
yield waitForActive(registration);
|
||||
});
|
||||
|
||||
var controlledFrame;
|
||||
add_task(function* createControlledIFrame() {
|
||||
controlledFrame = yield injectControlledFrame();
|
||||
});
|
||||
|
||||
add_task(function* togglePermission() {
|
||||
var subscription = yield registration.pushManager.subscribe();
|
||||
ok(subscription, "Should create a push subscription");
|
||||
|
||||
yield setPushPermission(false);
|
||||
var permissionState = yield registration.pushManager.permissionState();
|
||||
is(permissionState, "denied", "Should deny push permission");
|
||||
|
||||
var subscription = yield registration.pushManager.getSubscription();
|
||||
is(subscription, null, "Should not return subscription when permission is revoked");
|
||||
|
||||
var changePromise = controlledFrame.waitOnWorkerMessage("changed");
|
||||
yield setPushPermission(true);
|
||||
yield changePromise;
|
||||
|
||||
subscription = yield registration.pushManager.getSubscription();
|
||||
is(subscription, null, "Should drop subscription after reinstating permission");
|
||||
});
|
||||
|
||||
add_task(function* unsubscribe() {
|
||||
controlledFrame.remove();
|
||||
yield registration.unregister();
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
305
dom/push/test/test_try_registering_offline_disabled.html
Normal file
305
dom/push/test/test_try_registering_offline_disabled.html
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1150812: Try to register when serviced if offline or connection is disabled.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1150812</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1150812">Mozilla Bug 1150812</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
function debug(str) {
|
||||
// console.log(str + "\n");
|
||||
}
|
||||
|
||||
function registerServiceWorker() {
|
||||
return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
|
||||
.then(swr => waitForActive(swr));
|
||||
}
|
||||
|
||||
function unregister(swr) {
|
||||
return swr.unregister()
|
||||
.then(result => {
|
||||
ok(result, "Unregister should return true.");
|
||||
}, err => {
|
||||
dump("Unregistering the SW failed with " + err + "\n");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function subscribe(swr) {
|
||||
return swr.pushManager.subscribe()
|
||||
.then(sub => {
|
||||
ok(true, "successful registered for push notification");
|
||||
return sub;
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeFail(swr) {
|
||||
return new Promise((res, rej) => {
|
||||
swr.pushManager.subscribe()
|
||||
.then(sub => {
|
||||
ok(false, "successful registered for push notification");
|
||||
throw "Should fail";
|
||||
}, err => {
|
||||
ok(true, "could not register for push notification");
|
||||
res(swr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getEndpointExpectNull(swr) {
|
||||
return swr.pushManager.getSubscription()
|
||||
.then(pushSubscription => {
|
||||
ok(pushSubscription == null, "getEndpoint should return null when app not subscribed.");
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function getEndpoint(swr, subOld) {
|
||||
return swr.pushManager.getSubscription()
|
||||
.then(sub => {
|
||||
ok(subOld.endpoint == sub.endpoint, "getEndpoint - Got the same endpoint back.");
|
||||
return sub;
|
||||
}, err => {
|
||||
ok(false, "could not register for push notification");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Load chrome script to change offline status in the
|
||||
// parent process.
|
||||
var chromeScript = SpecialPowers.loadChromeScript(_ => {
|
||||
var { classes: Cc, interfaces: Ci } = Components;
|
||||
var ioService = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
addMessageListener("change-status", function(offline) {
|
||||
ioService.offline = offline;
|
||||
});
|
||||
});
|
||||
|
||||
function offlineObserver(res) {
|
||||
this._res = res;
|
||||
}
|
||||
offlineObserver.prototype = {
|
||||
_res: null,
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
debug("observe: " + subject + " " + topic + " " + data);
|
||||
if (topic === "network:offline-status-changed") {
|
||||
var obsService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(SpecialPowers.Ci.nsIObserverService);
|
||||
obsService.removeObserver(this, topic);
|
||||
this._res(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeOfflineState(offline) {
|
||||
return new Promise(function(res, rej) {
|
||||
var obsService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
|
||||
.getService(SpecialPowers.Ci.nsIObserverService);
|
||||
obsService.addObserver(SpecialPowers.wrapCallbackObject(new offlineObserver(res)),
|
||||
"network:offline-status-changed",
|
||||
false);
|
||||
chromeScript.sendAsyncMessage("change-status", offline);
|
||||
});
|
||||
}
|
||||
|
||||
function changePushServerConnectionEnabled(enable) {
|
||||
debug("changePushServerConnectionEnabled");
|
||||
SpecialPowers.setBoolPref("dom.push.connection.enabled", enable);
|
||||
}
|
||||
|
||||
function unsubscribe(sub) {
|
||||
return sub.unsubscribe()
|
||||
.then(_ => {ok(true, "Unsubscribed!");});
|
||||
}
|
||||
|
||||
// go offline then go online
|
||||
function runTest1() {
|
||||
return registerServiceWorker()
|
||||
.then(swr =>
|
||||
getEndpointExpectNull(swr)
|
||||
.then(_ => changeOfflineState(true))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changeOfflineState(false))
|
||||
.then(_ => subscribe(swr))
|
||||
.then(sub => getEndpoint(swr, sub)
|
||||
.then(sub => unsubscribe(sub))
|
||||
)
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => unregister(swr))
|
||||
)
|
||||
.catch(err => {
|
||||
ok(false, "Some test failed with error " + err);
|
||||
})
|
||||
}
|
||||
|
||||
// disable - enable push connection.
|
||||
function runTest2() {
|
||||
return registerServiceWorker()
|
||||
.then(swr =>
|
||||
getEndpointExpectNull(swr)
|
||||
.then(_ => changePushServerConnectionEnabled(false))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changePushServerConnectionEnabled(true))
|
||||
.then(_ => subscribe(swr))
|
||||
.then(sub => getEndpoint(swr, sub)
|
||||
.then(sub => unsubscribe(sub))
|
||||
)
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => unregister(swr))
|
||||
)
|
||||
.catch(err => {
|
||||
ok(false, "Some test failed with error " + err);
|
||||
})
|
||||
}
|
||||
|
||||
// go offline - disable - enable - go online
|
||||
function runTest3() {
|
||||
return registerServiceWorker()
|
||||
.then(swr =>
|
||||
getEndpointExpectNull(swr)
|
||||
.then(_ => changeOfflineState(true))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changePushServerConnectionEnabled(false))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changePushServerConnectionEnabled(true))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changeOfflineState(false))
|
||||
.then(_ => subscribe(swr))
|
||||
.then(sub => getEndpoint(swr, sub)
|
||||
.then(sub => unsubscribe(sub))
|
||||
)
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => unregister(swr))
|
||||
)
|
||||
.catch(err => {
|
||||
ok(false, "Some test failed with error " + err);
|
||||
})
|
||||
}
|
||||
|
||||
// disable - offline - online - enable.
|
||||
function runTest4() {
|
||||
return registerServiceWorker()
|
||||
.then(swr =>
|
||||
getEndpointExpectNull(swr)
|
||||
.then(_ => changePushServerConnectionEnabled(false))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changeOfflineState(true))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changeOfflineState(false))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changePushServerConnectionEnabled(true))
|
||||
.then(_ => subscribe(swr))
|
||||
.then(sub => getEndpoint(swr, sub)
|
||||
.then(sub => unsubscribe(sub))
|
||||
)
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => unregister(swr))
|
||||
)
|
||||
.catch(err => {
|
||||
ok(false, "Some test failed with error " + err);
|
||||
})
|
||||
}
|
||||
|
||||
// go offline - disable - go online - enable
|
||||
function runTest5() {
|
||||
return registerServiceWorker()
|
||||
.then(swr =>
|
||||
getEndpointExpectNull(swr)
|
||||
.then(_ => changeOfflineState(true))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changePushServerConnectionEnabled(false))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changeOfflineState(false))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changePushServerConnectionEnabled(true))
|
||||
.then(_ => subscribe(swr))
|
||||
.then(sub => getEndpoint(swr, sub)
|
||||
.then(sub => unsubscribe(sub))
|
||||
)
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => unregister(swr))
|
||||
)
|
||||
.catch(err => {
|
||||
ok(false, "Some test failed with error " + err);
|
||||
})
|
||||
}
|
||||
|
||||
// disable - go offline - enable - go online.
|
||||
function runTest6() {
|
||||
return registerServiceWorker()
|
||||
.then(swr =>
|
||||
getEndpointExpectNull(swr)
|
||||
.then(_ => changePushServerConnectionEnabled(false))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changeOfflineState(true))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changePushServerConnectionEnabled(true))
|
||||
.then(_ => subscribeFail(swr))
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => changeOfflineState(false))
|
||||
.then(_ => subscribe(swr))
|
||||
.then(sub => getEndpoint(swr, sub)
|
||||
.then(sub => unsubscribe(sub))
|
||||
)
|
||||
.then(_ => getEndpointExpectNull(swr))
|
||||
.then(_ => unregister(swr))
|
||||
)
|
||||
.catch(err => {
|
||||
ok(false, "Some test failed with error " + err);
|
||||
})
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
runTest1()
|
||||
.then(_ => runTest2())
|
||||
.then(_ => runTest3())
|
||||
.then(_ => runTest4())
|
||||
.then(_ => runTest5())
|
||||
.then(_ => runTest6())
|
||||
.then(SimpleTest.finish);
|
||||
}
|
||||
|
||||
setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
|
||||
SpecialPowers.addPermission("desktop-notification", true, document);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
81
dom/push/test/test_unregister.html
Normal file
81
dom/push/test/test_unregister.html
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1170817: Push tests.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1170817</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1170817">Mozilla Bug 1170817</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
function generateURL() {
|
||||
return "worker.js" + "?" + (Math.random());
|
||||
}
|
||||
|
||||
var registration;
|
||||
add_task(function* start() {
|
||||
yield setupPrefsAndMockSocket(new MockWebSocket());
|
||||
yield setPushPermission(true);
|
||||
|
||||
registration = yield navigator.serviceWorker.register(
|
||||
generateURL(), {scope: "."});
|
||||
yield waitForActive(registration);
|
||||
});
|
||||
|
||||
var pushSubscription;
|
||||
add_task(function* setupPushNotification() {
|
||||
pushSubscription = yield registration.pushManager.subscribe();
|
||||
ok(pushSubscription, "successful registered for push notification");
|
||||
});
|
||||
|
||||
add_task(function* unregisterPushNotification() {
|
||||
var result = yield pushSubscription.unsubscribe();
|
||||
ok(result, "unsubscribe() on existing subscription should return true.");
|
||||
});
|
||||
|
||||
add_task(function* unregisterAgain() {
|
||||
var result = yield pushSubscription.unsubscribe();
|
||||
ok(!result, "unsubscribe() on previously unsubscribed subscription should return false.");
|
||||
});
|
||||
|
||||
add_task(function* subscribeAgain() {
|
||||
pushSubscription = yield registration.pushManager.subscribe();
|
||||
ok(pushSubscription, "Should create a new push subscription");
|
||||
|
||||
var result = yield registration.unregister();
|
||||
ok(result, "Should unregister the service worker");
|
||||
|
||||
registration = yield navigator.serviceWorker.register(
|
||||
generateURL(), {scope: "."});
|
||||
yield waitForActive(registration);
|
||||
var pushSubscription = yield registration.pushManager.getSubscription();
|
||||
ok(!pushSubscription,
|
||||
"Unregistering a service worker should drop its subscription");
|
||||
});
|
||||
|
||||
add_task(function* unregister() {
|
||||
var result = yield registration.unregister();
|
||||
ok(result, "Unregister should return true.");
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
245
dom/push/test/test_utils.js
Normal file
245
dom/push/test/test_utils.js
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
(function (g) {
|
||||
"use strict";
|
||||
|
||||
let url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
|
||||
let chromeScript = SpecialPowers.loadChromeScript(url);
|
||||
|
||||
/**
|
||||
* Replaces `PushService.jsm` with a mock implementation that handles requests
|
||||
* from the DOM API. This allows tests to simulate local errors and error
|
||||
* reporting, bypassing the `PushService.jsm` machinery.
|
||||
*/
|
||||
function replacePushService(mockService) {
|
||||
chromeScript.sendSyncMessage("service-replace");
|
||||
chromeScript.addMessageListener("service-delivery-error", function(msg) {
|
||||
mockService.reportDeliveryError(msg.messageId, msg.reason);
|
||||
});
|
||||
chromeScript.addMessageListener("service-request", function(msg) {
|
||||
let promise;
|
||||
try {
|
||||
let handler = mockService[msg.name];
|
||||
promise = Promise.resolve(handler(msg.params));
|
||||
} catch (error) {
|
||||
promise = Promise.reject(error);
|
||||
}
|
||||
promise.then(result => {
|
||||
chromeScript.sendAsyncMessage("service-response", {
|
||||
id: msg.id,
|
||||
result: result,
|
||||
});
|
||||
}, error => {
|
||||
chromeScript.sendAsyncMessage("service-response", {
|
||||
id: msg.id,
|
||||
error: error,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restorePushService() {
|
||||
chromeScript.sendSyncMessage("service-restore");
|
||||
}
|
||||
|
||||
let userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
|
||||
|
||||
let currentMockSocket = null;
|
||||
|
||||
/**
|
||||
* Sets up a mock connection for the WebSocket backend. This only replaces
|
||||
* the transport layer; `PushService.jsm` still handles DOM API requests,
|
||||
* observes permission changes, writes to IndexedDB, and notifies service
|
||||
* workers of incoming push messages.
|
||||
*/
|
||||
function setupMockPushSocket(mockWebSocket) {
|
||||
currentMockSocket = mockWebSocket;
|
||||
currentMockSocket._isActive = true;
|
||||
chromeScript.sendSyncMessage("socket-setup");
|
||||
chromeScript.addMessageListener("socket-client-msg", function(msg) {
|
||||
mockWebSocket.handleMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
function teardownMockPushSocket() {
|
||||
if (currentMockSocket) {
|
||||
return new Promise(resolve => {
|
||||
currentMockSocket._isActive = false;
|
||||
chromeScript.addMessageListener("socket-server-teardown", resolve);
|
||||
chromeScript.sendSyncMessage("socket-teardown");
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal implementation of web sockets for use in testing. Forwards
|
||||
* messages to a mock web socket in the parent process that is used
|
||||
* by the push service.
|
||||
*/
|
||||
function MockWebSocket() {}
|
||||
|
||||
let registerCount = 0;
|
||||
|
||||
// Default implementation to make the push server work minimally.
|
||||
// Override methods to implement custom functionality.
|
||||
MockWebSocket.prototype = {
|
||||
// We only allow one active mock web socket to talk to the parent.
|
||||
// This flag is used to keep track of which mock web socket is active.
|
||||
_isActive: false,
|
||||
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: "hello",
|
||||
uaid: userAgentID,
|
||||
status: 200,
|
||||
use_webpush: true,
|
||||
}));
|
||||
},
|
||||
|
||||
onRegister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: "register",
|
||||
uaid: userAgentID,
|
||||
channelID: request.channelID,
|
||||
status: 200,
|
||||
pushEndpoint: "https://example.com/endpoint/" + registerCount++
|
||||
}));
|
||||
},
|
||||
|
||||
onUnregister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: "unregister",
|
||||
channelID: request.channelID,
|
||||
status: 200,
|
||||
}));
|
||||
},
|
||||
|
||||
onAck(request) {
|
||||
// Do nothing.
|
||||
},
|
||||
|
||||
handleMessage(msg) {
|
||||
let request = JSON.parse(msg);
|
||||
let messageType = request.messageType;
|
||||
switch (messageType) {
|
||||
case "hello":
|
||||
this.onHello(request);
|
||||
break;
|
||||
case "register":
|
||||
this.onRegister(request);
|
||||
break;
|
||||
case "unregister":
|
||||
this.onUnregister(request);
|
||||
break;
|
||||
case "ack":
|
||||
this.onAck(request);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unexpected message: " + messageType);
|
||||
}
|
||||
},
|
||||
|
||||
serverSendMsg(msg) {
|
||||
if (this._isActive) {
|
||||
chromeScript.sendAsyncMessage("socket-server-msg", msg);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
g.MockWebSocket = MockWebSocket;
|
||||
g.setupMockPushSocket = setupMockPushSocket;
|
||||
g.teardownMockPushSocket = teardownMockPushSocket;
|
||||
g.replacePushService = replacePushService;
|
||||
g.restorePushService = restorePushService;
|
||||
}(this));
|
||||
|
||||
// Remove permissions and prefs when the test finishes.
|
||||
SimpleTest.registerCleanupFunction(() => {
|
||||
return new Promise(resolve =>
|
||||
SpecialPowers.flushPermissions(resolve)
|
||||
).then(_ => SpecialPowers.flushPrefEnv()).then(_ => {
|
||||
restorePushService();
|
||||
return teardownMockPushSocket();
|
||||
});
|
||||
});
|
||||
|
||||
function setPushPermission(allow) {
|
||||
return new Promise(resolve => {
|
||||
SpecialPowers.pushPermissions([
|
||||
{ type: "desktop-notification", allow, context: document },
|
||||
], resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function setupPrefs() {
|
||||
return SpecialPowers.pushPrefEnv({"set": [
|
||||
["dom.push.enabled", true],
|
||||
["dom.push.connection.enabled", true],
|
||||
["dom.push.maxRecentMessageIDsPerSubscription", 0],
|
||||
["dom.serviceWorkers.exemptFromPerDomainMax", true],
|
||||
["dom.serviceWorkers.enabled", true],
|
||||
["dom.serviceWorkers.testing.enabled", true]
|
||||
]});
|
||||
}
|
||||
|
||||
function setupPrefsAndReplaceService(mockService) {
|
||||
replacePushService(mockService);
|
||||
return setupPrefs();
|
||||
}
|
||||
|
||||
function setupPrefsAndMockSocket(mockSocket) {
|
||||
setupMockPushSocket(mockSocket);
|
||||
return setupPrefs();
|
||||
}
|
||||
|
||||
function injectControlledFrame(target = document.body) {
|
||||
return new Promise(function(res, rej) {
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.src = "/tests/dom/push/test/frame.html";
|
||||
|
||||
var controlledFrame = {
|
||||
remove() {
|
||||
target.removeChild(iframe);
|
||||
iframe = null;
|
||||
},
|
||||
waitOnWorkerMessage(type) {
|
||||
return iframe ? iframe.contentWindow.waitOnWorkerMessage(type) :
|
||||
Promise.reject(new Error("Frame removed from document"));
|
||||
},
|
||||
innerWindowId() {
|
||||
var utils = SpecialPowers.getDOMWindowUtils(iframe.contentWindow);
|
||||
return utils.currentInnerWindowID;
|
||||
},
|
||||
};
|
||||
|
||||
iframe.onload = () => res(controlledFrame);
|
||||
target.appendChild(iframe);
|
||||
});
|
||||
}
|
||||
|
||||
function sendRequestToWorker(request) {
|
||||
return navigator.serviceWorker.ready.then(registration => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var channel = new MessageChannel();
|
||||
channel.port1.onmessage = e => {
|
||||
(e.data.error ? reject : resolve)(e.data);
|
||||
};
|
||||
registration.active.postMessage(request, [channel.port2]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function waitForActive(swr) {
|
||||
let sw = swr.installing || swr.waiting || swr.active;
|
||||
return new Promise(resolve => {
|
||||
if (sw.state === 'activated') {
|
||||
resolve(swr);
|
||||
return;
|
||||
}
|
||||
sw.addEventListener('statechange', function onStateChange(evt) {
|
||||
if (sw.state === 'activated') {
|
||||
sw.removeEventListener('statechange', onStateChange);
|
||||
resolve(swr);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
186
dom/push/test/webpush.js
Normal file
186
dom/push/test/webpush.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Browser-based Web Push client for the application server piece.
|
||||
*
|
||||
* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/licenses/publicdomain/
|
||||
*
|
||||
* Uses the WebCrypto API.
|
||||
* Uses the fetch API. Polyfill: https://github.com/github/fetch
|
||||
*/
|
||||
|
||||
(function (g) {
|
||||
'use strict';
|
||||
|
||||
var P256DH = {
|
||||
name: 'ECDH',
|
||||
namedCurve: 'P-256'
|
||||
};
|
||||
var webCrypto = g.crypto.subtle;
|
||||
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;
|
||||
}
|
||||
|
||||
/* I can't believe that this is needed here, in this day and age ...
|
||||
* Note: these are not efficient, merely expedient.
|
||||
*/
|
||||
var base64url = {
|
||||
_strmap: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
|
||||
encode: function(data) {
|
||||
data = new Uint8Array(data);
|
||||
var len = Math.ceil(data.length * 4 / 3);
|
||||
return chunkArray(data, 3).map(chunk => [
|
||||
chunk[0] >>> 2,
|
||||
((chunk[0] & 0x3) << 4) | (chunk[1] >>> 4),
|
||||
((chunk[1] & 0xf) << 2) | (chunk[2] >>> 6),
|
||||
chunk[2] & 0x3f
|
||||
].map(v => base64url._strmap[v]).join('')).join('').slice(0, len);
|
||||
},
|
||||
_lookup: function(s, i) {
|
||||
return base64url._strmap.indexOf(s.charAt(i));
|
||||
},
|
||||
decode: function(str) {
|
||||
var v = new Uint8Array(Math.floor(str.length * 3 / 4));
|
||||
var vi = 0;
|
||||
for (var si = 0; si < str.length;) {
|
||||
var w = base64url._lookup(str, si++);
|
||||
var x = base64url._lookup(str, si++);
|
||||
var y = base64url._lookup(str, si++);
|
||||
var z = base64url._lookup(str, si++);
|
||||
v[vi++] = w << 2 | x >>> 4;
|
||||
v[vi++] = x << 4 | y >>> 2;
|
||||
v[vi++] = y << 6 | z;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
g.base64url = base64url;
|
||||
|
||||
/* Coerces data into a Uint8Array */
|
||||
function ensureView(data) {
|
||||
if (typeof data === 'string') {
|
||||
return new TextEncoder('utf-8').encode(data);
|
||||
}
|
||||
if (data instanceof ArrayBuffer) {
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
if (ArrayBuffer.isView(data)) {
|
||||
return new Uint8Array(data.buffer);
|
||||
}
|
||||
throw new Error('webpush() needs a string or BufferSource');
|
||||
}
|
||||
|
||||
function bsConcat(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));
|
||||
}
|
||||
|
||||
function hmac(key) {
|
||||
this.keyPromise = webCrypto.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' },
|
||||
false, ['sign']);
|
||||
}
|
||||
hmac.prototype.hash = function(input) {
|
||||
return this.keyPromise.then(k => webCrypto.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 = bsConcat([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) {
|
||||
var nonce = base.slice(0, 12);
|
||||
for (var i = 0; i < 6; ++i) {
|
||||
nonce[nonce.length - 1 - i] ^= (index / Math.pow(256, i)) & 0xff;
|
||||
}
|
||||
return nonce;
|
||||
}
|
||||
|
||||
function encrypt(localKey, remoteShare, salt, data) {
|
||||
return webCrypto.importKey('raw', remoteShare, P256DH, false, ['deriveBits'])
|
||||
.then(remoteKey =>
|
||||
webCrypto.deriveBits({ name: P256DH.name, public: remoteKey },
|
||||
localKey, 256))
|
||||
.then(rawKey => {
|
||||
var kdf = new hkdf(salt, rawKey);
|
||||
return Promise.all([
|
||||
kdf.generate(ENCRYPT_INFO, 16)
|
||||
.then(gcmBits =>
|
||||
webCrypto.importKey('raw', gcmBits, 'AES-GCM', false, ['encrypt'])),
|
||||
kdf.generate(NONCE_INFO, 12)
|
||||
]);
|
||||
})
|
||||
.then(([key, nonce]) => {
|
||||
if (data.byteLength === 0) {
|
||||
// Send an authentication tag for empty messages.
|
||||
return webCrypto.encrypt({
|
||||
name: 'AES-GCM',
|
||||
iv: generateNonce(nonce, 0)
|
||||
}, key, new Uint8Array([0])).then(value => [value]);
|
||||
}
|
||||
// 4096 is the default size, though we burn 1 for padding
|
||||
return Promise.all(chunkArray(data, 4095).map((slice, index) => {
|
||||
var padded = bsConcat([new Uint8Array([0]), slice]);
|
||||
return webCrypto.encrypt({
|
||||
name: 'AES-GCM',
|
||||
iv: generateNonce(nonce, index)
|
||||
}, key, padded);
|
||||
}));
|
||||
}).then(bsConcat);
|
||||
}
|
||||
|
||||
function webPushEncrypt(subscription, data) {
|
||||
data = ensureView(data);
|
||||
|
||||
var salt = g.crypto.getRandomValues(new Uint8Array(16));
|
||||
return webCrypto.generateKey(P256DH, false, ['deriveBits'])
|
||||
.then(localKey => {
|
||||
return Promise.all([
|
||||
encrypt(localKey.privateKey, subscription.getKey("p256dh"), salt, data),
|
||||
// 1337 p-256 specific haxx to get the raw value out of the spki value
|
||||
webCrypto.exportKey('raw', localKey.publicKey),
|
||||
]);
|
||||
}).then(([payload, pubkey]) => {
|
||||
return {
|
||||
data: base64url.encode(payload),
|
||||
encryption: 'keyid=p256dh;salt=' + base64url.encode(salt),
|
||||
encryption_key: 'keyid=p256dh;dh=' + base64url.encode(pubkey),
|
||||
encoding: 'aesgcm128'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
g.webPushEncrypt = webPushEncrypt;
|
||||
}(this));
|
||||
152
dom/push/test/worker.js
Normal file
152
dom/push/test/worker.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// Any copyright is dedicated to the Public Domain.
|
||||
// http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
// This worker is used for two types of tests. `handlePush` sends messages to
|
||||
// `frame.html`, which verifies that the worker can receive push messages.
|
||||
|
||||
// `handleMessage` receives messages from `test_push_manager_worker.html`
|
||||
// and `test_data.html`, and verifies that `PushManager` can be used from
|
||||
// the worker.
|
||||
|
||||
this.onpush = handlePush;
|
||||
this.onmessage = handleMessage;
|
||||
this.onpushsubscriptionchange = handlePushSubscriptionChange;
|
||||
|
||||
function getJSON(data) {
|
||||
var result = {
|
||||
ok: false,
|
||||
};
|
||||
try {
|
||||
result.value = data.json();
|
||||
result.ok = true;
|
||||
} catch (e) {
|
||||
// Ignore syntax errors for invalid JSON.
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function assert(value, message) {
|
||||
if (!value) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function broadcast(event, promise) {
|
||||
event.waitUntil(Promise.resolve(promise).then(message => {
|
||||
return self.clients.matchAll().then(clients => {
|
||||
clients.forEach(client => client.postMessage(message));
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function reply(event, promise) {
|
||||
event.waitUntil(Promise.resolve(promise).then(result => {
|
||||
event.ports[0].postMessage(result);
|
||||
}).catch(error => {
|
||||
event.ports[0].postMessage({
|
||||
error: String(error),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function handlePush(event) {
|
||||
if (event instanceof PushEvent) {
|
||||
if (!('data' in event)) {
|
||||
broadcast(event, {type: "finished", okay: "yes"});
|
||||
return;
|
||||
}
|
||||
var message = {
|
||||
type: "finished",
|
||||
okay: "yes",
|
||||
};
|
||||
if (event.data) {
|
||||
message.data = {
|
||||
text: event.data.text(),
|
||||
arrayBuffer: event.data.arrayBuffer(),
|
||||
json: getJSON(event.data),
|
||||
blob: event.data.blob(),
|
||||
};
|
||||
}
|
||||
broadcast(event, message);
|
||||
return;
|
||||
}
|
||||
broadcast(event, {type: "finished", okay: "no"});
|
||||
}
|
||||
|
||||
var testHandlers = {
|
||||
publicKey(data) {
|
||||
return self.registration.pushManager.getSubscription().then(
|
||||
subscription => ({
|
||||
p256dh: subscription.getKey("p256dh"),
|
||||
auth: subscription.getKey("auth"),
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
resubscribe(data) {
|
||||
return self.registration.pushManager.getSubscription().then(
|
||||
subscription => {
|
||||
assert(subscription.endpoint == data.endpoint,
|
||||
"Wrong push endpoint in worker");
|
||||
return subscription.unsubscribe();
|
||||
}
|
||||
).then(result => {
|
||||
assert(result, "Error unsubscribing in worker");
|
||||
return self.registration.pushManager.getSubscription();
|
||||
}).then(subscription => {
|
||||
assert(!subscription, "Subscription not removed in worker");
|
||||
return self.registration.pushManager.subscribe();
|
||||
}).then(subscription => {
|
||||
return {
|
||||
endpoint: subscription.endpoint,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
denySubscribe(data) {
|
||||
return self.registration.pushManager.getSubscription().then(
|
||||
subscription => {
|
||||
assert(!subscription,
|
||||
"Should not return worker subscription with revoked permission");
|
||||
return self.registration.pushManager.subscribe().then(_ => {
|
||||
assert(false, "Expected error subscribing with revoked permission");
|
||||
}, error => {
|
||||
return {
|
||||
isDOMException: error instanceof DOMException,
|
||||
name: error.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
subscribeWithKey(data) {
|
||||
return self.registration.pushManager.subscribe({
|
||||
applicationServerKey: data.key,
|
||||
}).then(subscription => {
|
||||
return {
|
||||
endpoint: subscription.endpoint,
|
||||
key: subscription.options.applicationServerKey,
|
||||
};
|
||||
}, error => {
|
||||
return {
|
||||
isDOMException: error instanceof DOMException,
|
||||
name: error.name,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function handleMessage(event) {
|
||||
var handler = testHandlers[event.data.type];
|
||||
if (handler) {
|
||||
reply(event, handler(event.data));
|
||||
} else {
|
||||
reply(event, Promise.reject(
|
||||
"Invalid message type: " + event.data.type));
|
||||
}
|
||||
}
|
||||
|
||||
function handlePushSubscriptionChange(event) {
|
||||
broadcast(event, {type: "changed", okay: "yes"});
|
||||
}
|
||||
31
dom/push/test/xpcshell/PushServiceHandler.js
Normal file
31
dom/push/test/xpcshell/PushServiceHandler.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// An XPCOM service that's registered with the category manager in the parent
|
||||
// process for handling push notifications with scope "chrome://test-scope"
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
let pushService = Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService);
|
||||
|
||||
function PushServiceHandler() {
|
||||
// So JS code can reach into us.
|
||||
this.wrappedJSObject = this;
|
||||
// Register a push observer.
|
||||
this.observed = [];
|
||||
Services.obs.addObserver(this, pushService.pushTopic, false);
|
||||
Services.obs.addObserver(this, pushService.subscriptionChangeTopic, false);
|
||||
Services.obs.addObserver(this, pushService.subscriptionModifiedTopic, false);
|
||||
}
|
||||
|
||||
PushServiceHandler.prototype = {
|
||||
classID: Components.ID("{bb7c5199-c0f7-4976-9f6d-1306e32c5591}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([]),
|
||||
|
||||
observe(subject, topic, data) {
|
||||
this.observed.push({ subject, topic, data });
|
||||
},
|
||||
}
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PushServiceHandler]);
|
||||
4
dom/push/test/xpcshell/PushServiceHandler.manifest
Normal file
4
dom/push/test/xpcshell/PushServiceHandler.manifest
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
component {bb7c5199-c0f7-4976-9f6d-1306e32c5591} PushServiceHandler.js
|
||||
contract @mozilla.org/dom/push/test/PushServiceHandler;1 {bb7c5199-c0f7-4976-9f6d-1306e32c5591}
|
||||
|
||||
category push chrome://test-scope @mozilla.org/dom/push/test/PushServiceHandler;1
|
||||
62
dom/push/test/xpcshell/head-http2.js
Normal file
62
dom/push/test/xpcshell/head-http2.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Returns the test H/2 server port, throwing if it's missing or invalid.
|
||||
function getTestServerPort() {
|
||||
let portEnv = Cc["@mozilla.org/process/environment;1"]
|
||||
.getService(Ci.nsIEnvironment).get("MOZHTTP2_PORT");
|
||||
let port = parseInt(portEnv, 10);
|
||||
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
||||
throw new Error(`Invalid port in MOZHTTP2_PORT env var: ${portEnv}`);
|
||||
}
|
||||
do_print(`Using HTTP/2 server on port ${port}`);
|
||||
return port;
|
||||
}
|
||||
|
||||
// Support for making sure we can talk to the invalid cert the server presents
|
||||
var CertOverrideListener = function(host, port, bits) {
|
||||
this.host = host;
|
||||
this.port = port || 443;
|
||||
this.bits = bits;
|
||||
};
|
||||
|
||||
CertOverrideListener.prototype = {
|
||||
host: null,
|
||||
bits: null,
|
||||
|
||||
getInterface: function(aIID) {
|
||||
return this.QueryInterface(aIID);
|
||||
},
|
||||
|
||||
QueryInterface: function(aIID) {
|
||||
if (aIID.equals(Ci.nsIBadCertListener2) ||
|
||||
aIID.equals(Ci.nsIInterfaceRequestor) ||
|
||||
aIID.equals(Ci.nsISupports))
|
||||
return this;
|
||||
throw Components.results.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
|
||||
notifyCertProblem: function(socketInfo, sslStatus, targetHost) {
|
||||
var cert = sslStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
|
||||
var cos = Cc["@mozilla.org/security/certoverride;1"].
|
||||
getService(Ci.nsICertOverrideService);
|
||||
cos.rememberValidityOverride(this.host, this.port, cert, this.bits, false);
|
||||
dump("Certificate Override in place\n");
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
function addCertOverride(host, port, bits) {
|
||||
var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
||||
.createInstance(Ci.nsIXMLHttpRequest);
|
||||
try {
|
||||
var url;
|
||||
if (port && (port > 0) && (port !== 443)) {
|
||||
url = "https://" + host + ":" + port + "/";
|
||||
} else {
|
||||
url = "https://" + host + "/";
|
||||
}
|
||||
req.open("GET", url, false);
|
||||
req.channel.notificationCallbacks = new CertOverrideListener(host, port, bits);
|
||||
req.send(null);
|
||||
} catch (e) {
|
||||
// This will fail since the server is not trusted yet
|
||||
}
|
||||
}
|
||||
463
dom/push/test/xpcshell/head.js
Normal file
463
dom/push/test/xpcshell/head.js
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
|
||||
Cu.import('resource://gre/modules/Services.jsm');
|
||||
Cu.import('resource://gre/modules/Task.jsm');
|
||||
Cu.import('resource://gre/modules/Timer.jsm');
|
||||
Cu.import('resource://gre/modules/Promise.jsm');
|
||||
Cu.import('resource://gre/modules/Preferences.jsm');
|
||||
Cu.import('resource://gre/modules/PlacesUtils.jsm');
|
||||
Cu.import('resource://gre/modules/ObjectUtils.jsm');
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, 'PlacesTestUtils',
|
||||
'resource://testing-common/PlacesTestUtils.jsm');
|
||||
XPCOMUtils.defineLazyServiceGetter(this, 'PushServiceComponent',
|
||||
'@mozilla.org/push/Service;1', 'nsIPushService');
|
||||
|
||||
const serviceExports = Cu.import('resource://gre/modules/PushService.jsm', {});
|
||||
const servicePrefs = new Preferences('dom.push.');
|
||||
|
||||
const WEBSOCKET_CLOSE_GOING_AWAY = 1001;
|
||||
|
||||
const MS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
var isParent = Cc['@mozilla.org/xre/runtime;1']
|
||||
.getService(Ci.nsIXULRuntime).processType ==
|
||||
Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
||||
|
||||
// Stop and clean up after the PushService.
|
||||
Services.obs.addObserver(function observe(subject, topic, data) {
|
||||
Services.obs.removeObserver(observe, topic, false);
|
||||
serviceExports.PushService.uninit();
|
||||
// Occasionally, `profile-change-teardown` and `xpcom-shutdown` will fire
|
||||
// before the PushService and AlarmService finish writing to IndexedDB. This
|
||||
// causes spurious errors and crashes, so we spin the event loop to let the
|
||||
// writes finish.
|
||||
let done = false;
|
||||
setTimeout(() => done = true, 1000);
|
||||
let thread = Services.tm.mainThread;
|
||||
while (!done) {
|
||||
try {
|
||||
thread.processNextEvent(true);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
}, 'profile-change-net-teardown', false);
|
||||
|
||||
/**
|
||||
* Gates a function so that it is called only after the wrapper is called a
|
||||
* given number of times.
|
||||
*
|
||||
* @param {Number} times The number of wrapper calls before |func| is called.
|
||||
* @param {Function} func The function to gate.
|
||||
* @returns {Function} The gated function wrapper.
|
||||
*/
|
||||
function after(times, func) {
|
||||
return function afterFunc() {
|
||||
if (--times <= 0) {
|
||||
return func.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers one or more callbacks until the next turn of the event loop. Multiple
|
||||
* callbacks are executed in order.
|
||||
*
|
||||
* @param {Function[]} callbacks The callbacks to execute. One callback will be
|
||||
* executed per tick.
|
||||
*/
|
||||
function waterfall(...callbacks) {
|
||||
callbacks.reduce((promise, callback) => promise.then(() => {
|
||||
callback();
|
||||
}), Promise.resolve()).catch(Cu.reportError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for an observer notification to fire.
|
||||
*
|
||||
* @param {String} topic The notification topic.
|
||||
* @returns {Promise} A promise that fulfills when the notification is fired.
|
||||
*/
|
||||
function promiseObserverNotification(topic, matchFunc) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Services.obs.addObserver(function observe(subject, topic, data) {
|
||||
let matches = typeof matchFunc != 'function' || matchFunc(subject, data);
|
||||
if (!matches) {
|
||||
return;
|
||||
}
|
||||
Services.obs.removeObserver(observe, topic, false);
|
||||
resolve({subject, data});
|
||||
}, topic, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an object in a proxy that traps property gets and returns stubs. If
|
||||
* the stub is a function, the original value will be passed as the first
|
||||
* argument. If the original value is a function, the proxy returns a wrapper
|
||||
* that calls the stub; otherwise, the stub is called as a getter.
|
||||
*
|
||||
* @param {Object} target The object to wrap.
|
||||
* @param {Object} stubs An object containing stubbed values and functions.
|
||||
* @returns {Proxy} A proxy that returns stubs for property gets.
|
||||
*/
|
||||
function makeStub(target, stubs) {
|
||||
return new Proxy(target, {
|
||||
get(target, property) {
|
||||
if (!stubs || typeof stubs != 'object' || !(property in stubs)) {
|
||||
return target[property];
|
||||
}
|
||||
let stub = stubs[property];
|
||||
if (typeof stub != 'function') {
|
||||
return stub;
|
||||
}
|
||||
let original = target[property];
|
||||
if (typeof original != 'function') {
|
||||
return stub.call(this, original);
|
||||
}
|
||||
return function callStub(...params) {
|
||||
return stub.call(this, original, ...params);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default PushService preferences. All pref names are prefixed with
|
||||
* `dom.push.`; any additional preferences will override the defaults.
|
||||
*
|
||||
* @param {Object} [prefs] Additional preferences to set.
|
||||
*/
|
||||
function setPrefs(prefs = {}) {
|
||||
let defaultPrefs = Object.assign({
|
||||
loglevel: 'all',
|
||||
serverURL: 'wss://push.example.org',
|
||||
'connection.enabled': true,
|
||||
userAgentID: '',
|
||||
enabled: true,
|
||||
// Defaults taken from /modules/libpref/init/all.js.
|
||||
requestTimeout: 10000,
|
||||
retryBaseInterval: 5000,
|
||||
pingInterval: 30 * 60 * 1000,
|
||||
// Misc. defaults.
|
||||
'http2.maxRetries': 2,
|
||||
'http2.retryInterval': 500,
|
||||
'http2.reset_retry_count_after_ms': 60000,
|
||||
maxQuotaPerSubscription: 16,
|
||||
quotaUpdateDelay: 3000,
|
||||
'testing.notifyWorkers': false,
|
||||
}, prefs);
|
||||
for (let pref in defaultPrefs) {
|
||||
servicePrefs.set(pref, defaultPrefs[pref]);
|
||||
}
|
||||
}
|
||||
|
||||
function compareAscending(a, b) {
|
||||
return a > b ? 1 : a < b ? -1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock WebSocket object that implements a subset of the
|
||||
* nsIWebSocketChannel interface used by the PushService.
|
||||
*
|
||||
* The given protocol handlers are invoked for each Simple Push command sent
|
||||
* by the PushService. The ping handler is optional; all others will throw if
|
||||
* the PushService sends a command for which no handler is registered.
|
||||
*
|
||||
* All nsIWebSocketListener methods will be called asynchronously.
|
||||
* serverSendMsg() and serverClose() can be used to respond to client messages
|
||||
* and close the "server" end of the connection, respectively.
|
||||
*
|
||||
* @param {nsIURI} originalURI The original WebSocket URL.
|
||||
* @param {Function} options.onHello The "hello" handshake command handler.
|
||||
* @param {Function} options.onRegister The "register" command handler.
|
||||
* @param {Function} options.onUnregister The "unregister" command handler.
|
||||
* @param {Function} options.onACK The "ack" command handler.
|
||||
* @param {Function} [options.onPing] An optional ping handler.
|
||||
*/
|
||||
function MockWebSocket(originalURI, handlers = {}) {
|
||||
this._originalURI = originalURI;
|
||||
this._onHello = handlers.onHello;
|
||||
this._onRegister = handlers.onRegister;
|
||||
this._onUnregister = handlers.onUnregister;
|
||||
this._onACK = handlers.onACK;
|
||||
this._onPing = handlers.onPing;
|
||||
}
|
||||
|
||||
MockWebSocket.prototype = {
|
||||
_originalURI: null,
|
||||
_onHello: null,
|
||||
_onRegister: null,
|
||||
_onUnregister: null,
|
||||
_onACK: null,
|
||||
_onPing: null,
|
||||
|
||||
_listener: null,
|
||||
_context: null,
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsISupports,
|
||||
Ci.nsIWebSocketChannel
|
||||
]),
|
||||
|
||||
get originalURI() {
|
||||
return this._originalURI;
|
||||
},
|
||||
|
||||
asyncOpen(uri, origin, windowId, listener, context) {
|
||||
this._listener = listener;
|
||||
this._context = context;
|
||||
waterfall(() => this._listener.onStart(this._context));
|
||||
},
|
||||
|
||||
_handleMessage(msg) {
|
||||
let messageType, request;
|
||||
if (msg == '{}') {
|
||||
request = {};
|
||||
messageType = 'ping';
|
||||
} else {
|
||||
request = JSON.parse(msg);
|
||||
messageType = request.messageType;
|
||||
}
|
||||
switch (messageType) {
|
||||
case 'hello':
|
||||
if (typeof this._onHello != 'function') {
|
||||
throw new Error('Unexpected handshake request');
|
||||
}
|
||||
this._onHello(request);
|
||||
break;
|
||||
|
||||
case 'register':
|
||||
if (typeof this._onRegister != 'function') {
|
||||
throw new Error('Unexpected register request');
|
||||
}
|
||||
this._onRegister(request);
|
||||
break;
|
||||
|
||||
case 'unregister':
|
||||
if (typeof this._onUnregister != 'function') {
|
||||
throw new Error('Unexpected unregister request');
|
||||
}
|
||||
this._onUnregister(request);
|
||||
break;
|
||||
|
||||
case 'ack':
|
||||
if (typeof this._onACK != 'function') {
|
||||
throw new Error('Unexpected acknowledgement');
|
||||
}
|
||||
this._onACK(request);
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
if (typeof this._onPing == 'function') {
|
||||
this._onPing(request);
|
||||
} else {
|
||||
// Echo ping packets.
|
||||
this.serverSendMsg('{}');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Unexpected message: ' + messageType);
|
||||
}
|
||||
},
|
||||
|
||||
sendMsg(msg) {
|
||||
this._handleMessage(msg);
|
||||
},
|
||||
|
||||
close(code, reason) {
|
||||
waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
|
||||
},
|
||||
|
||||
/**
|
||||
* Responds with the given message, calling onMessageAvailable() and
|
||||
* onAcknowledge() synchronously. Throws if the message is not a string.
|
||||
* Used by the tests to respond to client commands.
|
||||
*
|
||||
* @param {String} msg The message to send to the client.
|
||||
*/
|
||||
serverSendMsg(msg) {
|
||||
if (typeof msg != 'string') {
|
||||
throw new Error('Invalid response message');
|
||||
}
|
||||
waterfall(
|
||||
() => this._listener.onMessageAvailable(this._context, msg),
|
||||
() => this._listener.onAcknowledge(this._context, 0)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes the server end of the connection, calling onServerClose()
|
||||
* followed by onStop(). Used to test abrupt connection termination.
|
||||
*
|
||||
* @param {Number} [statusCode] The WebSocket connection close code.
|
||||
* @param {String} [reason] The connection close reason.
|
||||
*/
|
||||
serverClose(statusCode, reason = '') {
|
||||
if (!isFinite(statusCode)) {
|
||||
statusCode = WEBSOCKET_CLOSE_GOING_AWAY;
|
||||
}
|
||||
waterfall(
|
||||
() => this._listener.onServerClose(this._context, statusCode, reason),
|
||||
() => this._listener.onStop(this._context, Cr.NS_BASE_STREAM_CLOSED)
|
||||
);
|
||||
},
|
||||
|
||||
serverInterrupt(result = Cr.NS_ERROR_NET_RESET) {
|
||||
waterfall(() => this._listener.onStop(this._context, result));
|
||||
},
|
||||
};
|
||||
|
||||
var setUpServiceInParent = Task.async(function* (service, db) {
|
||||
if (!isParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let userAgentID = 'ce704e41-cb77-4206-b07b-5bf47114791b';
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
|
||||
yield db.put({
|
||||
channelID: '6e2814e1-5f84-489e-b542-855cc1311f09',
|
||||
pushEndpoint: 'https://example.org/push/get',
|
||||
scope: 'https://example.com/get/ok',
|
||||
originAttributes: '',
|
||||
version: 1,
|
||||
pushCount: 10,
|
||||
lastPush: 1438360548322,
|
||||
quota: 16,
|
||||
});
|
||||
yield db.put({
|
||||
channelID: '3a414737-2fd0-44c0-af05-7efc172475fc',
|
||||
pushEndpoint: 'https://example.org/push/unsub',
|
||||
scope: 'https://example.com/unsub/ok',
|
||||
originAttributes: '',
|
||||
version: 2,
|
||||
pushCount: 10,
|
||||
lastPush: 1438360848322,
|
||||
quota: 4,
|
||||
});
|
||||
yield db.put({
|
||||
channelID: 'ca3054e8-b59b-4ea0-9c23-4a3c518f3161',
|
||||
pushEndpoint: 'https://example.org/push/stale',
|
||||
scope: 'https://example.com/unsub/fail',
|
||||
originAttributes: '',
|
||||
version: 3,
|
||||
pushCount: 10,
|
||||
lastPush: 1438362348322,
|
||||
quota: 1,
|
||||
});
|
||||
|
||||
service.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db: makeStub(db, {
|
||||
put(prev, record) {
|
||||
if (record.scope == 'https://example.com/sub/fail') {
|
||||
return Promise.reject('synergies not aligned');
|
||||
}
|
||||
return prev.call(this, record);
|
||||
},
|
||||
delete: function(prev, channelID) {
|
||||
if (channelID == 'ca3054e8-b59b-4ea0-9c23-4a3c518f3161') {
|
||||
return Promise.reject('splines not reticulated');
|
||||
}
|
||||
return prev.call(this, channelID);
|
||||
},
|
||||
getByIdentifiers(prev, identifiers) {
|
||||
if (identifiers.scope == 'https://example.com/get/fail') {
|
||||
return Promise.reject('qualia unsynchronized');
|
||||
}
|
||||
return prev.call(this, identifiers);
|
||||
},
|
||||
}),
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
uaid: userAgentID,
|
||||
status: 200,
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
if (request.key) {
|
||||
let appServerKey = new Uint8Array(
|
||||
ChromeUtils.base64URLDecode(request.key, {
|
||||
padding: "require",
|
||||
})
|
||||
);
|
||||
equal(appServerKey.length, 65, 'Wrong app server key length');
|
||||
equal(appServerKey[0], 4, 'Wrong app server key format');
|
||||
}
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
uaid: userAgentID,
|
||||
channelID: request.channelID,
|
||||
status: 200,
|
||||
pushEndpoint: 'https://example.org/push/' + request.channelID,
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'unregister',
|
||||
channelID: request.channelID,
|
||||
status: 200,
|
||||
}));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
var tearDownServiceInParent = Task.async(function* (db) {
|
||||
if (!isParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let record = yield db.getByIdentifiers({
|
||||
scope: 'https://example.com/sub/ok',
|
||||
originAttributes: '',
|
||||
});
|
||||
ok(record.pushEndpoint.startsWith('https://example.org/push'),
|
||||
'Wrong push endpoint in subscription record');
|
||||
|
||||
record = yield db.getByIdentifiers({
|
||||
scope: 'https://example.net/scope/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: 1, inIsolatedMozBrowser: true }),
|
||||
});
|
||||
ok(record.pushEndpoint.startsWith('https://example.org/push'),
|
||||
'Wrong push endpoint in app record');
|
||||
|
||||
record = yield db.getByKeyID('3a414737-2fd0-44c0-af05-7efc172475fc');
|
||||
ok(!record, 'Unsubscribed record should not exist');
|
||||
});
|
||||
|
||||
function putTestRecord(db, keyID, scope, quota) {
|
||||
return db.put({
|
||||
channelID: keyID,
|
||||
pushEndpoint: 'https://example.org/push/' + keyID,
|
||||
scope: scope,
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: quota,
|
||||
systemRecord: quota == Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
function getAllKeyIDs(db) {
|
||||
return db.getAllKeyIDs().then(records =>
|
||||
records.map(record => record.keyID).sort(compareAscending)
|
||||
);
|
||||
}
|
||||
4
dom/push/test/xpcshell/moz.build
Normal file
4
dom/push/test/xpcshell/moz.build
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
EXTRA_COMPONENTS += [
|
||||
'PushServiceHandler.js',
|
||||
'PushServiceHandler.manifest',
|
||||
]
|
||||
115
dom/push/test/xpcshell/test_clearAll_successful.js
Normal file
115
dom/push/test/xpcshell/test_clearAll_successful.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
var db;
|
||||
var unregisterDefers = {};
|
||||
var userAgentID = '4ce480ef-55b2-4f83-924c-dcd35ab978b4';
|
||||
|
||||
function promiseUnregister(keyID, code) {
|
||||
return new Promise(r => unregisterDefers[keyID] = r);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(_ => db.drop().then(_ => db.close()));
|
||||
|
||||
// Active subscriptions; should be expired then dropped.
|
||||
yield putTestRecord(db, 'active-1', 'https://example.info/some-page', 8);
|
||||
yield putTestRecord(db, 'active-2', 'https://example.com/another-page', 16);
|
||||
|
||||
// Expired subscription; should be dropped.
|
||||
yield putTestRecord(db, 'expired', 'https://example.net/yet-another-page', 0);
|
||||
|
||||
// A privileged subscription that should not be affected by sanitizing data
|
||||
// because its quota is set to `Infinity`.
|
||||
yield putTestRecord(db, 'privileged', 'app://chrome/only', Infinity);
|
||||
|
||||
let handshakeDone;
|
||||
let handshakePromise = new Promise(r => handshakeDone = r);
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db: db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
uaid: userAgentID,
|
||||
status: 200,
|
||||
use_webpush: true,
|
||||
}));
|
||||
handshakeDone();
|
||||
},
|
||||
onUnregister(request) {
|
||||
let resolve = unregisterDefers[request.channelID];
|
||||
equal(typeof resolve, 'function',
|
||||
'Dropped unexpected channel ID ' + request.channelID);
|
||||
delete unregisterDefers[request.channelID];
|
||||
equal(request.code, 200,
|
||||
'Expected manual unregister reason');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'unregister',
|
||||
channelID: request.channelID,
|
||||
status: 200,
|
||||
}));
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
yield handshakePromise;
|
||||
});
|
||||
|
||||
add_task(function* test_sanitize() {
|
||||
let modifiedScopes = [];
|
||||
let changeScopes = [];
|
||||
|
||||
let promiseCleared = Promise.all([
|
||||
// Active subscriptions should be unregistered.
|
||||
promiseUnregister('active-1'),
|
||||
promiseUnregister('active-2'),
|
||||
promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
|
||||
modifiedScopes.push(data);
|
||||
return modifiedScopes.length == 3;
|
||||
}),
|
||||
|
||||
// Privileged should be recreated.
|
||||
promiseUnregister('privileged'),
|
||||
promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionChangeTopic, (subject, data) => {
|
||||
changeScopes.push(data);
|
||||
return changeScopes.length == 1;
|
||||
}),
|
||||
]);
|
||||
|
||||
yield PushService.clear({
|
||||
domain: '*',
|
||||
});
|
||||
|
||||
yield promiseCleared;
|
||||
|
||||
deepEqual(modifiedScopes.sort(compareAscending), [
|
||||
'app://chrome/only',
|
||||
'https://example.com/another-page',
|
||||
'https://example.info/some-page',
|
||||
], 'Should modify active subscription scopes');
|
||||
|
||||
deepEqual(changeScopes, ['app://chrome/only'],
|
||||
'Should fire change notification for privileged scope');
|
||||
|
||||
let remainingIDs = yield getAllKeyIDs(db);
|
||||
deepEqual(remainingIDs, [], 'Should drop all subscriptions');
|
||||
});
|
||||
128
dom/push/test/xpcshell/test_clear_forgetAboutSite.js
Normal file
128
dom/push/test/xpcshell/test_clear_forgetAboutSite.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
'use strict';
|
||||
|
||||
const {PushService, PushServiceWebSocket} = serviceExports;
|
||||
const {ForgetAboutSite} = Cu.import(
|
||||
'resource://gre/modules/ForgetAboutSite.jsm', {});
|
||||
|
||||
var db;
|
||||
var unregisterDefers = {};
|
||||
var userAgentID = '4fe01c2d-72ac-4c13-93d2-bb072caf461d';
|
||||
|
||||
function promiseUnregister(keyID) {
|
||||
return new Promise(r => unregisterDefers[keyID] = r);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(_ => db.drop().then(_ => db.close()));
|
||||
|
||||
// Active and expired subscriptions for a subdomain. The active subscription
|
||||
// should be expired, then removed; the expired subscription should be
|
||||
// removed immediately.
|
||||
yield putTestRecord(db, 'active-sub', 'https://sub.example.com/sub-page', 4);
|
||||
yield putTestRecord(db, 'expired-sub', 'https://sub.example.com/yet-another-page', 0);
|
||||
|
||||
// Active subscriptions for another subdomain. Should be unsubscribed and
|
||||
// dropped.
|
||||
yield putTestRecord(db, 'active-1', 'https://sub2.example.com/some-page', 8);
|
||||
yield putTestRecord(db, 'active-2', 'https://sub3.example.com/another-page', 16);
|
||||
|
||||
// A privileged subscription with a real URL that should not be affected
|
||||
// because its quota is set to `Infinity`.
|
||||
yield putTestRecord(db, 'privileged', 'https://sub.example.com/real-url', Infinity);
|
||||
|
||||
let handshakeDone;
|
||||
let handshakePromise = new Promise(r => handshakeDone = r);
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db: db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
uaid: userAgentID,
|
||||
status: 200,
|
||||
use_webpush: true,
|
||||
}));
|
||||
handshakeDone();
|
||||
},
|
||||
onUnregister(request) {
|
||||
let resolve = unregisterDefers[request.channelID];
|
||||
equal(typeof resolve, 'function',
|
||||
'Dropped unexpected channel ID ' + request.channelID);
|
||||
delete unregisterDefers[request.channelID];
|
||||
equal(request.code, 200,
|
||||
'Expected manual unregister reason');
|
||||
resolve();
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'unregister',
|
||||
status: 200,
|
||||
channelID: request.channelID,
|
||||
}));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
// For cleared subscriptions, we only send unregister requests in the
|
||||
// background and if we're connected.
|
||||
yield handshakePromise;
|
||||
});
|
||||
|
||||
add_task(function* test_forgetAboutSubdomain() {
|
||||
let modifiedScopes = [];
|
||||
let promiseForgetSubs = Promise.all([
|
||||
// Active subscriptions should be dropped.
|
||||
promiseUnregister('active-sub'),
|
||||
promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
|
||||
modifiedScopes.push(data);
|
||||
return modifiedScopes.length == 1;
|
||||
}
|
||||
),
|
||||
]);
|
||||
yield ForgetAboutSite.removeDataFromDomain('sub.example.com');
|
||||
yield promiseForgetSubs;
|
||||
|
||||
deepEqual(modifiedScopes.sort(compareAscending), [
|
||||
'https://sub.example.com/sub-page',
|
||||
], 'Should fire modified notifications for active subscriptions');
|
||||
|
||||
let remainingIDs = yield getAllKeyIDs(db);
|
||||
deepEqual(remainingIDs, ['active-1', 'active-2', 'privileged'],
|
||||
'Should only forget subscriptions for subdomain');
|
||||
});
|
||||
|
||||
add_task(function* test_forgetAboutRootDomain() {
|
||||
let modifiedScopes = [];
|
||||
let promiseForgetSubs = Promise.all([
|
||||
promiseUnregister('active-1'),
|
||||
promiseUnregister('active-2'),
|
||||
promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
|
||||
modifiedScopes.push(data);
|
||||
return modifiedScopes.length == 2;
|
||||
}
|
||||
),
|
||||
]);
|
||||
|
||||
yield ForgetAboutSite.removeDataFromDomain('example.com');
|
||||
yield promiseForgetSubs;
|
||||
|
||||
deepEqual(modifiedScopes.sort(compareAscending), [
|
||||
'https://sub2.example.com/some-page',
|
||||
'https://sub3.example.com/another-page',
|
||||
], 'Should fire modified notifications for entire domain');
|
||||
|
||||
let remainingIDs = yield getAllKeyIDs(db);
|
||||
deepEqual(remainingIDs, ['privileged'],
|
||||
'Should ignore privileged records with a real URL');
|
||||
});
|
||||
141
dom/push/test/xpcshell/test_clear_origin_data.js
Normal file
141
dom/push/test/xpcshell/test_clear_origin_data.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = 'bd744428-f125-436a-b6d0-dd0c9845837f';
|
||||
|
||||
let clearForPattern = Task.async(function* (testRecords, pattern) {
|
||||
let patternString = JSON.stringify(pattern);
|
||||
yield PushService._clearOriginData(patternString);
|
||||
|
||||
for (let length = testRecords.length; length--;) {
|
||||
let test = testRecords[length];
|
||||
let originSuffix = ChromeUtils.originAttributesToSuffix(
|
||||
test.originAttributes);
|
||||
|
||||
let registration = yield PushService.registration({
|
||||
scope: test.scope,
|
||||
originAttributes: originSuffix,
|
||||
});
|
||||
|
||||
let url = test.scope + originSuffix;
|
||||
|
||||
if (ObjectUtils.deepEqual(test.clearIf, pattern)) {
|
||||
ok(!registration, 'Should clear registration ' + url +
|
||||
' for pattern ' + patternString);
|
||||
testRecords.splice(length, 1);
|
||||
} else {
|
||||
ok(registration, 'Should not clear registration ' + url +
|
||||
' for pattern ' + patternString);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_webapps_cleardata() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
let testRecords = [{
|
||||
scope: 'https://example.org/1',
|
||||
originAttributes: { appId: 1 },
|
||||
clearIf: { appId: 1, inIsolatedMozBrowser: false },
|
||||
}, {
|
||||
scope: 'https://example.org/1',
|
||||
originAttributes: { appId: 1, inIsolatedMozBrowser: true },
|
||||
clearIf: { appId: 1 },
|
||||
}, {
|
||||
scope: 'https://example.org/1',
|
||||
originAttributes: { appId: 2, inIsolatedMozBrowser: true },
|
||||
clearIf: { appId: 2, inIsolatedMozBrowser: true },
|
||||
}, {
|
||||
scope: 'https://example.org/2',
|
||||
originAttributes: { appId: 1 },
|
||||
clearIf: { appId: 1, inIsolatedMozBrowser: false },
|
||||
}, {
|
||||
scope: 'https://example.org/2',
|
||||
originAttributes: { appId: 2, inIsolatedMozBrowser: true },
|
||||
clearIf: { appId: 2, inIsolatedMozBrowser: true },
|
||||
}, {
|
||||
scope: 'https://example.org/3',
|
||||
originAttributes: { appId: 3, inIsolatedMozBrowser: true },
|
||||
clearIf: { inIsolatedMozBrowser: true },
|
||||
}, {
|
||||
scope: 'https://example.org/3',
|
||||
originAttributes: { appId: 4, inIsolatedMozBrowser: true },
|
||||
clearIf: { inIsolatedMozBrowser: true },
|
||||
}];
|
||||
|
||||
let unregisterDone;
|
||||
let unregisterPromise = new Promise(resolve =>
|
||||
unregisterDone = after(testRecords.length, resolve));
|
||||
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(data) {
|
||||
equal(data.messageType, 'hello', 'Handshake: wrong message type');
|
||||
equal(data.uaid, userAgentID, 'Handshake: wrong device ID');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
},
|
||||
onRegister(data) {
|
||||
equal(data.messageType, 'register', 'Register: wrong message type');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
channelID: data.channelID,
|
||||
uaid: userAgentID,
|
||||
pushEndpoint: 'https://example.com/update/' + Math.random(),
|
||||
}));
|
||||
},
|
||||
onUnregister(data) {
|
||||
equal(data.code, 200, 'Expected manual unregister reason');
|
||||
unregisterDone();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield Promise.all(testRecords.map(test =>
|
||||
PushService.register({
|
||||
scope: test.scope,
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
test.originAttributes),
|
||||
})
|
||||
));
|
||||
|
||||
// Removes records for all scopes with the same app ID. Excludes records
|
||||
// where `inIsolatedMozBrowser` is true.
|
||||
yield clearForPattern(testRecords, { appId: 1, inIsolatedMozBrowser: false });
|
||||
|
||||
// Removes the remaining record for app ID 1, where `inIsolatedMozBrowser` is true.
|
||||
yield clearForPattern(testRecords, { appId: 1 });
|
||||
|
||||
// Removes all records for all scopes with the same app ID, where
|
||||
// `inIsolatedMozBrowser` is true.
|
||||
yield clearForPattern(testRecords, { appId: 2, inIsolatedMozBrowser: true });
|
||||
|
||||
// Removes all records where `inIsolatedMozBrowser` is true.
|
||||
yield clearForPattern(testRecords, { inIsolatedMozBrowser: true });
|
||||
|
||||
equal(testRecords.length, 0, 'Should remove all test records');
|
||||
yield unregisterPromise;
|
||||
});
|
||||
249
dom/push/test/xpcshell/test_crypto.js
Normal file
249
dom/push/test/xpcshell/test_crypto.js
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
getCryptoParams,
|
||||
PushCrypto,
|
||||
} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_crypto_getCryptoParams() {
|
||||
// These headers should parse correctly.
|
||||
let shouldParse = [{
|
||||
desc: 'aesgcm with multiple keys',
|
||||
headers: {
|
||||
encoding: 'aesgcm',
|
||||
crypto_key: 'keyid=p256dh;dh=Iy1Je2Kv11A,p256ecdsa=o2M8QfiEKuI',
|
||||
encryption: 'keyid=p256dh;salt=upk1yFkp1xI',
|
||||
},
|
||||
params: {
|
||||
dh: 'Iy1Je2Kv11A',
|
||||
salt: 'upk1yFkp1xI',
|
||||
rs: 4096,
|
||||
padSize: 2,
|
||||
},
|
||||
}, {
|
||||
desc: 'aesgcm with quoted key param',
|
||||
headers: {
|
||||
encoding: 'aesgcm',
|
||||
crypto_key: 'dh="byfHbUffc-k"',
|
||||
encryption: 'salt=C11AvAsp6Gc',
|
||||
},
|
||||
params: {
|
||||
dh: 'byfHbUffc-k',
|
||||
salt: 'C11AvAsp6Gc',
|
||||
rs: 4096,
|
||||
padSize: 2,
|
||||
},
|
||||
}, {
|
||||
desc: 'aesgcm with Crypto-Key and rs = 24',
|
||||
headers: {
|
||||
encoding: 'aesgcm',
|
||||
crypto_key: 'dh="ybuT4VDz-Bg"',
|
||||
encryption: 'salt=H7U7wcIoIKs; rs=24',
|
||||
},
|
||||
params: {
|
||||
dh: 'ybuT4VDz-Bg',
|
||||
salt: 'H7U7wcIoIKs',
|
||||
rs: 24,
|
||||
padSize: 2,
|
||||
},
|
||||
}, {
|
||||
desc: 'aesgcm128 with Encryption-Key and rs = 2',
|
||||
headers: {
|
||||
encoding: 'aesgcm128',
|
||||
encryption_key: 'keyid=legacy; dh=LqrDQuVl9lY',
|
||||
encryption: 'keyid=legacy; salt=YngI8B7YapM; rs=2',
|
||||
},
|
||||
params: {
|
||||
dh: 'LqrDQuVl9lY',
|
||||
salt: 'YngI8B7YapM',
|
||||
rs: 2,
|
||||
padSize: 1,
|
||||
},
|
||||
}, {
|
||||
desc: 'aesgcm128 with Encryption-Key',
|
||||
headers: {
|
||||
encoding: 'aesgcm128',
|
||||
encryption_key: 'keyid=v2; dh=VA6wmY1IpiE',
|
||||
encryption: 'keyid=v2; salt=khtpyXhpDKM',
|
||||
},
|
||||
params: {
|
||||
dh: 'VA6wmY1IpiE',
|
||||
salt: 'khtpyXhpDKM',
|
||||
rs: 4096,
|
||||
padSize: 1,
|
||||
}
|
||||
}];
|
||||
for (let test of shouldParse) {
|
||||
let params = getCryptoParams(test.headers);
|
||||
deepEqual(params, test.params, test.desc);
|
||||
}
|
||||
|
||||
// These headers should be rejected.
|
||||
let shouldThrow = [{
|
||||
desc: 'aesgcm128 with Crypto-Key',
|
||||
headers: {
|
||||
encoding: 'aesgcm128',
|
||||
crypto_key: 'keyid=v2; dh=VA6wmY1IpiE',
|
||||
encryption: 'keyid=v2; salt=F0Im7RtGgNY',
|
||||
},
|
||||
}, {
|
||||
desc: 'Invalid encoding',
|
||||
headers: {
|
||||
encoding: 'nonexistent',
|
||||
},
|
||||
}, {
|
||||
desc: 'Invalid record size',
|
||||
headers: {
|
||||
encoding: 'aesgcm',
|
||||
crypto_key: 'dh=pbmv1QkcEDY',
|
||||
encryption: 'dh=Esao8aTBfIk;rs=bad',
|
||||
},
|
||||
}, {
|
||||
desc: 'Insufficiently large record size',
|
||||
headers: {
|
||||
encoding: 'aesgcm',
|
||||
crypto_key: 'dh=fK0EXaw5IU8',
|
||||
encryption: 'salt=orbLLmlbJfM;rs=1',
|
||||
},
|
||||
}, {
|
||||
desc: 'aesgcm with Encryption-Key',
|
||||
headers: {
|
||||
encoding: 'aesgcm',
|
||||
encryption_key: 'dh=FplK5KkvUF0',
|
||||
encryption: 'salt=p6YHhFF3BQY',
|
||||
},
|
||||
}];
|
||||
for (let test of shouldThrow) {
|
||||
throws(() => getCryptoParams(test.headers), test.desc);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* test_crypto_decodeMsg() {
|
||||
let privateKey = {
|
||||
crv: 'P-256',
|
||||
d: '4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg',
|
||||
ext: true,
|
||||
key_ops: ['deriveBits'],
|
||||
kty: 'EC',
|
||||
x: 'sd85ZCbEG6dEkGMCmDyGBIt454Qy-Yo-1xhbaT2Jlk4',
|
||||
y: 'vr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs',
|
||||
};
|
||||
let publicKey = ChromeUtils.base64URLDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs', {
|
||||
padding: "reject",
|
||||
});
|
||||
|
||||
let expectedSuccesses = [{
|
||||
desc: 'padSize = 2, rs = 24, pad = 0',
|
||||
result: 'Some message',
|
||||
data: 'Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU',
|
||||
authSecret: 'aTDc6JebzR6eScy2oLo4RQ',
|
||||
headers: {
|
||||
crypto_key: 'dh=BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo',
|
||||
encryption: 'salt=zCU18Rw3A5aB_Xi-vfixmA; rs=24',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
}, {
|
||||
desc: 'padSize = 2, rs = 8, pad = 16',
|
||||
result: 'Yet another message',
|
||||
data: 'uEC5B_tR-fuQ3delQcrzrDCp40W6ipMZjGZ78USDJ5sMj-6bAOVG3AK6JqFl9E6AoWiBYYvMZfwThVxmDnw6RHtVeLKFM5DWgl1EwkOohwH2EhiDD0gM3io-d79WKzOPZE9rDWUSv64JstImSfX_ADQfABrvbZkeaWxh53EG59QMOElFJqHue4dMURpsMXg',
|
||||
authSecret: '6plwZnSpVUbF7APDXus3UQ',
|
||||
headers: {
|
||||
crypto_key: 'dh=BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ',
|
||||
encryption: 'salt=ZFhzj0S-n29g9P2p4-I7tA; rs=8',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
}, {
|
||||
desc: 'padSize = 1, rs = 4096, pad = 2',
|
||||
result: 'aesgcm128 encrypted message',
|
||||
data: 'ljBJ44NPzJFH9EuyT5xWMU4vpZ90MdAqaq1TC1kOLRoPNHtNFXeJ0GtuSaE',
|
||||
headers: {
|
||||
encryption_key: 'dh=BOmnfg02vNd6RZ7kXWWrCGFF92bI-rQ-bV0Pku3-KmlHwbGv4ejWqgasEdLGle5Rhmp6SKJunZw2l2HxKvrIjfI',
|
||||
encryption: 'salt=btxxUtclbmgcc30b9rT3Bg; rs=4096',
|
||||
encoding: 'aesgcm128',
|
||||
},
|
||||
}, {
|
||||
desc: 'padSize = 2, rs = 3, pad = 0',
|
||||
result: 'Small record size',
|
||||
data: 'oY4e5eDatDVt2fpQylxbPJM-3vrfhDasfPc8Q1PWt4tPfMVbz_sDNL_cvr0DXXkdFzS1lxsJsj550USx4MMl01ihjImXCjrw9R5xFgFrCAqJD3GwXA1vzS4T5yvGVbUp3SndMDdT1OCcEofTn7VC6xZ-zP8rzSQfDCBBxmPU7OISzr8Z4HyzFCGJeBfqiZ7yUfNlKF1x5UaZ4X6iU_TXx5KlQy_toV1dXZ2eEAMHJUcSdArvB6zRpFdEIxdcHcJyo1BIYgAYTDdAIy__IJVCPY_b2CE5W_6ohlYKB7xDyH8giNuWWXAgBozUfScLUVjPC38yJTpAUi6w6pXgXUWffende5FreQpnMFL1L4G-38wsI_-ISIOzdO8QIrXHxmtc1S5xzYu8bMqSgCinvCEwdeGFCmighRjj8t1zRWo0D14rHbQLPR_b1P5SvEeJTtS9Nm3iibM',
|
||||
authSecret: 'g2rWVHUCpUxgcL9Tz7vyeQ',
|
||||
headers: {
|
||||
crypto_key: 'dh=BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_fYHBP4ObxwJNl56bk',
|
||||
encryption: 'salt=5LIDBXbvkBvvb7ZdD-T4PQ; rs=3',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
}];
|
||||
for (let test of expectedSuccesses) {
|
||||
let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
|
||||
padding: "reject",
|
||||
}) : null;
|
||||
let data = ChromeUtils.base64URLDecode(test.data, {
|
||||
padding: "reject",
|
||||
});
|
||||
let result = yield PushCrypto.decrypt(privateKey, publicKey, authSecret,
|
||||
test.headers, data);
|
||||
let decoder = new TextDecoder('utf-8');
|
||||
equal(decoder.decode(new Uint8Array(result)), test.result, test.desc);
|
||||
}
|
||||
|
||||
let expectedFailures = [{
|
||||
desc: 'padSize = 1, rs = 4096, auth secret, pad = 8',
|
||||
data: 'h0FmyldY8aT5EQ6CJrbfRn_IdDvytoLeHb9_q5CjtdFRfgDRknxLmOzavLaVG4oOiS0r',
|
||||
senderPublicKey: '',
|
||||
authSecret: 'Sxb6u0gJIhGEogyLawjmCw',
|
||||
headers: {
|
||||
crypto_key: 'dh=BCXHk7O8CE-9AOp6xx7g7c-NCaNpns1PyyHpdcmDaijLbT6IdGq0ezGatBwtFc34BBfscFxdk4Tjksa2Mx5rRCM',
|
||||
encryption: 'salt=aGBpoKklLtrLcAUCcCr7JQ',
|
||||
encoding: 'aesgcm128',
|
||||
},
|
||||
}, {
|
||||
desc: 'Missing padding',
|
||||
data: 'anvsHj7oBQTPMhv7XSJEsvyMS4-8EtbC7HgFZsKaTg',
|
||||
headers: {
|
||||
crypto_key: 'dh=BMSqfc3ohqw2DDgu3nsMESagYGWubswQPGxrW1bAbYKD18dIHQBUmD3ul_lu7MyQiT5gNdzn5JTXQvCcpf-oZE4',
|
||||
encryption: 'salt=Czx2i18rar8XWOXAVDnUuw',
|
||||
encoding: 'aesgcm128',
|
||||
},
|
||||
}, {
|
||||
desc: 'padSize > rs',
|
||||
data: 'Ct_h1g7O55e6GvuhmpjLsGnv8Rmwvxgw8iDESNKGxk_8E99iHKDzdV8wJPyHA-6b2E6kzuVa5UWiQ7s4Zms1xzJ4FKgoxvBObXkc_r_d4mnb-j245z3AcvRmcYGk5_HZ0ci26SfhAN3lCgxGzTHS4nuHBRkGwOb4Tj4SFyBRlLoTh2jyVK2jYugNjH9tTrGOBg7lP5lajLTQlxOi91-RYZSfFhsLX3LrAkXuRoN7G1CdiI7Y3_eTgbPIPabDcLCnGzmFBTvoJSaQF17huMl_UnWoCj2WovA4BwK_TvWSbdgElNnQ4CbArJ1h9OqhDOphVu5GUGr94iitXRQR-fqKPMad0ULLjKQWZOnjuIdV1RYEZ873r62Yyd31HoveJcSDb1T8l_QK2zVF8V4k0xmK9hGuC0rF5YJPYPHgl5__usknzxMBnRrfV5_MOL5uPZwUEFsu',
|
||||
headers: {
|
||||
crypto_key: 'dh=BAcMdWLJRGx-kPpeFtwqR3GE1LWzd1TYh2rg6CEFu53O-y3DNLkNe_BtGtKRR4f7ZqpBMVS6NgfE2NwNPm3Ndls',
|
||||
encryption: 'salt=NQVTKhB0rpL7ZzKkotTGlA; rs=1',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
}, {
|
||||
desc: 'Encrypted with padSize = 1, decrypted with padSize = 2 and auth secret',
|
||||
data: 'fwkuwTTChcLnrzsbDI78Y2EoQzfnbMI8Ax9Z27_rwX8',
|
||||
authSecret: 'BhbpNTWyO5wVJmVKTV6XaA',
|
||||
headers: {
|
||||
crypto_key: 'dh=BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
|
||||
encryption: 'salt=c6JQl9eJ0VvwrUVCQDxY7Q',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
}, {
|
||||
desc: 'Truncated input',
|
||||
data: 'AlDjj6NvT5HGyrHbT8M5D6XBFSra6xrWS9B2ROaCIjwSu3RyZ1iyuv0',
|
||||
headers: {
|
||||
crypto_key: 'dh=BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
|
||||
encryption: 'salt=c6JQl9eJ0VvwrUVCQDxY7Q; rs=25',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
}];
|
||||
for (let test of expectedFailures) {
|
||||
let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
|
||||
padding: "reject",
|
||||
}) : null;
|
||||
let data = ChromeUtils.base64URLDecode(test.data, {
|
||||
padding: "reject",
|
||||
});
|
||||
yield rejects(
|
||||
PushCrypto.decrypt(privateKey, publicKey, authSecret,
|
||||
test.headers, data),
|
||||
test.desc
|
||||
);
|
||||
}
|
||||
});
|
||||
154
dom/push/test/xpcshell/test_drop_expired.js
Normal file
154
dom/push/test/xpcshell/test_drop_expired.js
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '2c43af06-ab6e-476a-adc4-16cbda54fb89';
|
||||
|
||||
var db;
|
||||
var quotaURI;
|
||||
var permURI;
|
||||
|
||||
function visitURI(uri, timestamp) {
|
||||
return PlacesTestUtils.addVisits({
|
||||
uri: uri,
|
||||
title: uri.spec,
|
||||
visitDate: timestamp * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_LINK
|
||||
});
|
||||
}
|
||||
|
||||
var putRecord = Task.async(function* ({scope, perm, quota, lastPush, lastVisit}) {
|
||||
let uri = Services.io.newURI(scope, null, null);
|
||||
|
||||
Services.perms.add(uri, 'desktop-notification',
|
||||
Ci.nsIPermissionManager[perm]);
|
||||
do_register_cleanup(() => {
|
||||
Services.perms.remove(uri, 'desktop-notification');
|
||||
});
|
||||
|
||||
yield visitURI(uri, lastVisit);
|
||||
|
||||
yield db.put({
|
||||
channelID: uri.path,
|
||||
pushEndpoint: 'https://example.org/push' + uri.path,
|
||||
scope: uri.spec,
|
||||
pushCount: 0,
|
||||
lastPush: lastPush,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: quota,
|
||||
});
|
||||
|
||||
return uri;
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
|
||||
db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* setUp() {
|
||||
// An expired registration that should be evicted on startup. Permission is
|
||||
// granted for this origin, and the last visit is more recent than the last
|
||||
// push message.
|
||||
yield putRecord({
|
||||
scope: 'https://example.com/expired-quota-restored',
|
||||
perm: 'ALLOW_ACTION',
|
||||
quota: 0,
|
||||
lastPush: Date.now() - 10,
|
||||
lastVisit: Date.now(),
|
||||
});
|
||||
|
||||
// An expired registration that we should evict when the origin is visited
|
||||
// again.
|
||||
quotaURI = yield putRecord({
|
||||
scope: 'https://example.xyz/expired-quota-exceeded',
|
||||
perm: 'ALLOW_ACTION',
|
||||
quota: 0,
|
||||
lastPush: Date.now() - 10,
|
||||
lastVisit: Date.now() - 20,
|
||||
});
|
||||
|
||||
// An expired registration that we should evict when permission is granted
|
||||
// again.
|
||||
permURI = yield putRecord({
|
||||
scope: 'https://example.info/expired-perm-revoked',
|
||||
perm: 'DENY_ACTION',
|
||||
quota: 0,
|
||||
lastPush: Date.now() - 10,
|
||||
lastVisit: Date.now(),
|
||||
});
|
||||
|
||||
// An active registration that we should leave alone.
|
||||
yield putRecord({
|
||||
scope: 'https://example.ninja/active',
|
||||
perm: 'ALLOW_ACTION',
|
||||
quota: 16,
|
||||
lastPush: Date.now() - 10,
|
||||
lastVisit: Date.now() - 20,
|
||||
});
|
||||
|
||||
let subChangePromise = promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionChangeTopic,
|
||||
(subject, data) => data == 'https://example.com/expired-quota-restored'
|
||||
);
|
||||
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'unregister',
|
||||
channelID: request.channelID,
|
||||
status: 200,
|
||||
}));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield subChangePromise;
|
||||
});
|
||||
|
||||
add_task(function* test_site_visited() {
|
||||
let subChangePromise = promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionChangeTopic,
|
||||
(subject, data) => data == 'https://example.xyz/expired-quota-exceeded'
|
||||
);
|
||||
|
||||
yield visitURI(quotaURI, Date.now());
|
||||
PushService.observe(null, 'idle-daily', '');
|
||||
|
||||
yield subChangePromise;
|
||||
});
|
||||
|
||||
add_task(function* test_perm_restored() {
|
||||
let subChangePromise = promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionChangeTopic,
|
||||
(subject, data) => data == 'https://example.info/expired-perm-revoked'
|
||||
);
|
||||
|
||||
Services.perms.add(permURI, 'desktop-notification',
|
||||
Ci.nsIPermissionManager.ALLOW_ACTION);
|
||||
|
||||
yield subChangePromise;
|
||||
});
|
||||
47
dom/push/test/xpcshell/test_handler_service.js
Normal file
47
dom/push/test/xpcshell/test_handler_service.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"use strict";
|
||||
|
||||
// Here we test that if an xpcom component is registered with the category
|
||||
// manager for push notifications against a specific scope, that service is
|
||||
// instantiated before the message is delivered.
|
||||
|
||||
// This component is registered for "chrome://test-scope"
|
||||
const kServiceContractID = "@mozilla.org/dom/push/test/PushServiceHandler;1";
|
||||
|
||||
let pushService = Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService);
|
||||
|
||||
add_test(function test_service_instantiation() {
|
||||
do_load_manifest("PushServiceHandler.manifest");
|
||||
|
||||
let scope = "chrome://test-scope";
|
||||
let pushNotifier = Cc["@mozilla.org/push/Notifier;1"].getService(Ci.nsIPushNotifier);
|
||||
let principal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||
pushNotifier.notifyPush(scope, principal, "");
|
||||
|
||||
// Now get a handle to our service and check it received the notification.
|
||||
let handlerService = Cc[kServiceContractID]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
|
||||
equal(handlerService.observed.length, 1);
|
||||
equal(handlerService.observed[0].topic, pushService.pushTopic);
|
||||
let message = handlerService.observed[0].subject.QueryInterface(Ci.nsIPushMessage);
|
||||
equal(message.principal, principal);
|
||||
strictEqual(message.data, null);
|
||||
equal(handlerService.observed[0].data, scope);
|
||||
|
||||
// and a subscription change.
|
||||
pushNotifier.notifySubscriptionChange(scope, principal);
|
||||
equal(handlerService.observed.length, 2);
|
||||
equal(handlerService.observed[1].topic, pushService.subscriptionChangeTopic);
|
||||
equal(handlerService.observed[1].subject, principal);
|
||||
equal(handlerService.observed[1].data, scope);
|
||||
|
||||
// and a subscription modified event.
|
||||
pushNotifier.notifySubscriptionModified(scope, principal);
|
||||
equal(handlerService.observed.length, 3);
|
||||
equal(handlerService.observed[2].topic, pushService.subscriptionModifiedTopic);
|
||||
equal(handlerService.observed[2].subject, principal);
|
||||
equal(handlerService.observed[2].data, scope);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
125
dom/push/test/xpcshell/test_notification_ack.js
Normal file
125
dom/push/test/xpcshell/test_notification_ack.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
var userAgentID = '5ab1d1df-7a3d-4024-a469-b9e1bb399fad';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({userAgentID});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_notification_ack() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
let records = [{
|
||||
channelID: '21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
|
||||
pushEndpoint: 'https://example.com/update/1',
|
||||
scope: 'https://example.org/1',
|
||||
originAttributes: '',
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
|
||||
pushEndpoint: 'https://example.com/update/2',
|
||||
scope: 'https://example.org/2',
|
||||
originAttributes: '',
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
channelID: '5477bfda-22db-45d4-9614-fee369630260',
|
||||
pushEndpoint: 'https://example.com/update/3',
|
||||
scope: 'https://example.org/3',
|
||||
originAttributes: '',
|
||||
version: 3,
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
let notifyCount = 0;
|
||||
let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, () =>
|
||||
++notifyCount == 3);
|
||||
|
||||
let acks = 0;
|
||||
let ackDone;
|
||||
let ackPromise = new Promise(resolve => ackDone = resolve);
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
equal(request.uaid, userAgentID,
|
||||
'Should send matching device IDs in handshake');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
uaid: userAgentID,
|
||||
status: 200
|
||||
}));
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: '21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
|
||||
version: 2
|
||||
}]
|
||||
}));
|
||||
},
|
||||
onACK(request) {
|
||||
equal(request.messageType, 'ack', 'Should send acknowledgements');
|
||||
let updates = request.updates;
|
||||
switch (++acks) {
|
||||
case 1:
|
||||
deepEqual([{
|
||||
channelID: '21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
|
||||
version: 2,
|
||||
code: 100,
|
||||
}], updates, 'Wrong updates for acknowledgement 1');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
|
||||
version: 4
|
||||
}, {
|
||||
channelID: '5477bfda-22db-45d4-9614-fee369630260',
|
||||
version: 6
|
||||
}]
|
||||
}));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
deepEqual([{
|
||||
channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
|
||||
version: 4,
|
||||
code: 100,
|
||||
}], updates, 'Wrong updates for acknowledgement 2');
|
||||
break;
|
||||
|
||||
case 3:
|
||||
deepEqual([{
|
||||
channelID: '5477bfda-22db-45d4-9614-fee369630260',
|
||||
version: 6,
|
||||
code: 100,
|
||||
}], updates, 'Wrong updates for acknowledgement 3');
|
||||
ackDone();
|
||||
break;
|
||||
|
||||
default:
|
||||
ok(false, 'Unexpected acknowledgement ' + acks);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield notifyPromise;
|
||||
yield ackPromise;
|
||||
});
|
||||
280
dom/push/test/xpcshell/test_notification_data.js
Normal file
280
dom/push/test/xpcshell/test_notification_data.js
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
let db;
|
||||
let userAgentID = 'f5b47f8d-771f-4ea3-b999-91c135f8766d';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function putRecord(channelID, scope, publicKey, privateKey, authSecret) {
|
||||
return db.put({
|
||||
channelID: channelID,
|
||||
pushEndpoint: 'https://example.org/push/' + channelID,
|
||||
scope: scope,
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
p256dhPublicKey: ChromeUtils.base64URLDecode(publicKey, {
|
||||
padding: "reject",
|
||||
}),
|
||||
p256dhPrivateKey: privateKey,
|
||||
authenticationSecret: ChromeUtils.base64URLDecode(authSecret, {
|
||||
padding: "reject",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let ackDone;
|
||||
let server;
|
||||
add_task(function* test_notification_ack_data_setup() {
|
||||
db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
yield putRecord(
|
||||
'subscription1',
|
||||
'https://example.com/page/1',
|
||||
'BPCd4gNQkjwRah61LpdALdzZKLLnU5UAwDztQ5_h0QsT26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA',
|
||||
{
|
||||
crv: 'P-256',
|
||||
d: '1jUPhzVsRkzV0vIzwL4ZEsOlKdNOWm7TmaTfzitJkgM',
|
||||
ext: true,
|
||||
key_ops: ["deriveBits"],
|
||||
kty: "EC",
|
||||
x: '8J3iA1CSPBFqHrUul0At3NkosudTlQDAPO1Dn-HRCxM',
|
||||
y: '26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA'
|
||||
},
|
||||
'c_sGN6uCv9Hu7JOQT34jAQ'
|
||||
);
|
||||
yield putRecord(
|
||||
'subscription2',
|
||||
'https://example.com/page/2',
|
||||
'BPnWyUo7yMnuMlyKtERuLfWE8a09dtdjHSW2lpC9_BqR5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E',
|
||||
{
|
||||
crv: 'P-256',
|
||||
d: 'lFm4nPsUKYgNGBJb5nXXKxl8bspCSp0bAhCYxbveqT4',
|
||||
ext: true,
|
||||
key_ops: ["deriveBits"],
|
||||
kty: 'EC',
|
||||
x: '-dbJSjvIye4yXIq0RG4t9YTxrT1212MdJbaWkL38GpE',
|
||||
y: '5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E'
|
||||
},
|
||||
't3P246Gj9vjKDHHRYaY6hw'
|
||||
);
|
||||
yield putRecord(
|
||||
'subscription3',
|
||||
'https://example.com/page/3',
|
||||
'BDhUHITSeVrWYybFnb7ylVTCDDLPdQWMpf8gXhcWwvaaJa6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI',
|
||||
{
|
||||
crv: 'P-256',
|
||||
d: 'Q1_SE1NySTYzjbqgWwPgrYh7XRg3adqZLkQPsy319G8',
|
||||
ext: true,
|
||||
key_ops: ["deriveBits"],
|
||||
kty: 'EC',
|
||||
x: 'OFQchNJ5WtZjJsWdvvKVVMIMMs91BYyl_yBeFxbC9po',
|
||||
y: 'Ja6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI'
|
||||
},
|
||||
'E0qiXGWvFSR0PS352ES1_Q'
|
||||
);
|
||||
|
||||
let setupDone;
|
||||
let setupDonePromise = new Promise(r => setupDone = r);
|
||||
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
equal(request.uaid, userAgentID,
|
||||
'Should send matching device IDs in handshake');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
uaid: userAgentID,
|
||||
status: 200,
|
||||
use_webpush: true,
|
||||
}));
|
||||
server = this;
|
||||
setupDone();
|
||||
},
|
||||
onACK(request) {
|
||||
if (ackDone) {
|
||||
ackDone(request);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
yield setupDonePromise;
|
||||
});
|
||||
|
||||
add_task(function* test_notification_ack_data() {
|
||||
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',
|
||||
},
|
||||
ackCode: 100,
|
||||
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',
|
||||
},
|
||||
ackCode: 100,
|
||||
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',
|
||||
},
|
||||
ackCode: 100,
|
||||
receive: {
|
||||
scope: 'https://example.com/page/3',
|
||||
data: 'Some message'
|
||||
}
|
||||
},
|
||||
// A message encoded with `aesgcm` (2 bytes of padding, authenticated).
|
||||
{
|
||||
channelID: 'subscription1',
|
||||
version: 'v5',
|
||||
send: {
|
||||
headers: {
|
||||
crypto_key: 'keyid=v4;dh="BMh_vsnqu79ZZkMTYkxl4gWDLdPSGE72Lr4w2hksSFW398xCMJszjzdblAWXyhSwakRNEU_GopAm4UGzyMVR83w"',
|
||||
encryption: 'keyid="v4";salt="C14Wb7rQTlXzrgcPHtaUzw"',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
data: 'pus4kUaBWzraH34M-d_oN8e0LPpF_X6acx695AMXovDe',
|
||||
},
|
||||
ackCode: 100,
|
||||
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="BOp-DpyR9eLY5Ci11_loIFqeHzWfc_0evJmq7N8NKzgp60UAMMM06XIi2VZp2_TSdw1omk7E19SyeCCwRp76E-U"',
|
||||
encryption: 'keyid=v5;salt="TvjOou1TqJOQY_ZsOYV3Ww";rs=24',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
data: 'rG9WYQ2ZwUgfj_tMlZ0vcIaNpBN05FW-9RUBZAM-UUZf0_9eGpuENBpUDAw3mFmd2XJpmvPvAtLVs54l3rGwg1o',
|
||||
},
|
||||
ackCode: 100,
|
||||
receive: {
|
||||
scope: 'https://example.com/page/2',
|
||||
data: 'Some message'
|
||||
}
|
||||
},
|
||||
// A message without key identifiers.
|
||||
{
|
||||
channelID: 'subscription3',
|
||||
version: 'v6',
|
||||
send: {
|
||||
headers: {
|
||||
crypto_key: 'dh="BEEjwWbF5jZKCgW0kmUWgG-wNcRvaa9_3zZElHAF8przHwd4cp5_kQsc-IMNZcVA0iUix31jxuMOytU-5DwWtyQ"',
|
||||
encryption: 'salt=aAQcr2khAksgNspPiFEqiQ',
|
||||
encoding: 'aesgcm',
|
||||
},
|
||||
data: 'pEYgefdI-7L46CYn5dR9TIy2AXGxe07zxclbhstY',
|
||||
},
|
||||
ackCode: 100,
|
||||
receive: {
|
||||
scope: 'https://example.com/page/3',
|
||||
data: 'Some message'
|
||||
}
|
||||
},
|
||||
// A malformed encrypted message.
|
||||
{
|
||||
channelID: 'subscription3',
|
||||
version: 'v7',
|
||||
send: {
|
||||
headers: {
|
||||
crypto_key: 'dh=AAAAAAAA',
|
||||
encryption: 'salt=AAAAAAAA',
|
||||
},
|
||||
data: 'AAAAAAAA',
|
||||
},
|
||||
ackCode: 101,
|
||||
receive: null,
|
||||
},
|
||||
];
|
||||
|
||||
let sendAndReceive = testData => {
|
||||
let messageReceived = testData.receive ? promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) => {
|
||||
let notification = subject.QueryInterface(Ci.nsIPushMessage).data;
|
||||
equal(notification.text(), testData.receive.data,
|
||||
'Check data for notification ' + testData.version);
|
||||
equal(data, testData.receive.scope,
|
||||
'Check scope for notification ' + testData.version);
|
||||
return true;
|
||||
}) : Promise.resolve();
|
||||
|
||||
let ackReceived = new Promise(resolve => ackDone = resolve)
|
||||
.then(ackData => {
|
||||
deepEqual({
|
||||
messageType: 'ack',
|
||||
updates: [{
|
||||
channelID: testData.channelID,
|
||||
version: testData.version,
|
||||
code: testData.ackCode,
|
||||
}],
|
||||
}, ackData, 'Check updates for acknowledgment ' + testData.version);
|
||||
});
|
||||
|
||||
let msg = JSON.parse(JSON.stringify(testData.send));
|
||||
msg.messageType = 'notification';
|
||||
msg.channelID = testData.channelID;
|
||||
msg.version = testData.version;
|
||||
server.serverSendMsg(JSON.stringify(msg));
|
||||
|
||||
return Promise.all([messageReceived, ackReceived]);
|
||||
};
|
||||
|
||||
yield allTestData.reduce((p, testData) => {
|
||||
return p.then(_ => sendAndReceive(testData));
|
||||
}, Promise.resolve());
|
||||
});
|
||||
140
dom/push/test/xpcshell/test_notification_duplicate.js
Normal file
140
dom/push/test/xpcshell/test_notification_duplicate.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '1500e7d9-8cbe-4ee6-98da-7fa5d6a39852';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
maxRecentMessageIDsPerSubscription: 4,
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Should acknowledge duplicate notifications, but not notify apps.
|
||||
add_task(function* test_notification_duplicate() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
let records = [{
|
||||
channelID: 'has-recents',
|
||||
pushEndpoint: 'https://example.org/update/1',
|
||||
scope: 'https://example.com/1',
|
||||
originAttributes: "",
|
||||
recentMessageIDs: ['dupe'],
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
channelID: 'no-recents',
|
||||
pushEndpoint: 'https://example.org/update/2',
|
||||
scope: 'https://example.com/2',
|
||||
originAttributes: "",
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
channelID: 'dropped-recents',
|
||||
pushEndpoint: 'https://example.org/update/3',
|
||||
scope: 'https://example.com/3',
|
||||
originAttributes: '',
|
||||
recentMessageIDs: ['newest', 'newer', 'older', 'oldest'],
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
let testData = [{
|
||||
channelID: 'has-recents',
|
||||
updates: 1,
|
||||
acks: [{
|
||||
version: 'dupe',
|
||||
code: 102,
|
||||
}, {
|
||||
version: 'not-dupe',
|
||||
code: 100,
|
||||
}],
|
||||
recents: ['not-dupe', 'dupe'],
|
||||
}, {
|
||||
channelID: 'no-recents',
|
||||
updates: 1,
|
||||
acks: [{
|
||||
version: 'not-dupe',
|
||||
code: 100,
|
||||
}],
|
||||
recents: ['not-dupe'],
|
||||
}, {
|
||||
channelID: 'dropped-recents',
|
||||
acks: [{
|
||||
version: 'overflow',
|
||||
code: 100,
|
||||
}, {
|
||||
version: 'oldest',
|
||||
code: 100,
|
||||
}],
|
||||
updates: 2,
|
||||
recents: ['oldest', 'overflow', 'newest', 'newer'],
|
||||
}];
|
||||
|
||||
let expectedUpdates = testData.reduce((sum, {updates}) => sum + updates, 0);
|
||||
let notifiedScopes = [];
|
||||
let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) => {
|
||||
notifiedScopes.push(data);
|
||||
return notifiedScopes.length == expectedUpdates;
|
||||
});
|
||||
|
||||
let expectedAcks = testData.reduce((sum, {acks}) => sum + acks.length, 0);
|
||||
let ackDone;
|
||||
let ackPromise = new Promise(resolve => ackDone = after(expectedAcks, resolve));
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
use_webpush: true,
|
||||
}));
|
||||
for (let {channelID, acks} of testData) {
|
||||
for (let {version} of acks) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
channelID: channelID,
|
||||
version: version,
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
onACK(request) {
|
||||
let [ack] = request.updates;
|
||||
let expectedData = testData.find(test =>
|
||||
test.channelID == ack.channelID);
|
||||
ok(expectedData, `Unexpected channel ${ack.channelID}`);
|
||||
let expectedAck = expectedData.acks.find(expectedAck =>
|
||||
expectedAck.version == ack.version);
|
||||
ok(expectedAck, `Unexpected ack for message ${
|
||||
ack.version} on ${ack.channelID}`);
|
||||
equal(expectedAck.code, ack.code, `Wrong ack status for message ${
|
||||
ack.version} on ${ack.channelID}`);
|
||||
ackDone();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield notifyPromise;
|
||||
yield ackPromise;
|
||||
|
||||
for (let {channelID, recents} of testData) {
|
||||
let record = yield db.getByKeyID(channelID);
|
||||
deepEqual(record.recentMessageIDs, recents,
|
||||
`Wrong recent message IDs for ${channelID}`);
|
||||
}
|
||||
});
|
||||
117
dom/push/test/xpcshell/test_notification_error.js
Normal file
117
dom/push/test/xpcshell/test_notification_error.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '3c7462fc-270f-45be-a459-b9d631b0d093';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_notification_error() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
let originAttributes = '';
|
||||
let records = [{
|
||||
channelID: 'f04f1e46-9139-4826-b2d1-9411b0821283',
|
||||
pushEndpoint: 'https://example.org/update/success-1',
|
||||
scope: 'https://example.com/a',
|
||||
originAttributes: originAttributes,
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
channelID: '3c3930ba-44de-40dc-a7ca-8a133ec1a866',
|
||||
pushEndpoint: 'https://example.org/update/error',
|
||||
scope: 'https://example.com/b',
|
||||
originAttributes: originAttributes,
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
channelID: 'b63f7bef-0a0d-4236-b41e-086a69dfd316',
|
||||
pushEndpoint: 'https://example.org/update/success-2',
|
||||
scope: 'https://example.com/c',
|
||||
originAttributes: originAttributes,
|
||||
version: 3,
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
let scopes = [];
|
||||
let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) =>
|
||||
scopes.push(data) == 2);
|
||||
|
||||
let ackDone;
|
||||
let ackPromise = new Promise(resolve => ackDone = after(records.length, resolve));
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db: makeStub(db, {
|
||||
getByKeyID(prev, channelID) {
|
||||
if (channelID == '3c3930ba-44de-40dc-a7ca-8a133ec1a866') {
|
||||
return Promise.reject('splines not reticulated');
|
||||
}
|
||||
return prev.call(this, channelID);
|
||||
}
|
||||
}),
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: records.map(({channelID, version}) =>
|
||||
({channelID, version: ++version}))
|
||||
}));
|
||||
},
|
||||
// Should acknowledge all received updates, even if updating
|
||||
// IndexedDB fails.
|
||||
onACK: ackDone
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield notifyPromise;
|
||||
ok(scopes.includes('https://example.com/a'),
|
||||
'Missing scope for notification A');
|
||||
ok(scopes.includes('https://example.com/c'),
|
||||
'Missing scope for notification C');
|
||||
|
||||
yield ackPromise;
|
||||
|
||||
let aRecord = yield db.getByIdentifiers({scope: 'https://example.com/a',
|
||||
originAttributes: originAttributes });
|
||||
equal(aRecord.channelID, 'f04f1e46-9139-4826-b2d1-9411b0821283',
|
||||
'Wrong channel ID for record A');
|
||||
strictEqual(aRecord.version, 2,
|
||||
'Should return the new version for record A');
|
||||
|
||||
let bRecord = yield db.getByIdentifiers({scope: 'https://example.com/b',
|
||||
originAttributes: originAttributes });
|
||||
equal(bRecord.channelID, '3c3930ba-44de-40dc-a7ca-8a133ec1a866',
|
||||
'Wrong channel ID for record B');
|
||||
strictEqual(bRecord.version, 2,
|
||||
'Should return the previous version for record B');
|
||||
|
||||
let cRecord = yield db.getByIdentifiers({scope: 'https://example.com/c',
|
||||
originAttributes: originAttributes });
|
||||
equal(cRecord.channelID, 'b63f7bef-0a0d-4236-b41e-086a69dfd316',
|
||||
'Wrong channel ID for record C');
|
||||
strictEqual(cRecord.version, 4,
|
||||
'Should return the new version for record C');
|
||||
});
|
||||
189
dom/push/test/xpcshell/test_notification_http2.js
Normal file
189
dom/push/test/xpcshell/test_notification_http2.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
/* 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;
|
||||
|
||||
var prefs;
|
||||
var tlsProfile;
|
||||
|
||||
var serverPort = -1;
|
||||
|
||||
function run_test() {
|
||||
serverPort = getTestServerPort();
|
||||
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
'testing.allowInsecureServerURL': true,
|
||||
});
|
||||
prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
|
||||
|
||||
tlsProfile = prefs.getBoolPref("network.http.spdy.enforce-tls-profile");
|
||||
|
||||
// Set to allow the cert presented by our H2 server
|
||||
var oldPref = prefs.getIntPref("network.http.speculative-parallel-limit");
|
||||
prefs.setIntPref("network.http.speculative-parallel-limit", 0);
|
||||
prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false);
|
||||
prefs.setBoolPref("dom.push.enabled", true);
|
||||
prefs.setBoolPref("dom.push.connection.enabled", true);
|
||||
|
||||
addCertOverride("localhost", serverPort,
|
||||
Ci.nsICertOverrideService.ERROR_UNTRUSTED |
|
||||
Ci.nsICertOverrideService.ERROR_MISMATCH |
|
||||
Ci.nsICertOverrideService.ERROR_TIME);
|
||||
|
||||
prefs.setIntPref("network.http.speculative-parallel-limit", oldPref);
|
||||
|
||||
run_next_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;
|
||||
|
||||
let records = [{
|
||||
subscriptionUri: serverURL + '/pushNotifications/subscription1',
|
||||
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, inIsolatedMozBrowser: false }),
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/pushNotifications/subscription2',
|
||||
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, inIsolatedMozBrowser: false }),
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/pushNotifications/subscription3',
|
||||
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, inIsolatedMozBrowser: false }),
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/pushNotifications/subscription4',
|
||||
pushEndpoint: serverURL + '/pushEndpoint4',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint4',
|
||||
scope: 'https://example.com/page/4',
|
||||
p256dhPublicKey: ChromeUtils.base64URLDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU', {
|
||||
padding: "reject",
|
||||
}),
|
||||
p256dhPrivateKey: {
|
||||
crv: 'P-256',
|
||||
d: 'fWi7tZaX0Pk6WnLrjQ3kiRq_g5XStL5pdH4pllNCqXw',
|
||||
ext: true,
|
||||
key_ops: ["deriveBits"],
|
||||
kty: 'EC',
|
||||
x: 'Ry8PORYKtS2NT_DKAv3yxtAJCtbWVj2Ku2AaeUJzgHQ',
|
||||
y: 'JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'
|
||||
},
|
||||
authenticationSecret: ChromeUtils.base64URLDecode('cwDVC1iwAn8E37mkR3tMSg', {
|
||||
padding: "reject",
|
||||
}),
|
||||
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) {
|
||||
var message = subject.QueryInterface(Ci.nsIPushMessage).data;
|
||||
if (message && (data == "https://example.com/page/1")){
|
||||
equal(message.text(), "Some message", "decoded message is incorrect");
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
|
||||
var message = subject.QueryInterface(Ci.nsIPushMessage).data;
|
||||
if (message && (data == "https://example.com/page/2")){
|
||||
equal(message.text(), "Some message", "decoded message is incorrect");
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
|
||||
var message = subject.QueryInterface(Ci.nsIPushMessage).data;
|
||||
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).data;
|
||||
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 notifyPromise;
|
||||
});
|
||||
|
||||
add_task(function* test_complete() {
|
||||
prefs.setBoolPref("network.http.spdy.enforce-tls-profile", tlsProfile);
|
||||
});
|
||||
130
dom/push/test/xpcshell/test_notification_incomplete.js
Normal file
130
dom/push/test/xpcshell/test_notification_incomplete.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '1ca1cf66-eeb4-4df7-87c1-d5c92906ab90';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_notification_incomplete() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
let records = [{
|
||||
channelID: '123',
|
||||
pushEndpoint: 'https://example.org/update/1',
|
||||
scope: 'https://example.com/page/1',
|
||||
version: 1,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '3ad1ed95-d37a-4d88-950f-22cbe2e240d7',
|
||||
pushEndpoint: 'https://example.org/update/2',
|
||||
scope: 'https://example.com/page/2',
|
||||
version: 1,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'd239498b-1c85-4486-b99b-205866e82d1f',
|
||||
pushEndpoint: 'https://example.org/update/3',
|
||||
scope: 'https://example.com/page/3',
|
||||
version: 3,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'a50de97d-b496-43ce-8b53-05522feb78db',
|
||||
pushEndpoint: 'https://example.org/update/4',
|
||||
scope: 'https://example.com/page/4',
|
||||
version: 10,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
function observeMessage(subject, topic, data) {
|
||||
ok(false, 'Should not deliver malformed updates');
|
||||
}
|
||||
do_register_cleanup(() =>
|
||||
Services.obs.removeObserver(observeMessage, PushServiceComponent.pushTopic));
|
||||
Services.obs.addObserver(observeMessage, PushServiceComponent.pushTopic, false);
|
||||
|
||||
let notificationDone;
|
||||
let notificationPromise = new Promise(resolve => notificationDone = after(2, resolve));
|
||||
let prevHandler = PushServiceWebSocket._handleNotificationReply;
|
||||
PushServiceWebSocket._handleNotificationReply = function _handleNotificationReply() {
|
||||
notificationDone();
|
||||
return prevHandler.apply(this, arguments);
|
||||
};
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
// Missing "updates" field; should ignore message.
|
||||
messageType: 'notification'
|
||||
}));
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
// Wrong channel ID field type.
|
||||
channelID: 123,
|
||||
version: 3
|
||||
}, {
|
||||
// Missing version field.
|
||||
channelID: '3ad1ed95-d37a-4d88-950f-22cbe2e240d7'
|
||||
}, {
|
||||
// Wrong version field type.
|
||||
channelID: 'd239498b-1c85-4486-b99b-205866e82d1f',
|
||||
version: true
|
||||
}, {
|
||||
// Negative versions should be ignored.
|
||||
channelID: 'a50de97d-b496-43ce-8b53-05522feb78db',
|
||||
version: -5
|
||||
}]
|
||||
}));
|
||||
},
|
||||
onACK() {
|
||||
ok(false, 'Should not acknowledge malformed updates');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield notificationPromise;
|
||||
|
||||
let storeRecords = yield db.getAllKeyIDs();
|
||||
storeRecords.sort(({pushEndpoint: a}, {pushEndpoint: b}) =>
|
||||
compareAscending(a, b));
|
||||
recordsAreEqual(records, storeRecords);
|
||||
});
|
||||
|
||||
function recordIsEqual(a, b) {
|
||||
strictEqual(a.channelID, b.channelID, 'Wrong channel ID in record');
|
||||
strictEqual(a.pushEndpoint, b.pushEndpoint, 'Wrong push endpoint in record');
|
||||
strictEqual(a.scope, b.scope, 'Wrong scope in record');
|
||||
strictEqual(a.version, b.version, 'Wrong version in record');
|
||||
}
|
||||
|
||||
function recordsAreEqual(a, b) {
|
||||
equal(a.length, b.length, 'Mismatched record count');
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
recordIsEqual(a[i], b[i]);
|
||||
}
|
||||
}
|
||||
69
dom/push/test/xpcshell/test_notification_version_string.js
Normal file
69
dom/push/test/xpcshell/test_notification_version_string.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = 'ba31ac13-88d4-4984-8e6b-8731315a7cf8';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_notification_version_string() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
yield db.put({
|
||||
channelID: '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b',
|
||||
pushEndpoint: 'https://example.org/updates/1',
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: '',
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
});
|
||||
|
||||
let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic);
|
||||
|
||||
let ackDone;
|
||||
let ackPromise = new Promise(resolve => ackDone = resolve);
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b',
|
||||
version: '4'
|
||||
}]
|
||||
}));
|
||||
},
|
||||
onACK: ackDone
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let {subject: message, data: scope} = yield notifyPromise;
|
||||
equal(message.QueryInterface(Ci.nsIPushMessage).data, null,
|
||||
'Unexpected data for Simple Push message');
|
||||
|
||||
yield ackPromise;
|
||||
|
||||
let storeRecord = yield db.getByKeyID(
|
||||
'6ff97d56-d0c0-43bc-8f5b-61b855e1d93b');
|
||||
strictEqual(storeRecord.version, 4, 'Wrong record version');
|
||||
equal(storeRecord.quota, Infinity, 'Wrong quota');
|
||||
});
|
||||
42
dom/push/test/xpcshell/test_observer_data.js
Normal file
42
dom/push/test/xpcshell/test_observer_data.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
var pushNotifier = Cc['@mozilla.org/push/Notifier;1']
|
||||
.getService(Ci.nsIPushNotifier);
|
||||
var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_notifyWithData() {
|
||||
let textData = '{"hello":"world"}';
|
||||
let payload = new TextEncoder('utf-8').encode(textData);
|
||||
|
||||
let notifyPromise =
|
||||
promiseObserverNotification(PushServiceComponent.pushTopic);
|
||||
pushNotifier.notifyPushWithData('chrome://notify-test', systemPrincipal,
|
||||
'' /* messageId */, payload.length, payload);
|
||||
|
||||
let data = (yield notifyPromise).subject.QueryInterface(
|
||||
Ci.nsIPushMessage).data;
|
||||
deepEqual(data.json(), {
|
||||
hello: 'world',
|
||||
}, 'Should extract JSON values');
|
||||
deepEqual(data.binary(), Array.from(payload),
|
||||
'Should extract raw binary data');
|
||||
equal(data.text(), textData, 'Should extract text data');
|
||||
});
|
||||
|
||||
add_task(function* test_empty_notifyWithData() {
|
||||
let notifyPromise =
|
||||
promiseObserverNotification(PushServiceComponent.pushTopic);
|
||||
pushNotifier.notifyPushWithData('chrome://notify-test', systemPrincipal,
|
||||
'' /* messageId */, 0, null);
|
||||
|
||||
let data = (yield notifyPromise).subject.QueryInterface(
|
||||
Ci.nsIPushMessage).data;
|
||||
throws(_ => data.json(),
|
||||
'Should throw an error when parsing an empty string as JSON');
|
||||
strictEqual(data.text(), '', 'Should return an empty string');
|
||||
deepEqual(data.binary(), [], 'Should return an empty array');
|
||||
});
|
||||
111
dom/push/test/xpcshell/test_observer_remoting.js
Normal file
111
dom/push/test/xpcshell/test_observer_remoting.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
'use strict';
|
||||
|
||||
const pushNotifier = Cc['@mozilla.org/push/Notifier;1']
|
||||
.getService(Ci.nsIPushNotifier);
|
||||
|
||||
add_task(function* test_observer_remoting() {
|
||||
if (isParent) {
|
||||
yield testInParent();
|
||||
} else {
|
||||
yield testInChild();
|
||||
}
|
||||
});
|
||||
|
||||
const childTests = [{
|
||||
text: 'Hello from child!',
|
||||
principal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
}];
|
||||
|
||||
const parentTests = [{
|
||||
text: 'Hello from parent!',
|
||||
principal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
}];
|
||||
|
||||
function* testInParent() {
|
||||
// Register observers for notifications from the child, then run the test in
|
||||
// the child and wait for the notifications.
|
||||
let promiseNotifications = childTests.reduce(
|
||||
(p, test) => p.then(_ => waitForNotifierObservers(test, /* shouldNotify = */ false)),
|
||||
Promise.resolve()
|
||||
);
|
||||
let promiseFinished = run_test_in_child('./test_observer_remoting.js');
|
||||
yield promiseNotifications;
|
||||
|
||||
// Wait until the child is listening for notifications from the parent.
|
||||
yield do_await_remote_message('push_test_observer_remoting_child_ready');
|
||||
|
||||
// Fire an observer notification in the parent that should be forwarded to
|
||||
// the child.
|
||||
yield parentTests.reduce(
|
||||
(p, test) => p.then(_ => waitForNotifierObservers(test, /* shouldNotify = */ true)),
|
||||
Promise.resolve()
|
||||
);
|
||||
|
||||
// Wait for the child to exit.
|
||||
yield promiseFinished;
|
||||
}
|
||||
|
||||
function* testInChild() {
|
||||
// Fire an observer notification in the child that should be forwarded to
|
||||
// the parent.
|
||||
yield childTests.reduce(
|
||||
(p, test) => p.then(_ => waitForNotifierObservers(test, /* shouldNotify = */ true)),
|
||||
Promise.resolve()
|
||||
);
|
||||
|
||||
// Register observers for notifications from the parent, let the parent know
|
||||
// we're ready, and wait for the notifications.
|
||||
let promiseNotifierObservers = parentTests.reduce(
|
||||
(p, test) => p.then(_ => waitForNotifierObservers(test, /* shouldNotify = */ false)),
|
||||
Promise.resolve()
|
||||
);
|
||||
do_send_remote_message('push_test_observer_remoting_child_ready');
|
||||
yield promiseNotifierObservers;
|
||||
}
|
||||
|
||||
var waitForNotifierObservers = Task.async(function* ({ text, principal }, shouldNotify = false) {
|
||||
let notifyPromise = promiseObserverNotification(
|
||||
PushServiceComponent.pushTopic);
|
||||
let subChangePromise = promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionChangeTopic);
|
||||
let subModifiedPromise = promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionModifiedTopic);
|
||||
|
||||
let scope = 'chrome://test-scope';
|
||||
let data = new TextEncoder('utf-8').encode(text);
|
||||
|
||||
if (shouldNotify) {
|
||||
pushNotifier.notifyPushWithData(scope, principal, '', data.length, data);
|
||||
pushNotifier.notifySubscriptionChange(scope, principal);
|
||||
pushNotifier.notifySubscriptionModified(scope, principal);
|
||||
}
|
||||
|
||||
let {
|
||||
data: notifyScope,
|
||||
subject: notifySubject,
|
||||
} = yield notifyPromise;
|
||||
equal(notifyScope, scope,
|
||||
'Should fire push notifications with the correct scope');
|
||||
let message = notifySubject.QueryInterface(Ci.nsIPushMessage);
|
||||
equal(message.principal, principal,
|
||||
'Should include the principal in the push message');
|
||||
strictEqual(message.data.text(), text, 'Should include data');
|
||||
|
||||
let {
|
||||
data: subChangeScope,
|
||||
subject: subChangePrincipal,
|
||||
} = yield subChangePromise;
|
||||
equal(subChangeScope, scope,
|
||||
'Should fire subscription change notifications with the correct scope');
|
||||
equal(subChangePrincipal, principal,
|
||||
'Should pass the principal as the subject of a change notification');
|
||||
|
||||
let {
|
||||
data: subModifiedScope,
|
||||
subject: subModifiedPrincipal,
|
||||
} = yield subModifiedPromise;
|
||||
equal(subModifiedScope, scope,
|
||||
'Should fire subscription modified notifications with the correct scope');
|
||||
equal(subModifiedPrincipal, principal,
|
||||
'Should pass the principal as the subject of a modified notification');
|
||||
});
|
||||
296
dom/push/test/xpcshell/test_permissions.js
Normal file
296
dom/push/test/xpcshell/test_permissions.js
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '2c43af06-ab6e-476a-adc4-16cbda54fb89';
|
||||
|
||||
let db;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
});
|
||||
|
||||
db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
let unregisterDefers = {};
|
||||
|
||||
function promiseUnregister(keyID) {
|
||||
return new Promise(r => unregisterDefers[keyID] = r);
|
||||
}
|
||||
|
||||
function makePushPermission(url, capability) {
|
||||
return {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPermission]),
|
||||
capability: Ci.nsIPermissionManager[capability],
|
||||
expireTime: 0,
|
||||
expireType: Ci.nsIPermissionManager.EXPIRE_NEVER,
|
||||
principal: Services.scriptSecurityManager.getCodebasePrincipal(
|
||||
Services.io.newURI(url, null, null)
|
||||
),
|
||||
type: 'desktop-notification',
|
||||
};
|
||||
}
|
||||
|
||||
function promiseObserverNotifications(topic, count) {
|
||||
let notifiedScopes = [];
|
||||
let subChangePromise = promiseObserverNotification(topic, (subject, data) => {
|
||||
notifiedScopes.push(data);
|
||||
return notifiedScopes.length == count;
|
||||
});
|
||||
return subChangePromise.then(_ => notifiedScopes.sort());
|
||||
}
|
||||
|
||||
function promiseSubscriptionChanges(count) {
|
||||
return promiseObserverNotifications(
|
||||
PushServiceComponent.subscriptionChangeTopic, count);
|
||||
}
|
||||
|
||||
function promiseSubscriptionModifications(count) {
|
||||
return promiseObserverNotifications(
|
||||
PushServiceComponent.subscriptionModifiedTopic, count);
|
||||
}
|
||||
|
||||
function allExpired(...keyIDs) {
|
||||
return Promise.all(keyIDs.map(
|
||||
keyID => db.getByKeyID(keyID)
|
||||
)).then(records =>
|
||||
records.every(record => record.isExpired())
|
||||
);
|
||||
}
|
||||
|
||||
add_task(function* setUp() {
|
||||
// Active registration; quota should be reset to 16. Since the quota isn't
|
||||
// exposed to content, we shouldn't receive a subscription change event.
|
||||
yield putTestRecord(db, 'active-allow', 'https://example.info/page/1', 8);
|
||||
|
||||
// Expired registration; should be dropped.
|
||||
yield putTestRecord(db, 'expired-allow', 'https://example.info/page/2', 0);
|
||||
|
||||
// Active registration; should be expired when we change the permission
|
||||
// to "deny".
|
||||
yield putTestRecord(db, 'active-deny-changed', 'https://example.xyz/page/1', 16);
|
||||
|
||||
// Two active registrations for a visited site. These will expire when we
|
||||
// add a "deny" permission.
|
||||
yield putTestRecord(db, 'active-deny-added-1', 'https://example.net/ham', 16);
|
||||
yield putTestRecord(db, 'active-deny-added-2', 'https://example.net/green', 8);
|
||||
|
||||
// An already-expired registration for a visited site. We shouldn't send an
|
||||
// `unregister` request for this one, but still receive an observer
|
||||
// notification when we restore permissions.
|
||||
yield putTestRecord(db, 'expired-deny-added', 'https://example.net/eggs', 0);
|
||||
|
||||
// A registration that should not be affected by permission list changes
|
||||
// because its quota is set to `Infinity`.
|
||||
yield putTestRecord(db, 'never-expires', 'app://chrome/only', Infinity);
|
||||
|
||||
// A registration that should be dropped when we clear the permission
|
||||
// list.
|
||||
yield putTestRecord(db, 'drop-on-clear', 'https://example.edu/lonely', 16);
|
||||
|
||||
let handshakeDone;
|
||||
let handshakePromise = new Promise(resolve => handshakeDone = resolve);
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
handshakeDone();
|
||||
},
|
||||
onUnregister(request) {
|
||||
let resolve = unregisterDefers[request.channelID];
|
||||
equal(typeof resolve, 'function',
|
||||
'Dropped unexpected channel ID ' + request.channelID);
|
||||
delete unregisterDefers[request.channelID];
|
||||
equal(request.code, 202,
|
||||
'Expected permission revoked unregister reason');
|
||||
resolve();
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'unregister',
|
||||
status: 200,
|
||||
channelID: request.channelID,
|
||||
}));
|
||||
},
|
||||
onACK(request) {},
|
||||
});
|
||||
}
|
||||
});
|
||||
yield handshakePromise;
|
||||
});
|
||||
|
||||
add_task(function* test_permissions_allow_added() {
|
||||
let subChangePromise = promiseSubscriptionChanges(1);
|
||||
|
||||
yield PushService._onPermissionChange(
|
||||
makePushPermission('https://example.info', 'ALLOW_ACTION'),
|
||||
'added'
|
||||
);
|
||||
let notifiedScopes = yield subChangePromise;
|
||||
|
||||
deepEqual(notifiedScopes, [
|
||||
'https://example.info/page/2',
|
||||
], 'Wrong scopes after adding allow');
|
||||
|
||||
let record = yield db.getByKeyID('active-allow');
|
||||
equal(record.quota, 16,
|
||||
'Should reset quota for active records after adding allow');
|
||||
|
||||
record = yield db.getByKeyID('expired-allow');
|
||||
ok(!record, 'Should drop expired records after adding allow');
|
||||
});
|
||||
|
||||
add_task(function* test_permissions_allow_deleted() {
|
||||
let subModifiedPromise = promiseSubscriptionModifications(1);
|
||||
|
||||
let unregisterPromise = promiseUnregister('active-allow');
|
||||
|
||||
yield PushService._onPermissionChange(
|
||||
makePushPermission('https://example.info', 'ALLOW_ACTION'),
|
||||
'deleted'
|
||||
);
|
||||
|
||||
yield unregisterPromise;
|
||||
|
||||
let notifiedScopes = yield subModifiedPromise;
|
||||
deepEqual(notifiedScopes, [
|
||||
'https://example.info/page/1',
|
||||
], 'Wrong scopes modified after deleting allow');
|
||||
|
||||
let record = yield db.getByKeyID('active-allow');
|
||||
ok(record.isExpired(),
|
||||
'Should expire active record after deleting allow');
|
||||
});
|
||||
|
||||
add_task(function* test_permissions_deny_added() {
|
||||
let subModifiedPromise = promiseSubscriptionModifications(2);
|
||||
|
||||
let unregisterPromise = Promise.all([
|
||||
promiseUnregister('active-deny-added-1'),
|
||||
promiseUnregister('active-deny-added-2'),
|
||||
]);
|
||||
|
||||
yield PushService._onPermissionChange(
|
||||
makePushPermission('https://example.net', 'DENY_ACTION'),
|
||||
'added'
|
||||
);
|
||||
yield unregisterPromise;
|
||||
|
||||
let notifiedScopes = yield subModifiedPromise;
|
||||
deepEqual(notifiedScopes, [
|
||||
'https://example.net/green',
|
||||
'https://example.net/ham',
|
||||
], 'Wrong scopes modified after adding deny');
|
||||
|
||||
let isExpired = yield allExpired(
|
||||
'active-deny-added-1',
|
||||
'expired-deny-added'
|
||||
);
|
||||
ok(isExpired, 'Should expire all registrations after adding deny');
|
||||
});
|
||||
|
||||
add_task(function* test_permissions_deny_deleted() {
|
||||
yield PushService._onPermissionChange(
|
||||
makePushPermission('https://example.net', 'DENY_ACTION'),
|
||||
'deleted'
|
||||
);
|
||||
|
||||
let isExpired = yield allExpired(
|
||||
'active-deny-added-1',
|
||||
'expired-deny-added'
|
||||
);
|
||||
ok(isExpired, 'Should retain expired registrations after deleting deny');
|
||||
});
|
||||
|
||||
add_task(function* test_permissions_allow_changed() {
|
||||
let subChangePromise = promiseSubscriptionChanges(3);
|
||||
|
||||
yield PushService._onPermissionChange(
|
||||
makePushPermission('https://example.net', 'ALLOW_ACTION'),
|
||||
'changed'
|
||||
);
|
||||
|
||||
let notifiedScopes = yield subChangePromise;
|
||||
|
||||
deepEqual(notifiedScopes, [
|
||||
'https://example.net/eggs',
|
||||
'https://example.net/green',
|
||||
'https://example.net/ham'
|
||||
], 'Wrong scopes after changing to allow');
|
||||
|
||||
let droppedRecords = yield Promise.all([
|
||||
db.getByKeyID('active-deny-added-1'),
|
||||
db.getByKeyID('active-deny-added-2'),
|
||||
db.getByKeyID('expired-deny-added'),
|
||||
]);
|
||||
ok(!droppedRecords.some(Boolean),
|
||||
'Should drop all expired registrations after changing to allow');
|
||||
});
|
||||
|
||||
add_task(function* test_permissions_deny_changed() {
|
||||
let subModifiedPromise = promiseSubscriptionModifications(1);
|
||||
|
||||
let unregisterPromise = promiseUnregister('active-deny-changed');
|
||||
|
||||
yield PushService._onPermissionChange(
|
||||
makePushPermission('https://example.xyz', 'DENY_ACTION'),
|
||||
'changed'
|
||||
);
|
||||
|
||||
yield unregisterPromise;
|
||||
|
||||
let notifiedScopes = yield subModifiedPromise;
|
||||
deepEqual(notifiedScopes, [
|
||||
'https://example.xyz/page/1',
|
||||
], 'Wrong scopes modified after changing to deny');
|
||||
|
||||
let record = yield db.getByKeyID('active-deny-changed');
|
||||
ok(record.isExpired(),
|
||||
'Should expire active record after changing to deny');
|
||||
});
|
||||
|
||||
add_task(function* test_permissions_clear() {
|
||||
let subModifiedPromise = promiseSubscriptionModifications(3);
|
||||
|
||||
deepEqual(yield getAllKeyIDs(db), [
|
||||
'active-allow',
|
||||
'active-deny-changed',
|
||||
'drop-on-clear',
|
||||
'never-expires',
|
||||
], 'Wrong records in database before clearing');
|
||||
|
||||
let unregisterPromise = Promise.all([
|
||||
promiseUnregister('active-allow'),
|
||||
promiseUnregister('active-deny-changed'),
|
||||
promiseUnregister('drop-on-clear'),
|
||||
]);
|
||||
|
||||
yield PushService._onPermissionChange(null, 'cleared');
|
||||
|
||||
yield unregisterPromise;
|
||||
|
||||
let notifiedScopes = yield subModifiedPromise;
|
||||
deepEqual(notifiedScopes, [
|
||||
'https://example.edu/lonely',
|
||||
'https://example.info/page/1',
|
||||
'https://example.xyz/page/1',
|
||||
], 'Wrong scopes modified after clearing registrations');
|
||||
|
||||
deepEqual(yield getAllKeyIDs(db), [
|
||||
'never-expires',
|
||||
], 'Unrestricted registrations should not be dropped');
|
||||
});
|
||||
141
dom/push/test/xpcshell/test_quota_exceeded.js
Normal file
141
dom/push/test/xpcshell/test_quota_exceeded.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
const userAgentID = '7eb873f9-8d47-4218-804b-fff78dc04e88';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
'testing.ignorePermission': true,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_expiration_origin_threshold() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => db.drop().then(_ => db.close()));
|
||||
|
||||
yield db.put({
|
||||
channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
|
||||
pushEndpoint: 'https://example.org/push/1',
|
||||
scope: 'https://example.com/auctions',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 16,
|
||||
});
|
||||
yield db.put({
|
||||
channelID: '46cc6f6a-c106-4ffa-bb7c-55c60bd50c41',
|
||||
pushEndpoint: 'https://example.org/push/2',
|
||||
scope: 'https://example.com/deals',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 16,
|
||||
});
|
||||
|
||||
// The notification threshold is per-origin, even with multiple service
|
||||
// workers for different scopes.
|
||||
yield PlacesTestUtils.addVisits([
|
||||
{
|
||||
uri: 'https://example.com/login',
|
||||
title: 'Sign in to see your auctions',
|
||||
visitDate: (Date.now() - 7 * 24 * 60 * 60 * 1000) * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_LINK
|
||||
},
|
||||
// We'll always use your most recent visit to an origin.
|
||||
{
|
||||
uri: 'https://example.com/auctions',
|
||||
title: 'Your auctions',
|
||||
visitDate: (Date.now() - 2 * 24 * 60 * 60 * 1000) * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_LINK
|
||||
},
|
||||
// ...But we won't count downloads or embeds.
|
||||
{
|
||||
uri: 'https://example.com/invoices/invoice.pdf',
|
||||
title: 'Invoice #123',
|
||||
visitDate: (Date.now() - 1 * 24 * 60 * 60 * 1000) * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_EMBED
|
||||
},
|
||||
{
|
||||
uri: 'https://example.com/invoices/invoice.pdf',
|
||||
title: 'Invoice #123',
|
||||
visitDate: Date.now() * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD
|
||||
}
|
||||
]);
|
||||
|
||||
// We expect to receive 6 notifications: 5 on the `auctions` channel,
|
||||
// and 1 on the `deals` channel. They're from the same origin, but
|
||||
// different scopes, so each can send 5 notifications before we remove
|
||||
// their subscription.
|
||||
let updates = 0;
|
||||
let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) => {
|
||||
updates++;
|
||||
return updates == 6;
|
||||
});
|
||||
|
||||
let unregisterDone;
|
||||
let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
|
||||
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
// We last visited the site 2 days ago, so we can send 5
|
||||
// notifications without throttling. Sending a 6th should
|
||||
// drop the registration.
|
||||
for (let version = 1; version <= 6; version++) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
|
||||
version,
|
||||
}],
|
||||
}));
|
||||
}
|
||||
// But the limits are per-channel, so we can send 5 more
|
||||
// notifications on a different channel.
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: '46cc6f6a-c106-4ffa-bb7c-55c60bd50c41',
|
||||
version: 1,
|
||||
}],
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
equal(request.channelID, 'eb33fc90-c883-4267-b5cb-613969e8e349', 'Unregistered wrong channel ID');
|
||||
equal(request.code, 201, 'Expected quota exceeded unregister reason');
|
||||
unregisterDone();
|
||||
},
|
||||
// We expect to receive acks, but don't care about their
|
||||
// contents.
|
||||
onACK(request) {},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield unregisterPromise;
|
||||
|
||||
yield notifyPromise;
|
||||
|
||||
let expiredRecord = yield db.getByKeyID('eb33fc90-c883-4267-b5cb-613969e8e349');
|
||||
strictEqual(expiredRecord.quota, 0, 'Expired record not updated');
|
||||
});
|
||||
183
dom/push/test/xpcshell/test_quota_observer.js
Normal file
183
dom/push/test/xpcshell/test_quota_observer.js
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '28cd09e2-7506-42d8-9e50-b02785adc7ef';
|
||||
|
||||
var db;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
let putRecord = Task.async(function* (perm, record) {
|
||||
let uri = Services.io.newURI(record.scope, null, null);
|
||||
|
||||
Services.perms.add(uri, 'desktop-notification',
|
||||
Ci.nsIPermissionManager[perm]);
|
||||
do_register_cleanup(() => {
|
||||
Services.perms.remove(uri, 'desktop-notification');
|
||||
});
|
||||
|
||||
yield db.put(record);
|
||||
});
|
||||
|
||||
add_task(function* test_expiration_history_observer() {
|
||||
db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => db.drop().then(_ => db.close()));
|
||||
|
||||
// A registration that we'll expire...
|
||||
yield putRecord('ALLOW_ACTION', {
|
||||
channelID: '379c0668-8323-44d2-a315-4ee83f1a9ee9',
|
||||
pushEndpoint: 'https://example.org/push/1',
|
||||
scope: 'https://example.com/deals',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 16,
|
||||
});
|
||||
|
||||
// ...And a registration that we'll evict on startup.
|
||||
yield putRecord('ALLOW_ACTION', {
|
||||
channelID: '4cb6e454-37cf-41c4-a013-4e3a7fdd0bf1',
|
||||
pushEndpoint: 'https://example.org/push/3',
|
||||
scope: 'https://example.com/stuff',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 0,
|
||||
});
|
||||
|
||||
yield PlacesTestUtils.addVisits({
|
||||
uri: 'https://example.com/infrequent',
|
||||
title: 'Infrequently-visited page',
|
||||
visitDate: (Date.now() - 14 * 24 * 60 * 60 * 1000) * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_LINK
|
||||
});
|
||||
|
||||
let unregisterDone;
|
||||
let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
|
||||
let subChangePromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic, (subject, data) =>
|
||||
data == 'https://example.com/stuff');
|
||||
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: '379c0668-8323-44d2-a315-4ee83f1a9ee9',
|
||||
version: 2,
|
||||
}],
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
equal(request.channelID, '379c0668-8323-44d2-a315-4ee83f1a9ee9', 'Dropped wrong channel ID');
|
||||
equal(request.code, 201, 'Expected quota exceeded unregister reason');
|
||||
unregisterDone();
|
||||
},
|
||||
onACK(request) {},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield subChangePromise;
|
||||
yield unregisterPromise;
|
||||
|
||||
let expiredRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9');
|
||||
strictEqual(expiredRecord.quota, 0, 'Expired record not updated');
|
||||
|
||||
let notifiedScopes = [];
|
||||
subChangePromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic, (subject, data) => {
|
||||
notifiedScopes.push(data);
|
||||
return notifiedScopes.length == 2;
|
||||
});
|
||||
|
||||
// Add an expired registration that we'll revive later using the idle
|
||||
// observer.
|
||||
yield putRecord('ALLOW_ACTION', {
|
||||
channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
|
||||
pushEndpoint: 'https://example.org/push/2',
|
||||
scope: 'https://example.com/auctions',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 0,
|
||||
});
|
||||
// ...And an expired registration that we'll revive on fetch.
|
||||
yield putRecord('ALLOW_ACTION', {
|
||||
channelID: '6b2d13fe-d848-4c5f-bdda-e9fc89727dca',
|
||||
pushEndpoint: 'https://example.org/push/4',
|
||||
scope: 'https://example.net/sales',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 0,
|
||||
});
|
||||
|
||||
// Now visit the site...
|
||||
yield PlacesTestUtils.addVisits({
|
||||
uri: 'https://example.com/another-page',
|
||||
title: 'Infrequently-visited page',
|
||||
visitDate: Date.now() * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_LINK
|
||||
});
|
||||
Services.obs.notifyObservers(null, 'idle-daily', '');
|
||||
|
||||
// And we should receive notifications for both scopes.
|
||||
yield subChangePromise;
|
||||
deepEqual(notifiedScopes.sort(), [
|
||||
'https://example.com/auctions',
|
||||
'https://example.com/deals'
|
||||
], 'Wrong scopes for subscription changes');
|
||||
|
||||
let aRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9');
|
||||
ok(!aRecord, 'Should drop expired record');
|
||||
|
||||
let bRecord = yield db.getByKeyID('eb33fc90-c883-4267-b5cb-613969e8e349');
|
||||
ok(!bRecord, 'Should drop evicted record');
|
||||
|
||||
// Simulate a visit to a site with an expired registration, then fetch the
|
||||
// record. This should drop the expired record and fire an observer
|
||||
// notification.
|
||||
yield PlacesTestUtils.addVisits({
|
||||
uri: 'https://example.net/sales',
|
||||
title: 'Firefox plushies, 99% off',
|
||||
visitDate: Date.now() * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_LINK
|
||||
});
|
||||
subChangePromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic, (subject, data) => {
|
||||
if (data == 'https://example.net/sales') {
|
||||
ok(subject.isCodebasePrincipal,
|
||||
'Should pass subscription principal as the subject');
|
||||
return true;
|
||||
}
|
||||
});
|
||||
let record = yield PushService.registration({
|
||||
scope: 'https://example.net/sales',
|
||||
originAttributes: '',
|
||||
});
|
||||
ok(!record, 'Should not return evicted record');
|
||||
ok(!(yield db.getByKeyID('6b2d13fe-d848-4c5f-bdda-e9fc89727dca')),
|
||||
'Should drop evicted record on fetch');
|
||||
yield subChangePromise;
|
||||
});
|
||||
120
dom/push/test/xpcshell/test_quota_with_notification.js
Normal file
120
dom/push/test/xpcshell/test_quota_with_notification.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
const userAgentID = 'aaabf1f8-2f68-44f1-a920-b88e9e7d7559';
|
||||
const nsIPushQuotaManager = Components.interfaces.nsIPushQuotaManager;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
'testing.ignorePermission': true,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_expiration_origin_threshold() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
PushService.notificationForOriginClosed("https://example.com");
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
// Simulate a notification being shown for the origin,
|
||||
// this should relax the quota and allow as many push messages
|
||||
// as we want.
|
||||
PushService.notificationForOriginShown("https://example.com");
|
||||
|
||||
yield db.put({
|
||||
channelID: 'f56645a9-1f32-4655-92ad-ddc37f6d54fb',
|
||||
pushEndpoint: 'https://example.org/push/1',
|
||||
scope: 'https://example.com/quota',
|
||||
pushCount: 0,
|
||||
lastPush: 0,
|
||||
version: null,
|
||||
originAttributes: '',
|
||||
quota: 16,
|
||||
});
|
||||
|
||||
// A visit one day ago should provide a quota of 8 messages.
|
||||
yield PlacesTestUtils.addVisits({
|
||||
uri: 'https://example.com/login',
|
||||
title: 'Sign in to see your auctions',
|
||||
visitDate: (Date.now() - MS_IN_ONE_DAY) * 1000,
|
||||
transition: Ci.nsINavHistoryService.TRANSITION_LINK
|
||||
});
|
||||
|
||||
let numMessages = 10;
|
||||
|
||||
let updates = 0;
|
||||
let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) => {
|
||||
updates++;
|
||||
return updates == numMessages;
|
||||
});
|
||||
|
||||
let modifications = 0;
|
||||
let modifiedPromise = promiseObserverNotification(PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
|
||||
// Each subscription should be modified twice: once to update the message
|
||||
// count and last push time, and the second time to update the quota.
|
||||
modifications++;
|
||||
return modifications == numMessages * 2;
|
||||
});
|
||||
|
||||
let updateQuotaPromise = new Promise((resolve, reject) => {
|
||||
let quotaUpdateCount = 0;
|
||||
PushService._updateQuotaTestCallback = function() {
|
||||
quotaUpdateCount++;
|
||||
if (quotaUpdateCount == numMessages) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
|
||||
// If the origin has visible notifications, the
|
||||
// message should not affect quota.
|
||||
for (let version = 1; version <= 10; version++) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: 'f56645a9-1f32-4655-92ad-ddc37f6d54fb',
|
||||
version,
|
||||
}],
|
||||
}));
|
||||
}
|
||||
},
|
||||
onUnregister(request) {
|
||||
ok(false, "Channel should not be unregistered.");
|
||||
},
|
||||
// We expect to receive acks, but don't care about their
|
||||
// contents.
|
||||
onACK(request) {},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield notifyPromise;
|
||||
|
||||
yield updateQuotaPromise;
|
||||
yield modifiedPromise;
|
||||
|
||||
let expiredRecord = yield db.getByKeyID('f56645a9-1f32-4655-92ad-ddc37f6d54fb');
|
||||
notStrictEqual(expiredRecord.quota, 0, 'Expired record not updated');
|
||||
});
|
||||
73
dom/push/test/xpcshell/test_reconnect_retry.js
Normal file
73
dom/push/test/xpcshell/test_reconnect_retry.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
requestTimeout: 10000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_reconnect_retry() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
let registers = 0;
|
||||
let channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: '083e6c17-1063-4677-8638-ab705aebebc2'
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
registers++;
|
||||
if (registers == 1) {
|
||||
channelID = request.channelID;
|
||||
this.serverClose();
|
||||
return;
|
||||
}
|
||||
if (registers == 2) {
|
||||
equal(request.channelID, channelID,
|
||||
'Should retry registers after reconnect');
|
||||
}
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
channelID: request.channelID,
|
||||
pushEndpoint: 'https://example.org/push/' + request.channelID,
|
||||
status: 200,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let registration = yield PushService.register({
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
let retryEndpoint = 'https://example.org/push/' + channelID;
|
||||
equal(registration.endpoint, retryEndpoint, 'Wrong endpoint for retried request');
|
||||
|
||||
registration = yield PushService.register({
|
||||
scope: 'https://example.com/page/2',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
notEqual(registration.endpoint, retryEndpoint, 'Wrong endpoint for new request');
|
||||
|
||||
equal(registers, 3, 'Wrong registration count');
|
||||
});
|
||||
93
dom/push/test/xpcshell/test_record.js
Normal file
93
dom/push/test/xpcshell/test_record.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
'use strict';
|
||||
|
||||
const {PushRecord} = Cu.import('resource://gre/modules/PushRecord.jsm', {});
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_updateQuota() {
|
||||
let record = new PushRecord({
|
||||
quota: 8,
|
||||
lastPush: Date.now() - 1 * MS_IN_ONE_DAY,
|
||||
});
|
||||
|
||||
record.updateQuota(Date.now() - 2 * MS_IN_ONE_DAY);
|
||||
equal(record.quota, 8,
|
||||
'Should not update quota if last visit is older than last push');
|
||||
|
||||
record.updateQuota(Date.now());
|
||||
equal(record.quota, 16,
|
||||
'Should reset quota if last visit is newer than last push');
|
||||
|
||||
record.reduceQuota();
|
||||
equal(record.quota, 15, 'Should reduce quota');
|
||||
|
||||
// Make sure we calculate the quota correctly for visit dates in the
|
||||
// future (bug 1206424).
|
||||
record.updateQuota(Date.now() + 1 * MS_IN_ONE_DAY);
|
||||
equal(record.quota, 16,
|
||||
'Should reset quota to maximum if last visit is in the future');
|
||||
|
||||
record.updateQuota(-1);
|
||||
strictEqual(record.quota, 0, 'Should set quota to 0 if history was cleared');
|
||||
ok(record.isExpired(), 'Should expire records once the quota reaches 0');
|
||||
record.reduceQuota();
|
||||
strictEqual(record.quota, 0, 'Quota should never be negative');
|
||||
});
|
||||
|
||||
add_task(function* test_systemRecord_updateQuota() {
|
||||
let systemRecord = new PushRecord({
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
});
|
||||
systemRecord.updateQuota(Date.now() - 3 * MS_IN_ONE_DAY);
|
||||
equal(systemRecord.quota, Infinity,
|
||||
'System subscriptions should ignore quota updates');
|
||||
systemRecord.updateQuota(-1);
|
||||
equal(systemRecord.quota, Infinity,
|
||||
'System subscriptions should ignore the last visit time');
|
||||
systemRecord.reduceQuota();
|
||||
equal(systemRecord.quota, Infinity,
|
||||
'System subscriptions should ignore quota reductions');
|
||||
});
|
||||
|
||||
function testPermissionCheck(props) {
|
||||
let record = new PushRecord(props);
|
||||
equal(record.uri.spec, props.scope,
|
||||
`Record URI should match scope URL for ${JSON.stringify(props)}`);
|
||||
if (props.originAttributes) {
|
||||
let originSuffix = ChromeUtils.originAttributesToSuffix(
|
||||
record.principal.originAttributes);
|
||||
equal(originSuffix, props.originAttributes,
|
||||
`Origin suffixes should match for ${JSON.stringify(props)}`);
|
||||
}
|
||||
ok(!record.hasPermission(), `Record ${
|
||||
JSON.stringify(props)} should not have permission yet`);
|
||||
let permURI = Services.io.newURI(props.scope, null, null);
|
||||
Services.perms.add(permURI, 'desktop-notification',
|
||||
Ci.nsIPermissionManager.ALLOW_ACTION);
|
||||
try {
|
||||
ok(record.hasPermission(), `Record ${
|
||||
JSON.stringify(props)} should have permission`);
|
||||
} finally {
|
||||
Services.perms.remove(permURI, 'desktop-notification');
|
||||
}
|
||||
}
|
||||
|
||||
add_task(function* test_principal_permissions() {
|
||||
let testProps = [{
|
||||
scope: 'https://example.com/',
|
||||
}, {
|
||||
scope: 'https://example.com/',
|
||||
originAttributes: '^userContextId=1',
|
||||
}, {
|
||||
scope: 'https://блог.фанфрог.рф/',
|
||||
}, {
|
||||
scope: 'https://блог.фанфрог.рф/',
|
||||
originAttributes: '^userContextId=1',
|
||||
}];
|
||||
for (let props of testProps) {
|
||||
testPermissionCheck(props);
|
||||
}
|
||||
});
|
||||
112
dom/push/test/xpcshell/test_register_5xxCode_http2.js
Normal file
112
dom/push/test/xpcshell/test_register_5xxCode_http2.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/* 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;
|
||||
});
|
||||
|
||||
var retries = 0
|
||||
|
||||
function subscribe5xxCodeHandler(metadata, response) {
|
||||
if (retries == 0) {
|
||||
ok(true, "Subscribe 5xx code");
|
||||
do_test_finished();
|
||||
response.setHeader("Retry-After", '1');
|
||||
response.setStatusLine(metadata.httpVersion, 500, "Retry");
|
||||
} else {
|
||||
ok(true, "Subscribed");
|
||||
do_test_finished();
|
||||
response.setHeader("Location",
|
||||
'http://localhost:' + serverPort + '/subscription')
|
||||
response.setHeader("Link",
|
||||
'</pushEndpoint>; rel="urn:ietf:params:push", ' +
|
||||
'</receiptPushEndpoint>; rel="urn:ietf:params:push:receipt"');
|
||||
response.setStatusLine(metadata.httpVersion, 201, "OK");
|
||||
}
|
||||
retries++;
|
||||
}
|
||||
|
||||
function listenSuccessHandler(metadata, response) {
|
||||
do_check_true(true, "New listener point");
|
||||
ok(retries == 2, "Should try 2 times.");
|
||||
do_test_finished();
|
||||
response.setHeader("Retry-After", '10');
|
||||
response.setStatusLine(metadata.httpVersion, 500, "Retry");
|
||||
}
|
||||
|
||||
|
||||
httpServer = new HttpServer();
|
||||
httpServer.registerPathHandler("/subscribe5xxCode", subscribe5xxCodeHandler);
|
||||
httpServer.registerPathHandler("/subscription", listenSuccessHandler);
|
||||
httpServer.start(-1);
|
||||
|
||||
function run_test() {
|
||||
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
'testing.allowInsecureServerURL': true,
|
||||
'http2.retryInterval': 1000,
|
||||
'http2.maxRetries': 2
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test1() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
do_test_pending();
|
||||
do_test_pending();
|
||||
do_test_pending();
|
||||
do_test_pending();
|
||||
|
||||
var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/subscribe5xxCode",
|
||||
db
|
||||
});
|
||||
|
||||
let originAttributes = ChromeUtils.originAttributesToSuffix({
|
||||
appId: Ci.nsIScriptSecurityManager.NO_APP_ID,
|
||||
inIsolatedMozBrowser: false,
|
||||
});
|
||||
let newRecord = yield PushService.register({
|
||||
scope: 'https://example.com/retry5xxCode',
|
||||
originAttributes: originAttributes,
|
||||
});
|
||||
|
||||
var subscriptionUri = serverURL + '/subscription';
|
||||
var pushEndpoint = serverURL + '/pushEndpoint';
|
||||
var pushReceiptEndpoint = serverURL + '/receiptPushEndpoint';
|
||||
equal(newRecord.endpoint, pushEndpoint,
|
||||
'Wrong push endpoint in registration record');
|
||||
|
||||
equal(newRecord.pushReceiptEndpoint, pushReceiptEndpoint,
|
||||
'Wrong push endpoint receipt in registration record');
|
||||
|
||||
let record = yield db.getByKeyID(subscriptionUri);
|
||||
equal(record.subscriptionUri, subscriptionUri,
|
||||
'Wrong subscription ID in database record');
|
||||
equal(record.pushEndpoint, pushEndpoint,
|
||||
'Wrong push endpoint in database record');
|
||||
equal(record.pushReceiptEndpoint, pushReceiptEndpoint,
|
||||
'Wrong push endpoint receipt in database record');
|
||||
equal(record.scope, 'https://example.com/retry5xxCode',
|
||||
'Wrong scope in database record');
|
||||
|
||||
httpServer.stop(do_test_finished);
|
||||
});
|
||||
56
dom/push/test/xpcshell/test_register_case.js
Normal file
56
dom/push/test/xpcshell/test_register_case.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '1760b1f5-c3ba-40e3-9344-adef7c18ab12';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_case() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'HELLO',
|
||||
uaid: userAgentID,
|
||||
status: 200
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'ReGiStEr',
|
||||
uaid: userAgentID,
|
||||
channelID: request.channelID,
|
||||
status: 200,
|
||||
pushEndpoint: 'https://example.com/update/case'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let newRecord = yield PushService.register({
|
||||
scope: 'https://example.net/case',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
equal(newRecord.endpoint, 'https://example.com/update/case',
|
||||
'Wrong push endpoint in registration record');
|
||||
|
||||
let record = yield db.getByPushEndpoint('https://example.com/update/case');
|
||||
equal(record.scope, 'https://example.net/case',
|
||||
'Wrong scope in database record');
|
||||
});
|
||||
201
dom/push/test/xpcshell/test_register_error_http2.js
Normal file
201
dom/push/test/xpcshell/test_register_error_http2.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/* 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;
|
||||
|
||||
var prefs;
|
||||
var tlsProfile;
|
||||
var serverURL;
|
||||
|
||||
var serverPort = -1;
|
||||
|
||||
function run_test() {
|
||||
serverPort = getTestServerPort();
|
||||
|
||||
do_get_profile();
|
||||
prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
|
||||
|
||||
tlsProfile = prefs.getBoolPref("network.http.spdy.enforce-tls-profile");
|
||||
|
||||
serverURL = "https://localhost:" + serverPort;
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Connection will fail because of the certificates.
|
||||
add_task(function* test_pushSubscriptionNoConnection() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/pushSubscriptionNoConnection/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/invalid-response',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for not being able to establish connecion.'
|
||||
);
|
||||
|
||||
let record = yield db.getAllKeyIDs();
|
||||
ok(record.length === 0, "Should not store records when connection couldn't be established.");
|
||||
PushService.uninit();
|
||||
});
|
||||
|
||||
add_task(function* test_TLS() {
|
||||
// Set to allow the cert presented by our H2 server
|
||||
var oldPref = prefs.getIntPref("network.http.speculative-parallel-limit");
|
||||
prefs.setIntPref("network.http.speculative-parallel-limit", 0);
|
||||
prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false);
|
||||
|
||||
addCertOverride("localhost", serverPort,
|
||||
Ci.nsICertOverrideService.ERROR_UNTRUSTED |
|
||||
Ci.nsICertOverrideService.ERROR_MISMATCH |
|
||||
Ci.nsICertOverrideService.ERROR_TIME);
|
||||
|
||||
prefs.setIntPref("network.http.speculative-parallel-limit", oldPref);
|
||||
});
|
||||
|
||||
add_task(function* test_pushSubscriptionMissingLocation() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/pushSubscriptionMissingLocation/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/invalid-response',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for the missing location header.'
|
||||
);
|
||||
|
||||
let record = yield db.getAllKeyIDs();
|
||||
ok(record.length === 0, 'Should not store records when the location header is missing.');
|
||||
PushService.uninit();
|
||||
});
|
||||
|
||||
add_task(function* test_pushSubscriptionMissingLink() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/pushSubscriptionMissingLink/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/invalid-response',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for the missing link header.'
|
||||
);
|
||||
|
||||
let record = yield db.getAllKeyIDs();
|
||||
ok(record.length === 0, 'Should not store records when a link header is missing.');
|
||||
PushService.uninit();
|
||||
});
|
||||
|
||||
add_task(function* test_pushSubscriptionMissingLink1() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/pushSubscriptionMissingLink1/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/invalid-response',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for the missing push endpoint.'
|
||||
);
|
||||
|
||||
let record = yield db.getAllKeyIDs();
|
||||
ok(record.length === 0, 'Should not store records when the push endpoint is missing.');
|
||||
PushService.uninit();
|
||||
});
|
||||
|
||||
add_task(function* test_pushSubscriptionLocationBogus() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/pushSubscriptionLocationBogus/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/invalid-response',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for the bogus location'
|
||||
);
|
||||
|
||||
let record = yield db.getAllKeyIDs();
|
||||
ok(record.length === 0, 'Should not store records when location header is bogus.');
|
||||
PushService.uninit();
|
||||
});
|
||||
|
||||
add_task(function* test_pushSubscriptionNot2xxCode() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/pushSubscriptionNot201Code/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/invalid-response',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for not 201 responce code.'
|
||||
);
|
||||
|
||||
let record = yield db.getAllKeyIDs();
|
||||
ok(record.length === 0, 'Should not store records when respons code is not 201.');
|
||||
});
|
||||
|
||||
add_task(function* test_complete() {
|
||||
prefs.setBoolPref("network.http.spdy.enforce-tls-profile", tlsProfile);
|
||||
});
|
||||
96
dom/push/test/xpcshell/test_register_flush.js
Normal file
96
dom/push/test/xpcshell/test_register_flush.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '9ce1e6d3-7bdb-4fe9-90a5-def1d64716f1';
|
||||
const channelID = 'c26892c5-6e08-4c16-9f0c-0044697b4d85';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_flush() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
let record = {
|
||||
channelID: '9bcc7efb-86c7-4457-93ea-e24e6eb59b74',
|
||||
pushEndpoint: 'https://example.org/update/1',
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: '',
|
||||
version: 2,
|
||||
quota: Infinity,
|
||||
systemRecord: true,
|
||||
};
|
||||
yield db.put(record);
|
||||
|
||||
let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic);
|
||||
|
||||
let ackDone;
|
||||
let ackPromise = new Promise(resolve => ackDone = after(2, resolve));
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'notification',
|
||||
updates: [{
|
||||
channelID: request.channelID,
|
||||
version: 2
|
||||
}, {
|
||||
channelID: '9bcc7efb-86c7-4457-93ea-e24e6eb59b74',
|
||||
version: 3
|
||||
}]
|
||||
}));
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
channelID: request.channelID,
|
||||
uaid: userAgentID,
|
||||
pushEndpoint: 'https://example.org/update/2'
|
||||
}));
|
||||
},
|
||||
onACK: ackDone
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let newRecord = yield PushService.register({
|
||||
scope: 'https://example.com/page/2',
|
||||
originAttributes: '',
|
||||
});
|
||||
equal(newRecord.endpoint, 'https://example.org/update/2',
|
||||
'Wrong push endpoint in record');
|
||||
|
||||
let {data: scope} = yield notifyPromise;
|
||||
equal(scope, 'https://example.com/page/1', 'Wrong notification scope');
|
||||
|
||||
yield ackPromise;
|
||||
|
||||
let prevRecord = yield db.getByKeyID(
|
||||
'9bcc7efb-86c7-4457-93ea-e24e6eb59b74');
|
||||
equal(prevRecord.pushEndpoint, 'https://example.org/update/1',
|
||||
'Wrong existing push endpoint');
|
||||
strictEqual(prevRecord.version, 3,
|
||||
'Should record version updates sent before register responses');
|
||||
|
||||
let registeredRecord = yield db.getByPushEndpoint('https://example.org/update/2');
|
||||
ok(!registeredRecord.version, 'Should not record premature updates');
|
||||
});
|
||||
57
dom/push/test/xpcshell/test_register_invalid_channel.js
Normal file
57
dom/push/test/xpcshell/test_register_invalid_channel.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '52b2b04c-b6cc-42c6-abdf-bef9cbdbea00';
|
||||
const channelID = 'cafed00d';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_invalid_channel() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushServiceWebSocket._generateID = () => channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
uaid: userAgentID,
|
||||
status: 200
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 403,
|
||||
channelID,
|
||||
error: 'Invalid channel ID'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.com/invalid-channel',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for invalid channel ID'
|
||||
);
|
||||
|
||||
let record = yield db.getByKeyID(channelID);
|
||||
ok(!record, 'Should not store records for error responses');
|
||||
});
|
||||
58
dom/push/test/xpcshell/test_register_invalid_endpoint.js
Normal file
58
dom/push/test/xpcshell/test_register_invalid_endpoint.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = 'c9a12e81-ea5e-40f9-8bf4-acee34621671';
|
||||
const channelID = 'c0660af8-b532-4931-81f0-9fd27a12d6ab';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_invalid_endpoint() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushServiceWebSocket._generateID = () => channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
channelID,
|
||||
uaid: userAgentID,
|
||||
pushEndpoint: '!@#$%^&*'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/invalid-endpoint',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for invalid endpoint'
|
||||
);
|
||||
|
||||
let record = yield db.getByKeyID(channelID);
|
||||
ok(!record, 'Should not store records with invalid endpoints');
|
||||
});
|
||||
58
dom/push/test/xpcshell/test_register_invalid_json.js
Normal file
58
dom/push/test/xpcshell/test_register_invalid_json.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '8271186b-8073-43a3-adf6-225bd44a8b0a';
|
||||
const channelID = '2d08571e-feab-48a0-9f05-8254c3c7e61f';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_invalid_json() {
|
||||
let helloDone;
|
||||
let helloPromise = new Promise(resolve => helloDone = after(2, resolve));
|
||||
let registers = 0;
|
||||
|
||||
PushServiceWebSocket._generateID = () => channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
helloDone();
|
||||
},
|
||||
onRegister(request) {
|
||||
equal(request.channelID, channelID, 'Register: wrong channel ID');
|
||||
this.serverSendMsg(');alert(1);(');
|
||||
registers++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/invalid-json',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for invalid JSON response'
|
||||
);
|
||||
|
||||
yield helloPromise;
|
||||
equal(registers, 1, 'Wrong register count');
|
||||
});
|
||||
62
dom/push/test/xpcshell/test_register_no_id.js
Normal file
62
dom/push/test/xpcshell/test_register_no_id.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
var userAgentID = '9a2f9efe-2ebb-4bcb-a5d9-9e2b73d30afe';
|
||||
var channelID = '264c2ba0-f6db-4e84-acdb-bd225b62d9e3';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_no_id() {
|
||||
let registers = 0;
|
||||
let helloDone;
|
||||
let helloPromise = new Promise(resolve => helloDone = after(2, resolve));
|
||||
|
||||
PushServiceWebSocket._generateID = () => channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
helloDone();
|
||||
},
|
||||
onRegister(request) {
|
||||
registers++;
|
||||
equal(request.channelID, channelID, 'Register: wrong channel ID');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.com/incomplete',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for incomplete register response'
|
||||
);
|
||||
|
||||
yield helloPromise;
|
||||
equal(registers, 1, 'Wrong register count');
|
||||
});
|
||||
61
dom/push/test/xpcshell/test_register_request_queue.js
Normal file
61
dom/push/test/xpcshell/test_register_request_queue.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_request_queue() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
let onHello;
|
||||
let helloPromise = new Promise(resolve => onHello = after(2, function onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: '54b08a9e-59c6-4ed7-bb54-f4fd60d6f606'
|
||||
}));
|
||||
resolve();
|
||||
}));
|
||||
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello,
|
||||
onRegister() {
|
||||
ok(false, 'Should cancel timed-out requests');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let firstRegister = PushService.register({
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
let secondRegister = PushService.register({
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
|
||||
yield Promise.all([
|
||||
rejects(firstRegister, 'Should time out the first request'),
|
||||
rejects(secondRegister, 'Should time out the second request')
|
||||
]);
|
||||
|
||||
yield helloPromise;
|
||||
});
|
||||
87
dom/push/test/xpcshell/test_register_rollback.js
Normal file
87
dom/push/test/xpcshell/test_register_rollback.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = 'b2546987-4f63-49b1-99f7-739cd3c40e44';
|
||||
const channelID = '35a820f7-d7dd-43b3-af21-d65352212ae3';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_rollback() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
let handshakes = 0;
|
||||
let registers = 0;
|
||||
let unregisterDone;
|
||||
let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
|
||||
PushServiceWebSocket._generateID = () => channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db: makeStub(db, {
|
||||
put(prev, record) {
|
||||
return Promise.reject('universe has imploded');
|
||||
}
|
||||
}),
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
handshakes++;
|
||||
equal(request.uaid, userAgentID, 'Handshake: wrong device ID');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
equal(request.channelID, channelID, 'Register: wrong channel ID');
|
||||
registers++;
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
channelID,
|
||||
pushEndpoint: 'https://example.com/update/rollback'
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
equal(request.channelID, channelID, 'Unregister: wrong channel ID');
|
||||
equal(request.code, 200, 'Expected manual unregister reason');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'unregister',
|
||||
status: 200,
|
||||
channelID
|
||||
}));
|
||||
unregisterDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Should return a rejected promise if storage fails.
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.com/storage-error',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for unregister database failure'
|
||||
);
|
||||
|
||||
// Should send an out-of-band unregister request.
|
||||
yield unregisterPromise;
|
||||
equal(handshakes, 1, 'Wrong handshake count');
|
||||
equal(registers, 1, 'Wrong register count');
|
||||
});
|
||||
77
dom/push/test/xpcshell/test_register_success.js
Normal file
77
dom/push/test/xpcshell/test_register_success.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = 'bd744428-f125-436a-b6d0-dd0c9845837f';
|
||||
const channelID = '0ef2ad4a-6c49-41ad-af6e-95d2425276bf';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_success() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushServiceWebSocket._generateID = () => channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(data) {
|
||||
equal(data.messageType, 'hello', 'Handshake: wrong message type');
|
||||
equal(data.uaid, userAgentID, 'Handshake: wrong device ID');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
},
|
||||
onRegister(data) {
|
||||
equal(data.messageType, 'register', 'Register: wrong message type');
|
||||
equal(data.channelID, channelID, 'Register: wrong channel ID');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
channelID: channelID,
|
||||
uaid: userAgentID,
|
||||
pushEndpoint: 'https://example.com/update/1',
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let subModifiedPromise = promiseObserverNotification(
|
||||
PushServiceComponent.subscriptionModifiedTopic);
|
||||
|
||||
let newRecord = yield PushService.register({
|
||||
scope: 'https://example.org/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
equal(newRecord.endpoint, 'https://example.com/update/1',
|
||||
'Wrong push endpoint in registration record');
|
||||
|
||||
let {data: subModifiedScope} = yield subModifiedPromise;
|
||||
equal(subModifiedScope, 'https://example.org/1',
|
||||
'Should fire a subscription modified event after subscribing');
|
||||
|
||||
let record = yield db.getByKeyID(channelID);
|
||||
equal(record.channelID, channelID,
|
||||
'Wrong channel ID in database record');
|
||||
equal(record.pushEndpoint, 'https://example.com/update/1',
|
||||
'Wrong push endpoint in database record');
|
||||
equal(record.quota, 16,
|
||||
'Wrong quota in database record');
|
||||
});
|
||||
128
dom/push/test/xpcshell/test_register_success_http2.js
Normal file
128
dom/push/test/xpcshell/test_register_success_http2.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/* 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;
|
||||
|
||||
var prefs;
|
||||
var tlsProfile;
|
||||
var serverURL;
|
||||
var serverPort = -1;
|
||||
var pushEnabled;
|
||||
var pushConnectionEnabled;
|
||||
var db;
|
||||
|
||||
function run_test() {
|
||||
serverPort = getTestServerPort();
|
||||
|
||||
do_get_profile();
|
||||
prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
|
||||
|
||||
tlsProfile = prefs.getBoolPref("network.http.spdy.enforce-tls-profile");
|
||||
pushEnabled = prefs.getBoolPref("dom.push.enabled");
|
||||
pushConnectionEnabled = prefs.getBoolPref("dom.push.connection.enabled");
|
||||
|
||||
// Set to allow the cert presented by our H2 server
|
||||
var oldPref = prefs.getIntPref("network.http.speculative-parallel-limit");
|
||||
prefs.setIntPref("network.http.speculative-parallel-limit", 0);
|
||||
prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false);
|
||||
prefs.setBoolPref("dom.push.enabled", true);
|
||||
prefs.setBoolPref("dom.push.connection.enabled", true);
|
||||
|
||||
addCertOverride("localhost", serverPort,
|
||||
Ci.nsICertOverrideService.ERROR_UNTRUSTED |
|
||||
Ci.nsICertOverrideService.ERROR_MISMATCH |
|
||||
Ci.nsICertOverrideService.ERROR_TIME);
|
||||
|
||||
prefs.setIntPref("network.http.speculative-parallel-limit", oldPref);
|
||||
|
||||
serverURL = "https://localhost:" + serverPort;
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_setup() {
|
||||
|
||||
db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
add_task(function* test_pushSubscriptionSuccess() {
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/pushSubscriptionSuccess/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
let newRecord = yield PushService.register({
|
||||
scope: 'https://example.org/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
|
||||
var subscriptionUri = serverURL + '/pushSubscriptionSuccesss';
|
||||
var pushEndpoint = serverURL + '/pushEndpointSuccess';
|
||||
var pushReceiptEndpoint = serverURL + '/receiptPushEndpointSuccess';
|
||||
equal(newRecord.endpoint, pushEndpoint,
|
||||
'Wrong push endpoint in registration record');
|
||||
|
||||
equal(newRecord.pushReceiptEndpoint, pushReceiptEndpoint,
|
||||
'Wrong push endpoint receipt in registration record');
|
||||
|
||||
let record = yield db.getByKeyID(subscriptionUri);
|
||||
equal(record.subscriptionUri, subscriptionUri,
|
||||
'Wrong subscription ID in database record');
|
||||
equal(record.pushEndpoint, pushEndpoint,
|
||||
'Wrong push endpoint in database record');
|
||||
equal(record.pushReceiptEndpoint, pushReceiptEndpoint,
|
||||
'Wrong push endpoint receipt in database record');
|
||||
equal(record.scope, 'https://example.org/1',
|
||||
'Wrong scope in database record');
|
||||
|
||||
PushService.uninit()
|
||||
});
|
||||
|
||||
add_task(function* test_pushSubscriptionMissingLink2() {
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/pushSubscriptionMissingLink2/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
let newRecord = yield PushService.register({
|
||||
scope: 'https://example.org/no_receiptEndpoint',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
|
||||
var subscriptionUri = serverURL + '/subscriptionMissingLink2';
|
||||
var pushEndpoint = serverURL + '/pushEndpointMissingLink2';
|
||||
var pushReceiptEndpoint = '';
|
||||
equal(newRecord.endpoint, pushEndpoint,
|
||||
'Wrong push endpoint in registration record');
|
||||
|
||||
equal(newRecord.pushReceiptEndpoint, pushReceiptEndpoint,
|
||||
'Wrong push endpoint receipt in registration record');
|
||||
|
||||
let record = yield db.getByKeyID(subscriptionUri);
|
||||
equal(record.subscriptionUri, subscriptionUri,
|
||||
'Wrong subscription ID in database record');
|
||||
equal(record.pushEndpoint, pushEndpoint,
|
||||
'Wrong push endpoint in database record');
|
||||
equal(record.pushReceiptEndpoint, pushReceiptEndpoint,
|
||||
'Wrong push endpoint receipt in database record');
|
||||
equal(record.scope, 'https://example.org/no_receiptEndpoint',
|
||||
'Wrong scope in database record');
|
||||
});
|
||||
|
||||
add_task(function* test_complete() {
|
||||
prefs.setBoolPref("network.http.spdy.enforce-tls-profile", tlsProfile);
|
||||
prefs.setBoolPref("dom.push.enabled", pushEnabled);
|
||||
prefs.setBoolPref("dom.push.connection.enabled", pushConnectionEnabled);
|
||||
});
|
||||
87
dom/push/test/xpcshell/test_register_timeout.js
Normal file
87
dom/push/test/xpcshell/test_register_timeout.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = 'a4be0df9-b16d-4b5f-8f58-0f93b6f1e23d';
|
||||
const channelID = 'e1944e0b-48df-45e7-bdc0-d1fbaa7986d3';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_timeout() {
|
||||
let handshakes = 0;
|
||||
let timeoutDone;
|
||||
let timeoutPromise = new Promise(resolve => timeoutDone = resolve);
|
||||
let registers = 0;
|
||||
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushServiceWebSocket._generateID = () => channelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
if (handshakes === 0) {
|
||||
equal(request.uaid, null, 'Should not include device ID');
|
||||
} else if (handshakes === 1) {
|
||||
// Should use the previously-issued device ID when reconnecting,
|
||||
// but should not include the timed-out channel ID.
|
||||
equal(request.uaid, userAgentID,
|
||||
'Should include device ID on reconnect');
|
||||
} else {
|
||||
ok(false, 'Unexpected reconnect attempt ' + handshakes);
|
||||
}
|
||||
handshakes++;
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
equal(request.channelID, channelID,
|
||||
'Wrong channel ID in register request');
|
||||
setTimeout(() => {
|
||||
// Should ignore replies for timed-out requests.
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
channelID: channelID,
|
||||
uaid: userAgentID,
|
||||
pushEndpoint: 'https://example.com/update/timeout',
|
||||
}));
|
||||
timeoutDone();
|
||||
}, 2000);
|
||||
registers++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.net/page/timeout',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for request timeout'
|
||||
);
|
||||
|
||||
let record = yield db.getByKeyID(channelID);
|
||||
ok(!record, 'Should not store records for timed-out responses');
|
||||
|
||||
yield timeoutPromise;
|
||||
equal(registers, 1, 'Should not handle timed-out register requests');
|
||||
});
|
||||
68
dom/push/test/xpcshell/test_register_wrong_id.js
Normal file
68
dom/push/test/xpcshell/test_register_wrong_id.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '84afc774-6995-40d1-9c90-8c34ddcd0cb4';
|
||||
const clientChannelID = '4b42a681c99e4dfbbb166a7e01a09b8b';
|
||||
const serverChannelID = '3f5aeb89c6e8405a9569619522783436';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_wrong_id() {
|
||||
// Should reconnect after the register request times out.
|
||||
let registers = 0;
|
||||
let helloDone;
|
||||
let helloPromise = new Promise(resolve => helloDone = after(2, resolve));
|
||||
|
||||
PushServiceWebSocket._generateID = () => clientChannelID;
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
helloDone();
|
||||
},
|
||||
onRegister(request) {
|
||||
equal(request.channelID, clientChannelID,
|
||||
'Register: wrong channel ID');
|
||||
registers++;
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
// Reply with a different channel ID. Since the ID is used as a
|
||||
// nonce, the registration request will time out.
|
||||
channelID: serverChannelID
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.com/mismatched',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for mismatched register reply'
|
||||
);
|
||||
|
||||
yield helloPromise;
|
||||
equal(registers, 1, 'Wrong register count');
|
||||
});
|
||||
62
dom/push/test/xpcshell/test_register_wrong_type.js
Normal file
62
dom/push/test/xpcshell/test_register_wrong_type.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService} = serviceExports;
|
||||
|
||||
const userAgentID = 'c293fdc5-a75e-4eb1-af88-a203991c0787';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
requestTimeout: 1000,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_register_wrong_type() {
|
||||
let registers = 0;
|
||||
let helloDone;
|
||||
let helloPromise = new Promise(resolve => helloDone = after(2, resolve));
|
||||
|
||||
PushService._generateID = () => '1234';
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
helloDone();
|
||||
},
|
||||
onRegister(request) {
|
||||
registers++;
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
channelID: 1234,
|
||||
uaid: userAgentID,
|
||||
pushEndpoint: 'https://example.org/update/wrong-type'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: 'https://example.com/mistyped',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for non-string channel ID'
|
||||
);
|
||||
|
||||
yield helloPromise;
|
||||
equal(registers, 1, 'Wrong register count');
|
||||
});
|
||||
43
dom/push/test/xpcshell/test_registration_error.js
Normal file
43
dom/push/test/xpcshell/test_registration_error.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: '6faed1f0-1439-4aac-a978-db21c81cd5eb'
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_registrations_error() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db: makeStub(db, {
|
||||
getByIdentifiers(prev, scope) {
|
||||
return Promise.reject('Database error');
|
||||
}
|
||||
}),
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri);
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.registration({
|
||||
scope: 'https://example.net/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
function(error) {
|
||||
return error == 'Database error';
|
||||
},
|
||||
'Wrong message'
|
||||
);
|
||||
});
|
||||
37
dom/push/test/xpcshell/test_registration_error_http2.js
Normal file
37
dom/push/test/xpcshell/test_registration_error_http2.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceHttp2} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_registrations_error() {
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushService.init({
|
||||
serverURI: "https://push.example.org/",
|
||||
db: makeStub(db, {
|
||||
getByIdentifiers() {
|
||||
return Promise.reject('Database error');
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.registration({
|
||||
scope: 'https://example.net/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
function(error) {
|
||||
return error == 'Database error';
|
||||
},
|
||||
'Wrong message'
|
||||
);
|
||||
});
|
||||
25
dom/push/test/xpcshell/test_registration_missing_scope.js
Normal file
25
dom/push/test/xpcshell/test_registration_missing_scope.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_registration_missing_scope() {
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri);
|
||||
}
|
||||
});
|
||||
yield rejects(
|
||||
PushService.registration({ scope: '', originAttributes: '' }),
|
||||
'Record missing page and manifest URLs'
|
||||
);
|
||||
});
|
||||
31
dom/push/test/xpcshell/test_registration_none.js
Normal file
31
dom/push/test/xpcshell/test_registration_none.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService} = serviceExports;
|
||||
|
||||
const userAgentID = 'a722e448-c481-4c48-aea0-fc411cb7c9ed';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({userAgentID});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Should not open a connection if the client has no registrations.
|
||||
add_task(function* test_registration_none() {
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri);
|
||||
}
|
||||
});
|
||||
|
||||
let registration = yield PushService.registration({
|
||||
scope: 'https://example.net/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
ok(!registration, 'Should not open a connection without registration');
|
||||
});
|
||||
78
dom/push/test/xpcshell/test_registration_success.js
Normal file
78
dom/push/test/xpcshell/test_registration_success.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '997ee7ba-36b1-4526-ae9e-2d3f38d6efe8';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({userAgentID});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_registration_success() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
let records = [{
|
||||
channelID: 'bf001fe0-2684-42f2-bc4d-a3e14b11dd5b',
|
||||
pushEndpoint: 'https://example.com/update/same-manifest/1',
|
||||
scope: 'https://example.net/a',
|
||||
originAttributes: '',
|
||||
version: 5,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'f6edfbcd-79d6-49b8-9766-48b9dcfeff0f',
|
||||
pushEndpoint: 'https://example.com/update/same-manifest/2',
|
||||
scope: 'https://example.net/b',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: 42 }),
|
||||
version: 10,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: 'b1cf38c9-6836-4d29-8a30-a3e98d59b728',
|
||||
pushEndpoint: 'https://example.org/update/different-manifest',
|
||||
scope: 'https://example.org/c',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: 42, inIsolatedMozBrowser: true }),
|
||||
version: 15,
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
let handshakeDone;
|
||||
let handshakePromise = new Promise(resolve => handshakeDone = resolve);
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
equal(request.uaid, userAgentID, 'Wrong device ID in handshake');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID
|
||||
}));
|
||||
handshakeDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield handshakePromise;
|
||||
|
||||
let registration = yield PushService.registration({
|
||||
scope: 'https://example.net/a',
|
||||
originAttributes: '',
|
||||
});
|
||||
equal(
|
||||
registration.endpoint,
|
||||
'https://example.com/update/same-manifest/1',
|
||||
'Wrong push endpoint for scope'
|
||||
);
|
||||
equal(registration.version, 5, 'Wrong version for scope');
|
||||
});
|
||||
77
dom/push/test/xpcshell/test_registration_success_http2.js
Normal file
77
dom/push/test/xpcshell/test_registration_success_http2.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/* 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;
|
||||
|
||||
var prefs;
|
||||
|
||||
var serverPort = -1;
|
||||
|
||||
function run_test() {
|
||||
serverPort = getTestServerPort();
|
||||
|
||||
do_get_profile();
|
||||
prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_pushNotifications() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
var serverURL = "https://localhost:" + serverPort;
|
||||
|
||||
let records = [{
|
||||
subscriptionUri: serverURL + '/subscriptionA',
|
||||
pushEndpoint: serverURL + '/pushEndpointA',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpointA',
|
||||
scope: 'https://example.net/a',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
quota: Infinity,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/subscriptionB',
|
||||
pushEndpoint: serverURL + '/pushEndpointB',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpointB',
|
||||
scope: 'https://example.net/b',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
quota: Infinity,
|
||||
}, {
|
||||
subscriptionUri: serverURL + '/subscriptionC',
|
||||
pushEndpoint: serverURL + '/pushEndpointC',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpointC',
|
||||
scope: 'https://example.net/c',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
quota: Infinity,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL,
|
||||
db
|
||||
});
|
||||
|
||||
let registration = yield PushService.registration({
|
||||
scope: 'https://example.net/a',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
equal(
|
||||
registration.endpoint,
|
||||
serverURL + '/pushEndpointA',
|
||||
'Wrong push endpoint for scope'
|
||||
);
|
||||
});
|
||||
103
dom/push/test/xpcshell/test_resubscribe_4xxCode_http2.js
Normal file
103
dom/push/test/xpcshell/test_resubscribe_4xxCode_http2.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/* 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;
|
||||
});
|
||||
|
||||
var handlerDone;
|
||||
var handlerPromise = new Promise(r => handlerDone = after(3, r));
|
||||
|
||||
function listen4xxCodeHandler(metadata, response) {
|
||||
ok(true, "Listener point error")
|
||||
handlerDone();
|
||||
response.setStatusLine(metadata.httpVersion, 410, "GONE");
|
||||
}
|
||||
|
||||
function resubscribeHandler(metadata, response) {
|
||||
ok(true, "Ask for new subscription");
|
||||
handlerDone();
|
||||
response.setHeader("Location",
|
||||
'http://localhost:' + serverPort + '/newSubscription')
|
||||
response.setHeader("Link",
|
||||
'</newPushEndpoint>; rel="urn:ietf:params:push", ' +
|
||||
'</newReceiptPushEndpoint>; rel="urn:ietf:params:push:receipt"');
|
||||
response.setStatusLine(metadata.httpVersion, 201, "OK");
|
||||
}
|
||||
|
||||
function listenSuccessHandler(metadata, response) {
|
||||
do_check_true(true, "New listener point");
|
||||
httpServer.stop(handlerDone);
|
||||
response.setStatusLine(metadata.httpVersion, 204, "Try again");
|
||||
}
|
||||
|
||||
|
||||
httpServer = new HttpServer();
|
||||
httpServer.registerPathHandler("/subscription4xxCode", listen4xxCodeHandler);
|
||||
httpServer.registerPathHandler("/subscribe", resubscribeHandler);
|
||||
httpServer.registerPathHandler("/newSubscription", listenSuccessHandler);
|
||||
httpServer.start(-1);
|
||||
|
||||
function run_test() {
|
||||
|
||||
do_get_profile();
|
||||
|
||||
setPrefs({
|
||||
'testing.allowInsecureServerURL': true,
|
||||
'testing.notifyWorkers': false,
|
||||
'testing.notifyAllObservers': true,
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test1() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
|
||||
|
||||
let records = [{
|
||||
subscriptionUri: serverURL + '/subscription4xxCode',
|
||||
pushEndpoint: serverURL + '/pushEndpoint',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
|
||||
scope: 'https://example.com/page',
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield handlerPromise;
|
||||
|
||||
let record = yield db.getByIdentifiers({
|
||||
scope: 'https://example.com/page',
|
||||
originAttributes: '',
|
||||
});
|
||||
equal(record.keyID, serverURL + '/newSubscription',
|
||||
'Should update subscription URL');
|
||||
equal(record.pushEndpoint, serverURL + '/newPushEndpoint',
|
||||
'Should update push endpoint');
|
||||
equal(record.pushReceiptEndpoint, serverURL + '/newReceiptPushEndpoint',
|
||||
'Should update push receipt endpoint');
|
||||
|
||||
});
|
||||
106
dom/push/test/xpcshell/test_resubscribe_5xxCode_http2.js
Normal file
106
dom/push/test/xpcshell/test_resubscribe_5xxCode_http2.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/* 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;
|
||||
});
|
||||
|
||||
var retries = 0;
|
||||
var handlerDone;
|
||||
var handlerPromise = new Promise(r => handlerDone = after(5, r));
|
||||
|
||||
function listen5xxCodeHandler(metadata, response) {
|
||||
ok(true, "Listener 5xx code");
|
||||
handlerDone();
|
||||
retries++;
|
||||
response.setHeader("Retry-After", '1');
|
||||
response.setStatusLine(metadata.httpVersion, 500, "Retry");
|
||||
}
|
||||
|
||||
function resubscribeHandler(metadata, response) {
|
||||
ok(true, "Ask for new subscription");
|
||||
ok(retries == 3, "Should retry 2 times.");
|
||||
handlerDone();
|
||||
response.setHeader("Location",
|
||||
'http://localhost:' + serverPort + '/newSubscription')
|
||||
response.setHeader("Link",
|
||||
'</newPushEndpoint>; rel="urn:ietf:params:push", ' +
|
||||
'</newReceiptPushEndpoint>; rel="urn:ietf:params:push:receipt"');
|
||||
response.setStatusLine(metadata.httpVersion, 201, "OK");
|
||||
}
|
||||
|
||||
function listenSuccessHandler(metadata, response) {
|
||||
do_check_true(true, "New listener point");
|
||||
httpServer.stop(handlerDone);
|
||||
response.setStatusLine(metadata.httpVersion, 204, "Try again");
|
||||
}
|
||||
|
||||
|
||||
httpServer = new HttpServer();
|
||||
httpServer.registerPathHandler("/subscription5xxCode", listen5xxCodeHandler);
|
||||
httpServer.registerPathHandler("/subscribe", resubscribeHandler);
|
||||
httpServer.registerPathHandler("/newSubscription", listenSuccessHandler);
|
||||
httpServer.start(-1);
|
||||
|
||||
function run_test() {
|
||||
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
'testing.allowInsecureServerURL': true,
|
||||
'http2.retryInterval': 1000,
|
||||
'http2.maxRetries': 2
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test1() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
|
||||
|
||||
let records = [{
|
||||
subscriptionUri: serverURL + '/subscription5xxCode',
|
||||
pushEndpoint: serverURL + '/pushEndpoint',
|
||||
pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
|
||||
scope: 'https://example.com/page',
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield handlerPromise;
|
||||
|
||||
let record = yield db.getByIdentifiers({
|
||||
scope: 'https://example.com/page',
|
||||
originAttributes: '',
|
||||
});
|
||||
equal(record.keyID, serverURL + '/newSubscription',
|
||||
'Should update subscription URL');
|
||||
equal(record.pushEndpoint, serverURL + '/newPushEndpoint',
|
||||
'Should update push endpoint');
|
||||
equal(record.pushReceiptEndpoint, serverURL + '/newReceiptPushEndpoint',
|
||||
'Should update push receipt endpoint');
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/* 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;
|
||||
});
|
||||
|
||||
var handlerDone;
|
||||
var handlerPromise = new Promise(r => handlerDone = after(2, r));
|
||||
|
||||
function resubscribeHandler(metadata, response) {
|
||||
ok(true, "Ask for new subscription");
|
||||
handlerDone();
|
||||
response.setHeader("Location",
|
||||
'http://localhost:' + serverPort + '/newSubscription')
|
||||
response.setHeader("Link",
|
||||
'</newPushEndpoint>; rel="urn:ietf:params:push", ' +
|
||||
'</newReceiptPushEndpoint>; rel="urn:ietf:params:push:receipt"');
|
||||
response.setStatusLine(metadata.httpVersion, 201, "OK");
|
||||
}
|
||||
|
||||
function listenSuccessHandler(metadata, response) {
|
||||
do_check_true(true, "New listener point");
|
||||
httpServer.stop(handlerDone);
|
||||
response.setStatusLine(metadata.httpVersion, 204, "Try again");
|
||||
}
|
||||
|
||||
|
||||
httpServer = new HttpServer();
|
||||
httpServer.registerPathHandler("/subscribe", resubscribeHandler);
|
||||
httpServer.registerPathHandler("/newSubscription", listenSuccessHandler);
|
||||
httpServer.start(-1);
|
||||
|
||||
function run_test() {
|
||||
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
'testing.allowInsecureServerURL': true,
|
||||
'http2.retryInterval': 1000,
|
||||
'http2.maxRetries': 2
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test1() {
|
||||
|
||||
let db = PushServiceHttp2.newPushDB();
|
||||
do_register_cleanup(() => {
|
||||
return db.drop().then(_ => db.close());
|
||||
});
|
||||
|
||||
var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
|
||||
|
||||
let records = [{
|
||||
subscriptionUri: 'http://localhost/subscriptionNotExist',
|
||||
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,
|
||||
}];
|
||||
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
PushService.init({
|
||||
serverURI: serverURL + "/subscribe",
|
||||
db
|
||||
});
|
||||
|
||||
yield handlerPromise;
|
||||
|
||||
let record = yield db.getByIdentifiers({
|
||||
scope: 'https://example.com/page',
|
||||
originAttributes: '',
|
||||
});
|
||||
equal(record.keyID, serverURL + '/newSubscription',
|
||||
'Should update subscription URL');
|
||||
equal(record.pushEndpoint, serverURL + '/newPushEndpoint',
|
||||
'Should update push endpoint');
|
||||
equal(record.pushReceiptEndpoint, serverURL + '/newReceiptPushEndpoint',
|
||||
'Should update push receipt endpoint');
|
||||
|
||||
});
|
||||
69
dom/push/test/xpcshell/test_retry_ws.js
Normal file
69
dom/push/test/xpcshell/test_retry_ws.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '05f7b940-51b6-4b6f-8032-b83ebb577ded';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID: userAgentID,
|
||||
pingInterval: 2000,
|
||||
retryBaseInterval: 25,
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_ws_retry() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
yield db.put({
|
||||
channelID: '61770ba9-2d57-4134-b949-d40404630d5b',
|
||||
pushEndpoint: 'https://example.org/push/1',
|
||||
scope: 'https://example.net/push/1',
|
||||
version: 1,
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
});
|
||||
|
||||
// Use a mock timer to avoid waiting for the backoff interval.
|
||||
let reconnects = 0;
|
||||
PushServiceWebSocket._backoffTimer = {
|
||||
init(observer, delay, type) {
|
||||
reconnects++;
|
||||
ok(delay >= 5 && delay <= 2000, `Backoff delay ${
|
||||
delay} out of range for attempt ${reconnects}`);
|
||||
observer.observe(this, "timer-callback", null);
|
||||
},
|
||||
|
||||
cancel() {},
|
||||
};
|
||||
|
||||
let handshakeDone;
|
||||
let handshakePromise = new Promise(resolve => handshakeDone = resolve);
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
if (reconnects == 10) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
}));
|
||||
handshakeDone();
|
||||
return;
|
||||
}
|
||||
this.serverInterrupt();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield handshakePromise;
|
||||
});
|
||||
307
dom/push/test/xpcshell/test_service_child.js
Normal file
307
dom/push/test/xpcshell/test_service_child.js
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
Cu.importGlobalProperties(["crypto"]);
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
var db;
|
||||
|
||||
function done() {
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function generateKey() {
|
||||
return crypto.subtle.generateKey({
|
||||
name: "ECDSA",
|
||||
namedCurve: "P-256",
|
||||
}, true, ["sign", "verify"]).then(cryptoKey =>
|
||||
crypto.subtle.exportKey("raw", cryptoKey.publicKey)
|
||||
).then(publicKey => new Uint8Array(publicKey));
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
if (isParent) {
|
||||
do_get_profile();
|
||||
}
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
if (isParent) {
|
||||
add_test(function setUp() {
|
||||
db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
setUpServiceInParent(PushService, db).then(run_next_test, run_next_test);
|
||||
});
|
||||
}
|
||||
|
||||
add_test(function test_subscribe_success() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.subscribe(
|
||||
'https://example.com/sub/ok',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, subscription) => {
|
||||
ok(Components.isSuccessCode(result), 'Error creating subscription');
|
||||
ok(subscription.isSystemSubscription, 'Expected system subscription');
|
||||
ok(subscription.endpoint.startsWith('https://example.org/push'), 'Wrong endpoint prefix');
|
||||
equal(subscription.pushCount, 0, 'Wrong push count');
|
||||
equal(subscription.lastPush, 0, 'Wrong last push time');
|
||||
equal(subscription.quota, -1, 'Wrong quota for system subscription');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_subscribeWithKey_error() {
|
||||
do_test_pending();
|
||||
|
||||
let invalidKey = [0, 1];
|
||||
PushServiceComponent.subscribeWithKey(
|
||||
'https://example.com/sub-key/invalid',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
invalidKey.length,
|
||||
invalidKey,
|
||||
(result, subscription) => {
|
||||
ok(!Components.isSuccessCode(result), 'Expected error creating subscription with invalid key');
|
||||
equal(result, Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR, 'Wrong error code for invalid key');
|
||||
strictEqual(subscription, null, 'Unexpected subscription');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_subscribeWithKey_success() {
|
||||
do_test_pending();
|
||||
|
||||
generateKey().then(key => {
|
||||
PushServiceComponent.subscribeWithKey(
|
||||
'https://example.com/sub-key/ok',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
key.length,
|
||||
key,
|
||||
(result, subscription) => {
|
||||
ok(Components.isSuccessCode(result), 'Error creating subscription with key');
|
||||
notStrictEqual(subscription, null, 'Expected subscription');
|
||||
done();
|
||||
}
|
||||
);
|
||||
}, error => {
|
||||
ok(false, "Error generating app server key");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_subscribeWithKey_conflict() {
|
||||
do_test_pending();
|
||||
|
||||
generateKey().then(differentKey => {
|
||||
PushServiceComponent.subscribeWithKey(
|
||||
'https://example.com/sub-key/ok',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
differentKey.length,
|
||||
differentKey,
|
||||
(result, subscription) => {
|
||||
ok(!Components.isSuccessCode(result), 'Expected error creating subscription with conflicting key');
|
||||
equal(result, Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR, 'Wrong error code for mismatched key');
|
||||
strictEqual(subscription, null, 'Unexpected subscription');
|
||||
done();
|
||||
}
|
||||
);
|
||||
}, error => {
|
||||
ok(false, "Error generating different app server key");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_subscribe_error() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.subscribe(
|
||||
'https://example.com/sub/fail',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, subscription) => {
|
||||
ok(!Components.isSuccessCode(result), 'Expected error creating subscription');
|
||||
strictEqual(subscription, null, 'Unexpected subscription');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_getSubscription_exists() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.getSubscription(
|
||||
'https://example.com/get/ok',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, subscription) => {
|
||||
ok(Components.isSuccessCode(result), 'Error getting subscription');
|
||||
|
||||
equal(subscription.endpoint, 'https://example.org/push/get', 'Wrong endpoint');
|
||||
equal(subscription.pushCount, 10, 'Wrong push count');
|
||||
equal(subscription.lastPush, 1438360548322, 'Wrong last push');
|
||||
equal(subscription.quota, 16, 'Wrong quota for subscription');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_getSubscription_missing() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.getSubscription(
|
||||
'https://example.com/get/missing',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, subscription) => {
|
||||
ok(Components.isSuccessCode(result), 'Error getting nonexistent subscription');
|
||||
strictEqual(subscription, null, 'Nonexistent subscriptions should return null');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_getSubscription_error() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.getSubscription(
|
||||
'https://example.com/get/fail',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, subscription) => {
|
||||
ok(!Components.isSuccessCode(result), 'Expected error getting subscription');
|
||||
strictEqual(subscription, null, 'Unexpected subscription');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_unsubscribe_success() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.unsubscribe(
|
||||
'https://example.com/unsub/ok',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, success) => {
|
||||
ok(Components.isSuccessCode(result), 'Error unsubscribing');
|
||||
strictEqual(success, true, 'Expected successful unsubscribe');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_unsubscribe_nonexistent() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.unsubscribe(
|
||||
'https://example.com/unsub/ok',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, success) => {
|
||||
ok(Components.isSuccessCode(result), 'Error removing nonexistent subscription');
|
||||
strictEqual(success, false, 'Nonexistent subscriptions should return false');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_unsubscribe_error() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.unsubscribe(
|
||||
'https://example.com/unsub/fail',
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
(result, success) => {
|
||||
ok(!Components.isSuccessCode(result), 'Expected error unsubscribing');
|
||||
strictEqual(success, false, 'Unexpected successful unsubscribe');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_subscribe_app_principal() {
|
||||
let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
|
||||
Services.io.newURI('https://example.net/app/1', null, null),
|
||||
1, /* appId */
|
||||
true /* browserOnly */
|
||||
);
|
||||
|
||||
do_test_pending();
|
||||
PushServiceComponent.subscribe('https://example.net/scope/1', principal, (result, subscription) => {
|
||||
ok(Components.isSuccessCode(result), 'Error creating subscription');
|
||||
ok(subscription.endpoint.startsWith('https://example.org/push'),
|
||||
'Wrong push endpoint in app subscription');
|
||||
ok(!subscription.isSystemSubscription,
|
||||
'Unexpected system subscription for app principal');
|
||||
equal(subscription.quota, 16, 'Wrong quota for app subscription');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_subscribe_origin_principal() {
|
||||
let scope = 'https://example.net/origin-principal';
|
||||
let principal =
|
||||
Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(scope);
|
||||
|
||||
do_test_pending();
|
||||
PushServiceComponent.subscribe(scope, principal, (result, subscription) => {
|
||||
ok(Components.isSuccessCode(result),
|
||||
'Expected error creating subscription with origin principal');
|
||||
ok(!subscription.isSystemSubscription,
|
||||
'Unexpected system subscription for origin principal');
|
||||
equal(subscription.quota, 16, 'Wrong quota for origin subscription');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_subscribe_null_principal() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.subscribe(
|
||||
'chrome://push/null-principal',
|
||||
Services.scriptSecurityManager.createNullPrincipal({}),
|
||||
(result, subscription) => {
|
||||
ok(!Components.isSuccessCode(result),
|
||||
'Expected error creating subscription with null principal');
|
||||
strictEqual(subscription, null,
|
||||
'Unexpected subscription with null principal');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_test(function test_subscribe_missing_principal() {
|
||||
do_test_pending();
|
||||
PushServiceComponent.subscribe('chrome://push/missing-principal', null,
|
||||
(result, subscription) => {
|
||||
ok(!Components.isSuccessCode(result),
|
||||
'Expected error creating subscription without principal');
|
||||
strictEqual(subscription, null,
|
||||
'Unexpected subscription without principal');
|
||||
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (isParent) {
|
||||
add_test(function tearDown() {
|
||||
tearDownServiceInParent(db).then(run_next_test, run_next_test);
|
||||
});
|
||||
}
|
||||
28
dom/push/test/xpcshell/test_service_parent.js
Normal file
28
dom/push/test/xpcshell/test_service_parent.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_service_parent() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
yield setUpServiceInParent(PushService, db);
|
||||
|
||||
// Accessing the lazy service getter will start the service in the main
|
||||
// process.
|
||||
equal(PushServiceComponent.pushTopic, "push-message",
|
||||
"Wrong push message observer topic");
|
||||
equal(PushServiceComponent.subscriptionChangeTopic,
|
||||
"push-subscription-change", "Wrong subscription change observer topic");
|
||||
|
||||
yield run_test_in_child('./test_service_child.js');
|
||||
|
||||
yield tearDownServiceInParent(db);
|
||||
});
|
||||
71
dom/push/test/xpcshell/test_startup_error.js
Normal file
71
dom/push/test/xpcshell/test_startup_error.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
'use strict';
|
||||
|
||||
const {PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
setPrefs();
|
||||
do_get_profile();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_startup_error() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db: makeStub(db, {
|
||||
getAllExpired(prev) {
|
||||
return Promise.reject('database corruption on startup');
|
||||
},
|
||||
}),
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
ok(false, 'Unexpected handshake');
|
||||
},
|
||||
onRegister(request) {
|
||||
ok(false, 'Unexpected register request');
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.register({
|
||||
scope: `https://example.net/1`,
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Should not register if startup failed'
|
||||
);
|
||||
|
||||
PushService.uninit();
|
||||
|
||||
PushService.init({
|
||||
serverURI: 'wss://push.example.org/',
|
||||
db: makeStub(db, {
|
||||
getAllUnexpired(prev) {
|
||||
return Promise.reject('database corruption on connect');
|
||||
},
|
||||
}),
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
ok(false, 'Unexpected handshake');
|
||||
},
|
||||
onRegister(request) {
|
||||
ok(false, 'Unexpected register request');
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
yield rejects(
|
||||
PushService.registration({
|
||||
scope: `https://example.net/1`,
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Should not return registration if connection failed'
|
||||
);
|
||||
});
|
||||
38
dom/push/test/xpcshell/test_unregister_empty_scope.js
Normal file
38
dom/push/test/xpcshell/test_unregister_empty_scope.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_unregister_empty_scope() {
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: '5619557c-86fe-4711-8078-d1fd6987aef7'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.unregister({
|
||||
scope: '',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for empty endpoint'
|
||||
);
|
||||
});
|
||||
68
dom/push/test/xpcshell/test_unregister_error.js
Normal file
68
dom/push/test/xpcshell/test_unregister_error.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const channelID = '00c7fa13-7b71-447d-bd27-a91abc09d1b2';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_unregister_error() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
yield db.put({
|
||||
channelID: channelID,
|
||||
pushEndpoint: 'https://example.org/update/failure',
|
||||
scope: 'https://example.net/page/failure',
|
||||
originAttributes: '',
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
});
|
||||
|
||||
let unregisterDone;
|
||||
let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: '083e6c17-1063-4677-8638-ab705aebebc2'
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
// The server is notified out-of-band. Since channels may be pruned,
|
||||
// any failures are swallowed.
|
||||
equal(request.channelID, channelID, 'Unregister: wrong channel ID');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'unregister',
|
||||
status: 500,
|
||||
error: 'omg, everything is exploding',
|
||||
channelID
|
||||
}));
|
||||
unregisterDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield PushService.unregister({
|
||||
scope: 'https://example.net/page/failure',
|
||||
originAttributes: '',
|
||||
});
|
||||
|
||||
let result = yield db.getByKeyID(channelID);
|
||||
ok(!result, 'Deleted push record exists');
|
||||
|
||||
// Make sure we send a request to the server.
|
||||
yield unregisterPromise;
|
||||
});
|
||||
92
dom/push/test/xpcshell/test_unregister_invalid_json.js
Normal file
92
dom/push/test/xpcshell/test_unregister_invalid_json.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
|
||||
|
||||
const userAgentID = '7f0af1bb-7e1f-4fb8-8e4a-e8de434abde3';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
requestTimeout: 150,
|
||||
retryBaseInterval: 150
|
||||
});
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_unregister_invalid_json() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
let records = [{
|
||||
channelID: '87902e90-c57e-4d18-8354-013f4a556559',
|
||||
pushEndpoint: 'https://example.org/update/1',
|
||||
scope: 'https://example.edu/page/1',
|
||||
originAttributes: '',
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '057caa8f-9b99-47ff-891c-adad18ce603e',
|
||||
pushEndpoint: 'https://example.com/update/2',
|
||||
scope: 'https://example.net/page/1',
|
||||
originAttributes: '',
|
||||
version: 1,
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
let unregisterDone;
|
||||
let unregisterPromise = new Promise(resolve => unregisterDone = after(2, resolve));
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
use_webpush: true,
|
||||
}));
|
||||
},
|
||||
onUnregister(request) {
|
||||
this.serverSendMsg(');alert(1);(');
|
||||
unregisterDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
yield rejects(
|
||||
PushService.unregister({
|
||||
scope: 'https://example.edu/page/1',
|
||||
originAttributes: '',
|
||||
}),
|
||||
'Expected error for first invalid JSON response'
|
||||
);
|
||||
|
||||
let record = yield db.getByKeyID(
|
||||
'87902e90-c57e-4d18-8354-013f4a556559');
|
||||
ok(!record, 'Failed to delete unregistered record');
|
||||
|
||||
yield rejects(
|
||||
PushService.unregister({
|
||||
scope: 'https://example.net/page/1',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
}),
|
||||
'Expected error for second invalid JSON response'
|
||||
);
|
||||
|
||||
record = yield db.getByKeyID(
|
||||
'057caa8f-9b99-47ff-891c-adad18ce603e');
|
||||
ok(!record,
|
||||
'Failed to delete unregistered record after receiving invalid JSON');
|
||||
|
||||
yield unregisterPromise;
|
||||
});
|
||||
36
dom/push/test/xpcshell/test_unregister_not_found.js
Normal file
36
dom/push/test/xpcshell/test_unregister_not_found.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService} = serviceExports;
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_unregister_not_found() {
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: 'f074ed80-d479-44fa-ba65-792104a79ea9'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let result = yield PushService.unregister({
|
||||
scope: 'https://example.net/nonexistent',
|
||||
originAttributes: ChromeUtils.originAttributesToSuffix(
|
||||
{ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
|
||||
});
|
||||
ok(result === false, "unregister should resolve with false for nonexistent scope");
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue