# (c) 2009-2024 Martin Wendt and contributors; see WsgiDAV https://github.com/mar10/wsgidav
# Original PyFileServer (c) 2005 Ho Chun Wei.
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
"""
ext_wsgiutils_server.py is an extension of the wsgiutils server in Paste.
It supports passing all of the HTTP and WebDAV (rfc 2518) methods.
It includes code from the following sources:
``wsgiServer.py`` from wsgiKit <http://www.owlfish.com/software/wsgiutils/> under PSF license,
``wsgiutils_server.py`` from Paste <http://pythonpaste.org> under PSF license,
flexible handler method <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/307618>
under public domain.
Running as standalone server
----------------------------
To run as a standalone server using the bundled ext_wsgiutils_server.py::
usage: python ext_wsgiutils_server.py [options] [config-file]
config-file:
The configuration file for WsgiDAV. if omitted, the application
will look for a file named 'WsgiDAV.conf' in the current directory
options:
--port=PORT Port to serve on (default: 8080)
--host=HOST Host to serve from (default: localhost, which is only
accessible from the local computer; use 0.0.0.0 to make your
application public)
-h, --help show this help message and exit
Running using other web servers
-------------------------------
To run it with other WSGI web servers, you can::
from wsgidav.mainappwrapper import PyFileApp
publish_app = PyFileApp('WsgiDAV.conf')
# construct the application with configuration file
# if configuration file is omitted, the application
# will look for a file named 'WsgiDAV.conf'
# in the current directory
where ``publish_app`` is the WSGI application to be run, it will be called with
``publish_app(environ, start_response)`` for each incoming request, as described in
`WSGI <http://www.python.org/peps/pep-3333.html>`_
Note: if you are using the paster development server (from Paste <http://pythonpaste.org>), you
can copy ``ext_wsgi_server.py`` to ``<Paste-installation>/paste/servers`` and use this server to
run the application by specifying ``server='ext_wsgiutils'`` in the ``server.conf`` or appropriate
paste configuration.
"""
__docformat__ = "reStructuredText"
import logging
import socket
import socketserver
import sys
import threading
import time
import traceback
from http import client as http_client
from http import server as BaseHTTPServer
from io import StringIO
from urllib.parse import urlparse
from wsgidav import util
_logger = util.get_module_logger(__name__)
_version = 1.0
SERVER_ERROR = """\
<html>
<head>
<title>Server Error</title>
</head>
<body>
<h1>Server Error</h1>
A server error has occurred. Please contact the system administrator for
more information.
</body>
</html>
"""
[docs]
class ExtHandler(BaseHTTPServer.BaseHTTPRequestHandler):
_SUPPORTED_METHODS = [
"HEAD",
"GET",
"PUT",
"POST",
"OPTIONS",
"TRACE",
"DELETE",
"PROPFIND",
"PROPPATCH",
"MKCOL",
"COPY",
"MOVE",
"LOCK",
"UNLOCK",
]
# Enable automatic keep-alive:
protocol_version = "HTTP/1.1"
server_version = f"{util.public_wsgidav_info} ExtServer/{_version} {BaseHTTPServer.BaseHTTPRequestHandler.server_version}"
[docs]
def log_message(self, *args):
pass
# BaseHTTPServer.BaseHTTPRequestHandler.log_message(self, *args)
[docs]
def log_request(self, *args):
pass
# BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, *args)
[docs]
def getApp(self):
# We want fragments to be returned as part of <path>
_protocol, _host, path, _parameters, query, _fragment = urlparse(
f"http://dummyhost{self.path}", allow_fragments=False
)
# Find any application we might have
for appPath, app in self.server.wsgiApplications:
if path.startswith(appPath):
# We found the application to use - work out the script_name and path_info
path_info = path[len(appPath) :]
if len(path_info) > 0:
if not path_info.startswith("/"):
path_info = "/" + path_info
if appPath.endswith("/"):
script_name = appPath[:-1]
else:
script_name = appPath
# Return all this
return app, script_name, path_info, query
return None, None, None, None
[docs]
def handlerFunctionClosure(self, name):
def handlerFunction(*args, **kwargs):
self.do_method()
return handlerFunction
[docs]
def do_method(self):
app, script_name, path_info, query = self.getApp()
if not app:
self.send_error(404, "Application not found.")
return
self.runWSGIApp(app, script_name, path_info, query)
def __getattr__(self, name):
if len(name) > 3 and name[0:3] == "do_" and name[3:] in self._SUPPORTED_METHODS:
return self.handlerFunctionClosure(name)
elif name == "_headers_buffer":
# 2015-10-22: py3: prevent recursion/stackovervlow
raise AttributeError
self.send_error(501, "Method Not Implemented.")
return
[docs]
def runWSGIApp(self, application, script_name, path_info, query):
# logging.info ("Running application with SCRIPT_NAME {} PATH_INFO {}".format(
# script_name, path_info))
if self.command == "PUT":
pass # breakpoint
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,
"SCRIPT_NAME": script_name,
"PATH_INFO": path_info,
"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": util.to_str(self.server.server_address[1]),
"SERVER_PROTOCOL": self.request_version,
}
for httpHeader, httpValue in self.headers.items():
if httpHeader.lower() not in ("content-type", "content-length"):
env["HTTP_{}".format(httpHeader.replace("-", "_").upper())] = httpValue
# Setup the state
self.wsgiSentHeaders = 0
self.wsgiHeaders = []
try:
# We have there environment, now invoke the application
_logger.debug("runWSGIApp application()...")
result = application(env, self.wsgiStartResponse)
try:
for data in result:
if data:
self.wsgiWriteData(data)
else:
_logger.debug("runWSGIApp empty data")
finally:
_logger.debug("runWSGIApp finally.")
if hasattr(result, "close"):
result.close()
except Exception:
_logger.debug("runWSGIApp caught exception...")
errorMsg = StringIO()
traceback.print_exc(file=errorMsg)
logging.error(errorMsg.getvalue())
if not self.wsgiSentHeaders:
self.wsgiStartResponse(
"500 Server Error", [("Content-type", "text/html")]
)
self.wsgiWriteData(SERVER_ERROR)
if not self.wsgiSentHeaders:
# GC issue 29 sending one byte, when content-length is '0' seems wrong
# We must write out something!
# self.wsgiWriteData (" ")
self.wsgiWriteData(b"")
return
[docs]
def wsgiStartResponse(self, response_status, response_headers, exc_info=None):
_logger.debug(
f"wsgiStartResponse({response_status}, {response_headers}, {exc_info})"
)
if self.wsgiSentHeaders:
raise Exception("Headers already sent and start_response called again!")
# Should really take a copy to avoid changes in the application....
self.wsgiHeaders = (response_status, response_headers)
return self.wsgiWriteData
[docs]
def wsgiWriteData(self, data):
if not self.wsgiSentHeaders:
status, headers = self.wsgiHeaders
# Need to send header prior to data
statusCode = status[: status.find(" ")]
statusMsg = status[status.find(" ") + 1 :]
_logger.debug(f"wsgiWriteData: send headers '{status!r}', {headers!r}")
self.send_response(int(statusCode), statusMsg)
for header, value in headers:
self.send_header(header, value)
self.end_headers()
self.wsgiSentHeaders = 1
# Send the data
assert type(data) is bytes # If not, Content-Length is probably wrong!
_logger.debug(
f"wsgiWriteData: write {len(data)} bytes: '{util.to_str(data[:50])!r}'..."
)
if util.is_str(data): # If not, Content-Length is probably wrong!
_logger.info(f"ext_wsgiutils_server: Got unicode data: {data!r}")
# data = util.wsgi_to_bytes(data)
data = util.to_bytes(data)
try:
self.wfile.write(data)
except OSError as e:
# Suppress stack trace when client aborts connection disgracefully:
# 10053: Software caused connection abort
# 10054: Connection reset by peer
if e.args[0] in (10053, 10054):
_logger.info("*** Caught socket.error: %s", e, file=sys.stderr)
else:
raise
[docs]
class ExtServer(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
[docs]
def handle_error(self, request, client_address):
"""Handle an error gracefully. May be overridden.
The default is to _logger.info a traceback and continue.
"""
ei = sys.exc_info()
e = ei[1]
# Suppress stack trace when client aborts connection disgracefully:
# 10053: Software caused connection abort
# 10054: Connection reset by peer
if e.args[0] in (10053, 10054):
_logger.error(f"*** Caught socket.error: {e}")
return
# This is what BaseHTTPServer.HTTPServer.handle_error does, but with
# added thread ID and using stderr
_logger.error("-" * 40, file=sys.stderr)
_logger.error(
f"<{threading.current_thread().ident}> Exception happened during processing of request from {client_address}"
)
_logger.error(client_address, file=sys.stderr)
traceback.print_exc()
_logger.error("-" * 40, file=sys.stderr)
_logger.error(request, file=sys.stderr)
# BaseHTTPServer.HTTPServer.handle_error(self, request, client_address)
[docs]
def stop_serve_forever(self):
"""Stop serve_forever_stoppable()."""
assert hasattr(
self, "stop_request"
), "serve_forever_stoppable() must be called before"
assert not self.stop_request, "stop_serve_forever() must only be called once"
# # Flag stop request
self.stop_request = True
time.sleep(0.1)
if self.stopped:
# _logger.info "stop_serve_forever() 'stopped'."
return
# Add a do_SHUTDOWN method to to the ExtHandler class
def _shutdownHandler(self):
"""Send 200 OK response, and set server.stop_request to True.
http://code.activestate.com/recipes/336012/
"""
# _logger.info "Handling do_SHUTDOWN request"
self.send_response(200)
self.end_headers()
self.server.stop_request = True
if not hasattr(ExtHandler, "do_SHUTDOWN"):
ExtHandler.do_SHUTDOWN = _shutdownHandler
# Send request, so socket is unblocked
(host, port) = self.server_address
# _logger.info "stop_serve_forever() sending {}:{}/ SHUTDOWN...".format(host, port)
conn = http_client.HTTPConnection(f"{host}:{port}")
conn.request("SHUTDOWN", "/")
# _logger.info "stop_serve_forever() sent SHUTDOWN request, reading response..."
conn.getresponse()
# _logger.info "stop_serve_forever() received SHUTDOWN response."
assert self.stop_request
[docs]
def serve_forever_stoppable(self):
"""Handle one request at a time until stop_serve_forever().
http://code.activestate.com/recipes/336012/
"""
self.stop_request = False
self.stopped = False
while not self.stop_request:
self.handle_request()
# _logger.info "serve_forever_stoppable() stopped."
self.stopped = True
def __init__(self, serverAddress, wsgiApplications, serveFiles=1):
BaseHTTPServer.HTTPServer.__init__(self, serverAddress, ExtHandler)
appList = []
for urlPath, wsgiApp in wsgiApplications.items():
appList.append((urlPath, wsgiApp))
self.wsgiApplications = appList
self.serveFiles = serveFiles
self.serverShuttingDown = 0
[docs]
def serve(conf, app):
host = conf.get("host", "localhost")
port = int(conf.get("port", 8080))
server = ExtServer((host, port), {"": app})
server_version = ExtHandler.server_version
if conf.get("verbose") >= 1:
_logger.info(f"Running {server_version}")
if host in ("", "0.0.0.0"):
(hostname, _aliaslist, ipaddrlist) = socket.gethostbyname_ex(
socket.gethostname()
)
_logger.info(
f"Serving at {host}, port {port} (host={hostname!r} {ipaddrlist})..."
)
else:
_logger.info(f"Serving at {host}, port {port}...")
server.serve_forever()
# server.serve_forever_stoppable()
if __name__ == "__main__":
raise RuntimeError("Use run_server.py")