Bug 1689547 - Provide a ping encryption plugin (#96)

This commit is contained in:
Beatriz Rizental 2021-03-12 15:17:02 +01:00 коммит произвёл GitHub
Родитель d5e953e792
Коммит 2c1481d612
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 420 добавлений и 106 удалений

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

@ -2,6 +2,12 @@
[Full changelog](https://github.com/mozilla/glean.js/compare/v0.4.0...main)
* [#96](https://github.com/mozilla/glean.js/pull/96): Provide a ping encryption plugin.
* This plugin listens to the `afterPingCollection` event. It receives the collected payload of a ping and returns an encrypted version of it using a JWK provided upon instantiation.
* [#95](https://github.com/mozilla/glean.js/pull/95): Add a `plugins` property to the configuration options and create an event abstraction for triggering internal Glean events.
* The only internal event triggered at this point is the `afterPingCollection` event, which is triggered after ping collection and logging, and before ping storing.
* Plugins are built to listen to a specific Glean event. Each plugin must define an `action`, which is executed everytime the event they are listening to is triggered.
* [#101](https://github.com/mozilla/glean.js/pull/101): BUGFIX: Only validate Debug View Tag and Source Tags when they are present.
* [#101](https://github.com/mozilla/glean.js/pull/101): BUGFIX: Only validate Debug View Tag and Source Tags when they are present.
* [#102](https://github.com/mozilla/glean.js/pull/102): BUGFIX: Include a Glean User-Agent header in all pings.

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

@ -45,7 +45,8 @@
],
"rules": {
"mocha/no-skipped-tests": "off",
"mocha/no-pending-tests": "off"
"mocha/no-pending-tests": "off",
"mocha/no-setup-in-describe": "off"
}
},
{

14
glean/package-lock.json сгенерированный
Просмотреть файл

@ -9,6 +9,7 @@
"version": "0.4.0",
"license": "MPL-2.0",
"dependencies": {
"jose": "^3.7.0",
"uuid": "^8.3.2"
},
"devDependencies": {
@ -2863,6 +2864,14 @@
"node": ">=8"
}
},
"node_modules/jose": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-3.7.0.tgz",
"integrity": "sha512-9vAWSk4zmWH60zbubOZ5Rju3bO6rr5eOUjxuU8snB17Fl2fc7ytFQ6AjtDk8vLzHvecNMj2F4yFkAyurJTpuww==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -7967,6 +7976,11 @@
}
}
},
"jose": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-3.7.0.tgz",
"integrity": "sha512-9vAWSk4zmWH60zbubOZ5Rju3bO6rr5eOUjxuU8snB17Fl2fc7ytFQ6AjtDk8vLzHvecNMj2F4yFkAyurJTpuww=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

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

@ -18,6 +18,11 @@
"browser": "./dist/webext/browser/core/pings/index.js",
"import": "./dist/webext/esm/core/pings/index.js",
"require": "./dist/webext/cjs/core/pings/index.js"
},
"./webext/plugins/*": {
"browser": "./dist/webext/browser/plugins/*.js",
"import": "./dist/webext/esm/plugins/*.js",
"require": "./dist/webext/cjs/plugins/*.js"
}
},
"typesVersions": {
@ -30,6 +35,9 @@
],
"webext/private/metrics/*": [
"./dist/webext/types/core/metrics/types/*"
],
"webext/plugins/*": [
"./dist/webext/types/plugins/*"
]
}
},
@ -39,8 +47,9 @@
"dist/**/*"
],
"scripts": {
"test": "npm run test:core && npm run test:platform",
"test": "npm run test:core && npm run test:platform && npm run test:plugins",
"test:core": "ts-mocha \"tests/core/**/*.spec.ts\" --recursive",
"test:plugins": "ts-mocha \"tests/plugins/**/*.spec.ts\" --recursive",
"test:platform": "npm run build:test-webext && ts-mocha \"tests/platform/**/*.spec.ts\" --recursive --timeout 0",
"build:test-webext": "cd tests/platform/utils/webext/sample/ && npm install && npm run build:xpi",
"lint": "eslint . --ext .ts,.js,.json --max-warnings=0",
@ -96,6 +105,7 @@
"webpack-cli": "^4.5.0"
},
"dependencies": {
"jose": "^3.7.0",
"uuid": "^8.3.2"
}
}

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

