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

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

mercurial