import os import posixpath import sys from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler from io import BytesIO from urllib import parse as urlparse from urllib.parse import unquote from . import utils from . import webapi as web __all__ = ["runsimple"] def runbasic(func, server_address=("0.0.0.0", 8080)): """ Runs a simple HTTP server hosting WSGI app `func`. The directory `static/` is hosted statically. Based on [WsgiServer][ws] from [Colin Stewart][cs]. [ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html [cs]: http://www.owlfish.com/ """ # Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/) # Modified somewhat for simplicity # Used under the modified BSD license: # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 import errno import socket import traceback import SocketServer class WSGIHandler(SimpleHTTPRequestHandler): def run_wsgi_app(self): protocol, host, path, parameters, query, fragment = urlparse.urlparse( "http://dummyhost%s" % self.path ) # we only use path, query env = { "wsgi.version": (1, 0), "wsgi.url_scheme": "http", "wsgi.input": self.rfile, "wsgi.errors": sys.stderr, "wsgi.multithread": 1, "wsgi.multiprocess": 0, "wsgi.run_once": 0, "REQUEST_METHOD": self.command, "REQUEST_URI": self.path, "PATH_INFO": path, "QUERY_STRING": query, "CONTENT_TYPE": self.headers.get("Content-Type", ""), "CONTENT_LENGTH": self.headers.get("Content-Length", ""), "REMOTE_ADDR": self.client_address[0], "SERVER_NAME": self.server.server_address[0], "SERVER_PORT": str(self.server.server_address[1]), "SERVER_PROTOCOL": self.request_version, } for http_header, http_value in self.headers.items(): env["HTTP_%s" % http_header.replace("-", "_").upper()] = http_value # Setup the state self.wsgi_sent_headers = 0 self.wsgi_headers = [] try: # We have there environment, now invoke the application result = self.server.app(env, self.wsgi_start_response) try: try: for data in result: if data: self.wsgi_write_data(data) finally: if hasattr(result, "close"): result.close() except OSError as socket_err: # Catch common network errors and suppress them if socket_err.args[0] in (errno.ECONNABORTED, errno.EPIPE): return except socket.timeout: return except: print(traceback.format_exc(), file=web.debug) if not self.wsgi_sent_headers: # We must write out something! self.wsgi_write_data(" ") return do_POST = run_wsgi_app do_PUT = run_wsgi_app do_DELETE = run_wsgi_app def do_GET(self): if self.path.startswith("/static/"): SimpleHTTPRequestHandler.do_GET(self) else: self.run_wsgi_app() def wsgi_start_response(self, response_status, response_headers, exc_info=None): if self.wsgi_sent_headers: raise Exception("Headers already sent and start_response called again!") # Should really take a copy to avoid changes in the application.... self.wsgi_headers = (response_status, response_headers) return self.wsgi_write_data def wsgi_write_data(self, data): if not self.wsgi_sent_headers: status, headers = self.wsgi_headers # Need to send header prior to data status_code = status[: status.find(" ")] status_msg = status[status.find(" ") + 1 :] self.send_response(int(status_code), status_msg) for header, value in headers: self.send_header(header, value) self.end_headers() self.wsgi_sent_headers = 1 # Send the data self.wfile.write(data) class WSGIServer(SocketServer.ThreadingMixIn, HTTPServer): def __init__(self, func, server_address): HTTPServer.HTTPServer.__init__(self, server_address, WSGIHandler) self.app = func self.serverShuttingDown = 0 print("http://%s:%d/" % server_address) WSGIServer(func, server_address).serve_forever() # The WSGIServer instance. # Made global so that it can be stopped in embedded mode. server = None def runsimple(func, server_address=("0.0.0.0", 8080)): """ Runs [CherryPy][cp] WSGI server hosting WSGI app `func`. The directory `static/` is hosted statically. [cp]: http://www.cherrypy.org """ global server func = StaticMiddleware(func) func = LogMiddleware(func) server = WSGIServer(server_address, func) if "/" in server_address[0]: print("%s" % server_address) else: if server.ssl_adapter: print("https://%s:%d/" % server_address) else: print("http://%s:%d/" % server_address) try: server.start() except (KeyboardInterrupt, SystemExit): server.stop() server = None def WSGIServer(server_address, wsgi_app): """Creates CherryPy WSGI server listening at `server_address` to serve `wsgi_app`. This function can be overwritten to customize the webserver or use a different webserver. """ from cheroot import wsgi server = wsgi.Server(server_address, wsgi_app, server_name="localhost") server.nodelay = not sys.platform.startswith( "java" ) # TCP_NODELAY isn't supported on the JVM return server class StaticApp(SimpleHTTPRequestHandler): """WSGI application for serving static files.""" def __init__(self, environ, start_response): self.headers = [] self.environ = environ self.start_response = start_response self.directory = os.getcwd() def send_response(self, status, msg=""): # the int(status) call is needed because in Py3 status is an enum.IntEnum and we need the integer behind self.status = str(int(status)) + " " + msg def send_header(self, name, value): self.headers.append((name, value)) def end_headers(self): pass def log_message(*a): pass def __iter__(self): environ = self.environ self.path = environ.get("PATH_INFO", "") self.client_address = ( environ.get("REMOTE_ADDR", "-"), environ.get("REMOTE_PORT", "-"), ) self.command = environ.get("REQUEST_METHOD", "-") self.wfile = BytesIO() # for capturing error try: path = self.translate_path(self.path) etag = '"%s"' % os.path.getmtime(path) client_etag = environ.get("HTTP_IF_NONE_MATCH") self.send_header("ETag", etag) if etag == client_etag: self.send_response(304, "Not Modified") self.start_response(self.status, self.headers) return except OSError: pass # Probably a 404 f = self.send_head() self.start_response(self.status, self.headers) if f: block_size = 16 * 1024 while True: buf = f.read(block_size) if not buf: break yield buf f.close() else: value = self.wfile.getvalue() yield value class StaticMiddleware: """WSGI middleware for serving static files.""" def __init__(self, app, prefix="/static/"): self.app = app self.prefix = prefix def __call__(self, environ, start_response): path = environ.get("PATH_INFO", "") path = self.normpath(path) if path.startswith(self.prefix): return StaticApp(environ, start_response) else: return self.app(environ, start_response) def normpath(self, path): path2 = posixpath.normpath(unquote(path)) if path.endswith("/"): path2 += "/" return path2 class LogMiddleware: """WSGI middleware for logging the status.""" def __init__(self, app): self.app = app self.format = '%s - - [%s] "%s %s %s" - %s' f = BytesIO() class FakeSocket: def makefile(self, *a): return f # take log_date_time_string method from BaseHTTPRequestHandler self.log_date_time_string = BaseHTTPRequestHandler( FakeSocket(), None, None ).log_date_time_string def __call__(self, environ, start_response): def xstart_response(status, response_headers, *args): out = start_response(status, response_headers, *args) self.log(status, environ) return out return self.app(environ, xstart_response) def log(self, status, environ): outfile = environ.get("wsgi.errors", web.debug) req = environ.get("PATH_INFO", "_") protocol = environ.get("ACTUAL_SERVER_PROTOCOL", "-") method = environ.get("REQUEST_METHOD", "-") host = "{}:{}".format( environ.get("REMOTE_ADDR", "-"), environ.get("REMOTE_PORT", "-"), ) time = self.log_date_time_string() msg = self.format % (host, time, protocol, method, req, status) print(utils.safestr(msg), file=outfile)