1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/common/modules-testing/storageserver.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1659 @@ 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 file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * This file contains an implementation of the Storage Server in JavaScript. 1.10 + * 1.11 + * The server should not be used for any production purposes. 1.12 + */ 1.13 + 1.14 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.15 + 1.16 +this.EXPORTED_SYMBOLS = [ 1.17 + "ServerBSO", 1.18 + "StorageServerCallback", 1.19 + "StorageServerCollection", 1.20 + "StorageServer", 1.21 + "storageServerForUsers", 1.22 +]; 1.23 + 1.24 +Cu.import("resource://testing-common/httpd.js"); 1.25 +Cu.import("resource://services-common/async.js"); 1.26 +Cu.import("resource://gre/modules/Log.jsm"); 1.27 +Cu.import("resource://services-common/utils.js"); 1.28 + 1.29 +const STORAGE_HTTP_LOGGER = "Services.Common.Test.Server"; 1.30 +const STORAGE_API_VERSION = "2.0"; 1.31 + 1.32 +// Use the same method that record.js does, which mirrors the server. 1.33 +function new_timestamp() { 1.34 + return Math.round(Date.now()); 1.35 +} 1.36 + 1.37 +function isInteger(s) { 1.38 + let re = /^[0-9]+$/; 1.39 + return re.test(s); 1.40 +} 1.41 + 1.42 +function writeHttpBody(response, body) { 1.43 + if (!body) { 1.44 + return; 1.45 + } 1.46 + 1.47 + response.bodyOutputStream.write(body, body.length); 1.48 +} 1.49 + 1.50 +function sendMozSvcError(request, response, code) { 1.51 + response.setStatusLine(request.httpVersion, 400, "Bad Request"); 1.52 + response.setHeader("Content-Type", "text/plain", false); 1.53 + response.bodyOutputStream.write(code, code.length); 1.54 +} 1.55 + 1.56 +/** 1.57 + * Represent a BSO on the server. 1.58 + * 1.59 + * A BSO is constructed from an ID, content, and a modified time. 1.60 + * 1.61 + * @param id 1.62 + * (string) ID of the BSO being created. 1.63 + * @param payload 1.64 + * (strong|object) Payload for the BSO. Should ideally be a string. If 1.65 + * an object is passed, it will be fed into JSON.stringify and that 1.66 + * output will be set as the payload. 1.67 + * @param modified 1.68 + * (number) Milliseconds since UNIX epoch that the BSO was last 1.69 + * modified. If not defined or null, the current time will be used. 1.70 + */ 1.71 +this.ServerBSO = function ServerBSO(id, payload, modified) { 1.72 + if (!id) { 1.73 + throw new Error("No ID for ServerBSO!"); 1.74 + } 1.75 + 1.76 + if (!id.match(/^[a-zA-Z0-9_-]{1,64}$/)) { 1.77 + throw new Error("BSO ID is invalid: " + id); 1.78 + } 1.79 + 1.80 + this._log = Log.repository.getLogger(STORAGE_HTTP_LOGGER); 1.81 + 1.82 + this.id = id; 1.83 + if (!payload) { 1.84 + return; 1.85 + } 1.86 + 1.87 + CommonUtils.ensureMillisecondsTimestamp(modified); 1.88 + 1.89 + if (typeof payload == "object") { 1.90 + payload = JSON.stringify(payload); 1.91 + } 1.92 + 1.93 + this.payload = payload; 1.94 + this.modified = modified || new_timestamp(); 1.95 +} 1.96 +ServerBSO.prototype = { 1.97 + FIELDS: [ 1.98 + "id", 1.99 + "modified", 1.100 + "payload", 1.101 + "ttl", 1.102 + "sortindex", 1.103 + ], 1.104 + 1.105 + toJSON: function toJSON() { 1.106 + let obj = {}; 1.107 + 1.108 + for each (let key in this.FIELDS) { 1.109 + if (this[key] !== undefined) { 1.110 + obj[key] = this[key]; 1.111 + } 1.112 + } 1.113 + 1.114 + return obj; 1.115 + }, 1.116 + 1.117 + delete: function delete_() { 1.118 + this.deleted = true; 1.119 + 1.120 + delete this.payload; 1.121 + delete this.modified; 1.122 + }, 1.123 + 1.124 + /** 1.125 + * Handler for GET requests for this BSO. 1.126 + */ 1.127 + getHandler: function getHandler(request, response) { 1.128 + let code = 200; 1.129 + let status = "OK"; 1.130 + let body; 1.131 + 1.132 + function sendResponse() { 1.133 + response.setStatusLine(request.httpVersion, code, status); 1.134 + writeHttpBody(response, body); 1.135 + } 1.136 + 1.137 + if (request.hasHeader("x-if-modified-since")) { 1.138 + let headerModified = parseInt(request.getHeader("x-if-modified-since"), 1.139 + 10); 1.140 + CommonUtils.ensureMillisecondsTimestamp(headerModified); 1.141 + 1.142 + if (headerModified >= this.modified) { 1.143 + code = 304; 1.144 + status = "Not Modified"; 1.145 + 1.146 + sendResponse(); 1.147 + return; 1.148 + } 1.149 + } else if (request.hasHeader("x-if-unmodified-since")) { 1.150 + let requestModified = parseInt(request.getHeader("x-if-unmodified-since"), 1.151 + 10); 1.152 + let serverModified = this.modified; 1.153 + 1.154 + if (serverModified > requestModified) { 1.155 + code = 412; 1.156 + status = "Precondition Failed"; 1.157 + sendResponse(); 1.158 + return; 1.159 + } 1.160 + } 1.161 + 1.162 + if (!this.deleted) { 1.163 + body = JSON.stringify(this.toJSON()); 1.164 + response.setHeader("Content-Type", "application/json", false); 1.165 + response.setHeader("X-Last-Modified", "" + this.modified, false); 1.166 + } else { 1.167 + code = 404; 1.168 + status = "Not Found"; 1.169 + } 1.170 + 1.171 + sendResponse(); 1.172 + }, 1.173 + 1.174 + /** 1.175 + * Handler for PUT requests for this BSO. 1.176 + */ 1.177 + putHandler: function putHandler(request, response) { 1.178 + if (request.hasHeader("Content-Type")) { 1.179 + let ct = request.getHeader("Content-Type"); 1.180 + if (ct != "application/json") { 1.181 + throw HTTP_415; 1.182 + } 1.183 + } 1.184 + 1.185 + let input = CommonUtils.readBytesFromInputStream(request.bodyInputStream); 1.186 + let parsed; 1.187 + try { 1.188 + parsed = JSON.parse(input); 1.189 + } catch (ex) { 1.190 + return sendMozSvcError(request, response, "8"); 1.191 + } 1.192 + 1.193 + if (typeof(parsed) != "object") { 1.194 + return sendMozSvcError(request, response, "8"); 1.195 + } 1.196 + 1.197 + // Don't update if a conditional request fails preconditions. 1.198 + if (request.hasHeader("x-if-unmodified-since")) { 1.199 + let reqModified = parseInt(request.getHeader("x-if-unmodified-since")); 1.200 + 1.201 + if (reqModified < this.modified) { 1.202 + response.setStatusLine(request.httpVersion, 412, "Precondition Failed"); 1.203 + return; 1.204 + } 1.205 + } 1.206 + 1.207 + let code, status; 1.208 + if (this.payload) { 1.209 + code = 204; 1.210 + status = "No Content"; 1.211 + } else { 1.212 + code = 201; 1.213 + status = "Created"; 1.214 + } 1.215 + 1.216 + // Alert when we see unrecognized fields. 1.217 + for (let [key, value] in Iterator(parsed)) { 1.218 + switch (key) { 1.219 + case "payload": 1.220 + if (typeof(value) != "string") { 1.221 + sendMozSvcError(request, response, "8"); 1.222 + return true; 1.223 + } 1.224 + 1.225 + this.payload = value; 1.226 + break; 1.227 + 1.228 + case "ttl": 1.229 + if (!isInteger(value)) { 1.230 + sendMozSvcError(request, response, "8"); 1.231 + return true; 1.232 + } 1.233 + this.ttl = parseInt(value, 10); 1.234 + break; 1.235 + 1.236 + case "sortindex": 1.237 + if (!isInteger(value) || value.length > 9) { 1.238 + sendMozSvcError(request, response, "8"); 1.239 + return true; 1.240 + } 1.241 + this.sortindex = parseInt(value, 10); 1.242 + break; 1.243 + 1.244 + case "id": 1.245 + break; 1.246 + 1.247 + default: 1.248 + this._log.warn("Unexpected field in BSO record: " + key); 1.249 + sendMozSvcError(request, response, "8"); 1.250 + return true; 1.251 + } 1.252 + } 1.253 + 1.254 + this.modified = request.timestamp; 1.255 + this.deleted = false; 1.256 + response.setHeader("X-Last-Modified", "" + this.modified, false); 1.257 + 1.258 + response.setStatusLine(request.httpVersion, code, status); 1.259 + }, 1.260 +}; 1.261 + 1.262 +/** 1.263 + * Represent a collection on the server. 1.264 + * 1.265 + * The '_bsos' attribute is a mapping of id -> ServerBSO objects. 1.266 + * 1.267 + * Note that if you want these records to be accessible individually, 1.268 + * you need to register their handlers with the server separately, or use a 1.269 + * containing HTTP server that will do so on your behalf. 1.270 + * 1.271 + * @param bsos 1.272 + * An object mapping BSO IDs to ServerBSOs. 1.273 + * @param acceptNew 1.274 + * If true, POSTs to this collection URI will result in new BSOs being 1.275 + * created and wired in on the fly. 1.276 + * @param timestamp 1.277 + * An optional timestamp value to initialize the modified time of the 1.278 + * collection. This should be in the format returned by new_timestamp(). 1.279 + */ 1.280 +this.StorageServerCollection = 1.281 + function StorageServerCollection(bsos, acceptNew, timestamp=new_timestamp()) { 1.282 + this._bsos = bsos || {}; 1.283 + this.acceptNew = acceptNew || false; 1.284 + 1.285 + /* 1.286 + * Track modified timestamp. 1.287 + * We can't just use the timestamps of contained BSOs: an empty collection 1.288 + * has a modified time. 1.289 + */ 1.290 + CommonUtils.ensureMillisecondsTimestamp(timestamp); 1.291 + this._timestamp = timestamp; 1.292 + 1.293 + this._log = Log.repository.getLogger(STORAGE_HTTP_LOGGER); 1.294 +} 1.295 +StorageServerCollection.prototype = { 1.296 + BATCH_MAX_COUNT: 100, // # of records. 1.297 + BATCH_MAX_SIZE: 1024 * 1024, // # bytes. 1.298 + 1.299 + _timestamp: null, 1.300 + 1.301 + get timestamp() { 1.302 + return this._timestamp; 1.303 + }, 1.304 + 1.305 + set timestamp(timestamp) { 1.306 + CommonUtils.ensureMillisecondsTimestamp(timestamp); 1.307 + this._timestamp = timestamp; 1.308 + }, 1.309 + 1.310 + get totalPayloadSize() { 1.311 + let size = 0; 1.312 + for each (let bso in this.bsos()) { 1.313 + size += bso.payload.length; 1.314 + } 1.315 + 1.316 + return size; 1.317 + }, 1.318 + 1.319 + /** 1.320 + * Convenience accessor for our BSO keys. 1.321 + * Excludes deleted items, of course. 1.322 + * 1.323 + * @param filter 1.324 + * A predicate function (applied to the ID and BSO) which dictates 1.325 + * whether to include the BSO's ID in the output. 1.326 + * 1.327 + * @return an array of IDs. 1.328 + */ 1.329 + keys: function keys(filter) { 1.330 + return [id for ([id, bso] in Iterator(this._bsos)) 1.331 + if (!bso.deleted && (!filter || filter(id, bso)))]; 1.332 + }, 1.333 + 1.334 + /** 1.335 + * Convenience method to get an array of BSOs. 1.336 + * Optionally provide a filter function. 1.337 + * 1.338 + * @param filter 1.339 + * A predicate function, applied to the BSO, which dictates whether to 1.340 + * include the BSO in the output. 1.341 + * 1.342 + * @return an array of ServerBSOs. 1.343 + */ 1.344 + bsos: function bsos(filter) { 1.345 + let os = [bso for ([id, bso] in Iterator(this._bsos)) 1.346 + if (!bso.deleted)]; 1.347 + 1.348 + if (!filter) { 1.349 + return os; 1.350 + } 1.351 + 1.352 + return os.filter(filter); 1.353 + }, 1.354 + 1.355 + /** 1.356 + * Obtain a BSO by ID. 1.357 + */ 1.358 + bso: function bso(id) { 1.359 + return this._bsos[id]; 1.360 + }, 1.361 + 1.362 + /** 1.363 + * Obtain the payload of a specific BSO. 1.364 + * 1.365 + * Raises if the specified BSO does not exist. 1.366 + */ 1.367 + payload: function payload(id) { 1.368 + return this.bso(id).payload; 1.369 + }, 1.370 + 1.371 + /** 1.372 + * Insert the provided BSO under its ID. 1.373 + * 1.374 + * @return the provided BSO. 1.375 + */ 1.376 + insertBSO: function insertBSO(bso) { 1.377 + return this._bsos[bso.id] = bso; 1.378 + }, 1.379 + 1.380 + /** 1.381 + * Insert the provided payload as part of a new ServerBSO with the provided 1.382 + * ID. 1.383 + * 1.384 + * @param id 1.385 + * The GUID for the BSO. 1.386 + * @param payload 1.387 + * The payload, as provided to the ServerBSO constructor. 1.388 + * @param modified 1.389 + * An optional modified time for the ServerBSO. If not specified, the 1.390 + * current time will be used. 1.391 + * 1.392 + * @return the inserted BSO. 1.393 + */ 1.394 + insert: function insert(id, payload, modified) { 1.395 + return this.insertBSO(new ServerBSO(id, payload, modified)); 1.396 + }, 1.397 + 1.398 + /** 1.399 + * Removes an object entirely from the collection. 1.400 + * 1.401 + * @param id 1.402 + * (string) ID to remove. 1.403 + */ 1.404 + remove: function remove(id) { 1.405 + delete this._bsos[id]; 1.406 + }, 1.407 + 1.408 + _inResultSet: function _inResultSet(bso, options) { 1.409 + if (!bso.payload) { 1.410 + return false; 1.411 + } 1.412 + 1.413 + if (options.ids) { 1.414 + if (options.ids.indexOf(bso.id) == -1) { 1.415 + return false; 1.416 + } 1.417 + } 1.418 + 1.419 + if (options.newer) { 1.420 + if (bso.modified <= options.newer) { 1.421 + return false; 1.422 + } 1.423 + } 1.424 + 1.425 + if (options.older) { 1.426 + if (bso.modified >= options.older) { 1.427 + return false; 1.428 + } 1.429 + } 1.430 + 1.431 + return true; 1.432 + }, 1.433 + 1.434 + count: function count(options) { 1.435 + options = options || {}; 1.436 + let c = 0; 1.437 + for (let [id, bso] in Iterator(this._bsos)) { 1.438 + if (bso.modified && this._inResultSet(bso, options)) { 1.439 + c++; 1.440 + } 1.441 + } 1.442 + return c; 1.443 + }, 1.444 + 1.445 + get: function get(options) { 1.446 + let data = []; 1.447 + for each (let bso in this._bsos) { 1.448 + if (!bso.modified) { 1.449 + continue; 1.450 + } 1.451 + 1.452 + if (!this._inResultSet(bso, options)) { 1.453 + continue; 1.454 + } 1.455 + 1.456 + data.push(bso); 1.457 + } 1.458 + 1.459 + if (options.sort) { 1.460 + if (options.sort == "oldest") { 1.461 + data.sort(function sortOldest(a, b) { 1.462 + if (a.modified == b.modified) { 1.463 + return 0; 1.464 + } 1.465 + 1.466 + return a.modified < b.modified ? -1 : 1; 1.467 + }); 1.468 + } else if (options.sort == "newest") { 1.469 + data.sort(function sortNewest(a, b) { 1.470 + if (a.modified == b.modified) { 1.471 + return 0; 1.472 + } 1.473 + 1.474 + return a.modified > b.modified ? -1 : 1; 1.475 + }); 1.476 + } else if (options.sort == "index") { 1.477 + data.sort(function sortIndex(a, b) { 1.478 + if (a.sortindex == b.sortindex) { 1.479 + return 0; 1.480 + } 1.481 + 1.482 + if (a.sortindex !== undefined && b.sortindex == undefined) { 1.483 + return 1; 1.484 + } 1.485 + 1.486 + if (a.sortindex === undefined && b.sortindex !== undefined) { 1.487 + return -1; 1.488 + } 1.489 + 1.490 + return a.sortindex > b.sortindex ? -1 : 1; 1.491 + }); 1.492 + } 1.493 + } 1.494 + 1.495 + if (options.limit) { 1.496 + data = data.slice(0, options.limit); 1.497 + } 1.498 + 1.499 + return data; 1.500 + }, 1.501 + 1.502 + post: function post(input, timestamp) { 1.503 + let success = []; 1.504 + let failed = {}; 1.505 + let count = 0; 1.506 + let size = 0; 1.507 + 1.508 + // This will count records where we have an existing ServerBSO 1.509 + // registered with us as successful and all other records as failed. 1.510 + for each (let record in input) { 1.511 + count += 1; 1.512 + if (count > this.BATCH_MAX_COUNT) { 1.513 + failed[record.id] = "Max record count exceeded."; 1.514 + continue; 1.515 + } 1.516 + 1.517 + if (typeof(record.payload) != "string") { 1.518 + failed[record.id] = "Payload is not a string!"; 1.519 + continue; 1.520 + } 1.521 + 1.522 + size += record.payload.length; 1.523 + if (size > this.BATCH_MAX_SIZE) { 1.524 + failed[record.id] = "Payload max size exceeded!"; 1.525 + continue; 1.526 + } 1.527 + 1.528 + if (record.sortindex) { 1.529 + if (!isInteger(record.sortindex)) { 1.530 + failed[record.id] = "sortindex is not an integer."; 1.531 + continue; 1.532 + } 1.533 + 1.534 + if (record.sortindex.length > 9) { 1.535 + failed[record.id] = "sortindex is too long."; 1.536 + continue; 1.537 + } 1.538 + } 1.539 + 1.540 + if ("ttl" in record) { 1.541 + if (!isInteger(record.ttl)) { 1.542 + failed[record.id] = "ttl is not an integer."; 1.543 + continue; 1.544 + } 1.545 + } 1.546 + 1.547 + try { 1.548 + let bso = this.bso(record.id); 1.549 + if (!bso && this.acceptNew) { 1.550 + this._log.debug("Creating BSO " + JSON.stringify(record.id) + 1.551 + " on the fly."); 1.552 + bso = new ServerBSO(record.id); 1.553 + this.insertBSO(bso); 1.554 + } 1.555 + if (bso) { 1.556 + bso.payload = record.payload; 1.557 + bso.modified = timestamp; 1.558 + bso.deleted = false; 1.559 + success.push(record.id); 1.560 + 1.561 + if (record.sortindex) { 1.562 + bso.sortindex = parseInt(record.sortindex, 10); 1.563 + } 1.564 + 1.565 + } else { 1.566 + failed[record.id] = "no bso configured"; 1.567 + } 1.568 + } catch (ex) { 1.569 + this._log.info("Exception when processing BSO: " + 1.570 + CommonUtils.exceptionStr(ex)); 1.571 + failed[record.id] = "Exception when processing."; 1.572 + } 1.573 + } 1.574 + return {success: success, failed: failed}; 1.575 + }, 1.576 + 1.577 + delete: function delete_(options) { 1.578 + options = options || {}; 1.579 + 1.580 + // Protocol 2.0 only allows the "ids" query string argument. 1.581 + let keys = Object.keys(options).filter(function(k) { 1.582 + return k != "ids"; 1.583 + }); 1.584 + if (keys.length) { 1.585 + this._log.warn("Invalid query string parameter to collection delete: " + 1.586 + keys.join(", ")); 1.587 + throw new Error("Malformed client request."); 1.588 + } 1.589 + 1.590 + if (options.ids && options.ids.length > this.BATCH_MAX_COUNT) { 1.591 + throw HTTP_400; 1.592 + } 1.593 + 1.594 + let deleted = []; 1.595 + for (let [id, bso] in Iterator(this._bsos)) { 1.596 + if (this._inResultSet(bso, options)) { 1.597 + this._log.debug("Deleting " + JSON.stringify(bso)); 1.598 + deleted.push(bso.id); 1.599 + bso.delete(); 1.600 + } 1.601 + } 1.602 + return deleted; 1.603 + }, 1.604 + 1.605 + parseOptions: function parseOptions(request) { 1.606 + let options = {}; 1.607 + 1.608 + for each (let chunk in request.queryString.split("&")) { 1.609 + if (!chunk) { 1.610 + continue; 1.611 + } 1.612 + chunk = chunk.split("="); 1.613 + let key = decodeURIComponent(chunk[0]); 1.614 + if (chunk.length == 1) { 1.615 + options[key] = ""; 1.616 + } else { 1.617 + options[key] = decodeURIComponent(chunk[1]); 1.618 + } 1.619 + } 1.620 + 1.621 + if (options.ids) { 1.622 + options.ids = options.ids.split(","); 1.623 + } 1.624 + 1.625 + if (options.newer) { 1.626 + if (!isInteger(options.newer)) { 1.627 + throw HTTP_400; 1.628 + } 1.629 + 1.630 + CommonUtils.ensureMillisecondsTimestamp(options.newer); 1.631 + options.newer = parseInt(options.newer, 10); 1.632 + } 1.633 + 1.634 + if (options.older) { 1.635 + if (!isInteger(options.older)) { 1.636 + throw HTTP_400; 1.637 + } 1.638 + 1.639 + CommonUtils.ensureMillisecondsTimestamp(options.older); 1.640 + options.older = parseInt(options.older, 10); 1.641 + } 1.642 + 1.643 + if (options.limit) { 1.644 + if (!isInteger(options.limit)) { 1.645 + throw HTTP_400; 1.646 + } 1.647 + 1.648 + options.limit = parseInt(options.limit, 10); 1.649 + } 1.650 + 1.651 + return options; 1.652 + }, 1.653 + 1.654 + getHandler: function getHandler(request, response) { 1.655 + let options = this.parseOptions(request); 1.656 + let data = this.get(options); 1.657 + 1.658 + if (request.hasHeader("x-if-modified-since")) { 1.659 + let requestModified = parseInt(request.getHeader("x-if-modified-since"), 1.660 + 10); 1.661 + let newestBSO = 0; 1.662 + for each (let bso in data) { 1.663 + if (bso.modified > newestBSO) { 1.664 + newestBSO = bso.modified; 1.665 + } 1.666 + } 1.667 + 1.668 + if (requestModified >= newestBSO) { 1.669 + response.setHeader("X-Last-Modified", "" + newestBSO); 1.670 + response.setStatusLine(request.httpVersion, 304, "Not Modified"); 1.671 + return; 1.672 + } 1.673 + } else if (request.hasHeader("x-if-unmodified-since")) { 1.674 + let requestModified = parseInt(request.getHeader("x-if-unmodified-since"), 1.675 + 10); 1.676 + let serverModified = this.timestamp; 1.677 + 1.678 + if (serverModified > requestModified) { 1.679 + response.setHeader("X-Last-Modified", "" + serverModified); 1.680 + response.setStatusLine(request.httpVersion, 412, "Precondition Failed"); 1.681 + return; 1.682 + } 1.683 + } 1.684 + 1.685 + if (options.full) { 1.686 + data = data.map(function map(bso) { 1.687 + return bso.toJSON(); 1.688 + }); 1.689 + } else { 1.690 + data = data.map(function map(bso) { 1.691 + return bso.id; 1.692 + }); 1.693 + } 1.694 + 1.695 + // application/json is default media type. 1.696 + let newlines = false; 1.697 + if (request.hasHeader("accept")) { 1.698 + let accept = request.getHeader("accept"); 1.699 + if (accept == "application/newlines") { 1.700 + newlines = true; 1.701 + } else if (accept != "application/json") { 1.702 + throw HTTP_406; 1.703 + } 1.704 + } 1.705 + 1.706 + let body; 1.707 + if (newlines) { 1.708 + response.setHeader("Content-Type", "application/newlines", false); 1.709 + let normalized = data.map(function map(d) { 1.710 + return JSON.stringify(d); 1.711 + }); 1.712 + 1.713 + body = normalized.join("\n") + "\n"; 1.714 + } else { 1.715 + response.setHeader("Content-Type", "application/json", false); 1.716 + body = JSON.stringify({items: data}); 1.717 + } 1.718 + 1.719 + this._log.info("Records: " + data.length); 1.720 + response.setHeader("X-Num-Records", "" + data.length, false); 1.721 + response.setHeader("X-Last-Modified", "" + this.timestamp, false); 1.722 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.723 + response.bodyOutputStream.write(body, body.length); 1.724 + }, 1.725 + 1.726 + postHandler: function postHandler(request, response) { 1.727 + let options = this.parseOptions(request); 1.728 + 1.729 + if (!request.hasHeader("content-type")) { 1.730 + this._log.info("No Content-Type request header!"); 1.731 + throw HTTP_400; 1.732 + } 1.733 + 1.734 + let inputStream = request.bodyInputStream; 1.735 + let inputBody = CommonUtils.readBytesFromInputStream(inputStream); 1.736 + let input = []; 1.737 + 1.738 + let inputMediaType = request.getHeader("content-type"); 1.739 + if (inputMediaType == "application/json") { 1.740 + try { 1.741 + input = JSON.parse(inputBody); 1.742 + } catch (ex) { 1.743 + this._log.info("JSON parse error on input body!"); 1.744 + throw HTTP_400; 1.745 + } 1.746 + 1.747 + if (!Array.isArray(input)) { 1.748 + this._log.info("Input JSON type not an array!"); 1.749 + return sendMozSvcError(request, response, "8"); 1.750 + } 1.751 + } else if (inputMediaType == "application/newlines") { 1.752 + for each (let line in inputBody.split("\n")) { 1.753 + let record; 1.754 + try { 1.755 + record = JSON.parse(line); 1.756 + } catch (ex) { 1.757 + this._log.info("JSON parse error on line!"); 1.758 + return sendMozSvcError(request, response, "8"); 1.759 + } 1.760 + 1.761 + input.push(record); 1.762 + } 1.763 + } else { 1.764 + this._log.info("Unknown media type: " + inputMediaType); 1.765 + throw HTTP_415; 1.766 + } 1.767 + 1.768 + if (this._ensureUnmodifiedSince(request, response)) { 1.769 + return; 1.770 + } 1.771 + 1.772 + let res = this.post(input, request.timestamp); 1.773 + let body = JSON.stringify(res); 1.774 + response.setHeader("Content-Type", "application/json", false); 1.775 + this.timestamp = request.timestamp; 1.776 + response.setHeader("X-Last-Modified", "" + this.timestamp, false); 1.777 + 1.778 + response.setStatusLine(request.httpVersion, "200", "OK"); 1.779 + response.bodyOutputStream.write(body, body.length); 1.780 + }, 1.781 + 1.782 + deleteHandler: function deleteHandler(request, response) { 1.783 + this._log.debug("Invoking StorageServerCollection.DELETE."); 1.784 + 1.785 + let options = this.parseOptions(request); 1.786 + 1.787 + if (this._ensureUnmodifiedSince(request, response)) { 1.788 + return; 1.789 + } 1.790 + 1.791 + let deleted = this.delete(options); 1.792 + response.deleted = deleted; 1.793 + this.timestamp = request.timestamp; 1.794 + 1.795 + response.setStatusLine(request.httpVersion, 204, "No Content"); 1.796 + }, 1.797 + 1.798 + handler: function handler() { 1.799 + let self = this; 1.800 + 1.801 + return function(request, response) { 1.802 + switch(request.method) { 1.803 + case "GET": 1.804 + return self.getHandler(request, response); 1.805 + 1.806 + case "POST": 1.807 + return self.postHandler(request, response); 1.808 + 1.809 + case "DELETE": 1.810 + return self.deleteHandler(request, response); 1.811 + 1.812 + } 1.813 + 1.814 + request.setHeader("Allow", "GET,POST,DELETE"); 1.815 + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); 1.816 + }; 1.817 + }, 1.818 + 1.819 + _ensureUnmodifiedSince: function _ensureUnmodifiedSince(request, response) { 1.820 + if (!request.hasHeader("x-if-unmodified-since")) { 1.821 + return false; 1.822 + } 1.823 + 1.824 + let requestModified = parseInt(request.getHeader("x-if-unmodified-since"), 1.825 + 10); 1.826 + let serverModified = this.timestamp; 1.827 + 1.828 + this._log.debug("Request modified time: " + requestModified + 1.829 + "; Server modified time: " + serverModified); 1.830 + if (serverModified <= requestModified) { 1.831 + return false; 1.832 + } 1.833 + 1.834 + this._log.info("Conditional request rejected because client time older " + 1.835 + "than collection timestamp."); 1.836 + response.setStatusLine(request.httpVersion, 412, "Precondition Failed"); 1.837 + return true; 1.838 + }, 1.839 +}; 1.840 + 1.841 + 1.842 +//===========================================================================// 1.843 +// httpd.js-based Storage server. // 1.844 +//===========================================================================// 1.845 + 1.846 +/** 1.847 + * In general, the preferred way of using StorageServer is to directly 1.848 + * introspect it. Callbacks are available for operations which are hard to 1.849 + * verify through introspection, such as deletions. 1.850 + * 1.851 + * One of the goals of this server is to provide enough hooks for test code to 1.852 + * find out what it needs without monkeypatching. Use this object as your 1.853 + * prototype, and override as appropriate. 1.854 + */ 1.855 +this.StorageServerCallback = { 1.856 + onCollectionDeleted: function onCollectionDeleted(user, collection) {}, 1.857 + onItemDeleted: function onItemDeleted(user, collection, bsoID) {}, 1.858 + 1.859 + /** 1.860 + * Called at the top of every request. 1.861 + * 1.862 + * Allows the test to inspect the request. Hooks should be careful not to 1.863 + * modify or change state of the request or they may impact future processing. 1.864 + */ 1.865 + onRequest: function onRequest(request) {}, 1.866 +}; 1.867 + 1.868 +/** 1.869 + * Construct a new test Storage server. Takes a callback object (e.g., 1.870 + * StorageServerCallback) as input. 1.871 + */ 1.872 +this.StorageServer = function StorageServer(callback) { 1.873 + this.callback = callback || {__proto__: StorageServerCallback}; 1.874 + this.server = new HttpServer(); 1.875 + this.started = false; 1.876 + this.users = {}; 1.877 + this.requestCount = 0; 1.878 + this._log = Log.repository.getLogger(STORAGE_HTTP_LOGGER); 1.879 + 1.880 + // Install our own default handler. This allows us to mess around with the 1.881 + // whole URL space. 1.882 + let handler = this.server._handler; 1.883 + handler._handleDefault = this.handleDefault.bind(this, handler); 1.884 +} 1.885 +StorageServer.prototype = { 1.886 + DEFAULT_QUOTA: 1024 * 1024, // # bytes. 1.887 + 1.888 + server: null, // HttpServer. 1.889 + users: null, // Map of username => {collections, password}. 1.890 + 1.891 + /** 1.892 + * If true, the server will allow any arbitrary user to be used. 1.893 + * 1.894 + * No authentication will be performed. Whatever user is detected from the 1.895 + * URL or auth headers will be created (if needed) and used. 1.896 + */ 1.897 + allowAllUsers: false, 1.898 + 1.899 + /** 1.900 + * Start the StorageServer's underlying HTTP server. 1.901 + * 1.902 + * @param port 1.903 + * The numeric port on which to start. A falsy value implies to 1.904 + * select any available port. 1.905 + * @param cb 1.906 + * A callback function (of no arguments) which is invoked after 1.907 + * startup. 1.908 + */ 1.909 + start: function start(port, cb) { 1.910 + if (this.started) { 1.911 + this._log.warn("Warning: server already started on " + this.port); 1.912 + return; 1.913 + } 1.914 + if (!port) { 1.915 + port = -1; 1.916 + } 1.917 + this.port = port; 1.918 + 1.919 + try { 1.920 + this.server.start(this.port); 1.921 + this.port = this.server.identity.primaryPort; 1.922 + this.started = true; 1.923 + if (cb) { 1.924 + cb(); 1.925 + } 1.926 + } catch (ex) { 1.927 + _("=========================================="); 1.928 + _("Got exception starting Storage HTTP server on port " + this.port); 1.929 + _("Error: " + CommonUtils.exceptionStr(ex)); 1.930 + _("Is there a process already listening on port " + this.port + "?"); 1.931 + _("=========================================="); 1.932 + do_throw(ex); 1.933 + } 1.934 + }, 1.935 + 1.936 + /** 1.937 + * Start the server synchronously. 1.938 + * 1.939 + * @param port 1.940 + * The numeric port on which to start. The default is to choose 1.941 + * any available port. 1.942 + */ 1.943 + startSynchronous: function startSynchronous(port=-1) { 1.944 + let cb = Async.makeSpinningCallback(); 1.945 + this.start(port, cb); 1.946 + cb.wait(); 1.947 + }, 1.948 + 1.949 + /** 1.950 + * Stop the StorageServer's HTTP server. 1.951 + * 1.952 + * @param cb 1.953 + * A callback function. Invoked after the server has been stopped. 1.954 + * 1.955 + */ 1.956 + stop: function stop(cb) { 1.957 + if (!this.started) { 1.958 + this._log.warn("StorageServer: Warning: server not running. Can't stop " + 1.959 + "me now!"); 1.960 + return; 1.961 + } 1.962 + 1.963 + this.server.stop(cb); 1.964 + this.started = false; 1.965 + }, 1.966 + 1.967 + serverTime: function serverTime() { 1.968 + return new_timestamp(); 1.969 + }, 1.970 + 1.971 + /** 1.972 + * Create a new user, complete with an empty set of collections. 1.973 + * 1.974 + * @param username 1.975 + * The username to use. An Error will be thrown if a user by that name 1.976 + * already exists. 1.977 + * @param password 1.978 + * A password string. 1.979 + * 1.980 + * @return a user object, as would be returned by server.user(username). 1.981 + */ 1.982 + registerUser: function registerUser(username, password) { 1.983 + if (username in this.users) { 1.984 + throw new Error("User already exists."); 1.985 + } 1.986 + 1.987 + if (!isFinite(parseInt(username))) { 1.988 + throw new Error("Usernames must be numeric: " + username); 1.989 + } 1.990 + 1.991 + this._log.info("Registering new user with server: " + username); 1.992 + this.users[username] = { 1.993 + password: password, 1.994 + collections: {}, 1.995 + quota: this.DEFAULT_QUOTA, 1.996 + }; 1.997 + return this.user(username); 1.998 + }, 1.999 + 1.1000 + userExists: function userExists(username) { 1.1001 + return username in this.users; 1.1002 + }, 1.1003 + 1.1004 + getCollection: function getCollection(username, collection) { 1.1005 + return this.users[username].collections[collection]; 1.1006 + }, 1.1007 + 1.1008 + _insertCollection: function _insertCollection(collections, collection, bsos) { 1.1009 + let coll = new StorageServerCollection(bsos, true); 1.1010 + coll.collectionHandler = coll.handler(); 1.1011 + collections[collection] = coll; 1.1012 + return coll; 1.1013 + }, 1.1014 + 1.1015 + createCollection: function createCollection(username, collection, bsos) { 1.1016 + if (!(username in this.users)) { 1.1017 + throw new Error("Unknown user."); 1.1018 + } 1.1019 + let collections = this.users[username].collections; 1.1020 + if (collection in collections) { 1.1021 + throw new Error("Collection already exists."); 1.1022 + } 1.1023 + return this._insertCollection(collections, collection, bsos); 1.1024 + }, 1.1025 + 1.1026 + deleteCollection: function deleteCollection(username, collection) { 1.1027 + if (!(username in this.users)) { 1.1028 + throw new Error("Unknown user."); 1.1029 + } 1.1030 + delete this.users[username].collections[collection]; 1.1031 + }, 1.1032 + 1.1033 + /** 1.1034 + * Accept a map like the following: 1.1035 + * { 1.1036 + * meta: {global: {version: 1, ...}}, 1.1037 + * crypto: {"keys": {}, foo: {bar: 2}}, 1.1038 + * bookmarks: {} 1.1039 + * } 1.1040 + * to cause collections and BSOs to be created. 1.1041 + * If a collection already exists, no error is raised. 1.1042 + * If a BSO already exists, it will be updated to the new contents. 1.1043 + */ 1.1044 + createContents: function createContents(username, collections) { 1.1045 + if (!(username in this.users)) { 1.1046 + throw new Error("Unknown user."); 1.1047 + } 1.1048 + let userCollections = this.users[username].collections; 1.1049 + for (let [id, contents] in Iterator(collections)) { 1.1050 + let coll = userCollections[id] || 1.1051 + this._insertCollection(userCollections, id); 1.1052 + for (let [bsoID, payload] in Iterator(contents)) { 1.1053 + coll.insert(bsoID, payload); 1.1054 + } 1.1055 + } 1.1056 + }, 1.1057 + 1.1058 + /** 1.1059 + * Insert a BSO in an existing collection. 1.1060 + */ 1.1061 + insertBSO: function insertBSO(username, collection, bso) { 1.1062 + if (!(username in this.users)) { 1.1063 + throw new Error("Unknown user."); 1.1064 + } 1.1065 + let userCollections = this.users[username].collections; 1.1066 + if (!(collection in userCollections)) { 1.1067 + throw new Error("Unknown collection."); 1.1068 + } 1.1069 + userCollections[collection].insertBSO(bso); 1.1070 + return bso; 1.1071 + }, 1.1072 + 1.1073 + /** 1.1074 + * Delete all of the collections for the named user. 1.1075 + * 1.1076 + * @param username 1.1077 + * The name of the affected user. 1.1078 + */ 1.1079 + deleteCollections: function deleteCollections(username) { 1.1080 + if (!(username in this.users)) { 1.1081 + throw new Error("Unknown user."); 1.1082 + } 1.1083 + let userCollections = this.users[username].collections; 1.1084 + for each (let [name, coll] in Iterator(userCollections)) { 1.1085 + this._log.trace("Bulk deleting " + name + " for " + username + "..."); 1.1086 + coll.delete({}); 1.1087 + } 1.1088 + this.users[username].collections = {}; 1.1089 + }, 1.1090 + 1.1091 + getQuota: function getQuota(username) { 1.1092 + if (!(username in this.users)) { 1.1093 + throw new Error("Unknown user."); 1.1094 + } 1.1095 + 1.1096 + return this.users[username].quota; 1.1097 + }, 1.1098 + 1.1099 + /** 1.1100 + * Obtain the newest timestamp of all collections for a user. 1.1101 + */ 1.1102 + newestCollectionTimestamp: function newestCollectionTimestamp(username) { 1.1103 + let collections = this.users[username].collections; 1.1104 + let newest = 0; 1.1105 + for each (let collection in collections) { 1.1106 + if (collection.timestamp > newest) { 1.1107 + newest = collection.timestamp; 1.1108 + } 1.1109 + } 1.1110 + 1.1111 + return newest; 1.1112 + }, 1.1113 + 1.1114 + /** 1.1115 + * Compute the object that is returned for an info/collections request. 1.1116 + */ 1.1117 + infoCollections: function infoCollections(username) { 1.1118 + let responseObject = {}; 1.1119 + let colls = this.users[username].collections; 1.1120 + for (let coll in colls) { 1.1121 + responseObject[coll] = colls[coll].timestamp; 1.1122 + } 1.1123 + this._log.trace("StorageServer: info/collections returning " + 1.1124 + JSON.stringify(responseObject)); 1.1125 + return responseObject; 1.1126 + }, 1.1127 + 1.1128 + infoCounts: function infoCounts(username) { 1.1129 + let data = {}; 1.1130 + let collections = this.users[username].collections; 1.1131 + for (let [k, v] in Iterator(collections)) { 1.1132 + let count = v.count(); 1.1133 + if (!count) { 1.1134 + continue; 1.1135 + } 1.1136 + 1.1137 + data[k] = count; 1.1138 + } 1.1139 + 1.1140 + return data; 1.1141 + }, 1.1142 + 1.1143 + infoUsage: function infoUsage(username) { 1.1144 + let data = {}; 1.1145 + let collections = this.users[username].collections; 1.1146 + for (let [k, v] in Iterator(collections)) { 1.1147 + data[k] = v.totalPayloadSize; 1.1148 + } 1.1149 + 1.1150 + return data; 1.1151 + }, 1.1152 + 1.1153 + infoQuota: function infoQuota(username) { 1.1154 + let total = 0; 1.1155 + for each (let value in this.infoUsage(username)) { 1.1156 + total += value; 1.1157 + } 1.1158 + 1.1159 + return { 1.1160 + quota: this.getQuota(username), 1.1161 + usage: total 1.1162 + }; 1.1163 + }, 1.1164 + 1.1165 + /** 1.1166 + * Simple accessor to allow collective binding and abbreviation of a bunch of 1.1167 + * methods. Yay! 1.1168 + * Use like this: 1.1169 + * 1.1170 + * let u = server.user("john"); 1.1171 + * u.collection("bookmarks").bso("abcdefg").payload; // Etc. 1.1172 + * 1.1173 + * @return a proxy for the user data stored in this server. 1.1174 + */ 1.1175 + user: function user(username) { 1.1176 + let collection = this.getCollection.bind(this, username); 1.1177 + let createCollection = this.createCollection.bind(this, username); 1.1178 + let createContents = this.createContents.bind(this, username); 1.1179 + let modified = function (collectionName) { 1.1180 + return collection(collectionName).timestamp; 1.1181 + } 1.1182 + let deleteCollections = this.deleteCollections.bind(this, username); 1.1183 + let quota = this.getQuota.bind(this, username); 1.1184 + return { 1.1185 + collection: collection, 1.1186 + createCollection: createCollection, 1.1187 + createContents: createContents, 1.1188 + deleteCollections: deleteCollections, 1.1189 + modified: modified, 1.1190 + quota: quota, 1.1191 + }; 1.1192 + }, 1.1193 + 1.1194 + _pruneExpired: function _pruneExpired() { 1.1195 + let now = Date.now(); 1.1196 + 1.1197 + for each (let user in this.users) { 1.1198 + for each (let collection in user.collections) { 1.1199 + for each (let bso in collection.bsos()) { 1.1200 + // ttl === 0 is a special case, so we can't simply !ttl. 1.1201 + if (typeof(bso.ttl) != "number") { 1.1202 + continue; 1.1203 + } 1.1204 + 1.1205 + let ttlDate = bso.modified + (bso.ttl * 1000); 1.1206 + if (ttlDate < now) { 1.1207 + this._log.info("Deleting BSO because TTL expired: " + bso.id); 1.1208 + bso.delete(); 1.1209 + } 1.1210 + } 1.1211 + } 1.1212 + } 1.1213 + }, 1.1214 + 1.1215 + /* 1.1216 + * Regular expressions for splitting up Storage request paths. 1.1217 + * Storage URLs are of the form: 1.1218 + * /$apipath/$version/$userid/$further 1.1219 + * where $further is usually: 1.1220 + * storage/$collection/$bso 1.1221 + * or 1.1222 + * storage/$collection 1.1223 + * or 1.1224 + * info/$op 1.1225 + * 1.1226 + * We assume for the sake of simplicity that $apipath is empty. 1.1227 + * 1.1228 + * N.B., we don't follow any kind of username spec here, because as far as I 1.1229 + * can tell there isn't one. See Bug 689671. Instead we follow the Python 1.1230 + * server code. 1.1231 + * 1.1232 + * Path: [all, version, first, rest] 1.1233 + * Storage: [all, collection?, id?] 1.1234 + */ 1.1235 + pathRE: /^\/([0-9]+(?:\.[0-9]+)?)(?:\/([0-9]+)\/([^\/]+)(?:\/(.+))?)?$/, 1.1236 + storageRE: /^([-_a-zA-Z0-9]+)(?:\/([-_a-zA-Z0-9]+)\/?)?$/, 1.1237 + 1.1238 + defaultHeaders: {}, 1.1239 + 1.1240 + /** 1.1241 + * HTTP response utility. 1.1242 + */ 1.1243 + respond: function respond(req, resp, code, status, body, headers, timestamp) { 1.1244 + this._log.info("Response: " + code + " " + status); 1.1245 + resp.setStatusLine(req.httpVersion, code, status); 1.1246 + for each (let [header, value] in Iterator(headers || this.defaultHeaders)) { 1.1247 + resp.setHeader(header, value, false); 1.1248 + } 1.1249 + 1.1250 + if (timestamp) { 1.1251 + resp.setHeader("X-Timestamp", "" + timestamp, false); 1.1252 + } 1.1253 + 1.1254 + if (body) { 1.1255 + resp.bodyOutputStream.write(body, body.length); 1.1256 + } 1.1257 + }, 1.1258 + 1.1259 + /** 1.1260 + * This is invoked by the HttpServer. `this` is bound to the StorageServer; 1.1261 + * `handler` is the HttpServer's handler. 1.1262 + * 1.1263 + * TODO: need to use the correct Storage API response codes and errors here. 1.1264 + */ 1.1265 + handleDefault: function handleDefault(handler, req, resp) { 1.1266 + this.requestCount++; 1.1267 + let timestamp = new_timestamp(); 1.1268 + try { 1.1269 + this._handleDefault(handler, req, resp, timestamp); 1.1270 + } catch (e) { 1.1271 + if (e instanceof HttpError) { 1.1272 + this.respond(req, resp, e.code, e.description, "", {}, timestamp); 1.1273 + } else { 1.1274 + this._log.warn(CommonUtils.exceptionStr(e)); 1.1275 + throw e; 1.1276 + } 1.1277 + } 1.1278 + }, 1.1279 + 1.1280 + _handleDefault: function _handleDefault(handler, req, resp, timestamp) { 1.1281 + let path = req.path; 1.1282 + if (req.queryString.length) { 1.1283 + path += "?" + req.queryString; 1.1284 + } 1.1285 + 1.1286 + this._log.debug("StorageServer: Handling request: " + req.method + " " + 1.1287 + path); 1.1288 + 1.1289 + if (this.callback.onRequest) { 1.1290 + this.callback.onRequest(req); 1.1291 + } 1.1292 + 1.1293 + // Prune expired records for all users at top of request. This is the 1.1294 + // easiest way to process TTLs since all requests go through here. 1.1295 + this._pruneExpired(); 1.1296 + 1.1297 + req.timestamp = timestamp; 1.1298 + resp.setHeader("X-Timestamp", "" + timestamp, false); 1.1299 + 1.1300 + let parts = this.pathRE.exec(req.path); 1.1301 + if (!parts) { 1.1302 + this._log.debug("StorageServer: Unexpected request: bad URL " + req.path); 1.1303 + throw HTTP_404; 1.1304 + } 1.1305 + 1.1306 + let [all, version, userPath, first, rest] = parts; 1.1307 + if (version != STORAGE_API_VERSION) { 1.1308 + this._log.debug("StorageServer: Unknown version."); 1.1309 + throw HTTP_404; 1.1310 + } 1.1311 + 1.1312 + let username; 1.1313 + 1.1314 + // By default, the server requires users to be authenticated. When a 1.1315 + // request arrives, the user must have been previously configured and 1.1316 + // the request must have authentication. In "allow all users" mode, we 1.1317 + // take the username from the URL, create the user on the fly, and don't 1.1318 + // perform any authentication. 1.1319 + if (!this.allowAllUsers) { 1.1320 + // Enforce authentication. 1.1321 + if (!req.hasHeader("authorization")) { 1.1322 + this.respond(req, resp, 401, "Authorization Required", "{}", { 1.1323 + "WWW-Authenticate": 'Basic realm="secret"' 1.1324 + }); 1.1325 + return; 1.1326 + } 1.1327 + 1.1328 + let ensureUserExists = function ensureUserExists(username) { 1.1329 + if (this.userExists(username)) { 1.1330 + return; 1.1331 + } 1.1332 + 1.1333 + this._log.info("StorageServer: Unknown user: " + username); 1.1334 + throw HTTP_401; 1.1335 + }.bind(this); 1.1336 + 1.1337 + let auth = req.getHeader("authorization"); 1.1338 + this._log.debug("Authorization: " + auth); 1.1339 + 1.1340 + if (auth.indexOf("Basic ") == 0) { 1.1341 + let decoded = CommonUtils.safeAtoB(auth.substr(6)); 1.1342 + this._log.debug("Decoded Basic Auth: " + decoded); 1.1343 + let [user, password] = decoded.split(":", 2); 1.1344 + 1.1345 + if (!password) { 1.1346 + this._log.debug("Malformed HTTP Basic Authorization header: " + auth); 1.1347 + throw HTTP_400; 1.1348 + } 1.1349 + 1.1350 + this._log.debug("Got HTTP Basic auth for user: " + user); 1.1351 + ensureUserExists(user); 1.1352 + username = user; 1.1353 + 1.1354 + if (this.users[user].password != password) { 1.1355 + this._log.debug("StorageServer: Provided password is not correct."); 1.1356 + throw HTTP_401; 1.1357 + } 1.1358 + // TODO support token auth. 1.1359 + } else { 1.1360 + this._log.debug("Unsupported HTTP authorization type: " + auth); 1.1361 + throw HTTP_500; 1.1362 + } 1.1363 + // All users mode. 1.1364 + } else { 1.1365 + // Auto create user with dummy password. 1.1366 + if (!this.userExists(userPath)) { 1.1367 + this.registerUser(userPath, "DUMMY-PASSWORD-*&%#"); 1.1368 + } 1.1369 + 1.1370 + username = userPath; 1.1371 + } 1.1372 + 1.1373 + // Hand off to the appropriate handler for this path component. 1.1374 + if (first in this.toplevelHandlers) { 1.1375 + let handler = this.toplevelHandlers[first]; 1.1376 + try { 1.1377 + return handler.call(this, handler, req, resp, version, username, rest); 1.1378 + } catch (ex) { 1.1379 + this._log.warn("Got exception during request: " + 1.1380 + CommonUtils.exceptionStr(ex)); 1.1381 + throw ex; 1.1382 + } 1.1383 + } 1.1384 + this._log.debug("StorageServer: Unknown top-level " + first); 1.1385 + throw HTTP_404; 1.1386 + }, 1.1387 + 1.1388 + /** 1.1389 + * Collection of the handler methods we use for top-level path components. 1.1390 + */ 1.1391 + toplevelHandlers: { 1.1392 + "storage": function handleStorage(handler, req, resp, version, username, 1.1393 + rest) { 1.1394 + let respond = this.respond.bind(this, req, resp); 1.1395 + if (!rest || !rest.length) { 1.1396 + this._log.debug("StorageServer: top-level storage " + 1.1397 + req.method + " request."); 1.1398 + 1.1399 + if (req.method != "DELETE") { 1.1400 + respond(405, "Method Not Allowed", null, {"Allow": "DELETE"}); 1.1401 + return; 1.1402 + } 1.1403 + 1.1404 + this.user(username).deleteCollections(); 1.1405 + 1.1406 + respond(204, "No Content"); 1.1407 + return; 1.1408 + } 1.1409 + 1.1410 + let match = this.storageRE.exec(rest); 1.1411 + if (!match) { 1.1412 + this._log.warn("StorageServer: Unknown storage operation " + rest); 1.1413 + throw HTTP_404; 1.1414 + } 1.1415 + let [all, collection, bsoID] = match; 1.1416 + let coll = this.getCollection(username, collection); 1.1417 + let collectionExisted = !!coll; 1.1418 + 1.1419 + switch (req.method) { 1.1420 + case "GET": 1.1421 + // Tried to GET on a collection that doesn't exist. 1.1422 + if (!coll) { 1.1423 + respond(404, "Not Found"); 1.1424 + return; 1.1425 + } 1.1426 + 1.1427 + // No BSO URL parameter goes to collection handler. 1.1428 + if (!bsoID) { 1.1429 + return coll.collectionHandler(req, resp); 1.1430 + } 1.1431 + 1.1432 + // Handle non-existent BSO. 1.1433 + let bso = coll.bso(bsoID); 1.1434 + if (!bso) { 1.1435 + respond(404, "Not Found"); 1.1436 + return; 1.1437 + } 1.1438 + 1.1439 + // Proxy to BSO handler. 1.1440 + return bso.getHandler(req, resp); 1.1441 + 1.1442 + case "DELETE": 1.1443 + // Collection doesn't exist. 1.1444 + if (!coll) { 1.1445 + respond(404, "Not Found"); 1.1446 + return; 1.1447 + } 1.1448 + 1.1449 + // Deleting a specific BSO. 1.1450 + if (bsoID) { 1.1451 + let bso = coll.bso(bsoID); 1.1452 + 1.1453 + // BSO does not exist on the server. Nothing to do. 1.1454 + if (!bso) { 1.1455 + respond(404, "Not Found"); 1.1456 + return; 1.1457 + } 1.1458 + 1.1459 + if (req.hasHeader("x-if-unmodified-since")) { 1.1460 + let modified = parseInt(req.getHeader("x-if-unmodified-since")); 1.1461 + CommonUtils.ensureMillisecondsTimestamp(modified); 1.1462 + 1.1463 + if (bso.modified > modified) { 1.1464 + respond(412, "Precondition Failed"); 1.1465 + return; 1.1466 + } 1.1467 + } 1.1468 + 1.1469 + bso.delete(); 1.1470 + coll.timestamp = req.timestamp; 1.1471 + this.callback.onItemDeleted(username, collection, bsoID); 1.1472 + respond(204, "No Content"); 1.1473 + return; 1.1474 + } 1.1475 + 1.1476 + // Proxy to collection handler. 1.1477 + coll.collectionHandler(req, resp); 1.1478 + 1.1479 + // Spot if this is a DELETE for some IDs, and don't blow away the 1.1480 + // whole collection! 1.1481 + // 1.1482 + // We already handled deleting the BSOs by invoking the deleted 1.1483 + // collection's handler. However, in the case of 1.1484 + // 1.1485 + // DELETE storage/foobar 1.1486 + // 1.1487 + // we also need to remove foobar from the collections map. This 1.1488 + // clause tries to differentiate the above request from 1.1489 + // 1.1490 + // DELETE storage/foobar?ids=foo,baz 1.1491 + // 1.1492 + // and do the right thing. 1.1493 + // TODO: less hacky method. 1.1494 + if (-1 == req.queryString.indexOf("ids=")) { 1.1495 + // When you delete the entire collection, we drop it. 1.1496 + this._log.debug("Deleting entire collection."); 1.1497 + delete this.users[username].collections[collection]; 1.1498 + this.callback.onCollectionDeleted(username, collection); 1.1499 + } 1.1500 + 1.1501 + // Notify of item deletion. 1.1502 + let deleted = resp.deleted || []; 1.1503 + for (let i = 0; i < deleted.length; ++i) { 1.1504 + this.callback.onItemDeleted(username, collection, deleted[i]); 1.1505 + } 1.1506 + return; 1.1507 + 1.1508 + case "POST": 1.1509 + case "PUT": 1.1510 + // Auto-create collection if it doesn't exist. 1.1511 + if (!coll) { 1.1512 + coll = this.createCollection(username, collection); 1.1513 + } 1.1514 + 1.1515 + try { 1.1516 + if (bsoID) { 1.1517 + let bso = coll.bso(bsoID); 1.1518 + if (!bso) { 1.1519 + this._log.trace("StorageServer: creating BSO " + collection + 1.1520 + "/" + bsoID); 1.1521 + try { 1.1522 + bso = coll.insert(bsoID); 1.1523 + } catch (ex) { 1.1524 + return sendMozSvcError(req, resp, "8"); 1.1525 + } 1.1526 + } 1.1527 + 1.1528 + bso.putHandler(req, resp); 1.1529 + 1.1530 + coll.timestamp = req.timestamp; 1.1531 + return resp; 1.1532 + } 1.1533 + 1.1534 + return coll.collectionHandler(req, resp); 1.1535 + } catch (ex) { 1.1536 + if (ex instanceof HttpError) { 1.1537 + if (!collectionExisted) { 1.1538 + this.deleteCollection(username, collection); 1.1539 + } 1.1540 + } 1.1541 + 1.1542 + throw ex; 1.1543 + } 1.1544 + 1.1545 + default: 1.1546 + throw new Error("Request method " + req.method + " not implemented."); 1.1547 + } 1.1548 + }, 1.1549 + 1.1550 + "info": function handleInfo(handler, req, resp, version, username, rest) { 1.1551 + switch (rest) { 1.1552 + case "collections": 1.1553 + return this.handleInfoCollections(req, resp, username); 1.1554 + 1.1555 + case "collection_counts": 1.1556 + return this.handleInfoCounts(req, resp, username); 1.1557 + 1.1558 + case "collection_usage": 1.1559 + return this.handleInfoUsage(req, resp, username); 1.1560 + 1.1561 + case "quota": 1.1562 + return this.handleInfoQuota(req, resp, username); 1.1563 + 1.1564 + default: 1.1565 + this._log.warn("StorageServer: Unknown info operation " + rest); 1.1566 + throw HTTP_404; 1.1567 + } 1.1568 + } 1.1569 + }, 1.1570 + 1.1571 + handleInfoConditional: function handleInfoConditional(request, response, 1.1572 + user) { 1.1573 + if (!request.hasHeader("x-if-modified-since")) { 1.1574 + return false; 1.1575 + } 1.1576 + 1.1577 + let requestModified = request.getHeader("x-if-modified-since"); 1.1578 + requestModified = parseInt(requestModified, 10); 1.1579 + 1.1580 + let serverModified = this.newestCollectionTimestamp(user); 1.1581 + 1.1582 + this._log.info("Server mtime: " + serverModified + "; Client modified: " + 1.1583 + requestModified); 1.1584 + if (serverModified > requestModified) { 1.1585 + return false; 1.1586 + } 1.1587 + 1.1588 + this.respond(request, response, 304, "Not Modified", null, { 1.1589 + "X-Last-Modified": "" + serverModified 1.1590 + }); 1.1591 + 1.1592 + return true; 1.1593 + }, 1.1594 + 1.1595 + handleInfoCollections: function handleInfoCollections(request, response, 1.1596 + user) { 1.1597 + if (this.handleInfoConditional(request, response, user)) { 1.1598 + return; 1.1599 + } 1.1600 + 1.1601 + let info = this.infoCollections(user); 1.1602 + let body = JSON.stringify(info); 1.1603 + this.respond(request, response, 200, "OK", body, { 1.1604 + "Content-Type": "application/json", 1.1605 + "X-Last-Modified": "" + this.newestCollectionTimestamp(user), 1.1606 + }); 1.1607 + }, 1.1608 + 1.1609 + handleInfoCounts: function handleInfoCounts(request, response, user) { 1.1610 + if (this.handleInfoConditional(request, response, user)) { 1.1611 + return; 1.1612 + } 1.1613 + 1.1614 + let counts = this.infoCounts(user); 1.1615 + let body = JSON.stringify(counts); 1.1616 + 1.1617 + this.respond(request, response, 200, "OK", body, { 1.1618 + "Content-Type": "application/json", 1.1619 + "X-Last-Modified": "" + this.newestCollectionTimestamp(user), 1.1620 + }); 1.1621 + }, 1.1622 + 1.1623 + handleInfoUsage: function handleInfoUsage(request, response, user) { 1.1624 + if (this.handleInfoConditional(request, response, user)) { 1.1625 + return; 1.1626 + } 1.1627 + 1.1628 + let body = JSON.stringify(this.infoUsage(user)); 1.1629 + this.respond(request, response, 200, "OK", body, { 1.1630 + "Content-Type": "application/json", 1.1631 + "X-Last-Modified": "" + this.newestCollectionTimestamp(user), 1.1632 + }); 1.1633 + }, 1.1634 + 1.1635 + handleInfoQuota: function handleInfoQuota(request, response, user) { 1.1636 + if (this.handleInfoConditional(request, response, user)) { 1.1637 + return; 1.1638 + } 1.1639 + 1.1640 + let body = JSON.stringify(this.infoQuota(user)); 1.1641 + this.respond(request, response, 200, "OK", body, { 1.1642 + "Content-Type": "application/json", 1.1643 + "X-Last-Modified": "" + this.newestCollectionTimestamp(user), 1.1644 + }); 1.1645 + }, 1.1646 +}; 1.1647 + 1.1648 +/** 1.1649 + * Helper to create a storage server for a set of users. 1.1650 + * 1.1651 + * Each user is specified by a map of username to password. 1.1652 + */ 1.1653 +this.storageServerForUsers = 1.1654 + function storageServerForUsers(users, contents, callback) { 1.1655 + let server = new StorageServer(callback); 1.1656 + for (let [user, pass] in Iterator(users)) { 1.1657 + server.registerUser(user, pass); 1.1658 + server.createContents(user, contents); 1.1659 + } 1.1660 + server.start(); 1.1661 + return server; 1.1662 +}