322 lines
8.6 KiB
JavaScript
322 lines
8.6 KiB
JavaScript
/* 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");
|