##// END OF EJS Templates
only compare host:port in Websocket.check_origin...
MinRK -
Show More
@@ -1,205 +1,206 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 or host is None):
46 46 return False
47 47
48 host_origin = "{0}://{1}".format(self.request.protocol, host)
48 origin = origin.lower()
49 origin_host = urlparse(origin).netloc
49 50
50 51 # OK if origin matches host
51 if origin == host_origin:
52 if origin_host == host:
52 53 return True
53 54
54 55 # Check CORS headers
55 56 if self.allow_origin:
56 57 return self.allow_origin == origin
57 58 elif self.allow_origin_pat:
58 59 return bool(self.allow_origin_pat.match(origin))
59 60 else:
60 61 # No CORS headers deny the request
62 self.log.warn("Cross Origin WebSocket Attempt from %s", self.get_origin())
61 63 return False
62 64
63 65 def clear_cookie(self, *args, **kwargs):
64 66 """meaningless for websockets"""
65 67 pass
66 68
67 69 def _reserialize_reply(self, msg_list):
68 70 """Reserialize a reply message using JSON.
69 71
70 72 This takes the msg list from the ZMQ socket, unserializes it using
71 73 self.session and then serializes the result using JSON. This method
72 74 should be used by self._on_zmq_reply to build messages that can
73 75 be sent back to the browser.
74 76 """
75 77 idents, msg_list = self.session.feed_identities(msg_list)
76 78 msg = self.session.unserialize(msg_list)
77 79 try:
78 80 msg['header'].pop('date')
79 81 except KeyError:
80 82 pass
81 83 try:
82 84 msg['parent_header'].pop('date')
83 85 except KeyError:
84 86 pass
85 87 msg.pop('buffers')
86 88 return json.dumps(msg, default=date_default)
87 89
88 90 def _on_zmq_reply(self, msg_list):
89 91 # Sometimes this gets triggered when the on_close method is scheduled in the
90 92 # eventloop but hasn't been called.
91 93 if self.stream.closed(): return
92 94 try:
93 95 msg = self._reserialize_reply(msg_list)
94 96 except Exception:
95 97 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
96 98 else:
97 99 self.write_message(msg)
98 100
99 101 def allow_draft76(self):
100 102 """Allow draft 76, until browsers such as Safari update to RFC 6455.
101 103
102 104 This has been disabled by default in tornado in release 2.2.0, and
103 105 support will be removed in later versions.
104 106 """
105 107 return True
106 108
107 109 # ping interval for keeping websockets alive (30 seconds)
108 110 WS_PING_INTERVAL = 30000
109 111
110 112 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
111 113 ping_callback = None
112 114 last_ping = 0
113 115 last_pong = 0
114 116
115 117 @property
116 118 def ping_interval(self):
117 119 """The interval for websocket keep-alive pings.
118 120
119 121 Set ws_ping_interval = 0 to disable pings.
120 122 """
121 123 return self.settings.get('ws_ping_interval', WS_PING_INTERVAL)
122 124
123 125 @property
124 126 def ping_timeout(self):
125 127 """If no ping is received in this many milliseconds,
126 128 close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
127 129 Default is max of 3 pings or 30 seconds.
128 130 """
129 131 return self.settings.get('ws_ping_timeout',
130 132 max(3 * self.ping_interval, WS_PING_INTERVAL)
131 133 )
132 134
133 135 def set_default_headers(self):
134 136 """Undo the set_default_headers in IPythonHandler
135 137
136 138 which doesn't make sense for websockets
137 139 """
138 140 pass
139 141
140 142 def open(self, kernel_id):
141 143 self.kernel_id = cast_unicode(kernel_id, 'ascii')
142 144 # Check to see that origin matches host directly, including ports
143 145 # Tornado 4 already does CORS checking
144 146 if tornado.version_info[0] < 4:
145 147 if not self.check_origin(self.get_origin()):
146 self.log.warn("Cross Origin WebSocket Attempt from %s", self.get_origin())
147 148 raise web.HTTPError(403)
148 149
149 150 self.session = Session(config=self.config)
150 151 self.save_on_message = self.on_message
151 152 self.on_message = self.on_first_message
152 153
153 154 # start the pinging
154 155 if self.ping_interval > 0:
155 156 self.last_ping = ioloop.IOLoop.instance().time() # Remember time of last ping
156 157 self.last_pong = self.last_ping
157 158 self.ping_callback = ioloop.PeriodicCallback(self.send_ping, self.ping_interval)
158 159 self.ping_callback.start()
159 160
160 161 def send_ping(self):
161 162 """send a ping to keep the websocket alive"""
162 163 if self.stream.closed() and self.ping_callback is not None:
163 164 self.ping_callback.stop()
164 165 return
165 166
166 167 # check for timeout on pong. Make sure that we really have sent a recent ping in
167 168 # case the machine with both server and client has been suspended since the last ping.
168 169 now = ioloop.IOLoop.instance().time()
169 170 since_last_pong = 1e3 * (now - self.last_pong)
170 171 since_last_ping = 1e3 * (now - self.last_ping)
171 172 if since_last_ping < 2*self.ping_interval and since_last_pong > self.ping_timeout:
172 173 self.log.warn("WebSocket ping timeout after %i ms.", since_last_pong)
173 174 self.close()
174 175 return
175 176
176 177 self.ping(b'')
177 178 self.last_ping = now
178 179
179 180 def on_pong(self, data):
180 181 self.last_pong = ioloop.IOLoop.instance().time()
181 182
182 183 def _inject_cookie_message(self, msg):
183 184 """Inject the first message, which is the document cookie,
184 185 for authentication."""
185 186 if not PY3 and isinstance(msg, unicode):
186 187 # Cookie constructor doesn't accept unicode strings
187 188 # under Python 2.x for some reason
188 189 msg = msg.encode('utf8', 'replace')
189 190 try:
190 191 identity, msg = msg.split(':', 1)
191 192 self.session.session = cast_unicode(identity, 'ascii')
192 193 except Exception:
193 194 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
194 195
195 196 try:
196 197 self.request._cookies = SimpleCookie(msg)
197 198 except:
198 199 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
199 200
200 201 def on_first_message(self, msg):
201 202 self._inject_cookie_message(msg)
202 203 if self.get_current_user() is None:
203 204 self.log.warn("Couldn't authenticate WebSocket connection")
204 205 raise web.HTTPError(403)
205 206 self.on_message = self.save_on_message
General Comments 0
You need to be logged in to leave comments. Login now