]> git.the-white-hart.net Git - gemini/cbs-server.git/commitdiff
Initial commit with existing code
authorrs <>
Sat, 19 Feb 2022 23:20:23 +0000 (17:20 -0600)
committerrs <>
Sat, 19 Feb 2022 23:20:23 +0000 (17:20 -0600)
cbs-srv.py [new file with mode: 0755]

diff --git a/cbs-srv.py b/cbs-srv.py
new file mode 100755 (executable)
index 0000000..259f668
--- /dev/null
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+
+import select
+import socket
+from OpenSSL import SSL
+from urllib.parse import urlparse
+
+from os import path, PathLike, environ
+import subprocess
+import mimetypes
+
+import logging
+import argparse
+
+logging.basicConfig(level=logging.INFO)
+mimetypes.add_type('text/gemini', '.gmi')
+mimetypes.add_type('text/gemini', '.gemini')
+
+# ------------------------------------------------------------------------------
+
+
+def recv_req(conn: SSL.Connection, timeout=.1):
+    data = b''
+    while True:
+        ready = select.select([conn], [], [], timeout)
+        if ready[0]:
+            data += conn.recv(4096)
+            if b'\r\n' in data:
+                lines = data.splitlines()
+                if len(lines) > 1:
+                    logging.warning('Discarding data after URL line of request: {}'.format(data))
+                try:
+                    req = lines[0].decode('ascii')
+                except Exception:
+                    logging.error('URL is not ascii: {}'.format(data))
+                    return None
+                return req
+        else:
+            logging.error('Timeout while waiting for URL')
+            return None
+
+
+def serve_req(conn: SSL.Connection, addr, url: str, servedir: PathLike, cgidir: PathLike):
+    url = urlparse(url)
+    servedir = path.abspath(servedir)
+    cgidir = path.join(servedir, cgidir)
+    reqdir = path.abspath(path.join(servedir, '.'+url.path))
+    if path.commonpath([servedir, reqdir]) != servedir:
+        return serve_notfound(conn)
+    if path.isdir(reqdir):
+        reqdir = path.join(reqdir, 'index.gmi')
+    if not path.isfile(reqdir):
+        return serve_notfound(conn)
+    if path.commonpath([cgidir, reqdir]) == cgidir:
+        return serve_cgi(conn, addr, reqdir, url)
+    return serve_file(conn, reqdir)
+
+
+def serve_notfound(conn: SSL.Connection):
+    conn.send(b'51 Page not found\r\n')
+
+
+def serve_cgi(conn: SSL.Connection, addr, scriptdir: PathLike, url):
+    cert = conn.get_peer_certificate()
+    env = environ.copy()
+
+    # RFC 3875
+    env['AUTH_TYPE'] = 'CERTIFICATE' if cert is not None else ''
+    env['CONTENT_LENGTH'] = ''
+    env['CONTENT_TYPE'] = ''
+    env['GATEWAY_INTERFACE'] = 'CGI/1.1'
+    env['PATH_INFO'] = ''          # TODO: maybe later
+    env['PATH_TRANSLATED'] = ''    # TODO: maybe later
+    env['QUERY_STRING'] = url.query
+    env['REMOTE_ADDR'] = addr
+    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, I worry about that idempotency tho
+    env['SCRIPT_NAME'] = str(scriptdir)
+    env['SERVER_NAME'] = url.hostname
+    env['SERVER_PORT'] = '1965'    # FIXME: pull this from options just in case it's overridden
+    env['SERVER_PROTOCOL'] = 'GEMINI/0.16.1'
+    env['SERVER_SOFTWARE'] = 'CORNED_BEEF_SANDWICH/0.0.0'
+
+    env['TLS_CIPHER'] = conn.get_cipher_name()
+    env['TLS_VERSION'] = conn.get_cipher_version()
+    env['TLS_CLIENT_HASH'] = 'SHA256:'+''  # SHA-256 hash of raw cert bytes
+    env['TLS_CLIENT_ISSUER'] = ''
+    env['TLS_CLIENT_ISSUER_DN'] = ''
+    env['TLS_CLIENT_SUBJECT'] = ''
+    env['TLS_CLIENT_SUBJECT_DN'] = ''
+    env['TLS_CLIENT_PUBKEY'] = ''
+    env['TLS_CLIENT_SERIAL_NUMBER'] = ''
+
+    env['GEMINI_URL'] = ''
+
+    print(cert.get_issuer())
+    print(cert.get_subject())
+    print(cert.get_pubkey())
+
+    # subprocess.Popen(scriptdir, env=env).wait(timeout=10)
+    if conn.get_peer_certificate() is None:
+        conn.send(b'60\r\n')
+    else:
+        conn.send(b'20 text/gemini\r\n')
+        conn.send(b'# Your mother runs CGI scripts\r\n')
+
+
+def serve_file(conn: SSL.Connection, filedir: PathLike):
+    (mime_type, encoding) = mimetypes.guess_type(filedir)
+    logging.info('mime_type:{}, encoding:{}'.format(mime_type, encoding))
+    with open(filedir, 'rb') as f:
+        conn.send('20 {}\r\n'.format(mime_type or 'application/octet-stream').encode('utf-8'))
+        conn.send(f.read())
+
+
+# ------------------------------------------------------------------------------
+
+
+def accept_client_cert(conn, cert, err_num, err_depth, ret_code):
+    return True
+
+
+def main():
+    parser = argparse.ArgumentParser('Corned Beef Sandwich Gemini Server')
+    parser.add_argument('--addr', '-a', default='127.0.0.1', help='IP address to bind to (default:"127.0.0.1")')
+    parser.add_argument('--port', '-p', type=int, default=1965, help='TCP port to listen on (default: 1965)')
+    parser.add_argument('--servedir', '-s', default='./serve', help='Directory to serve (devault: "./serve")')
+    parser.add_argument('--cgidir', '-g', default='cgi-bin', help='CGI script directory, relative to --servedir (default: "cgi-bin")')
+    parser.add_argument('--certfile', '-c', default='./crypt/server.crt', help='Cert file')
+    parser.add_argument('--keyfile', '-k', default='./crypt/server.key', help='Private key file')
+    args = parser.parse_args()
+
+    ctxt = SSL.Context(SSL.TLS_SERVER_METHOD)
+    ctxt.set_verify(SSL.VERIFY_PEER, accept_client_cert)
+    ctxt.use_certificate_file(args.certfile, SSL.FILETYPE_PEM)
+    ctxt.use_privatekey_file(args.keyfile, SSL.FILETYPE_PEM)
+
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.bind((args.addr, args.port))
+        sock.listen()
+        ssock = SSL.Connection(ctxt, sock)
+        ssock.set_accept_state()
+        while True:
+            conn, addr = ssock.accept()
+            conn.do_handshake()
+            logging.info('Connection from {}'.format(addr))
+            req = recv_req(conn)
+            if req is not None:
+                serve_req(conn, addr, req, args.servedir, args.cgidir)
+            conn.shutdown()
+            conn.sock_shutdown(socket.SHUT_RDWR)
+
+
+if __name__ == '__main__':
+    main()
+