297 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import os
 | |
| import sys
 | |
| import asyncio
 | |
| 
 | |
| from engineio.static_files import get_static_file
 | |
| 
 | |
| 
 | |
| class ASGIApp:
 | |
|     """ASGI application middleware for Engine.IO.
 | |
| 
 | |
|     This middleware dispatches traffic to an Engine.IO application. It can
 | |
|     also serve a list of static files to the client, or forward unrelated
 | |
|     HTTP traffic to another ASGI application.
 | |
| 
 | |
|     :param engineio_server: The Engine.IO server. Must be an instance of the
 | |
|                             ``engineio.AsyncServer`` class.
 | |
|     :param static_files: A dictionary with static file mapping rules. See the
 | |
|                          documentation for details on this argument.
 | |
|     :param other_asgi_app: A separate ASGI app that receives all other traffic.
 | |
|     :param engineio_path: The endpoint where the Engine.IO application should
 | |
|                           be installed. The default value is appropriate for
 | |
|                           most cases. With a value of ``None``, all incoming
 | |
|                           traffic is directed to the Engine.IO server, with the
 | |
|                           assumption that routing, if necessary, is handled by
 | |
|                           a different layer. When this option is set to
 | |
|                           ``None``, ``static_files`` and ``other_asgi_app`` are
 | |
|                           ignored.
 | |
|     :param on_startup: function to be called on application startup; can be
 | |
|                        coroutine
 | |
|     :param on_shutdown: function to be called on application shutdown; can be
 | |
|                         coroutine
 | |
| 
 | |
|     Example usage::
 | |
| 
 | |
|         import engineio
 | |
|         import uvicorn
 | |
| 
 | |
|         eio = engineio.AsyncServer()
 | |
|         app = engineio.ASGIApp(eio, static_files={
 | |
|             '/': {'content_type': 'text/html', 'filename': 'index.html'},
 | |
|             '/index.html': {'content_type': 'text/html',
 | |
|                             'filename': 'index.html'},
 | |
|         })
 | |
|         uvicorn.run(app, '127.0.0.1', 5000)
 | |
|     """
 | |
|     def __init__(self, engineio_server, other_asgi_app=None,
 | |
|                  static_files=None, engineio_path='engine.io',
 | |
|                  on_startup=None, on_shutdown=None):
 | |
|         self.engineio_server = engineio_server
 | |
|         self.other_asgi_app = other_asgi_app
 | |
|         self.engineio_path = engineio_path
 | |
|         if self.engineio_path is not None:
 | |
|             if not self.engineio_path.startswith('/'):
 | |
|                 self.engineio_path = '/' + self.engineio_path
 | |
|             if not self.engineio_path.endswith('/'):
 | |
|                 self.engineio_path += '/'
 | |
|         self.static_files = static_files or {}
 | |
|         self.on_startup = on_startup
 | |
|         self.on_shutdown = on_shutdown
 | |
| 
 | |
|     async def __call__(self, scope, receive, send):
 | |
|         if scope['type'] == 'lifespan':
 | |
|             await self.lifespan(scope, receive, send)
 | |
|         elif scope['type'] in ['http', 'websocket'] and (
 | |
|                 self.engineio_path is None
 | |
|                 or self._ensure_trailing_slash(scope['path']).startswith(
 | |
|                     self.engineio_path)):
 | |
|             await self.engineio_server.handle_request(scope, receive, send)
 | |
|         else:
 | |
|             static_file = get_static_file(scope['path'], self.static_files) \
 | |
|                 if scope['type'] == 'http' and self.static_files else None
 | |
|             if static_file and os.path.exists(static_file['filename']):
 | |
|                 await self.serve_static_file(static_file, receive, send)
 | |
|             elif self.other_asgi_app is not None:
 | |
|                 await self.other_asgi_app(scope, receive, send)
 | |
|             else:
 | |
|                 await self.not_found(receive, send)
 | |
| 
 | |
|     async def serve_static_file(self, static_file, receive,
 | |
|                                 send):  # pragma: no cover
 | |
|         event = await receive()
 | |
|         if event['type'] == 'http.request':
 | |
|             with open(static_file['filename'], 'rb') as f:
 | |
|                 payload = f.read()
 | |
|             await send({'type': 'http.response.start',
 | |
|                         'status': 200,
 | |
|                         'headers': [(b'Content-Type', static_file[
 | |
|                             'content_type'].encode('utf-8'))]})
 | |
|             await send({'type': 'http.response.body',
 | |
|                         'body': payload})
 | |
