|
1 // Serves a file with a given mime type and size at an optionally given rate. |
|
2 |
|
3 function getQuery(request) { |
|
4 var query = {}; |
|
5 request.queryString.split('&').forEach(function (val) { |
|
6 var [name, value] = val.split('='); |
|
7 query[name] = unescape(value); |
|
8 }); |
|
9 return query; |
|
10 } |
|
11 |
|
12 function handleResponse() { |
|
13 // Is this a rate limited response? |
|
14 if (this.state.rate > 0) { |
|
15 // Calculate how many bytes we have left to send. |
|
16 var bytesToWrite = this.state.totalBytes - this.state.sentBytes; |
|
17 |
|
18 // Do we have any bytes left to send? If not we'll just fall thru and |
|
19 // cancel our repeating timer and finalize the response. |
|
20 if (bytesToWrite > 0) { |
|
21 // Figure out how many bytes to send, based on the rate limit. |
|
22 bytesToWrite = |
|
23 (bytesToWrite > this.state.rate) ? this.state.rate : bytesToWrite; |
|
24 |
|
25 for (let i = 0; i < bytesToWrite; i++) { |
|
26 try { |
|
27 this.response.bodyOutputStream.write("0", 1); |
|
28 } catch (e) { |
|
29 // Connection was closed by client. |
|
30 if (e == Components.results.NS_ERROR_NOT_AVAILABLE) { |
|
31 // There's no harm in calling this multiple times. |
|
32 this.response.finish(); |
|
33 |
|
34 // It's possible that our timer wasn't cancelled in time |
|
35 // and we'll be called again. |
|
36 if (this.timer) { |
|
37 this.timer.cancel(); |
|
38 this.timer = null; |
|
39 } |
|
40 |
|
41 return; |
|
42 } |
|
43 } |
|
44 } |
|
45 |
|
46 // Update the number of bytes we've sent to the client. |
|
47 this.state.sentBytes += bytesToWrite; |
|
48 |
|
49 // Wait until the next call to do anything else. |
|
50 return; |
|
51 } |
|
52 } |
|
53 else { |
|
54 // Not rate limited, write it all out. |
|
55 for (let i = 0; i < this.state.totalBytes; i++) { |
|
56 this.response.write("0"); |
|
57 } |
|
58 } |
|
59 |
|
60 // Finalize the response. |
|
61 this.response.finish(); |
|
62 |
|
63 // All done sending, go ahead and cancel our repeating timer. |
|
64 this.timer.cancel(); |
|
65 |
|
66 // Clear the timer. |
|
67 this.timer = null; |
|
68 } |
|
69 |
|
70 function handleRequest(request, response) { |
|
71 var query = getQuery(request); |
|
72 |
|
73 // sending at a specific rate requires our response to be asynchronous so |
|
74 // we handle all requests asynchronously. See handleResponse(). |
|
75 response.processAsync(); |
|
76 |
|
77 // Default status when responding. |
|
78 var version = "1.1"; |
|
79 var statusCode = 200; |
|
80 var description = "OK"; |
|
81 |
|
82 // Default values for content type, size and rate. |
|
83 var contentType = "text/plain"; |
|
84 var contentRange = null; |
|
85 var size = 1024; |
|
86 var rate = 0; |
|
87 |
|
88 // optional content type to be used by our response. |
|
89 if ("contentType" in query) { |
|
90 contentType = query["contentType"]; |
|
91 } |
|
92 |
|
93 // optional size (in bytes) for generated file. |
|
94 if ("size" in query) { |
|
95 size = parseInt(query["size"]); |
|
96 } |
|
97 |
|
98 // optional range request check. |
|
99 if (request.hasHeader("range")) { |
|
100 version = "1.1"; |
|
101 statusCode = 206; |
|
102 description = "Partial Content"; |
|
103 |
|
104 // We'll only support simple range byte style requests. |
|
105 var [offset, total] = request.getHeader("range").slice("bytes=".length).split("-"); |
|
106 // Enforce valid Number values. |
|
107 offset = parseInt(offset); |
|
108 offset = isNaN(offset) ? 0 : offset; |
|
109 // Same. |
|
110 total = parseInt(total); |
|
111 total = isNaN(total) ? 0 : total; |
|
112 |
|
113 // We'll need to original total size as part of the Content-Range header |
|
114 // value in our response. |
|
115 var originalSize = size; |
|
116 |
|
117 // If we have a total size requested, we must make sure to send that number |
|
118 // of bytes only (minus the start offset). |
|
119 if (total && total < size) { |
|
120 size = total - offset; |
|
121 } else if (offset) { |
|
122 // Looks like we just have a byte offset to deal with. |
|
123 size = size - offset; |
|
124 } |
|
125 |
|
126 // We specifically need to add a Content-Range header to all responses for |
|
127 // requests that include a range request header. |
|
128 contentRange = "bytes " + offset + "-" + (size - 1) + "/" + originalSize; |
|
129 } |
|
130 |
|
131 // optional rate (in bytes/s) at which to send the file. |
|
132 if ("rate" in query) { |
|
133 rate = parseInt(query["rate"]); |
|
134 } |
|
135 |
|
136 // The context for the responseHandler. |
|
137 var context = { |
|
138 response: response, |
|
139 state: { |
|
140 contentType: contentType, |
|
141 totalBytes: size, |
|
142 sentBytes: 0, |
|
143 rate: rate |
|
144 }, |
|
145 timer: null |
|
146 }; |
|
147 |
|
148 // The notify implementation for the timer. |
|
149 context.notify = handleResponse.bind(context); |
|
150 |
|
151 context.timer = |
|
152 Components.classes["@mozilla.org/timer;1"] |
|
153 .createInstance(Components.interfaces.nsITimer); |
|
154 |
|
155 // generate the content. |
|
156 response.setStatusLine(version, statusCode, description); |
|
157 response.setHeader("Content-Type", contentType, false); |
|
158 if (contentRange) { |
|
159 response.setHeader("Content-Range", contentRange, false); |
|
160 } |
|
161 response.setHeader("Content-Length", size.toString(), false); |
|
162 |
|
163 // initialize the timer and start writing out the response. |
|
164 context.timer.initWithCallback( |
|
165 context, |
|
166 1000, |
|
167 Components.interfaces.nsITimer.TYPE_REPEATING_SLACK |
|
168 ); |
|
169 |
|
170 } |