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