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 cors_origin(self): | |||
|
161 | """Normal Access-Control-Allow-Origin""" | |||
|
162 | return self.settings.get('cors_origin', '') | |||
|
163 | ||||
|
164 | @property | |||
|
165 | def cors_origin_pat(self): | |||
|
166 | """Regular expression version of cors_origin""" | |||
|
167 | return self.settings.get('cors_origin_pat', None) | |||
|
168 | ||||
|
169 | @property | |||
|
170 | def cors_credentials(self): | |||
|
171 | """Whether to set Access-Control-Allow-Credentials""" | |||
|
172 | return self.settings.get('cors_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.cors_origin: | |||
|
178 | self.set_header("Access-Control-Allow-Origin", self.cors_origin) | |||
|
179 | elif self.cors_origin_pat: | |||
|
180 | origin = self.get_origin() | |||
|
181 | if origin and self.cors_origin_pat.match(origin): | |||
|
182 | self.set_header("Access-Control-Allow-Origin", origin) | |||
|
183 | if self.cors_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,35 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 CORS origins.""" | |
32 |
|
34 | if self.cors_origin == '*': | ||
33 | # The difference between version 8 and 13 is that in 8 the |
|
35 | return True | |
34 | # client sends a "Sec-Websocket-Origin" header and in 13 it's |
|
|||
35 | # simply "Origin". |
|
|||
36 | if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8"): |
|
|||
37 | origin_header = self.request.headers.get("Sec-Websocket-Origin") |
|
|||
38 | else: |
|
|||
39 | origin_header = self.request.headers.get("Origin") |
|
|||
40 |
|
36 | |||
41 | host = self.request.headers.get("Host") |
|
37 | host = self.request.headers.get("Host") | |
42 |
|
38 | |||
43 | # If no header is provided, assume we can't verify origin |
|
39 | # If no header is provided, assume we can't verify origin | |
44 |
if(origin |
|
40 | if(origin is None or host is None): | |
|
41 | return False | |||
|
42 | ||||
|
43 | host_origin = "{0}://{1}".format(self.request.protocol, host) | |||
|
44 | ||||
|
45 | # OK if origin matches host | |||
|
46 | if origin == host_origin: | |||
|
47 | return True | |||
|
48 | ||||
|
49 | # Check CORS headers | |||
|
50 | if self.cors_origin: | |||
|
51 | if self.cors_origin == '*': | |||
|
52 | return True | |||
|
53 | else: | |||
|
54 | return self.cors_origin == origin | |||
|
55 | elif self.cors_origin_pat: | |||
|
56 | return bool(self.cors_origin_pat.match(origin)) | |||
|
57 | else: | |||
|
58 | # No CORS headers, deny the request | |||
45 | return False |
|
59 | 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 |
|
60 | |||
53 | def clear_cookie(self, *args, **kwargs): |
|
61 | def clear_cookie(self, *args, **kwargs): | |
54 | """meaningless for websockets""" |
|
62 | """meaningless for websockets""" | |
@@ -96,13 +104,21 b' class ZMQStreamHandler(websocket.WebSocketHandler):' | |||||
96 |
|
104 | |||
97 |
|
105 | |||
98 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): |
|
106 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): | |
|
107 | def set_default_headers(self): | |||
|
108 | """Undo the set_default_headers in IPythonHandler | |||
|
109 | ||||
|
110 | which doesn't make sense for websockets | |||
|
111 | """ | |||
|
112 | pass | |||
99 |
|
113 | |||
100 | def open(self, kernel_id): |
|
114 | def open(self, kernel_id): | |
101 | self.kernel_id = cast_unicode(kernel_id, 'ascii') |
|
115 | self.kernel_id = cast_unicode(kernel_id, 'ascii') | |
102 | # Check to see that origin matches host directly, including ports |
|
116 | # Check to see that origin matches host directly, including ports | |
103 | if not self.same_origin(): |
|
117 | # Tornado 4 already does CORS checking | |
104 | self.log.warn("Cross Origin WebSocket Attempt.") |
|
118 | if tornado.version_info[0] < 4: | |
105 | raise web.HTTPError(404) |
|
119 | if not self.check_origin(self.get_origin()): | |
|
120 | self.log.warn("Cross Origin WebSocket Attempt.") | |||
|
121 | raise web.HTTPError(404) | |||
106 |
|
122 | |||
107 | self.session = Session(config=self.config) |
|
123 | self.session = Session(config=self.config) | |
108 | self.save_on_message = self.on_message |
|
124 | self.save_on_message = self.on_message |
@@ -12,6 +12,7 b' import json' | |||||
12 | import logging |
|
12 | import logging | |
13 | import os |
|
13 | import os | |
14 | import random |
|
14 | import random | |
|
15 | import re | |||
15 | import select |
|
16 | import select | |
16 | import signal |
|
17 | import signal | |
17 | import socket |
|
18 | import socket | |
@@ -333,8 +334,34 b' class NotebookApp(BaseIPythonApplication):' | |||||
333 | self.file_to_run = base |
|
334 | self.file_to_run = base | |
334 | self.notebook_dir = path |
|
335 | self.notebook_dir = path | |
335 |
|
336 | |||
336 |
# Network related information |
|
337 | # Network related information | |
337 |
|
338 | |||
|
339 | cors_origin = Unicode('', config=True, | |||
|
340 | help="""Set the Access-Control-Allow-Origin header | |||
|
341 | ||||
|
342 | Use '*' to allow any origin to access your server. | |||
|
343 | ||||
|
344 | Mutually exclusive with cors_origin_pat. | |||
|
345 | """ | |||
|
346 | ) | |||
|
347 | ||||
|
348 | cors_origin_pat = Unicode('', config=True, | |||
|
349 | help="""Use a regular expression for the Access-Control-Allow-Origin header | |||
|
350 | ||||
|
351 | Requests from an origin matching the expression will get replies with: | |||
|
352 | ||||
|
353 | Access-Control-Allow-Origin: origin | |||
|
354 | ||||
|
355 | where `origin` is the origin of the request. | |||
|
356 | ||||
|
357 | Mutually exclusive with cors_origin. | |||
|
358 | """ | |||
|
359 | ) | |||
|
360 | ||||
|
361 | cors_credentials = Bool(False, config=True, | |||
|
362 | help="Set the Access-Control-Allow-Credentials: true header" | |||
|
363 | ) | |||
|
364 | ||||
338 | ip = Unicode('localhost', config=True, |
|
365 | ip = Unicode('localhost', config=True, | |
339 | help="The IP address the notebook server will listen on." |
|
366 | help="The IP address the notebook server will listen on." | |
340 | ) |
|
367 | ) | |
@@ -622,6 +649,10 b' class NotebookApp(BaseIPythonApplication):' | |||||
622 |
|
649 | |||
623 | def init_webapp(self): |
|
650 | def init_webapp(self): | |
624 | """initialize tornado webapp and httpserver""" |
|
651 | """initialize tornado webapp and httpserver""" | |
|
652 | self.webapp_settings['cors_origin'] = self.cors_origin | |||
|
653 | self.webapp_settings['cors_origin_pat'] = re.compile(self.cors_origin_pat) | |||
|
654 | self.webapp_settings['cors_credentials'] = self.cors_credentials | |||
|
655 | ||||
625 | self.web_app = NotebookWebApplication( |
|
656 | self.web_app = NotebookWebApplication( | |
626 | self, self.kernel_manager, self.notebook_manager, |
|
657 | self, self.kernel_manager, self.notebook_manager, | |
627 | self.cluster_manager, self.session_manager, self.kernel_spec_manager, |
|
658 | self.cluster_manager, self.session_manager, self.kernel_spec_manager, |
General Comments 0
You need to be logged in to leave comments.
Login now