michael@0: // Serves a file with a given mime type and size at an optionally given rate. michael@0: michael@0: function getQuery(request) { michael@0: var query = {}; michael@0: request.queryString.split('&').forEach(function (val) { michael@0: var [name, value] = val.split('='); michael@0: query[name] = unescape(value); michael@0: }); michael@0: return query; michael@0: } michael@0: michael@0: function handleResponse() { michael@0: // Is this a rate limited response? michael@0: if (this.state.rate > 0) { michael@0: // Calculate how many bytes we have left to send. michael@0: var bytesToWrite = this.state.totalBytes - this.state.sentBytes; michael@0: michael@0: // Do we have any bytes left to send? If not we'll just fall thru and michael@0: // cancel our repeating timer and finalize the response. michael@0: if (bytesToWrite > 0) { michael@0: // Figure out how many bytes to send, based on the rate limit. michael@0: bytesToWrite = michael@0: (bytesToWrite > this.state.rate) ? this.state.rate : bytesToWrite; michael@0: michael@0: for (let i = 0; i < bytesToWrite; i++) { michael@0: try { michael@0: this.response.bodyOutputStream.write("0", 1); michael@0: } catch (e) { michael@0: // Connection was closed by client. michael@0: if (e == Components.results.NS_ERROR_NOT_AVAILABLE) { michael@0: // There's no harm in calling this multiple times. michael@0: this.response.finish(); michael@0: michael@0: // It's possible that our timer wasn't cancelled in time michael@0: // and we'll be called again. michael@0: if (this.timer) { michael@0: this.timer.cancel(); michael@0: this.timer = null; michael@0: } michael@0: michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Update the number of bytes we've sent to the client. michael@0: this.state.sentBytes += bytesToWrite; michael@0: michael@0: // Wait until the next call to do anything else. michael@0: return; michael@0: } michael@0: } michael@0: else { michael@0: // Not rate limited, write it all out. michael@0: for (let i = 0; i < this.state.totalBytes; i++) { michael@0: this.response.write("0"); michael@0: } michael@0: } michael@0: michael@0: // Finalize the response. michael@0: this.response.finish(); michael@0: michael@0: // All done sending, go ahead and cancel our repeating timer. michael@0: this.timer.cancel(); michael@0: michael@0: // Clear the timer. michael@0: this.timer = null; michael@0: } michael@0: michael@0: function handleRequest(request, response) { michael@0: var query = getQuery(request); michael@0: michael@0: // sending at a specific rate requires our response to be asynchronous so michael@0: // we handle all requests asynchronously. See handleResponse(). michael@0: response.processAsync(); michael@0: michael@0: // Default status when responding. michael@0: var version = "1.1"; michael@0: var statusCode = 200; michael@0: var description = "OK"; michael@0: michael@0: // Default values for content type, size and rate. michael@0: var contentType = "text/plain"; michael@0: var contentRange = null; michael@0: var size = 1024; michael@0: var rate = 0; michael@0: michael@0: // optional content type to be used by our response. michael@0: if ("contentType" in query) { michael@0: contentType = query["contentType"]; michael@0: } michael@0: michael@0: // optional size (in bytes) for generated file. michael@0: if ("size" in query) { michael@0: size = parseInt(query["size"]); michael@0: } michael@0: michael@0: // optional range request check. michael@0: if (request.hasHeader("range")) { michael@0: version = "1.1"; michael@0: statusCode = 206; michael@0: description = "Partial Content"; michael@0: michael@0: // We'll only support simple range byte style requests. michael@0: var [offset, total] = request.getHeader("range").slice("bytes=".length).split("-"); michael@0: // Enforce valid Number values. michael@0: offset = parseInt(offset); michael@0: offset = isNaN(offset) ? 0 : offset; michael@0: // Same. michael@0: total = parseInt(total); michael@0: total = isNaN(total) ? 0 : total; michael@0: michael@0: // We'll need to original total size as part of the Content-Range header michael@0: // value in our response. michael@0: var originalSize = size; michael@0: michael@0: // If we have a total size requested, we must make sure to send that number michael@0: // of bytes only (minus the start offset). michael@0: if (total && total < size) { michael@0: size = total - offset; michael@0: } else if (offset) { michael@0: // Looks like we just have a byte offset to deal with. michael@0: size = size - offset; michael@0: } michael@0: michael@0: // We specifically need to add a Content-Range header to all responses for michael@0: // requests that include a range request header. michael@0: contentRange = "bytes " + offset + "-" + (size - 1) + "/" + originalSize; michael@0: } michael@0: michael@0: // optional rate (in bytes/s) at which to send the file. michael@0: if ("rate" in query) { michael@0: rate = parseInt(query["rate"]); michael@0: } michael@0: michael@0: // The context for the responseHandler. michael@0: var context = { michael@0: response: response, michael@0: state: { michael@0: contentType: contentType, michael@0: totalBytes: size, michael@0: sentBytes: 0, michael@0: rate: rate michael@0: }, michael@0: timer: null michael@0: }; michael@0: michael@0: // The notify implementation for the timer. michael@0: context.notify = handleResponse.bind(context); michael@0: michael@0: context.timer = michael@0: Components.classes["@mozilla.org/timer;1"] michael@0: .createInstance(Components.interfaces.nsITimer); michael@0: michael@0: // generate the content. michael@0: response.setStatusLine(version, statusCode, description); michael@0: response.setHeader("Content-Type", contentType, false); michael@0: if (contentRange) { michael@0: response.setHeader("Content-Range", contentRange, false); michael@0: } michael@0: response.setHeader("Content-Length", size.toString(), false); michael@0: michael@0: // initialize the timer and start writing out the response. michael@0: context.timer.initWithCallback( michael@0: context, michael@0: 1000, michael@0: Components.interfaces.nsITimer.TYPE_REPEATING_SLACK michael@0: ); michael@0: michael@0: }