Thu, 22 Jan 2015 13:21:57 +0100
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);