]> git.the-white-hart.net Git - gemini/cbs-server.git/commitdiff
Update CGI script dispatch
authorrs <>
Mon, 22 Dec 2025 00:26:52 +0000 (18:26 -0600)
committerrs <>
Mon, 22 Dec 2025 00:26:52 +0000 (18:26 -0600)
* Adjust environment variables
* Use Popen.communicate to capture stdin/stderr on error return
* Log CGI script output on errors

cbs-srv.py

index c21b28dc39e0c9252b132d71f8fea0665c68a416..cb7b6f045033e11cde76a34331f3726b2d51af19 100755 (executable)
@@ -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()
+