mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
9b928cab7a
It was observed that FHR was sending invalid JSON payloads to the server. Specifically, JSON payloads contained invalid Unicode strings. Investigation revealed that the culprint was CommonUtils.convertString() silently swallowing high bytes. When the Bagheera client went to gzip the JSON payload, the input buffer into gzip was missing high bytes. This patch changes the bagheera client to UTF-8 encode strings before gzip, thus ensuring all data is preserved. A corresponding change was also added to the mock bagheera server implementation. Alternatively, we could have changed CommonUtils.convertString() to be high byte aware. However, many consumers rely on this function. This patch is written with the intent of being uplifted and the change performed is targeted at the specific problem. Tests for Unicode preserving behavior have been added to both the generic Bagheera client and to FHR. The latter test is arguably not necessary, but peace of mind is a good thing, especially with FHR. See also bug 915850. --HG-- extra : rebase_source : 4efddea7767c2e5f8cf19df247c3aba07c40eec6 extra : amend_source : ae3b6d89efa54fc9ed1794404476622946ad4b22
299 lines
7.8 KiB
JavaScript
299 lines
7.8 KiB
JavaScript
/* 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 {utils: Cu} = Components;
|
|
|
|
this.EXPORTED_SYMBOLS = ["BagheeraServer"];
|
|
|
|
Cu.import("resource://gre/modules/Log.jsm");
|
|
Cu.import("resource://services-common/utils.js");
|
|
Cu.import("resource://testing-common/httpd.js");
|
|
|
|
|
|
/**
|
|
* This is an implementation of the Bagheera server.
|
|
*
|
|
* The purpose of the server is to facilitate testing of the Bagheera
|
|
* client and the Firefox Health report. It is *not* meant to be a
|
|
* production grade server.
|
|
*
|
|
* The Bagheera server is essentially a glorified document store.
|
|
*/
|
|
this.BagheeraServer = function BagheeraServer() {
|
|
this._log = Log.repository.getLogger("metrics.BagheeraServer");
|
|
|
|
this.server = new HttpServer();
|
|
this.namespaces = {};
|
|
|
|
this.allowAllNamespaces = false;
|
|
}
|
|
|
|
BagheeraServer.prototype = {
|
|
/**
|
|
* Whether this server has a namespace defined.
|
|
*
|
|
* @param ns
|
|
* (string) Namepsace whose existence to query for.
|
|
* @return bool
|
|
*/
|
|
hasNamespace: function hasNamespace(ns) {
|
|
return ns in this.namespaces;
|
|
},
|
|
|
|
/**
|
|
* Whether this server has an ID in a particular namespace.
|
|
*
|
|
* @param ns
|
|
* (string) Namespace to look for item in.
|
|
* @param id
|
|
* (string) ID of object to look for.
|
|
* @return bool
|
|
*/
|
|
hasDocument: function hasDocument(ns, id) {
|
|
let namespace = this.namespaces[ns];
|
|
|
|
if (!namespace) {
|
|
return false;
|
|
}
|
|
|
|
return id in namespace;
|
|
},
|
|
|
|
/**
|
|
* Obtain a document from the server.
|
|
*
|
|
* @param ns
|
|
* (string) Namespace to retrieve document from.
|
|
* @param id
|
|
* (string) ID of document to retrieve.
|
|
*
|
|
* @return string The content of the document or null if the document
|
|
* does not exist.
|
|
*/
|
|
getDocument: function getDocument(ns, id) {
|
|
let namespace = this.namespaces[ns];
|
|
|
|
if (!namespace) {
|
|
return null;
|
|
}
|
|
|
|
return namespace[id];
|
|
},
|
|
|
|
/**
|
|
* Set the contents of a document in the server.
|
|
*
|
|
* @param ns
|
|
* (string) Namespace to add document to.
|
|
* @param id
|
|
* (string) ID of document being added.
|
|
* @param payload
|
|
* (string) The content of the document.
|
|
*/
|
|
setDocument: function setDocument(ns, id, payload) {
|
|
let namespace = this.namespaces[ns];
|
|
|
|
if (!namespace) {
|
|
if (!this.allowAllNamespaces) {
|
|
throw new Error("Namespace does not exist: " + ns);
|
|
}
|
|
|
|
this.createNamespace(ns);
|
|
namespace = this.namespaces[ns];
|
|
}
|
|
|
|
namespace[id] = payload;
|
|
},
|
|
|
|
/**
|
|
* Create a namespace in the server.
|
|
*
|
|
* The namespace will initially be empty.
|
|
*
|
|
* @param ns
|
|
* (string) The name of the namespace to create.
|
|
*/
|
|
createNamespace: function createNamespace(ns) {
|
|
if (ns in this.namespaces) {
|
|
throw new Error("Namespace already exists: " + ns);
|
|
}
|
|
|
|
this.namespaces[ns] = {};
|
|
},
|
|
|
|
start: function start(port=-1) {
|
|
this.server.registerPrefixHandler("/", this._handleRequest.bind(this));
|
|
this.server.start(port);
|
|
let i = this.server.identity;
|
|
|
|
this.serverURI = i.primaryScheme + "://" + i.primaryHost + ":" +
|
|
i.primaryPort + "/";
|
|
this.port = i.primaryPort;
|
|
},
|
|
|
|
stop: function stop(cb) {
|
|
let handler = {onStopped: cb};
|
|
|
|
this.server.stop(handler);
|
|
},
|
|
|
|
/**
|
|
* Our root path handler.
|
|
*/
|
|
_handleRequest: function _handleRequest(request, response) {
|
|
let path = request.path;
|
|
this._log.info("Received request: " + request.method + " " + path + " " +
|
|
"HTTP/" + request.httpVersion);
|
|
|
|
try {
|
|
if (path.startsWith("/1.0/submit/")) {
|
|
return this._handleV1Submit(request, response,
|
|
path.substr("/1.0/submit/".length));
|
|
} else {
|
|
throw HTTP_404;
|
|
}
|
|
} catch (ex) {
|
|
if (ex instanceof HttpError) {
|
|
this._log.info("HttpError thrown: " + ex.code + " " + ex.description);
|
|
} else {
|
|
this._log.warn("Exception processing request: " +
|
|
CommonUtils.exceptionStr(ex));
|
|
}
|
|
|
|
throw ex;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handles requests to /submit/*.
|
|
*/
|
|
_handleV1Submit: function _handleV1Submit(request, response, rest) {
|
|
if (!rest.length) {
|
|
throw HTTP_404;
|
|
}
|
|
|
|
let namespace;
|
|
let index = rest.indexOf("/");
|
|
if (index == -1) {
|
|
namespace = rest;
|
|
rest = "";
|
|
} else {
|
|
namespace = rest.substr(0, index);
|
|
rest = rest.substr(index + 1);
|
|
}
|
|
|
|
this._handleNamespaceSubmit(namespace, rest, request, response);
|
|
},
|
|
|
|
_handleNamespaceSubmit: function _handleNamespaceSubmit(namespace, rest,
|
|
request, response) {
|
|
if (!this.hasNamespace(namespace)) {
|
|
if (!this.allowAllNamespaces) {
|
|
this._log.info("Request to unknown namespace: " + namespace);
|
|
throw HTTP_404;
|
|
}
|
|
|
|
this.createNamespace(namespace);
|
|
}
|
|
|
|
if (!rest) {
|
|
this._log.info("No ID defined.");
|
|
throw HTTP_404;
|
|
}
|
|
|
|
let id = rest;
|
|
if (id.contains("/")) {
|
|
this._log.info("URI has too many components.");
|
|
throw HTTP_404;
|
|
}
|
|
|
|
if (request.method == "POST") {
|
|
return this._handleNamespaceSubmitPost(namespace, id, request, response);
|
|
}
|
|
|
|
if (request.method == "DELETE") {
|
|
return this._handleNamespaceSubmitDelete(namespace, id, request, response);
|
|
}
|
|
|
|
this._log.info("Unsupported HTTP method on namespace handler: " +
|
|
request.method);
|
|
response.setHeader("Allow", "POST,DELETE");
|
|
throw HTTP_405;
|
|
},
|
|
|
|
_handleNamespaceSubmitPost:
|
|
function _handleNamespaceSubmitPost(namespace, id, request, response) {
|
|
|
|
this._log.info("Handling data upload for " + namespace + ":" + id);
|
|
|
|
let requestBody = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
|
this._log.info("Raw body length: " + requestBody.length);
|
|
|
|
if (!request.hasHeader("Content-Type")) {
|
|
this._log.info("Request does not have Content-Type header.");
|
|
throw HTTP_400;
|
|
}
|
|
|
|
const ALLOWED_TYPES = [
|
|
// TODO proper content types from bug 807134.
|
|
"application/json; charset=utf-8",
|
|
"application/json+zlib; charset=utf-8",
|
|
];
|
|
|
|
let ct = request.getHeader("Content-Type");
|
|
if (ALLOWED_TYPES.indexOf(ct) == -1) {
|
|
this._log.info("Unknown media type: " + ct);
|
|
// Should generate proper HTTP response headers for this error.
|
|
throw HTTP_415;
|
|
}
|
|
|
|
if (ct.startsWith("application/json+zlib")) {
|
|
this._log.debug("Uncompressing entity body with deflate.");
|
|
requestBody = CommonUtils.convertString(requestBody, "deflate",
|
|
"uncompressed");
|
|
}
|
|
|
|
requestBody = CommonUtils.decodeUTF8(requestBody);
|
|
|
|
this._log.debug("HTTP request body: " + requestBody);
|
|
|
|
let doc;
|
|
try {
|
|
doc = JSON.parse(requestBody);
|
|
} catch(ex) {
|
|
this._log.info("JSON parse error.");
|
|
throw HTTP_400;
|
|
}
|
|
|
|
this.namespaces[namespace][id] = doc;
|
|
|
|
if (request.hasHeader("X-Obsolete-Document")) {
|
|
let obsolete = request.getHeader("X-Obsolete-Document");
|
|
this._log.info("Deleting from X-Obsolete-Document header: " + obsolete);
|
|
for (let obsolete_id of obsolete.split(",")) {
|
|
delete this.namespaces[namespace][obsolete_id];
|
|
}
|
|
}
|
|
|
|
response.setStatusLine(request.httpVersion, 201, "Created");
|
|
response.setHeader("Content-Type", "text/plain");
|
|
|
|
let body = id;
|
|
response.bodyOutputStream.write(body, body.length);
|
|
},
|
|
|
|
_handleNamespaceSubmitDelete:
|
|
function _handleNamespaceSubmitDelete(namespace, id, request, response) {
|
|
|
|
delete this.namespaces[namespace][id];
|
|
|
|
let body = id;
|
|
response.bodyOutputStream.write(body, body.length);
|
|
},
|
|
};
|
|
|
|
Object.freeze(BagheeraServer.prototype);
|