services/common/modules-testing/bagheeraserver.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 const {utils: Cu} = Components;
     9 this.EXPORTED_SYMBOLS = ["BagheeraServer"];
    11 Cu.import("resource://gre/modules/Log.jsm");
    12 Cu.import("resource://services-common/utils.js");
    13 Cu.import("resource://testing-common/httpd.js");
    16 /**
    17  * This is an implementation of the Bagheera server.
    18  *
    19  * The purpose of the server is to facilitate testing of the Bagheera
    20  * client and the Firefox Health report. It is *not* meant to be a
    21  * production grade server.
    22  *
    23  * The Bagheera server is essentially a glorified document store.
    24  */
    25 this.BagheeraServer = function BagheeraServer() {
    26   this._log = Log.repository.getLogger("metrics.BagheeraServer");
    28   this.server = new HttpServer();
    29   this.namespaces = {};
    31   this.allowAllNamespaces = false;
    32 }
    34 BagheeraServer.prototype = {
    35   /**
    36    * Whether this server has a namespace defined.
    37    *
    38    * @param ns
    39    *        (string) Namepsace whose existence to query for.
    40    * @return bool
    41    */
    42   hasNamespace: function hasNamespace(ns) {
    43     return ns in this.namespaces;
    44   },
    46   /**
    47    * Whether this server has an ID in a particular namespace.
    48    *
    49    * @param ns
    50    *        (string) Namespace to look for item in.
    51    * @param id
    52    *        (string) ID of object to look for.
    53    * @return bool
    54    */
    55   hasDocument: function hasDocument(ns, id) {
    56     let namespace = this.namespaces[ns];
    58     if (!namespace) {
    59       return false;
    60     }
    62     return id in namespace;
    63   },
    65   /**
    66    * Obtain a document from the server.
    67    *
    68    * @param ns
    69    *        (string) Namespace to retrieve document from.
    70    * @param id
    71    *        (string) ID of document to retrieve.
    72    *
    73    * @return string The content of the document or null if the document
    74    *                does not exist.
    75    */
    76   getDocument: function getDocument(ns, id) {
    77     let namespace = this.namespaces[ns];
    79     if (!namespace) {
    80       return null;
    81     }
    83     return namespace[id];
    84   },
    86   /**
    87    * Set the contents of a document in the server.
    88    *
    89    * @param ns
    90    *        (string) Namespace to add document to.
    91    * @param id
    92    *        (string) ID of document being added.
    93    * @param payload
    94    *        (string) The content of the document.
    95    */
    96   setDocument: function setDocument(ns, id, payload) {
    97     let namespace = this.namespaces[ns];
    99     if (!namespace) {
   100       if (!this.allowAllNamespaces) {
   101         throw new Error("Namespace does not exist: " + ns);
   102       }
   104       this.createNamespace(ns);
   105       namespace = this.namespaces[ns];
   106     }
   108     namespace[id] = payload;
   109   },
   111   /**
   112    * Create a namespace in the server.
   113    *
   114    * The namespace will initially be empty.
   115    *
   116    * @param ns
   117    *        (string) The name of the namespace to create.
   118    */
   119   createNamespace: function createNamespace(ns) {
   120     if (ns in this.namespaces) {
   121       throw new Error("Namespace already exists: " + ns);
   122     }
   124     this.namespaces[ns] = {};
   125   },
   127   start: function start(port=-1) {
   128     this.server.registerPrefixHandler("/", this._handleRequest.bind(this));
   129     this.server.start(port);
   130     let i = this.server.identity;
   132     this.serverURI = i.primaryScheme + "://" + i.primaryHost + ":" +
   133                      i.primaryPort + "/";
   134     this.port = i.primaryPort;
   135   },
   137   stop: function stop(cb) {
   138     let handler = {onStopped: cb};
   140     this.server.stop(handler);
   141   },
   143   /**
   144    * Our root path handler.
   145    */
   146   _handleRequest: function _handleRequest(request, response) {
   147     let path = request.path;
   148     this._log.info("Received request: " + request.method + " " + path + " " +
   149                    "HTTP/" + request.httpVersion);
   151     try {
   152       if (path.startsWith("/1.0/submit/")) {
   153         return this._handleV1Submit(request, response,
   154                                     path.substr("/1.0/submit/".length));
   155       } else {
   156         throw HTTP_404;
   157       }
   158     } catch (ex) {
   159       if (ex instanceof HttpError) {
   160         this._log.info("HttpError thrown: " + ex.code + " " + ex.description);
   161       } else {
   162         this._log.warn("Exception processing request: " +
   163                        CommonUtils.exceptionStr(ex));
   164       }
   166       throw ex;
   167     }
   168   },
   170   /**
   171    * Handles requests to /submit/*.
   172    */
   173   _handleV1Submit: function _handleV1Submit(request, response, rest) {
   174     if (!rest.length) {
   175       throw HTTP_404;
   176     }
   178     let namespace;
   179     let index = rest.indexOf("/");
   180     if (index == -1) {
   181       namespace = rest;
   182       rest = "";
   183     } else {
   184       namespace = rest.substr(0, index);
   185       rest = rest.substr(index + 1);
   186     }
   188     this._handleNamespaceSubmit(namespace, rest, request, response);
   189   },
   191   _handleNamespaceSubmit: function _handleNamespaceSubmit(namespace, rest,
   192                                                           request, response) {
   193     if (!this.hasNamespace(namespace)) {
   194       if (!this.allowAllNamespaces) {
   195         this._log.info("Request to unknown namespace: " + namespace);
   196         throw HTTP_404;
   197       }
   199       this.createNamespace(namespace);
   200     }
   202     if (!rest) {
   203       this._log.info("No ID defined.");
   204       throw HTTP_404;
   205     }
   207     let id = rest;
   208     if (id.contains("/")) {
   209       this._log.info("URI has too many components.");
   210       throw HTTP_404;
   211     }
   213     if (request.method == "POST") {
   214       return this._handleNamespaceSubmitPost(namespace, id, request, response);
   215     }
   217     if (request.method == "DELETE") {
   218       return this._handleNamespaceSubmitDelete(namespace, id, request, response);
   219     }
   221     this._log.info("Unsupported HTTP method on namespace handler: " +
   222                    request.method);
   223     response.setHeader("Allow", "POST,DELETE");
   224     throw HTTP_405;
   225   },
   227   _handleNamespaceSubmitPost:
   228     function _handleNamespaceSubmitPost(namespace, id, request, response) {
   230     this._log.info("Handling data upload for " + namespace + ":" + id);
   232     let requestBody = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
   233     this._log.info("Raw body length: " + requestBody.length);
   235     if (!request.hasHeader("Content-Type")) {
   236       this._log.info("Request does not have Content-Type header.");
   237       throw HTTP_400;
   238     }
   240     const ALLOWED_TYPES = [
   241       // TODO proper content types from bug 807134.
   242       "application/json; charset=utf-8",
   243       "application/json+zlib; charset=utf-8",
   244     ];
   246     let ct = request.getHeader("Content-Type");
   247     if (ALLOWED_TYPES.indexOf(ct) == -1) {
   248       this._log.info("Unknown media type: " + ct);
   249       // Should generate proper HTTP response headers for this error.
   250       throw HTTP_415;
   251     }
   253     if (ct.startsWith("application/json+zlib")) {
   254       this._log.debug("Uncompressing entity body with deflate.");
   255       requestBody = CommonUtils.convertString(requestBody, "deflate",
   256                                               "uncompressed");
   257     }
   259     this._log.debug("HTTP request body: " + requestBody);
   261     let doc;
   262     try {
   263       doc = JSON.parse(requestBody);
   264     } catch(ex) {
   265       this._log.info("JSON parse error.");
   266       throw HTTP_400;
   267     }
   269     this.namespaces[namespace][id] = doc;
   271     if (request.hasHeader("X-Obsolete-Document")) {
   272       let obsolete = request.getHeader("X-Obsolete-Document");
   273       this._log.info("Deleting from X-Obsolete-Document header: " + obsolete);
   274       for (let obsolete_id of obsolete.split(",")) {
   275         delete this.namespaces[namespace][obsolete_id];
   276       }
   277     }
   279     response.setStatusLine(request.httpVersion, 201, "Created");
   280     response.setHeader("Content-Type", "text/plain");
   282     let body = id;
   283     response.bodyOutputStream.write(body, body.length);
   284   },
   286   _handleNamespaceSubmitDelete:
   287     function _handleNamespaceSubmitDelete(namespace, id, request, response) {
   289     delete this.namespaces[namespace][id];
   291     let body = id;
   292     response.bodyOutputStream.write(body, body.length);
   293   },
   294 };
   296 Object.freeze(BagheeraServer.prototype);

mercurial