| 
 | |
|     async def lifespan(self, scope, receive, send):
 | |
|         if self.other_asgi_app is not None and self.on_startup is None and \
 | |
|                 self.on_shutdown is None:
 | |
|             # let the other ASGI app handle lifespan events
 | |
|             await self.other_asgi_app(scope, receive, send)
 | |
|             return
 | |
| 
 | |
|         while True:
 | |
|             event = await receive()
 | |
|             if event['type'] == 'lifespan.startup':
 | |
|                 if self.on_startup:
 | |
|                     try:
 | |
|                         await self.on_startup() \
 | |
|                             if asyncio.iscoroutinefunction(self.on_startup) \
 | |
|                             else self.on_startup()
 | |
|                     except:
 | |
|                         await send({'type': 'lifespan.startup.failed'})
 | |
|                         return
 | |
|                 await send({'type': 'lifespan.startup.complete'})
 | |
|             elif event['type'] == 'lifespan.shutdown':
 | |
|                 if self.on_shutdown:
 | |
|                     try:
 | |
|                         await self.on_shutdown() \
 | |
|                             if asyncio.iscoroutinefunction(self.on_shutdown) \
 | |
|                             else self.on_shutdown()
 | |
|                     except:
 | |
|                         await send({'type': 'lifespan.shutdown.failed'})
 | |
|                         return
 | |
|                 await send({'type': 'lifespan.shutdown.complete'})
 | |
|                 return
 | |
| 
 | |
|     async def not_found(self, receive, send):
 | |
|         """Return a 404 Not Found error to the client."""
 | |
|         await send({'type': 'http.response.start',
 | |
|                     'status': 404,
 | |
|                     'headers': [(b'Content-Type', b'text/plain')]})
 | |
|         await send({'type': 'http.response.body',
 | |
|                     'body': b'Not Found'})
 | |
| 
 | |
|     def _ensure_trailing_slash(self, path):
 | |
|         if not path.endswith('/'):
 | |
|             path += '/'
 | |
|         return path
 | |
| 
 | |
| 
 | |
| async def translate_request(scope, receive, send):
 | |
|     class AwaitablePayload:  # pragma: no cover
 | |
|         def __init__(self, payload):
 | |
|             self.payload = payload or b''
 | |
| 
 | |
|         async def read(self, length=None):
 | |
|             if length is None:
 | |
|                 r = self.payload
 | |
|                 self.payload = b''
 | |
|             else:
 | |
|                 r = self.payload[:length]
 | |
|                 self.payload = self.payload[length:]
 | |
|             return r
 | |
| 
 | |
|     event = await receive()
 | |
|     payload = b''
 | |
|     if event['type'] == 'http.request':
 | |
|         payload += event.get('body') or b''
 | |
|         while event.get('more_body'):
 | |
|             event = await receive()
 | |
|             if event['type'] == 'http.request':
 | |
|                 payload += event.get('body') or b''
 | |
|     elif event['type'] == 'websocket.connect':
 | |
|         pass
 | |
|     else:
 | |
|         return {}
 | |
| 
 | |
|     raw_uri = scope['path']
 | |
|     query_string = ''
 | |
|     if 'query_string' in scope and scope['query_string']:
 | |
|         try:
 | |
|             query_string = scope['query_string'].decode('utf-8')
 | |
|         except UnicodeDecodeError:
 | |
|             pass
 | |
|         else:
 | |
|             raw_uri += '?' + query_string
 | |
|     environ = {
 | |
|         'wsgi.input': AwaitablePayload(payload),
 | |
|         'wsgi.errors': sys.stderr,
 | |
|         'wsgi.version': (1, 0),
 | |
|         'wsgi.async': True,
 | |
|         'wsgi.multithread': False,
 | |
|         'wsgi.multiprocess': False,
 | |
|         'wsgi.run_once': False,
 | |
|         'SERVER_SOFTWARE': 'asgi',
 | |
|         'REQUEST_METHOD': scope.get('method', 'GET'),
 | |
|         'PATH_INFO': scope['path'],
 | |
|         'QUERY_STRING': query_string,
 | |
|         'RAW_URI': raw_uri,
 | |
|         'SCRIPT_NAME': '',
 | |
|         'SERVER_PROTOCOL': 'HTTP/1.1',
 | |
|         'REMOTE_ADDR': '127.0.0.1',
 | |
|         'REMOTE_PORT': '0',
 | |
|         'SERVER_NAME': 'asgi',
 | |
|         'SERVER_PORT': '0',
 | |
|         'asgi.receive': receive,
 | |
|         'asgi.send': send,
 | |
|         'asgi.scope': scope,
 | |
|     }
 | |
