##// END OF EJS Templates
Backport PR #6061: make CORS configurable...
MinRK -
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 same_origin(self):
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_header is None or host is None):
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 return origin == 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 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