1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/common/modules-testing/bagheeraserver.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,296 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {utils: Cu} = Components; 1.11 + 1.12 +this.EXPORTED_SYMBOLS = ["BagheeraServer"]; 1.13 + 1.14 +Cu.import("resource://gre/modules/Log.jsm"); 1.15 +Cu.import("resource://services-common/utils.js"); 1.16 +Cu.import("resource://testing-common/httpd.js"); 1.17 + 1.18 + 1.19 +/** 1.20 + * This is an implementation of the Bagheera server. 1.21 + * 1.22 + * The purpose of the server is to facilitate testing of the Bagheera 1.23 + * client and the Firefox Health report. It is *not* meant to be a 1.24 + * production grade server. 1.25 + * 1.26 + * The Bagheera server is essentially a glorified document store. 1.27 + */ 1.28 +this.BagheeraServer = function BagheeraServer() { 1.29 + this._log = Log.repository.getLogger("metrics.BagheeraServer"); 1.30 + 1.31 + this.server = new HttpServer(); 1.32 + this.namespaces = {}; 1.33 + 1.34 + this.allowAllNamespaces = false; 1.35 +} 1.36 + 1.37 +BagheeraServer.prototype = { 1.38 + /** 1.39 + * Whether this server has a namespace defined. 1.40 + * 1.41 + * @param ns 1.42 + * (string) Namepsace whose existence to query for. 1.43 + * @return bool 1.44 + */ 1.45 + hasNamespace: function hasNamespace(ns) { 1.46 + return ns in this.namespaces; 1.47 + }, 1.48 + 1.49 + /** 1.50 + * Whether this server has an ID in a particular namespace. 1.51 + * 1.52 + * @param ns 1.53 + * (string) Namespace to look for item in. 1.54 + * @param id 1.55 + * (string) ID of object to look for. 1.56 + * @return bool 1.57 + */ 1.58 + hasDocument: function hasDocument(ns, id) { 1.59 + let namespace = this.namespaces[ns]; 1.60 + 1.61 + if (!namespace) { 1.62 + return false; 1.63 + } 1.64 + 1.65 + return id in namespace; 1.66 + }, 1.67 + 1.68 + /** 1.69 + * Obtain a document from the server. 1.70 + * 1.71 + * @param ns 1.72 + * (string) Namespace to retrieve document from. 1.73 + * @param id 1.74 + * (string) ID of document to retrieve. 1.75 + * 1.76 + * @return string The content of the document or null if the document 1.77 + * does not exist. 1.78 + */ 1.79 + getDocument: function getDocument(ns, id) { 1.80 + let namespace = this.namespaces[ns]; 1.81 + 1.82 + if (!namespace) { 1.83 + return null; 1.84 + } 1.85 + 1.86 + return namespace[id]; 1.87 + }, 1.88 + 1.89 + /** 1.90 + * Set the contents of a document in the server. 1.91 + * 1.92 + * @param ns 1.93 + * (string) Namespace to add document to. 1.94 + * @param id 1.95 + * (string) ID of document being added. 1.96 + * @param payload 1.97 + * (string) The content of the document. 1.98 + */ 1.99 + setDocument: function setDocument(ns, id, payload) { 1.100 + let namespace = this.namespaces[ns]; 1.101 + 1.102 + if (!namespace) { 1.103 + if (!this.allowAllNamespaces) { 1.104 + throw new Error("Namespace does not exist: " + ns); 1.105 + } 1.106 + 1.107 + this.createNamespace(ns); 1.108 + namespace = this.namespaces[ns]; 1.109 + } 1.110 + 1.111 + namespace[id] = payload; 1.112 + }, 1.113 + 1.114 + /** 1.115 + * Create a namespace in the server. 1.116 + * 1.117 + * The namespace will initially be empty. 1.118 + * 1.119 + * @param ns 1.120 + * (string) The name of the namespace to create. 1.121 + */ 1.122 + createNamespace: function createNamespace(ns) { 1.123 + if (ns in this.namespaces) { 1.124 + throw new Error("Namespace already exists: " + ns); 1.125 + } 1.126 + 1.127 + this.namespaces[ns] = {}; 1.128 + }, 1.129 + 1.130 + start: function start(port=-1) { 1.131 + this.server.registerPrefixHandler("/", this._handleRequest.bind(this)); 1.132 + this.server.start(port); 1.133 + let i = this.server.identity; 1.134 + 1.135 + this.serverURI = i.primaryScheme + "://" + i.primaryHost + ":" + 1.136 + i.primaryPort + "/"; 1.137 + this.port = i.primaryPort; 1.138 + }, 1.139 + 1.140 + stop: function stop(cb) { 1.141 + let handler = {onStopped: cb}; 1.142 + 1.143 + this.server.stop(handler); 1.144 + }, 1.145 + 1.146 + /** 1.147 + * Our root path handler. 1.148 + */ 1.149 + _handleRequest: function _handleRequest(request, response) { 1.150 + let path = request.path; 1.151 + this._log.info("Received request: " + request.method + " " + path + " " + 1.152 + "HTTP/" + request.httpVersion); 1.153 + 1.154 + try { 1.155 + if (path.startsWith("/1.0/submit/")) { 1.156 + return this._handleV1Submit(request, response, 1.157 + path.substr("/1.0/submit/".length)); 1.158 + } else { 1.159 + throw HTTP_404; 1.160 + } 1.161 + } catch (ex) { 1.162 + if (ex instanceof HttpError) { 1.163 + this._log.info("HttpError thrown: " + ex.code + " " + ex.description); 1.164 + } else { 1.165 + this._log.warn("Exception processing request: " + 1.166 + CommonUtils.exceptionStr(ex)); 1.167 + } 1.168 + 1.169 + throw ex; 1.170 + } 1.171 + }, 1.172 + 1.173 + /** 1.174 + * Handles requests to /submit/*. 1.175 + */ 1.176 + _handleV1Submit: function _handleV1Submit(request, response, rest) { 1.177 + if (!rest.length) { 1.178 + throw HTTP_404; 1.179 + } 1.180 + 1.181 + let namespace; 1.182 + let index = rest.indexOf("/"); 1.183 + if (index == -1) { 1.184 + namespace = rest; 1.185 + rest = ""; 1.186 + } else { 1.187 + namespace = rest.substr(0, index); 1.188 + rest = rest.substr(index + 1); 1.189 + } 1.190 + 1.191 + this._handleNamespaceSubmit(namespace, rest, request, response); 1.192 + }, 1.193 + 1.194 + _handleNamespaceSubmit: function _handleNamespaceSubmit(namespace, rest, 1.195 + request, response) { 1.196 + if (!this.hasNamespace(namespace)) { 1.197 + if (!this.allowAllNamespaces) { 1.198 + this._log.info("Request to unknown namespace: " + namespace); 1.199 + throw HTTP_404; 1.200 + } 1.201 + 1.202 + this.createNamespace(namespace); 1.203 + } 1.204 + 1.205 + if (!rest) { 1.206 + this._log.info("No ID defined."); 1.207 + throw HTTP_404; 1.208 + } 1.209 + 1.210 + let id = rest; 1.211 + if (id.contains("/")) { 1.212 + this._log.info("URI has too many components."); 1.213 + throw HTTP_404; 1.214 + } 1.215 + 1.216 + if (request.method == "POST") { 1.217 + return this._handleNamespaceSubmitPost(namespace, id, request, response); 1.218 + } 1.219 + 1.220 + if (request.method == "DELETE") { 1.221 + return this._handleNamespaceSubmitDelete(namespace, id, request, response); 1.222 + } 1.223 + 1.224 + this._log.info("Unsupported HTTP method on namespace handler: " + 1.225 + request.method); 1.226 + response.setHeader("Allow", "POST,DELETE"); 1.227 + throw HTTP_405; 1.228 + }, 1.229 + 1.230 + _handleNamespaceSubmitPost: 1.231 + function _handleNamespaceSubmitPost(namespace, id, request, response) { 1.232 + 1.233 + this._log.info("Handling data upload for " + namespace + ":" + id); 1.234 + 1.235 + let requestBody = CommonUtils.readBytesFromInputStream(request.bodyInputStream); 1.236 + this._log.info("Raw body length: " + requestBody.length); 1.237 + 1.238 + if (!request.hasHeader("Content-Type")) { 1.239 + this._log.info("Request does not have Content-Type header."); 1.240 + throw HTTP_400; 1.241 + } 1.242 + 1.243 + const ALLOWED_TYPES = [ 1.244 + // TODO proper content types from bug 807134. 1.245 + "application/json; charset=utf-8", 1.246 + "application/json+zlib; charset=utf-8", 1.247 + ]; 1.248 + 1.249 + let ct = request.getHeader("Content-Type"); 1.250 + if (ALLOWED_TYPES.indexOf(ct) == -1) { 1.251 + this._log.info("Unknown media type: " + ct); 1.252 + // Should generate proper HTTP response headers for this error. 1.253 + throw HTTP_415; 1.254 + } 1.255 + 1.256 + if (ct.startsWith("application/json+zlib")) { 1.257 + this._log.debug("Uncompressing entity body with deflate."); 1.258 + requestBody = CommonUtils.convertString(requestBody, "deflate", 1.259 + "uncompressed"); 1.260 + } 1.261 + 1.262 + this._log.debug("HTTP request body: " + requestBody); 1.263 + 1.264 + let doc; 1.265 + try { 1.266 + doc = JSON.parse(requestBody); 1.267 + } catch(ex) { 1.268 + this._log.info("JSON parse error."); 1.269 + throw HTTP_400; 1.270 + } 1.271 + 1.272 + this.namespaces[namespace][id] = doc; 1.273 + 1.274 + if (request.hasHeader("X-Obsolete-Document")) { 1.275 + let obsolete = request.getHeader("X-Obsolete-Document"); 1.276 + this._log.info("Deleting from X-Obsolete-Document header: " + obsolete); 1.277 + for (let obsolete_id of obsolete.split(",")) { 1.278 + delete this.namespaces[namespace][obsolete_id]; 1.279 + } 1.280 + } 1.281 + 1.282 + response.setStatusLine(request.httpVersion, 201, "Created"); 1.283 + response.setHeader("Content-Type", "text/plain"); 1.284 + 1.285 + let body = id; 1.286 + response.bodyOutputStream.write(body, body.length); 1.287 + }, 1.288 + 1.289 + _handleNamespaceSubmitDelete: 1.290 + function _handleNamespaceSubmitDelete(namespace, id, request, response) { 1.291 + 1.292 + delete this.namespaces[namespace][id]; 1.293 + 1.294 + let body = id; 1.295 + response.bodyOutputStream.write(body, body.length); 1.296 + }, 1.297 +}; 1.298 + 1.299 +Object.freeze(BagheeraServer.prototype);