Skip to content

Instantly share code, notes, and snippets.

@kmaglione
Created May 12, 2016 19:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kmaglione/2fa1e69008e81a681a1870015ab2d48c to your computer and use it in GitHub Desktop.
Save kmaglione/2fa1e69008e81a681a1870015ab2d48c to your computer and use it in GitHub Desktop.
/* 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/Services.jsm");
function rewind(stream) {
try {
stream.seek(0, 0);
} catch (e) {
// It might be already closed, e.g. because of a previous error.
}
}
function parseFormData(stream, channel) {
const BUFFER_SIZE = 8192; // Empirically it seemed a good compromise.
if (stream.data instanceof Ci.nsIInputStream) {
stream = stream.data;
}
let multiplex = null;
if (stream instanceof Ci.nsIMultiplexInputStream) {
multiplex = 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, textStream.DEFAULT_REPLACEMENT_CHARACTER);
if (stream instanceof Ci.nsISeekableStream) {
touchedStreams.add(stream);
}
return textStream;
}
let streamIdx = 0;
function nextTextStream() {
for (; streamIdx < multiplex.count;) {
let currentStream = multiplex.getStream(streamIdx++);
if (currentStream instanceof Ci.nsIStringInputStream) {
touchedStreams.add(multiplex);
return createTextStream(currentStream);
}
}
return null;
}
let textStream;
if (multiplex) {
textStream = nextTextStream();
} else {
textStream = createTextStream(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 (multiplex) {
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) {
let formData = Object.create(null);
let 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="(|(?:.*?)[^\\])"(?:;\s*filename="(|(?:.*?)[^\\])"|[^;])\r?\n(?:Content-Type: (\S+))?.*\r?\n/i);
if (!match) {
continue;
}
let [header, name, fileName, contentType] = match;
name = unslash(name);
if (contentType) {
appendFormData(formData, name, fileName ? unslash(fileName) : "");
} else {
appendFormData(formData, 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 (multiplex) {
touchedStreams.add(multiplex);
return parseMultiPart(chunk);
} else {
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[2];
if (boundary) {
return parseMultiPart(chunk);
} else {
return parseUrlEncoded(chunk);
}
}
}
} finally {
for (let stream of touchedStreams) {
rewind(stream);
}
}
return null;
}
function createFormData(stream, channel) {
try {
rewind(stream);
return parseFormData(stream.unbufferedStream || stream, channel);
} catch (e) {
Cu.reportError(e);
} finally {
rewind(stream);
}
return null;
}
function convertRawData(outerStream) {
const MAX_BYTES = Services.prefs.getIntPref("webextensions.webRequest.requestBodyMaxRawBytes");
let raw = [];
let totalBytes = 0;
// Here we read the stream up to MAX_BYTES, returning true 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;
}
rewind(stream);
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
binaryStream.setInputStream(stream);
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;
return true;
}
break;
}
}
} finally {
rewind(stream);
}
return false;
}
let unbuffered = outerStream;
if (outerStream instanceof Ci.nsIStreamBufferAccess) {
unbuffered = outerStream.unbufferedStream;
}
if (unbuffered instanceof Ci.nsIMultiplexInputStream) {
for (let j = 0, count = unbuffered.count; j < count; j++) {
if (readAll(unbuffered.getStream(j))) {
break;
}
}
} else {
readAll(outerStream);
}
return raw;
}
function createGetter(weakChannelRef) {
let requestBody;
return () => {
if (!requestBody) {
try {
let channel = weakChannelRef.get();
if (channel instanceof Ci.nsIUploadChannel && channel.uploadStream) {
let stream = channel.uploadStream.QueryInterface(Ci.nsISeekableStream);
let formData = createFormData(stream, channel);
requestBody = formData ? {formData} : {raw: convertRawData(stream)};
} else {
throw new Error("Upload data not available anymore");
}
} catch (e) {
Cu.reportError(e);
requestBody = {error: e.message || String(e)};
}
requestBody = Object.freeze(requestBody);
}
return requestBody;
};
}
var WebRequestUpload = {
createGetter(channel) {
if (channel instanceof Ci.nsIUploadChannel && channel.uploadStream) {
return createGetter(Cu.getWeakReference(channel));
}
return null;
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment