diff --git a/IPython/html/allow76.py b/IPython/html/allow76.py
new file mode 100644
index 000000000..327fd8b86
--- /dev/null
+++ b/IPython/html/allow76.py
@@ -0,0 +1,312 @@
+"""WebsocketProtocol76 from tornado 3.2.2 for tornado >= 4.0
+
+The contents of this file are Copyright (c) Tornado
+Used under the Apache 2.0 license
+"""
+
+
+from __future__ import absolute_import, division, print_function, with_statement
+# Author: Jacob Kristhammar, 2010
+
+import functools
+import hashlib
+import struct
+import time
+import tornado.escape
+import tornado.web
+
+from tornado.log import gen_log, app_log
+from tornado.util import bytes_type, unicode_type
+
+from tornado.websocket import WebSocketHandler, WebSocketProtocol13
+
+class AllowDraftWebSocketHandler(WebSocketHandler):
+ """Restore Draft76 support for tornado 4
+
+ Remove when we can run tests without phantomjs + qt4
+ """
+
+ # get is unmodified except between the BEGIN/END PATCH lines
+ @tornado.web.asynchronous
+ def get(self, *args, **kwargs):
+ self.open_args = args
+ self.open_kwargs = kwargs
+
+ # Upgrade header should be present and should be equal to WebSocket
+ if self.request.headers.get("Upgrade", "").lower() != 'websocket':
+ self.set_status(400)
+ self.finish("Can \"Upgrade\" only to \"WebSocket\".")
+ return
+
+ # Connection header should be upgrade. Some proxy servers/load balancers
+ # might mess with it.
+ headers = self.request.headers
+ connection = map(lambda s: s.strip().lower(), headers.get("Connection", "").split(","))
+ if 'upgrade' not in connection:
+ self.set_status(400)
+ self.finish("\"Connection\" must be \"Upgrade\".")
+ return
+
+ # Handle WebSocket Origin naming convention differences
+ # The difference between version 8 and 13 is that in 8 the
+ # client sends a "Sec-Websocket-Origin" header and in 13 it's
+ # simply "Origin".
+ if "Origin" in self.request.headers:
+ origin = self.request.headers.get("Origin")
+ else:
+ origin = self.request.headers.get("Sec-Websocket-Origin", None)
+
+
+ # If there was an origin header, check to make sure it matches
+ # according to check_origin. When the origin is None, we assume it
+ # did not come from a browser and that it can be passed on.
+ if origin is not None and not self.check_origin(origin):
+ self.set_status(403)
+ self.finish("Cross origin websockets not allowed")
+ return
+
+ self.stream = self.request.connection.detach()
+ self.stream.set_close_callback(self.on_connection_close)
+
+ if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
+ self.ws_connection = WebSocketProtocol13(
+ self, compression_options=self.get_compression_options())
+ self.ws_connection.accept_connection()
+ #--------------- BEGIN PATCH ----------------
+ elif (self.allow_draft76() and
+ "Sec-WebSocket-Version" not in self.request.headers):
+ self.ws_connection = WebSocketProtocol76(self)
+ self.ws_connection.accept_connection()
+ #--------------- END PATCH ----------------
+ else:
+ if not self.stream.closed():
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 426 Upgrade Required\r\n"
+ "Sec-WebSocket-Version: 8\r\n\r\n"))
+ self.stream.close()
+
+ # 3.2 methods removed in 4.0:
+ def allow_draft76(self):
+ """Using this class allows draft76 connections by default"""
+ return True
+
+ def get_websocket_scheme(self):
+ """Return the url scheme used for this request, either "ws" or "wss".
+ This is normally decided by HTTPServer, but applications
+ may wish to override this if they are using an SSL proxy
+ that does not provide the X-Scheme header as understood
+ by HTTPServer.
+ Note that this is only used by the draft76 protocol.
+ """
+ return "wss" if self.request.protocol == "https" else "ws"
+
+
+
+# No modifications from tornado-3.2.2 below this line
+
+class WebSocketProtocol(object):
+ """Base class for WebSocket protocol versions.
+ """
+ def __init__(self, handler):
+ self.handler = handler
+ self.request = handler.request
+ self.stream = handler.stream
+ self.client_terminated = False
+ self.server_terminated = False
+
+ def async_callback(self, callback, *args, **kwargs):
+ """Wrap callbacks with this if they are used on asynchronous requests.
+
+ Catches exceptions properly and closes this WebSocket if an exception
+ is uncaught.
+ """
+ if args or kwargs:
+ callback = functools.partial(callback, *args, **kwargs)
+
+ def wrapper(*args, **kwargs):
+ try:
+ return callback(*args, **kwargs)
+ except Exception:
+ app_log.error("Uncaught exception in %s",
+ self.request.path, exc_info=True)
+ self._abort()
+ return wrapper
+
+ def on_connection_close(self):
+ self._abort()
+
+ def _abort(self):
+ """Instantly aborts the WebSocket connection by closing the socket"""
+ self.client_terminated = True
+ self.server_terminated = True
+ self.stream.close() # forcibly tear down the connection
+ self.close() # let the subclass cleanup
+
+
+class WebSocketProtocol76(WebSocketProtocol):
+ """Implementation of the WebSockets protocol, version hixie-76.
+
+ This class provides basic functionality to process WebSockets requests as
+ specified in
+ http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+ """
+ def __init__(self, handler):
+ WebSocketProtocol.__init__(self, handler)
+ self.challenge = None
+ self._waiting = None
+
+ def accept_connection(self):
+ try:
+ self._handle_websocket_headers()
+ except ValueError:
+ gen_log.debug("Malformed WebSocket request received")
+ self._abort()
+ return
+
+ scheme = self.handler.get_websocket_scheme()
+
+ # draft76 only allows a single subprotocol
+ subprotocol_header = ''
+ subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
+ if subprotocol:
+ selected = self.handler.select_subprotocol([subprotocol])
+ if selected:
+ assert selected == subprotocol
+ subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
+
+ # Write the initial headers before attempting to read the challenge.
+ # This is necessary when using proxies (such as HAProxy), which
+ # need to see the Upgrade headers before passing through the
+ # non-HTTP traffic that follows.
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
+ "Upgrade: WebSocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Server: TornadoServer/%(version)s\r\n"
+ "Sec-WebSocket-Origin: %(origin)s\r\n"
+ "Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
+ "%(subprotocol)s"
+ "\r\n" % (dict(
+ version=tornado.version,
+ origin=self.request.headers["Origin"],
+ scheme=scheme,
+ host=self.request.host,
+ uri=self.request.uri,
+ subprotocol=subprotocol_header))))
+ self.stream.read_bytes(8, self._handle_challenge)
+
+ def challenge_response(self, challenge):
+ """Generates the challenge response that's needed in the handshake
+
+ The challenge parameter should be the raw bytes as sent from the
+ client.
+ """
+ key_1 = self.request.headers.get("Sec-Websocket-Key1")
+ key_2 = self.request.headers.get("Sec-Websocket-Key2")
+ try:
+ part_1 = self._calculate_part(key_1)
+ part_2 = self._calculate_part(key_2)
+ except ValueError:
+ raise ValueError("Invalid Keys/Challenge")
+ return self._generate_challenge_response(part_1, part_2, challenge)
+
+ def _handle_challenge(self, challenge):
+ try:
+ challenge_response = self.challenge_response(challenge)
+ except ValueError:
+ gen_log.debug("Malformed key data in WebSocket request")
+ self._abort()
+ return
+ self._write_response(challenge_response)
+
+ def _write_response(self, challenge):
+ self.stream.write(challenge)
+ self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
+ self._receive_message()
+
+ def _handle_websocket_headers(self):
+ """Verifies all invariant- and required headers
+
+ If a header is missing or have an incorrect value ValueError will be
+ raised
+ """
+ fields = ("Origin", "Host", "Sec-Websocket-Key1",
+ "Sec-Websocket-Key2")
+ if not all(map(lambda f: self.request.headers.get(f), fields)):
+ raise ValueError("Missing/Invalid WebSocket headers")
+
+ def _calculate_part(self, key):
+ """Processes the key headers and calculates their key value.
+
+ Raises ValueError when feed invalid key."""
+ # pyflakes complains about variable reuse if both of these lines use 'c'
+ number = int(''.join(c for c in key if c.isdigit()))
+ spaces = len([c2 for c2 in key if c2.isspace()])
+ try:
+ key_number = number // spaces
+ except (ValueError, ZeroDivisionError):
+ raise ValueError
+ return struct.pack(">I", key_number)
+
+ def _generate_challenge_response(self, part_1, part_2, part_3):
+ m = hashlib.md5()
+ m.update(part_1)
+ m.update(part_2)
+ m.update(part_3)
+ return m.digest()
+
+ def _receive_message(self):
+ self.stream.read_bytes(1, self._on_frame_type)
+
+ def _on_frame_type(self, byte):
+ frame_type = ord(byte)
+ if frame_type == 0x00:
+ self.stream.read_until(b"\xff", self._on_end_delimiter)
+ elif frame_type == 0xff:
+ self.stream.read_bytes(1, self._on_length_indicator)
+ else:
+ self._abort()
+
+ def _on_end_delimiter(self, frame):
+ if not self.client_terminated:
+ self.async_callback(self.handler.on_message)(
+ frame[:-1].decode("utf-8", "replace"))
+ if not self.client_terminated:
+ self._receive_message()
+
+ def _on_length_indicator(self, byte):
+ if ord(byte) != 0x00:
+ self._abort()
+ return
+ self.client_terminated = True
+ self.close()
+
+ def write_message(self, message, binary=False):
+ """Sends the given message to the client of this Web Socket."""
+ if binary:
+ raise ValueError(
+ "Binary messages not supported by this version of websockets")
+ if isinstance(message, unicode_type):
+ message = message.encode("utf-8")
+ assert isinstance(message, bytes_type)
+ self.stream.write(b"\x00" + message + b"\xff")
+
+ def write_ping(self, data):
+ """Send ping frame."""
+ raise ValueError("Ping messages not supported by this version of websockets")
+
+ def close(self):
+ """Closes the WebSocket connection."""
+ if not self.server_terminated:
+ if not self.stream.closed():
+ self.stream.write("\xff\x00")
+ self.server_terminated = True
+ if self.client_terminated:
+ if self._waiting is not None:
+ self.stream.io_loop.remove_timeout(self._waiting)
+ self._waiting = None
+ self.stream.close()
+ elif self._waiting is None:
+ self._waiting = self.stream.io_loop.add_timeout(
+ time.time() + 5, self._abort)
+
diff --git a/IPython/html/base/zmqhandlers.py b/IPython/html/base/zmqhandlers.py
index e828366c0..3b72c860c 100644
--- a/IPython/html/base/zmqhandlers.py
+++ b/IPython/html/base/zmqhandlers.py
@@ -4,8 +4,10 @@
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
+import os
import json
import struct
+import warnings
try:
from urllib.parse import urlparse # Py 3
@@ -13,7 +15,8 @@ except ImportError:
from urlparse import urlparse # Py 2
import tornado
-from tornado import gen, ioloop, web, websocket
+from tornado import gen, ioloop, web
+from tornado.websocket import WebSocketHandler
from IPython.kernel.zmq.session import Session
from IPython.utils.jsonutil import date_default, extract_dates
@@ -21,7 +24,6 @@ from IPython.utils.py3compat import cast_unicode
from .handlers import IPythonHandler
-
def serialize_binary_message(msg):
"""serialize a message as a binary blob
@@ -79,8 +81,18 @@ def deserialize_binary_message(bmsg):
msg['buffers'] = bufs[1:]
return msg
+# ping interval for keeping websockets alive (30 seconds)
+WS_PING_INTERVAL = 30000
-class ZMQStreamHandler(websocket.WebSocketHandler):
+if os.environ.get('IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS', False):
+ warnings.warn("""Allowing draft76 websocket connections!
+ This should only be done for testing with phantomjs!""")
+ from IPython.html import allow76
+ WebSocketHandler = allow76.AllowDraftWebSocketHandler
+ # draft 76 doesn't support ping
+ WS_PING_INTERVAL = 0
+
+class ZMQStreamHandler(WebSocketHandler):
def check_origin(self, origin):
"""Check Origin == Host or Access-Control-Allow-Origin.
@@ -154,17 +166,6 @@ class ZMQStreamHandler(websocket.WebSocketHandler):
else:
self.write_message(msg, binary=isinstance(msg, bytes))
- def allow_draft76(self):
- """Allow draft 76, until browsers such as Safari update to RFC 6455.
-
- This has been disabled by default in tornado in release 2.2.0, and
- support will be removed in later versions.
- """
- return True
-
-# ping interval for keeping websockets alive (30 seconds)
-WS_PING_INTERVAL = 30000
-
class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
ping_callback = None
last_ping = 0
diff --git a/IPython/testing/iptestcontroller.py b/IPython/testing/iptestcontroller.py
index 899cad906..fed140501 100644
--- a/IPython/testing/iptestcontroller.py
+++ b/IPython/testing/iptestcontroller.py
@@ -325,7 +325,15 @@ class JSController(TestController):
command.append('--KernelManager.transport=ipc')
self.stream_capturer = c = StreamCapturer()
c.start()
- self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT, cwd=self.nbdir.name)
+ env = os.environ.copy()
+ if self.engine == 'phantomjs':
+ env['IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS'] = '1'
+ self.server = subprocess.Popen(command,
+ stdout=c.writefd,
+ stderr=subprocess.STDOUT,
+ cwd=self.nbdir.name,
+ env=env,
+ )
self.server_info_file = os.path.join(self.ipydir.name,
'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
)