@ -12,53 +12,57 @@ export class CoreEvent<
Context extends unknown[] = unknown[],
// The expected type of the action result. To be returned by the plugin.
Result extends unknown = unknown
> {
// The plugin to be triggered eveytime this even occurs.
private plugin?: Plugin<CoreEvent<Context, Result>>;
> {
// The plugin to be triggered eveytime this even occurs.
private plugin?: Plugin<CoreEvent<Context, Result>>;
constructor(readonly name: string) {}
constructor(readonly name: string) {}
/**
* Registers a plugin that listens to this event.
*
* @param plugin The plugin to register.
*/
registerPlugin(plugin: Plugin<CoreEvent<Context, Result>>): void {
if (this.plugin) {
console.error(
`Attempted to register plugin '${plugin.name}', which listens to the event '${plugin.event}'.`,
`That event is already watched by plugin '${this.plugin.name}'`,
`Plugin '${plugin.name}' will be ignored.`
);
return;
}
this.plugin = plugin;
}
/**
* Deregisters the currently registered plugin.
*
* If no plugin is currently registered this is a no-op.
*/
deregisterPlugin(): void {
this.plugin = undefined;
}
get registeredPluginIdentifier(): string | undefined {
return this.plugin?.name;
}
/**
* Triggers this event.
*
* Will execute the action of the registered plugin, if there is any.
*
* @param args The arguments to be passed as context to the registered plugin.
*
* @returns The result from the plugin execution.
*/
trigger(...args: Context): Result | void {
if (this.plugin) {
return this.plugin.action(...args);
}
}
/**
* Registers a plugin that listens to this event.
*
* @param plugin The plugin to register.
*/
registerPlugin(plugin: Plugin<CoreEvent<Context, Result>>): void {
if (this.plugin) {
console.error(
`Attempted to register plugin '${plugin.name}', which listens to the event '${plugin.event}'.`,
`That event is already watched by plugin '${this.plugin.name}'`,
`Plugin '${plugin.name}' will be ignored.`
);
return;
}
this.plugin = plugin;
}
/**
* Deregisters the currently registered plugin.
*
* If no plugin is currently registered this is a no-op.
*/
deregisterPlugin(): void {
this.plugin = undefined;
}
/**
* Triggers this event.
*
* Will execute the action of the registered plugin, if there is any.
*
* @param args The arguments to be passed as context to the registered plugin.
*
* @returns The result from the plugin execution.
*/
trigger(...args: Context): Result | void {
if (this.plugin) {
return this.plugin.action(...args);
}
}
}
/**

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

@ -209,7 +209,7 @@ export async function collectPing(ping: PingType, reason?: string): Promise<Ping
*
* @returns The final submission path.
*/
function makePath(identifier: string, ping: PingType): string {
export function makePath(identifier: string, ping: PingType): string {
// We are sure that the applicationId is not `undefined` at this point,
// this function is only called when submitting a ping
// and that function return early when Glean is not initialized.
@ -237,15 +237,25 @@ export async function collectAndStorePing(identifier: string, ping: PingType, re
return;
}
let modifiedPayload;
try {
modifiedPayload = await CoreEvents.afterPingCollection.trigger(collectedPayload);
} catch(e) {
console.error(
`Error while attempting to modify ping payload for the "${ping.name}" ping using`,
`the ${JSON.stringify(CoreEvents.afterPingCollection.registeredPluginIdentifier)} plugin.`,
"Ping will not be submitted. See more logs below.\n\n",
e
);
return;
}
if (Glean.logPings) {
console.info(JSON.stringify(collectedPayload, null, 2));
}
const headers = getPingHeaders();
const modifiedPayload = await CoreEvents.afterPingCollection.trigger(collectedPayload);
const finalPayload = modifiedPayload ? modifiedPayload : collectedPayload;
const headers = getPingHeaders();
return Glean.pingsDatabase.recordPing(
makePath(identifier, ping),
identifier,

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

@ -0,0 +1,60 @@
/* 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/. */
import CompactEncrypt from "jose/jwe/compact/encrypt";
import parseJwk from "jose/jwk/parse";
import calculateThumbprint from "jose/jwk/thumbprint";
import { JWK } from "jose/types";
import Plugin from "./index";
import { PingPayload } from "../core/pings/database";
import { JSONObject } from "../core/utils";
import CoreEvents from "../core/events";
// These are the chosen defaults, because they are the ones expected by Glean's data pipeline.
//
// That is the case because they are the only algorithm and content encoding pair supported
// by Firefox's hand-rolled JWE implementation.
// See: https://searchfox.org/mozilla-central/rev/eeb8cf278192d68b3977d0adb4d43f1463439269/services/crypto/modules/jwcrypto.jsm#58-74
const JWE_ALGORITHM = "ECDH-ES";
const JWE_CONTENT_ENCODING = "A256GCM";
/**
* A plugin that listens for the `afterPingCollection` event and encrypts **all** outgoing pings
* with the JWK provided upon initialization.
*
* This plugin will modify the schema of outgoing pings to:
*
* ```json
* {
* payload: "<encrypted-payload>"
* }
* ```
*/
class PingEncryptionPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
/**
* Creates a new PingEncryptionPlugin instance.
*
* @param jwk The JWK that will be used to encode outgoing ping payloads.
*/
constructor(private jwk: JWK) {
super(CoreEvents["afterPingCollection"].name, "pingEncryptionPlugin");
}
async action(payload: PingPayload): Promise<JSONObject> {
const key = await parseJwk(this.jwk, JWE_ALGORITHM);
const encoder = new TextEncoder();
const encodedPayload = await new CompactEncrypt(encoder.encode(JSON.stringify(payload)))
.setProtectedHeader({
kid: await calculateThumbprint(this.jwk),
alg: JWE_ALGORITHM,
enc: JWE_CONTENT_ENCODING,
typ: "JWE",
})
.encrypt(key);
return { payload: encodedPayload };
}
}
export default PingEncryptionPlugin;

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

@ -15,7 +15,6 @@ import { JSONObject } from "../../src/core/utils";
import TestPlatform from "../../src/platform/qt";
import Plugin from "../../src/plugins";
const GLOBAL_APPLICATION_ID = "org.mozilla.glean.test.app";
class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
constructor() {
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
@ -29,8 +28,10 @@ class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
const sandbox = sinon.createSandbox();
describe("Glean", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean(GLOBAL_APPLICATION_ID);
await Glean.testResetGlean(testAppId);
});
afterEach(function () {
@ -45,7 +46,7 @@ describe("Glean", function() {
await Glean["coreMetrics"]["firstRunDate"].testGetValue(CLIENT_INFO_STORAGE), undefined);
await Glean.testUninitialize();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
await Glean.testInitialize(testAppId, true);
assert.ok(await Glean["coreMetrics"]["clientId"].testGetValue(CLIENT_INFO_STORAGE));
assert.ok(await Glean["coreMetrics"]["firstRunDate"].testGetValue(CLIENT_INFO_STORAGE));
});
@ -123,7 +124,7 @@ describe("Glean", function() {
it("client_id is set to known value when uploading disabled at start", async function() {
await Glean.testUninitialize();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, false);
await Glean.testInitialize(testAppId, false);
assert.strictEqual(
await Glean["coreMetrics"]["clientId"].testGetValue(CLIENT_INFO_STORAGE),
KNOWN_CLIENT_ID
@ -133,7 +134,7 @@ describe("Glean", function() {
it("client_id is set to random value when uploading enabled at start", async function() {
Glean.setUploadEnabled(false);
await Glean.testUninitialize();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
await Glean.testInitialize(testAppId, true);
const clientId = await Glean["coreMetrics"]["clientId"]
.testGetValue(CLIENT_INFO_STORAGE);
assert.ok(clientId);
@ -170,7 +171,7 @@ describe("Glean", function() {
it("initialization throws if serverEndpoint is an invalida URL", async function() {
await Glean.testUninitialize();
try {
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, { serverEndpoint: "" });
await Glean.testInitialize(testAppId, true, { serverEndpoint: "" });
assert.ok(false);
} catch {
assert.ok(true);
@ -181,7 +182,7 @@ describe("Glean", function() {
await Glean.testUninitialize();
const mockPlugin = new MockPlugin();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, {
await Glean.testInitialize(testAppId, true, {
// We need to ignore TypeScript here,
// otherwise it will error since mockEvent is not listed as a Glean event in core/events.ts
//
@ -193,7 +194,7 @@ describe("Glean", function() {
assert.deepStrictEqual(CoreEvents["afterPingCollection"]["plugin"], mockPlugin);
await Glean.testUninitialize();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
await Glean.testInitialize(testAppId, true);
assert.strictEqual(CoreEvents["afterPingCollection"]["plugin"], undefined);
});
@ -221,12 +222,12 @@ describe("Glean", function() {
it("initializing twice is a no-op", async function() {
await Glean.testUninitialize();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
await Glean.testInitialize(testAppId, true);
// initialize is dispatched, we must await on the queue being completed to assert things.
await Glean.dispatcher.testBlockOnQueue();
// This time it should not be called, which means upload should not be switched to `false`.
await Glean.testInitialize(GLOBAL_APPLICATION_ID, false);
await Glean.testInitialize(testAppId, false);
assert.ok(Glean.isUploadEnabled());
});
@ -241,7 +242,7 @@ describe("Glean", function() {
const postSpy = sandbox.spy(Glean.platform.uploader, "post");
// Start Glean with upload enabled.
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
await Glean.testInitialize(testAppId, true);
// Immediatelly disable upload.
Glean.setUploadEnabled(false);
ping.submit();
@ -280,7 +281,7 @@ describe("Glean", function() {
// Can't use testResetGlean here because it clears all stores
// and when there is no client_id at all stored, a deletion ping is also not sent.
await Glean.testUninitialize();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, false);
await Glean.testInitialize(testAppId, false);
// TODO: Make this nicer once Bug 1691033 is resolved.
await Glean["pingUploader"]["currentJob"];
@ -303,7 +304,7 @@ describe("Glean", function() {
// Can't use testResetGlean here because it clears all stores
// and when there is no client_id at all stored, a deletion ping is also not set.
await Glean.testUninitialize();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, false);
await Glean.testInitialize(testAppId, false);
await Glean.dispatcher.testBlockOnQueue();
// TODO: Make this nicer once we resolve Bug 1691033 is resolved.
await Glean["pingUploader"]["currentJob"];
@ -314,7 +315,7 @@ describe("Glean", function() {
it("deletion request ping is not sent when user starts Glean for the first time with upload disabled", async function () {
const postSpy = sandbox.spy(Glean.platform.uploader, "post");
await Glean.testResetGlean(GLOBAL_APPLICATION_ID, false);
await Glean.testResetGlean(testAppId, false);
assert.strictEqual(postSpy.callCount, 0);
});
@ -322,7 +323,7 @@ describe("Glean", function() {
await Glean.testUninitialize();
// Setting on initialize.
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, { debug: { logPings: true } });
await Glean.testInitialize(testAppId, true, { debug: { logPings: true } });
await Glean.dispatcher.testBlockOnQueue();
assert.ok(Glean.logPings);
@ -330,7 +331,7 @@ describe("Glean", function() {
// Setting before initialize.
Glean.setLogPings(true);
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
await Glean.testInitialize(testAppId, true);
await Glean.dispatcher.testBlockOnQueue();
assert.ok(Glean.logPings);
@ -345,7 +346,7 @@ describe("Glean", function() {
const testTag = "test";
// Setting on initialize.
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, { debug: { debugViewTag: testTag } });
await Glean.testInitialize(testAppId, true, { debug: { debugViewTag: testTag } });
await Glean.dispatcher.testBlockOnQueue();
assert.strictEqual(Glean.debugViewTag, testTag);
@ -353,7 +354,7 @@ describe("Glean", function() {
// Setting before initialize.
Glean.setDebugViewTag(testTag);
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true);
await Glean.testInitialize(testAppId, true);
await Glean.dispatcher.testBlockOnQueue();
assert.strictEqual(Glean.debugViewTag, testTag);
@ -386,7 +387,7 @@ describe("Glean", function() {
it("setting source tags on initialize works", async function () {
await Glean.testUninitialize();
await Glean.testInitialize(GLOBAL_APPLICATION_ID, true, { debug: { sourceTags: ["1", "2", "3", "4", "5"] } });
await Glean.testInitialize(testAppId, true, { debug: { sourceTags: ["1", "2", "3", "4", "5"] } });
await Glean.dispatcher.testBlockOnQueue();
assert.strictEqual(Glean.sourceTags, "1,2,3,4,5");
});

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

@ -9,8 +9,10 @@ import BooleanMetricType from "../../../src/core/metrics/types/boolean";
import { Lifetime } from "../../../src/core/metrics";
describe("BooleanMetric", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("attemping to get the value of a metric that hasn't been recorded doesn't error", async function() {

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

@ -7,10 +7,12 @@ import assert from "assert";
import Glean from "../../../src/core/glean";
import CounterMetricType from "../../../src/core/metrics/types/counter";
import { Lifetime } from "../../../src/core/metrics";
describe("CounterMetric", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("attemping to get the value of a metric that hasn't been recorded doesn't error", async function() {

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

@ -11,8 +11,10 @@ import { JSONValue } from "../../../src/core/utils";
import Glean from "../../../src/core/glean";
describe("MetricsDatabase", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
describe("record", function() {
@ -483,9 +485,6 @@ describe("MetricsDatabase", function() {
});
it("clears data from specific ping when specified", async function () {
// Must initialize Glean, otherwise `testGetValue` will hang forever.
await Glean.testResetGlean("something something", true);
const metric = new StringMetricType({
category: "some",
name: "metric",

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

@ -13,12 +13,14 @@ import TimeUnit from "../../../src/core/metrics/time_unit";
const sandbox = sinon.createSandbox();
describe("DatetimeMetric", function() {
const testAppId = `gleanjs.test.${this.title}`;
afterEach(function () {
sandbox.restore();
});
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("datetime internal representation validation works as expected", function () {

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

@ -18,8 +18,10 @@ class TestEventMetricType extends EventMetricType {
}
describe("EventMetric", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("the API records to its storage engine", async function () {

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

@ -11,8 +11,10 @@ import EventMetricType from "../../../src/core/metrics/types/event";
import { JSONObject } from "../../../src/core/utils";
describe("EventsDatabase", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("stable serialization", function () {

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

@ -9,8 +9,10 @@ import StringMetricType, { MAX_LENGTH_VALUE } from "../../../src/core/metrics/ty
import { Lifetime } from "../../../src/core/metrics";
describe("StringMetric", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("attemping to get the value of a metric that hasn't been recorded doesn't error", async function() {

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

@ -10,8 +10,10 @@ import UUIDMetricType from "../../../src/core/metrics/types/uuid";
import { Lifetime } from "../../../src/core/metrics";
describe("UUIDMetric", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("attemping to get the value of a metric that hasn't been recorded doesn't error", async function() {

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

@ -8,8 +8,10 @@ import Database, { Observer, isValidPingInternalRepresentation } from "../../../
import Glean from "../../../src/core/glean";
describe("PingsDatabase", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
describe("record", function () {

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

@ -24,12 +24,14 @@ async function submitSync(ping: PingType): Promise<void> {
}
describe("PingType", function() {
const testAppId = `gleanjs.test.${this.title}`;
afterEach(function () {
sandbox.restore();
});
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("collects and stores ping on submit", async function () {

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

@ -14,9 +14,21 @@ import { JSONObject } from "../../../src/core/utils";
const sandbox = sinon.createSandbox();
class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
constructor() {
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
}
action(): Promise<JSONObject> {
return Promise.resolve({ "you": "got mocked!" });
}
}
describe("PingMaker", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
afterEach(function () {
@ -61,7 +73,7 @@ describe("PingMaker", function() {
assert.ok("telemetry_sdk_build" in clientInfo1);
// Initialize will also initialize core metrics that are part of the client info.
await Glean.testInitialize("something something", true, {
await Glean.testInitialize(testAppId, true, {
appBuild:"build",
appDisplayVersion: "display version",
serverEndpoint: "http://localhost:8080"
@ -131,18 +143,7 @@ describe("PingMaker", function() {
// Disable ping uploading for it not to interfere with this tests.
sandbox.stub(Glean["pingUploader"], "triggerUpload").callsFake(() => Promise.resolve());
class MockPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
constructor() {
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
}
action(): Promise<JSONObject> {
return Promise.resolve({ "you": "got mocked!" });
}
}
await Glean.testUninitialize();
await Glean.testInitialize("something something", true, { plugins: [ new MockPlugin() ]});
await Glean.testResetGlean(testAppId, true, { plugins: [ new MockPlugin() ]});
const ping = new PingType({
name: "ping",
includeClientId: true,
@ -153,10 +154,68 @@ describe("PingMaker", function() {
const recordedPing = (await Glean.pingsDatabase.getAllPings())["ident"];
assert.deepStrictEqual(recordedPing.payload, { "you": "got mocked!" });
await Glean.testUninitialize();
await Glean.testInitialize("something something", true);
await Glean.testResetGlean(testAppId, true);
await PingMaker.collectAndStorePing("ident", ping);
const recordedPingNoPlugin = (await Glean.pingsDatabase.getAllPings())["ident"];
assert.notDeepStrictEqual(recordedPingNoPlugin.payload, { "you": "got mocked!" });
});
it("ping payload is logged before it is modified by a plugin", async function () {
// Disable ping uploading for it not to interfere with this tests.
sandbox.stub(Glean["pingUploader"], "triggerUpload").callsFake(() => Promise.resolve());
await Glean.testResetGlean(testAppId, true, {
debug: {
logPings: true
},
plugins: [ new MockPlugin() ]
});
const ping = new PingType({
name: "ping",
includeClientId: true,
sendIfEmpty: true,
});
const consoleSpy = sandbox.spy(console, "info");
await PingMaker.collectAndStorePing("ident", ping);
const loggedPayload = JSON.parse(consoleSpy.lastCall.args[0]) as JSONObject;
const recordedPing = (await Glean.pingsDatabase.getAllPings())["ident"];
assert.deepStrictEqual(recordedPing.payload, { "you": "got mocked!" });
assert.notDeepStrictEqual(loggedPayload, { "you": "got mocked!" });
assert.ok("client_info" in loggedPayload);
assert.ok("ping_info" in loggedPayload);
});
it("pings are not recorded in case a plugin throws", async function () {
class ThrowingPlugin extends Plugin<typeof CoreEvents["afterPingCollection"]> {
constructor() {
super(CoreEvents["afterPingCollection"].name, "mockPlugin");
}
action(): Promise<JSONObject> {
throw new Error();
}
}
await Glean.testResetGlean(testAppId, true, {
debug: {
logPings: true
},
plugins: [ new ThrowingPlugin() ]
});
const ping = new PingType({
name: "ping",
includeClientId: true,
sendIfEmpty: true,
});
await PingMaker.collectAndStorePing("ident", ping);
const recordedPings = await Glean.pingsDatabase.getAllPings();
assert.ok(!("ident" in recordedPings));
});
});

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

@ -55,12 +55,14 @@ function disableGleanUploader(): void {
}
describe("PingUploader", function() {
const testAppId = `gleanjs.test.${this.title}`;
afterEach(function () {
sandbox.restore();
});
beforeEach(async function() {
await Glean.testResetGlean("something something");
await Glean.testResetGlean(testAppId);
});
it("scanning the pending pings directory fills up the queue", async function() {

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

@ -0,0 +1,114 @@
/* 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/. */
import assert from "assert";
import sinon from "sinon";
import generateKeyPair from "jose/util/generate_key_pair";
import fromKeyLike from "jose/jwk/from_key_like";
import compactDecrypt from "jose/jwe/compact/decrypt";
import Glean from "../../src/core/glean";
import PingType from "../../src/core/pings";
import { JSONObject } from "../../src/core/utils";
import TestPlatform from "../../src/platform/qt";
import PingEncryptionPlugin from "../../src/plugins/encryption";
import collectAndStorePing, { makePath } from "../../src/core/pings/maker";
const sandbox = sinon.createSandbox();
describe("PingEncryptionPlugin", function() {
const testAppId = `gleanjs.test.${this.title}`;
beforeEach(async function() {
await Glean.testResetGlean(testAppId);
});
afterEach(function () {
sandbox.restore();
});
it("collect and store triggers the AfterPingCollection and deals with possible result correctly", async function () {
await Glean.testResetGlean(
testAppId,
true,
{
plugins: [
new PingEncryptionPlugin({
"crv": "P-256",
"kid": "test",
"kty": "EC",
"x": "Q20tsJdrryWJeuPXTM27wIPb_YbsdYPpkK2N9O6aXwM",
"y": "1onW1swaCcN1jkmkIwhXpCm55aMP8GRJln5E8WQKLJk"
})
]
}
);
const ping = new PingType({
name: "test",
includeClientId: true,
sendIfEmpty: true,
});
const pingId = "ident";
const postSpy = sandbox.spy(TestPlatform.uploader, "post").withArgs(
sinon.match(makePath(pingId, ping)),
sinon.match.string
);
await collectAndStorePing(pingId, ping);
assert.ok(postSpy.calledOnce);
const payload = JSON.parse(postSpy.args[0][1]) as JSONObject;
assert.ok("payload" in payload);
assert.ok(typeof payload.payload === "string");
});
it("decrypting encrypted ping returns expected payload and protected headers", async function () {
// Disable ping uploading for it not to interfere with this tests.
sandbox.stub(Glean["pingUploader"], "triggerUpload").callsFake(() => Promise.resolve());
const { publicKey, privateKey } = await generateKeyPair("ECDH-ES");
await Glean.testResetGlean(
testAppId,
true,
{
plugins: [
new PingEncryptionPlugin(await fromKeyLike(publicKey))
],
debug: {
logPings: true
}
}
);
const ping = new PingType({
name: "test",
includeClientId: true,
sendIfEmpty: true,
});
const pingId = "ident";
const consoleSpy = sandbox.spy(console, "info");
await collectAndStorePing(pingId, ping);
const preEncryptionPayload = JSON.parse(consoleSpy.lastCall.args[0]) as JSONObject;
const encryptedPayload = (await Glean.pingsDatabase.getAllPings())[pingId]["payload"]["payload"];
const { plaintext, protectedHeader } = await compactDecrypt(
encryptedPayload as string,
privateKey
);
const decoder = new TextDecoder();
const decodedPayload = JSON.parse(decoder.decode(plaintext)) as JSONObject;
assert.deepStrictEqual(decodedPayload, preEncryptionPayload);
assert.ok("kid" in protectedHeader);
assert.ok("alg" in protectedHeader);
assert.ok("enc" in protectedHeader);
assert.ok("typ" in protectedHeader);
});
});

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

@ -1,6 +1,10 @@
{
"extends": "../base.json",
"include": ["../../src/index/webext.ts", "../../src/core/metrics/types/*.ts"],
"include": [
"../../src/index/webext.ts",
"../../src/core/metrics/types/*.ts",
"../../src/plugins/*.ts"
],
"compilerOptions": {
"target": "ES2018",
"module": "ES6",

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

@ -1,6 +1,10 @@
{
"extends": "../base.json",
"include": ["../../src/index/webext.ts", "../../src/core/metrics/types/*.ts"],
"include": [
"../../src/index/webext.ts",
"../../src/core/metrics/types/*.ts",
"../../src/plugins/*.ts"
],
"compilerOptions": {
"target": "ES2019",
"module": "CommonJS",

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

@ -1,6 +1,10 @@
{
"extends": "../base.json",
"include": ["../../src/index/webext.ts", "../../src/core/metrics/types/*.ts"],
"include": [
"../../src/index/webext.ts",
"../../src/core/metrics/types/*.ts",
"../../src/plugins/*.ts"
],
"compilerOptions": {
"target": "ES2019",
"module": "ES2020",

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

@ -1,6 +1,10 @@
{
"extends": "../base.json",
"include": ["../../src/index/webext.ts", "../../src/core/metrics/types/*.ts"],
"include": [
"../../src/index/webext.ts",
"../../src/core/metrics/types/*.ts",
"../../src/plugins/*.ts"
],
"compilerOptions": {
"outDir": "../../dist/webext/types",
"removeComments": false,