Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

status commit; check-imap almost has all its pieces

  • Loading branch information...
commit 9ec8e304eb403da4e454d12661454b41fde0b09f 1 parent 5ee6765
@asutherland asutherland authored
View
1  .gitignore
@@ -1,2 +1,3 @@
data/deps/
node-transformed-deps/
+jetpack-tcp-imap-demo.xpi
View
7 Makefile
@@ -26,8 +26,11 @@ $(DEP_NODE_PKGS): $(TRANS_NODE_PKGS)
xpi: $(DEP_NODE_PKGS)
- echo $(RSYNC) deps/wmsy/lib/wmsy data/deps/
- echo cfx xpi
+ $(RSYNC) deps/wmsy/lib/wmsy data/deps/
+ cfx xpi
+
+run: xpi
+ wget --post-file=jetpack-tcp-imap-demo.xpi http://localhost:8222/
clean:
rm -rf data/deps
View
3  chrome.manifest
@@ -0,0 +1,3 @@
+content jetpack-tcp-imap-demo chrome/content/
+overlay chrome://browser/content/browser.xul chrome://jetpack-tcp-imap-demo/content/browser-overlay.xul
+style chrome://browser/content/browser.xul chrome://jetpack-tcp-imap-demo/content/browser-overlay.css
View
7 chrome/content/browser-overlay.css
@@ -0,0 +1,7 @@
+#webapi-tcp-notification-icon {
+ list-style-image: url(chrome://global/skin/icons/question-16.png);
+}
+.popup-notification-icon[popupid="webapi-tcp-permission-prompt"],
+.popup-notification-icon[popupid="webapi-tcp-exception-prompt"] {
+ list-style-image: url(chrome://global/skin/icons/question-64.png);
+}
View
7 chrome/content/browser-overlay.xul
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<overlay id="sample"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <box id="notification-popup-box">
+ <image id="webapi-tcp-notification-icon" class="notification-anchor-icon" role="button"/>
+ </box>
+</overlay>
View
160 data/checkImap.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <base href="resource://jid1-reoZ1pC7LQZcEA-at-jetpack/jetpack-tcp-imap-demo/data/" />
+ <title>Check IMAP!</title>
+ <style type="text/css">
+ html {
+ font-family: "Lucida Grande", Verdana, sans-serif;
+ font-size: 9pt;
+ }
+ #connectomatic { text-align: center; }
+ #hostname { width: 16em; }
+ #port { width: 3em; }
+ .status { color: blue; }
+ .error { color: red; }
+ .data { color: #333; }
+ </style>
+<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAVxQTFRFLjQ2MDY4MTc5PUNEP0RGQEVHQEZIQkdJSU5QSU9QTFFTT1RWUFVXVltdWF1eWV5fW2BiXmJkZnz/Z33/aGxtaW5vaX//am5waoD/bHBxbXJzbYL/boP/cYX/cob/c3d5dYj/dnp7d3t8d4r/eHx9eIv/e36Ae43/gJL/gZP/gpT/i5z/jZCRjZGSkqL/maf/nJ+gnKr/naChn6GioK7/paipqLT/qbX/qbb/q62urK6vr7GysLKzsbO0sbz/sr3/tL//tri4t8H/ubu8ury8vb+/v8HCwMn/wcPDwsTEw8TFxs7/x8//yszMzNP/z9DR0Nb/0tj/09r/1dv/2N3/2d//3eL/3uL/3uP/4ub/4+Pk4+Tk5OXl5Of/5en/5un/6Onp6erq6u3/6+7/7O3t7e7u7+/w8PL/8/X/9PX/9/j/+Pj4+fn5+fn//Pz//f39/f3//v7+/v7/////BvG6swAAAT1JREFUOMtjKCYAGMhTkGWnKafggFtBpKqQkJBcEk4FgdJAeYkwnFYEiQHlhfxwuiFUAiTvhNORybIgeROcvsjSAMlr52FRkOtjnl5cbAiSV8/AEg7hQKMtin1A8tJx2ALKDuT1WBmQggCsIekuBAP22IM6AiavV4RdQYEcRF45E1dkmYHlhUNxxmY8WIEFnujWB8prZeFRkBfsHZJHrRSFqgCEeAqB7EJusHJjRmOwDAMDm1oaTAGfG1DIlQ+kIJ/DlDMfojVBXgmmwFkQKCTgAlLgwVvM7wm1PIUZpiCfy7/YlysfJChuXWwjjqGg2FKqWMQKxIxiyS7OZokGCycqKsIV5LA7sueAmDoMIKALdiSrSipcQbEBkxGIzmaJARodDTSGAcWbcE/bSIIZora4FPB5gRlu/EgKKAxqAMryefXvHv3iAAAAAElFTkSuQmCC" />
+</head>
+<body id="body">
+ <form id="connectomatic">
+ <label for="hostname">Host:</label> <input id="hostname" type="text"></input><br />
+ <label for="port">Port:</label> <input id="port" type="number">993</input><br />
+ <label for="crypto">Security:</label>
+ <select id="crypto">
+ <option>ssl</option>
+ <option>starttls</option>
+ <option>plaintext</option>
+ </select>
+
+ <button id="connect">Connect</button>
+ </form>
+ <h3>Log: (newest to oldest)</h3>
+ <div id="results">
+ </div>
+ <button id="clear">Clear Output</button>
+<script type="text/javascript">//<![CDATA[
+var resultsNode = document.getElementById("results");
+function logResult(styleClass, msg) {
+ var node = document.createElement("span");
+ node.class = styleClass;
+ node.textContent = msg;
+ resultsNode.insertBefore(node, resultsNode.firstChild);
+}
+
+function checkImapServer(host, port, crypto) {
+ var socket = new TCPSocket();
+ socket.onopen = function(evt) {
+ logResult("status", "Connection opened!");
+ sendNextCommand();
+ };
+ var invokeOnSecure = null;
+ socket.onsecure = function(evt) {
+ logResult("status", "Connection secured.");
+ if (invokeOnSecure)
+ invokeOnSecure();
+ };
+ socket.onclose = function(evt) {
+ logResult("status", "Connection closed.");
+ };
+
+ socket.onerror = function(evt) {
+ logResult("error", evt.data);
+ logResult("status", "readyState is now: " + socket.readyState);
+ };
+
+ socket.oncertoverride = function(evt) {
+ logResult("status",
+ "Certificate overriden; we could automatically retry now.");
+ };
+ socket.onsendoverflow = function(evt) {
+ logResult("error", "Send overflow!");
+ };
+
+ var imapCommands = [
+ {
+ send: "a001 CAPABILITY\n",
+ waitFor: /^a0001 OK .+$/m,
+ },
+ ];
+ var idxImapCommand = 0;
+
+ function sendNextCommand() {
+ var curImapCommand = imapCommands[idxImapCommand],
+ buffer = new ArrayBuffer(curImapCommand.send.length),
+ u8View = new Uint8Array(buffer);
+ // We are only sending 7-bit ASCII, so this is fine.
+ for (var i = 0; i < u8View.length; i++) {
+ u8View[i] = curImapCommand.send.charCodeAt(i);
+ }
+ socket.send(u8View);
+ }
+
+ var buffered = "";
+ socket.ondata = function(evt) {
+ // We get the data as a uint8 typed array view. We are only going to see
+ // 7-bit US-ASCII from the server, so fromCharCode is good enough for us.
+ var dataAsChars = [], u8data = evt.data;
+ for (var i = 0; i < u8data.length; i++) {
+ dataAsChars.push(String.fromCharCode(u8data[i]));
+ }
+ var dataAsStr = dataAsChars.join("");
+
+ logResults("data", dataAsStr);
+
+ if (idxImapCommand < imapCommands.length) {
+ buffered += dataAsStr;
+ var curImapCommand = imapCommands[idxImapCommand];
+ var match = curImapCommand.waitFor.exec(buffered);
+ if (match) {
+ buffered = buffered.substring(match.index + match[0].length);
+ if (curImapCommand.callback)
+ curImapCommand.callback();
+ idxImapCommand++;
+ sendNextCommand();
+ }
+ }
+ };
+
+ var sslOptions;
+ if (crypto === "ssl") {
+ sslOptions = { allowOverride: true };
+ }
+ else if (crypto === "starttls") {
+ // add the TLS upgrade command
+ imapCommands.push({
+ send: "a002 STARTTLS\n",
+ waitFor: /^a0002 OK .+$/m,
+ callback: function() {
+ socket.startTLS({ allowOverride: true });
+ }
+ });
+ // ask for the capabilities again once the encryption is established
+ invokeOnSecure = function() {
+ imapCommands.push({
+ send: "a003 CAPABILITY\n",
+ waitFor: /^a0003 OK .+$/m,
+ });
+ sendNextCommand();
+ }
+ }
+ socket.open(host, port, sslOptions);
+ logResult("status", "connecting... (readyState: " + socket.readyState + ")");
+}
+
+
+document.getElementById("clear").addEventListener("click", function(evt) {
+ while (resultsNode.lastChild)
+ resultsNode.removeChild(resultsNode.lastChild);
+}, false);
+
+document.getElementById("connect").addEventListener("click", function(evt) {
+ checkImapServer(document.getElementById("hostname").value,
+ parseInt(document.getElementById("port").value),
+ document.getElementById("crypto").value);
+}, false);
+//]]></script>
+</body>
+</html>
View
220 lib/TCPSocket.js
@@ -19,6 +19,15 @@ function LOG(msg) {
dump(msg);
}
+const nsITransport = Ci.nsITransport,
+ BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream", "setInputStream"),
+ BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream", "setOutputStream"),
+ Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init"),
+ AsyncStreamCopier = CC("@mozilla.org/network/async-stream-copier;1",
+ "nsIAsyncStreamCopier", "init");
+
/*
* nsITCPSocketEvent object
*/
@@ -59,71 +68,181 @@ let InputStreamPump = CC("@mozilla.org/network/input-stream-pump;1",
let ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
"nsIScriptableInputStream");
-const CONNECTING = 0;
-const OPEN = 1;
-const CLOSING = 2;
-const CLOSED = 3;
+/**
+ * Maximum send buffer size (in 64KiB chunks) before we threaten to close the
+ * connection.
+ */
+const MAX_SEND_BUFFER_SIZE_64K_CHUNKS = 128 * 16;
-function TCPSocket() {
- this.readyState = CLOSED;
+/*
+ * Permission checks.
+ * - Is this webapp authorized to attempt to try establish TCP connections?
+ * This allows the user to blacklist use of the API by the extension and does
+ * not convey the ability to connect to anything.
+ * - Is this webapp authorized to attempt to connect to the given hostname/IP?
+ * Eternal permission is granted if authorized.
+ *
+ * SSL checks:
+ * - In the event of a bad certificate and if the open call asked to allow for
+ * exceptions, then we ask the user if they want to add an exception for the
+ * certificate. Before asking, we check if there already was an exception
+ * and are sure to mention that.
+ */
+function TCPSocket(ownerInfo) {
+ /**
+ * The state is always one of:
+ * - closed: We are neither connected nor attempting to initiate a
+ * connection.
+ * - authorizing: We are waiting on the user to authorize the connection.
+ * - connecting: We are trying to establish the connection.
+ * - connected: The connection is established.
+ * - securing: The connection is upgrading to a TLS connection from a non-SSL
+ * cleartext connection. Once secured, our state returns to connected.
+ */
+ this.readyState = 'closed'; // readOnly
+
+ /**
+ * This event is generated when the connection is established.
+ */
this.onopen = null;
- this.onmessage = null;
+ /**
+ * This event is generated when a plaintext connection is upgraded to an
+ * encrypted connection using startTLS().
+ */
+ this.onsecure = null;
+ /**
+ * This event is generated whenever data is received from the socket. The
+ * recipient should make no assumption about the amount of data received
+ * with each event.
+ */
+ this.ondata = null;
+ /**
+ * This event is synchronously generated when an attempt to write to the
+ * socket would overflow the send buffer. No bytes are enqueued in that case.
+ * If preventDefault is not invoked on the event then the connection will be
+ * closed. If preventDefault is invoked, the caller must take care to
+ * observe the state of
+ */
+ this.onsendoverflow = null;
+ /**
+ * This event is generated whenever any type of error is encountered involving
+ * the connection.
+ */
this.onerror = null;
this.onclose = null;
+ /**
+ * This event is generated when an SSL certificate exception is added by the
+ * user and so it is reasonable to attempt to retry the connection.
+ */
+ this.oncertoverride = null;
+
+ this._ownerInfo = ownerInfo;
this.host = "";
this.port = -1;
this.ssl = false;
+ this._sslSettings = null;
};
TCPSocket.prototype = {
_transport: null,
_outputStream: null,
_inputStream: null,
- _scriptableInputStream: null,
+ _binaryInputStream: null,
+ _binaryOutputStream: null,
_request: null,
dispatchEvent: function ts_dispatchEvent(type, data) {
if (!this[type])
- return;
+ return null;
- this[type].handleEvent(new TCPSocketEvent(type, data || ""));
+ let event = new TCPSocketEvent(type, data || "");
+ this[type].handleEvent(event);
+ return event;
},
// nsITCPSocket
open: function ts_open(host, port, ssl) {
- if (this.readyState != CLOSED) {
+ if (this.readyState !== 'closed') {
this.dispatchEvent("onerror", "Socket is already opened");
return;
}
+ this.readyState = 'authorizing';
+
+ let self = this;
+ PermissionChecker.checkTCPConnectionAllowed(
+ this._ownerInfo, host, port, Boolean(ssl),
+ function allowed() {
+ self._open(host, port, ssl);
+ });
+ },
+
+ _open: function(host, port, ssl) {
LOG("startup called\n");
LOG("Host info: " + host + ":" + port + "\n");
- this.readyState = CONNECTING;
+
+
+ this.readyState = 'connecting';
this.host = host;
this.port = port;
- this.ssl = (ssl === true);
+ if (ssl) {
+ this.ssl = true;
+ if (typeof(ssl) === 'object')
+ this._sslSettings = ssl;
+ else
+ this._sslSettings = {};
+ }
let transport = this._transport = createTransport(host, port, this.ssl);
transport.securityCallbacks = new SecurityCallbacks(this);
-
- this._inputStream = transport.openInputStream(0, 0, 0);
- this._outputStream = transport.openOutputStream(1, 65536, 0);
+ // - Output Stream
+ // Open the socket as unbuffered and non-blocking so that the raw socket
+ // output stream will be exposed and we can manually hook a pipe up to it.
+ // By manually hooking up the pipe we are able to see both its input and
+ // output streams. If we had openOutputStream create the pipe for us,
+ // we would not get to see the input stream, and so would be unable to
+ // use its available() method to know how much data is buffered for our
+ // `bufferedAmount` getter.
+ this._rawOutputStream = transport.openOutputStream(
+ nsITransport.OPEN_UNBUFFERED, 0, 0);
+ // We open the pipe non-blocking; we will detect buffer overflow when
+ // sending and (by default) automatically close the connection.
+ this._outputStreamPipe = Pipe(true, true,
+ 65536, MAX_SEND_BUFFER_SIZE_64K_CHUNKS, null);
+
+ // (nsIASyncStreamCopier ends up calling NS_AsyncCopy under the hood, the
+ // same as openOutputStream's buffered mode.)
+ this._outputStreamCopier =
+ AsyncStreamCopier(this._outputStreamPipe.inputStream,
+ this._rawOutputStream,
+ // (nsSocketTransport uses gSocketTransportService)
+ Cc["@mozilla.org/network/socket-transport-service;1"]
+ .getService(Ci.nsIEventTarget),
+ /* source buffered */ true, /* sink buffered */ false,
+ 65536, /* close source*/ true, /* close sink */ true);
+ // Since we drive the output stream, we don't need to listen to it.
+ this._outputStreamCopier.asyncCopy(null, null);
+ this._binaryOutputStream =
+ new BinaryOutputStream(this._outputStreamPipe.outputStream);
+
+ // - Input Stream
+ this._inputStream = transport.openInputStream(0, 0, 0);
let pump = new InputStreamPump(this._inputStream, -1, -1, 0, 0, false);
pump.asyncRead(this, null);
- this._scriptableInputStream = new ScriptableInputStream();
+ this._binaryInputStream = new BinaryInputStream(this._inputStream);
},
close: function ts_close() {
- if (this.readyState === CLOSING || this.readyState === CLOSED)
+ if (this.readyState === 'closing' || this.readyState === 'closed')
return;
- LOG("shutdown called\n");
- this.readyState = CLOSING;
+ LOG("close called\n");
+ this.readyState = 'closing';
this._outputStream.close();
this._inputStream.close();
@@ -139,11 +258,21 @@ TCPSocket.prototype = {
if (data === undefined)
return;
-
- // TODO
- // Because data is a |jsval| this method use JS coercion rules but
- // Blob and ArrayBuffer are should be handled correctly.
- this._outputStream.write(data, data.length);
+ try {
+ this._binaryOutputStream.writeByteArray(data, data.length);
+ }
+ catch (ex) {
+ if (ex.result === Cr.NS_BASE_STREAM_WOULD_BLOCK) {
+ // Synchronously dispatch a notification about the send overflow
+ // and close the connection if they don't prevent it.
+ let event = this.dispatchEvent("onsendoverflow");
+ if (!event || !event.defaultPrevented)
+ this.close();
+ }
+ else {
+ throw ex;
+ }
+ }
},
suspend: function ts_suspend() {
@@ -158,6 +287,10 @@ TCPSocket.prototype = {
}
},
+ get bufferedAmount() {
+ return this._outputStreamPipe.inputStream.available();
+ },
+
// nsIStreamListener
onStartRequest: function ts_onStartRequest(request, context) {
this.readyState = OPEN;
@@ -177,11 +310,22 @@ TCPSocket.prototype = {
this.dispatchEvent("onclose");
},
- onDataAvailable: function ts_onDataAvailable(request, context, inputStream, offset, count) {
+ onDataAvailable: function ts_onDataAvailable(request, context, inputStream,
+ offset, count) {
+ // Although XPConnect can accept typed arrays, it cannot produce them, so
+ // we need to create the typed array ourselves here and shuttle the bytes.
+ let xpcArray = this._binaryInputStream.readByteArray(count),
+ buffer = new ArrayBuffer(count),
+ u8View = new Uint8Array(buffer);
+ for (let i = 0; i < count; i++) {
+ u8View[i] = xpcArray[i];
+ }
+
this._scriptableInputStream.init(inputStream);
- this.dispatchEvent("onmessage", this._scriptableInputStream.read(count));
+ this.dispatchEvent("ondata", u8View);
},
-
+
+/*
classID: Components.ID("{cda91b22-6472-11e1-aa11-834fec09cd0a}"),
classInfo: XPCOMUtils.generateCI({
@@ -195,6 +339,7 @@ TCPSocket.prototype = {
QueryInterface: XPCOMUtils.generateQI([
Ci.nsITCPSocket
])
+*/
}
function SecurityCallbacks(socket) {
@@ -206,8 +351,23 @@ SecurityCallbacks.prototype = {
this._socket.dispatchEvent("onerror", "SSL Error: " + error);
},
- notifyCertProblem: function sc_notifyCertProblem(socketInfo, status, targetSite) {
- let msg = "Certificat error: ";
+ /**
+ * Translate error messages and potentially trigger UI for generating
+ * certificate exceptions if the
+ */
+ notifyCertProblem: function sc_notifyCertProblem(socketInfo, status,
+ targetSite) {
+ if (this._socket._sslSettings.allowOverride) {
+ let socket = this._socket;
+ PermissionChecker.handleBadCertificate(
+ socket._ownerInfo, socket.host, socket.port, targetSite,
+ function exceptionAddedRetryConnection() {
+ // The user added an exception.
+ socket.dispatchEvent("oncertoverride");
+ });
+ }
+
+ let msg = "Certificate error: ";
if (status.isDomainMismatch) {
msg = msg + "Domain Mismatch";
} else if (status.isNotValidAtThisTime) {
View
148 lib/main.js
@@ -0,0 +1,148 @@
+/* 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/. */
+
+/**
+ * Facilitate an in-Firefox demonstration of the proposed TCP WebAPI. We
+ * define about URLs to provide human-readable names to the demo webpages/apps
+ * that we host in this module.
+ *
+ * We use an observer notification to know when content pages have their global
+ * created and at that instant (it's a synchronous API), we inject the TCP API
+ * if they match one of our URLs.
+ *
+ * Defines the following mappings:
+ *
+ * - about:imap-check, a simple webpage that will connect to an IMAP server
+ * and report its capability line. This can be used to verify that the
+ * TCP API is operational and that certificates are being dealt with
+ * correctly is using SSL.
+ *
+ * - about:imap-client, our IMAP client/UI. Although we are using the deuxdrop
+ * architecture which keeps the back-end and front-end logically partitioned,
+ * we are not putting them in separate frames/pages.
+ *
+ * Important notes:
+ * - All our example webpages in here use the *same ORIGIN* which means the
+ * same localStorage universe, the same IndexedDB universe, etc.
+ **/
+
+const $protocol = require('./jetpack-protocol/index'),
+ $unload = require('unload'),
+ $tabBrowser = require('tab-browser'),
+ $windowUtils = require('window/utils'),
+ $self = require('self'),
+ $observe = require('api-utils/observer-service'),
+ { Cu, Ci } = require('chrome')
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const CONTENT_GLOBAL_CREATED = 'content-document-global-created';
+
+let PAGES = [
+ {
+ name: 'imap-check',
+ url: $self.data.url('checkImap.html'),
+ },
+ {
+ name: 'imap-client',
+ url: $self.data.url('imapClient.html'),
+ }
+];
+
+let gTracker;
+
+exports.main = function() {
+ let pageUrls = {};
+ PAGES.forEach(function(pageDef) {
+ // - protocol
+ pageDef.protocol = $protocol.about(pageDef.name, {
+ onRequest: function(request, response) {
+ response.uri = pageDef.url;
+ // this may not be required
+ response.principalURI = pageDef.url;
+ }
+ });
+ pageDef.protocol.register();
+ $unload.when(function() {
+ pageDef.protocol.unregister();
+ });
+
+ pageUrls[pageDef.url] = true;
+ });
+
+ function contentGlobalCreated(domWindow) {
+ if (!pageUrls.hasOwnProperty(domWindow.document.URL))
+ return;
+ console.log("injecting TCPSocket!");
+
+ let weakrefs = [];
+
+ function cullDeadSockets() {
+ for (let i = weakrefs.length - 1; i >= 0; i--) {
+ if (!weakrefs[i].get())
+ weakrefs.splice(i, 1);
+ }
+ }
+
+ let ownerInfo = {
+ // For aliased things like about: URLs, this will be the about: URL
+ uri: Services.io.newURI(domWindow.location),
+ contentWin: domWindow,
+ browserWin: $windowUtils.getBaseWindow(domWindow),
+ };
+ // We need the window ID to use inner-window-destroyed to know when the
+ // window/document gets destroyed. We are imitating jetpack's
+ // api-utils/content/worker.js implementation which claims it does it this
+ // way to avoid interfering with bfcache (which would happen if one added
+ // an unload listener.)
+ let windowID = contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+
+ // Create a special constructor because we are not using XPConnect, but we
+ // want to look like it, including only surfacing public functions that
+ // would be on the interface. So we:
+ // - use Jetpack's "cortex" to wrap the public methods and re-expose them
+ // on a public instance.
+ // - capture the document's window in the process so we can use it for
+ // authentication
+ domWindow.wrappedJSObject.TCPSocket = function() {
+ // Cull any dead sockets so long-lived apps with high socket turnover
+ // don't cause horrible problems.
+ cullDeadSockets();
+
+ let realSocket = new $tcpsocket.TCPSocket(ownerURI);
+ weakrefs.push(Cu.getWeakReference(realSocket));
+
+ return $cortex.Cortex(realSocket);
+ };
+
+ function killSocketsForWindow(subject, topic, data) {
+ if (!weakrefs)
+ return;
+ let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (innerWindowID === windowID)
+ return;
+ for (let i = 0; i < weakrefs.length; i++) {
+ let socket = weakrefs[i].get();
+ if (socket) {
+ // kill off the socket and ignore any complaints.
+ try {
+ socket.close();
+ }
+ catch() {
+ }
+ }
+ }
+ weakrefs = null;
+ $observe.remove('inner-window-destroyed', killSocketsForWindow);
+ };
+ $observe.add('inner-window-destroyed', killSocketsForWindow);
+ $unload.when(killSocketsForWindow);
+ }
+ $observe.add(CONTENT_GLOBAL_CREATED, contentGlobalCreated);
+ $unload.when(function() {
+ $observe.remove(CONTENT_GLOBAL_CREATED, contentGlobalCreated);
+ });
+};
View
217 lib/tcp_permissions.js
@@ -0,0 +1,217 @@
+/* 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/. */
+
+/**
+ * Provides the UI and persistence for WebAPI TCP connections.
+ *
+ * While we currently hard-code our string bundle for prototyping, we do,
+ * however depend on a chrome.manifest-driven overlay (xul, css) to include our
+ * icon in the address bar for our door-hanger.
+ **/
+
+"use strict";
+const {Cc,Ci,Cu} = require("chrome");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const nsIPermissionManager = Ci.nsIPermissionManager;
+
+
+const TCP_NO_SSL_PERM_PREFIX = 'webtcp:',
+ TCP_SSL_PERM_PREFIX = 'webtcps:';
+
+// The strings are in the JS source only for prototyping simplicity.
+const stringPool = {
+ "webapi.permission.tcp.prompt":
+ "This website (%1$S) is asking to initiate an unencrypted network" +
+ " connection to %2$S:%3$S.",
+ "webapi.permission.tcps.prompt":
+ "This website (%1$S) is asking to initiate an encrypted network" +
+ " connection to %2$S:%3$S.",
+
+ "webapi.permission.allow": "Allow",
+ "webapi.permission.allowAccessKey": "A",
+ "webapi.permission.neverThis":
+ "Never allow this site to connect to %1$S:%2$S",
+ "webapi.permission.neverThisAccessKey": "e",
+ "webapi.permission.neverAny":
+ "Never allow this site to connect to any server in this fashion",
+ "webapi.permission.neverAnyAccessKey": "v",
+ "webapi.permission.notNow": "Not Now",
+ "webapi.permission.notNowAccessKey": "N",
+
+ // about:certerror is Firefox's UI for this. It uses strings from
+ // netError.dtd and aboutCertError.dtd.
+ "webapi.security.certException.prompt":
+ "We are trying to connect securely to the serverr at %1$S:%2$S, but we" +
+ " can't confirm that the connection is secure.",
+ "webapi.security.certException.promptExtraExistingException":
+ " You had previously made an exception for this server, but its identity" +
+ " has changed since then.",
+
+ "webapi.security.seeCertificate": "See certificate and possibly add exception",
+ "webapi.security.seeCertificateAccessKey": "S",
+ "webapi.security.notNow": "Do not connect, hide this message",
+ "webapi.security.notNowAccessKey": "D",
+};
+const gStringBundle = {
+ getString: function(name) {
+ return stringPool[name];
+ },
+ getFormattedString: function(name, args) {
+ let s = stringPool[name];
+ for (let i = 0; i < args.length; i++) {
+ s = s.replace("%" + i + "$S", args[i]);
+ }
+ return s;
+ }
+};
+
+exports.PermissionChecker = {
+ checkTCPConnectionAllowed: function(ownerInfo, host, port, useSSL,
+ allowedCallback) {
+ // - Check existing permissions
+ let permType =
+ (useSSL ? TCP_SSL_PERM_PREFIX : TCP_NO_SSL_PERM_PREFIX) +
+ host + ':' + port,
+ perm = Services.perms.testExactPermission(ownerInfo.uri, permType);
+
+ // If allowed, indicate this immediately.
+ if (perm === nsIPermissionManager.ALLOW_ACTION) {
+ allowedCallback();
+ return;
+ }
+ // If forbidden, never generate the callback.
+ if (perm === nsIPermissionManager.DENY_ACTION)
+ return;
+
+ // Check if all connections are forbidden to this app.
+ let allPermType =
+ (useSSL ? TCP_SSL_PERM_PREFIX : TCP_NO_SSL_PERM_PREFIX) + '*';
+ if (Services.perms.testExactPermission(originURI, allPermType) ===
+ nsIPermissionManager.DENY_ACTION)
+ return;
+
+ // - Ask for Permission
+ let browserNode = ownerInfo.browserWin.gBrowser.getBrowserForDocument(
+ ownerInfo.contentWin),
+ PopupNotifications = ownerInfo.browserWin.PopupNotifications;
+
+ let allowAction = {
+ label: gStringBundle.getString("webapi.permission.allow"),
+ accessKey: gStringBundle.getString("webapi.permission.allowAccessKey"),
+ callback: function allowCallback() {
+ Services.perms.add(ownerInfo.uri, permType,
+ nsIPermissionManager.ALLOW_ACTION);
+ allowedCallback();
+ },
+ };
+ let neverThisAction = {
+ label: gStringBundle.getFormattedString("webapi.permission.neverThis",
+ [host, port]),
+ accessKey: gStringBundle.getString("webapi.permission.neverThisAccessKey"),
+ callback: function neverThisCallback() {
+ Services.perm.add(ownerInfo.uri, permType,
+ nsIPermissionManager.DENY_ACTION);
+ // do not trigger any callback notification
+ },
+ };
+ let neverAnyAction = {
+ label: gStringBundle.getString("webapi.permission.neverAny"),
+ accessKey: gStringBundle.getString("webapi.permission.neverAnyAccessKey"),
+ callback: function neverAnyCallback() {
+ Services.perm.add(ownerInfo.uri, allPermType,
+ nsIPermissionManager.DENY_ACTION);
+ // do not trigger any callback notification
+ },
+ };
+ let notNowAction = {
+ label: gStringBundle.getString("webapi.permission.notNow"),
+ accessKey: gStringBundle.getString("webapi.permission.notNowAccessKey"),
+ callback: function notNowCallback() {
+ // This UI choice is a no-op.
+ },
+ };
+
+ PopupNotifications.show(
+ browserNode,
+ "webapi-tcp-permission-prompt",
+ gStringBundle.getFormattedString(
+ "webapi.permission." + useSSL ? "tcps" : "tcp" + ".prompt",
+ // contentWin is an xray wrapper, so the access is safe
+ [ownerInfo.contentWin.location.host, host, port])
+ "webapi-tcp-notification-icon",
+ allowAction,
+ [neverThisAction, neverAnyAction, notNowAction],
+ {});
+ },
+
+ /**
+ * Display UI for dealing with a bad certificate with the goal of potentially
+ * adding an exception. This should only be called when certificate
+ * exceptions are frequently required for the protocol in question (ex: IMAP).
+ * That decision is left to the webpage requesting the connection.
+ */
+ handleBadCertificate: function(ownerInfo, host, port, targetSite,
+ retryCallback) {
+ let browserNode = ownerInfo.browserWin.gBrowser.getBrowserForDocument(
+ ownerInfo.contentWin),
+ PopupNotifications = ownerInfo.browserWin.PopupNotifications;
+
+ let overrideService = Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService),
+ existingBits = {},
+ hasExistingOverride = overrideService.getValidityOverride(
+ host, port, {}, {}, existingBits, {});
+
+ let promptString = gStringBundle.getFormattedString(
+ "webapi.security.certException.prompt", [host, port]);
+ if (hasExistingOverride) {
+ promptString += gStringBundle.getString(
+ "webapi.security.certException.promptExtraExistingException");
+ }
+
+ let seeAction = {
+ label: gStringBundle.getString("webapi.security.seeCertificate"),
+ accessKey: gStringBundle.getString(
+ "webapi.security.seeCertificateAccessKey"),
+ callback: function allowCallback() {
+ // Bring up the exception dialog which also provides the ability to see
+ // all the details of the certificate and explains the problems with
+ // the certificate. But do this in a timeout so the popup has a chance
+ // to hide.
+ setTimeout(function() {
+ let params = {
+ exceptionAdded: false,
+ prefetechCert: true,
+ location: targetSite,
+ };
+ ownerInfo.browserWin.openDialog(
+ "chrome://pippki/content/exceptionDialog.xul", "",
+ "chrome,centerscreen,modal",
+ params);
+ // (the modal dialog spins a nested event loop, so we have a result)
+ if (params.exceptionAdded && retryCallback)
+ retryCallback();
+ }, 0);
+ },
+ };
+ let notNowAction = {
+ label: gStringBundle.getString("webapi.security.notNow"),
+ accessKey: gStringBundle.getString("webapi.security.notNowAccessKey"),
+ callback: function notNowCallback() {
+ // This UI choice is a no-op.
+ },
+ };
+
+ PopupNotifications.show(
+ browserNode,
+ "webapi-tcp-exception-prompt",
+ promptString,
+ "webapi-tcp-notification-icon",
+ seeAction,
+ [notNowAction],
+ {});
+ },
+};
View
8 package.json
@@ -1,9 +1,9 @@
{
- "name": "jetpack-tcp-imap-demo",
+ "description": "IMAP in Firefox",
"license": "MPL 2.0/GPL 2.0/LGPL 2.1",
"author": "Andrew Sutherland <asutherland@asutherland.org>",
- "version": "0.1",
+ "version": "0.1",
"fullName": "jetpack-tcp-imap-demo",
- "description": "IMAP in Firefox"
+ "id": "jid1-reoZ1pC7LQZcEA",
+ "name": "jetpack-tcp-imap-demo"
}
-
Please sign in to comment.
Something went wrong with that request. Please try again.