// Shared logging for all HTTP server functions. var { Log } = ChromeUtils.importESModule("resource://gre/modules/Log.sys.mjs"); var { CommonUtils } = ChromeUtils.importESModule( "resource://services-common/utils.sys.mjs"
); var { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs"
); var {
MockFxaStorageManager,
SyncTestingInfrastructure,
configureFxAccountIdentity,
configureIdentity,
encryptPayload,
getLoginTelemetryScalar,
makeFxAccountsInternalMock,
makeIdentityConfig,
promiseNamedTimer,
promiseZeroTimer,
sumHistogram,
syncTestLogging,
waitForZeroTimer,
} = ChromeUtils.importESModule( "resource://testing-common/services/sync/utils.sys.mjs"
);
const SYNC_HTTP_LOGGER = "Sync.Test.Server";
// While the sync code itself uses 1.5, the tests hard-code 1.1, // so we're sticking with 1.1 here. const SYNC_API_VERSION = "1.1";
// Use the same method that record.js does, which mirrors the server. // The server returns timestamps with 1/100 sec granularity. Note that this is // subject to change: see Bug 650435. function new_timestamp() { return round_timestamp(Date.now());
}
// Rounds a millisecond timestamp `t` to seconds, with centisecond precision. function round_timestamp(t) { return Math.round(t / 10) / 100;
}
function return_timestamp(request, response, timestamp) { if (!timestamp) {
timestamp = new_timestamp();
}
let body = "" + timestamp;
response.setHeader("X-Weave-Timestamp", body);
response.setStatusLine(request.httpVersion, 200, "OK");
writeBytesToOutputStream(response.bodyOutputStream, body); return timestamp;
}
function has_hawk_header(req) { return (
req.hasHeader("Authorization") &&
req.getHeader("Authorization").startsWith("Hawk")
);
}
function basic_auth_matches(req, user, password) { if (!req.hasHeader("Authorization")) { returnfalse;
}
let expected = basic_auth_header(user, CommonUtils.encodeUTF8(password)); return req.getHeader("Authorization") == expected;
}
function httpd_basic_auth_handler(body, metadata, response) { if (basic_auth_matches(metadata, "guest", "guest")) {
response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
} else {
body = "This path exists and is protected - failed";
response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
}
writeBytesToOutputStream(response.bodyOutputStream, body);
}
/* * Represent a WBO on the server
*/ function ServerWBO(id, initialPayload, modified) { if (!id) { thrownew Error("No ID for ServerWBO!");
} this.id = id; if (!initialPayload) { return;
}
// This handler sets `newModified` on the response body if the collection // timestamp has changed. This allows wrapper handlers to extract information // that otherwise would exist only in the body stream.
handler() {
let self = this;
returnfunction (request, response) { var statusCode = 200; var status = "OK"; var body;
switch (request.method) { case"GET": if (self.payload) {
body = JSON.stringify(self.get());
} else {
statusCode = 404;
status = "Not Found";
body = "Not Found";
} break;
/** * Get the cleartext data stored in the payload. * * This isn't `get cleartext`, because `x.cleartext.blah = 3;` wouldn't work, * which seems like a footgun.
*/
getCleartext() { return JSON.parse(JSON.parse(this.payload).ciphertext);
},
/** * Setter for getCleartext(), but lets you adjust the modified timestamp too. * Returns this ServerWBO object.
*/
setCleartext(cleartext, modifiedTimestamp = this.modified) { this.payload = JSON.stringify(encryptPayload(cleartext)); this.modified = modifiedTimestamp; returnthis;
},
};
/** * Represent a collection on the server. The '_wbos' attribute is a * mapping of id -> ServerWBO objects. * * Note that if you want these records to be accessible individually, * you need to register their handlers with the server separately, or use a * containing HTTP server that will do so on your behalf. * * @param wbos * An object mapping WBO IDs to ServerWBOs. * @param acceptNew * If true, POSTs to this collection URI will result in new WBOs being * created and wired in on the fly. * @param timestamp * An optional timestamp value to initialize the modified time of the * collection. This should be in the format returned by new_timestamp(). * * @return the new ServerCollection instance. *
*/ function ServerCollection(wbos, acceptNew, timestamp) { this._wbos = wbos || {}; this.acceptNew = acceptNew || false;
/* * Track modified timestamp. * We can't just use the timestamps of contained WBOs: an empty collection * has a modified time.
*/ this.timestamp = timestamp || new_timestamp(); this._log = Log.repository.getLogger(SYNC_HTTP_LOGGER);
}
ServerCollection.prototype = { /** * Convenience accessor for our WBO keys. * Excludes deleted items, of course. * * @param filter * A predicate function (applied to the ID and WBO) which dictates * whether to include the WBO's ID in the output. * * @return an array of IDs.
*/
keys: function keys(filter) {
let ids = []; for (let [id, wbo] of Object.entries(this._wbos)) { if (wbo.payload && (!filter || filter(id, wbo))) {
ids.push(id);
}
} return ids;
},
/** * Convenience method to get an array of WBOs. * Optionally provide a filter function. * * @param filter * A predicate function, applied to the WBO, which dictates whether to * include the WBO in the output. * * @return an array of ServerWBOs.
*/
wbos: function wbos(filter) {
let os = []; for (let wbo of Object.values(this._wbos)) { if (wbo.payload) {
os.push(wbo);
}
}
if (filter) { return os.filter(filter);
} return os;
},
/** * Convenience method to get an array of parsed ciphertexts. * * @return an array of the payloads of each stored WBO.
*/
payloads() { returnthis.wbos().map(wbo => wbo.getCleartext());
},
// Just for syntactic elegance.
wbo: function wbo(id) { returnthis._wbos[id];
},
payload: function payload(id) { returnthis.wbo(id).payload;
},
/** * Insert the provided WBO under its ID. * * @return the provided WBO.
*/
insertWBO: function insertWBO(wbo) { this.timestamp = Math.max(this.timestamp, wbo.modified); return (this._wbos[wbo.id] = wbo);
},
/** * Update an existing WBO's cleartext using a callback function that modifies * the record in place, or returns a new record.
*/
updateRecord(id, updateCallback, optTimestamp) {
let wbo = this.wbo(id); if (!wbo) { thrownew Error("No record with provided ID");
}
let curCleartext = wbo.getCleartext(); // Allow update callback to either return a new cleartext, or modify in place.
let newCleartext = updateCallback(curCleartext) || curCleartext;
wbo.setCleartext(newCleartext, optTimestamp); // It is already inserted, but we might need to update our timestamp based // on it's `modified` value, if `optTimestamp` was provided. returnthis.insertWBO(wbo);
},
/** * Insert a record, which may either an object with a cleartext property, or * the cleartext property itself.
*/
insertRecord(record, timestamp = Math.round(Date.now() / 10) / 100) { if (typeof timestamp != "number") { thrownew TypeError("insertRecord: Timestamp is not a number.");
} if (!record.id) { thrownew Error("Attempt to insert record with no id");
} // Allow providing either the cleartext directly, or the CryptoWrapper-like.
let cleartext = record.cleartext || record; returnthis.insert(record.id, encryptPayload(cleartext), timestamp);
},
/** * Insert the provided payload as part of a new ServerWBO with the provided * ID. * * @param id * The GUID for the WBO. * @param payload * The payload, as provided to the ServerWBO constructor. * @param modified * An optional modified time for the ServerWBO. * * @return the inserted WBO.
*/
insert: function insert(id, payload, modified) { returnthis.insertWBO(new ServerWBO(id, payload, modified));
},
/** * Removes an object entirely from the collection. * * @param id * (string) ID to remove.
*/
remove: function remove(id) { deletethis._wbos[id];
},
count(options) {
options = options || {};
let c = 0; for (let wbo of Object.values(this._wbos)) { if (wbo.modified && this._inResultSet(wbo, options)) {
c++;
}
} return c;
},
get(options, request) {
let data = []; for (let wbo of Object.values(this._wbos)) { if (wbo.modified && this._inResultSet(wbo, options)) {
data.push(wbo);
}
} switch (options.sort) { case"newest":
data.sort((a, b) => b.modified - a.modified); break;
case"oldest":
data.sort((a, b) => a.modified - b.modified); break;
case"index":
data.sort((a, b) => b.sortindex - a.sortindex); break;
default: if (options.sort) { this._log.error( "Error: client requesting unknown sort order",
options.sort
); thrownew Error("Unknown sort order");
} // If the client didn't request a sort order, shuffle the records // to ensure that we don't accidentally depend on the default order.
TestUtils.shuffle(data);
} if (options.full) {
data = data.map(wbo => wbo.get());
let start = options.offset || 0; if (options.limit) {
let numItemsPastOffset = data.length - start;
data = data.slice(start, start + options.limit); // use options as a backchannel to set x-weave-next-offset if (numItemsPastOffset > options.limit) {
options.nextOffset = start + options.limit;
}
} elseif (start) {
data = data.slice(start);
}
if (request && request.getHeader("accept") == "application/newlines") { this._log.error( "Error: client requesting application/newlines content"
); thrownew Error( "This server should not serve application/newlines content"
);
}
// Use options as a backchannel to report count.
options.recordCount = data.length;
} else {
data = data.map(wbo => wbo.id);
let start = options.offset || 0; if (options.limit) {
data = data.slice(start, start + options.limit);
options.nextOffset = start + options.limit;
} elseif (start) {
data = data.slice(start);
}
options.recordCount = data.length;
} return JSON.stringify(data);
},
post(input) {
input = JSON.parse(input);
let success = [];
let failed = {};
// This will count records where we have an existing ServerWBO // registered with us as successful and all other records as failed. for (let key in input) {
let record = input[key];
let wbo = this.wbo(record.id); if (!wbo && this.acceptNew) { this._log.debug( "Creating WBO " + JSON.stringify(record.id) + " on the fly."
);
wbo = new ServerWBO(record.id); this.insertWBO(wbo);
} if (wbo) {
wbo.payload = record.payload;
wbo.modified = new_timestamp();
wbo.sortindex = record.sortindex || 0;
success.push(record.id);
} else {
failed[record.id] = "no wbo configured";
}
} return { modified: new_timestamp(), success, failed };
},
delete(options) {
let deleted = []; for (let wbo of Object.values(this._wbos)) { if (this._inResultSet(wbo, options)) { this._log.debug("Deleting " + JSON.stringify(wbo));
deleted.push(wbo.id);
wbo.delete();
}
} return deleted;
},
// This handler sets `newModified` on the response body if the collection // timestamp has changed.
handler() {
let self = this;
returnfunction (request, response) { var statusCode = 200; var status = "OK"; var body;
// Parse queryString
let options = {}; for (let chunk of request.queryString.split("&")) { if (!chunk) { continue;
}
chunk = chunk.split("="); if (chunk.length == 1) {
options[chunk[0]] = "";
} else {
options[chunk[0]] = chunk[1];
}
} // The real servers return 400 if ids= is specified without a list of IDs. if (options.hasOwnProperty("ids")) { if (!options.ids) {
response.setStatusLine(request.httpVersion, "400", "Bad Request");
body = "Bad Request";
writeBytesToOutputStream(response.bodyOutputStream, body); return;
}
options.ids = options.ids.split(",");
} if (options.newer) {
options.newer = parseFloat(options.newer);
} if (options.older) {
options.older = parseFloat(options.older);
} if (options.limit) {
options.limit = parseInt(options.limit, 10);
} if (options.offset) {
options.offset = parseInt(options.offset, 10);
}
case"POST":
let res = self.post(
readBytesFromInputStream(request.bodyInputStream),
request
);
body = JSON.stringify(res);
response.newModified = res.modified; break;
case"DELETE":
self._log.debug("Invoking ServerCollection.DELETE.");
let deleted = self.delete(options, request);
let ts = new_timestamp();
body = JSON.stringify(ts);
response.newModified = ts;
response.deleted = deleted; break;
}
response.setHeader("X-Weave-Timestamp", "" + new_timestamp(), false);
// Update the collection timestamp to the appropriate modified time. // This is either a value set by the handler, or the current time. if (request.method != "GET") {
self.timestamp =
response.newModified >= 0 ? response.newModified : new_timestamp();
}
response.setHeader("X-Last-Modified", "" + self.timestamp, false);
/* * Test setup helpers.
*/ function sync_httpd_setup(handlers) {
handlers["/1.1/foo/storage/meta/global"] = new ServerWBO( "global",
{}
).handler(); return httpd_setup(handlers);
}
/* * Track collection modified times. Return closures. * * XXX - DO NOT USE IN NEW TESTS * * This code has very limited and very hacky timestamp support - the test * server now has more complete and correct support - using this helper * may cause strangeness wrt timestamp headers and 412 responses.
*/ function track_collections_helper() { /* * Our tracking object.
*/
let collections = {};
/* * Update the timestamp of a collection.
*/ function update_collection(coll, ts) {
_("Updating collection " + coll + " to " + ts);
let timestamp = ts || new_timestamp();
collections[coll] = timestamp;
}
/* * Invoke a handler, updating the collection's modified timestamp unless * it's a GET request.
*/ function with_updated_collection(coll, f) { returnfunction (request, response) {
f.call(this, request, response);
// Update the collection timestamp to the appropriate modified time. // This is either a value set by the handler, or the current time. if (request.method != "GET") {
update_collection(coll, response.newModified);
}
};
}
/* * Return the info/collections object.
*/ function info_collections(request, response) {
let body = "Error."; switch (request.method) { case"GET":
body = JSON.stringify(collections); break; default: thrownew Error("Non-GET on info_collections.");
}
/** * In general, the preferred way of using SyncServer is to directly introspect * it. Callbacks are available for operations which are hard to verify through * introspection, such as deletions. * * One of the goals of this server is to provide enough hooks for test code to * find out what it needs without monkeypatching. Use this object as your * prototype, and override as appropriate.
*/ var SyncServerCallback = {
onCollectionDeleted: function onCollectionDeleted() {},
onItemDeleted: function onItemDeleted() {},
/** * Called at the top of every request. * * Allows the test to inspect the request. Hooks should be careful not to * modify or change state of the request or they may impact future processing. * The response is also passed so the callback can set headers etc - but care * must be taken to not screw with the response body or headers that may * conflict with normal operation of this server.
*/
onRequest: function onRequest() {},
};
/** * Construct a new test Sync server. Takes a callback object (e.g., * SyncServerCallback) as input.
*/ function SyncServer(callback) { this.callback = callback || Object.create(SyncServerCallback); this.server = new HttpServer(); this.started = false; this.users = {}; this._log = Log.repository.getLogger(SYNC_HTTP_LOGGER);
// Install our own default handler. This allows us to mess around with the // whole URL space.
let handler = this.server._handler;
handler._handleDefault = this.handleDefault.bind(this, handler);
}
SyncServer.prototype = {
server: null, // HttpServer.
users: null, // Map of username => {collections, password}.
/** * Start the SyncServer's underlying HTTP server. * * @param port * The numeric port on which to start. -1 implies the default, a * randomly chosen port. * @param cb * A callback function (of no arguments) which is invoked after * startup.
*/
start: function start(port = -1, cb) { if (this.started) { this._log.warn("Warning: server already started on " + this.port); return;
} try { this.server.start(port);
let i = this.server.identity; this.port = i.primaryPort; this.baseURI =
i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort + "/"; this.started = true; if (cb) {
cb();
}
} catch (ex) {
_("==========================================");
_("Got exception starting Sync HTTP server.");
_("Error: " + Log.exceptionStr(ex));
_("Is there a process already listening on port " + port + "?");
_("==========================================");
do_throw(ex);
}
},
/** * Stop the SyncServer's HTTP server. * * @param cb * A callback function. Invoked after the server has been stopped. *
*/
stop: function stop(cb) { if (!this.started) { this._log.warn( "SyncServer: Warning: server not running. Can't stop me now!"
); return;
}
this.server.stop(cb); this.started = false;
},
/** * Return a server timestamp for a record. * The server returns timestamps with 1/100 sec granularity. Note that this is * subject to change: see Bug 650435.
*/
timestamp: function timestamp() { return new_timestamp();
},
/** * Create a new user, complete with an empty set of collections. * * @param username * The username to use. An Error will be thrown if a user by that name * already exists. * @param password * A password string. * * @return a user object, as would be returned by server.user(username).
*/
registerUser: function registerUser(username, password) { if (username in this.users) { thrownew Error("User already exists.");
} this.users[username] = {
password,
collections: {},
}; returnthis.user(username);
},
userExists: function userExists(username) { return username in this.users;
},
getCollection: function getCollection(username, collection) { returnthis.users[username].collections[collection];
},
_insertCollection: function _insertCollection(collections, collection, wbos) {
let coll = new ServerCollection(wbos, true);
coll.collectionHandler = coll.handler();
collections[collection] = coll; return coll;
},
createCollection: function createCollection(username, collection, wbos) { if (!(username in this.users)) { thrownew Error("Unknown user.");
}
let collections = this.users[username].collections; if (collection in collections) { thrownew Error("Collection already exists.");
} returnthis._insertCollection(collections, collection, wbos);
},
/** * Accept a map like the following: * { * meta: {global: {version: 1, ...}}, * crypto: {"keys": {}, foo: {bar: 2}}, * bookmarks: {} * } * to cause collections and WBOs to be created. * If a collection already exists, no error is raised. * If a WBO already exists, it will be updated to the new contents.
*/
createContents: function createContents(username, collections) { if (!(username in this.users)) { thrownew Error("Unknown user.");
}
let userCollections = this.users[username].collections; for (let [id, contents] of Object.entries(collections)) {
let coll =
userCollections[id] || this._insertCollection(userCollections, id); for (let [wboID, payload] of Object.entries(contents)) {
coll.insert(wboID, payload);
}
}
},
/** * Insert a WBO in an existing collection.
*/
insertWBO: function insertWBO(username, collection, wbo) { if (!(username in this.users)) { thrownew Error("Unknown user.");
}
let userCollections = this.users[username].collections; if (!(collection in userCollections)) { thrownew Error("Unknown collection.");
}
userCollections[collection].insertWBO(wbo); return wbo;
},
/** * Delete all of the collections for the named user. * * @param username * The name of the affected user. * * @return a timestamp.
*/
deleteCollections: function deleteCollections(username) { if (!(username in this.users)) { thrownew Error("Unknown user.");
}
let userCollections = this.users[username].collections; for (let name in userCollections) {
let coll = userCollections[name]; this._log.trace("Bulk deleting " + name + " for " + username + "...");
coll.delete({});
} this.users[username].collections = {}; returnthis.timestamp();
},
/** * Simple accessor to allow collective binding and abbreviation of a bunch of * methods. Yay! * Use like this: * * let u = server.user("john"); * u.collection("bookmarks").wbo("abcdefg").payload; // Etc. * * @return a proxy for the user data stored in this server.
*/
user: function user(username) {
let collection = this.getCollection.bind(this, username);
let createCollection = this.createCollection.bind(this, username);
let createContents = this.createContents.bind(this, username);
let modified = function (collectionName) { return collection(collectionName).timestamp;
};
let deleteCollections = this.deleteCollections.bind(this, username); return {
collection,
createCollection,
createContents,
deleteCollections,
modified,
};
},
/* * Regular expressions for splitting up Sync request paths. * Sync URLs are of the form: * /$apipath/$version/$user/$further * where $further is usually: * storage/$collection/$wbo * or * storage/$collection * or * info/$op * We assume for the sake of simplicity that $apipath is empty. * * N.B., we don't follow any kind of username spec here, because as far as I * can tell there isn't one. See Bug 689671. Instead we follow the Python * server code. * * Path: [all, version, username, first, rest] * Storage: [all, collection?, id?]
*/
pathRE:
/^\/([0-9]+(?:\.[0-9]+)?)\/([-._a-zA-Z0-9]+)(?:\/([^\/]+)(?:\/(.+))?)?$/,
storageRE: /^([-_a-zA-Z0-9]+)(?:\/([-_a-zA-Z0-9]+)\/?)?$/,
defaultHeaders: {},
/** * HTTP response utility.
*/
respond: function respond(req, resp, code, status, body, headers) {
resp.setStatusLine(req.httpVersion, code, status); if (!headers) {
headers = this.defaultHeaders;
} for (let header in headers) {
let value = headers[header];
resp.setHeader(header, value);
}
resp.setHeader("X-Weave-Timestamp", "" + this.timestamp(), false);
writeBytesToOutputStream(resp.bodyOutputStream, body);
},
/** * This is invoked by the HttpServer. `this` is bound to the SyncServer; * `handler` is the HttpServer's handler. * * TODO: need to use the correct Sync API response codes and errors here. * TODO: Basic Auth. * TODO: check username in path against username in BasicAuth.
*/
handleDefault: function handleDefault(handler, req, resp) { try { this._handleDefault(handler, req, resp);
} catch (e) { if (e instanceof HttpError) { this.respond(req, resp, e.code, e.description, "", {});
} else { throw e;
}
}
},
if (this.callback.onRequest) { this.callback.onRequest(req, resp);
}
let parts = this.pathRE.exec(req.path); if (!parts) { this._log.debug("SyncServer: Unexpected request: bad URL " + req.path); throw HTTP_404;
}
let [, version, username, first, rest] = parts; // Doing a float compare of the version allows for us to pretend there was // a node-reassignment - eg, we could re-assign from "1.1/user/" to // "1.10/user" - this server will then still accept requests with the new // URL while any code in sync itself which compares URLs will see a // different URL. if (parseFloat(version) != parseFloat(SYNC_API_VERSION)) { this._log.debug("SyncServer: Unknown version."); throw HTTP_404;
}
if (!this.userExists(username)) { this._log.debug("SyncServer: Unknown user."); throw HTTP_401;
}
// Hand off to the appropriate handler for this path component. if (first in this.toplevelHandlers) {
let newHandler = this.toplevelHandlers[first]; return newHandler.call( this,
newHandler,
req,
resp,
version,
username,
rest
);
} this._log.debug("SyncServer: Unknown top-level " + first); throw HTTP_404;
},
/** * Compute the object that is returned for an info/collections request.
*/
infoCollections: function infoCollections(username) {
let responseObject = {};
let colls = this.users[username].collections; for (let coll in colls) {
responseObject[coll] = colls[coll].timestamp;
} this._log.trace( "SyncServer: info/collections returning " + JSON.stringify(responseObject)
); return responseObject;
},
/** * Collection of the handler methods we use for top-level path components.
*/
toplevelHandlers: {
storage: function handleStorage(
handler,
req,
resp,
version,
username,
rest
) {
let respond = this.respond.bind(this, req, resp); if (!rest || !rest.length) { this._log.debug( "SyncServer: top-level storage " + req.method + " request."
);
// TODO: verify if this is spec-compliant. if (req.method != "DELETE") {
respond(405, "Method Not Allowed", "[]", { Allow: "DELETE" }); return undefined;
}
// Delete all collections and track the timestamp for the response.
let timestamp = this.user(username).deleteCollections();
// Return timestamp and OK for deletion.
respond(200, "OK", JSON.stringify(timestamp)); return undefined;
}
let match = this.storageRE.exec(rest); if (!match) { this._log.warn("SyncServer: Unknown storage operation " + rest); throw HTTP_404;
}
let [, collection, wboID] = match;
let coll = this.getCollection(username, collection);
let checkXIUSFailure = () => { if (req.hasHeader("x-if-unmodified-since")) {
let xius = parseFloat(req.getHeader("x-if-unmodified-since")); // Sadly the way our tests are setup, we often end up with xius of // zero (typically when syncing just one engine, so the date from // info/collections isn't used) - so we allow that to work. // Further, the Python server treats non-existing collections as // having a timestamp of 0.
let collTimestamp = coll ? coll.timestamp : 0; if (xius && xius < collTimestamp) { this._log.info(
`x-if-unmodified-since mismatch - request wants ${xius} but our collection has ${collTimestamp}`
);
respond(412, "precondition failed", "precondition failed"); returntrue;
}
} returnfalse;
};
switch (req.method) { case"GET": { if (!coll) { if (wboID) {
respond(404, "Not found", "Not found"); return undefined;
} // *cries inside*: - apparently the real sync server returned 200 // here for some time, then returned 404 for some time (bug 687299), // and now is back to 200 (bug 963332).
respond(200, "OK", "[]"); return undefined;
} if (!wboID) { return coll.collectionHandler(req, resp);
}
let wbo = coll.wbo(wboID); if (!wbo) {
respond(404, "Not found", "Not found"); return undefined;
} return wbo.handler()(req, resp);
} case"DELETE": { if (!coll) {
respond(200, "OK", "{}"); return undefined;
} if (checkXIUSFailure()) { return undefined;
} if (wboID) {
let wbo = coll.wbo(wboID); if (wbo) {
wbo.delete(); this.callback.onItemDeleted(username, collection, wboID);
}
respond(200, "OK", "{}"); return undefined;
}
coll.collectionHandler(req, resp);
// Spot if this is a DELETE for some IDs, and don't blow away the // whole collection! // // We already handled deleting the WBOs by invoking the deleted // collection's handler. However, in the case of // // DELETE storage/foobar // // we also need to remove foobar from the collections map. This // clause tries to differentiate the above request from // // DELETE storage/foobar?ids=foo,baz // // and do the right thing. // TODO: less hacky method. if (-1 == req.queryString.indexOf("ids=")) { // When you delete the entire collection, we drop it. this._log.debug("Deleting entire collection."); deletethis.users[username].collections[collection]; this.callback.onCollectionDeleted(username, collection);
}
// Notify of item deletion.
let deleted = resp.deleted || []; for (let i = 0; i < deleted.length; ++i) { this.callback.onItemDeleted(username, collection, deleted[i]);
} return undefined;
} case"PUT": // PUT and POST have slightly different XIUS semantics - for PUT, // the check is against the item, whereas for POST it is against // the collection. So first, a special-case for PUT. if (req.hasHeader("x-if-unmodified-since")) {
let xius = parseFloat(req.getHeader("x-if-unmodified-since")); // treat and xius of zero as if it wasn't specified - this happens // in some of our tests for a new collection. if (xius > 0) {
let wbo = coll.wbo(wboID); if (xius < wbo.modified) { this._log.info(
`x-if-unmodified-since mismatch - request wants ${xius} but wbo has ${wbo.modified}`
);
respond(412, "precondition failed", "precondition failed"); return undefined;
}
wbo.handler()(req, resp);
coll.timestamp = resp.newModified; return resp;
}
} // fall through to post. case"POST": if (checkXIUSFailure()) { return undefined;
} if (!coll) {
coll = this.createCollection(username, collection);
}
if (wboID) {
let wbo = coll.wbo(wboID); if (!wbo) { this._log.trace( "SyncServer: creating WBO " + collection + "/" + wboID
);
wbo = coll.insert(wboID);
} // Rather than instantiate each WBO's handler function, do it once // per request. They get hit far less often than do collections.
wbo.handler()(req, resp);
coll.timestamp = resp.newModified; return resp;
} return coll.collectionHandler(req, resp); default: thrownew Error("Request method " + req.method + " not implemented.");
}
},
info: function handleInfo(handler, req, resp, version, username, rest) { switch (rest) { case"collections":
let body = JSON.stringify(this.infoCollections(username)); this.respond(req, resp, 200, "OK", body, { "Content-Type": "application/json",
}); return; case"collection_usage": case"collection_counts": case"quota": // TODO: implement additional info methods. this.respond(req, resp, 200, "OK", "TODO"); return; default: // TODO this._log.warn("SyncServer: Unknown info operation " + rest); throw HTTP_404;
}
},
},
};
/** * Test helper.
*/ function serverForUsers(users, contents, callback) {
let server = new SyncServer(callback); for (let [user, pass] of Object.entries(users)) {
server.registerUser(user, pass);
server.createContents(user, contents);
}
server.start(); return server;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.