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)