PageRenderTime 73ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/services/common/modules-testing/storageserver.js

https://github.com/edwindotcom/gecko-dev
JavaScript | 1659 lines | 1124 code | 246 blank | 289 comment | 227 complexity | 394147e165f6b277f10abe29cca405c5 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, LGPL-3.0, MIT, AGPL-1.0, MPL-2.0-no-copyleft-exception, MPL-2.0, GPL-2.0, JSON, 0BSD, LGPL-2.1, BSD-2-Clause
  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 file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. /**
  5. * This file contains an implementation of the Storage Server in JavaScript.
  6. *
  7. * The server should not be used for any production purposes.
  8. */
  9. const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
  10. this.EXPORTED_SYMBOLS = [
  11. "ServerBSO",
  12. "StorageServerCallback",
  13. "StorageServerCollection",
  14. "StorageServer",
  15. "storageServerForUsers",
  16. ];
  17. Cu.import("resource://testing-common/httpd.js");
  18. Cu.import("resource://services-common/async.js");
  19. Cu.import("resource://gre/modules/Log.jsm");
  20. Cu.import("resource://services-common/utils.js");
  21. const STORAGE_HTTP_LOGGER = "Services.Common.Test.Server";
  22. const STORAGE_API_VERSION = "2.0";
  23. // Use the same method that record.js does, which mirrors the server.
  24. function new_timestamp() {
  25. return Math.round(Date.now());
  26. }
  27. function isInteger(s) {
  28. let re = /^[0-9]+$/;
  29. return re.test(s);
  30. }
  31. function writeHttpBody(response, body) {
  32. if (!body) {
  33. return;
  34. }
  35. response.bodyOutputStream.write(body, body.length);
  36. }
  37. function sendMozSvcError(request, response, code) {
  38. response.setStatusLine(request.httpVersion, 400, "Bad Request");
  39. response.setHeader("Content-Type", "text/plain", false);
  40. response.bodyOutputStream.write(code, code.length);
  41. }
  42. /**
  43. * Represent a BSO on the server.
  44. *
  45. * A BSO is constructed from an ID, content, and a modified time.
  46. *
  47. * @param id
  48. * (string) ID of the BSO being created.
  49. * @param payload
  50. * (strong|object) Payload for the BSO. Should ideally be a string. If
  51. * an object is passed, it will be fed into JSON.stringify and that
  52. * output will be set as the payload.
  53. * @param modified
  54. * (number) Milliseconds since UNIX epoch that the BSO was last
  55. * modified. If not defined or null, the current time will be used.
  56. */
  57. this.ServerBSO = function ServerBSO(id, payload, modified) {
  58. if (!id) {
  59. throw new Error("No ID for ServerBSO!");
  60. }
  61. if (!id.match(/^[a-zA-Z0-9_-]{1,64}$/)) {
  62. throw new Error("BSO ID is invalid: " + id);
  63. }
  64. this._log = Log.repository.getLogger(STORAGE_HTTP_LOGGER);
  65. this.id = id;
  66. if (!payload) {
  67. return;
  68. }
  69. CommonUtils.ensureMillisecondsTimestamp(modified);
  70. if (typeof payload == "object") {
  71. payload = JSON.stringify(payload);
  72. }
  73. this.payload = payload;
  74. this.modified = modified || new_timestamp();
  75. }
  76. ServerBSO.prototype = {
  77. FIELDS: [
  78. "id",
  79. "modified",
  80. "payload",
  81. "ttl",
  82. "sortindex",
  83. ],
  84. toJSON: function toJSON() {
  85. let obj = {};
  86. for each (let key in this.FIELDS) {
  87. if (this[key] !== undefined) {
  88. obj[key] = this[key];
  89. }
  90. }
  91. return obj;
  92. },
  93. delete: function delete_() {
  94. this.deleted = true;
  95. delete this.payload;
  96. delete this.modified;
  97. },
  98. /**
  99. * Handler for GET requests for this BSO.
  100. */
  101. getHandler: function getHandler(request, response) {
  102. let code = 200;
  103. let status = "OK";
  104. let body;
  105. function sendResponse() {
  106. response.setStatusLine(request.httpVersion, code, status);
  107. writeHttpBody(response, body);
  108. }
  109. if (request.hasHeader("x-if-modified-since")) {
  110. let headerModified = parseInt(request.getHeader("x-if-modified-since"),
  111. 10);
  112. CommonUtils.ensureMillisecondsTimestamp(headerModified);
  113. if (headerModified >= this.modified) {
  114. code = 304;
  115. status = "Not Modified";
  116. sendResponse();
  117. return;
  118. }
  119. } else if (request.hasHeader("x-if-unmodified-since")) {
  120. let requestModified = parseInt(request.getHeader("x-if-unmodified-since"),
  121. 10);
  122. let serverModified = this.modified;
  123. if (serverModified > requestModified) {
  124. code = 412;
  125. status = "Precondition Failed";
  126. sendResponse();
  127. return;
  128. }
  129. }
  130. if (!this.deleted) {
  131. body = JSON.stringify(this.toJSON());
  132. response.setHeader("Content-Type", "application/json", false);
  133. response.setHeader("X-Last-Modified", "" + this.modified, false);
  134. } else {
  135. code = 404;
  136. status = "Not Found";
  137. }
  138. sendResponse();
  139. },
  140. /**
  141. * Handler for PUT requests for this BSO.
  142. */
  143. putHandler: function putHandler(request, response) {
  144. if (request.hasHeader("Content-Type")) {
  145. let ct = request.getHeader("Content-Type");
  146. if (ct != "application/json") {
  147. throw HTTP_415;
  148. }
  149. }
  150. let input = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
  151. let parsed;
  152. try {
  153. parsed = JSON.parse(input);
  154. } catch (ex) {
  155. return sendMozSvcError(request, response, "8");
  156. }
  157. if (typeof(parsed) != "object") {
  158. return sendMozSvcError(request, response, "8");
  159. }
  160. // Don't update if a conditional request fails preconditions.
  161. if (request.hasHeader("x-if-unmodified-since")) {
  162. let reqModified = parseInt(request.getHeader("x-if-unmodified-since"));
  163. if (reqModified < this.modified) {
  164. response.setStatusLine(request.httpVersion, 412, "Precondition Failed");
  165. return;
  166. }
  167. }
  168. let code, status;
  169. if (this.payload) {
  170. code = 204;
  171. status = "No Content";
  172. } else {
  173. code = 201;
  174. status = "Created";
  175. }
  176. // Alert when we see unrecognized fields.
  177. for (let [key, value] in Iterator(parsed)) {
  178. switch (key) {
  179. case "payload":
  180. if (typeof(value) != "string") {
  181. sendMozSvcError(request, response, "8");
  182. return true;
  183. }
  184. this.payload = value;
  185. break;
  186. case "ttl":
  187. if (!isInteger(value)) {
  188. sendMozSvcError(request, response, "8");
  189. return true;
  190. }
  191. this.ttl = parseInt(value, 10);
  192. break;
  193. case "sortindex":
  194. if (!isInteger(value) || value.length > 9) {
  195. sendMozSvcError(request, response, "8");
  196. return true;
  197. }
  198. this.sortindex = parseInt(value, 10);
  199. break;
  200. case "id":
  201. break;
  202. default:
  203. this._log.warn("Unexpected field in BSO record: " + key);
  204. sendMozSvcError(request, response, "8");
  205. return true;
  206. }
  207. }
  208. this.modified = request.timestamp;
  209. this.deleted = false;
  210. response.setHeader("X-Last-Modified", "" + this.modified, false);
  211. response.setStatusLine(request.httpVersion, code, status);
  212. },
  213. };
  214. /**
  215. * Represent a collection on the server.
  216. *
  217. * The '_bsos' attribute is a mapping of id -> ServerBSO objects.
  218. *
  219. * Note that if you want these records to be accessible individually,
  220. * you need to register their handlers with the server separately, or use a
  221. * containing HTTP server that will do so on your behalf.
  222. *
  223. * @param bsos
  224. * An object mapping BSO IDs to ServerBSOs.
  225. * @param acceptNew
  226. * If true, POSTs to this collection URI will result in new BSOs being
  227. * created and wired in on the fly.
  228. * @param timestamp
  229. * An optional timestamp value to initialize the modified time of the
  230. * collection. This should be in the format returned by new_timestamp().
  231. */
  232. this.StorageServerCollection =
  233. function StorageServerCollection(bsos, acceptNew, timestamp=new_timestamp()) {
  234. this._bsos = bsos || {};
  235. this.acceptNew = acceptNew || false;
  236. /*
  237. * Track modified timestamp.
  238. * We can't just use the timestamps of contained BSOs: an empty collection
  239. * has a modified time.
  240. */
  241. CommonUtils.ensureMillisecondsTimestamp(timestamp);
  242. this._timestamp = timestamp;
  243. this._log = Log.repository.getLogger(STORAGE_HTTP_LOGGER);
  244. }
  245. StorageServerCollection.prototype = {
  246. BATCH_MAX_COUNT: 100, // # of records.
  247. BATCH_MAX_SIZE: 1024 * 1024, // # bytes.
  248. _timestamp: null,
  249. get timestamp() {
  250. return this._timestamp;
  251. },
  252. set timestamp(timestamp) {
  253. CommonUtils.ensureMillisecondsTimestamp(timestamp);
  254. this._timestamp = timestamp;
  255. },
  256. get totalPayloadSize() {
  257. let size = 0;
  258. for each (let bso in this.bsos()) {
  259. size += bso.payload.length;
  260. }
  261. return size;
  262. },
  263. /**
  264. * Convenience accessor for our BSO keys.
  265. * Excludes deleted items, of course.
  266. *
  267. * @param filter
  268. * A predicate function (applied to the ID and BSO) which dictates
  269. * whether to include the BSO's ID in the output.
  270. *
  271. * @return an array of IDs.
  272. */
  273. keys: function keys(filter) {
  274. return [id for ([id, bso] in Iterator(this._bsos))
  275. if (!bso.deleted && (!filter || filter(id, bso)))];
  276. },
  277. /**
  278. * Convenience method to get an array of BSOs.
  279. * Optionally provide a filter function.
  280. *
  281. * @param filter
  282. * A predicate function, applied to the BSO, which dictates whether to
  283. * include the BSO in the output.
  284. *
  285. * @return an array of ServerBSOs.
  286. */
  287. bsos: function bsos(filter) {
  288. let os = [bso for ([id, bso] in Iterator(this._bsos))
  289. if (!bso.deleted)];
  290. if (!filter) {
  291. return os;
  292. }
  293. return os.filter(filter);
  294. },
  295. /**
  296. * Obtain a BSO by ID.
  297. */
  298. bso: function bso(id) {
  299. return this._bsos[id];
  300. },
  301. /**
  302. * Obtain the payload of a specific BSO.
  303. *
  304. * Raises if the specified BSO does not exist.
  305. */
  306. payload: function payload(id) {
  307. return this.bso(id).payload;
  308. },
  309. /**
  310. * Insert the provided BSO under its ID.
  311. *
  312. * @return the provided BSO.
  313. */
  314. insertBSO: function insertBSO(bso) {
  315. return this._bsos[bso.id] = bso;
  316. },
  317. /**
  318. * Insert the provided payload as part of a new ServerBSO with the provided
  319. * ID.
  320. *
  321. * @param id
  322. * The GUID for the BSO.
  323. * @param payload
  324. * The payload, as provided to the ServerBSO constructor.
  325. * @param modified
  326. * An optional modified time for the ServerBSO. If not specified, the
  327. * current time will be used.
  328. *
  329. * @return the inserted BSO.
  330. */
  331. insert: function insert(id, payload, modified) {
  332. return this.insertBSO(new ServerBSO(id, payload, modified));
  333. },
  334. /**
  335. * Removes an object entirely from the collection.
  336. *
  337. * @param id
  338. * (string) ID to remove.
  339. */
  340. remove: function remove(id) {
  341. delete this._bsos[id];
  342. },
  343. _inResultSet: function _inResultSet(bso, options) {
  344. if (!bso.payload) {
  345. return false;
  346. }
  347. if (options.ids) {
  348. if (options.ids.indexOf(bso.id) == -1) {
  349. return false;
  350. }
  351. }
  352. if (options.newer) {
  353. if (bso.modified <= options.newer) {
  354. return false;
  355. }
  356. }
  357. if (options.older) {
  358. if (bso.modified >= options.older) {
  359. return false;
  360. }
  361. }
  362. return true;
  363. },
  364. count: function count(options) {
  365. options = options || {};
  366. let c = 0;
  367. for (let [id, bso] in Iterator(this._bsos)) {
  368. if (bso.modified && this._inResultSet(bso, options)) {
  369. c++;
  370. }
  371. }
  372. return c;
  373. },
  374. get: function get(options) {
  375. let data = [];
  376. for each (let bso in this._bsos) {
  377. if (!bso.modified) {
  378. continue;
  379. }
  380. if (!this._inResultSet(bso, options)) {
  381. continue;
  382. }
  383. data.push(bso);
  384. }
  385. if (options.sort) {
  386. if (options.sort == "oldest") {
  387. data.sort(function sortOldest(a, b) {
  388. if (a.modified == b.modified) {
  389. return 0;
  390. }
  391. return a.modified < b.modified ? -1 : 1;
  392. });
  393. } else if (options.sort == "newest") {
  394. data.sort(function sortNewest(a, b) {
  395. if (a.modified == b.modified) {
  396. return 0;
  397. }
  398. return a.modified > b.modified ? -1 : 1;
  399. });
  400. } else if (options.sort == "index") {
  401. data.sort(function sortIndex(a, b) {
  402. if (a.sortindex == b.sortindex) {
  403. return 0;
  404. }
  405. if (a.sortindex !== undefined && b.sortindex == undefined) {
  406. return 1;
  407. }
  408. if (a.sortindex === undefined && b.sortindex !== undefined) {
  409. return -1;
  410. }
  411. return a.sortindex > b.sortindex ? -1 : 1;
  412. });
  413. }
  414. }
  415. if (options.limit) {
  416. data = data.slice(0, options.limit);
  417. }
  418. return data;
  419. },
  420. post: function post(input, timestamp) {
  421. let success = [];
  422. let failed = {};
  423. let count = 0;
  424. let size = 0;
  425. // This will count records where we have an existing ServerBSO
  426. // registered with us as successful and all other records as failed.
  427. for each (let record in input) {
  428. count += 1;
  429. if (count > this.BATCH_MAX_COUNT) {
  430. failed[record.id] = "Max record count exceeded.";
  431. continue;
  432. }
  433. if (typeof(record.payload) != "string") {
  434. failed[record.id] = "Payload is not a string!";
  435. continue;
  436. }
  437. size += record.payload.length;
  438. if (size > this.BATCH_MAX_SIZE) {
  439. failed[record.id] = "Payload max size exceeded!";
  440. continue;
  441. }
  442. if (record.sortindex) {
  443. if (!isInteger(record.sortindex)) {
  444. failed[record.id] = "sortindex is not an integer.";
  445. continue;
  446. }
  447. if (record.sortindex.length > 9) {
  448. failed[record.id] = "sortindex is too long.";
  449. continue;
  450. }
  451. }
  452. if ("ttl" in record) {
  453. if (!isInteger(record.ttl)) {
  454. failed[record.id] = "ttl is not an integer.";
  455. continue;
  456. }
  457. }
  458. try {
  459. let bso = this.bso(record.id);
  460. if (!bso && this.acceptNew) {
  461. this._log.debug("Creating BSO " + JSON.stringify(record.id) +
  462. " on the fly.");
  463. bso = new ServerBSO(record.id);
  464. this.insertBSO(bso);
  465. }
  466. if (bso) {
  467. bso.payload = record.payload;
  468. bso.modified = timestamp;
  469. bso.deleted = false;
  470. success.push(record.id);
  471. if (record.sortindex) {
  472. bso.sortindex = parseInt(record.sortindex, 10);
  473. }
  474. } else {
  475. failed[record.id] = "no bso configured";
  476. }
  477. } catch (ex) {
  478. this._log.info("Exception when processing BSO: " +
  479. CommonUtils.exceptionStr(ex));
  480. failed[record.id] = "Exception when processing.";
  481. }
  482. }
  483. return {success: success, failed: failed};
  484. },
  485. delete: function delete_(options) {
  486. options = options || {};
  487. // Protocol 2.0 only allows the "ids" query string argument.
  488. let keys = Object.keys(options).filter(function(k) {
  489. return k != "ids";
  490. });
  491. if (keys.length) {
  492. this._log.warn("Invalid query string parameter to collection delete: " +
  493. keys.join(", "));
  494. throw new Error("Malformed client request.");
  495. }
  496. if (options.ids && options.ids.length > this.BATCH_MAX_COUNT) {
  497. throw HTTP_400;
  498. }
  499. let deleted = [];
  500. for (let [id, bso] in Iterator(this._bsos)) {
  501. if (this._inResultSet(bso, options)) {
  502. this._log.debug("Deleting " + JSON.stringify(bso));
  503. deleted.push(bso.id);
  504. bso.delete();
  505. }
  506. }
  507. return deleted;
  508. },
  509. parseOptions: function parseOptions(request) {
  510. let options = {};
  511. for each (let chunk in request.queryString.split("&")) {
  512. if (!chunk) {
  513. continue;
  514. }
  515. chunk = chunk.split("=");
  516. let key = decodeURIComponent(chunk[0]);
  517. if (chunk.length == 1) {
  518. options[key] = "";
  519. } else {
  520. options[key] = decodeURIComponent(chunk[1]);
  521. }
  522. }
  523. if (options.ids) {
  524. options.ids = options.ids.split(",");
  525. }
  526. if (options.newer) {
  527. if (!isInteger(options.newer)) {
  528. throw HTTP_400;
  529. }
  530. CommonUtils.ensureMillisecondsTimestamp(options.newer);
  531. options.newer = parseInt(options.newer, 10);
  532. }
  533. if (options.older) {
  534. if (!isInteger(options.older)) {
  535. throw HTTP_400;
  536. }
  537. CommonUtils.ensureMillisecondsTimestamp(options.older);
  538. options.older = parseInt(options.older, 10);
  539. }
  540. if (options.limit) {
  541. if (!isInteger(options.limit)) {
  542. throw HTTP_400;
  543. }
  544. options.limit = parseInt(options.limit, 10);
  545. }
  546. return options;
  547. },
  548. getHandler: function getHandler(request, response) {
  549. let options = this.parseOptions(request);
  550. let data = this.get(options);
  551. if (request.hasHeader("x-if-modified-since")) {
  552. let requestModified = parseInt(request.getHeader("x-if-modified-since"),
  553. 10);
  554. let newestBSO = 0;
  555. for each (let bso in data) {
  556. if (bso.modified > newestBSO) {
  557. newestBSO = bso.modified;
  558. }
  559. }
  560. if (requestModified >= newestBSO) {
  561. response.setHeader("X-Last-Modified", "" + newestBSO);
  562. response.setStatusLine(request.httpVersion, 304, "Not Modified");
  563. return;
  564. }
  565. } else if (request.hasHeader("x-if-unmodified-since")) {
  566. let requestModified = parseInt(request.getHeader("x-if-unmodified-since"),
  567. 10);
  568. let serverModified = this.timestamp;
  569. if (serverModified > requestModified) {
  570. response.setHeader("X-Last-Modified", "" + serverModified);
  571. response.setStatusLine(request.httpVersion, 412, "Precondition Failed");
  572. return;
  573. }
  574. }
  575. if (options.full) {
  576. data = data.map(function map(bso) {
  577. return bso.toJSON();
  578. });
  579. } else {
  580. data = data.map(function map(bso) {
  581. return bso.id;
  582. });
  583. }
  584. // application/json is default media type.
  585. let newlines = false;
  586. if (request.hasHeader("accept")) {
  587. let accept = request.getHeader("accept");
  588. if (accept == "application/newlines") {
  589. newlines = true;
  590. } else if (accept != "application/json") {
  591. throw HTTP_406;
  592. }
  593. }
  594. let body;
  595. if (newlines) {
  596. response.setHeader("Content-Type", "application/newlines", false);
  597. let normalized = data.map(function map(d) {
  598. return JSON.stringify(d);
  599. });
  600. body = normalized.join("\n") + "\n";
  601. } else {
  602. response.setHeader("Content-Type", "application/json", false);
  603. body = JSON.stringify({items: data});
  604. }
  605. this._log.info("Records: " + data.length);
  606. response.setHeader("X-Num-Records", "" + data.length, false);
  607. response.setHeader("X-Last-Modified", "" + this.timestamp, false);
  608. response.setStatusLine(request.httpVersion, 200, "OK");
  609. response.bodyOutputStream.write(body, body.length);
  610. },
  611. postHandler: function postHandler(request, response) {
  612. let options = this.parseOptions(request);
  613. if (!request.hasHeader("content-type")) {
  614. this._log.info("No Content-Type request header!");
  615. throw HTTP_400;
  616. }
  617. let inputStream = request.bodyInputStream;
  618. let inputBody = CommonUtils.readBytesFromInputStream(inputStream);
  619. let input = [];
  620. let inputMediaType = request.getHeader("content-type");
  621. if (inputMediaType == "application/json") {
  622. try {
  623. input = JSON.parse(inputBody);
  624. } catch (ex) {
  625. this._log.info("JSON parse error on input body!");
  626. throw HTTP_400;
  627. }
  628. if (!Array.isArray(input)) {
  629. this._log.info("Input JSON type not an array!");
  630. return sendMozSvcError(request, response, "8");
  631. }
  632. } else if (inputMediaType == "application/newlines") {
  633. for each (let line in inputBody.split("\n")) {
  634. let record;
  635. try {
  636. record = JSON.parse(line);
  637. } catch (ex) {
  638. this._log.info("JSON parse error on line!");
  639. return sendMozSvcError(request, response, "8");
  640. }
  641. input.push(record);
  642. }
  643. } else {
  644. this._log.info("Unknown media type: " + inputMediaType);
  645. throw HTTP_415;
  646. }
  647. if (this._ensureUnmodifiedSince(request, response)) {
  648. return;
  649. }
  650. let res = this.post(input, request.timestamp);
  651. let body = JSON.stringify(res);
  652. response.setHeader("Content-Type", "application/json", false);
  653. this.timestamp = request.timestamp;
  654. response.setHeader("X-Last-Modified", "" + this.timestamp, false);
  655. response.setStatusLine(request.httpVersion, "200", "OK");
  656. response.bodyOutputStream.write(body, body.length);
  657. },
  658. deleteHandler: function deleteHandler(request, response) {
  659. this._log.debug("Invoking StorageServerCollection.DELETE.");
  660. let options = this.parseOptions(request);
  661. if (this._ensureUnmodifiedSince(request, response)) {
  662. return;
  663. }
  664. let deleted = this.delete(options);
  665. response.deleted = deleted;
  666. this.timestamp = request.timestamp;
  667. response.setStatusLine(request.httpVersion, 204, "No Content");
  668. },
  669. handler: function handler() {
  670. let self = this;
  671. return function(request, response) {
  672. switch(request.method) {
  673. case "GET":
  674. return self.getHandler(request, response);
  675. case "POST":
  676. return self.postHandler(request, response);
  677. case "DELETE":
  678. return self.deleteHandler(request, response);
  679. }
  680. request.setHeader("Allow", "GET,POST,DELETE");
  681. response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
  682. };
  683. },
  684. _ensureUnmodifiedSince: function _ensureUnmodifiedSince(request, response) {
  685. if (!request.hasHeader("x-if-unmodified-since")) {
  686. return false;
  687. }
  688. let requestModified = parseInt(request.getHeader("x-if-unmodified-since"),
  689. 10);
  690. let serverModified = this.timestamp;
  691. this._log.debug("Request modified time: " + requestModified +
  692. "; Server modified time: " + serverModified);
  693. if (serverModified <= requestModified) {
  694. return false;
  695. }
  696. this._log.info("Conditional request rejected because client time older " +
  697. "than collection timestamp.");
  698. response.setStatusLine(request.httpVersion, 412, "Precondition Failed");
  699. return true;
  700. },
  701. };
  702. //===========================================================================//
  703. // httpd.js-based Storage server. //
  704. //===========================================================================//
  705. /**
  706. * In general, the preferred way of using StorageServer is to directly
  707. * introspect it. Callbacks are available for operations which are hard to
  708. * verify through introspection, such as deletions.
  709. *
  710. * One of the goals of this server is to provide enough hooks for test code to
  711. * find out what it needs without monkeypatching. Use this object as your
  712. * prototype, and override as appropriate.
  713. */
  714. this.StorageServerCallback = {
  715. onCollectionDeleted: function onCollectionDeleted(user, collection) {},
  716. onItemDeleted: function onItemDeleted(user, collection, bsoID) {},
  717. /**
  718. * Called at the top of every request.
  719. *
  720. * Allows the test to inspect the request. Hooks should be careful not to
  721. * modify or change state of the request or they may impact future processing.
  722. */
  723. onRequest: function onRequest(request) {},
  724. };
  725. /**
  726. * Construct a new test Storage server. Takes a callback object (e.g.,
  727. * StorageServerCallback) as input.
  728. */
  729. this.StorageServer = function StorageServer(callback) {
  730. this.callback = callback || {__proto__: StorageServerCallback};
  731. this.server = new HttpServer();
  732. this.started = false;
  733. this.users = {};
  734. this.requestCount = 0;
  735. this._log = Log.repository.getLogger(STORAGE_HTTP_LOGGER);
  736. // Install our own default handler. This allows us to mess around with the
  737. // whole URL space.
  738. let handler = this.server._handler;
  739. handler._handleDefault = this.handleDefault.bind(this, handler);
  740. }
  741. StorageServer.prototype = {
  742. DEFAULT_QUOTA: 1024 * 1024, // # bytes.
  743. server: null, // HttpServer.
  744. users: null, // Map of username => {collections, password}.
  745. /**
  746. * If true, the server will allow any arbitrary user to be used.
  747. *
  748. * No authentication will be performed. Whatever user is detected from the
  749. * URL or auth headers will be created (if needed) and used.
  750. */
  751. allowAllUsers: false,
  752. /**
  753. * Start the StorageServer's underlying HTTP server.
  754. *
  755. * @param port
  756. * The numeric port on which to start. A falsy value implies to
  757. * select any available port.
  758. * @param cb
  759. * A callback function (of no arguments) which is invoked after
  760. * startup.
  761. */
  762. start: function start(port, cb) {
  763. if (this.started) {
  764. this._log.warn("Warning: server already started on " + this.port);
  765. return;
  766. }
  767. if (!port) {
  768. port = -1;
  769. }
  770. this.port = port;
  771. try {
  772. this.server.start(this.port);
  773. this.port = this.server.identity.primaryPort;
  774. this.started = true;
  775. if (cb) {
  776. cb();
  777. }
  778. } catch (ex) {
  779. _("==========================================");
  780. _("Got exception starting Storage HTTP server on port " + this.port);
  781. _("Error: " + CommonUtils.exceptionStr(ex));
  782. _("Is there a process already listening on port " + this.port + "?");
  783. _("==========================================");
  784. do_throw(ex);
  785. }
  786. },
  787. /**
  788. * Start the server synchronously.
  789. *
  790. * @param port
  791. * The numeric port on which to start. The default is to choose
  792. * any available port.
  793. */
  794. startSynchronous: function startSynchronous(port=-1) {
  795. let cb = Async.makeSpinningCallback();
  796. this.start(port, cb);
  797. cb.wait();
  798. },
  799. /**
  800. * Stop the StorageServer's HTTP server.
  801. *
  802. * @param cb
  803. * A callback function. Invoked after the server has been stopped.
  804. *
  805. */
  806. stop: function stop(cb) {
  807. if (!this.started) {
  808. this._log.warn("StorageServer: Warning: server not running. Can't stop " +
  809. "me now!");
  810. return;
  811. }
  812. this.server.stop(cb);
  813. this.started = false;
  814. },
  815. serverTime: function serverTime() {
  816. return new_timestamp();
  817. },
  818. /**
  819. * Create a new user, complete with an empty set of collections.
  820. *
  821. * @param username
  822. * The username to use. An Error will be thrown if a user by that name
  823. * already exists.
  824. * @param password
  825. * A password string.
  826. *
  827. * @return a user object, as would be returned by server.user(username).
  828. */
  829. registerUser: function registerUser(username, password) {
  830. if (username in this.users) {
  831. throw new Error("User already exists.");
  832. }
  833. if (!isFinite(parseInt(username))) {
  834. throw new Error("Usernames must be numeric: " + username);
  835. }
  836. this._log.info("Registering new user with server: " + username);
  837. this.users[username] = {
  838. password: password,
  839. collections: {},
  840. quota: this.DEFAULT_QUOTA,
  841. };
  842. return this.user(username);
  843. },
  844. userExists: function userExists(username) {
  845. return username in this.users;
  846. },
  847. getCollection: function getCollection(username, collection) {
  848. return this.users[username].collections[collection];
  849. },
  850. _insertCollection: function _insertCollection(collections, collection, bsos) {
  851. let coll = new StorageServerCollection(bsos, true);
  852. coll.collectionHandler = coll.handler();
  853. collections[collection] = coll;
  854. return coll;
  855. },
  856. createCollection: function createCollection(username, collection, bsos) {
  857. if (!(username in this.users)) {
  858. throw new Error("Unknown user.");
  859. }
  860. let collections = this.users[username].collections;
  861. if (collection in collections) {
  862. throw new Error("Collection already exists.");
  863. }
  864. return this._insertCollection(collections, collection, bsos);
  865. },
  866. deleteCollection: function deleteCollection(username, collection) {
  867. if (!(username in this.users)) {
  868. throw new Error("Unknown user.");
  869. }
  870. delete this.users[username].collections[collection];
  871. },
  872. /**
  873. * Accept a map like the following:
  874. * {
  875. * meta: {global: {version: 1, ...}},
  876. * crypto: {"keys": {}, foo: {bar: 2}},
  877. * bookmarks: {}
  878. * }
  879. * to cause collections and BSOs to be created.
  880. * If a collection already exists, no error is raised.
  881. * If a BSO already exists, it will be updated to the new contents.
  882. */
  883. createContents: function createContents(username, collections) {
  884. if (!(username in this.users)) {
  885. throw new Error("Unknown user.");
  886. }
  887. let userCollections = this.users[username].collections;
  888. for (let [id, contents] in Iterator(collections)) {
  889. let coll = userCollections[id] ||
  890. this._insertCollection(userCollections, id);
  891. for (let [bsoID, payload] in Iterator(contents)) {
  892. coll.insert(bsoID, payload);
  893. }
  894. }
  895. },
  896. /**
  897. * Insert a BSO in an existing collection.
  898. */
  899. insertBSO: function insertBSO(username, collection, bso) {
  900. if (!(username in this.users)) {
  901. throw new Error("Unknown user.");
  902. }
  903. let userCollections = this.users[username].collections;
  904. if (!(collection in userCollections)) {
  905. throw new Error("Unknown collection.");
  906. }
  907. userCollections[collection].insertBSO(bso);
  908. return bso;
  909. },
  910. /**
  911. * Delete all of the collections for the named user.
  912. *
  913. * @param username
  914. * The name of the affected user.
  915. */
  916. deleteCollections: function deleteCollections(username) {
  917. if (!(username in this.users)) {
  918. throw new Error("Unknown user.");
  919. }
  920. let userCollections = this.users[username].collections;
  921. for each (let [name, coll] in Iterator(userCollections)) {
  922. this._log.trace("Bulk deleting " + name + " for " + username + "...");
  923. coll.delete({});
  924. }
  925. this.users[username].collections = {};
  926. },
  927. getQuota: function getQuota(username) {
  928. if (!(username in this.users)) {
  929. throw new Error("Unknown user.");
  930. }
  931. return this.users[username].quota;
  932. },
  933. /**
  934. * Obtain the newest timestamp of all collections for a user.
  935. */
  936. newestCollectionTimestamp: function newestCollectionTimestamp(username) {
  937. let collections = this.users[username].collections;
  938. let newest = 0;
  939. for each (let collection in collections) {
  940. if (collection.timestamp > newest) {
  941. newest = collection.timestamp;
  942. }
  943. }
  944. return newest;
  945. },
  946. /**
  947. * Compute the object that is returned for an info/collections request.
  948. */
  949. infoCollections: function infoCollections(username) {
  950. let responseObject = {};
  951. let colls = this.users[username].collections;
  952. for (let coll in colls) {
  953. responseObject[coll] = colls[coll].timestamp;
  954. }
  955. this._log.trace("StorageServer: info/collections returning " +
  956. JSON.stringify(responseObject));
  957. return responseObject;
  958. },
  959. infoCounts: function infoCounts(username) {
  960. let data = {};
  961. let collections = this.users[username].collections;
  962. for (let [k, v] in Iterator(collections)) {
  963. let count = v.count();
  964. if (!count) {
  965. continue;
  966. }
  967. data[k] = count;
  968. }
  969. return data;
  970. },
  971. infoUsage: function infoUsage(username) {
  972. let data = {};
  973. let collections = this.users[username].collections;
  974. for (let [k, v] in Iterator(collections)) {
  975. data[k] = v.totalPayloadSize;
  976. }
  977. return data;
  978. },
  979. infoQuota: function infoQuota(username) {
  980. let total = 0;
  981. for each (let value in this.infoUsage(username)) {
  982. total += value;
  983. }
  984. return {
  985. quota: this.getQuota(username),
  986. usage: total
  987. };
  988. },
  989. /**
  990. * Simple accessor to allow collective binding and abbreviation of a bunch of
  991. * methods. Yay!
  992. * Use like this:
  993. *
  994. * let u = server.user("john");
  995. * u.collection("bookmarks").bso("abcdefg").payload; // Etc.
  996. *
  997. * @return a proxy for the user data stored in this server.
  998. */
  999. user: function user(username) {
  1000. let collection = this.getCollection.bind(this, username);
  1001. let createCollection = this.createCollection.bind(this, username);
  1002. let createContents = this.createContents.bind(this, username);
  1003. let modified = function (collectionName) {
  1004. return collection(collectionName).timestamp;
  1005. }
  1006. let deleteCollections = this.deleteCollections.bind(this, username);
  1007. let quota = this.getQuota.bind(this, username);
  1008. return {
  1009. collection: collection,
  1010. createCollection: createCollection,
  1011. createContents: createContents,
  1012. deleteCollections: deleteCollections,
  1013. modified: modified,
  1014. quota: quota,
  1015. };
  1016. },
  1017. _pruneExpired: function _pruneExpired() {
  1018. let now = Date.now();
  1019. for each (let user in this.users) {
  1020. for each (let collection in user.collections) {
  1021. for each (let bso in collection.bsos()) {
  1022. // ttl === 0 is a special case, so we can't simply !ttl.
  1023. if (typeof(bso.ttl) != "number") {
  1024. continue;
  1025. }
  1026. let ttlDate = bso.modified + (bso.ttl * 1000);
  1027. if (ttlDate < now) {
  1028. this._log.info("Deleting BSO because TTL expired: " + bso.id);
  1029. bso.delete();
  1030. }
  1031. }
  1032. }
  1033. }
  1034. },
  1035. /*
  1036. * Regular expressions for splitting up Storage request paths.
  1037. * Storage URLs are of the form:
  1038. * /$apipath/$version/$userid/$further
  1039. * where $further is usually:
  1040. * storage/$collection/$bso
  1041. * or
  1042. * storage/$collection
  1043. * or
  1044. * info/$op
  1045. *
  1046. * We assume for the sake of simplicity that $apipath is empty.
  1047. *
  1048. * N.B., we don't follow any kind of username spec here, because as far as I
  1049. * can tell there isn't one. See Bug 689671. Instead we follow the Python
  1050. * server code.
  1051. *
  1052. * Path: [all, version, first, rest]
  1053. * Storage: [all, collection?, id?]
  1054. */
  1055. pathRE: /^\/([0-9]+(?:\.[0-9]+)?)(?:\/([0-9]+)\/([^\/]+)(?:\/(.+))?)?$/,
  1056. storageRE: /^([-_a-zA-Z0-9]+)(?:\/([-_a-zA-Z0-9]+)\/?)?$/,
  1057. defaultHeaders: {},
  1058. /**
  1059. * HTTP response utility.
  1060. */
  1061. respond: function respond(req, resp, code, status, body, headers, timestamp) {
  1062. this._log.info("Response: " + code + " " + status);
  1063. resp.setStatusLine(req.httpVersion, code, status);
  1064. for each (let [header, value] in Iterator(headers || this.defaultHeaders)) {
  1065. resp.setHeader(header, value, false);
  1066. }
  1067. if (timestamp) {
  1068. resp.setHeader("X-Timestamp", "" + timestamp, false);
  1069. }
  1070. if (body) {
  1071. resp.bodyOutputStream.write(body, body.length);
  1072. }
  1073. },
  1074. /**
  1075. * This is invoked by the HttpServer. `this` is bound to the StorageServer;
  1076. * `handler` is the HttpServer's handler.
  1077. *
  1078. * TODO: need to use the correct Storage API response codes and errors here.
  1079. */
  1080. handleDefault: function handleDefault(handler, req, resp) {
  1081. this.requestCount++;
  1082. let timestamp = new_timestamp();
  1083. try {
  1084. this._handleDefault(handler, req, resp, timestamp);
  1085. } catch (e) {
  1086. if (e instanceof HttpError) {
  1087. this.respond(req, resp, e.code, e.description, "", {}, timestamp);
  1088. } else {
  1089. this._log.warn(CommonUtils.exceptionStr(e));
  1090. throw e;
  1091. }
  1092. }
  1093. },
  1094. _handleDefault: function _handleDefault(handler, req, resp, timestamp) {
  1095. let path = req.path;
  1096. if (req.queryString.length) {
  1097. path += "?" + req.queryString;
  1098. }
  1099. this._log.debug("StorageServer: Handling request: " + req.method + " " +
  1100. path);
  1101. if (this.callback.onRequest) {
  1102. this.callback.onRequest(req);
  1103. }
  1104. // Prune expired records for all users at top of request. This is the
  1105. // easiest way to process TTLs since all requests go through here.
  1106. this._pruneExpired();
  1107. req.timestamp = timestamp;
  1108. resp.setHeader("X-Timestamp", "" + timestamp, false);
  1109. let parts = this.pathRE.exec(req.path);
  1110. if (!parts) {
  1111. this._log.debug("StorageServer: Unexpected request: bad URL " + req.path);
  1112. throw HTTP_404;
  1113. }
  1114. let [all, version, userPath, first, rest] = parts;
  1115. if (version != STORAGE_API_VERSION) {
  1116. this._log.debug("StorageServer: Unknown version.");
  1117. throw HTTP_404;
  1118. }
  1119. let username;
  1120. // By default, the server requires users to be authenticated. When a
  1121. // request arrives, the user must have been previously configured and
  1122. // the request must have authentication. In "allow all users" mode, we
  1123. // take the username from the URL, create the user on the fly, and don't
  1124. // perform any authentication.
  1125. if (!this.allowAllUsers) {
  1126. // Enforce authentication.
  1127. if (!req.hasHeader("authorization")) {
  1128. this.respond(req, resp, 401, "Authorization Required", "{}", {
  1129. "WWW-Authenticate": 'Basic realm="secret"'
  1130. });
  1131. return;
  1132. }
  1133. let ensureUserExists = function ensureUserExists(username) {
  1134. if (this.userExists(username)) {
  1135. return;
  1136. }
  1137. this._log.info("StorageServer: Unknown user: " + username);
  1138. throw HTTP_401;
  1139. }.bind(this);
  1140. let auth = req.getHeader("authorization");
  1141. this._log.debug("Authorization: " + auth);
  1142. if (auth.indexOf("Basic ") == 0) {
  1143. let decoded = CommonUtils.safeAtoB(auth.substr(6));
  1144. this._log.debug("Decoded Basic Auth: " + decoded);
  1145. let [user, password] = decoded.split(":", 2);
  1146. if (!password) {
  1147. this._log.debug("Malformed HTTP Basic Authorization header: " + auth);
  1148. throw HTTP_400;
  1149. }
  1150. this._log.debug("Got HTTP Basic auth for user: " + user);
  1151. ensureUserExists(user);
  1152. username = user;
  1153. if (this.users[user].password != password) {
  1154. this._log.debug("StorageServer: Provided password is not correct.");
  1155. throw HTTP_401;
  1156. }
  1157. // TODO support token auth.
  1158. } else {
  1159. this._log.debug("Unsupported HTTP authorization type: " + auth);
  1160. throw HTTP_500;
  1161. }
  1162. // All users mode.
  1163. } else {
  1164. // Auto create user with dummy password.
  1165. if (!this.userExists(userPath)) {
  1166. this.registerUser(userPath, "DUMMY-PASSWORD-*&%#");
  1167. }
  1168. username = userPath;
  1169. }
  1170. // Hand off to the appropriate handler for this path component.
  1171. if (first in this.toplevelHandlers) {
  1172. let handler = this.toplevelHandlers[first];
  1173. try {
  1174. return handler.call(this, handler, req, resp, version, username, rest);
  1175. } catch (ex) {
  1176. this._log.warn("Got exception during request: " +
  1177. CommonUtils.exceptionStr(ex));
  1178. throw ex;
  1179. }
  1180. }
  1181. this._log.debug("StorageServer: Unknown top-level " + first);
  1182. throw HTTP_404;
  1183. },
  1184. /**
  1185. * Collection of the handler methods we use for top-level path components.
  1186. */
  1187. toplevelHandlers: {
  1188. "storage": function handleStorage(handler, req, resp, version, username,
  1189. rest) {
  1190. let respond = this.respond.bind(this, req, resp);
  1191. if (!rest || !rest.length) {
  1192. this._log.debug("StorageServer: top-level storage " +
  1193. req.method + " request.");
  1194. if (req.method != "DELETE") {
  1195. respond(405, "Method Not Allowed", null, {"Allow": "DELETE"});
  1196. return;
  1197. }
  1198. this.user(username).deleteCollections();
  1199. respond(204, "No Content");
  1200. return;
  1201. }
  1202. let match = this.storageRE.exec(rest);
  1203. if (!match) {
  1204. this._log.warn("StorageServer: Unknown storage operation " + rest);
  1205. throw HTTP_404;
  1206. }
  1207. let [all, collection, bsoID] = match;
  1208. let coll = this.getCollection(username, collection);
  1209. let collectionExisted = !!coll;
  1210. switch (req.method) {
  1211. case "GET":
  1212. // Tried to GET on a collection that doesn't exist.
  1213. if (!coll) {
  1214. respond(404, "Not Found");
  1215. return;
  1216. }
  1217. // No BSO URL parameter goes to collection handler.
  1218. if (!bsoID) {
  1219. return coll.collectionHandler(req, resp);
  1220. }
  1221. // Handle non-existent BSO.
  1222. let bso = coll.bso(bsoID);
  1223. if (!bso) {
  1224. respond(404, "Not Found");
  1225. return;
  1226. }
  1227. // Proxy to BSO handler.
  1228. return bso.getHandler(req, resp);
  1229. case "DELETE":
  1230. // Collection doesn't exist.
  1231. if (!coll) {
  1232. respond(404, "Not Found");
  1233. return;
  1234. }
  1235. // Deleting a specific BSO.
  1236. if (bsoID) {
  1237. let bso = coll.bso(bsoID);
  1238. // BSO does not exist on the server. Nothing to do.
  1239. if (!bso) {
  1240. respond(404, "Not Found");
  1241. return;
  1242. }
  1243. if (req.hasHeader("x-if-unmodified-since")) {
  1244. let modified = parseInt(req.getHeader("x-if-unmodified-since"));
  1245. CommonUtils.ensureMillisecondsTimestamp(modified);
  1246. if (bso.modified > modified) {
  1247. respond(412, "Precondition Failed");
  1248. return;
  1249. }
  1250. }
  1251. bso.delete();
  1252. coll.timestamp = req.timestamp;
  1253. this.callback.onItemDeleted(username, collection, bsoID);
  1254. respond(204, "No Content");
  1255. return;
  1256. }
  1257. // Proxy to collection handler.
  1258. coll.collectionHandler(req, resp);
  1259. // Spot if this is a DELETE for some IDs, and don't blow away the
  1260. // whole collection!
  1261. //
  1262. // We already handled deleting the BSOs by invoking the deleted
  1263. // collection's handler. However, in the case of
  1264. //
  1265. // DELETE storage/foobar
  1266. //
  1267. // we also need to remove foobar from the collections map. This
  1268. // clause tries to differentiate the above request from
  1269. //
  1270. // DELETE storage/foobar?ids=foo,baz
  1271. //
  1272. // and do the right thing.
  1273. // TODO: less hacky method.
  1274. if (-1 == req.queryString.indexOf("ids=")) {
  1275. // When you delete the entire collection, we drop it.
  1276. this._log.debug("Deleting entire collection.");
  1277. delete this.users[username].collections[collection];
  1278. this.callback.onCollectionDeleted(username, collection);
  1279. }
  1280. // Notify of item deletion.
  1281. let deleted = resp.deleted || [];
  1282. for (let i = 0; i < deleted.length; ++i) {
  1283. this.callback.onItemDeleted(username, collection, deleted[i]);
  1284. }
  1285. return;
  1286. case "POST":
  1287. case "PUT":
  1288. // Auto-create collection if it doesn't exist.
  1289. if (!coll) {
  1290. coll = this.createCollection(username, collection);
  1291. }
  1292. try {
  1293. if (bsoID) {
  1294. let bso = coll.bso(bsoID);
  1295. if (!bso) {
  1296. this._log.trace("StorageServer: creating BSO " + collection +
  1297. "/" + bsoID);
  1298. try {
  1299. bso = coll.insert(bsoID);
  1300. } catch (ex) {
  1301. return sendMozSvcError(req, resp, "8");
  1302. }
  1303. }
  1304. bso.putHandler(req, resp);
  1305. coll.timestamp = req.timestamp;
  1306. return resp;
  1307. }
  1308. return coll.collectionHandler(req, resp);
  1309. } catch (ex) {
  1310. if (ex instanceof HttpError) {
  1311. if (!collectionExisted) {
  1312. this.deleteCollection(username, collection);
  1313. }
  1314. }
  1315. throw ex;
  1316. }
  1317. default:
  1318. throw new Error("Request method " + req.method + " not implemented.");
  1319. }
  1320. },
  1321. "info": function handleInfo(handler, req, resp, version, username, rest) {
  1322. switch (rest) {
  1323. case "collections":
  1324. return this.handleInfoCollections(req, resp, username);
  1325. case "collection_counts":
  1326. return this.handleInfoCounts(req, resp, username);
  1327. case "collection_usage":
  1328. return this.handleInfoUsage(req, resp, username);
  1329. case "quota":
  1330. return this.handleInfoQuota(req, resp, username);
  1331. default:
  1332. this._log.warn("StorageServer: Unknown info operation " + rest);
  1333. throw HTTP_404;
  1334. }
  1335. }
  1336. },
  1337. handleInfoConditional: function handleInfoConditional(request, response,
  1338. user) {
  1339. if (!request.hasHeader("x-if-modified-since")) {
  1340. return false;
  1341. }
  1342. let requestModified = request.getHeader("x-if-modified-since");
  1343. requestModified = parseInt(requestModified, 10);
  1344. let serverModified = this.newestCollectionTimestamp(user);
  1345. this._log.info("Server mtime: " + serverModified + "; Client modified: " +
  1346. requestModified);
  1347. if (serverModified > requestModified) {
  1348. return false;
  1349. }
  1350. this.respond(request, response, 304, "Not Modified", null, {
  1351. "X-Last-Modified": "" + serverModified
  1352. });
  1353. return true;
  1354. },
  1355. handleInfoCollections: function handleInfoCollections(request, response,
  1356. user) {
  1357. if (this.handleInfoConditional(request, response, user)) {
  1358. return;
  1359. }
  1360. let info = this.infoCollections(user);
  1361. let body = JSON.stringify(info);
  1362. this.respond(request, response, 200, "OK", body, {
  1363. "Content-Type": "application/json",
  1364. "X-Last-Modified": "" + this.newestCollectionTimestamp(user),
  1365. });
  1366. },
  1367. handleInfoCounts: function handleInfoCounts(request, response, user) {
  1368. if (this.handleInfoConditional(request, response, user)) {
  1369. return;
  1370. }
  1371. let counts = this.infoCounts(user);
  1372. let body = JSON.stringify(counts);
  1373. this.respond(request, response, 200, "OK", body, {
  1374. "Content-Type": "application/json",
  1375. "X-Last-Modified": "" + this.newestCollectionTimestamp(user),
  1376. });
  1377. },
  1378. handleInfoUsage: function handleInfoUsage(request, response, user) {
  1379. if (this.handleInfoConditional(request, response, user)) {
  1380. return;
  1381. }
  1382. let body = JSON.stringify(this.infoUsage(user));
  1383. this.respond(request, response, 200, "OK", body, {
  1384. "Content-Type": "application/json",
  1385. "X-Last-Modified": "" + this.newestCollectionTimestamp(user),
  1386. });
  1387. },
  1388. handleInfoQuota: function handleInfoQuota(request, response, user) {
  1389. if (this.handleInfoConditional(request, response, user)) {
  1390. return;
  1391. }
  1392. let body = JSON.stringify(this.infoQuota(user));
  1393. this.respond(request, response, 200, "OK", body, {
  1394. "Content-Type": "application/json",
  1395. "X-Last-Modified": "" + this.newestCollectionTimestamp(user),
  1396. });
  1397. },
  1398. };
  1399. /**
  1400. * Helper to create a storage server for a set of users.
  1401. *
  1402. * Each user is specified by a map of username to password.
  1403. */
  1404. this.storageServerForUsers =
  1405. function storageServerForUsers(users, contents, callback) {
  1406. let server = new StorageServer(callback);
  1407. for (let [user, pass] in Iterator(users)) {
  1408. server.registerUser(user, pass);
  1409. server.createContents(user, contents);
  1410. }
  1411. server.start();
  1412. return server;
  1413. }