Show More
@@ -153,6 +153,48 b' class IPythonHandler(AuthenticatedHandler):' | |||
|
153 | 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 | 198 | # template rendering |
|
157 | 199 | #--------------------------------------------------------------- |
|
158 | 200 |
@@ -15,6 +15,8 b' try:' | |||
|
15 | 15 | except ImportError: |
|
16 | 16 | from Cookie import SimpleCookie # Py 2 |
|
17 | 17 | import logging |
|
18 | ||
|
19 | import tornado | |
|
18 | 20 | from tornado import web |
|
19 | 21 | from tornado import websocket |
|
20 | 22 | |
@@ -26,29 +28,35 b' from .handlers import IPythonHandler' | |||
|
26 | 28 | |
|
27 | 29 | |
|
28 | 30 | class ZMQStreamHandler(websocket.WebSocketHandler): |
|
29 | ||
|
30 |
def |
|
|
31 | """Check to see that origin and host match in the headers.""" | |
|
32 | ||
|
33 | # The difference between version 8 and 13 is that in 8 the | |
|
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") | |
|
31 | ||
|
32 | def check_origin(self, origin): | |
|
33 | """Check Origin == Host or CORS origins.""" | |
|
34 | if self.cors_origin == '*': | |
|
35 | return True | |
|
40 | 36 | |
|
41 | 37 | host = self.request.headers.get("Host") |
|
42 | 38 | |
|
43 | 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 | 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 | 61 | def clear_cookie(self, *args, **kwargs): |
|
54 | 62 | """meaningless for websockets""" |
@@ -96,13 +104,21 b' class ZMQStreamHandler(websocket.WebSocketHandler):' | |||
|
96 | 104 | |
|
97 | 105 | |
|
98 | 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 | 114 | def open(self, kernel_id): |
|
101 | 115 | self.kernel_id = cast_unicode(kernel_id, 'ascii') |
|
102 | 116 | # Check to see that origin matches host directly, including ports |
|
103 | if not self.same_origin(): | |
|
104 | self.log.warn("Cross Origin WebSocket Attempt.") | |
|
105 | raise web.HTTPError(404) | |
|
117 | # Tornado 4 already does CORS checking | |
|
118 | if tornado.version_info[0] < 4: | |
|
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 | 123 | self.session = Session(config=self.config) |
|
108 | 124 | self.save_on_message = self.on_message |
@@ -12,6 +12,7 b' import json' | |||
|
12 | 12 | import logging |
|
13 | 13 | import os |
|
14 | 14 | import random |
|
15 | import re | |
|
15 | 16 | import select |
|
16 | 17 | import signal |
|
17 | 18 | import socket |
@@ -333,8 +334,34 b' class NotebookApp(BaseIPythonApplication):' | |||
|
333 | 334 | self.file_to_run = base |
|
334 | 335 | self.notebook_dir = path |
|
335 | 336 | |
|
336 |
# Network related information |
|
|
337 | ||
|
337 | # Network related information | |
|
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 | 365 | ip = Unicode('localhost', config=True, |
|
339 | 366 | help="The IP address the notebook server will listen on." |
|
340 | 367 | ) |
@@ -622,6 +649,10 b' class NotebookApp(BaseIPythonApplication):' | |||
|
622 | 649 | |
|
623 | 650 | def init_webapp(self): |
|
624 | 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 | 656 | self.web_app = NotebookWebApplication( |
|
626 | 657 | self, self.kernel_manager, self.notebook_manager, |
|
627 | 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