Show More
@@ -163,6 +163,48 b' class IPythonHandler(AuthenticatedHandler):' | |||
|
163 | 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 | 208 | # template rendering |
|
167 | 209 | #--------------------------------------------------------------- |
|
168 | 210 |
@@ -26,6 +26,8 b' try:' | |||
|
26 | 26 | except ImportError: |
|
27 | 27 | from Cookie import SimpleCookie # Py 2 |
|
28 | 28 | import logging |
|
29 | ||
|
30 | import tornado | |
|
29 | 31 | from tornado import web |
|
30 | 32 | from tornado import websocket |
|
31 | 33 | |
@@ -43,28 +45,35 b' from .handlers import IPythonHandler' | |||
|
43 | 45 | |
|
44 | 46 | class ZMQStreamHandler(websocket.WebSocketHandler): |
|
45 | 47 | |
|
46 |
def |
|
|
47 | """Check to see that origin and host match in the headers.""" | |
|
48 | def check_origin(self, origin): | |
|
49 | """Check Origin == Host or Access-Control-Allow-Origin. | |
|
48 | 50 |
|
|
49 | # The difference between version 8 and 13 is that in 8 the | |
|
50 | # client sends a "Sec-Websocket-Origin" header and in 13 it's | |
|
51 | # simply "Origin". | |
|
52 | if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8"): | |
|
53 | origin_header = self.request.headers.get("Sec-Websocket-Origin") | |
|
54 | else: | |
|
55 | origin_header = self.request.headers.get("Origin") | |
|
51 | Tornado >= 4 calls this method automatically, raising 403 if it returns False. | |
|
52 | We call it explicitly in `open` on Tornado < 4. | |
|
53 | """ | |
|
54 | if self.allow_origin == '*': | |
|
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 |
|
|
60 | if(origin is None or host is None): | |
|
61 | 61 | return False |
|
62 | 62 | |
|
63 | parsed_origin = urlparse(origin_header) | |
|
64 | origin = parsed_origin.netloc | |
|
63 | host_origin = "{0}://{1}".format(self.request.protocol, host) | |
|
65 | 64 | |
|
66 | # Check to see that origin matches host directly, including ports | |
|
67 |
|
|
|
65 | # OK if origin matches host | |
|
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 | 78 | def clear_cookie(self, *args, **kwargs): |
|
70 | 79 | """meaningless for websockets""" |
@@ -112,13 +121,21 b' class ZMQStreamHandler(websocket.WebSocketHandler):' | |||
|
112 | 121 | |
|
113 | 122 | |
|
114 | 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 | 131 | def open(self, kernel_id): |
|
117 | 132 | self.kernel_id = cast_unicode(kernel_id, 'ascii') |
|
118 | 133 | # Check to see that origin matches host directly, including ports |
|
119 | if not self.same_origin(): | |
|
120 | self.log.warn("Cross Origin WebSocket Attempt.") | |
|
121 | raise web.HTTPError(404) | |
|
134 | # Tornado 4 already does CORS checking | |
|
135 | if tornado.version_info[0] < 4: | |
|
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 | 140 | self.session = Session(config=self.config) |
|
124 | 141 | self.save_on_message = self.on_message |
@@ -24,6 +24,7 b' import json' | |||
|
24 | 24 | import logging |
|
25 | 25 | import os |
|
26 | 26 | import random |
|
27 | import re | |
|
27 | 28 | import select |
|
28 | 29 | import signal |
|
29 | 30 | import socket |
@@ -340,7 +341,33 b' class NotebookApp(BaseIPythonApplication):' | |||
|
340 | 341 | self.file_to_run = base |
|
341 | 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 | 372 | ip = Unicode('localhost', config=True, |
|
346 | 373 | help="The IP address the notebook server will listen on." |
@@ -603,6 +630,10 b' class NotebookApp(BaseIPythonApplication):' | |||
|
603 | 630 | |
|
604 | 631 | def init_webapp(self): |
|
605 | 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 | 637 | self.web_app = NotebookWebApplication( |
|
607 | 638 |
self, self.kernel_manager, self.notebook_manager, |
|
608 | 639 | self.cluster_manager, self.session_manager, |
General Comments 0
You need to be logged in to leave comments.
Login now