# 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()
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):
if __name__ == '__main__':
main()
+