From 00c797e2b54593baec356149b502cb9c38a9c868 Mon Sep 17 00:00:00 2001 From: MinRK Date: Sun, 30 Oct 2011 22:50:46 -0700 Subject: [PATCH 1/4] remove superfluous ws-hostname parameter from notebook This made the notebook server artificially and unnecessarily brittle to tunneling, and non-local hostname resolution. The ws_url is now defined based on the Origin of the request. This allows tunneled hosts and ports to preserve connections, as the ws_url always matches the one in use by the client. Implemented as a property, to facilitate future cases where it might behave differently. --- IPython/frontend/html/notebook/handlers.py | 14 ++++++++++---- IPython/frontend/html/notebook/notebookapp.py | 18 +----------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index e25f9a729..c009e9d39 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -139,7 +139,15 @@ class AuthenticatedHandler(web.RequestHandler): return True else: return False + + @property + def ws_url(self): + """websocket url matching the current request + turns http[s]://host[:port]/foo/bar into + ws[s]://host[:port]/foo/bar + """ + return self.request.headers.get('Origin').replace('http', 'ws', 1) class ProjectDashboardHandler(AuthenticatedHandler): @@ -221,8 +229,7 @@ class MainKernelHandler(AuthenticatedHandler): km = self.application.kernel_manager notebook_id = self.get_argument('notebook', default=None) kernel_id = km.start_kernel(notebook_id) - ws_url = self.application.ipython_app.get_ws_url() - data = {'ws_url':ws_url,'kernel_id':kernel_id} + data = {'ws_url':self.ws_url,'kernel_id':kernel_id} self.set_header('Location', '/'+kernel_id) self.finish(jsonapi.dumps(data)) @@ -249,8 +256,7 @@ class KernelActionHandler(AuthenticatedHandler): self.set_status(204) if action == 'restart': new_kernel_id = km.restart_kernel(kernel_id) - ws_url = self.application.ipython_app.get_ws_url() - data = {'ws_url':ws_url,'kernel_id':new_kernel_id} + data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id} self.set_header('Location', '/'+new_kernel_id) self.write(jsonapi.dumps(data)) self.finish() diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index 2f0f50f0b..6c4926952 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -147,7 +147,6 @@ aliases.update({ 'port': 'NotebookApp.port', 'keyfile': 'NotebookApp.keyfile', 'certfile': 'NotebookApp.certfile', - 'ws-hostname': 'NotebookApp.ws_hostname', 'notebook-dir': 'NotebookManager.notebook_dir', }) @@ -155,7 +154,7 @@ aliases.update({ # multi-kernel evironment: aliases.pop('f', None) -notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile', u'ws-hostname', +notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile', u'notebook-dir'] #----------------------------------------------------------------------------- @@ -200,13 +199,6 @@ class NotebookApp(BaseIPythonApplication): help="The port the notebook server will listen on." ) - ws_hostname = Unicode(LOCALHOST, config=True, - help="""The FQDN or IP for WebSocket connections. The default will work - fine when the server is listening on localhost, but this needs to - be set if the ip option is used. It will be used as the hostname part - of the WebSocket url: ws://hostname/path.""" - ) - certfile = Unicode(u'', config=True, help="""The full path to an SSL/TLS certificate file.""" ) @@ -226,14 +218,6 @@ class NotebookApp(BaseIPythonApplication): help="Whether to prevent editing/execution of notebooks." ) - def get_ws_url(self): - """Return the WebSocket URL for this server.""" - if self.certfile: - prefix = u'wss://' - else: - prefix = u'ws://' - return prefix + self.ws_hostname + u':' + unicode(self.port) - def parse_command_line(self, argv=None): super(NotebookApp, self).parse_command_line(argv) if argv is None: From b73d61627983672305f0f732b0fd7a31fc8fd630 Mon Sep 17 00:00:00 2001 From: MinRK Date: Sun, 30 Oct 2011 22:55:15 -0700 Subject: [PATCH 2/4] alert client on failed and lost web socket connections A long message is given if the connection fails within 1s. Otherwise, it is a short 'connection closed unexpectedly'. This also means that clients are notified on server termination. --- .../html/notebook/static/js/kernel.js | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/IPython/frontend/html/notebook/static/js/kernel.js b/IPython/frontend/html/notebook/static/js/kernel.js index 35980b5d3..15e170972 100644 --- a/IPython/frontend/html/notebook/static/js/kernel.js +++ b/IPython/frontend/html/notebook/static/js/kernel.js @@ -27,7 +27,7 @@ var IPython = (function (IPython) { } else if (typeof(MozWebSocket) !== 'undefined') { this.WebSocket = MozWebSocket } else { - alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.'); + alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox ≥ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.'); }; }; @@ -89,6 +89,7 @@ var IPython = (function (IPython) { Kernel.prototype.start_channels = function () { + var that = this; this.stop_channels(); var ws_url = this.ws_url + this.kernel_url; console.log("Starting WS:", ws_url); @@ -97,17 +98,50 @@ var IPython = (function (IPython) { send_cookie = function(){ this.send(document.cookie); } + var already_called_onclose = false; // only alert once + ws_closed_early = function(evt){ + if (already_called_onclose){ + return; + } + already_called_onclose = true; + if ( ! evt.wasClean ){ + alert("Websocket connection to " + ws_url + " could not be established." + + " You will not be able to run code." + + " Your browser may not be compatible with the websocket version in the server," + + " or if the url does not look right, there could be an error in the" + + " server's configuration."); + } + } + ws_closed_late = function(evt){ + if (already_called_onclose){ + return; + } + already_called_onclose = true; + if ( ! evt.wasClean ){ + alert("Websocket connection has closed unexpectedly." + + " The kernel will no longer be responsive."); + } + } this.shell_channel.onopen = send_cookie; + this.shell_channel.onclose = ws_closed_early; this.iopub_channel.onopen = send_cookie; + this.iopub_channel.onclose = ws_closed_early; + // switch from early-close to late-close message after 1s + setTimeout(function(){ + that.shell_channel.onclose = ws_closed_late; + that.iopub_channel.onclose = ws_closed_late; + }, 1000); }; Kernel.prototype.stop_channels = function () { if (this.shell_channel !== null) { + this.shell_channel.onclose = function (evt) {null}; this.shell_channel.close(); this.shell_channel = null; }; if (this.iopub_channel !== null) { + this.iopub_channel.onclose = function (evt) {null}; this.iopub_channel.close(); this.iopub_channel = null; }; From 487d1105573836d8708b6277bc2c1830ba10dbcb Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 31 Oct 2011 19:07:25 -0700 Subject: [PATCH 3/4] use jQuery dialog instead of alert() --- .../html/notebook/static/js/kernel.js | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/IPython/frontend/html/notebook/static/js/kernel.js b/IPython/frontend/html/notebook/static/js/kernel.js index 15e170972..1d74ac130 100644 --- a/IPython/frontend/html/notebook/static/js/kernel.js +++ b/IPython/frontend/html/notebook/static/js/kernel.js @@ -87,6 +87,34 @@ var IPython = (function (IPython) { IPython.kernel_status_widget.status_idle(); }; + Kernel.prototype._websocket_closed = function(ws_url, early){ + var msg; + var parent_item = $('body'); + if (early) { + msg = "Websocket connection to " + ws_url + " could not be established.
" + + " You will NOT be able to run code.
" + + " Your browser may not be compatible with the websocket version in the server," + + " or if the url does not look right, there could be an error in the" + + " server's configuration." + } else { + msg = "Websocket connection closed unexpectedly.
" + + " The kernel will no longer be responsive." + } + var dialog = $('
'); + dialog.html(msg); + parent_item.append(dialog); + dialog.dialog({ + resizable: false, + modal: true, + title: "Websocket closed", + buttons : { + "Okay": function () { + $(this).dialog('close'); + } + } + }); + + } Kernel.prototype.start_channels = function () { var that = this; @@ -105,11 +133,7 @@ var IPython = (function (IPython) { } already_called_onclose = true; if ( ! evt.wasClean ){ - alert("Websocket connection to " + ws_url + " could not be established." + - " You will not be able to run code." + - " Your browser may not be compatible with the websocket version in the server," + - " or if the url does not look right, there could be an error in the" + - " server's configuration."); + that._websocket_closed(ws_url, true); } } ws_closed_late = function(evt){ @@ -118,8 +142,7 @@ var IPython = (function (IPython) { } already_called_onclose = true; if ( ! evt.wasClean ){ - alert("Websocket connection has closed unexpectedly." + - " The kernel will no longer be responsive."); + that._websocket_closed(ws_url, false); } } this.shell_channel.onopen = send_cookie; From a4b6d6bb9f7a24db4fd55ef49781374d754c036f Mon Sep 17 00:00:00 2001 From: MinRK Date: Thu, 10 Nov 2011 19:01:44 -0800 Subject: [PATCH 4/4] don't use Origin header to determine ws_url --- IPython/frontend/html/notebook/handlers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index c009e9d39..15d3561ae 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -144,10 +144,11 @@ class AuthenticatedHandler(web.RequestHandler): def ws_url(self): """websocket url matching the current request - turns http[s]://host[:port]/foo/bar into - ws[s]://host[:port]/foo/bar + turns http[s]://host[:port] into + ws[s]://host[:port] """ - return self.request.headers.get('Origin').replace('http', 'ws', 1) + proto = self.request.protocol.replace('http', 'ws') + return "%s://%s" % (proto, self.request.host) class ProjectDashboardHandler(AuthenticatedHandler):