/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
ChromeUtils.defineESModuleGetters(
this, {
HttpServer:
"resource://testing-common/httpd.sys.mjs",
NetUtil:
"resource://gre/modules/NetUtil.sys.mjs",
});
const PingServer = {
_httpServer:
null,
_started:
false,
_defers: [Promise.withResolvers()],
_currentDeferred: 0,
get port() {
return this._httpServer.identity.primaryPort;
},
get host() {
return this._httpServer.identity.primaryHost;
},
get started() {
return this._started;
},
registerPingHandler(handler) {
this._httpServer.registerPrefixHandler(
"/submit/", handler);
},
resetPingHandler() {
this.registerPingHandler(request => {
let r = request;
console.trace(
`defaultPingHandler() - ${r.method} ${r.scheme}:
//${r.host}:${r.port}${r.path}`
);
let deferred =
this._defers[
this._defers.length - 1];
this._defers.push(Promise.withResolvers());
deferred.resolve(request);
});
},
start() {
this._httpServer =
new HttpServer();
this._httpServer.start(-1);
this._started =
true;
this.clearRequests();
this.resetPingHandler();
},
stop() {
return new Promise(resolve => {
this._httpServer.stop(resolve);
this._started =
false;
});
},
clearRequests() {
this._defers = [Promise.withResolvers()];
this._currentDeferred = 0;
},
promiseNextRequest() {
const deferred =
this._defers[
this._currentDeferred++];
// Send the ping to the consumer on the next tick, so that the completion gets
// signaled to Telemetry.
return new Promise(r =>
Services.tm.dispatchToMainThread(() => r(deferred.promise))
);
},
promiseNextPing() {
return this.promiseNextRequest().then(request =>
decodeRequestPayload(request)
);
},
async promiseNextRequests(count) {
let results = [];
for (let i = 0; i < count; ++i) {
results.push(await
this.promiseNextRequest());
}
return results;
},
promiseNextPings(count) {
return this.promiseNextRequests(count).then(requests => {
return Array.from(requests, decodeRequestPayload);
});
},
};
/**
* Decode the payload of an HTTP request into a ping.
*
* @param {object} request The data representing an HTTP request (nsIHttpRequest).
* @returns {object} The decoded ping payload.
*/
function decodeRequestPayload(request) {
let s = request.bodyInputStream;
let payload =
null;
if (
request.hasHeader(
"content-encoding") &&
request.getHeader(
"content-encoding") ==
"gzip"
) {
let observer = {
buffer:
"",
onStreamComplete(loader, context, status, length, result) {
// String.fromCharCode can only deal with 500,000 characters
// at a time, so chunk the result into parts of that size.
const chunkSize = 500000;
for (let offset = 0; offset < result.length; offset += chunkSize) {
this.buffer += String.fromCharCode.apply(
String,
result.slice(offset, offset + chunkSize)
);
}
},
};
let scs = Cc[
"@mozilla.org/streamConverters;1"].getService(
Ci.nsIStreamConverterService
);
let listener = Cc[
"@mozilla.org/network/stream-loader;1"].createInstance(
Ci.nsIStreamLoader
);
listener.init(observer);
let converter = scs.asyncConvertData(
"gzip",
"uncompressed",
listener,
null
);
converter.onStartRequest(
null,
null);
converter.onDataAvailable(
null, s, 0, s.available());
converter.onStopRequest(
null,
null,
null);
// TODO: nsIScriptableUnicodeConverter is deprecated
// But I can't figure out how else to ungzip bodyInputStream.
let unicodeConverter = Cc[
"@mozilla.org/intl/scriptableunicodeconverter"
].createInstance(Ci.nsIScriptableUnicodeConverter);
unicodeConverter.charset =
"UTF-8";
let utf8string = unicodeConverter.ConvertToUnicode(observer.buffer);
utf8string += unicodeConverter.Finish();
payload = JSON.parse(utf8string);
}
else {
let bytes = NetUtil.readInputStream(s, s.available());
payload = JSON.parse(
new TextDecoder().decode(bytes));
}
return payload;
}