blob: 01604b44e6ffe83cca9c862a062a7bf58e75adad [file] [log] [blame]
#!/usr/bin/env python3.5
# -*- coding: utf-8 -*-
"""
curio-server.py
~~~~~~~~~~~~~~~
A fully-functional HTTP/2 server written for curio.
Requires Python 3.5+.
"""
import mimetypes
import os
import sys
from curio import Kernel, Event, spawn, socket, ssl
import h2.config
import h2.connection
import h2.events
# The maximum amount of a file we'll send in a single DATA frame.
READ_CHUNK_SIZE = 8192
def create_listening_ssl_socket(address, certfile, keyfile):
"""
Create and return a listening TLS socket on a given address.
"""
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.options |= (
ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION
)
ssl_context.set_ciphers("ECDHE+AESGCM")
ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
ssl_context.set_alpn_protocols(["h2"])
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock = ssl_context.wrap_socket(sock)
sock.bind(address)
sock.listen()
return sock
async def h2_server(address, root, certfile, keyfile):
"""
Create an HTTP/2 server at the given address.
"""
sock = create_listening_ssl_socket(address, certfile, keyfile)
print("Now listening on %s:%d" % address)
async with sock:
while True:
client, _ = await sock.accept()
server = H2Server(client, root)
await spawn(server.run())
class H2Server:
"""
A basic HTTP/2 file server. This is essentially very similar to
SimpleHTTPServer from the standard library, but uses HTTP/2 instead of
HTTP/1.1.
"""
def __init__(self, sock, root):
config = h2.config.H2Configuration(
client_side=False, header_encoding='utf-8'
)
self.sock = sock
self.conn = h2.connection.H2Connection(config=config)
self.root = root
self.flow_control_events = {}
async def run(self):
"""
Loop over the connection, managing it appropriately.
"""
self.conn.initiate_connection()
await self.sock.sendall(self.conn.data_to_send())
while True:
# 65535 is basically arbitrary here: this amounts to "give me
# whatever data you have".
data = await self.sock.recv(65535)
if not data:
break
events = self.conn.receive_data(data)
for event in events:
if isinstance(event, h2.events.RequestReceived):
await spawn(
self.request_received(event.headers, event.stream_id)
)
elif isinstance(event, h2.events.DataReceived):
self.conn.reset_stream(event.stream_id)
elif isinstance(event, h2.events.WindowUpdated):
await self.window_updated(event)
await self.sock.sendall(self.conn.data_to_send())
async def request_received(self, headers, stream_id):
"""
Handle a request by attempting to serve a suitable file.
"""
headers = dict(headers)
assert headers[':method'] == 'GET'
path = headers[':path'].lstrip('/')
full_path = os.path.join(self.root, path)
if not os.path.exists(full_path):
response_headers = (
(':status', '404'),
('content-length', '0'),
('server', 'curio-h2'),
)
self.conn.send_headers(
stream_id, response_headers, end_stream=True
)
await self.sock.sendall(self.conn.data_to_send())
else:
await self.send_file(full_path, stream_id)
async def send_file(self, file_path, stream_id):
"""
Send a file, obeying the rules of HTTP/2 flow control.
"""
filesize = os.stat(file_path).st_size
content_type, content_encoding = mimetypes.guess_type(file_path)
response_headers = [
(':status', '200'),
('content-length', str(filesize)),
('server', 'curio-h2'),
]
if content_type:
response_headers.append(('content-type', content_type))
if content_encoding:
response_headers.append(('content-encoding', content_encoding))
self.conn.send_headers(stream_id, response_headers)
await self.sock.sendall(self.conn.data_to_send())
with open(file_path, 'rb', buffering=0) as f:
await self._send_file_data(f, stream_id)
async def _send_file_data(self, fileobj, stream_id):
"""
Send the data portion of a file. Handles flow control rules.
"""
while True:
while not self.conn.local_flow_control_window(stream_id):
await self.wait_for_flow_control(stream_id)
chunk_size = min(
self.conn.local_flow_control_window(stream_id),
READ_CHUNK_SIZE,
)
data = fileobj.read(chunk_size)
keep_reading = (len(data) == chunk_size)
self.conn.send_data(stream_id, data, not keep_reading)
await self.sock.sendall(self.conn.data_to_send())
if not keep_reading:
break
async def wait_for_flow_control(self, stream_id):
"""
Blocks until the flow control window for a given stream is opened.
"""
evt = Event()
self.flow_control_events[stream_id] = evt
await evt.wait()
async def window_updated(self, event):
"""
Unblock streams waiting on flow control, if needed.
"""
stream_id = event.stream_id
if stream_id and stream_id in self.flow_control_events:
evt = self.flow_control_events.pop(stream_id)
await evt.set()
elif not stream_id:
# Need to keep a real list here to use only the events present at
# this time.
blocked_streams = list(self.flow_control_events.keys())
for stream_id in blocked_streams:
event = self.flow_control_events.pop(stream_id)
await event.set()
return
if __name__ == '__main__':
host = sys.argv[2] if len(sys.argv) > 2 else "localhost"
kernel = Kernel(with_monitor=True)
print("Try GETting:")
print(" On OSX after 'brew install curl --with-c-ares --with-libidn --with-nghttp2 --with-openssl':")
print("/usr/local/opt/curl/bin/curl --tlsv1.2 --http2 -k https://localhost:5000/bundle.js")
print("Or open a browser to: https://localhost:5000/")
print(" (Accept all the warnings)")
kernel.run(h2_server((host, 5000),
sys.argv[1],
"{}.crt.pem".format(host),
"{}.key".format(host)))