diff options
Diffstat (limited to 'toolkit/modules/addons/WebRequestUpload.jsm')
-rw-r--r-- | toolkit/modules/addons/WebRequestUpload.jsm | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/toolkit/modules/addons/WebRequestUpload.jsm b/toolkit/modules/addons/WebRequestUpload.jsm new file mode 100644 index 000000000..789ce683f --- /dev/null +++ b/toolkit/modules/addons/WebRequestUpload.jsm @@ -0,0 +1,321 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["WebRequestUpload"]; + +/* exported WebRequestUpload */ + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var WebRequestUpload; + +function rewind(stream) { + try { + if (stream instanceof Ci.nsISeekableStream) { + stream.seek(0, 0); + } + } catch (e) { + // It might be already closed, e.g. because of a previous error. + } +} + +function parseFormData(stream, channel, lenient = false) { + const BUFFER_SIZE = 8192; // Empirically it seemed a good compromise. + + let mimeStream = null; + + if (stream instanceof Ci.nsIMIMEInputStream && stream.data) { + mimeStream = stream; + stream = stream.data; + } + let multiplexStream = null; + if (stream instanceof Ci.nsIMultiplexInputStream) { + multiplexStream = stream; + } + + let touchedStreams = new Set(); + + function createTextStream(stream) { + let textStream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); + textStream.init(stream, "UTF-8", 0, lenient ? textStream.DEFAULT_REPLACEMENT_CHARACTER : 0); + if (stream instanceof Ci.nsISeekableStream) { + touchedStreams.add(stream); + } + return textStream; + } + + let streamIdx = 0; + function nextTextStream() { + for (; streamIdx < multiplexStream.count;) { + let currentStream = multiplexStream.getStream(streamIdx++); + if (currentStream instanceof Ci.nsIStringInputStream) { + touchedStreams.add(multiplexStream); + return createTextStream(currentStream); + } + } + return null; + } + + let textStream; + if (multiplexStream) { + textStream = nextTextStream(); + } else { + textStream = createTextStream(mimeStream || stream); + } + + if (!textStream) { + return null; + } + + function readString() { + if (textStream) { + let textBuffer = {}; + textStream.readString(BUFFER_SIZE, textBuffer); + return textBuffer.value; + } + return ""; + } + + function multiplexRead() { + let str = readString(); + if (!str) { + textStream = nextTextStream(); + if (textStream) { + str = multiplexRead(); + } + } + return str; + } + + let readChunk; + if (multiplexStream) { + readChunk = multiplexRead; + } else { + readChunk = readString; + } + + function appendFormData(formData, name, value) { + if (name in formData) { + formData[name].push(value); + } else { + formData[name] = [value]; + } + } + + function parseMultiPart(firstChunk, boundary = "") { + let formData = Object.create(null); + + if (!boundary) { + let match = firstChunk.match(/^--\S+/); + if (!match) { + return null; + } + boundary = match[0]; + } + + let unslash = (s) => s.replace(/\\"/g, '"'); + let tail = ""; + for (let chunk = firstChunk; + chunk || tail; + chunk = readChunk()) { + let parts; + if (chunk) { + chunk = tail + chunk; + parts = chunk.split(boundary); + tail = parts.pop(); + } else { + parts = [tail]; + tail = ""; + } + + for (let part of parts) { + let match = part.match(/^\r\nContent-Disposition: form-data; name="(.*)"\r\n(?:Content-Type: (\S+))?.*\r\n/i); + if (!match) { + continue; + } + let [header, name, contentType] = match; + if (contentType) { + let fileName; + // Since escaping inside Content-Disposition subfields is still poorly defined and buggy (see Bug 136676), + // currently we always consider backslash-prefixed quotes as escaped even if that's not generally true + // (i.e. in a field whose value actually ends with a backslash). + // Therefore in this edge case we may end coalescing name and filename, which is marginally better than + // potentially truncating the name field at the wrong point, at least from a XSS filter POV. + match = name.match(/^(.*[^\\])"; filename="(.*)/); + if (match) { + [, name, fileName] = match; + } + appendFormData(formData, unslash(name), fileName ? unslash(fileName) : ""); + } else { + appendFormData(formData, unslash(name), part.slice(header.length, -2)); + } + } + } + + return formData; + } + + function parseUrlEncoded(firstChunk) { + let formData = Object.create(null); + + let tail = ""; + for (let chunk = firstChunk; + chunk || tail; + chunk = readChunk()) { + let pairs; + if (chunk) { + chunk = tail + chunk.trim(); + pairs = chunk.split("&"); + tail = pairs.pop(); + } else { + chunk = tail; + tail = ""; + pairs = [chunk]; + } + for (let pair of pairs) { + let [name, value] = pair.replace(/\+/g, " ").split("=").map(decodeURIComponent); + appendFormData(formData, name, value); + } + } + + return formData; + } + + try { + let chunk = readChunk(); + + if (multiplexStream) { + touchedStreams.add(multiplexStream); + return parseMultiPart(chunk); + } + let contentType; + if (/^Content-Type:/i.test(chunk)) { + contentType = chunk.replace(/^Content-Type:\s*/i, ""); + chunk = chunk.slice(chunk.indexOf("\r\n\r\n") + 4); + } else { + try { + contentType = channel.getRequestHeader("Content-Type"); + } catch (e) { + Cu.reportError(e); + return null; + } + } + + let match = contentType.match(/^(?:multipart\/form-data;\s*boundary=(\S*)|application\/x-www-form-urlencoded\s)/i); + if (match) { + let boundary = match[1]; + if (boundary) { + return parseMultiPart(chunk, boundary); + } + return parseUrlEncoded(chunk); + } + } finally { + for (let stream of touchedStreams) { + rewind(stream); + } + } + + return null; +} + +function createFormData(stream, channel) { + try { + rewind(stream); + return parseFormData(stream, channel); + } catch (e) { + Cu.reportError(e); + } finally { + rewind(stream); + } + return null; +} + +function convertRawData(outerStream) { + let raw = []; + let totalBytes = 0; + + // Here we read the stream up to WebRequestUpload.MAX_RAW_BYTES, returning false if we had to truncate the result. + function readAll(stream) { + let unbuffered = stream.unbufferedStream || stream; + if (unbuffered instanceof Ci.nsIFileInputStream) { + raw.push({file: "<file>"}); // Full paths not supported yet for naked files (follow up bug) + return true; + } + rewind(stream); + + let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + binaryStream.setInputStream(stream); + const MAX_BYTES = WebRequestUpload.MAX_RAW_BYTES; + try { + for (let available; (available = binaryStream.available());) { + let size = Math.min(MAX_BYTES - totalBytes, available); + let bytes = new ArrayBuffer(size); + binaryStream.readArrayBuffer(size, bytes); + let chunk = {bytes}; + raw.push(chunk); + totalBytes += size; + + if (totalBytes >= MAX_BYTES) { + if (size < available) { + chunk.truncated = true; + chunk.originalSize = available; + return false; + } + break; + } + } + } finally { + rewind(stream); + } + return true; + } + + let unbuffered = outerStream; + if (outerStream instanceof Ci.nsIStreamBufferAccess) { + unbuffered = outerStream.unbufferedStream; + } + + if (unbuffered instanceof Ci.nsIMultiplexInputStream) { + for (let i = 0, count = unbuffered.count; i < count; i++) { + if (!readAll(unbuffered.getStream(i))) { + break; + } + } + } else { + readAll(outerStream); + } + + return raw; +} + +WebRequestUpload = { + createRequestBody(channel) { + let requestBody = null; + if (channel instanceof Ci.nsIUploadChannel && channel.uploadStream) { + try { + let stream = channel.uploadStream.QueryInterface(Ci.nsISeekableStream); + let formData = createFormData(stream, channel); + if (formData) { + requestBody = {formData}; + } else { + requestBody = {raw: convertRawData(stream), lenientFormData: createFormData(stream, channel, true)}; + } + } catch (e) { + Cu.reportError(e); + requestBody = {error: e.message || String(e)}; + } + requestBody = Object.freeze(requestBody); + } + return requestBody; + }, +}; + +XPCOMUtils.defineLazyPreferenceGetter(WebRequestUpload, "MAX_RAW_BYTES", "webextensions.webRequest.requestBodyMaxRawBytes"); |