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