test_hmac_error.js (8389B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { Service } = ChromeUtils.importESModule( 5 "resource://services-sync/service.sys.mjs" 6 ); 7 8 // Track HMAC error counts. 9 var hmacErrorCount = 0; 10 (function () { 11 let hHE = Service.handleHMACEvent; 12 Service.handleHMACEvent = async function () { 13 hmacErrorCount++; 14 return hHE.call(Service); 15 }; 16 })(); 17 18 async function shared_setup() { 19 enableValidationPrefs(); 20 syncTestLogging(); 21 22 hmacErrorCount = 0; 23 24 let clientsEngine = Service.clientsEngine; 25 let clientsSyncID = await clientsEngine.resetLocalSyncID(); 26 27 // Make sure RotaryEngine is the only one we sync. 28 let { engine, syncID, tracker } = await registerRotaryEngine(); 29 await engine.setLastSync(123); // Needs to be non-zero so that tracker is queried. 30 engine._store.items = { 31 flying: "LNER Class A3 4472", 32 scotsman: "Flying Scotsman", 33 }; 34 await tracker.addChangedID("scotsman", 0); 35 Assert.equal(1, Service.engineManager.getEnabled().length); 36 37 let engines = { 38 rotary: { version: engine.version, syncID }, 39 clients: { version: clientsEngine.version, syncID: clientsSyncID }, 40 }; 41 42 // Common server objects. 43 let global = new ServerWBO("global", { engines }); 44 let keysWBO = new ServerWBO("keys"); 45 let rotaryColl = new ServerCollection({}, true); 46 let clientsColl = new ServerCollection({}, true); 47 48 return [engine, rotaryColl, clientsColl, keysWBO, global, tracker]; 49 } 50 51 add_task(async function hmac_error_during_404() { 52 _("Attempt to replicate the HMAC error setup."); 53 let [engine, rotaryColl, clientsColl, keysWBO, global, tracker] = 54 await shared_setup(); 55 56 // Hand out 404s for crypto/keys. 57 let keysHandler = keysWBO.handler(); 58 let key404Counter = 0; 59 let keys404Handler = function (request, response) { 60 if (key404Counter > 0) { 61 let body = "Not Found"; 62 response.setStatusLine(request.httpVersion, 404, body); 63 response.bodyOutputStream.write(body, body.length); 64 key404Counter--; 65 return; 66 } 67 keysHandler(request, response); 68 }; 69 70 let collectionsHelper = track_collections_helper(); 71 let upd = collectionsHelper.with_updated_collection; 72 let handlers = { 73 "/1.1/foo/info/collections": collectionsHelper.handler, 74 "/1.1/foo/storage/meta/global": upd("meta", global.handler()), 75 "/1.1/foo/storage/crypto/keys": upd("crypto", keys404Handler), 76 "/1.1/foo/storage/clients": upd("clients", clientsColl.handler()), 77 "/1.1/foo/storage/rotary": upd("rotary", rotaryColl.handler()), 78 }; 79 80 let server = sync_httpd_setup(handlers); 81 // Do not instantiate SyncTestingInfrastructure; we need real crypto. 82 await configureIdentity({ username: "foo" }, server); 83 await Service.login(); 84 85 try { 86 _("Syncing."); 87 await sync_and_validate_telem(); 88 89 _( 90 "Partially resetting client, as if after a restart, and forcing redownload." 91 ); 92 Service.collectionKeys.clear(); 93 await engine.setLastSync(0); // So that we redownload records. 94 key404Counter = 1; 95 _("---------------------------"); 96 await sync_and_validate_telem(); 97 _("---------------------------"); 98 99 // Two rotary items, one client record... no errors. 100 Assert.equal(hmacErrorCount, 0); 101 } finally { 102 await tracker.clearChangedIDs(); 103 await Service.engineManager.unregister(engine); 104 for (const pref of Svc.PrefBranch.getChildList("")) { 105 Svc.PrefBranch.clearUserPref(pref); 106 } 107 Service.recordManager.clearCache(); 108 await promiseStopServer(server); 109 } 110 }); 111 112 add_task(async function hmac_error_during_node_reassignment() { 113 _("Attempt to replicate an HMAC error during node reassignment."); 114 let [engine, rotaryColl, clientsColl, keysWBO, global, tracker] = 115 await shared_setup(); 116 117 let collectionsHelper = track_collections_helper(); 118 let upd = collectionsHelper.with_updated_collection; 119 120 // We'll provide a 401 mid-way through the sync. This function 121 // simulates shifting to a node which has no data. 122 function on401() { 123 _("Deleting server data..."); 124 global.delete(); 125 rotaryColl.delete(); 126 keysWBO.delete(); 127 clientsColl.delete(); 128 delete collectionsHelper.collections.rotary; 129 delete collectionsHelper.collections.crypto; 130 delete collectionsHelper.collections.clients; 131 _("Deleted server data."); 132 } 133 134 let should401 = false; 135 function upd401(coll, handler) { 136 return function (request, response) { 137 if (should401 && request.method != "DELETE") { 138 on401(); 139 should401 = false; 140 let body = '"reassigned!"'; 141 response.setStatusLine(request.httpVersion, 401, "Node reassignment."); 142 response.bodyOutputStream.write(body, body.length); 143 return; 144 } 145 handler(request, response); 146 }; 147 } 148 149 let handlers = { 150 "/1.1/foo/info/collections": collectionsHelper.handler, 151 "/1.1/foo/storage/meta/global": upd("meta", global.handler()), 152 "/1.1/foo/storage/crypto/keys": upd("crypto", keysWBO.handler()), 153 "/1.1/foo/storage/clients": upd401("clients", clientsColl.handler()), 154 "/1.1/foo/storage/rotary": upd("rotary", rotaryColl.handler()), 155 }; 156 157 let server = sync_httpd_setup(handlers); 158 // Do not instantiate SyncTestingInfrastructure; we need real crypto. 159 await configureIdentity({ username: "foo" }, server); 160 161 _("Syncing."); 162 // First hit of clients will 401. This will happen after meta/global and 163 // keys -- i.e., in the middle of the sync, but before RotaryEngine. 164 should401 = true; 165 166 // Use observers to perform actions when our sync finishes. 167 // This allows us to observe the automatic next-tick sync that occurs after 168 // an abort. 169 function onSyncError() { 170 do_throw("Should not get a sync error!"); 171 } 172 let onSyncFinished = function () {}; 173 let obs = { 174 observe: function observe(subject, topic) { 175 switch (topic) { 176 case "weave:service:sync:error": 177 onSyncError(); 178 break; 179 case "weave:service:sync:finish": 180 onSyncFinished(); 181 break; 182 } 183 }, 184 }; 185 186 Svc.Obs.add("weave:service:sync:finish", obs); 187 Svc.Obs.add("weave:service:sync:error", obs); 188 189 // This kicks off the actual test. Split into a function here to allow this 190 // source file to broadly follow actual execution order. 191 async function onwards() { 192 _("== Invoking first sync."); 193 await Service.sync(); 194 _("We should not simultaneously have data but no keys on the server."); 195 let hasData = rotaryColl.wbo("flying") || rotaryColl.wbo("scotsman"); 196 let hasKeys = keysWBO.modified; 197 198 _("We correctly handle 401s by aborting the sync and starting again."); 199 Assert.equal(!hasData, !hasKeys); 200 201 _("Be prepared for the second (automatic) sync..."); 202 } 203 204 _("Make sure that syncing again causes recovery."); 205 let callbacksPromise = new Promise(resolve => { 206 onSyncFinished = function () { 207 _("== First sync done."); 208 _("---------------------------"); 209 onSyncFinished = function () { 210 _("== Second (automatic) sync done."); 211 let hasData = rotaryColl.wbo("flying") || rotaryColl.wbo("scotsman"); 212 let hasKeys = keysWBO.modified; 213 Assert.equal(!hasData, !hasKeys); 214 215 // Kick off another sync. Can't just call it, because we're inside the 216 // lock... 217 (async () => { 218 await Async.promiseYield(); 219 _("Now a fresh sync will get no HMAC errors."); 220 _( 221 "Partially resetting client, as if after a restart, and forcing redownload." 222 ); 223 Service.collectionKeys.clear(); 224 await engine.setLastSync(0); 225 hmacErrorCount = 0; 226 227 onSyncFinished = async function () { 228 // Two rotary items, one client record... no errors. 229 Assert.equal(hmacErrorCount, 0); 230 231 Svc.Obs.remove("weave:service:sync:finish", obs); 232 Svc.Obs.remove("weave:service:sync:error", obs); 233 234 await tracker.clearChangedIDs(); 235 await Service.engineManager.unregister(engine); 236 for (const pref of Svc.PrefBranch.getChildList("")) { 237 Svc.PrefBranch.clearUserPref(pref); 238 } 239 Service.recordManager.clearCache(); 240 server.stop(resolve); 241 }; 242 243 Service.sync(); 244 })().catch(console.error); 245 }; 246 }; 247 }); 248 await onwards(); 249 await callbacksPromise; 250 });