tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

curio-server.py (6824B)


      1 #!/usr/bin/env python3.5
      2 # -*- coding: utf-8 -*-
      3 """
      4 curio-server.py
      5 ~~~~~~~~~~~~~~~
      6 
      7 A fully-functional HTTP/2 server written for curio.
      8 
      9 Requires Python 3.5+.
     10 """
     11 import mimetypes
     12 import os
     13 import sys
     14 
     15 from curio import Event, spawn, socket, ssl, run
     16 
     17 import h2.config
     18 import h2.connection
     19 import h2.events
     20 
     21 
     22 # The maximum amount of a file we'll send in a single DATA frame.
     23 READ_CHUNK_SIZE = 8192
     24 
     25 
     26 async def create_listening_ssl_socket(address, certfile, keyfile):
     27    """
     28    Create and return a listening TLS socket on a given address.
     29    """
     30    ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
     31    ssl_context.options |= (
     32        ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION
     33    )
     34    ssl_context.set_ciphers("ECDHE+AESGCM")
     35    ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
     36    ssl_context.set_alpn_protocols(["h2"])
     37 
     38    sock = socket.socket()
     39    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
     40    sock = await ssl_context.wrap_socket(sock)
     41    sock.bind(address)
     42    sock.listen()
     43 
     44    return sock
     45 
     46 
     47 async def h2_server(address, root, certfile, keyfile):
     48    """
     49    Create an HTTP/2 server at the given address.
     50    """
     51    sock = await create_listening_ssl_socket(address, certfile, keyfile)
     52    print("Now listening on %s:%d" % address)
     53 
     54    async with sock:
     55        while True:
     56            client, _ = await sock.accept()
     57            server = H2Server(client, root)
     58            await spawn(server.run())
     59 
     60 
     61 class H2Server:
     62    """
     63    A basic HTTP/2 file server. This is essentially very similar to
     64    SimpleHTTPServer from the standard library, but uses HTTP/2 instead of
     65    HTTP/1.1.
     66    """
     67    def __init__(self, sock, root):
     68        config = h2.config.H2Configuration(
     69            client_side=False, header_encoding='utf-8'
     70        )
     71        self.sock = sock
     72        self.conn = h2.connection.H2Connection(config=config)
     73        self.root = root
     74        self.flow_control_events = {}
     75 
     76    async def run(self):
     77        """
     78        Loop over the connection, managing it appropriately.
     79        """
     80        self.conn.initiate_connection()
     81        await self.sock.sendall(self.conn.data_to_send())
     82 
     83        while True:
     84            # 65535 is basically arbitrary here: this amounts to "give me
     85            # whatever data you have".
     86            data = await self.sock.recv(65535)
     87            if not data:
     88                break
     89 
     90            events = self.conn.receive_data(data)
     91            for event in events:
     92                if isinstance(event, h2.events.RequestReceived):
     93                    await spawn(
     94                        self.request_received(event.headers, event.stream_id)
     95                    )
     96                elif isinstance(event, h2.events.DataReceived):
     97                    self.conn.reset_stream(event.stream_id)
     98                elif isinstance(event, h2.events.WindowUpdated):
     99                    await self.window_updated(event)
    100 
    101            await self.sock.sendall(self.conn.data_to_send())
    102 
    103    async def request_received(self, headers, stream_id):
    104        """
    105        Handle a request by attempting to serve a suitable file.
    106        """
    107        headers = dict(headers)
    108        assert headers[':method'] == 'GET'
    109 
    110        path = headers[':path'].lstrip('/')
    111        full_path = os.path.join(self.root, path)
    112 
    113        if not os.path.exists(full_path):
    114            response_headers = (
    115                (':status', '404'),
    116                ('content-length', '0'),
    117                ('server', 'curio-h2'),
    118            )
    119            self.conn.send_headers(
    120                stream_id, response_headers, end_stream=True
    121            )
    122            await self.sock.sendall(self.conn.data_to_send())
    123        else:
    124            await self.send_file(full_path, stream_id)
    125 
    126    async def send_file(self, file_path, stream_id):
    127        """
    128        Send a file, obeying the rules of HTTP/2 flow control.
    129        """
    130        filesize = os.stat(file_path).st_size
    131        content_type, content_encoding = mimetypes.guess_type(file_path)
    132        response_headers = [
    133            (':status', '200'),
    134            ('content-length', str(filesize)),
    135            ('server', 'curio-h2'),
    136        ]
    137        if content_type:
    138            response_headers.append(('content-type', content_type))
    139        if content_encoding:
    140            response_headers.append(('content-encoding', content_encoding))
    141 
    142        self.conn.send_headers(stream_id, response_headers)
    143        await self.sock.sendall(self.conn.data_to_send())
    144 
    145        with open(file_path, 'rb', buffering=0) as f:
    146            await self._send_file_data(f, stream_id)
    147 
    148    async def _send_file_data(self, fileobj, stream_id):
    149        """
    150        Send the data portion of a file. Handles flow control rules.
    151        """
    152        while True:
    153            while self.conn.local_flow_control_window(stream_id) < 1:
    154                await self.wait_for_flow_control(stream_id)
    155 
    156            chunk_size = min(
    157                self.conn.local_flow_control_window(stream_id),
    158                READ_CHUNK_SIZE,
    159            )
    160 
    161            data = fileobj.read(chunk_size)
    162            keep_reading = (len(data) == chunk_size)
    163 
    164            self.conn.send_data(stream_id, data, not keep_reading)
    165            await self.sock.sendall(self.conn.data_to_send())
    166 
    167            if not keep_reading:
    168                break
    169 
    170    async def wait_for_flow_control(self, stream_id):
    171        """
    172        Blocks until the flow control window for a given stream is opened.
    173        """
    174        evt = Event()
    175        self.flow_control_events[stream_id] = evt
    176        await evt.wait()
    177 
    178    async def window_updated(self, event):
    179        """
    180        Unblock streams waiting on flow control, if needed.
    181        """
    182        stream_id = event.stream_id
    183 
    184        if stream_id and stream_id in self.flow_control_events:
    185            evt = self.flow_control_events.pop(stream_id)
    186            await evt.set()
    187        elif not stream_id:
    188            # Need to keep a real list here to use only the events present at
    189            # this time.
    190            blocked_streams = list(self.flow_control_events.keys())
    191            for stream_id in blocked_streams:
    192                event = self.flow_control_events.pop(stream_id)
    193                await event.set()
    194        return
    195 
    196 
    197 if __name__ == '__main__':
    198    host = sys.argv[2] if len(sys.argv) > 2 else "localhost"
    199    print("Try GETting:")
    200    print("    On OSX after 'brew install curl --with-c-ares --with-libidn --with-nghttp2 --with-openssl':")
    201    print("/usr/local/opt/curl/bin/curl --tlsv1.2 --http2 -k https://localhost:5000/bundle.js")
    202    print("Or open a browser to: https://localhost:5000/")
    203    print("   (Accept all the warnings)")
    204    run(h2_server((host, 5000), sys.argv[1],
    205                  "{}.crt.pem".format(host),
    206                  "{}.key".format(host)), with_monitor=True)