michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {utils: Cu} = Components; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["BagheeraServer"]; michael@0: michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: Cu.import("resource://testing-common/httpd.js"); michael@0: michael@0: michael@0: /** michael@0: * This is an implementation of the Bagheera server. michael@0: * michael@0: * The purpose of the server is to facilitate testing of the Bagheera michael@0: * client and the Firefox Health report. It is *not* meant to be a michael@0: * production grade server. michael@0: * michael@0: * The Bagheera server is essentially a glorified document store. michael@0: */ michael@0: this.BagheeraServer = function BagheeraServer() { michael@0: this._log = Log.repository.getLogger("metrics.BagheeraServer"); michael@0: michael@0: this.server = new HttpServer(); michael@0: this.namespaces = {}; michael@0: michael@0: this.allowAllNamespaces = false; michael@0: } michael@0: michael@0: BagheeraServer.prototype = { michael@0: /** michael@0: * Whether this server has a namespace defined. michael@0: * michael@0: * @param ns michael@0: * (string) Namepsace whose existence to query for. michael@0: * @return bool michael@0: */ michael@0: hasNamespace: function hasNamespace(ns) { michael@0: return ns in this.namespaces; michael@0: }, michael@0: michael@0: /** michael@0: * Whether this server has an ID in a particular namespace. michael@0: * michael@0: * @param ns michael@0: * (string) Namespace to look for item in. michael@0: * @param id michael@0: * (string) ID of object to look for. michael@0: * @return bool michael@0: */ michael@0: hasDocument: function hasDocument(ns, id) { michael@0: let namespace = this.namespaces[ns]; michael@0: michael@0: if (!namespace) { michael@0: return false; michael@0: } michael@0: michael@0: return id in namespace; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a document from the server. michael@0: * michael@0: * @param ns michael@0: * (string) Namespace to retrieve document from. michael@0: * @param id michael@0: * (string) ID of document to retrieve. michael@0: * michael@0: * @return string The content of the document or null if the document michael@0: * does not exist. michael@0: */ michael@0: getDocument: function getDocument(ns, id) { michael@0: let namespace = this.namespaces[ns]; michael@0: michael@0: if (!namespace) { michael@0: return null; michael@0: } michael@0: michael@0: return namespace[id]; michael@0: }, michael@0: michael@0: /** michael@0: * Set the contents of a document in the server. michael@0: * michael@0: * @param ns michael@0: * (string) Namespace to add document to. michael@0: * @param id michael@0: * (string) ID of document being added. michael@0: * @param payload michael@0: * (string) The content of the document. michael@0: */ michael@0: setDocument: function setDocument(ns, id, payload) { michael@0: let namespace = this.namespaces[ns]; michael@0: michael@0: if (!namespace) { michael@0: if (!this.allowAllNamespaces) { michael@0: throw new Error("Namespace does not exist: " + ns); michael@0: } michael@0: michael@0: this.createNamespace(ns); michael@0: namespace = this.namespaces[ns]; michael@0: } michael@0: michael@0: namespace[id] = payload; michael@0: }, michael@0: michael@0: /** michael@0: * Create a namespace in the server. michael@0: * michael@0: * The namespace will initially be empty. michael@0: * michael@0: * @param ns michael@0: * (string) The name of the namespace to create. michael@0: */ michael@0: createNamespace: function createNamespace(ns) { michael@0: if (ns in this.namespaces) { michael@0: throw new Error("Namespace already exists: " + ns); michael@0: } michael@0: michael@0: this.namespaces[ns] = {}; michael@0: }, michael@0: michael@0: start: function start(port=-1) { michael@0: this.server.registerPrefixHandler("/", this._handleRequest.bind(this)); michael@0: this.server.start(port); michael@0: let i = this.server.identity; michael@0: michael@0: this.serverURI = i.primaryScheme + "://" + i.primaryHost + ":" + michael@0: i.primaryPort + "/"; michael@0: this.port = i.primaryPort; michael@0: }, michael@0: michael@0: stop: function stop(cb) { michael@0: let handler = {onStopped: cb}; michael@0: michael@0: this.server.stop(handler); michael@0: }, michael@0: michael@0: /** michael@0: * Our root path handler. michael@0: */ michael@0: _handleRequest: function _handleRequest(request, response) { michael@0: let path = request.path; michael@0: this._log.info("Received request: " + request.method + " " + path + " " + michael@0: "HTTP/" + request.httpVersion); michael@0: michael@0: try { michael@0: if (path.startsWith("/1.0/submit/")) { michael@0: return this._handleV1Submit(request, response, michael@0: path.substr("/1.0/submit/".length)); michael@0: } else { michael@0: throw HTTP_404; michael@0: } michael@0: } catch (ex) { michael@0: if (ex instanceof HttpError) { michael@0: this._log.info("HttpError thrown: " + ex.code + " " + ex.description); michael@0: } else { michael@0: this._log.warn("Exception processing request: " + michael@0: CommonUtils.exceptionStr(ex)); michael@0: } michael@0: michael@0: throw ex; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handles requests to /submit/*. michael@0: */ michael@0: _handleV1Submit: function _handleV1Submit(request, response, rest) { michael@0: if (!rest.length) { michael@0: throw HTTP_404; michael@0: } michael@0: michael@0: let namespace; michael@0: let index = rest.indexOf("/"); michael@0: if (index == -1) { michael@0: namespace = rest; michael@0: rest = ""; michael@0: } else { michael@0: namespace = rest.substr(0, index); michael@0: rest = rest.substr(index + 1); michael@0: } michael@0: michael@0: this._handleNamespaceSubmit(namespace, rest, request, response); michael@0: }, michael@0: michael@0: _handleNamespaceSubmit: function _handleNamespaceSubmit(namespace, rest, michael@0: request, response) { michael@0: if (!this.hasNamespace(namespace)) { michael@0: if (!this.allowAllNamespaces) { michael@0: this._log.info("Request to unknown namespace: " + namespace); michael@0: throw HTTP_404; michael@0: } michael@0: michael@0: this.createNamespace(namespace); michael@0: } michael@0: michael@0: if (!rest) { michael@0: this._log.info("No ID defined."); michael@0: throw HTTP_404; michael@0: } michael@0: michael@0: let id = rest; michael@0: if (id.contains("/")) { michael@0: this._log.info("URI has too many components."); michael@0: throw HTTP_404; michael@0: } michael@0: michael@0: if (request.method == "POST") { michael@0: return this._handleNamespaceSubmitPost(namespace, id, request, response); michael@0: } michael@0: michael@0: if (request.method == "DELETE") { michael@0: return this._handleNamespaceSubmitDelete(namespace, id, request, response); michael@0: } michael@0: michael@0: this._log.info("Unsupported HTTP method on namespace handler: " + michael@0: request.method); michael@0: response.setHeader("Allow", "POST,DELETE"); michael@0: throw HTTP_405; michael@0: }, michael@0: michael@0: _handleNamespaceSubmitPost: michael@0: function _handleNamespaceSubmitPost(namespace, id, request, response) { michael@0: michael@0: this._log.info("Handling data upload for " + namespace + ":" + id); michael@0: michael@0: let requestBody = CommonUtils.readBytesFromInputStream(request.bodyInputStream); michael@0: this._log.info("Raw body length: " + requestBody.length); michael@0: michael@0: if (!request.hasHeader("Content-Type")) { michael@0: this._log.info("Request does not have Content-Type header."); michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: const ALLOWED_TYPES = [ michael@0: // TODO proper content types from bug 807134. michael@0: "application/json; charset=utf-8", michael@0: "application/json+zlib; charset=utf-8", michael@0: ]; michael@0: michael@0: let ct = request.getHeader("Content-Type"); michael@0: if (ALLOWED_TYPES.indexOf(ct) == -1) { michael@0: this._log.info("Unknown media type: " + ct); michael@0: // Should generate proper HTTP response headers for this error. michael@0: throw HTTP_415; michael@0: } michael@0: michael@0: if (ct.startsWith("application/json+zlib")) { michael@0: this._log.debug("Uncompressing entity body with deflate."); michael@0: requestBody = CommonUtils.convertString(requestBody, "deflate", michael@0: "uncompressed"); michael@0: } michael@0: michael@0: this._log.debug("HTTP request body: " + requestBody); michael@0: michael@0: let doc; michael@0: try { michael@0: doc = JSON.parse(requestBody); michael@0: } catch(ex) { michael@0: this._log.info("JSON parse error."); michael@0: throw HTTP_400; michael@0: } michael@0: michael@0: this.namespaces[namespace][id] = doc; michael@0: michael@0: if (request.hasHeader("X-Obsolete-Document")) { michael@0: let obsolete = request.getHeader("X-Obsolete-Document"); michael@0: this._log.info("Deleting from X-Obsolete-Document header: " + obsolete); michael@0: for (let obsolete_id of obsolete.split(",")) { michael@0: delete this.namespaces[namespace][obsolete_id]; michael@0: } michael@0: } michael@0: michael@0: response.setStatusLine(request.httpVersion, 201, "Created"); michael@0: response.setHeader("Content-Type", "text/plain"); michael@0: michael@0: let body = id; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: }, michael@0: michael@0: _handleNamespaceSubmitDelete: michael@0: function _handleNamespaceSubmitDelete(namespace, id, request, response) { michael@0: michael@0: delete this.namespaces[namespace][id]; michael@0: michael@0: let body = id; michael@0: response.bodyOutputStream.write(body, body.length); michael@0: }, michael@0: }; michael@0: michael@0: Object.freeze(BagheeraServer.prototype);