diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 06:29:37 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 06:29:37 +0000 |
commit | 9355e23a909a7801b3ccdf68ee05b3480be42407 (patch) | |
tree | 78220623341b88bd42d16929e2acb3e5ef52e0c8 /content/HttpRequest.jsm | |
parent | Initial commit. (diff) | |
download | tbsync-9355e23a909a7801b3ccdf68ee05b3480be42407.tar.xz tbsync-9355e23a909a7801b3ccdf68ee05b3480be42407.zip |
Adding upstream version 4.7.upstream/4.7
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'content/HttpRequest.jsm')
-rw-r--r-- | content/HttpRequest.jsm | 755 |
1 files changed, 755 insertions, 0 deletions
diff --git a/content/HttpRequest.jsm b/content/HttpRequest.jsm new file mode 100644 index 0000000..fa28f97 --- /dev/null +++ b/content/HttpRequest.jsm @@ -0,0 +1,755 @@ +/* + * This file is part of TbSync, contributed by John Bieling. + * + * 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/. + * + * Limitations: + * ============ + * - no real event support (cannot add eventlisteners) + * - send only supports string body + * - onprogress not supported + * - readyState 2 & 3 not supported + * + * Note about HttpRequest.open(method, url, async, username, password): + * ============================================================================ + * If an Authorization header is specified, HttpRequest will use the + * given header. + * + * If no Authorization header is specified, but a username, HttpRequest + * will delegate the authentication process to nsIHttpChannel. If a password is + * specified as well, it will be used for authentication. If no password is + * specified, it will call the passwordCallback(username, realm, host) callback to + * request a password for the given username, host and realm send back from + * the server (in the WWW-Authenticate header). + * + */ + + "use strict"; + +var EXPORTED_SYMBOLS = ["HttpRequest"]; + +var bug669675 = []; +var containers = []; +var sandboxes = {}; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var HttpRequest = class { + constructor() { + // a private object to store xhr related properties + this._xhr = {}; + + // HttpRequest supports two methods to receive data, using the + // streamLoader seems to be the more modern approach. + // BUT in order to overide MimeType, we need to call onStartRequest + this._xhr.useStreamLoader = false; + this._xhr.responseAsBase64 = false; + + this._xhr.loadFlags = Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; + this._xhr.headers = {}; + this._xhr.readyState = 0; + this._xhr.responseStatus = null; + this._xhr.responseStatusText = null; + this._xhr.responseText = null; + this._xhr.httpchannel = null; + this._xhr.method = null; + this._xhr.uri = null; + this._xhr.permanentlyRedirectedUrl = null; + this._xhr.username = ""; + this._xhr.password = ""; + this._xhr.overrideMimeType = null; + this._xhr.mozAnon = false; + this._xhr.mozBackgroundRequest = false; + this._xhr.timeout = 0; + this._xhr.redirectFlags = null; + this._xhr.containerReset = false; + this._xhr.containerRealm = "default"; + + this.onreadystatechange = function () {}; + this.onerror = function () {}; + this.onload = function () {}; + this.ontimeout = function () {}; + + // Redirects are handled internally, this callback is just called to + // inform the caller about the redirect. + // Flags: (https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIChannelEventSink) + // - REDIRECT_TEMPORARY = 1 << 0; + // - REDIRECT_PERMANENT = 1 << 1; + // - REDIRECT_INTERNAL = 1 << 2; + // TB enforces a redirect from http -> https without having received a redirect, + // but an STS header + // - REDIRECT_STS_UPGRADE = 1 << 3; + // This is a custom bit set by HttpRequest to indicate, that this redirect was missed by the nsIHttpChannel implementation + // probably due to a CORS violation (like https -> http) + // - REDIRECT_MISSED = 1 << 7; + this.onredirect = function(flags, newUri) {}; + + // Whenever a WWW-Authenticate header has been parsed, this callback is + // called to inform the caller about the found realm. + this.realmCallback = function (username, realm, host) {}; + + // Whenever a channel needs authentication, but the caller has only provided a username + // this callback is called to request the password. + this.passwordCallback = function (username, realm, host) {return null}; + + var self = this; + + this.notificationCallbacks = { + // nsIInterfaceRequestor + getInterface : function(aIID) { + if (aIID.equals(Components.interfaces.nsIAuthPrompt2)) { + // implement a custom nsIAuthPrompt2 - needed for auto authorization + if (!self._xhr.authPrompt) { + self._xhr.authPrompt = new HttpRequestPrompt(self._xhr.username, self._xhr.password, self.passwordCallback, self.realmCallback); + } + return self._xhr.authPrompt; + } else if (aIID.equals(Components.interfaces.nsIAuthPrompt)) { + // implement a custom nsIAuthPrompt + } else if (aIID.equals(Components.interfaces.nsIAuthPromptProvider)) { + // implement a custom nsIAuthPromptProvider + } else if (aIID.equals(Components.interfaces.nsIPrompt)) { + // implement a custom nsIPrompt + } else if (aIID.equals(Components.interfaces.nsIProgressEventSink)) { + // implement a custom nsIProgressEventSink + } else if (aIID.equals(Components.interfaces.nsIChannelEventSink)) { + // implement a custom nsIChannelEventSink + return self.redirect; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + }; + + this.redirect = { + // nsIChannelEventSink implementation + asyncOnChannelRedirect: function(aOldChannel, aNewChannel, aFlags, aCallback) { + // Disallow redirects from https to http. + if (aOldChannel.URI.scheme == "https" && aNewChannel.URI.scheme == "http") { + // Using an unused error code according to https://developer.mozilla.org/en-US/docs/Mozilla/Errors. + // REJECTED_REDIRECT_FROM_HTTPS_TO_HTTP' + aCallback.onRedirectVerifyCallback(0x804B002F); + return; + } + + let uploadData; + let uploadContent; + if (aOldChannel instanceof Ci.nsIUploadChannel && + aOldChannel instanceof Ci.nsIHttpChannel && + aOldChannel.uploadStream) { + uploadData = aOldChannel.uploadStream; + uploadContent = aOldChannel.getRequestHeader("Content-Type"); + } + + aNewChannel.QueryInterface(Ci.nsIHttpChannel); + aOldChannel.QueryInterface(Ci.nsIHttpChannel); + + function copyHeader(aHdr) { + try { + let hdrValue = aOldChannel.getRequestHeader(aHdr); + if (hdrValue) { + aNewChannel.setRequestHeader(aHdr, hdrValue, false); + } + } catch (e) { + if (e.code != Components.results.NS_ERROR_NOT_AVAILIBLE) { + // The header could possibly not be available, ignore that + // case but throw otherwise + throw e; + } + } + } + + // Copy manually added headers + for (let header in self._xhr.headers) { + if (self._xhr.headers.hasOwnProperty(header)) { + copyHeader(header); + } + } + + prepHttpChannelUploadData( + aNewChannel, + aOldChannel.requestMethod, + uploadData, + uploadContent); + + self._xhr.redirectFlags = aFlags; + if (aFlags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) { + self._xhr.permanentlyRedirectedUrl = aNewChannel.URI.spec; + } + self.onredirect(aFlags, aNewChannel.URI); + aCallback.onRedirectVerifyCallback(Components.results.NS_OK); + } + }; + + this.listener = { + _buffer: [], + + //nsIStreamListener (aUseStreamLoader = false) + onStartRequest: function(aRequest) { + //Services.console.logStringMessage("[onStartRequest] " + aRequest.URI.spec); + this._buffer = []; + + if (self._xhr.overrideMimeType) { + aRequest.contentType = self._xhr.overrideMimeType; + } + }, + onDataAvailable: function (aRequest, aInputStream, aOffset, aCount) { + //Services.console.logStringMessage("[onDataAvailable] " + aRequest.URI.spec + " : " + aCount); + let buffer = new ArrayBuffer(aCount); + let stream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream); + stream.setInputStream(aInputStream); + stream.readArrayBuffer(aCount, buffer); + + // store the chunk + this._buffer.push(Array.from(new Uint8Array(buffer))); + }, + onStopRequest: function(aRequest, aStatusCode) { + //Services.console.logStringMessage("[onStopRequest] " + aRequest.URI.spec + " : " + aStatusCode); + // combine all binary chunks to create a flat byte array; + let combined = [].concat.apply([], this._buffer); + let data = convertByteArray(combined, self.responseAsBase64); + this.processResponse(aRequest.QueryInterface(Components.interfaces.nsIHttpChannel), aStatusCode, data); + }, + + + + //nsIStreamLoaderObserver (aUseStreamLoader = true) + onStreamComplete: function(aLoader, aContext, aStatus, aResultLength, aResult) { + let result = convertByteArray(aResult, self.responseAsBase64); + this.processResponse(aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel), aStatus, result); + }, + + processResponse: function(aChannel, aStatus, aResult) { + //Services.console.logStringMessage("[processResponse] " + aChannel.URI.spec + " : " + aStatus); + // do not set any channal response data, before we know we failed + // and before we know we do not have to rerun (due to bug 669675) + + let responseStatus = null; + try { + responseStatus = aChannel.responseStatus; + } catch (ex) { + switch (aStatus) { + case Components.results.NS_ERROR_NET_TIMEOUT: + self._xhr.httpchannel = aChannel; + self._xhr.responseText = aResult; + self._xhr.responseStatus = 0; + self._xhr.responseStatusText = ""; + self._xhr.readyState = 4; + self.onreadystatechange(); + self.ontimeout(); + return; + case Components.results.NS_BINDING_ABORTED: + case 0x804B002F: //Custom error (REJECTED_REDIRECT_FROM_HTTPS_TO_HTTP') + self._xhr.httpchannel = aChannel; + self._xhr.responseText = aResult; + self._xhr.responseStatus = 0; + self._xhr.responseStatusText = ""; + self._xhr.readyState = 0; + self.onreadystatechange(); + self.onerror(); + return; + case 0x805303F4: //NS_ERROR_DOM_BAD_URI + // Error on Strict-Transport-Security induced Redirect http -> https (these do not even show up in the console) + // The redirect has been done already and the channel is already the new channel. + // Due to CORS violation, it cannot be fulfilled. + if (self._xhr.redirectFlags && aChannel.URI.spec != self._xhr.uri.spec) { + self._xhr.uri = aChannel.URI; + self.send(self._xhr.data); + return; + } + default: + self._xhr.httpchannel = aChannel; + self._xhr.responseText = aResult; + self._xhr.responseStatus = 0; + self._xhr.responseStatusText = ""; + self._xhr.readyState = 4; + self.onreadystatechange(); + self.onerror(); + return; + } + } + + // Usually redirects are handled internally, but any CORS violating request is + // returning a 30x, if CORS is not allowed. This is not true for STS induced redirects (see above). + if ([301,302,307,308].includes(responseStatus)) { + // aChannel is still the old channel + let redirected = self.getResponseHeader("location"); + if (redirected && redirected != aChannel.URI.spec) { + let flag = Ci.nsIChannelEventSink.REDIRECT_TEMPORARY; + let uri = Services.io.newURI(redirected); + if ([301,308].includes(responseStatus)) { + flag = Ci.nsIChannelEventSink.REDIRECT_PERMANENT; + self._xhr.permanentlyRedirectedUrl = uri.spec; + } + // inform caller about the redirect and set the REDIRECT_MISSED bit + + self.onredirect(flag | 0x80, uri); + self._xhr.uri = uri; + self.send(self._xhr.data); + return; + } + } + + // mitigation for bug https://bugzilla.mozilla.org/show_bug.cgi?id=669675 + // we need to check, if nsIHttpChannel was in charge of auth: + // if there was no Authentication header provided by the user, but a username + // nsIHttpChannel should have added one. Is there one? + if ( + (responseStatus == 401) && + !self._xhr.mozAnon && + !self.hasRequestHeader("Authorization") && // no user defined header, so nsIHttpChannel should have called the authPrompt + self._xhr.username && // we can only add basic auth header if user + self._xhr.password // and pass are present + ) { + // check the actual Authorization headers send + let unauthenticated; + try { + let header = aChannel.getRequestHeader("Authorization"); + unauthenticated = false; + } catch (e) { + unauthenticated = true; + } + + if (unauthenticated) { + if (!bug669675.includes(self._xhr.uri.spec)) { + bug669675.push(self._xhr.uri.spec) + console.log("Mitigation for bug 669675 for URL <"+self._xhr.uri.spec+"> (Once per URL per session)"); + // rerun + self.send(self._xhr.data); + return; + } else { + console.log("Mitigation failed for URL <"+self._xhr.uri.spec+">"); + } + } + } + + self._xhr.httpchannel = aChannel; + self._xhr.responseText = aResult; + self._xhr.responseStatus = responseStatus; + self._xhr.responseStatusText = aChannel.responseStatusText; + self._xhr.readyState = 4; + self.onreadystatechange(); + self.onload(); + } + }; + } + + + + + /** public **/ + + open(method, url, async = true, username = "", password = "") { + this._xhr.method = method; + + try { + this._xhr.uri = Services.io.newURI(url); + } catch (e) { + Components.utils.reportError(e); + throw new Error("HttpRequest: Invalid URL <"+url+">"); + } + if (!async) throw new Error ("HttpRequest: Synchronous requests not implemented."); + + this._xhr.username = username; + this._xhr.password = password; + + this._xhr.readyState = 1; + this.onreadystatechange(); + + } + + // must be called after open, before send + setContainerRealm(v) { + this._xhr.containerRealm = v; + } + + // must be called after open, before send + clearContainerCache() { + this._xhr.containerReset = true; + } + + send(data) { + //store the data, so we can rerun + this._xhr.data = data; + this._xhr.redirectFlags = null; + + // The sandbox will have a loadingNode + let sandbox = getSandboxForOrigin(this._xhr.username, this._xhr.uri, this._xhr.containerRealm, this._xhr.containerReset); + + // The XHR in the sandbox will have the correct loadInfo, which will allow us + // to use cookies and a CodebasePrincipal for us to use userContextIds and to + // contact nextcloud servers (error 503). + // We will not use the XHR or the sandbox itself. + let XHR = new sandbox.XMLHttpRequest(); + XHR.open(this._xhr.method, this._xhr.uri.spec); + + // Create the channel with the loadInfo from the sandboxed XHR + let channel = Services.io.newChannelFromURIWithLoadInfo(this._xhr.uri, XHR.channel.loadInfo); + + /* + // as of TB67 newChannelFromURI needs to specify a loading node to have access to the cookie jars + // using the main window + // another option would be workers + let options = {}; + let mainWindow = Services.wm.getMostRecentWindow("mail:3pane"); + + let channel = Services.io.newChannelFromURI( + this._xhr.uri, + mainWindow.document, + Services.scriptSecurityManager.createContentPrincipal(this._xhr.uri, options), + null, + Components.interfaces.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Components.interfaces.nsIContentPolicy.TYPE_OTHER); + */ + +/* + // enforce anonymous access if requested (will not work with proxy, see MDN) + if (this._xhr.mozAnon) { + channel.loadFlag |= Components.interfaces.nsIRequest.LOAD_ANONYMOUS; + } + + // set background request + if (this._xhr.mozBackgroundRequest) { + channel.loadFlag |= Components.interfaces.nsIRequest.LOAD_BACKGROUND; + } +*/ + this._xhr.httpchannel = channel.QueryInterface(Components.interfaces.nsIHttpChannel); + this._xhr.httpchannel.loadFlags |= this._xhr.loadFlags; + this._xhr.httpchannel.notificationCallbacks = this.notificationCallbacks; + + // Set default content type. + if (!this.hasRequestHeader("Content-Type")) { + this.setRequestHeader("Content-Type", "application/xml; charset=utf-8") + } + + // Set default accept value. + if (!this.hasRequestHeader("Accept")) { + this.setRequestHeader("Accept", "*/*"); + } + + // Set non-standard header to request authorization (https://github.com/jobisoft/DAV-4-TbSync/issues/106) + if (this._xhr.username) { + this.setRequestHeader("X-EnforceAuthentication", "True"); + } + + // calculate length of request and add header + if (data) { + let textEncoder = new TextEncoder(); + let encoded = textEncoder.encode(data); + this.setRequestHeader("Content-Length", encoded.length); + } + + // mitigation for bug 669675 + if ( + bug669675.includes(this._xhr.uri.spec) && + !this._xhr.mozAnon && + !this.hasRequestHeader("Authorization") && + this._xhr.username && + this._xhr.password + ) { + this.setRequestHeader("Authorization", "Basic " + b64EncodeUnicode(this._xhr.username + ':' + this._xhr.password)); + } + + // add all headers to the channel + for (let header in this._xhr.headers) { + if (this._xhr.headers.hasOwnProperty(header)) { + this._xhr.httpchannel.setRequestHeader(header, this._xhr.headers[header], false); + } + } + + // Will overwrite the Content-Type, so it must be called after the headers have been set. + prepHttpChannelUploadData(this._xhr.httpchannel, this._xhr.method, data, this.getRequestHeader("Content-Type")); + + if (this._xhr.useStreamLoader) { + let loader = Components.classes["@mozilla.org/network/stream-loader;1"].createInstance(Components.interfaces.nsIStreamLoader); + loader.init(this.listener); + this.listener = loader; + } + + this._startTimeout(); + this._xhr.httpchannel.asyncOpen(this.listener, this._xhr.httpchannel); + } + + get readyState() { return this._xhr.readyState; } + get responseURI() { return this._xhr.httpchannel.URI; } + get responseURL() { return this._xhr.httpchannel.URI.spec; } + get permanentlyRedirectedUrl() { return this._xhr.permanentlyRedirectedUrl; } + get responseText() { return this._xhr.responseText; } + get status() { return this._xhr.responseStatus; } + get statusText() { return this._xhr.responseStatusText; } + get channel() { return this._xhr.httpchannel; } + get loadFlags() { return this._xhr.loadFlags; } + get timeout() { return this._xhr.timeout; } + get mozBackgroundRequest() { return this._xhr.mozBackgroundRequest; } + get mozAnon() { return this._xhr.mozAnon; } + + set loadFlags(v) { this._xhr.loadFlags = v; } + set timeout(v) { this._xhr.timeout = v; } + set mozBackgroundRequest(v) { this._xhr.mozBackgroundRequest = (v === true); } + set mozAnon(v) { this._xhr.mozAnon = (v === true); } + + + // case insensitive method to check for headers + hasRequestHeader(header) { + let lowHeaders = Object.keys(this._xhr.headers).map(x => x.toLowerCase()); + return lowHeaders.includes(header.toLowerCase()); + } + + // if a header exists (case insensitive), it will be replaced (keeping the original capitalization) + setRequestHeader(header, value) { + let useHeader = header; + let lowHeader = header.toLowerCase(); + + for (let h in this._xhr.headers) { + if (this._xhr.headers.hasOwnProperty(h) && h.toLowerCase() == lowHeader) { + useHeader = h; + break; + } + } + this._xhr.headers[useHeader] = value; + } + + // checks if a header (case insensitive) has been set by setRequestHeader - that does not mean it has been added to the channel! + getRequestHeader(header) { + let lowHeader = header.toLowerCase(); + + for (let h in this._xhr.headers) { + if (this._xhr.headers.hasOwnProperty(h) && h.toLowerCase() == lowHeader) { + return this._xhr.headers[h]; + } + } + return null; + } + + getResponseHeader(header) { + try { + return this._xhr.httpchannel.getResponseHeader(header); + } catch (e) { + if (e.code != Components.results.NS_ERROR_NOT_AVAILIBLE) { + // The header could possibly not be available, ignore that + // case but throw otherwise + throw e; + } + } + return null; + } + + overrideMimeType(mime) { + this._xhr.overrideMimeType = mime; + } + + abort() { + this._cancel(Components.results.NS_BINDING_ABORTED); + } + + get responseAsBase64() { return this._xhr.responseAsBase64; } + set responseAsBase64(v) { this._xhr.responseAsBase64 = (v == true);} + + /* not used */ + + get responseXML() { throw new Error("HttpRequest: responseXML not implemented"); } + + get response() { throw new Error("HttpRequest: response not implemented"); } + set response(v) { throw new Error("HttpRequest: response not implemented"); } + + get responseType() { throw new Error("HttpRequest: response not implemented"); } + set responseType(v) { throw new Error("HttpRequest: response not implemented"); } + + get upload() { throw new Error("HttpRequest: upload not implemented"); } + set upload(v) { throw new Error("HttpRequest: upload not implemented"); } + + get withCredentials() { throw new Error("HttpRequest: withCredentials not implemented"); } + set withCredentials(v) { throw new Error("HttpRequest: withCredentials not implemented"); } + + + + + + + /** private helper methods **/ + + _startTimeout() { + let that = this; + + this._xhr.timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + let event = { + notify: function(timer) { + that._cancel(Components.results.NS_ERROR_NET_TIMEOUT) + } + } + this._xhr.timer.initWithCallback( + event, + this._xhr.timeout, + Components.interfaces.nsITimer.TYPE_ONE_SHOT); + } + + _cancel(error) { + if (this._xhr.httpchannel && error) { + this._xhr.httpchannel.cancel(error); + } + } +} + + + + + +var HttpRequestPrompt = class { + constructor(username, password, promptCallback, realmCallback) { + this.mCounts = 0; + this.mUsername = username; + this.mPassword = password; + this.mPromptCallback = promptCallback; + this.mRealmCallback = realmCallback; + } + + // boolean promptAuth(in nsIChannel aChannel, + // in uint32_t level, + // in nsIAuthInformation authInfo) + promptAuth (aChannel, aLevel, aAuthInfo) { + this.mRealmCallback(this.mUsername, aAuthInfo.realm, aChannel.URI.host); + if (this.mUsername && this.mPassword) { + console.log("Passing provided credentials for user <"+this.mUsername+"> to nsIHttpChannel."); + aAuthInfo.username = this.mUsername; + aAuthInfo.password = this.mPassword; + } else if (this.mUsername) { + console.log("Using passwordCallback callback to get password for user <"+this.mUsername+"> and realm <"+aAuthInfo.realm+"> @ host <"+aChannel.URI.host+">"); + let password = this.mPromptCallback(this.mUsername, aAuthInfo.realm, aChannel.URI.host); + if (password) { + aAuthInfo.username = this.mUsername; + aAuthInfo.password = password; + } else { + return false; + } + } else { + return false; + } + + // The provided password could be wrong, in whichcase + // we would be here more than once. + this.mCounts++ + return (this.mCounts < 2); + } +} + + + + +function getSandboxForOrigin(username, uri, containerRealm = "default", containerReset = false) { + let options = {}; + let origin = uri.scheme + "://" + uri.hostPort; + + // Note: + // Disabled check for username to always use containers, even if no username is given. + // There was an issue with NC (in its container) and google being in the default container, + // causing NC to fail with 503. Putting google inside a container as well fixed it. + options.userContextId = getContainerIdForUser(containerRealm + "::" + username); + origin = options.userContextId + "@" + origin; + if (containerReset) { + resetContainerWithId(options.userContextId); + } + + if (!sandboxes.hasOwnProperty(origin)) { + console.log("Creating sandbox for <"+origin+">"); + let principal = Services.scriptSecurityManager.createContentPrincipal(uri, options); + sandboxes[origin] = Components.utils.Sandbox(principal, { + wantXrays: true, + wantGlobalProperties: ["XMLHttpRequest"], + }); + } + + return sandboxes[origin]; +} + +function resetContainerWithId(id) { + Services.clearData.deleteDataFromOriginAttributesPattern({ userContextId: id }); +} + +function getContainerIdForUser(username) { + // Define the allowed range of container ids to be used + // TbSync is using 10000 - 19999 + // Lightning is using 20000 - 29999 + // Cardbook is using 30000 - 39999 + let min = 10000; + let max = 19999; + + //reset if adding an entry will exceed allowed range + if (containers.length > (max-min) && containers.indexOf(username) == -1) { + for (let i=0; i < containers.length; i++) { + resetContainerWithId(i + min); + } + containers = []; + } + + let idx = containers.indexOf(username); + return (idx == -1) ? containers.push(username) - 1 + min : (idx + min); +} + +// copied from cardbook +function b64EncodeUnicode (aString) { + return btoa(encodeURIComponent(aString).replace(/%([0-9A-F]{2})/g, function(match, p1) { + return String.fromCharCode('0x' + p1); + })); +} + +// copied from lightning +function prepHttpChannelUploadData(aHttpChannel, aMethod, aUploadData, aContentType) { + if (aUploadData) { + aHttpChannel.QueryInterface(Components.interfaces.nsIUploadChannel); + let stream; + if (aUploadData instanceof Components.interfaces.nsIInputStream) { + // Make sure the stream is reset + stream = aUploadData.QueryInterface(Components.interfaces.nsISeekableStream); + stream.seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, 0); + } else { + // Otherwise its something that should be a string, convert it. + let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + stream = converter.convertToInputStream(aUploadData.toString()); + } + + // If aContentType is empty, the protocol will assume that no content headers are to be + // added to the uploaded stream and that any required headers are already encoded in + // the stream. In the case of HTTP, if this parameter is non-empty, then its value will + // replace any existing Content-Type header on the HTTP request. In the case of FTP and + // FILE, this parameter is ignored. + aHttpChannel.setUploadStream(stream, aContentType, -1); + } + + //must be set after setUploadStream + //https://developer.mozilla.org/en-US/docs/Mozilla/Creating_sandboxed_HTTP_connections + aHttpChannel.QueryInterface(Ci.nsIHttpChannel); + aHttpChannel.requestMethod = aMethod; +} + +/** + * Convert a byte array to a string - copied from lightning + * + * @param {octet[]} aResult The bytes to convert + * @param {Boolean} responseAsBase64 Return a base64 encoded string + * @param {String} aCharset The character set of the bytes, defaults to utf-8 + * @param {Boolean} aThrow If true, the function will raise an exception on error + * @returns {?String} The string result, or null on error + */ +function convertByteArray(aResult, responseAsBase64 = false, aCharset="utf-8", aThrow) { + if (responseAsBase64) { + var bin = ''; + var bytes = Uint8Array.from(aResult); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + bin += String.fromCharCode( bytes[ i ] ); + } + return btoa( bin ); // if we ever need raw, return bin + } else { + try { + return new TextDecoder(aCharset).decode(Uint8Array.from(aResult)); + } catch (e) { + if (aThrow) { + throw e; + } + } + } + return null; +} + |