##// END OF EJS Templates
allow session_id to be undefined when starting kernel channels
MinRK -
Show More
@@ -1,202 +1,202 b''
1 1 """Tornado handlers for WebSocket <-> ZMQ sockets."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 8 try:
9 9 from urllib.parse import urlparse # Py 3
10 10 except ImportError:
11 11 from urlparse import urlparse # Py 2
12 12
13 13 try:
14 14 from http.cookies import SimpleCookie # Py 3
15 15 except ImportError:
16 16 from Cookie import SimpleCookie # Py 2
17 17 import logging
18 18
19 19 import tornado
20 20 from tornado import ioloop
21 21 from tornado import web
22 22 from tornado import websocket
23 23
24 24 from IPython.kernel.zmq.session import Session
25 25 from IPython.utils.jsonutil import date_default
26 26 from IPython.utils.py3compat import PY3, cast_unicode
27 27
28 28 from .handlers import IPythonHandler
29 29
30 30
31 31 class ZMQStreamHandler(websocket.WebSocketHandler):
32 32
33 33 def check_origin(self, origin):
34 34 """Check Origin == Host or Access-Control-Allow-Origin.
35 35
36 36 Tornado >= 4 calls this method automatically, raising 403 if it returns False.
37 37 We call it explicitly in `open` on Tornado < 4.
38 38 """
39 39 if self.allow_origin == '*':
40 40 return True
41 41
42 42 host = self.request.headers.get("Host")
43 43
44 44 # If no header is provided, assume we can't verify origin
45 45 if origin is None:
46 46 self.log.warn("Missing Origin header, rejecting WebSocket connection.")
47 47 return False
48 48 if host is None:
49 49 self.log.warn("Missing Host header, rejecting WebSocket connection.")
50 50 return False
51 51
52 52 origin = origin.lower()
53 53 origin_host = urlparse(origin).netloc
54 54
55 55 # OK if origin matches host
56 56 if origin_host == host:
57 57 return True
58 58
59 59 # Check CORS headers
60 60 if self.allow_origin:
61 61 allow = self.allow_origin == origin
62 62 elif self.allow_origin_pat:
63 63 allow = bool(self.allow_origin_pat.match(origin))
64 64 else:
65 65 # No CORS headers deny the request
66 66 allow = False
67 67 if not allow:
68 68 self.log.warn("Blocking Cross Origin WebSocket Attempt. Origin: %s, Host: %s",
69 69 origin, host,
70 70 )
71 71 return allow
72 72
73 73 def clear_cookie(self, *args, **kwargs):
74 74 """meaningless for websockets"""
75 75 pass
76 76
77 77 def _reserialize_reply(self, msg_list):
78 78 """Reserialize a reply message using JSON.
79 79
80 80 This takes the msg list from the ZMQ socket, unserializes it using
81 81 self.session and then serializes the result using JSON. This method
82 82 should be used by self._on_zmq_reply to build messages that can
83 83 be sent back to the browser.
84 84 """
85 85 idents, msg_list = self.session.feed_identities(msg_list)
86 86 msg = self.session.unserialize(msg_list)
87 87 try:
88 88 msg['header'].pop('date')
89 89 except KeyError:
90 90 pass
91 91 try:
92 92 msg['parent_header'].pop('date')
93 93 except KeyError:
94 94 pass
95 95 msg.pop('buffers')
96 96 return json.dumps(msg, default=date_default)
97 97
98 98 def _on_zmq_reply(self, msg_list):
99 99 # Sometimes this gets triggered when the on_close method is scheduled in the
100 100 # eventloop but hasn't been called.
101 101 if self.stream.closed(): return
102 102 try:
103 103 msg = self._reserialize_reply(msg_list)
104 104 except Exception:
105 105 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
106 106 else:
107 107 self.write_message(msg)
108 108
109 109 def allow_draft76(self):
110 110 """Allow draft 76, until browsers such as Safari update to RFC 6455.
111 111
112 112 This has been disabled by default in tornado in release 2.2.0, and
113 113 support will be removed in later versions.
114 114 """
115 115 return True
116 116
117 117 # ping interval for keeping websockets alive (30 seconds)
118 118 WS_PING_INTERVAL = 30000
119 119
120 120 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
121 121 ping_callback = None
122 122 last_ping = 0
123 123 last_pong = 0
124 124
125 125 @property
126 126 def ping_interval(self):
127 127 """The interval for websocket keep-alive pings.
128 128
129 129 Set ws_ping_interval = 0 to disable pings.
130 130 """
131 131 return self.settings.get('ws_ping_interval', WS_PING_INTERVAL)
132 132
133 133 @property
134 134 def ping_timeout(self):
135 135 """If no ping is received in this many milliseconds,
136 136 close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
137 137 Default is max of 3 pings or 30 seconds.
138 138 """
139 139 return self.settings.get('ws_ping_timeout',
140 140 max(3 * self.ping_interval, WS_PING_INTERVAL)
141 141 )
142 142
143 143 def set_default_headers(self):
144 144 """Undo the set_default_headers in IPythonHandler
145 145
146 146 which doesn't make sense for websockets
147 147 """
148 148 pass
149 149
150 150 def get(self, *args, **kwargs):
151 151 # Check to see that origin matches host directly, including ports
152 152 # Tornado 4 already does CORS checking
153 153 if tornado.version_info[0] < 4:
154 154 if not self.check_origin(self.get_origin()):
155 155 raise web.HTTPError(403)
156 156
157 157 # authenticate the request before opening the websocket
158 158 if self.get_current_user() is None:
159 159 self.log.warn("Couldn't authenticate WebSocket connection")
160 160 raise web.HTTPError(403)
161 161
162 if self.get_argument('session_id'):
162 if self.get_argument('session_id', False):
163 163 self.session.session = cast_unicode(self.get_argument('session_id'))
164 164 else:
165 165 self.log.warn("No session ID specified")
166 166
167 167 return super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
168 168
169 169 def initialize(self):
170 170 self.session = Session(config=self.config)
171 171
172 172 def open(self, kernel_id):
173 173 self.kernel_id = cast_unicode(kernel_id, 'ascii')
174 174
175 175 # start the pinging
176 176 if self.ping_interval > 0:
177 177 self.last_ping = ioloop.IOLoop.instance().time() # Remember time of last ping
178 178 self.last_pong = self.last_ping
179 179 self.ping_callback = ioloop.PeriodicCallback(self.send_ping, self.ping_interval)
180 180 self.ping_callback.start()
181 181
182 182 def send_ping(self):
183 183 """send a ping to keep the websocket alive"""
184 184 if self.stream.closed() and self.ping_callback is not None:
185 185 self.ping_callback.stop()
186 186 return
187 187
188 188 # check for timeout on pong. Make sure that we really have sent a recent ping in
189 189 # case the machine with both server and client has been suspended since the last ping.
190 190 now = ioloop.IOLoop.instance().time()
191 191 since_last_pong = 1e3 * (now - self.last_pong)
192 192 since_last_ping = 1e3 * (now - self.last_ping)
193 193 if since_last_ping < 2*self.ping_interval and since_last_pong > self.ping_timeout:
194 194 self.log.warn("WebSocket ping timeout after %i ms.", since_last_pong)
195 195 self.close()
196 196 return
197 197
198 198 self.ping(b'')
199 199 self.last_ping = now
200 200
201 201 def on_pong(self, data):
202 202 self.last_pong = ioloop.IOLoop.instance().time()
General Comments 0
You need to be logged in to leave comments. Login now