-
-
Save kmaglione/2fa1e69008e81a681a1870015ab2d48c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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