storageserver.js 46 KB

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