Source code for aiohttp.wsgi

"""wsgi server.

TODO:
  * proxy protocol
  * x-forward security
  * wsgi file support (os.sendfile)
"""

import asyncio
import inspect
import io
import os
import socket
import sys
from urllib.parse import urlsplit

import aiohttp
from aiohttp import server, hdrs

__all__ = ('WSGIServerHttpProtocol',)


[docs]class WSGIServerHttpProtocol(server.ServerHttpProtocol): """HTTP Server that implements the Python WSGI protocol. It uses 'wsgi.async' of 'True'. 'wsgi.input' can behave differently depends on 'readpayload' constructor parameter. If readpayload is set to True, wsgi server reads all incoming data into BytesIO object and sends it as 'wsgi.input' environ var. If readpayload is set to false 'wsgi.input' is a StreamReader and application should read incoming data with "yield from environ['wsgi.input'].read()". It defaults to False. """ SCRIPT_NAME = os.environ.get('SCRIPT_NAME', '') def __init__(self, app, readpayload=False, is_ssl=False, *args, **kw): super().__init__(*args, **kw) self.wsgi = app self.is_ssl = is_ssl self.readpayload = readpayload
[docs] def create_wsgi_response(self, message): return WsgiResponse(self.writer, message)
[docs] def create_wsgi_environ(self, message, payload): uri_parts = urlsplit(message.path) environ = { 'wsgi.input': payload, 'wsgi.errors': sys.stderr, 'wsgi.version': (1, 0), 'wsgi.async': True, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'wsgi.file_wrapper': FileWrapper, 'SERVER_SOFTWARE': aiohttp.HttpMessage.SERVER_SOFTWARE, 'REQUEST_METHOD': message.method, 'QUERY_STRING': uri_parts.query or '', 'RAW_URI': message.path, 'SERVER_PROTOCOL': 'HTTP/%s.%s' % message.version } script_name = self.SCRIPT_NAME for hdr_name, hdr_value in message.headers.items(): if hdr_name == 'SCRIPT_NAME': script_name = hdr_value elif 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 = '%s,%s' % (environ[key], hdr_value) environ[key] = hdr_value url_scheme = environ.get('HTTP_X_FORWARDED_PROTO') if url_scheme is None: url_scheme = 'https' if self.is_ssl else 'http' environ['wsgi.url_scheme'] = url_scheme # authors should be aware that REMOTE_HOST and REMOTE_ADDR # may not qualify the remote addr # also SERVER_PORT variable MUST be set to the TCP/IP port number on # which this request is received from the client. # http://www.ietf.org/rfc/rfc3875 family = self.transport.get_extra_info('socket').family if family in (socket.AF_INET, socket.AF_INET6): peername = self.transport.get_extra_info('peername') environ['REMOTE_ADDR'] = peername[0] environ['REMOTE_PORT'] = str(peername[1]) http_host = message.headers.get("HOST", None) if http_host: hostport = http_host.split(":") environ['SERVER_NAME'] = hostport[0] if len(hostport) > 1: environ['SERVER_PORT'] = str(hostport[1]) else: environ['SERVER_PORT'] = '80' else: # SERVER_NAME should be set to value of Host header, but this # header is not required. In this case we shoud set it to local # address of socket sockname = self.transport.get_extra_info('sockname') environ['SERVER_NAME'] = sockname[0] environ['SERVER_PORT'] = str(sockname[1]) else: # We are behind reverse proxy, so get all vars from headers for header in ('REMOTE_ADDR', 'REMOTE_PORT', 'SERVER_NAME', 'SERVER_PORT'): environ[header] = message.headers.get(header, '') path_info = uri_parts.path if script_name: path_info = path_info.split(script_name, 1)[-1] environ['PATH_INFO'] = path_info environ['SCRIPT_NAME'] = script_name environ['async.reader'] = self.reader environ['async.writer'] = self.writer return environ
@asyncio.coroutine
[docs] def handle_request(self, message, payload): """Handle a single HTTP request""" now = self._loop.time() if self.readpayload: wsgiinput = io.BytesIO() wsgiinput.write((yield from payload.read())) wsgiinput.seek(0) payload = wsgiinput environ = self.create_wsgi_environ(message, payload) response = self.create_wsgi_response(message) riter = self.wsgi(environ, response.start_response) if isinstance(riter, asyncio.Future) or inspect.isgenerator(riter): riter = yield from riter resp = response.response try: for item in riter: if isinstance(item, asyncio.Future): item = yield from item yield from resp.write(item) yield from resp.write_eof() finally: if hasattr(riter, 'close'): riter.close() if resp.keep_alive(): self.keep_alive(True) self.log_access( message, environ, response.response, self._loop.time() - now)
class FileWrapper: """Custom file wrapper.""" def __init__(self, fobj, chunk_size=8192): self.fobj = fobj self.chunk_size = chunk_size if hasattr(fobj, 'close'): self.close = fobj.close def __iter__(self): return self def __next__(self): data = self.fobj.read(self.chunk_size) if data: return data raise StopIteration class WsgiResponse: """Implementation of start_response() callable as specified by PEP 3333""" status = None HOP_HEADERS = { hdrs.CONNECTION, hdrs.KEEP_ALIVE, hdrs.PROXY_AUTHENTICATE, hdrs.PROXY_AUTHORIZATION, hdrs.TE, hdrs.TRAILER, hdrs.TRANSFER_ENCODING, hdrs.UPGRADE, } def __init__(self, writer, message): self.writer = writer self.message = message def start_response(self, status, headers, exc_info=None): if exc_info: try: if self.status: raise exc_info[1] finally: exc_info = None status_code = int(status.split(' ', 1)[0]) self.status = status resp = self.response = aiohttp.Response( self.writer, status_code, self.message.version, self.message.should_close) resp.HOP_HEADERS = self.HOP_HEADERS for name, value in headers: resp.add_header(name, value) if resp.has_chunked_hdr: resp.enable_chunked_encoding() # send headers immediately for websocket connection if status_code == 101 and resp.upgrade and resp.websocket: resp.send_headers() else: resp._send_headers = True return self.response.write