Show More
@@ -163,6 +163,48 b' class IPythonHandler(AuthenticatedHandler):' | |||||
163 | return self.notebook_manager.notebook_dir |
|
163 | return self.notebook_manager.notebook_dir | |
164 |
|
164 | |||
165 | #--------------------------------------------------------------- |
|
165 | #--------------------------------------------------------------- | |
|
166 | # CORS | |||
|
167 | #--------------------------------------------------------------- | |||
|
168 | ||||
|
169 | @property | |||
|
170 | def allow_origin(self): | |||
|
171 | """Normal Access-Control-Allow-Origin""" | |||
|
172 | return self.settings.get('allow_origin', '') | |||
|
173 | ||||
|
174 | @property | |||
|
175 | def allow_origin_pat(self): | |||
|
176 | """Regular expression version of allow_origin""" | |||
|
177 | return self.settings.get('allow_origin_pat', None) | |||
|
178 | ||||
|
179 | @property | |||
|
180 | def allow_credentials(self): | |||
|
181 | """Whether to set Access-Control-Allow-Credentials""" | |||
|
182 | return self.settings.get('allow_credentials', False) | |||
|
183 | ||||
|
184 | def set_default_headers(self): | |||
|
185 | """Add CORS headers, if defined""" | |||
|
186 | super(IPythonHandler, self).set_default_headers() | |||
|
187 | if self.allow_origin: | |||
|
188 | self.set_header("Access-Control-Allow-Origin", self.allow_origin) | |||
|
189 | elif self.allow_origin_pat: | |||
|
190 | origin = self.get_origin() | |||
|
191 | if origin and self.allow_origin_pat.match(origin): | |||
|
192 | self.set_header("Access-Control-Allow-Origin", origin) | |||
|
193 | if self.allow_credentials: | |||
|
194 | self.set_header("Access-Control-Allow-Credentials", 'true') | |||
|
195 | ||||
|
196 | def get_origin(self): | |||
|
197 | # Handle WebSocket Origin naming convention differences | |||
|
198 | # The difference between version 8 and 13 is that in 8 the | |||
|
199 | # client sends a "Sec-Websocket-Origin" header and in 13 it's | |||
|
200 | # simply "Origin". | |||
|
201 | if "Origin" in self.request.headers: | |||
|
202 | origin = self.request.headers.get("Origin") | |||
|
203 | else: | |||
|
204 | origin = self.request.headers.get("Sec-Websocket-Origin", None) | |||
|
205 | return origin | |||
|
206 | ||||
|
207 | #--------------------------------------------------------------- | |||
166 | # template rendering |
|
208 | # template rendering | |
167 | #--------------------------------------------------------------- |
|
209 | #--------------------------------------------------------------- | |
168 |
|
210 |
@@ -26,6 +26,8 b' try:' | |||||
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 | ||||
|
30 | import tornado | |||
29 | from tornado import web |
|
31 | from tornado import web | |
30 | from tornado import websocket |
|
32 | from tornado import websocket | |
31 |
|
33 | |||
@@ -43,28 +45,35 b' from .handlers import IPythonHandler' | |||||
43 |
|
45 | |||
44 | class ZMQStreamHandler(websocket.WebSocketHandler): |
|
46 | class ZMQStreamHandler(websocket.WebSocketHandler): | |
45 |
|
47 | |||
46 |
def |
|
48 | def check_origin(self, origin): | |
47 | """Check to see that origin and host match in the headers.""" |
|
49 | """Check Origin == Host or Access-Control-Allow-Origin. | |
48 |
|
|
50 | ||
49 | # The difference between version 8 and 13 is that in 8 the |
|
51 | Tornado >= 4 calls this method automatically, raising 403 if it returns False. | |
50 | # client sends a "Sec-Websocket-Origin" header and in 13 it's |
|
52 | We call it explicitly in `open` on Tornado < 4. | |
51 | # simply "Origin". |
|
53 | """ | |
52 | if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8"): |
|
54 | if self.allow_origin == '*': | |
53 | origin_header = self.request.headers.get("Sec-Websocket-Origin") |
|
55 | return True | |
54 | else: |
|
|||
55 | origin_header = self.request.headers.get("Origin") |
|
|||
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 |
|
60 | if(origin is None or host is None): | |
61 | return False |
|
61 | return False | |
62 |
|
62 | |||
63 | parsed_origin = urlparse(origin_header) |
|
63 | host_origin = "{0}://{1}".format(self.request.protocol, host) | |
64 | origin = parsed_origin.netloc |
|
|||
65 |
|
64 | |||
66 | # Check to see that origin matches host directly, including ports |
|
65 | # OK if origin matches host | |
67 |
|
|
66 | if origin == host_origin: | |
|
67 | return True | |||
|
68 | ||||
|
69 | # Check CORS headers | |||
|
70 | if self.allow_origin: | |||
|
71 | return self.allow_origin == origin | |||
|
72 | elif self.allow_origin_pat: | |||
|
73 | return bool(self.allow_origin_pat.match(origin)) | |||
|
74 | else: | |||
|
75 | # No CORS headers deny the request | |||
|
76 | return False | |||
68 |
|
77 | |||
69 | def clear_cookie(self, *args, **kwargs): |
|
78 | def clear_cookie(self, *args, **kwargs): | |
70 | """meaningless for websockets""" |
|
79 | """meaningless for websockets""" | |
@@ -112,13 +121,21 b' class ZMQStreamHandler(websocket.WebSocketHandler):' | |||||
112 |
|
121 | |||
113 |
|
122 | |||
114 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): |
|
123 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): | |
|
124 | def set_default_headers(self): | |||
|
125 | """Undo the set_default_headers in IPythonHandler | |||
|
126 | ||||
|
127 | which doesn't make sense for websockets | |||
|
128 | """ | |||
|
129 | pass | |||
115 |
|
130 | |||
116 | def open(self, kernel_id): |
|
131 | def open(self, kernel_id): | |
117 | self.kernel_id = cast_unicode(kernel_id, 'ascii') |
|
132 | self.kernel_id = cast_unicode(kernel_id, 'ascii') | |
118 | # Check to see that origin matches host directly, including ports |
|
133 | # Check to see that origin matches host directly, including ports | |
119 | if not self.same_origin(): |
|
134 | # Tornado 4 already does CORS checking | |
120 | self.log.warn("Cross Origin WebSocket Attempt.") |
|
135 | if tornado.version_info[0] < 4: | |
121 | raise web.HTTPError(404) |
|
136 | 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) | |||
122 |
|
139 | |||
123 | self.session = Session(config=self.config) |
|
140 | self.session = Session(config=self.config) | |
124 | self.save_on_message = self.on_message |
|
141 | self.save_on_message = self.on_message |
@@ -24,6 +24,7 b' import json' | |||||
24 | import logging |
|
24 | import logging | |
25 | import os |
|
25 | import os | |
26 | import random |
|
26 | import random | |
|
27 | import re | |||
27 | import select |
|
28 | import select | |
28 | import signal |
|
29 | import signal | |
29 | import socket |
|
30 | import socket | |
@@ -340,7 +341,33 b' class NotebookApp(BaseIPythonApplication):' | |||||
340 | self.file_to_run = base |
|
341 | self.file_to_run = base | |
341 | self.notebook_dir = path |
|
342 | self.notebook_dir = path | |
342 |
|
343 | |||
343 |
# Network related information |
|
344 | # Network related information | |
|
345 | ||||
|
346 | allow_origin = Unicode('', config=True, | |||
|
347 | help="""Set the Access-Control-Allow-Origin header | |||
|
348 | ||||
|
349 | Use '*' to allow any origin to access your server. | |||
|
350 | ||||
|
351 | Takes precedence over allow_origin_pat. | |||
|
352 | """ | |||
|
353 | ) | |||
|
354 | ||||
|
355 | allow_origin_pat = Unicode('', config=True, | |||
|
356 | help="""Use a regular expression for the Access-Control-Allow-Origin header | |||
|
357 | ||||
|
358 | Requests from an origin matching the expression will get replies with: | |||
|
359 | ||||
|
360 | Access-Control-Allow-Origin: origin | |||
|
361 | ||||
|
362 | where `origin` is the origin of the request. | |||
|
363 | ||||
|
364 | Ignored if allow_origin is set. | |||
|
365 | """ | |||
|
366 | ) | |||
|
367 | ||||
|
368 | allow_credentials = Bool(False, config=True, | |||
|
369 | help="Set the Access-Control-Allow-Credentials: true header" | |||
|
370 | ) | |||
344 |
|
371 | |||
345 | ip = Unicode('localhost', config=True, |
|
372 | ip = Unicode('localhost', config=True, | |
346 | help="The IP address the notebook server will listen on." |
|
373 | help="The IP address the notebook server will listen on." | |
@@ -603,6 +630,10 b' class NotebookApp(BaseIPythonApplication):' | |||||
603 |
|
630 | |||
604 | def init_webapp(self): |
|
631 | def init_webapp(self): | |
605 | """initialize tornado webapp and httpserver""" |
|
632 | """initialize tornado webapp and httpserver""" | |
|
633 | self.webapp_settings['allow_origin'] = self.allow_origin | |||
|
634 | self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat) | |||
|
635 | self.webapp_settings['allow_credentials'] = self.allow_credentials | |||
|
636 | ||||
606 | self.web_app = NotebookWebApplication( |
|
637 | self.web_app = NotebookWebApplication( | |
607 |
self, self.kernel_manager, self.notebook_manager, |
|
638 | self, self.kernel_manager, self.notebook_manager, | |
608 | self.cluster_manager, self.session_manager, |
|
639 | self.cluster_manager, self.session_manager, |
General Comments 0
You need to be logged in to leave comments.
Login now