server-stream-download.sjs (3959B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { setInterval, clearInterval } = ChromeUtils.importESModule( 5 "resource://gre/modules/Timer.sys.mjs" 6 ); 7 8 // stolen from file_blocked_script.sjs 9 function setGlobalState(data, key) { 10 x = { 11 data, 12 QueryInterface(iid) { 13 return this; 14 }, 15 }; 16 x.wrappedJSObject = x; 17 setObjectState(key, x); 18 } 19 20 function getGlobalState(key) { 21 var data; 22 getObjectState(key, function (x) { 23 data = x && x.wrappedJSObject.data; 24 }); 25 return data; 26 } 27 28 /* 29 * We want to let the sw_download_canceled.js service worker know when the 30 * stream was canceled. To this end, we let it issue a monitor request which we 31 * fulfill when the stream has been canceled. In order to coordinate between 32 * multiple requests, we use the getObjectState/setObjectState mechanism that 33 * httpd.js exposes to let data be shared and/or persist between requests. We 34 * handle both possible orderings of the requests because we currently don't 35 * try and impose an ordering between the two requests as issued by the SW, and 36 * file_blocked_script.sjs encourages us to do this, but we probably could order 37 * them. 38 */ 39 const MONITOR_KEY = "stream-monitor"; 40 function completeMonitorResponse(response, data) { 41 response.write(JSON.stringify(data)); 42 response.finish(); 43 } 44 function handleMonitorRequest(request, response) { 45 response.setHeader("Content-Type", "application/json"); 46 response.setStatusLine(request.httpVersion, 200, "Found"); 47 48 response.processAsync(); 49 // Necessary to cause the headers to be flushed; that or touching the 50 // bodyOutputStream getter. 51 response.write(""); 52 dump("server-stream-download.js: monitor headers issued\n"); 53 54 const alreadyCompleted = getGlobalState(MONITOR_KEY); 55 if (alreadyCompleted) { 56 completeMonitorResponse(response, alreadyCompleted); 57 setGlobalState(null, MONITOR_KEY); 58 } else { 59 setGlobalState(response, MONITOR_KEY); 60 } 61 } 62 63 const MAX_TICK_COUNT = 3000; 64 const TICK_INTERVAL = 2; 65 function handleStreamRequest(request, response) { 66 const name = "server-stream-download"; 67 68 // Create some payload to send. 69 let strChunk = 70 "Static routes are the future of ServiceWorkers! So say we all!\n"; 71 while (strChunk.length < 1024) { 72 strChunk += strChunk; 73 } 74 75 response.setHeader("Content-Disposition", `attachment; filename="${name}"`); 76 response.setHeader( 77 "Content-Type", 78 `application/octet-stream; name="${name}"` 79 ); 80 response.setHeader("Content-Length", `${strChunk.length * MAX_TICK_COUNT}`); 81 response.setStatusLine(request.httpVersion, 200, "Found"); 82 83 response.processAsync(); 84 response.write(strChunk); 85 dump("server-stream-download.js: stream headers + first payload issued\n"); 86 87 let count = 0; 88 let intervalId; 89 function closeStream(why, message) { 90 dump("server-stream-download.js: closing stream: " + why + "\n"); 91 clearInterval(intervalId); 92 response.finish(); 93 94 const data = { why, message }; 95 const monitorResponse = getGlobalState(MONITOR_KEY); 96 if (monitorResponse) { 97 completeMonitorResponse(monitorResponse, data); 98 setGlobalState(null, MONITOR_KEY); 99 } else { 100 setGlobalState(data, MONITOR_KEY); 101 } 102 } 103 function tick() { 104 try { 105 // bound worst-case behavior. 106 if (count++ > MAX_TICK_COUNT) { 107 closeStream("timeout", "timeout"); 108 return; 109 } 110 response.write(strChunk); 111 } catch (e) { 112 closeStream("canceled", e.message); 113 } 114 } 115 intervalId = setInterval(tick, TICK_INTERVAL); 116 } 117 118 function handleRequest(request, response) { 119 dump( 120 "server-stream-download.js: processing request for " + 121 request.path + 122 "?" + 123 request.queryString + 124 "\n" 125 ); 126 const query = new URLSearchParams(request.queryString); 127 if (query.has("monitor")) { 128 handleMonitorRequest(request, response); 129 } else { 130 handleStreamRequest(request, response); 131 } 132 }