Show More
@@ -153,6 +153,48 b' class IPythonHandler(AuthenticatedHandler):' | |||||
153 | return self.notebook_manager.notebook_dir |
|
153 | return self.notebook_manager.notebook_dir | |
154 |
|
154 | |||
155 | #--------------------------------------------------------------- |
|
155 | #--------------------------------------------------------------- | |
|
156 | # CORS | |||
|
157 | #--------------------------------------------------------------- | |||
|
158 | ||||
|
159 | @property | |||
|
160 | def allow_origin(self): | |||
|
161 | """Normal Access-Control-Allow-Origin""" | |||
|
162 | return self.settings.get('allow_origin', '') | |||
|
163 | ||||
|
164 | @property | |||
|
165 | def allow_origin_pat(self): | |||
|
166 | """Regular expression version of allow_origin""" | |||
|
167 | return self.settings.get('allow_origin_pat', None) | |||
|
168 | ||||
|
169 | @property | |||
|
170 | def allow_credentials(self): | |||
|
171 | """Whether to set Access-Control-Allow-Credentials""" | |||
|
172 | return self.settings.get('allow_credentials', False) | |||
|
173 | ||||
|
174 | def set_default_headers(self): | |||
|
175 | """Add CORS headers, if defined""" | |||
|
176 | super(IPythonHandler, self).set_default_headers() | |||
|
177 | if self.allow_origin: | |||
|
178 | self.set_header("Access-Control-Allow-Origin", self.allow_origin) | |||
|
179 | elif self.allow_origin_pat: | |||
|
180 | origin = self.get_origin() | |||
|
181 | if origin and self.allow_origin_pat.match(origin): | |||
|
182 | self.set_header("Access-Control-Allow-Origin", origin) | |||
|
183 | if self.allow_credentials: | |||
|
184 | self.set_header("Access-Control-Allow-Credentials", 'true') | |||
|
185 | ||||
|
186 | def get_origin(self): | |||
|
187 | # Handle WebSocket Origin naming convention differences | |||
|
188 | # The difference between version 8 and 13 is that in 8 the | |||
|
189 | # client sends a "Sec-Websocket-Origin" header and in 13 it's | |||
|
190 | # simply "Origin". | |||
|
191 | if "Origin" in self.request.headers: | |||
|
192 | origin = self.request.headers.get("Origin") | |||
|
193 | else: | |||
|
194 | origin = self.request.headers.get("Sec-Websocket-Origin", None) | |||
|
195 | return origin | |||
|
196 | ||||
|
197 | #--------------------------------------------------------------- | |||
156 | # template rendering |
|
198 | # template rendering | |
157 | #--------------------------------------------------------------- |
|
199 | #--------------------------------------------------------------- | |
158 |
|
200 |
@@ -15,6 +15,8 b' try:' | |||||
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 | ||||
|
19 | import tornado | |||
18 | from tornado import web |
|
20 | from tornado import web | |
19 | from tornado import websocket |
|
21 | from tornado import websocket | |
20 |
|
22 | |||
@@ -26,29 +28,36 b' from .handlers import IPythonHandler' | |||||
26 |
|
28 | |||
27 |
|
29 | |||
28 | class ZMQStreamHandler(websocket.WebSocketHandler): |
|
30 | class ZMQStreamHandler(websocket.WebSocketHandler): | |
29 |
|
31 | |||
30 |
def |
|
32 | def check_origin(self, origin): | |
31 | """Check to see that origin and host match in the headers.""" |
|
33 | """Check Origin == Host or Access-Control-Allow-Origin. | |
32 |
|
34 | |||
33 | # The difference between version 8 and 13 is that in 8 the |
|
35 | Tornado >= 4 calls this method automatically, raising 403 if it returns False. | |
34 | # client sends a "Sec-Websocket-Origin" header and in 13 it's |
|
36 | We call it explicitly in `open` on Tornado < 4. | |
35 | # simply "Origin". |
|
37 | """ | |
36 | if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8"): |
|
38 | if self.allow_origin == '*': | |
37 | origin_header = self.request.headers.get("Sec-Websocket-Origin") |
|
39 | return True | |
38 | else: |
|
|||
39 | origin_header = self.request.headers.get("Origin") |
|
|||
40 |
|
40 | |||
41 | host = self.request.headers.get("Host") |
|
41 | host = self.request.headers.get("Host") | |
42 |
|
42 | |||
43 | # If no header is provided, assume we can't verify origin |
|
43 | # If no header is provided, assume we can't verify origin | |
44 |
if(origin |
|
44 | if(origin is None or host is None): | |
|
45 | return False | |||
|
46 | ||||
|
47 | host_origin = "{0}://{1}".format(self.request.protocol, host) | |||
|
48 | ||||
|
49 | # OK if origin matches host | |||
|
50 | if origin == host_origin: | |||
|
51 | return True | |||
|
52 | ||||
|
53 | # Check CORS headers | |||
|
54 | if self.allow_origin: | |||
|
55 | return self.allow_origin == origin | |||
|
56 | elif self.allow_origin_pat: | |||
|
57 | return bool(self.allow_origin_pat.match(origin)) | |||
|
58 | else: | |||
|
59 | # No CORS headers deny the request | |||
45 | return False |
|
60 | return False | |
46 |
|
||||
47 | parsed_origin = urlparse(origin_header) |
|
|||
48 | origin = parsed_origin.netloc |
|
|||
49 |
|
||||
50 | # Check to see that origin matches host directly, including ports |
|
|||
51 | return origin == host |
|
|||
52 |
|
61 | |||
53 | def clear_cookie(self, *args, **kwargs): |
|
62 | def clear_cookie(self, *args, **kwargs): | |
54 | """meaningless for websockets""" |
|
63 | """meaningless for websockets""" | |
@@ -96,13 +105,21 b' class ZMQStreamHandler(websocket.WebSocketHandler):' | |||||
96 |
|
105 | |||
97 |
|
106 | |||
98 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): |
|
107 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): | |
|
108 | def set_default_headers(self): | |||
|
109 | """Undo the set_default_headers in IPythonHandler | |||
|
110 | ||||
|
111 | which doesn't make sense for websockets | |||
|
112 | """ | |||
|
113 | pass | |||
99 |
|
114 | |||
100 | def open(self, kernel_id): |
|
115 | def open(self, kernel_id): | |
101 | self.kernel_id = cast_unicode(kernel_id, 'ascii') |
|
116 | self.kernel_id = cast_unicode(kernel_id, 'ascii') | |
102 | # Check to see that origin matches host directly, including ports |
|
117 | # Check to see that origin matches host directly, including ports | |
103 | if not self.same_origin(): |
|
118 | # Tornado 4 already does CORS checking | |
104 | self.log.warn("Cross Origin WebSocket Attempt.") |
|
119 | if tornado.version_info[0] < 4: | |
105 | raise web.HTTPError(404) |
|
120 | if not self.check_origin(self.get_origin()): | |
|
121 | self.log.warn("Cross Origin WebSocket Attempt from %s", self.get_origin()) | |||
|
122 | raise web.HTTPError(403) | |||
106 |
|
123 | |||
107 | self.session = Session(config=self.config) |
|
124 | self.session = Session(config=self.config) | |
108 | self.save_on_message = self.on_message |
|
125 | self.save_on_message = self.on_message |
@@ -13,6 +13,7 b' import json' | |||||
13 | import logging |
|
13 | import logging | |
14 | import os |
|
14 | import os | |
15 | import random |
|
15 | import random | |
|
16 | import re | |||
16 | import select |
|
17 | import select | |
17 | import signal |
|
18 | import signal | |
18 | import socket |
|
19 | import socket | |
@@ -334,8 +335,34 b' class NotebookApp(BaseIPythonApplication):' | |||||
334 | self.file_to_run = base |
|
335 | self.file_to_run = base | |
335 | self.notebook_dir = path |
|
336 | self.notebook_dir = path | |
336 |
|
337 | |||
337 |
# Network related information |
|
338 | # Network related information | |
338 |
|
339 | |||
|
340 | allow_origin = Unicode('', config=True, | |||
|
341 | help="""Set the Access-Control-Allow-Origin header | |||
|
342 | ||||
|
343 | Use '*' to allow any origin to access your server. | |||
|
344 | ||||
|
345 | Takes precedence over allow_origin_pat. | |||
|
346 | """ | |||
|
347 | ) | |||
|
348 | ||||
|
349 | allow_origin_pat = Unicode('', config=True, | |||
|
350 | help="""Use a regular expression for the Access-Control-Allow-Origin header | |||
|
351 | ||||
|
352 | Requests from an origin matching the expression will get replies with: | |||
|
353 | ||||
|
354 | Access-Control-Allow-Origin: origin | |||
|
355 | ||||
|
356 | where `origin` is the origin of the request. | |||
|
357 | ||||
|
358 | Ignored if allow_origin is set. | |||
|
359 | """ | |||
|
360 | ) | |||
|
361 | ||||
|
362 | allow_credentials = Bool(False, config=True, | |||
|
363 | help="Set the Access-Control-Allow-Credentials: true header" | |||
|
364 | ) | |||
|
365 | ||||
339 | ip = Unicode('localhost', config=True, |
|
366 | ip = Unicode('localhost', config=True, | |
340 | help="The IP address the notebook server will listen on." |
|
367 | help="The IP address the notebook server will listen on." | |
341 | ) |
|
368 | ) | |
@@ -650,6 +677,10 b' class NotebookApp(BaseIPythonApplication):' | |||||
650 |
|
677 | |||
651 | def init_webapp(self): |
|
678 | def init_webapp(self): | |
652 | """initialize tornado webapp and httpserver""" |
|
679 | """initialize tornado webapp and httpserver""" | |
|
680 | self.webapp_settings['allow_origin'] = self.allow_origin | |||
|
681 | self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat) | |||
|
682 | self.webapp_settings['allow_credentials'] = self.allow_credentials | |||
|
683 | ||||
653 | self.web_app = NotebookWebApplication( |
|
684 | self.web_app = NotebookWebApplication( | |
654 | self, self.kernel_manager, self.notebook_manager, |
|
685 | self, self.kernel_manager, self.notebook_manager, | |
655 | self.cluster_manager, self.session_manager, self.kernel_spec_manager, |
|
686 | self.cluster_manager, self.session_manager, self.kernel_spec_manager, |
General Comments 0
You need to be logged in to leave comments.
Login now