@@ -28,59 +28,49 @@ def get(self, *args):
2828 dest = src ._replace (path = src .path + '/' )
2929 self .redirect (urlunparse (dest ))
3030
31- class LocalProxyHandler (WebSocketHandlerMixin , IPythonHandler ):
32-
31+ class ProxyHandler (WebSocketHandlerMixin , IPythonHandler ):
32+ """
33+ A tornado request handler that proxies HTTP and websockets from
34+ a given host/port combination. This class is not meant to be
35+ used directly as a means of overriding CORS. This presents significant
36+ security risks, and could allow arbitrary remote code access. Instead, it is
37+ meant to be subclassed and used for proxying URLs from trusted sources.
38+
39+ Subclasses should implement open, http_get, post, put, delete, head, patch,
40+ and options.
41+ """
3342 def __init__ (self , * args , ** kwargs ):
3443 self .proxy_base = ''
3544 self .absolute_url = kwargs .pop ('absolute_url' , False )
3645 super ().__init__ (* args , ** kwargs )
3746
38- async def open (self , port , proxied_path = '' ):
39- """
40- Called when a client opens a websocket connection.
47+ # Support all the methods that torando does by default except for GET which
48+ # is passed to WebSocketHandlerMixin and then to WebSocketHandler.
4149
42- We establish a websocket connection to the proxied backend &
43- set up a callback to relay messages through.
44- """
45- if not proxied_path .startswith ('/' ):
46- proxied_path = '/' + proxied_path
50+ async def open (self , port , proxied_path ):
51+ raise NotImplementedError ('Subclasses of ProxyHandler should implement open' )
4752
48- client_uri = self .get_client_uri ('ws' , port , proxied_path )
49- headers = self .request .headers
53+ async def http_get (self , host , port , proxy_path = '' ):
54+ '''Our non-websocket GET.'''
55+ raise NotImplementedError ('Subclasses of ProxyHandler should implement http_get' )
5056
51- def message_cb (message ):
52- """
53- Callback when the backend sends messages to us
57+ def post (self , host , port , proxy_path = '' ):
58+ raise NotImplementedError ('Subclasses of ProxyHandler should implement this post' )
5459
55- We just pass it back to the frontend
56- """
57- # Websockets support both string (utf-8) and binary data, so let's
58- # make sure we signal that appropriately when proxying
59- self ._record_activity ()
60- if message is None :
61- self .close ()
62- else :
63- self .write_message (message , binary = isinstance (message , bytes ))
60+ def put (self , port , proxy_path = '' ):
61+ raise NotImplementedError ('Subclasses of ProxyHandler should implement this put' )
6462
65- def ping_cb (data ):
66- """
67- Callback when the backend sends pings to us.
63+ def delete (self , host , port , proxy_path = '' ):
64+ raise NotImplementedError ('Subclasses of ProxyHandler should implement delete' )
6865
69- We just pass it back to the frontend.
70- """
71- self ._record_activity ()
72- self .ping (data )
66+ def head (self , host , port , proxy_path = '' ):
67+ raise NotImplementedError ('Subclasses of ProxyHandler should implement head' )
7368
74- async def start_websocket_connection ():
75- self .log .info ('Trying to establish websocket connection to {}' .format (client_uri ))
76- self ._record_activity ()
77- request = httpclient .HTTPRequest (url = client_uri , headers = headers )
78- self .ws = await pingable_ws_connect (request = request ,
79- on_message_callback = message_cb , on_ping_callback = ping_cb )
80- self ._record_activity ()
81- self .log .info ('Websocket connection established to {}' .format (client_uri ))
69+ def patch (self , host , port , proxy_path = '' ):
70+ raise NotImplementedError ('Subclasses of ProxyHandler should implement patch' )
8271
83- ioloop .IOLoop .current ().add_callback (start_websocket_connection )
72+ def options (self , host , port , proxy_path = '' ):
73+ raise NotImplementedError ('Subclasses of ProxyHandler should implement options' )
8474
8575 def on_message (self , message ):
8676 """
@@ -141,7 +131,7 @@ def _get_context_path(self, port):
141131 else :
142132 return url_path_join (self .base_url , 'proxy' , str (port ))
143133
144- def get_client_uri (self , protocol , port , proxied_path ):
134+ def get_client_uri (self , protocol , host , port , proxied_path ):
145135 context_path = self ._get_context_path (port )
146136 if self .absolute_url :
147137 client_path = url_path_join (context_path , proxied_path )
@@ -150,7 +140,7 @@ def get_client_uri(self, protocol, port, proxied_path):
150140
151141 client_uri = '{protocol}://{host}:{port}{path}' .format (
152142 protocol = protocol ,
153- host = 'localhost' ,
143+ host = host ,
154144 port = port ,
155145 path = client_path
156146 )
@@ -159,11 +149,11 @@ def get_client_uri(self, protocol, port, proxied_path):
159149
160150 return client_uri
161151
162- def _build_proxy_request (self , port , proxied_path , body ):
152+ def _build_proxy_request (self , host , port , proxied_path , body ):
163153
164154 headers = self .proxy_request_headers ()
165155
166- client_uri = self .get_client_uri ('http' , port , proxied_path )
156+ client_uri = self .get_client_uri ('http' , host , port , proxied_path )
167157 # Some applications check X-Forwarded-Context and X-ProxyContextPath
168158 # headers to see if and where they are being proxied from.
169159 if not self .absolute_url :
@@ -177,7 +167,7 @@ def _build_proxy_request(self, port, proxied_path, body):
177167 return req
178168
179169 @web .authenticated
180- async def proxy (self , port , proxied_path ):
170+ async def proxy (self , host , port , proxied_path ):
181171 '''
182172 This serverextension handles:
183173 {base_url}/proxy/{port([0-9]+)}/{proxied_path}
@@ -205,7 +195,7 @@ async def proxy(self, port, proxied_path):
205195
206196 client = httpclient .AsyncHTTPClient ()
207197
208- req = self ._build_proxy_request (port , proxied_path , body )
198+ req = self ._build_proxy_request (host , port , proxied_path , body )
209199 response = await client .fetch (req , raise_error = False )
210200 # record activity at start and end of requests
211201 self ._record_activity ()
@@ -229,40 +219,63 @@ async def proxy(self, port, proxied_path):
229219 if response .body :
230220 self .write (response .body )
231221
232- def proxy_request_headers (self ):
233- '''A dictionary of headers to be used when constructing
234- a tornado.httpclient.HTTPRequest instance for the proxy request.'''
235- return self .request .headers .copy ()
222+ async def proxy_open (self , host , port , proxied_path = '' ):
223+ """
224+ Called when a client opens a websocket connection.
236225
237- def proxy_request_options (self ):
238- '''A dictionary of options to be used when constructing
239- a tornado.httpclient.HTTPRequest instance for the proxy request.'''
240- return dict (follow_redirects = False )
226+ We establish a websocket connection to the proxied backend &
227+ set up a callback to relay messages through.
228+ """
229+ if not proxied_path .startswith ('/' ):
230+ proxied_path = '/' + proxied_path
241231
242- # Support all the methods that torando does by default except for GET which
243- # is passed to WebSocketHandlerMixin and then to WebSocketHandler.
232+ client_uri = self . get_client_uri ( 'ws' , host , port , proxied_path )
233+ headers = self . request . headers
244234
245- async def http_get ( self , port , proxy_path = '' ):
246- '''Our non-websocket GET.'''
247- return await self . proxy ( port , proxy_path )
235+ def message_cb ( message ):
236+ """
237+ Callback when the backend sends messages to us
248238
249- def post (self , port , proxy_path = '' ):
250- return self .proxy (port , proxy_path )
239+ We just pass it back to the frontend
240+ """
241+ # Websockets support both string (utf-8) and binary data, so let's
242+ # make sure we signal that appropriately when proxying
243+ self ._record_activity ()
244+ if message is None :
245+ self .close ()
246+ else :
247+ self .write_message (message , binary = isinstance (message , bytes ))
251248
252- def put (self , port , proxy_path = '' ):
253- return self .proxy (port , proxy_path )
249+ def ping_cb (data ):
250+ """
251+ Callback when the backend sends pings to us.
252+
253+ We just pass it back to the frontend.
254+ """
255+ self ._record_activity ()
256+ self .ping (data )
257+
258+ async def start_websocket_connection ():
259+ self .log .info ('Trying to establish websocket connection to {}' .format (client_uri ))
260+ self ._record_activity ()
261+ request = httpclient .HTTPRequest (url = client_uri , headers = headers )
262+ self .ws = await pingable_ws_connect (request = request ,
263+ on_message_callback = message_cb , on_ping_callback = ping_cb )
264+ self ._record_activity ()
265+ self .log .info ('Websocket connection established to {}' .format (client_uri ))
254266
255- def delete (self , port , proxy_path = '' ):
256- return self .proxy (port , proxy_path )
267+ ioloop .IOLoop .current ().add_callback (start_websocket_connection )
257268
258- def head (self , port , proxy_path = '' ):
259- return self .proxy (port , proxy_path )
260269
261- def patch (self , port , proxy_path = '' ):
262- return self .proxy (port , proxy_path )
270+ def proxy_request_headers (self ):
271+ '''A dictionary of headers to be used when constructing
272+ a tornado.httpclient.HTTPRequest instance for the proxy request.'''
273+ return self .request .headers .copy ()
263274
264- def options (self , port , proxy_path = '' ):
265- return self .proxy (port , proxy_path )
275+ def proxy_request_options (self ):
276+ '''A dictionary of options to be used when constructing
277+ a tornado.httpclient.HTTPRequest instance for the proxy request.'''
278+ return dict (follow_redirects = False )
266279
267280 def check_xsrf_cookie (self ):
268281 '''
@@ -280,6 +293,40 @@ def select_subprotocol(self, subprotocols):
280293 return super ().select_subprotocol (subprotocols )
281294
282295
296+ class LocalProxyHandler (ProxyHandler ):
297+ """
298+ A tornado request handler that proxies HTTP and websockets
299+ from a port on the local system. Same as the above ProxyHandler,
300+ but specific to 'localhost'.
301+ """
302+ async def http_get (self , port , proxied_path ):
303+ return await self .proxy (port , proxied_path )
304+
305+ async def open (self , port , proxied_path ):
306+ return await self .proxy_open ('localhost' , port , proxied_path )
307+
308+ def post (self , port , proxied_path ):
309+ return self .proxy (port , proxied_path )
310+
311+ def put (self , port , proxied_path ):
312+ return self .proxy (port , proxied_path )
313+
314+ def delete (self , port , proxied_path ):
315+ return self .proxy (port , proxied_path )
316+
317+ def head (self , port , proxied_path ):
318+ return self .proxy (port , proxied_path )
319+
320+ def patch (self , port , proxied_path ):
321+ return self .proxy (port , proxied_path )
322+
323+ def options (self , port , proxied_path ):
324+ return self .proxy (port , proxied_path )
325+
326+ def proxy (self , port , proxied_path ):
327+ return super ().proxy ('localhost' , port , proxied_path )
328+
329+
283330# FIXME: Move this to its own file. Too many packages now import this from nbrserverproxy.handlers
284331class SuperviseAndProxyHandler (LocalProxyHandler ):
285332 '''Manage a given process and requests to it '''
0 commit comments