9 from eventlet.green import BaseHTTPServer
10 from eventlet.green import socket
11 from eventlet.green import urllib
12 from eventlet import greenio
13 from eventlet import greenpool
14 from eventlet import support
15 from eventlet.support import six
18 DEFAULT_MAX_SIMULTANEOUS_REQUESTS = 1024
19 DEFAULT_MAX_HTTP_VERSION = 'HTTP/1.1'
20 MAX_REQUEST_LINE = 8192
21 MAX_HEADER_LINE = 8192
22 MAX_TOTAL_HEADER_SIZE = 65536
23 MINIMUM_CHUNK_SIZE = 4096
24 # %(client_port)s is also available
25 DEFAULT_LOG_FORMAT = ('%(client_ip)s - - [%(date_time)s] "%(request_line)s"'
26 ' %(status_code)s %(body_length)s %(wall_seconds).6f')
28 __all__ = ['server', 'format_date_time']
30 # Weekday and month names for HTTP date/time formatting; always English!
31 _weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
32 _monthname = [None, # Dummy so we can use 1-based month numbers
33 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
34 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
37 def format_date_time(timestamp):
38 """Formats a unix timestamp into an HTTP standard string."""
39 year, month, day, hh, mm, ss, wd, _y, _z = time.gmtime(timestamp)
40 return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
41 _weekdayname[wd], day, _monthname[month], year, hh, mm, ss
45 # Collections of error codes to compare against. Not all attributes are set
46 # on errno module on all platforms, so some are literals :(
47 BAD_SOCK = set((errno.EBADF, 10053))
48 BROKEN_SOCK = set((errno.EPIPE, errno.ECONNRESET))
51 # special flag return value for apps
52 class _AlreadyHandled(object):
63 ALREADY_HANDLED = _AlreadyHandled()
76 if content_length is not None:
77 content_length = int(content_length)
78 self.content_length = content_length
81 self.wfile_line = wfile_line
84 self.chunked_input = chunked_input
85 self.chunk_length = -1
87 # (optional) headers to send with a "100 Continue" response. Set by
88 # calling set_hundred_continue_respose_headers() on env['wsgi.input']
89 self.hundred_continue_headers = None
91 def _send_hundred_continue_response(self):
94 # 100 Continue status line
95 towrite.append(self.wfile_line)
98 if self.hundred_continue_headers is not None:
99 # 100 Continue headers
100 for header in self.hundred_continue_headers:
101 towrite.append('%s: %s\r\n' % header)
104 towrite.append('\r\n')
106 self.wfile.writelines(towrite)
108 self.wfile_line = None
110 def _do_read(self, reader, length=None):
111 if self.wfile is not None:
112 # 100 Continue response
113 self._send_hundred_continue_response()
114 if length is None and self.content_length is not None:
115 length = self.content_length - self.position
116 if length and length > self.content_length - self.position:
117 length = self.content_length - self.position
121 read = reader(length)
122 except greenio.SSL.ZeroReturnError:
124 self.position += len(read)
127 def _chunked_read(self, rfile, length=None, use_readline=False):
128 if self.wfile is not None:
129 # 100 Continue response
130 self._send_hundred_continue_response()
139 reader = self.rfile.readline
141 reader = self.rfile.read
144 while self.chunk_length != 0:
145 maxreadlen = self.chunk_length - self.position
146 if length is not None and length < maxreadlen:
150 data = reader(maxreadlen)
152 self.chunk_length = 0
153 raise IOError("unexpected end of file while parsing chunked data")
156 response.append(data)
158 self.position += datalen
159 if self.chunk_length == self.position:
162 if length is not None:
166 if use_readline and data[-1] == "\n":
169 self.chunk_length = int(rfile.readline().split(b";", 1)[0], 16)
171 if self.chunk_length == 0:
173 except greenio.SSL.ZeroReturnError:
175 return b''.join(response)
177 def read(self, length=None):
178 if self.chunked_input:
179 return self._chunked_read(self.rfile, length)
180 return self._do_read(self.rfile.read, length)
182 def readline(self, size=None):
183 if self.chunked_input:
184 return self._chunked_read(self.rfile, size, True)
186 return self._do_read(self.rfile.readline, size)
188 def readlines(self, hint=None):
189 return self._do_read(self.rfile.readlines, hint)
192 return iter(self.read, '')
194 def get_socket(self):
195 return self.rfile._sock
197 def set_hundred_continue_response_headers(self, headers,
198 capitalize_response_headers=True):
199 # Response headers capitalization (default)
200 # CONTent-TYpe: TExt/PlaiN -> Content-Type: TExt/PlaiN
201 # Per HTTP RFC standard, header name is case-insensitive.
202 # Please, fix your client to ignore header case if possible.
203 if capitalize_response_headers:
205 ('-'.join([x.capitalize() for x in key.split('-')]), value)
206 for key, value in headers]
207 self.hundred_continue_headers = headers
210 class HeaderLineTooLong(Exception):
214 class HeadersTooLarge(Exception):
218 class FileObjectForHeaders(object):
220 def __init__(self, fp):
222 self.total_header_size = 0
224 def readline(self, size=-1):
228 rv = self.fp.readline(sz)
229 if size < 0 and len(rv) >= MAX_HEADER_LINE:
230 raise HeaderLineTooLong()
231 self.total_header_size += len(rv)
232 if self.total_header_size > MAX_TOTAL_HEADER_SIZE:
233 raise HeadersTooLarge()
237 class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler):
238 protocol_version = 'HTTP/1.1'
239 minimum_chunk_size = MINIMUM_CHUNK_SIZE
240 capitalize_response_headers = True
243 # overriding SocketServer.setup to correctly handle SSL.Connection objects
244 conn = self.connection = self.request
246 self.rfile = conn.makefile('rb', self.rbufsize)
247 self.wfile = conn.makefile('wb', self.wbufsize)
248 except (AttributeError, NotImplementedError):
249 if hasattr(conn, 'send') and hasattr(conn, 'recv'):
250 # it's an SSL.Connection
251 self.rfile = socket._fileobject(conn, "rb", self.rbufsize)
252 self.wfile = socket._fileobject(conn, "wb", self.wbufsize)
254 # it's a SSLObject, or a martian
255 raise NotImplementedError("wsgi.py doesn't support sockets "
256 "of type %s" % type(conn))
258 def handle_one_request(self):
259 if self.server.max_http_version:
260 self.protocol_version = self.server.max_http_version
262 if self.rfile.closed:
263 self.close_connection = 1
267 self.raw_requestline = self.rfile.readline(self.server.url_length_limit)
268 if len(self.raw_requestline) == self.server.url_length_limit:
270 "HTTP/1.0 414 Request URI Too Long\r\n"
271 "Connection: close\r\nContent-length: 0\r\n\r\n")
272 self.close_connection = 1
274 except greenio.SSL.ZeroReturnError:
275 self.raw_requestline = ''
276 except socket.error as e:
277 if support.get_errno(e) not in BAD_SOCK:
279 self.raw_requestline = ''
281 if not self.raw_requestline:
282 self.close_connection = 1
285 orig_rfile = self.rfile
287 self.rfile = FileObjectForHeaders(self.rfile)
288 if not self.parse_request():
290 except HeaderLineTooLong:
292 "HTTP/1.0 400 Header Line Too Long\r\n"
293 "Connection: close\r\nContent-length: 0\r\n\r\n")
294 self.close_connection = 1
296 except HeadersTooLarge:
298 "HTTP/1.0 400 Headers Too Large\r\n"
299 "Connection: close\r\nContent-length: 0\r\n\r\n")
300 self.close_connection = 1
303 self.rfile = orig_rfile
305 content_length = self.headers.get('content-length')
311 "HTTP/1.0 400 Bad Request\r\n"
312 "Connection: close\r\nContent-length: 0\r\n\r\n")
313 self.close_connection = 1
316 self.environ = self.get_environ()
317 self.application = self.server.app
319 self.server.outstanding_requests += 1
321 self.handle_one_response()
322 except socket.error as e:
323 # Broken pipe, connection reset by peer
324 if support.get_errno(e) not in BROKEN_SOCK:
327 self.server.outstanding_requests -= 1
329 def handle_one_response(self):
336 use_chunked = [False]
340 def write(data, _writelines=wfile.writelines):
343 raise AssertionError("write() before start_response()")
344 elif not headers_sent:
345 status, response_headers = headers_set
346 headers_sent.append(1)
347 header_list = [header[0].lower() for header in response_headers]
348 towrite.append('%s %s\r\n' % (self.protocol_version, status))
349 for header in response_headers:
350 towrite.append('%s: %s\r\n' % header)
353 if 'date' not in header_list:
354 towrite.append('Date: %s\r\n' % (format_date_time(time.time()),))
356 client_conn = self.headers.get('Connection', '').lower()
357 send_keep_alive = False
358 if self.close_connection == 0 and \
359 self.server.keepalive and (client_conn == 'keep-alive' or
360 (self.request_version == 'HTTP/1.1' and
361 not client_conn == 'close')):
362 # only send keep-alives back to clients that sent them,
363 # it's redundant for 1.1 connections
364 send_keep_alive = (client_conn == 'keep-alive')
365 self.close_connection = 0
367 self.close_connection = 1
369 if 'content-length' not in header_list:
370 if self.request_version == 'HTTP/1.1':
371 use_chunked[0] = True
372 towrite.append('Transfer-Encoding: chunked\r\n')
373 elif 'content-length' not in header_list:
374 # client is 1.0 and therefore must read to EOF
375 self.close_connection = 1
377 if self.close_connection:
378 towrite.append('Connection: close\r\n')
379 elif send_keep_alive:
380 towrite.append('Connection: keep-alive\r\n')
381 towrite.append('\r\n')
382 # end of header writing
385 # Write the chunked encoding
386 towrite.append("%x\r\n%s\r\n" % (len(data), data))
391 length[0] = length[0] + sum(map(len, towrite))
392 except UnicodeEncodeError:
393 self.server.log_message(
394 "Encountered non-ascii unicode while attempting to write"
395 "wsgi response: %r" %
396 [x for x in towrite if isinstance(x, six.text_type)])
397 self.server.log_message(traceback.format_exc())
399 ["HTTP/1.1 500 Internal Server Error\r\n",
400 "Connection: close\r\n",
401 "Content-type: text/plain\r\n",
402 "Content-length: 98\r\n",
403 "Date: %s\r\n" % format_date_time(time.time()),
405 ("Internal Server Error: wsgi application passed "
406 "a unicode object to the server instead of a string.")])
408 def start_response(status, response_headers, exc_info=None):
409 status_code[0] = status.split()[0]
413 # Re-raise original exception if headers sent
414 six.reraise(exc_info[0], exc_info[1], exc_info[2])
416 # Avoid dangling circular ref
419 # Response headers capitalization
420 # CONTent-TYpe: TExt/PlaiN -> Content-Type: TExt/PlaiN
421 # Per HTTP RFC standard, header name is case-insensitive.
422 # Please, fix your client to ignore header case if possible.
423 if self.capitalize_response_headers:
425 ('-'.join([x.capitalize() for x in key.split('-')]), value)
426 for key, value in response_headers]
428 headers_set[:] = [status, response_headers]
433 result = self.application(self.environ, start_response)
434 if (isinstance(result, _AlreadyHandled)
435 or isinstance(getattr(result, '_obj', None), _AlreadyHandled)):
436 self.close_connection = 1
439 # Set content-length if possible
440 if not headers_sent and hasattr(result, '__len__') and \
441 'Content-Length' not in [h for h, _v in headers_set[1]]:
442 headers_set[1].append(('Content-Length', str(sum(map(len, result)))))
446 just_written_size = 0
447 minimum_write_chunk_size = int(self.environ.get(
448 'eventlet.minimum_write_chunk_size', self.minimum_chunk_size))
451 towrite_size += len(data)
452 if towrite_size >= minimum_write_chunk_size:
453 write(''.join(towrite))
455 just_written_size = towrite_size
458 just_written_size = towrite_size
459 write(''.join(towrite))
460 if not headers_sent or (use_chunked[0] and just_written_size):
463 self.close_connection = 1
464 tb = traceback.format_exc()
465 self.server.log_message(tb)
468 if(self.server.debug):
470 start_response("500 Internal Server Error",
471 [('Content-type', 'text/plain'),
472 ('Content-length', len(err_body))])
475 if hasattr(result, 'close'):
477 if (self.environ['eventlet.input'].chunked_input or
478 self.environ['eventlet.input'].position
479 < self.environ['eventlet.input'].content_length):
480 # Read and discard body if there was no pending 100-continue
481 if not self.environ['eventlet.input'].wfile:
482 # NOTE: MINIMUM_CHUNK_SIZE is used here for purpose different than chunking.
483 # We use it only cause it's at hand and has reasonable value in terms of
484 # emptying the buffer.
485 while self.environ['eventlet.input'].read(MINIMUM_CHUNK_SIZE):
489 for hook, args, kwargs in self.environ['eventlet.posthooks']:
490 hook(self.environ, *args, **kwargs)
492 if self.server.log_output:
493 self.server.log_message(self.server.log_format % {
494 'client_ip': self.get_client_ip(),
495 'client_port': self.client_address[1],
496 'date_time': self.log_date_time_string(),
497 'request_line': self.requestline,
498 'status_code': status_code[0],
499 'body_length': length[0],
500 'wall_seconds': finish - start,
503 def get_client_ip(self):
504 client_ip = self.client_address[0]
505 if self.server.log_x_forwarded_for:
506 forward = self.headers.get('X-Forwarded-For', '').replace(' ', '')
508 client_ip = "%s,%s" % (forward, client_ip)
511 def get_environ(self):
512 env = self.server.get_environ()
513 env['REQUEST_METHOD'] = self.command
514 env['SCRIPT_NAME'] = ''
516 pq = self.path.split('?', 1)
517 env['RAW_PATH_INFO'] = pq[0]
518 env['PATH_INFO'] = urllib.unquote(pq[0])
520 env['QUERY_STRING'] = pq[1]
522 ct = self.headers.get('content-type')
525 ct = self.headers.type
526 except AttributeError:
527 ct = self.headers.get_content_type()
528 env['CONTENT_TYPE'] = ct
530 length = self.headers.get('content-length')
532 env['CONTENT_LENGTH'] = length
533 env['SERVER_PROTOCOL'] = 'HTTP/1.0'
535 host, port = self.request.getsockname()[:2]
536 env['SERVER_NAME'] = host
537 env['SERVER_PORT'] = str(port)
538 env['REMOTE_ADDR'] = self.client_address[0]
539 env['REMOTE_PORT'] = str(self.client_address[1])
540 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
543 headers = self.headers.headers
544 except AttributeError:
545 headers = self.headers._headers
547 headers = [h.split(':', 1) for h in headers]
550 k = k.replace('-', '_').upper()
560 if env.get('HTTP_EXPECT') == '100-continue':
562 wfile_line = 'HTTP/1.1 100 Continue\r\n'
566 chunked = env.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked'
567 env['wsgi.input'] = env['eventlet.input'] = Input(
568 self.rfile, length, wfile=wfile, wfile_line=wfile_line,
569 chunked_input=chunked)
570 env['eventlet.posthooks'] = []
576 BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
577 except socket.error as e:
578 # Broken pipe, connection reset by peer
579 if support.get_errno(e) not in BROKEN_SOCK:
581 greenio.shutdown_safe(self.connection)
582 self.connection.close()
585 class Server(BaseHTTPServer.HTTPServer):
593 max_http_version=None,
594 protocol=HttpProtocol,
595 minimum_chunk_size=None,
596 log_x_forwarded_for=True,
599 log_format=DEFAULT_LOG_FORMAT,
600 url_length_limit=MAX_REQUEST_LINE,
603 capitalize_response_headers=True):
605 self.outstanding_requests = 0
607 self.address = address
611 self.log = sys.stderr
613 self.keepalive = keepalive
614 self.environ = environ
615 self.max_http_version = max_http_version
616 self.protocol = protocol
617 self.pid = os.getpid()
618 self.minimum_chunk_size = minimum_chunk_size
619 self.log_x_forwarded_for = log_x_forwarded_for
620 self.log_output = log_output
621 self.log_format = log_format
622 self.url_length_limit = url_length_limit
624 self.socket_timeout = socket_timeout
625 self.capitalize_response_headers = capitalize_response_headers
627 if not self.capitalize_response_headers:
628 warnings.warn("""capitalize_response_headers is disabled.
629 Please, make sure you know what you are doing.
630 HTTP headers names are case-insensitive per RFC standard.
631 Most likely, you need to fix HTTP parsing in your client software.""",
632 DeprecationWarning, stacklevel=3)
634 def get_environ(self):
636 'wsgi.errors': sys.stderr,
637 'wsgi.version': (1, 0),
638 'wsgi.multithread': True,
639 'wsgi.multiprocess': False,
640 'wsgi.run_once': False,
641 'wsgi.url_scheme': 'http',
643 # detect secure socket
644 if hasattr(self.socket, 'do_handshake'):
645 d['wsgi.url_scheme'] = 'https'
647 if self.environ is not None:
648 d.update(self.environ)
651 def process_request(self, sock_params):
652 # The actual request handling takes place in __init__, so we need to
653 # set minimum_chunk_size before __init__ executes and we don't want to modify
655 sock, address = sock_params
656 proto = new(self.protocol)
657 if self.minimum_chunk_size is not None:
658 proto.minimum_chunk_size = self.minimum_chunk_size
659 proto.capitalize_response_headers = self.capitalize_response_headers
661 proto.__init__(sock, address, self)
662 except socket.timeout:
663 # Expected exceptions are not exceptional
666 # similar to logging "accepted" in server()
667 self.log_message('(%s) timed out %r' % (self.pid, address))
669 def log_message(self, message):
670 self.log.write(message + '\n')
674 new = types.InstanceType
675 except AttributeError:
676 new = lambda cls: cls.__new__(cls)
681 ACCEPT_EXCEPTIONS = (socket.error, ssl.SSLError)
682 ACCEPT_ERRNO = set((errno.EPIPE, errno.EBADF, errno.ECONNRESET,
683 ssl.SSL_ERROR_EOF, ssl.SSL_ERROR_SSL))
685 ACCEPT_EXCEPTIONS = (socket.error,)
686 ACCEPT_ERRNO = set((errno.EPIPE, errno.EBADF, errno.ECONNRESET))
689 def server(sock, site,
693 max_http_version=DEFAULT_MAX_HTTP_VERSION,
694 protocol=HttpProtocol,
696 minimum_chunk_size=None,
697 log_x_forwarded_for=True,
701 log_format=DEFAULT_LOG_FORMAT,
702 url_length_limit=MAX_REQUEST_LINE,
705 capitalize_response_headers=True):
706 """Start up a WSGI server handling requests from the supplied server
707 socket. This function loops forever. The *sock* object will be
708 closed after server exits, but the underlying file descriptor will
709 remain open, so if you have a dup() of *sock*, it will remain usable.
711 :param sock: Server socket, must be already bound to a port and listening.
712 :param site: WSGI application function.
713 :param log: File-like object that logs should be written to.
714 If not specified, sys.stderr is used.
715 :param environ: Additional parameters that go into the environ dictionary of every request.
716 :param max_size: Maximum number of client connections opened at any time by this server.
717 :param max_http_version: Set to "HTTP/1.0" to make the server pretend it only supports HTTP 1.0.
718 This can help with applications or clients that don't behave properly using HTTP 1.1.
719 :param protocol: Protocol class. Deprecated.
720 :param server_event: Used to collect the Server object. Deprecated.
721 :param minimum_chunk_size: Minimum size in bytes for http chunks. This can be used to improve
722 performance of applications which yield many small strings, though
723 using it technically violates the WSGI spec. This can be overridden
724 on a per request basis by setting environ['eventlet.minimum_write_chunk_size'].
725 :param log_x_forwarded_for: If True (the default), logs the contents of the x-forwarded-for
726 header in addition to the actual client ip address in the 'client_ip' field of the
728 :param custom_pool: A custom GreenPool instance which is used to spawn client green threads.
729 If this is supplied, max_size is ignored.
730 :param keepalive: If set to False, disables keepalives on the server; all connections will be
731 closed after serving one request.
732 :param log_output: A Boolean indicating if the server will log data or not.
733 :param log_format: A python format string that is used as the template to generate log lines.
734 The following values can be formatted into it: client_ip, date_time, request_line,
735 status_code, body_length, wall_seconds. The default is a good example of how to
737 :param url_length_limit: A maximum allowed length of the request url. If exceeded, 414 error
739 :param debug: True if the server should send exception tracebacks to the clients on 500 errors.
740 If False, the server will respond with empty bodies.
741 :param socket_timeout: Timeout for client connections' socket operations. Default None means
743 :param capitalize_response_headers: Normalize response headers' names to Foo-Bar.
746 serv = Server(sock, sock.getsockname(),
749 max_http_version=max_http_version,
751 minimum_chunk_size=minimum_chunk_size,
752 log_x_forwarded_for=log_x_forwarded_for,
754 log_output=log_output,
755 log_format=log_format,
756 url_length_limit=url_length_limit,
758 socket_timeout=socket_timeout,
759 capitalize_response_headers=capitalize_response_headers,
761 if server_event is not None:
762 server_event.send(serv)
764 max_size = DEFAULT_MAX_SIMULTANEOUS_REQUESTS
765 if custom_pool is not None:
768 pool = greenpool.GreenPool(max_size)
770 host, port = sock.getsockname()[:2]
771 port = ':%s' % (port, )
772 if hasattr(sock, 'do_handshake'):
781 serv.log.write("(%s) wsgi starting up on %s://%s%s/\n" % (
782 serv.pid, scheme, host, port))
785 client_socket = sock.accept()
786 client_socket[0].settimeout(serv.socket_timeout)
788 serv.log.write("(%s) accepted %r\n" % (
789 serv.pid, client_socket[1]))
791 pool.spawn_n(serv.process_request, client_socket)
792 except AttributeError:
793 warnings.warn("wsgi's pool should be an instance of "
794 "eventlet.greenpool.GreenPool, is %s. Please convert your"
795 " call site to use GreenPool instead" % type(pool),
796 DeprecationWarning, stacklevel=2)
797 pool.execute_async(serv.process_request, client_socket)
798 except ACCEPT_EXCEPTIONS as e:
799 if support.get_errno(e) not in ACCEPT_ERRNO:
801 except (KeyboardInterrupt, SystemExit):
802 serv.log.write("wsgi exiting\n")
806 # NOTE: It's not clear whether we want this to leave the
807 # socket open or close it. Use cases like Spawning want
808 # the underlying fd to remain open, but if we're going
809 # that far we might as well not bother closing sock at
812 except socket.error as e:
813 if support.get_errno(e) not in BROKEN_SOCK:
814 traceback.print_exc()