test_cache_padding.html (9640B)
1 <!-- Any copyright is dedicated to the Public Domain. 2 - http://creativecommons.org/publicdomain/zero/1.0/ --> 3 <!DOCTYPE HTML> 4 <html> 5 <head> 6 <title>Test Cache generate padding size for opaque repsonse</title> 7 <script src="/tests/SimpleTest/SimpleTest.js"></script> 8 <script type="text/javascript" src="large_url_list.js"></script> 9 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 10 </head> 11 <body> 12 <script class="testbody" type="text/javascript"> 13 function setupTestIframe() { 14 return new Promise(function(resolve) { 15 var iframe = document.createElement("iframe"); 16 iframe.src = "empty.html"; 17 iframe.onload = function() { 18 window.caches = iframe.contentWindow.caches; 19 resolve(); 20 }; 21 document.body.appendChild(iframe); 22 }); 23 } 24 25 function clearStorage() { 26 return new Promise(function(resolve) { 27 var qms = SpecialPowers.Services.qms; 28 var principal = SpecialPowers.wrap(document).nodePrincipal; 29 var request = qms.clearStoragesForPrincipal(principal); 30 var cb = SpecialPowers.wrapCallback(resolve); 31 request.callback = cb; 32 }); 33 } 34 35 function resetStorage() { 36 return new Promise(function(resolve) { 37 var qms = SpecialPowers.Services.qms; 38 var principal = SpecialPowers.wrap(document).nodePrincipal; 39 var request = qms.resetStoragesForPrincipal(principal); 40 var cb = SpecialPowers.wrapCallback(resolve); 41 request.callback = cb; 42 }); 43 } 44 45 function getStorageUsage() { 46 return new Promise(function(resolve) { 47 var qms = SpecialPowers.Services.qms; 48 var principal = SpecialPowers.wrap(document).nodePrincipal; 49 var cb = SpecialPowers.wrapCallback(function(request) { 50 var result = request.result; 51 resolve(result.usage); 52 }); 53 qms.getUsageForPrincipal(principal, cb); 54 }); 55 } 56 57 function getCachedStorageUsage() { 58 return new Promise(function(resolve) { 59 var qms = SpecialPowers.Services.qms; 60 var principal = SpecialPowers.wrap(document).nodePrincipal; 61 var cb = SpecialPowers.wrapCallback(function(request) { 62 var result = request.result; 63 resolve(result); 64 }); 65 var request = qms.getCachedUsageForPrincipal(principal); 66 request.callback = cb; 67 }); 68 } 69 70 async function verifyUsage() { 71 // This returns origin usage by calculating it from an in-memory object. 72 let memoryUsage = await getCachedStorageUsage(); 73 // This returns origin usage by calculating it from file sizes on disk. 74 let diskUsage = await getStorageUsage(); 75 76 is(memoryUsage, diskUsage, 77 "In-memory usage and disk usage should be the same."); 78 return memoryUsage; 79 } 80 81 async function waitForIOToComplete(cache, request) { 82 info("Wait for IO complete."); 83 // The following lines ensure we've deleted orphaned body. 84 // First, wait for cache operation delete the orphaned body. 85 await cache.match(request); 86 87 // Finally, wait for -wal file finish its job. 88 return resetStorage(); 89 } 90 91 function fetchOpaqueResponse(url) { 92 return fetch(url, { mode: "no-cors" }); 93 } 94 95 // This test (and the underlying implementation being tested) relies on the 96 // implementation details of sqlite. In particular, when using the obfuscating 97 // VFS in private contexts [0][1], reserving 32 bytes per page [2] for the 98 // encryption IV causes sqlite to grow the database under some conditions when 99 // the serialized security info [3] exceeds 8119 bytes. The growth size is 100 // 32KiB [4], which means that in private contexts, the given values that are 101 // supposed to be equal here will be off by that amount. 102 // [0] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/dom/cache/Manager.cpp#79 103 // [1] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/storage/mozStorageConnection.cpp#1175 104 // [2] https://searchfox.org/firefox-main/rev/1b3787c361452ef9c9e37d8ffd4c414135d547f5/storage/ObfuscatingVFS.cpp#309 105 // [3] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/security/manager/ssl/TransportSecurityInfo.cpp#114 106 // [4] https://searchfox.org/firefox-main/rev/3aff965e839b7872bf48a9803b80669ca49276ac/dom/cache/DBSchema.cpp#255 107 function equalOrOffByOneGrowthChunk(a, b, message) { 108 if (SpecialPowers.getBoolPref("browser.privatebrowsing.autostart") && a != b) { 109 b += 32768; 110 } 111 is(a, b, message); 112 } 113 114 SimpleTest.waitForExplicitFinish(); 115 SpecialPowers.pushPrefEnv({ 116 "set": [["dom.caches.enabled", true], 117 ["dom.caches.testing.enabled", true], 118 ["dom.quotaManager.testing", true]], 119 }, async function() { 120 // This test is mainly to verify we only generate different padding size for 121 // the opaque response which is comming from netwrok. 122 // Besides, this test utilizes verifyUsage() to ensure Cache Acions does 123 // update thier usage/padding size to the QM, does record padding size to 124 // the directory padding file and does do above two things synchronously. 125 // So that, opaque response's size is bigger than the normal response's size 126 // and we always have the same usage bewteen from in-memory and from 127 // the file-system. 128 // Note: For the cloned and cached opaque response, the padding size shouldn't 129 // be changed. Thus, it makes the attacker harder to get the padding size. 130 131 const name = "cachePadding"; 132 const other_name = "cachePaddingOther"; 133 const cors_base = "https://example.com/tests/dom/cache/test/mochitest/"; 134 const url = "test_cache_add.js"; 135 136 await setupTestIframe(); 137 138 info("Stage 1: Clean storage."); 139 await clearStorage(); 140 141 let cache = await caches.open(name); 142 143 // XXX This arbitrary loop is a hack to restore the same growth database 144 // behavior as it was before the schema upgrade from version 28 to 29. 145 // XXX Obviously, this test shouldn't use the total usage (which includes 146 // both the database and the file usage) to compute the disk size of 147 // responses. It should use the file usage only. The problem is that the 148 // quota client implementation currently doesn't differentiate between the 149 // database and the file usage. Even if that gets fixed, there would be a 150 // problem with checking cached usage which currently doesn't differentiate 151 // between the database usage and the file usage as well. 152 for (let i = 0; i < 19; i++) { 153 const request = new Request("https://example.com/index" + i + ".html"); 154 const response = new Response("hello world"); 155 await cache.put(request, response); 156 } 157 158 await waitForIOToComplete(cache, url); 159 let usage1 = await verifyUsage(); 160 161 info("Stage 2: Verify opaque responses have padding."); 162 cache = await caches.open(name); 163 await cache.add(url); 164 await waitForIOToComplete(cache, url); 165 let usage2 = await verifyUsage(); 166 let sizeForNormalResponse = usage2 - usage1; 167 168 let opaqueResponse = await fetchOpaqueResponse(cors_base + url); 169 cache = await caches.open(name); 170 await cache.put(cors_base + url, opaqueResponse.clone()); 171 await waitForIOToComplete(cache, url); 172 let usage3 = await verifyUsage(); 173 let sizeForOpaqueResponse = usage3 - usage2; 174 ok(sizeForOpaqueResponse > sizeForNormalResponse, 175 "The opaque response should have larger size than the normal response."); 176 177 info("Stage 3: Verify the cloned response has the same size."); 178 cache = await caches.open(name); 179 await cache.put(cors_base + url, opaqueResponse.clone()); 180 await waitForIOToComplete(cache, url); 181 let usage4 = await verifyUsage(); 182 // Since we put the same request and response again, the size should be the 183 // same (DOM Cache removes the previous cached request and response) 184 equalOrOffByOneGrowthChunk(usage4, usage3, 185 "We won't generate different padding for cloned response"); 186 187 info("Stage 4: Verify the cached response has the same size."); 188 cache = await caches.open(name); 189 opaqueResponse = await cache.match(cors_base + url); 190 ok(opaqueResponse); 191 192 await cache.put(cors_base + url, opaqueResponse); 193 await waitForIOToComplete(cache, url); 194 let usage5 = await verifyUsage(); 195 equalOrOffByOneGrowthChunk(usage5, usage3, 196 "We won't generate different padding for cached response"); 197 198 info("Stage 5: Verify padding size may changes in different fetch()s."); 199 let paddingSizeChange = false; 200 // Since we randomly generate padding size and rounding the overall size up, 201 // we will probably have the same size. So, fetch it multiple times. 202 for (let i = 0; i < 10; i++) { 203 opaqueResponse = await fetchOpaqueResponse(cors_base + url); 204 cache = await caches.open(name); 205 await cache.put(cors_base + url, opaqueResponse); 206 await waitForIOToComplete(cache, url); 207 let usage6 = await verifyUsage(); 208 if (usage6 != usage5) { 209 paddingSizeChange = true; 210 break; 211 } 212 } 213 ok(paddingSizeChange, 214 "We should generate different padding size for fetching response"); 215 216 info("Stage 6: Verify the padding is removed once on caches.delete() and " + 217 "cache.delete()."); 218 // Add an opauqe response on other cache storage and then delete that storage. 219 cache = await caches.open(other_name); 220 opaqueResponse = await fetchOpaqueResponse(cors_base + url); 221 await cache.put(cors_base + url, opaqueResponse); 222 await caches.delete(other_name); 223 await caches.has(other_name); 224 // Force remove orphaned cached in the next action 225 await resetStorage(); 226 227 // Delete the opauqe repsonse on current cache storage. 228 cache = await caches.open(name); 229 await cache.delete(cors_base + url); 230 await waitForIOToComplete(cache, url); 231 let usage7 = await verifyUsage(); 232 equalOrOffByOneGrowthChunk(usage7, usage2, 233 "The opaque response should be removed by caches.delete() and " + 234 "cache.delete()"); 235 236 await SimpleTest.finish(); 237 }); 238 </script> 239 </body> 240 </html>