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