services/sync/tests/unit/head_http_server.js

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:58efebc7535f
1 const Cm = Components.manager;
2
3 // Shared logging for all HTTP server functions.
4 Cu.import("resource://gre/modules/Log.jsm");
5 const SYNC_HTTP_LOGGER = "Sync.Test.Server";
6 const SYNC_API_VERSION = "1.1";
7
8 // Use the same method that record.js does, which mirrors the server.
9 // The server returns timestamps with 1/100 sec granularity. Note that this is
10 // subject to change: see Bug 650435.
11 function new_timestamp() {
12 return Math.round(Date.now() / 10) / 100;
13 }
14
15 function return_timestamp(request, response, timestamp) {
16 if (!timestamp) {
17 timestamp = new_timestamp();
18 }
19 let body = "" + timestamp;
20 response.setHeader("X-Weave-Timestamp", body);
21 response.setStatusLine(request.httpVersion, 200, "OK");
22 response.bodyOutputStream.write(body, body.length);
23 return timestamp;
24 }
25
26 function basic_auth_header(user, password) {
27 return "Basic " + btoa(user + ":" + Utils.encodeUTF8(password));
28 }
29
30 function basic_auth_matches(req, user, password) {
31 if (!req.hasHeader("Authorization")) {
32 return false;
33 }
34
35 let expected = basic_auth_header(user, Utils.encodeUTF8(password));
36 return req.getHeader("Authorization") == expected;
37 }
38
39 function httpd_basic_auth_handler(body, metadata, response) {
40 if (basic_auth_matches(metadata, "guest", "guest")) {
41 response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
42 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
43 } else {
44 body = "This path exists and is protected - failed";
45 response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
46 response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
47 }
48 response.bodyOutputStream.write(body, body.length);
49 }
50
51 /*
52 * Represent a WBO on the server
53 */
54 function ServerWBO(id, initialPayload, modified) {
55 if (!id) {
56 throw "No ID for ServerWBO!";
57 }
58 this.id = id;
59 if (!initialPayload) {
60 return;
61 }
62
63 if (typeof initialPayload == "object") {
64 initialPayload = JSON.stringify(initialPayload);
65 }
66 this.payload = initialPayload;
67 this.modified = modified || new_timestamp();
68 }
69 ServerWBO.prototype = {
70
71 get data() {
72 return JSON.parse(this.payload);
73 },
74
75 get: function() {
76 return JSON.stringify(this, ["id", "modified", "payload"]);
77 },
78
79 put: function(input) {
80 input = JSON.parse(input);
81 this.payload = input.payload;
82 this.modified = new_timestamp();
83 },
84
85 delete: function() {
86 delete this.payload;
87 delete this.modified;
88 },
89
90 // This handler sets `newModified` on the response body if the collection
91 // timestamp has changed. This allows wrapper handlers to extract information
92 // that otherwise would exist only in the body stream.
93 handler: function() {
94 let self = this;
95
96 return function(request, response) {
97 var statusCode = 200;
98 var status = "OK";
99 var body;
100
101 switch(request.method) {
102 case "GET":
103 if (self.payload) {
104 body = self.get();
105 } else {
106 statusCode = 404;
107 status = "Not Found";
108 body = "Not Found";
109 }
110 break;
111
112 case "PUT":
113 self.put(readBytesFromInputStream(request.bodyInputStream));
114 body = JSON.stringify(self.modified);
115 response.setHeader("Content-Type", "application/json");
116 response.newModified = self.modified;
117 break;
118
119 case "DELETE":
120 self.delete();
121 let ts = new_timestamp();
122 body = JSON.stringify(ts);
123 response.setHeader("Content-Type", "application/json");
124 response.newModified = ts;
125 break;
126 }
127 response.setHeader("X-Weave-Timestamp", "" + new_timestamp(), false);
128 response.setStatusLine(request.httpVersion, statusCode, status);
129 response.bodyOutputStream.write(body, body.length);
130 };
131 }
132
133 };
134
135
136 /**
137 * Represent a collection on the server. The '_wbos' attribute is a
138 * mapping of id -> ServerWBO objects.
139 *
140 * Note that if you want these records to be accessible individually,
141 * you need to register their handlers with the server separately, or use a
142 * containing HTTP server that will do so on your behalf.
143 *
144 * @param wbos
145 * An object mapping WBO IDs to ServerWBOs.
146 * @param acceptNew
147 * If true, POSTs to this collection URI will result in new WBOs being
148 * created and wired in on the fly.
149 * @param timestamp
150 * An optional timestamp value to initialize the modified time of the
151 * collection. This should be in the format returned by new_timestamp().
152 *
153 * @return the new ServerCollection instance.
154 *
155 */
156 function ServerCollection(wbos, acceptNew, timestamp) {
157 this._wbos = wbos || {};
158 this.acceptNew = acceptNew || false;
159
160 /*
161 * Track modified timestamp.
162 * We can't just use the timestamps of contained WBOs: an empty collection
163 * has a modified time.
164 */
165 this.timestamp = timestamp || new_timestamp();
166 this._log = Log.repository.getLogger(SYNC_HTTP_LOGGER);
167 }
168 ServerCollection.prototype = {
169
170 /**
171 * Convenience accessor for our WBO keys.
172 * Excludes deleted items, of course.
173 *
174 * @param filter
175 * A predicate function (applied to the ID and WBO) which dictates
176 * whether to include the WBO's ID in the output.
177 *
178 * @return an array of IDs.
179 */
180 keys: function keys(filter) {
181 return [id for ([id, wbo] in Iterator(this._wbos))
182 if (wbo.payload &&
183 (!filter || filter(id, wbo)))];
184 },
185
186 /**
187 * Convenience method to get an array of WBOs.
188 * Optionally provide a filter function.
189 *
190 * @param filter
191 * A predicate function, applied to the WBO, which dictates whether to
192 * include the WBO in the output.
193 *
194 * @return an array of ServerWBOs.
195 */
196 wbos: function wbos(filter) {
197 let os = [wbo for ([id, wbo] in Iterator(this._wbos))
198 if (wbo.payload)];
199 if (filter) {
200 return os.filter(filter);
201 }
202 return os;
203 },
204
205 /**
206 * Convenience method to get an array of parsed ciphertexts.
207 *
208 * @return an array of the payloads of each stored WBO.
209 */
210 payloads: function () {
211 return this.wbos().map(function (wbo) {
212 return JSON.parse(JSON.parse(wbo.payload).ciphertext);
213 });
214 },
215
216 // Just for syntactic elegance.
217 wbo: function wbo(id) {
218 return this._wbos[id];
219 },
220
221 payload: function payload(id) {
222 return this.wbo(id).payload;
223 },
224
225 /**
226 * Insert the provided WBO under its ID.
227 *
228 * @return the provided WBO.
229 */
230 insertWBO: function insertWBO(wbo) {
231 return this._wbos[wbo.id] = wbo;
232 },
233
234 /**
235 * Insert the provided payload as part of a new ServerWBO with the provided
236 * ID.
237 *
238 * @param id
239 * The GUID for the WBO.
240 * @param payload
241 * The payload, as provided to the ServerWBO constructor.
242 * @param modified
243 * An optional modified time for the ServerWBO.
244 *
245 * @return the inserted WBO.
246 */
247 insert: function insert(id, payload, modified) {
248 return this.insertWBO(new ServerWBO(id, payload, modified));
249 },
250
251 /**
252 * Removes an object entirely from the collection.
253 *
254 * @param id
255 * (string) ID to remove.
256 */
257 remove: function remove(id) {
258 delete this._wbos[id];
259 },
260
261 _inResultSet: function(wbo, options) {
262 return wbo.payload
263 && (!options.ids || (options.ids.indexOf(wbo.id) != -1))
264 && (!options.newer || (wbo.modified > options.newer));
265 },
266
267 count: function(options) {
268 options = options || {};
269 let c = 0;
270 for (let [id, wbo] in Iterator(this._wbos)) {
271 if (wbo.modified && this._inResultSet(wbo, options)) {
272 c++;
273 }
274 }
275 return c;
276 },
277
278 get: function(options) {
279 let result;
280 if (options.full) {
281 let data = [wbo.get() for ([id, wbo] in Iterator(this._wbos))
282 // Drop deleted.
283 if (wbo.modified &&
284 this._inResultSet(wbo, options))];
285 if (options.limit) {
286 data = data.slice(0, options.limit);
287 }
288 // Our implementation of application/newlines.
289 result = data.join("\n") + "\n";
290
291 // Use options as a backchannel to report count.
292 options.recordCount = data.length;
293 } else {
294 let data = [id for ([id, wbo] in Iterator(this._wbos))
295 if (this._inResultSet(wbo, options))];
296 if (options.limit) {
297 data = data.slice(0, options.limit);
298 }
299 result = JSON.stringify(data);
300 options.recordCount = data.length;
301 }
302 return result;
303 },
304
305 post: function(input) {
306 input = JSON.parse(input);
307 let success = [];
308 let failed = {};
309
310 // This will count records where we have an existing ServerWBO
311 // registered with us as successful and all other records as failed.
312 for each (let record in input) {
313 let wbo = this.wbo(record.id);
314 if (!wbo && this.acceptNew) {
315 this._log.debug("Creating WBO " + JSON.stringify(record.id) +
316 " on the fly.");
317 wbo = new ServerWBO(record.id);
318 this.insertWBO(wbo);
319 }
320 if (wbo) {
321 wbo.payload = record.payload;
322 wbo.modified = new_timestamp();
323 success.push(record.id);
324 } else {
325 failed[record.id] = "no wbo configured";
326 }
327 }
328 return {modified: new_timestamp(),
329 success: success,
330 failed: failed};
331 },
332
333 delete: function(options) {
334 let deleted = [];
335 for (let [id, wbo] in Iterator(this._wbos)) {
336 if (this._inResultSet(wbo, options)) {
337 this._log.debug("Deleting " + JSON.stringify(wbo));
338 deleted.push(wbo.id);
339 wbo.delete();
340 }
341 }
342 return deleted;
343 },
344
345 // This handler sets `newModified` on the response body if the collection
346 // timestamp has changed.
347 handler: function() {
348 let self = this;
349
350 return function(request, response) {
351 var statusCode = 200;
352 var status = "OK";
353 var body;
354
355 // Parse queryString
356 let options = {};
357 for each (let chunk in request.queryString.split("&")) {
358 if (!chunk) {
359 continue;
360 }
361 chunk = chunk.split("=");
362 if (chunk.length == 1) {
363 options[chunk[0]] = "";
364 } else {
365 options[chunk[0]] = chunk[1];
366 }
367 }
368 if (options.ids) {
369 options.ids = options.ids.split(",");
370 }
371 if (options.newer) {
372 options.newer = parseFloat(options.newer);
373 }
374 if (options.limit) {
375 options.limit = parseInt(options.limit, 10);
376 }
377
378 switch(request.method) {
379 case "GET":
380 body = self.get(options);
381 // "If supported by the db, this header will return the number of
382 // records total in the request body of any multiple-record GET
383 // request."
384 let records = options.recordCount;
385 self._log.info("Records: " + records);
386 if (records != null) {
387 response.setHeader("X-Weave-Records", "" + records);
388 }
389 break;
390
391 case "POST":
392 let res = self.post(readBytesFromInputStream(request.bodyInputStream));
393 body = JSON.stringify(res);
394 response.newModified = res.modified;
395 break;
396
397 case "DELETE":
398 self._log.debug("Invoking ServerCollection.DELETE.");
399 let deleted = self.delete(options);
400 let ts = new_timestamp();
401 body = JSON.stringify(ts);
402 response.newModified = ts;
403 response.deleted = deleted;
404 break;
405 }
406 response.setHeader("X-Weave-Timestamp",
407 "" + new_timestamp(),
408 false);
409 response.setStatusLine(request.httpVersion, statusCode, status);
410 response.bodyOutputStream.write(body, body.length);
411
412 // Update the collection timestamp to the appropriate modified time.
413 // This is either a value set by the handler, or the current time.
414 if (request.method != "GET") {
415 this.timestamp = (response.newModified >= 0) ?
416 response.newModified :
417 new_timestamp();
418 }
419 };
420 }
421
422 };
423
424 /*
425 * Test setup helpers.
426 */
427 function sync_httpd_setup(handlers) {
428 handlers["/1.1/foo/storage/meta/global"]
429 = (new ServerWBO("global", {})).handler();
430 return httpd_setup(handlers);
431 }
432
433 /*
434 * Track collection modified times. Return closures.
435 */
436 function track_collections_helper() {
437
438 /*
439 * Our tracking object.
440 */
441 let collections = {};
442
443 /*
444 * Update the timestamp of a collection.
445 */
446 function update_collection(coll, ts) {
447 _("Updating collection " + coll + " to " + ts);
448 let timestamp = ts || new_timestamp();
449 collections[coll] = timestamp;
450 }
451
452 /*
453 * Invoke a handler, updating the collection's modified timestamp unless
454 * it's a GET request.
455 */
456 function with_updated_collection(coll, f) {
457 return function(request, response) {
458 f.call(this, request, response);
459
460 // Update the collection timestamp to the appropriate modified time.
461 // This is either a value set by the handler, or the current time.
462 if (request.method != "GET") {
463 update_collection(coll, response.newModified)
464 }
465 };
466 }
467
468 /*
469 * Return the info/collections object.
470 */
471 function info_collections(request, response) {
472 let body = "Error.";
473 switch(request.method) {
474 case "GET":
475 body = JSON.stringify(collections);
476 break;
477 default:
478 throw "Non-GET on info_collections.";
479 }
480
481 response.setHeader("Content-Type", "application/json");
482 response.setHeader("X-Weave-Timestamp",
483 "" + new_timestamp(),
484 false);
485 response.setStatusLine(request.httpVersion, 200, "OK");
486 response.bodyOutputStream.write(body, body.length);
487 }
488
489 return {"collections": collections,
490 "handler": info_collections,
491 "with_updated_collection": with_updated_collection,
492 "update_collection": update_collection};
493 }
494
495 //===========================================================================//
496 // httpd.js-based Sync server. //
497 //===========================================================================//
498
499 /**
500 * In general, the preferred way of using SyncServer is to directly introspect
501 * it. Callbacks are available for operations which are hard to verify through
502 * introspection, such as deletions.
503 *
504 * One of the goals of this server is to provide enough hooks for test code to
505 * find out what it needs without monkeypatching. Use this object as your
506 * prototype, and override as appropriate.
507 */
508 let SyncServerCallback = {
509 onCollectionDeleted: function onCollectionDeleted(user, collection) {},
510 onItemDeleted: function onItemDeleted(user, collection, wboID) {},
511
512 /**
513 * Called at the top of every request.
514 *
515 * Allows the test to inspect the request. Hooks should be careful not to
516 * modify or change state of the request or they may impact future processing.
517 */
518 onRequest: function onRequest(request) {},
519 };
520
521 /**
522 * Construct a new test Sync server. Takes a callback object (e.g.,
523 * SyncServerCallback) as input.
524 */
525 function SyncServer(callback) {
526 this.callback = callback || {__proto__: SyncServerCallback};
527 this.server = new HttpServer();
528 this.started = false;
529 this.users = {};
530 this._log = Log.repository.getLogger(SYNC_HTTP_LOGGER);
531
532 // Install our own default handler. This allows us to mess around with the
533 // whole URL space.
534 let handler = this.server._handler;
535 handler._handleDefault = this.handleDefault.bind(this, handler);
536 }
537 SyncServer.prototype = {
538 server: null, // HttpServer.
539 users: null, // Map of username => {collections, password}.
540
541 /**
542 * Start the SyncServer's underlying HTTP server.
543 *
544 * @param port
545 * The numeric port on which to start. A falsy value implies the
546 * default, a randomly chosen port.
547 * @param cb
548 * A callback function (of no arguments) which is invoked after
549 * startup.
550 */
551 start: function start(port, cb) {
552 if (this.started) {
553 this._log.warn("Warning: server already started on " + this.port);
554 return;
555 }
556 try {
557 this.server.start(port);
558 let i = this.server.identity;
559 this.port = i.primaryPort;
560 this.baseURI = i.primaryScheme + "://" + i.primaryHost + ":" +
561 i.primaryPort + "/";
562 this.started = true;
563 if (cb) {
564 cb();
565 }
566 } catch (ex) {
567 _("==========================================");
568 _("Got exception starting Sync HTTP server.");
569 _("Error: " + Utils.exceptionStr(ex));
570 _("Is there a process already listening on port " + port + "?");
571 _("==========================================");
572 do_throw(ex);
573 }
574
575 },
576
577 /**
578 * Stop the SyncServer's HTTP server.
579 *
580 * @param cb
581 * A callback function. Invoked after the server has been stopped.
582 *
583 */
584 stop: function stop(cb) {
585 if (!this.started) {
586 this._log.warn("SyncServer: Warning: server not running. Can't stop me now!");
587 return;
588 }
589
590 this.server.stop(cb);
591 this.started = false;
592 },
593
594 /**
595 * Return a server timestamp for a record.
596 * The server returns timestamps with 1/100 sec granularity. Note that this is
597 * subject to change: see Bug 650435.
598 */
599 timestamp: function timestamp() {
600 return new_timestamp();
601 },
602
603 /**
604 * Create a new user, complete with an empty set of collections.
605 *
606 * @param username
607 * The username to use. An Error will be thrown if a user by that name
608 * already exists.
609 * @param password
610 * A password string.
611 *
612 * @return a user object, as would be returned by server.user(username).
613 */
614 registerUser: function registerUser(username, password) {
615 if (username in this.users) {
616 throw new Error("User already exists.");
617 }
618 this.users[username] = {
619 password: password,
620 collections: {}
621 };
622 return this.user(username);
623 },
624
625 userExists: function userExists(username) {
626 return username in this.users;
627 },
628
629 getCollection: function getCollection(username, collection) {
630 return this.users[username].collections[collection];
631 },
632
633 _insertCollection: function _insertCollection(collections, collection, wbos) {
634 let coll = new ServerCollection(wbos, true);
635 coll.collectionHandler = coll.handler();
636 collections[collection] = coll;
637 return coll;
638 },
639
640 createCollection: function createCollection(username, collection, wbos) {
641 if (!(username in this.users)) {
642 throw new Error("Unknown user.");
643 }
644 let collections = this.users[username].collections;
645 if (collection in collections) {
646 throw new Error("Collection already exists.");
647 }
648 return this._insertCollection(collections, collection, wbos);
649 },
650
651 /**
652 * Accept a map like the following:
653 * {
654 * meta: {global: {version: 1, ...}},
655 * crypto: {"keys": {}, foo: {bar: 2}},
656 * bookmarks: {}
657 * }
658 * to cause collections and WBOs to be created.
659 * If a collection already exists, no error is raised.
660 * If a WBO already exists, it will be updated to the new contents.
661 */
662 createContents: function createContents(username, collections) {
663 if (!(username in this.users)) {
664 throw new Error("Unknown user.");
665 }
666 let userCollections = this.users[username].collections;
667 for (let [id, contents] in Iterator(collections)) {
668 let coll = userCollections[id] ||
669 this._insertCollection(userCollections, id);
670 for (let [wboID, payload] in Iterator(contents)) {
671 coll.insert(wboID, payload);
672 }
673 }
674 },
675
676 /**
677 * Insert a WBO in an existing collection.
678 */
679 insertWBO: function insertWBO(username, collection, wbo) {
680 if (!(username in this.users)) {
681 throw new Error("Unknown user.");
682 }
683 let userCollections = this.users[username].collections;
684 if (!(collection in userCollections)) {
685 throw new Error("Unknown collection.");
686 }
687 userCollections[collection].insertWBO(wbo);
688 return wbo;
689 },
690
691 /**
692 * Delete all of the collections for the named user.
693 *
694 * @param username
695 * The name of the affected user.
696 *
697 * @return a timestamp.
698 */
699 deleteCollections: function deleteCollections(username) {
700 if (!(username in this.users)) {
701 throw new Error("Unknown user.");
702 }
703 let userCollections = this.users[username].collections;
704 for each (let [name, coll] in Iterator(userCollections)) {
705 this._log.trace("Bulk deleting " + name + " for " + username + "...");
706 coll.delete({});
707 }
708 this.users[username].collections = {};
709 return this.timestamp();
710 },
711
712 /**
713 * Simple accessor to allow collective binding and abbreviation of a bunch of
714 * methods. Yay!
715 * Use like this:
716 *
717 * let u = server.user("john");
718 * u.collection("bookmarks").wbo("abcdefg").payload; // Etc.
719 *
720 * @return a proxy for the user data stored in this server.
721 */
722 user: function user(username) {
723 let collection = this.getCollection.bind(this, username);
724 let createCollection = this.createCollection.bind(this, username);
725 let createContents = this.createContents.bind(this, username);
726 let modified = function (collectionName) {
727 return collection(collectionName).timestamp;
728 }
729 let deleteCollections = this.deleteCollections.bind(this, username);
730 return {
731 collection: collection,
732 createCollection: createCollection,
733 createContents: createContents,
734 deleteCollections: deleteCollections,
735 modified: modified
736 };
737 },
738
739 /*
740 * Regular expressions for splitting up Sync request paths.
741 * Sync URLs are of the form:
742 * /$apipath/$version/$user/$further
743 * where $further is usually:
744 * storage/$collection/$wbo
745 * or
746 * storage/$collection
747 * or
748 * info/$op
749 * We assume for the sake of simplicity that $apipath is empty.
750 *
751 * N.B., we don't follow any kind of username spec here, because as far as I
752 * can tell there isn't one. See Bug 689671. Instead we follow the Python
753 * server code.
754 *
755 * Path: [all, version, username, first, rest]
756 * Storage: [all, collection?, id?]
757 */
758 pathRE: /^\/([0-9]+(?:\.[0-9]+)?)\/([-._a-zA-Z0-9]+)(?:\/([^\/]+)(?:\/(.+))?)?$/,
759 storageRE: /^([-_a-zA-Z0-9]+)(?:\/([-_a-zA-Z0-9]+)\/?)?$/,
760
761 defaultHeaders: {},
762
763 /**
764 * HTTP response utility.
765 */
766 respond: function respond(req, resp, code, status, body, headers) {
767 resp.setStatusLine(req.httpVersion, code, status);
768 for each (let [header, value] in Iterator(headers || this.defaultHeaders)) {
769 resp.setHeader(header, value);
770 }
771 resp.setHeader("X-Weave-Timestamp", "" + this.timestamp(), false);
772 resp.bodyOutputStream.write(body, body.length);
773 },
774
775 /**
776 * This is invoked by the HttpServer. `this` is bound to the SyncServer;
777 * `handler` is the HttpServer's handler.
778 *
779 * TODO: need to use the correct Sync API response codes and errors here.
780 * TODO: Basic Auth.
781 * TODO: check username in path against username in BasicAuth.
782 */
783 handleDefault: function handleDefault(handler, req, resp) {
784 try {
785 this._handleDefault(handler, req, resp);
786 } catch (e) {
787 if (e instanceof HttpError) {
788 this.respond(req, resp, e.code, e.description, "", {});
789 } else {
790 throw e;
791 }
792 }
793 },
794
795 _handleDefault: function _handleDefault(handler, req, resp) {
796 this._log.debug("SyncServer: Handling request: " + req.method + " " + req.path);
797
798 if (this.callback.onRequest) {
799 this.callback.onRequest(req);
800 }
801
802 let parts = this.pathRE.exec(req.path);
803 if (!parts) {
804 this._log.debug("SyncServer: Unexpected request: bad URL " + req.path);
805 throw HTTP_404;
806 }
807
808 let [all, version, username, first, rest] = parts;
809 // Doing a float compare of the version allows for us to pretend there was
810 // a node-reassignment - eg, we could re-assign from "1.1/user/" to
811 // "1.10/user" - this server will then still accept requests with the new
812 // URL while any code in sync itself which compares URLs will see a
813 // different URL.
814 if (parseFloat(version) != parseFloat(SYNC_API_VERSION)) {
815 this._log.debug("SyncServer: Unknown version.");
816 throw HTTP_404;
817 }
818
819 if (!this.userExists(username)) {
820 this._log.debug("SyncServer: Unknown user.");
821 throw HTTP_401;
822 }
823
824 // Hand off to the appropriate handler for this path component.
825 if (first in this.toplevelHandlers) {
826 let handler = this.toplevelHandlers[first];
827 return handler.call(this, handler, req, resp, version, username, rest);
828 }
829 this._log.debug("SyncServer: Unknown top-level " + first);
830 throw HTTP_404;
831 },
832
833 /**
834 * Compute the object that is returned for an info/collections request.
835 */
836 infoCollections: function infoCollections(username) {
837 let responseObject = {};
838 let colls = this.users[username].collections;
839 for (let coll in colls) {
840 responseObject[coll] = colls[coll].timestamp;
841 }
842 this._log.trace("SyncServer: info/collections returning " +
843 JSON.stringify(responseObject));
844 return responseObject;
845 },
846
847 /**
848 * Collection of the handler methods we use for top-level path components.
849 */
850 toplevelHandlers: {
851 "storage": function handleStorage(handler, req, resp, version, username, rest) {
852 let respond = this.respond.bind(this, req, resp);
853 if (!rest || !rest.length) {
854 this._log.debug("SyncServer: top-level storage " +
855 req.method + " request.");
856
857 // TODO: verify if this is spec-compliant.
858 if (req.method != "DELETE") {
859 respond(405, "Method Not Allowed", "[]", {"Allow": "DELETE"});
860 return undefined;
861 }
862
863 // Delete all collections and track the timestamp for the response.
864 let timestamp = this.user(username).deleteCollections();
865
866 // Return timestamp and OK for deletion.
867 respond(200, "OK", JSON.stringify(timestamp));
868 return undefined;
869 }
870
871 let match = this.storageRE.exec(rest);
872 if (!match) {
873 this._log.warn("SyncServer: Unknown storage operation " + rest);
874 throw HTTP_404;
875 }
876 let [all, collection, wboID] = match;
877 let coll = this.getCollection(username, collection);
878 switch (req.method) {
879 case "GET":
880 if (!coll) {
881 if (wboID) {
882 respond(404, "Not found", "Not found");
883 return undefined;
884 }
885 // *cries inside*: Bug 687299.
886 respond(200, "OK", "[]");
887 return undefined;
888 }
889 if (!wboID) {
890 return coll.collectionHandler(req, resp);
891 }
892 let wbo = coll.wbo(wboID);
893 if (!wbo) {
894 respond(404, "Not found", "Not found");
895 return undefined;
896 }
897 return wbo.handler()(req, resp);
898
899 // TODO: implement handling of X-If-Unmodified-Since for write verbs.
900 case "DELETE":
901 if (!coll) {
902 respond(200, "OK", "{}");
903 return undefined;
904 }
905 if (wboID) {
906 let wbo = coll.wbo(wboID);
907 if (wbo) {
908 wbo.delete();
909 this.callback.onItemDeleted(username, collection, wboID);
910 }
911 respond(200, "OK", "{}");
912 return undefined;
913 }
914 coll.collectionHandler(req, resp);
915
916 // Spot if this is a DELETE for some IDs, and don't blow away the
917 // whole collection!
918 //
919 // We already handled deleting the WBOs by invoking the deleted
920 // collection's handler. However, in the case of
921 //
922 // DELETE storage/foobar
923 //
924 // we also need to remove foobar from the collections map. This
925 // clause tries to differentiate the above request from
926 //
927 // DELETE storage/foobar?ids=foo,baz
928 //
929 // and do the right thing.
930 // TODO: less hacky method.
931 if (-1 == req.queryString.indexOf("ids=")) {
932 // When you delete the entire collection, we drop it.
933 this._log.debug("Deleting entire collection.");
934 delete this.users[username].collections[collection];
935 this.callback.onCollectionDeleted(username, collection);
936 }
937
938 // Notify of item deletion.
939 let deleted = resp.deleted || [];
940 for (let i = 0; i < deleted.length; ++i) {
941 this.callback.onItemDeleted(username, collection, deleted[i]);
942 }
943 return undefined;
944 case "POST":
945 case "PUT":
946 if (!coll) {
947 coll = this.createCollection(username, collection);
948 }
949 if (wboID) {
950 let wbo = coll.wbo(wboID);
951 if (!wbo) {
952 this._log.trace("SyncServer: creating WBO " + collection + "/" + wboID);
953 wbo = coll.insert(wboID);
954 }
955 // Rather than instantiate each WBO's handler function, do it once
956 // per request. They get hit far less often than do collections.
957 wbo.handler()(req, resp);
958 coll.timestamp = resp.newModified;
959 return resp;
960 }
961 return coll.collectionHandler(req, resp);
962 default:
963 throw "Request method " + req.method + " not implemented.";
964 }
965 },
966
967 "info": function handleInfo(handler, req, resp, version, username, rest) {
968 switch (rest) {
969 case "collections":
970 let body = JSON.stringify(this.infoCollections(username));
971 this.respond(req, resp, 200, "OK", body, {
972 "Content-Type": "application/json"
973 });
974 return;
975 case "collection_usage":
976 case "collection_counts":
977 case "quota":
978 // TODO: implement additional info methods.
979 this.respond(req, resp, 200, "OK", "TODO");
980 return;
981 default:
982 // TODO
983 this._log.warn("SyncServer: Unknown info operation " + rest);
984 throw HTTP_404;
985 }
986 }
987 }
988 };
989
990 /**
991 * Test helper.
992 */
993 function serverForUsers(users, contents, callback) {
994 let server = new SyncServer(callback);
995 for (let [user, pass] in Iterator(users)) {
996 server.registerUser(user, pass);
997 server.createContents(user, contents);
998 }
999 server.start();
1000 return server;
1001 }

mercurial