From 782b02dd37f4f28b9190effb5eeef6aeb69311e5 Mon Sep 17 00:00:00 2001 From: rs <> Date: Sun, 21 Dec 2025 18:26:52 -0600 Subject: [PATCH] Update CGI script dispatch * Adjust environment variables * Use Popen.communicate to capture stdin/stderr on error return * Log CGI script output on errors --- cbs-srv.py | 71 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/cbs-srv.py b/cbs-srv.py index c21b28d..cb7b6f0 100755 --- a/cbs-srv.py +++ b/cbs-srv.py @@ -137,25 +137,40 @@ def serve_cgi(conn: SSL.Connection, addr, req_path, extra_path, url, conf: dict) # TODO: validate cert valid dates # TODO: does the handshake still check the CertificateVerify message if the set_verify callback returns true? - # RFC 3875 - env = environ.copy() - env['AUTH_TYPE'] = 'CERTIFICATE' if cert is not None else '' - env['CONTENT_LENGTH'] = '' # Requests don't contain content, leave blank - env['CONTENT_TYPE'] = '' # Requests don't contain content, leave blank + script_path = req_path.removeprefix(conf['servedir']) + + env = dict() + + # Inherit some useful variables + env['PATH'] = environ['PATH'] + if 'VIRTUAL_ENV' in environ: + env['VIRTUAL_ENV'] = environ['VIRTUAL_ENV'] + + # Defined by RFC 3875 (CGI/1.1) + env['AUTH_TYPE'] = '' # TODO: set to "certificate" when client cert present? + env['CONTENT_LENGTH'] = '0' # No content in gemini requests + env['CONTENT_TYPE'] = '' # No content in gemini requests env['GATEWAY_INTERFACE'] = 'CGI/1.1' - env['PATH_INFO'] = unquote(extra_path) # RFC 3875 specifies no URL encoding or parameters - env['PATH_TRANSLATED'] = extra_trans - env['QUERY_STRING'] = url.query - env['REMOTE_ADDR'] = addr[0] - env['REMOTE_HOST'] = '' # TODO: pull domain name from cert? - env['REMOTE_IDENT'] = '' # There is no ident info in gemini, leave blank - env['REMOTE_USER'] = '' # TODO: populate with TLS session ID? Maybe name from cert? - env['REQUEST_METHOD'] = 'GET' # This is the closest reasonable value - env['SCRIPT_NAME'] = req_path - env['SERVER_NAME'] = url.hostname - env['SERVER_PORT'] = str(conf['port']) - env['SERVER_PROTOCOL'] = 'GEMINI/0.16.1' - env['SERVER_SOFTWARE'] = 'CORNED_BEEF_SANDWICH/0.0.0' + env['PATH_INFO'] = unquote(extra_path) # Not URL encoded + env['PATH_TRANSLATED'] = extra_trans + env['QUERY_STRING'] = url.query # URL encoded + env['REMOTE_ADDR'] = addr[0] + env['REMOTE_HOST'] = '' # TODO + env['REMOTE_IDENT'] = '' # TODO + env['REMOTE_USER'] = '' # TODO + env['REQUEST_METHOD'] = 'GET' # No request method in gemini, this is close enough + env['SCRIPT_NAME'] = script_path # Not URL encoded + env['SERVER_NAME'] = url.hostname + env['SERVER_PORT'] = str(conf['port']) + env['SERVER_PROTOCOL'] = 'GEMINI/0.16.1' + env['SERVER_SOFTWARE'] = 'CORNED_BEEF_SANDWICH/0.0.0' + + # Other common variables (https://www.cgi101.com/book/ch3/text.html) + env['DOCUMENT_ROOT'] = conf['servedir'] + env['REMOTE_PORT'] = f'{addr[1]}' + env['REQUEST_URI'] = '' # TODO + env['SCRIPT_FILENAME'] = req_path + env['SERVER_ADMIN'] = '' # TODO env['TLS_CIPHER'] = conn.get_cipher_name() env['TLS_VERSION'] = conn.get_cipher_version() @@ -169,17 +184,22 @@ def serve_cgi(conn: SSL.Connection, addr, req_path, extra_path, url, conf: dict) env['TLS_CLIENT_PUBKEY'] = pubkey # TODO: does this or something similar already exist in other servers? env['TLS_CLIENT_SERIAL_NUMBER'] = str(cert.get_serial_number()) if cert is not None else '' # TODO: compare format to other servers - env['GEMINI_URL'] = '' try: - proc = subprocess.run(req_path, env=env, timeout=10, capture_output=True, check=True) - except subprocess.TimeoutExpired: - raise CBSException(42, 'CGI script timeout', req_path) - except subprocess.CalledProcessError as x: - raise CBSException(42, 'CGI script error', '{} -> {}'.format(req_path, x.returncode)) + proc = subprocess.Popen([req_path], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except PermissionError: raise CBSException(42, 'CGI not executable', req_path) - conn.sendall(proc.stdout) + + try: + stdout, stderr = proc.communicate(timeout=5) + except subprocess.TimeoutExpired: + raise CBSException(42, 'CGI script timeout', req_path) + + if proc.returncode != 0: + logging.error(stdout.decode('utf-8')) + logging.error(stderr.decode('utf-8')) + raise CBSException(42, 'CGI script error', '{} -> {}'.format(req_path, proc.returncode)) + conn.sendall(stdout) def serve_file(conn: SSL.Connection, filedir): @@ -251,3 +271,4 @@ def main(): if __name__ == '__main__': main() + -- 2.43.0