| 
 | |
|     for hdr_name, hdr_value in scope['headers']:
 | |
|         try:
 | |
|             hdr_name = hdr_name.upper().decode('utf-8')
 | |
|             hdr_value = hdr_value.decode('utf-8')
 | |
|         except UnicodeDecodeError:
 | |
|             # skip header if it cannot be decoded
 | |
|             continue
 | |
|         if hdr_name == 'CONTENT-TYPE':
 | |
|             environ['CONTENT_TYPE'] = hdr_value
 | |
|             continue
 | |
|         elif hdr_name == 'CONTENT-LENGTH':
 | |
|             environ['CONTENT_LENGTH'] = hdr_value
 | |
|             continue
 | |
| 
 | |
|         key = 'HTTP_%s' % hdr_name.replace('-', '_')
 | |
|         if key in environ:
 | |
|             hdr_value = f'{environ[key]},{hdr_value}'
 | |
| 
 | |
|         environ[key] = hdr_value
 | |
| 
 | |
|     environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http')
 | |
|     return environ
 | |
| 
 | |
| 
 | |
| async def make_response(status, headers, payload, environ):
 | |
|     headers = [(h[0].encode('utf-8'), h[1].encode('utf-8')) for h in headers]
 | |
|     if environ['asgi.scope']['type'] == 'websocket':
 | |
|         if status.startswith('200 '):
 | |
|             await environ['asgi.send']({'type': 'websocket.accept',
 | |
|                                         'headers': headers})
 | |
|         else:
 | |
|             if payload:
 | |
|                 reason = payload.decode('utf-8') \
 | |
|                     if isinstance(payload, bytes) else str(payload)
 | |
|                 await environ['asgi.send']({'type': 'websocket.close',
 | |
|                                             'reason': reason})
 | |
|             else:
 | |
|                 await environ['asgi.send']({'type': 'websocket.close'})
 | |
|         return
 | |
| 
 | |
|     await environ['asgi.send']({'type': 'http.response.start',
 | |
|                                 'status': int(status.split(' ')[0]),
 | |
|                                 'headers': headers})
 | |
|     await environ['asgi.send']({'type': 'http.response.body',
 | |
|                                 'body': payload})
 | |
| 
 | |
| 
 | |
| class WebSocket:  # pragma: no cover
 | |
|     """
 | |
|     This wrapper class provides an asgi WebSocket interface that is
 | |
|     somewhat compatible with eventlet's implementation.
 | |
|     """
 | |
|     def __init__(self, handler, server):
 | |
|         self.handler = handler
 | |
|         self.asgi_receive = None
 | |
|         self.asgi_send = None
 | |
| 
 | |
|     async def __call__(self, environ):
 | |
|         self.asgi_receive = environ['asgi.receive']
 | |
|         self.asgi_send = environ['asgi.send']
 | |
|         await self.asgi_send({'type': 'websocket.accept'})
 | |
|         await self.handler(self)
 | |
|         return ''  # send nothing as response
 | |
| 
 | |
|     async def close(self):
 | |
|         try:
 | |
|             await self.asgi_send({'type': 'websocket.close'})
 | |
|         except Exception:
 | |
|             # if the socket is already close we don't care
 | |
|             pass
 | |
| 
 | |
|     async def send(self, message):
 | |
|         msg_bytes = None
 | |
|         msg_text = None
 | |
|         if isinstance(message, bytes):
 | |
|             msg_bytes = message
 | |
|         else:
 | |
|             msg_text = message
 | |
|         await self.asgi_send({'type': 'websocket.send',
 | |
|                               'bytes': msg_bytes,
 | |
|                               'text': msg_text})
 | |
| 
 | |
|     async def wait(self):
 | |
|         event = await self.asgi_receive()
 | |
|         if event['type'] != 'websocket.receive':
 | |
|             raise OSError()
 | |
|         if event.get('bytes', None) is not None:
 | |
|             return event['bytes']
 | |
|         elif event.get('text', None) is not None:
 | |
|             return event['text']
 | |
|         else:  # pragma: no cover
 | |
|             raise OSError()
 | |
| 
 | |
| 
 | |
| _async = {
 | |
|     'asyncio': True,
 | |
|     'translate_request': translate_request,
 | |
|     'make_response': make_response,
 | |
|     'websocket': WebSocket,
 | |
| }
 |