##// END OF EJS Templates
Backport PR #6480: only compare host:port in Websocket.check_origin...
MinRK -
Show More
@@ -1,167 +1,176 b''
1 """Tornado handlers for WebSocket <-> ZMQ sockets.
1 """Tornado handlers for WebSocket <-> ZMQ sockets.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 try:
19 try:
20 from urllib.parse import urlparse # Py 3
20 from urllib.parse import urlparse # Py 3
21 except ImportError:
21 except ImportError:
22 from urlparse import urlparse # Py 2
22 from urlparse import urlparse # Py 2
23
23
24 try:
24 try:
25 from http.cookies import SimpleCookie # Py 3
25 from http.cookies import SimpleCookie # Py 3
26 except ImportError:
26 except ImportError:
27 from Cookie import SimpleCookie # Py 2
27 from Cookie import SimpleCookie # Py 2
28 import logging
28 import logging
29
29
30 import tornado
30 import tornado
31 from tornado import web
31 from tornado import web
32 from tornado import websocket
32 from tornado import websocket
33
33
34 from zmq.utils import jsonapi
34 from zmq.utils import jsonapi
35
35
36 from IPython.kernel.zmq.session import Session
36 from IPython.kernel.zmq.session import Session
37 from IPython.utils.jsonutil import date_default
37 from IPython.utils.jsonutil import date_default
38 from IPython.utils.py3compat import PY3, cast_unicode
38 from IPython.utils.py3compat import PY3, cast_unicode
39
39
40 from .handlers import IPythonHandler
40 from .handlers import IPythonHandler
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # ZMQ handlers
43 # ZMQ handlers
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46 class ZMQStreamHandler(websocket.WebSocketHandler):
46 class ZMQStreamHandler(websocket.WebSocketHandler):
47
47
48 def check_origin(self, origin):
48 def check_origin(self, origin):
49 """Check Origin == Host or Access-Control-Allow-Origin.
49 """Check Origin == Host or Access-Control-Allow-Origin.
50
50
51 Tornado >= 4 calls this method automatically, raising 403 if it returns False.
51 Tornado >= 4 calls this method automatically, raising 403 if it returns False.
52 We call it explicitly in `open` on Tornado < 4.
52 We call it explicitly in `open` on Tornado < 4.
53 """
53 """
54 if self.allow_origin == '*':
54 if self.allow_origin == '*':
55 return True
55 return True
56
56
57 host = self.request.headers.get("Host")
57 host = self.request.headers.get("Host")
58
58
59 # If no header is provided, assume we can't verify origin
59 # If no header is provided, assume we can't verify origin
60 if(origin is None or host is None):
60 if origin is None:
61 self.log.warn("Missing Origin header, rejecting WebSocket connection.")
62 return False
63 if host is None:
64 self.log.warn("Missing Host header, rejecting WebSocket connection.")
61 return False
65 return False
62
66
63 host_origin = "{0}://{1}".format(self.request.protocol, host)
67 origin = origin.lower()
68 origin_host = urlparse(origin).netloc
64
69
65 # OK if origin matches host
70 # OK if origin matches host
66 if origin == host_origin:
71 if origin_host == host:
67 return True
72 return True
68
73
69 # Check CORS headers
74 # Check CORS headers
70 if self.allow_origin:
75 if self.allow_origin:
71 return self.allow_origin == origin
76 allow = self.allow_origin == origin
72 elif self.allow_origin_pat:
77 elif self.allow_origin_pat:
73 return bool(self.allow_origin_pat.match(origin))
78 allow = bool(self.allow_origin_pat.match(origin))
74 else:
79 else:
75 # No CORS headers deny the request
80 # No CORS headers deny the request
76 return False
81 allow = False
82 if not allow:
83 self.log.warn("Blocking Cross Origin WebSocket Attempt. Origin: %s, Host: %s",
84 origin, host,
85 )
86 return allow
77
87
78 def clear_cookie(self, *args, **kwargs):
88 def clear_cookie(self, *args, **kwargs):
79 """meaningless for websockets"""
89 """meaningless for websockets"""
80 pass
90 pass
81
91
82 def _reserialize_reply(self, msg_list):
92 def _reserialize_reply(self, msg_list):
83 """Reserialize a reply message using JSON.
93 """Reserialize a reply message using JSON.
84
94
85 This takes the msg list from the ZMQ socket, unserializes it using
95 This takes the msg list from the ZMQ socket, unserializes it using
86 self.session and then serializes the result using JSON. This method
96 self.session and then serializes the result using JSON. This method
87 should be used by self._on_zmq_reply to build messages that can
97 should be used by self._on_zmq_reply to build messages that can
88 be sent back to the browser.
98 be sent back to the browser.
89 """
99 """
90 idents, msg_list = self.session.feed_identities(msg_list)
100 idents, msg_list = self.session.feed_identities(msg_list)
91 msg = self.session.unserialize(msg_list)
101 msg = self.session.unserialize(msg_list)
92 try:
102 try:
93 msg['header'].pop('date')
103 msg['header'].pop('date')
94 except KeyError:
104 except KeyError:
95 pass
105 pass
96 try:
106 try:
97 msg['parent_header'].pop('date')
107 msg['parent_header'].pop('date')
98 except KeyError:
108 except KeyError:
99 pass
109 pass
100 msg.pop('buffers')
110 msg.pop('buffers')
101 return jsonapi.dumps(msg, default=date_default)
111 return jsonapi.dumps(msg, default=date_default)
102
112
103 def _on_zmq_reply(self, msg_list):
113 def _on_zmq_reply(self, msg_list):
104 # Sometimes this gets triggered when the on_close method is scheduled in the
114 # Sometimes this gets triggered when the on_close method is scheduled in the
105 # eventloop but hasn't been called.
115 # eventloop but hasn't been called.
106 if self.stream.closed(): return
116 if self.stream.closed(): return
107 try:
117 try:
108 msg = self._reserialize_reply(msg_list)
118 msg = self._reserialize_reply(msg_list)
109 except Exception:
119 except Exception:
110 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
120 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
111 else:
121 else:
112 self.write_message(msg)
122 self.write_message(msg)
113
123
114 def allow_draft76(self):
124 def allow_draft76(self):
115 """Allow draft 76, until browsers such as Safari update to RFC 6455.
125 """Allow draft 76, until browsers such as Safari update to RFC 6455.
116
126
117 This has been disabled by default in tornado in release 2.2.0, and
127 This has been disabled by default in tornado in release 2.2.0, and
118 support will be removed in later versions.
128 support will be removed in later versions.
119 """
129 """
120 return True
130 return True
121
131
122
132
123 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
133 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
124 def set_default_headers(self):
134 def set_default_headers(self):
125 """Undo the set_default_headers in IPythonHandler
135 """Undo the set_default_headers in IPythonHandler
126
136
127 which doesn't make sense for websockets
137 which doesn't make sense for websockets
128 """
138 """
129 pass
139 pass
130
140
131 def open(self, kernel_id):
141 def open(self, kernel_id):
132 self.kernel_id = cast_unicode(kernel_id, 'ascii')
142 self.kernel_id = cast_unicode(kernel_id, 'ascii')
133 # Check to see that origin matches host directly, including ports
143 # Check to see that origin matches host directly, including ports
134 # Tornado 4 already does CORS checking
144 # Tornado 4 already does CORS checking
135 if tornado.version_info[0] < 4:
145 if tornado.version_info[0] < 4:
136 if not self.check_origin(self.get_origin()):
146 if not self.check_origin(self.get_origin()):
137 self.log.warn("Cross Origin WebSocket Attempt from %s", self.get_origin())
138 raise web.HTTPError(403)
147 raise web.HTTPError(403)
139
148
140 self.session = Session(config=self.config)
149 self.session = Session(config=self.config)
141 self.save_on_message = self.on_message
150 self.save_on_message = self.on_message
142 self.on_message = self.on_first_message
151 self.on_message = self.on_first_message
143
152
144 def _inject_cookie_message(self, msg):
153 def _inject_cookie_message(self, msg):
145 """Inject the first message, which is the document cookie,
154 """Inject the first message, which is the document cookie,
146 for authentication."""
155 for authentication."""
147 if not PY3 and isinstance(msg, unicode):
156 if not PY3 and isinstance(msg, unicode):
148 # Cookie constructor doesn't accept unicode strings
157 # Cookie constructor doesn't accept unicode strings
149 # under Python 2.x for some reason
158 # under Python 2.x for some reason
150 msg = msg.encode('utf8', 'replace')
159 msg = msg.encode('utf8', 'replace')
151 try:
160 try:
152 identity, msg = msg.split(':', 1)
161 identity, msg = msg.split(':', 1)
153 self.session.session = cast_unicode(identity, 'ascii')
162 self.session.session = cast_unicode(identity, 'ascii')
154 except Exception:
163 except Exception:
155 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
164 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
156
165
157 try:
166 try:
158 self.request._cookies = SimpleCookie(msg)
167 self.request._cookies = SimpleCookie(msg)
159 except:
168 except:
160 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
169 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
161
170
162 def on_first_message(self, msg):
171 def on_first_message(self, msg):
163 self._inject_cookie_message(msg)
172 self._inject_cookie_message(msg)
164 if self.get_current_user() is None:
173 if self.get_current_user() is None:
165 self.log.warn("Couldn't authenticate WebSocket connection")
174 self.log.warn("Couldn't authenticate WebSocket connection")
166 raise web.HTTPError(403)
175 raise web.HTTPError(403)
167 self.on_message = self.save_on_message
176 self.on_message = self.save_on_message
General Comments 0
You need to be logged in to leave comments. Login now