| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/env python |
| 2 |
| 3 __copyright__ = """\ |
| 4 Copyright (c) 2008-2009 Mark Nottingham |
| 5 |
| 6 Permission is hereby granted, free of charge, to any person obtaining a copy |
| 7 of this software and associated documentation files (the "Software"), to deal |
| 8 in the Software without restriction, including without limitation the rights |
| 9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 10 copies of the Software, and to permit persons to whom the Software is |
| 11 furnished to do so, subject to the following conditions: |
| 12 |
| 13 The above copyright notice and this permission notice shall be included in |
| 14 all copies or substantial portions of the Software. |
| 15 |
| 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| 22 THE SOFTWARE. |
| 23 """ |
| 24 |
| 25 """ |
| 26 Non-Blocking HTTP Server |
| 27 |
| 28 This library allow implementation of an HTTP/1.1 server that is "non-blocking," |
| 29 "asynchronous" and "event-driven" -- i.e., it achieves very high performance |
| 30 and concurrency, so long as the application code does not block (e.g., |
| 31 upon network, disk or database access). Blocking on one request will block |
| 32 the entire server. |
| 33 |
| 34 Instantiate a Server with the following parameters: |
| 35 - host (string) |
| 36 - port (int) |
| 37 - req_start (callable) |
| 38 |
| 39 req_start is called when a request starts. It must take the following arguments: |
| 40 - method (string) |
| 41 - uri (string) |
| 42 - req_hdrs (list of (name, value) tuples) |
| 43 - res_start (callable) |
| 44 - req_body_pause (callable) |
| 45 and return: |
| 46 - req_body (callable) |
| 47 - req_done (callable) |
| 48 |
| 49 req_body is called when part of the request body is available. It must take the |
| 50 following argument: |
| 51 - chunk (string) |
| 52 |
| 53 req_done is called when the request is complete, whether or not it contains a |
| 54 body. It must take the following argument: |
| 55 - err (error dictionary, or None for no error) |
| 56 |
| 57 Call req_body_pause when you want the server to temporarily stop sending the |
| 58 request body, or restart. You must provide the following argument: |
| 59 - paused (boolean; True means pause, False means unpause) |
| 60 |
| 61 Call res_start when you want to start the response, and provide the following |
| 62 arguments: |
| 63 - status_code (string) |
| 64 - status_phrase (string) |
| 65 - res_hdrs (list of (name, value) tuples) |
| 66 - res_body_pause |
| 67 It returns: |
| 68 - res_body (callable) |
| 69 - res_done (callable) |
| 70 |
| 71 Call res_body to send part of the response body to the client. Provide the |
| 72 following parameter: |
| 73 - chunk (string) |
| 74 |
| 75 Call res_done when the response is finished, and provide the |
| 76 following argument if appropriate: |
| 77 - err (error dictionary, or None for no error) |
| 78 |
| 79 See the error module for the complete list of valid error dictionaries. |
| 80 |
| 81 Where possible, errors in the request will be responded to with the appropriate |
| 82 4xx HTTP status code. However, if a response has already been started, the |
| 83 connection will be dropped (for example, when the request chunking or |
| 84 indicated length are incorrect). |
| 85 """ |
| 86 |
| 87 __author__ = "Mark Nottingham <mnot@mnot.net>" |
| 88 |
| 89 import os |
| 90 import sys |
| 91 import logging |
| 92 |
| 93 import push_tcp |
| 94 from http_common import HttpMessageHandler, \ |
| 95 CLOSE, COUNTED, CHUNKED, \ |
| 96 WAITING, HEADERS_DONE, \ |
| 97 hop_by_hop_hdrs, \ |
| 98 dummy, get_hdr |
| 99 |
| 100 from error import ERR_HTTP_VERSION, ERR_HOST_REQ, ERR_WHITESPACE_HDR, ERR_TRANSF
ER_CODE |
| 101 |
| 102 # FIXME: assure that the connection isn't closed before reading the entire req b
ody |
| 103 # TODO: filter out 100 responses to HTTP/1.0 clients that didn't ask for it. |
| 104 |
| 105 class Server: |
| 106 "An asynchronous HTTP server." |
| 107 def __init__(self, host, port, request_handler): |
| 108 self.request_handler = request_handler |
| 109 self.server = push_tcp.create_server(host, port, self.handle_connection) |
| 110 self.log = logging.getLogger('server') |
| 111 self.log.setLevel(logging.WARNING) |
| 112 |
| 113 |
| 114 def handle_connection(self, tcp_conn): |
| 115 "Process a new push_tcp connection, tcp_conn." |
| 116 conn = HttpServerConnection(self.request_handler, tcp_conn) |
| 117 return conn._handle_input, conn._conn_closed, conn._res_body_pause |
| 118 |
| 119 |
| 120 class HttpServerConnection(HttpMessageHandler): |
| 121 "A handler for an HTTP server connection." |
| 122 def __init__(self, request_handler, tcp_conn): |
| 123 HttpMessageHandler.__init__(self) |
| 124 self.request_handler = request_handler |
| 125 self._tcp_conn = tcp_conn |
| 126 self.req_body_cb = None |
| 127 self.req_done_cb = None |
| 128 self.method = None |
| 129 self.req_version = None |
| 130 self.connection_hdr = [] |
| 131 self._res_body_pause_cb = None |
| 132 |
| 133 def res_start(self, status_code, status_phrase, res_hdrs, res_body_pause): |
| 134 "Start a response. Must only be called once per response." |
| 135 self._res_body_pause_cb = res_body_pause |
| 136 res_hdrs = [i for i in res_hdrs \ |
| 137 if not i[0].lower() in hop_by_hop_hdrs ] |
| 138 |
| 139 try: |
| 140 body_len = int(get_hdr(res_hdrs, "content-length").pop(0)) |
| 141 except (IndexError, ValueError): |
| 142 body_len = None |
| 143 if body_len is not None: |
| 144 delimit = COUNTED |
| 145 res_hdrs.append(("Connection", "keep-alive")) |
| 146 elif 2.0 > self.req_version >= 1.1: |
| 147 delimit = CHUNKED |
| 148 res_hdrs.append(("Transfer-Encoding", "chunked")) |
| 149 else: |
| 150 delimit = CLOSE |
| 151 res_hdrs.append(("Connection", "close")) |
| 152 |
| 153 self._output_start("HTTP/1.1 %s %s" % (status_code, status_phrase), res_
hdrs, delimit) |
| 154 return self.res_body, self.res_done |
| 155 |
| 156 def res_body(self, chunk): |
| 157 "Send part of the response body. May be called zero to many times." |
| 158 self._output_body(chunk) |
| 159 |
| 160 def res_done(self, err): |
| 161 """ |
| 162 Signal the end of the response, whether or not there was a body. MUST be |
| 163 called exactly once for each response. |
| 164 |
| 165 If err is not None, it is an error dictionary (see the error module) |
| 166 indicating that an HTTP-specific (i.e., non-application) error occured |
| 167 in the generation of the response; this is useful for debugging. |
| 168 """ |
| 169 self._output_end(err) |
| 170 |
| 171 def req_body_pause(self, paused): |
| 172 "Indicate that the server should pause (True) or unpause (False) the req
uest." |
| 173 if self._tcp_conn and self._tcp_conn.tcp_connected: |
| 174 self._tcp_conn.pause(paused) |
| 175 |
| 176 # Methods called by push_tcp |
| 177 |
| 178 def _res_body_pause(self, paused): |
| 179 "Pause/unpause sending the response body." |
| 180 if self._res_body_pause_cb: |
| 181 self._res_body_pause_cb(paused) |
| 182 |
| 183 def _conn_closed(self): |
| 184 "The server connection has closed." |
| 185 if self._output_state != WAITING: |
| 186 pass # FIXME: any cleanup necessary? |
| 187 # self.pause() |
| 188 # self._queue = [] |
| 189 # self.tcp_conn.handler = None |
| 190 # self.tcp_conn = None |
| 191 |
| 192 # Methods called by common.HttpRequestHandler |
| 193 |
| 194 def _output(self, chunk): |
| 195 self._tcp_conn.write(chunk) |
| 196 |
| 197 def _input_start(self, top_line, hdr_tuples, conn_tokens, transfer_codes, co
ntent_length): |
| 198 """ |
| 199 Take the top set of headers from the input stream, parse them |
| 200 and queue the request to be processed by the application. |
| 201 """ |
| 202 assert self._input_state == WAITING, "pipelining not supported" # FIXME:
pipelining |
| 203 try: |
| 204 method, _req_line = top_line.split(None, 1) |
| 205 uri, req_version = _req_line.rsplit(None, 1) |
| 206 self.req_version = float(req_version.rsplit('/', 1)[1]) |
| 207 except ValueError: |
| 208 self._handle_error(ERR_HTTP_VERSION, top_line) # FIXME: more fine-gr
ained |
| 209 raise ValueError |
| 210 if self.req_version == 1.1 and 'host' not in [t[0].lower() for t in hdr_
tuples]: |
| 211 self._handle_error(ERR_HOST_REQ) |
| 212 raise ValueError |
| 213 if hdr_tuples[:1][:1][:1] in [" ", "\t"]: |
| 214 self._handle_error(ERR_WHITESPACE_HDR) |
| 215 for code in transfer_codes: # we only support 'identity' and chunked' co
des |
| 216 if code not in ['identity', 'chunked']: |
| 217 # FIXME: SHOULD also close connection |
| 218 self._handle_error(ERR_TRANSFER_CODE) |
| 219 raise ValueError |
| 220 # FIXME: MUST 400 request messages with whitespace between name and colo
n |
| 221 self.method = method |
| 222 self.connection_hdr = conn_tokens |
| 223 |
| 224 self.log.info("%s server req_start %s %s %s", |
| 225 id(self), method, uri, self.req_version) |
| 226 self.req_body_cb, self.req_done_cb = self.request_handler( |
| 227 method, uri, hdr_tuples, self.res_start, self.req_body_pause) |
| 228 allows_body = (content_length) or (transfer_codes != []) |
| 229 return allows_body |
| 230 |
| 231 def _input_body(self, chunk): |
| 232 "Process a request body chunk from the wire." |
| 233 self.req_body_cb(chunk) |
| 234 |
| 235 def _input_end(self): |
| 236 "Indicate that the request body is complete." |
| 237 self.req_done_cb(None) |
| 238 |
| 239 def _input_error(self, err, detail=None): |
| 240 "Indicate a parsing problem with the request body." |
| 241 err['detail'] = detail |
| 242 if self._tcp_conn: |
| 243 self._tcp_conn.close() |
| 244 self._tcp_conn = None |
| 245 self.req_done_cb(err) |
| 246 |
| 247 def _handle_error(self, err, detail=None): |
| 248 "Handle a problem with the request by generating an appropriate response
." |
| 249 # self._queue.append(ErrorHandler(status_code, status_phrase, body, self)
) |
| 250 assert self._output_state == WAITING |
| 251 if detail: |
| 252 err['detail'] = detail |
| 253 status_code, status_phrase = err.get('status', ('400', 'Bad Request')) |
| 254 hdrs = [ |
| 255 ('Content-Type', 'text/plain'), |
| 256 ] |
| 257 body = err['desc'] |
| 258 if err.has_key('detail'): |
| 259 body += " (%s)" % err['detail'] |
| 260 self.res_start(status_code, status_phrase, hdrs, dummy) |
| 261 self.res_body(body) |
| 262 self.res_done() |
| 263 |
| 264 |
| 265 def test_handler(method, uri, hdrs, res_start, req_pause): |
| 266 """ |
| 267 An extremely simple (and limited) server request_handler. |
| 268 """ |
| 269 code = "200" |
| 270 phrase = "OK" |
| 271 res_hdrs = [('Content-Type', 'text/plain')] |
| 272 res_body, res_done = res_start(code, phrase, res_hdrs, dummy) |
| 273 res_body('foo!') |
| 274 res_done(None) |
| 275 return dummy, dummy |
| 276 |
| 277 if __name__ == "__main__": |
| 278 sys.stderr.write("PID: %s\n" % os.getpid()) |
| 279 h, p = '127.0.0.1', int(sys.argv[1]) |
| 280 server = Server(h, p, test_handler) |
| 281 push_tcp.run() |
| OLD | NEW |