services/sync/tests/unit/head_http_server.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 const Cm = Components.manager;
     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";
     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 }
    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 }
    26 function basic_auth_header(user, password) {
    27   return "Basic " + btoa(user + ":" + Utils.encodeUTF8(password));
    28 }
    30 function basic_auth_matches(req, user, password) {
    31   if (!req.hasHeader("Authorization")) {
    32     return false;
    33   }
    35   let expected = basic_auth_header(user, Utils.encodeUTF8(password));
    36   return req.getHeader("Authorization") == expected;
    37 }
    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 }
    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   }
    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 = {
    71   get data() {
    72     return JSON.parse(this.payload);
    73   },
    75   get: function() {
    76     return JSON.stringify(this, ["id", "modified", "payload"]);
    77   },
    79   put: function(input) {
    80     input = JSON.parse(input);
    81     this.payload = input.payload;
    82     this.modified = new_timestamp();
    83   },
    85   delete: function() {
    86     delete this.payload;
    87     delete this.modified;
    88   },
    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;
    96     return function(request, response) {
    97       var statusCode = 200;
    98       var status = "OK";
    99       var body;
   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;
   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;
   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   }
   133 };
   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;
   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 = {
   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   },
   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   },
   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   },
   216   // Just for syntactic elegance.
   217   wbo: function wbo(id) {
   218     return this._wbos[id];
   219   },
   221   payload: function payload(id) {
   222     return this.wbo(id).payload;
   223   },
   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   },
   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   },
   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   },
   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   },
   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   },
   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";
   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   },
   305   post: function(input) {
   306     input = JSON.parse(input);
   307     let success = [];
   308     let failed = {};
   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   },
   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   },
   345   // This handler sets `newModified` on the response body if the collection
   346   // timestamp has changed.
   347   handler: function() {
   348     let self = this;
   350     return function(request, response) {
   351       var statusCode = 200;
   352       var status = "OK";
   353       var body;
   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       }
   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;
   391         case "POST":
   392           let res = self.post(readBytesFromInputStream(request.bodyInputStream));
   393           body = JSON.stringify(res);
   394           response.newModified = res.modified;
   395           break;
   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);
   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   }
   422 };
   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 }
   433 /*
   434  * Track collection modified times. Return closures.
   435  */
   436 function track_collections_helper() {
   438   /*
   439    * Our tracking object.
   440    */
   441   let collections = {};
   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   }
   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);
   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   }
   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     }
   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   }
   489   return {"collections": collections,
   490           "handler": info_collections,
   491           "with_updated_collection": with_updated_collection,
   492           "update_collection": update_collection};
   493 }
   495 //===========================================================================//
   496 // httpd.js-based Sync server.                                               //
   497 //===========================================================================//
   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) {},
   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 };
   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);
   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}.
   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     }
   575   },
   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     }
   590     this.server.stop(cb);
   591     this.started = false;
   592   },
   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   },
   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   },
   625   userExists: function userExists(username) {
   626     return username in this.users;
   627   },
   629   getCollection: function getCollection(username, collection) {
   630     return this.users[username].collections[collection];
   631   },
   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   },
   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   },
   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   },
   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   },
   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   },
   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   },
   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]+)\/?)?$/,
   761   defaultHeaders: {},
   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   },
   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   },
   795   _handleDefault: function _handleDefault(handler, req, resp) {
   796     this._log.debug("SyncServer: Handling request: " + req.method + " " + req.path);
   798     if (this.callback.onRequest) {
   799       this.callback.onRequest(req);
   800     }
   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     }
   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     }
   819     if (!this.userExists(username)) {
   820       this._log.debug("SyncServer: Unknown user.");
   821       throw HTTP_401;
   822     }
   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   },
   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   },
   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.");
   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         }
   863         // Delete all collections and track the timestamp for the response.
   864         let timestamp = this.user(username).deleteCollections();
   866         // Return timestamp and OK for deletion.
   867         respond(200, "OK", JSON.stringify(timestamp));
   868         return undefined;
   869       }
   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);
   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);
   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           }
   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     },
   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 };
   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;

mercurial