=> 528c1f08cf847bb1f708fca851e0a480d9fcf18c
[1mdiff --git a/examplerc b/examplerc[m [1mnew file mode 100644[m [1mindex 0000000..9180a2f[m [1m--- /dev/null[m [1m+++ b/examplerc[m [36m@@ -0,0 +1,7 @@[m [32m+[m[32m[cgi.upload][m [32m+[m[32mprotocol = titan[m [32m+[m[32mpath = /cat[m [32m+[m[32mcommand = cat[m [32m+[m [32m+[m[32m[cgi.test][m [32m+[m[32mcommand = printenv[m [1mdiff --git a/gemini.py b/gemini.py[m [1mindex 7297a3b..fec004e 100644[m [1m--- a/gemini.py[m [1m+++ b/gemini.py[m [36m@@ -3,7 +3,9 @@[m [m import fnmatch[m import hashlib[m [32m+[m[32mimport queue[m import socket[m [32m+[m[32mimport threading[m import time[m from urllib.parse import urlparse[m [m [36m@@ -12,6 +14,7 @@[m [mfrom OpenSSL import SSL, crypto[m [m [m def report_error(stream, code, msg):[m [32m+[m[32m print(time.strftime('%Y-%m-%d %H:%M:%S'), f' ', '--', code, msg)[m stream.sendall(f'{code} {msg}\r\n'.encode('utf-8'))[m [m [m [36m@@ -54,6 +57,8 @@[m [mdef verify_callback(connection, cert, err_num, err_depth, ret_code):[m [m [m class Cache:[m [32m+[m[32m """Response cache."""[m [32m+[m def __init__(self):[m pass[m [m [36m@@ -65,112 +70,35 @@[m [mclass Cache:[m return[m [m [m [31m-class Server:[m [31m- def __init__(self, hostname_or_hostnames, cert_path, key_path,[m [31m- address='localhost', port=1965,[m [31m- cache=None, session_id=None, max_upload_size=0):[m [31m- self.hostnames = [hostname_or_hostnames] \[m [31m- if type(hostname_or_hostnames) == str else hostname_or_hostnames[m [31m- self.address = address[m [31m- self.port = port[m [31m- self.entrypoints = {'gemini': {}, 'titan': {}}[m [31m- for proto in ['gemini', 'titan']:[m [31m- self.entrypoints[proto] = {}[m [31m- for hostname in self.hostnames:[m [31m- self.entrypoints[proto][hostname] = [][m [31m- self.cache = cache[m [31m- self.max_upload_size = max_upload_size[m [31m-[m [31m- self.context = SSL.Context(SSL.TLS_SERVER_METHOD)[m [31m- self.context.use_certificate_file(str(cert_path))[m [31m- self.context.use_privatekey_file(str(key_path))[m [31m- self.context.set_verify(SSL.VERIFY_PEER, verify_callback)[m [31m- if session_id:[m [31m- if type(session_id) != bytes:[m [31m- raise Exception("session_id type must be `bytes`")[m [31m- self.context.set_session_id(session_id)[m [31m-[m [31m- attempts = 60[m [31m- print(f'Opening port {port}...')[m [31m- while True:[m [31m- try:[m [31m- self.sock = socket.socket()[m [31m- self.sock.bind((address, port))[m [31m- self.sock.listen(5)[m [31m- self.sv_conn = SSL.Connection(self.context, self.sock)[m [31m- self.sv_conn.set_accept_state()[m [31m- break[m [31m- except:[m [31m- attempts -= 1[m [31m- if attempts == 0:[m [31m- raise Exception(f'Failed to open port {port} for listening')[m [31m- time.sleep(2.0)[m [31m- print('...')[m [31m- print(f'Server started on port {port}')[m [31m-[m [31m- def add_entrypoint(self, protocol, hostname, path_pattern, entrypoint):[m [31m- self.entrypoints[protocol][hostname].append((path_pattern, entrypoint))[m [31m-[m [31m- def __setitem__(self, key, value):[m [31m- #if key.endswith('*'):[m [31m- # self.wild_entrypoints[key[:-1]] = value[m [31m- #else:[m [31m- # self.entrypoints[key] = value[m [31m- for hostname in self.hostnames:[m [31m- self.add_entrypoint('gemini', hostname, key, value)[m [32m+[m[32mclass Worker(threading.Thread):[m [32m+[m[32m """Thread that processes incoming requests from clients."""[m [m [31m- # def __getitem__(self, key):[m [31m- # if key.endswith('*'):[m [31m- # return self.wild_entrypoints[key[:-1]][m [31m- # return self.entrypoints[key][m [32m+[m[32m def __init__(self, id, server):[m [32m+[m[32m super().__init__()[m [32m+[m[32m self.id = id[m [32m+[m[32m self.server = server[m [32m+[m[32m self.jobs = server.work_queue[m [m def run(self):[m [32m+[m[32m print(f'Worker {self.id} started')[m while True:[m [32m+[m[32m job = self.jobs.get()[m [32m+[m[32m if job is None:[m [32m+[m[32m break[m [32m+[m[32m stream, from_addr = job[m try:[m [31m- stream = None[m [31m- try:[m [31m- stream, from_addr = self.sv_conn.accept()[m [31m- #print(stream, from_addr)[m [31m- self.process_request(stream, from_addr)[m [31m- except Exception as ex:[m [31m- import traceback[m [31m- traceback.print_exc()[m [31m- print(ex)[m [31m- if stream:[m [31m- report_error(stream, 42, str(ex))[m [31m- finally:[m [31m- if stream:[m [31m- stream.shutdown()[m [31m- #print('Goodbye', from_addr)[m [31m- except Exception as ex:[m [31m- print(ex)[m [32m+[m[32m self.process_request(stream, from_addr)[m [32m+[m[32m except Exception as error:[m [32m+[m[32m report_error(stream, 42, str(error))[m [32m+[m[32m finally:[m [32m+[m[32m stream.shutdown()[m [m [31m- def find_entrypoint(self, protocol, hostname, path):[m [31m- try:[m [31m- for entry in self.entrypoints[protocol][hostname]:[m [31m- if len(entry[0]) == 0 or fnmatch.fnmatch(path, entry[0]):[m [31m- return entry[1][m [31m- except:[m [31m- return None[m [32m+[m[32m print(f'Worker {self.id} stopped')[m [m [31m- # # Check the more specific virtual host entrypoint first.[m [31m- # virt_path = f":{hostname}:{path}"[m [31m- # if virt_path in self.entrypoints:[m [31m- # return self.entrypoints[virt_path][m [31m- # for entry in self.wild_entrypoints:[m [31m- # if virt_path.startswith(entry):[m [31m- # return self.wild_entrypoints[entry][m [31m-[m [31m- # if path in self.entrypoints:[m [31m- # return self.entrypoints[path][m [31m- # for entry in self.wild_entrypoints:[m [31m- # if path.startswith(entry):[m [31m- # return self.wild_entrypoints[entry][m [31m-[m [31m- return None[m [32m+[m[32m def log(self, *args):[m [32m+[m[32m print(time.strftime('%Y-%m-%d %H:%M:%S'), f'[{self.id}]', '--', *args)[m [m def process_request(self, stream, from_addr):[m [31m- print(time.strftime('%Y-%m-%d %H:%M:%S'))[m data = bytes()[m MAX_LEN = 1024[m request = None[m [36m@@ -179,8 +107,6 @@[m [mclass Server:[m req_mime = None[m incoming = stream.recv(MAX_LEN)[m [m [31m- print(dir(stream))[m [31m-[m while len(data) < MAX_LEN:[m data += incoming[m crlf_pos = data.find(b'\r\n')[m [36m@@ -199,7 +125,14 @@[m [mclass Server:[m report_error(stream, 59, "Unsupported protocol")[m return[m [m [32m+[m[32m cl_cert = stream.get_peer_certificate()[m [32m+[m[32m identity = Identity(cl_cert) if cl_cert else None[m [32m+[m if request.startswith('titan:'):[m [32m+[m[32m if identity is None and self.server.require_upload_identity:[m [32m+[m[32m report_error(stream, 60, "Client certificate required for upload")[m [32m+[m[32m return[m [32m+[m # Read the rest of the data.[m parms = request.split(';')[m request = parms[0][m [36m@@ -210,7 +143,8 @@[m [mclass Server:[m req_token = p[6:][m elif p.startswith('mime='):[m req_mime = p[5:][m [31m- if expected_size > self.max_upload_size and self.max_upload_size > 0:[m [32m+[m[32m self.log(f'Receiving Titan content: {expected_size}')[m [32m+[m[32m if expected_size > self.server.max_upload_size and self.server.max_upload_size > 0:[m report_error(stream, 59, "Maximum content length exceeded")[m return[m while len(data) < expected_size:[m [36m@@ -227,17 +161,25 @@[m [mclass Server:[m report_error(stream, 59, "Gemini disallows request content")[m return[m [m [32m+[m[32m self.log(request)[m [32m+[m url = urlparse(request)[m [31m- cl_cert = stream.get_peer_certificate()[m [31m- identity = Identity(cl_cert) if cl_cert else None[m path = url.path[m if path == '':[m path = '/'[m [31m- # TODO: get TLS SNI[m hostname = url.hostname[m [31m- entrypoint = self.find_entrypoint(url.scheme, hostname, path)[m [31m- print(entrypoint)[m [31m- cache = None if identity or len(url.query) > 0 else self.cache[m [32m+[m[32m entrypoint = self.server.find_entrypoint(url.scheme, hostname, path)[m [32m+[m [32m+[m[32m # Server name indication is required.[m [32m+[m[32m if not stream.get_servername():[m [32m+[m[32m report_error(stream, 59, "Missing TLS server name indication")[m [32m+[m[32m return[m [32m+[m[32m if stream.get_servername().decode() != url.hostname:[m [32m+[m[32m report_error(stream, 53, "Proxy request refused")[m [32m+[m[32m return[m [32m+[m [32m+[m[32m cache = None if (url.scheme != 'gemini' or identity or len(url.query) > 0) \[m [32m+[m[32m else self.server.cache[m is_from_cache = False[m [m # print(f'Request : {request}')[m [36m@@ -246,7 +188,7 @@[m [mclass Server:[m if entrypoint:[m # Check the cache first.[m if cache:[m [31m- media, content = cache.try_load(path)[m [32m+[m[32m media, content = cache.try_load(hostname + path)[m if not media is None:[m response = 20, media, content[m is_from_cache = True[m [36m@@ -291,6 +233,125 @@[m [mclass Server:[m [m # Save to cache.[m if not is_from_cache and cache and status == 20:[m [31m- cache.save(path, meta, response_data)[m [32m+[m[32m cache.save(hostname + path, meta, response_data)[m else:[m report_error(stream, 50, 'Permanent failure')[m [32m+[m [32m+[m [32m+[m[32mclass Server:[m [32m+[m[32m def __init__(self, hostname_or_hostnames, cert_path, key_path,[m [32m+[m[32m address='localhost', port=1965,[m [32m+[m[32m cache=None, session_id=None, max_upload_size=0, num_threads=1,[m [32m+[m[32m require_upload_identity=True):[m [32m+[m[32m self.hostnames = [hostname_or_hostnames] \[m [32m+[m[32m if type(hostname_or_hostnames) == str else hostname_or_hostnames[m [32m+[m[32m self.address = address[m [32m+[m[32m self.port = port[m [32m+[m[32m self.entrypoints = {'gemini': {}, 'titan': {}}[m [32m+[m[32m for proto in ['gemini', 'titan']:[m [32m+[m[32m self.entrypoints[proto] = {}[m [32m+[m[32m for hostname in self.hostnames:[m [32m+[m[32m self.entrypoints[proto][hostname] = [][m [32m+[m[32m self.cache = cache[m [32m+[m[32m self.max_upload_size = max_upload_size[m [32m+[m[32m self.require_upload_identity = require_upload_identity[m [32m+[m [32m+[m[32m self.context = SSL.Context(SSL.TLS_SERVER_METHOD)[m [32m+[m[32m self.context.use_certificate_file(str(cert_path))[m [32m+[m[32m self.context.use_privatekey_file(str(key_path))[m [32m+[m[32m self.context.set_verify(SSL.VERIFY_PEER, verify_callback)[m [32m+[m[32m if session_id:[m [32m+[m[32m if type(session_id) != bytes:[m [32m+[m[32m raise Exception("session_id type must be `bytes`")[m [32m+[m[32m self.context.set_session_id(session_id)[m [32m+[m [32m+[m[32m # Spawn the worker threads.[m [32m+[m[32m self.workers = [][m [32m+[m[32m self.work_queue = queue.Queue()[m [32m+[m[32m for worker_id in range(max(num_threads, 1)):[m [32m+[m[32m worker = Worker(worker_id, self)[m [32m+[m[32m self.workers.append(worker)[m [32m+[m [32m+[m[32m attempts = 60[m [32m+[m[32m print(f'Opening port {port}...')[m [32m+[m[32m while True:[m [32m+[m[32m try:[m [32m+[m[32m self.sock = socket.socket()[m [32m+[m[32m self.sock.bind((address, port))[m [32m+[m[32m self.sock.listen(5)[m [32m+[m[32m self.sv_conn = SSL.Connection(self.context, self.sock)[m [32m+[m[32m self.sv_conn.set_accept_state()[m [32m+[m[32m break[m [32m+[m[32m except:[m [32m+[m[32m attempts -= 1[m [32m+[m[32m if attempts == 0:[m [32m+[m[32m raise Exception(f'Failed to open port {port} for listening')[m [32m+[m[32m time.sleep(2.0)[m [32m+[m[32m print('...')[m [32m+[m[32m print(f'Server started on port {port}')[m [32m+[m [32m+[m[32m def add_entrypoint(self, protocol, hostname, path_pattern, entrypoint):[m [32m+[m[32m self.entrypoints[protocol][hostname].append((path_pattern, entrypoint))[m [32m+[m [32m+[m[32m def __setitem__(self, key, value):[m [32m+[m[32m #if key.endswith('*'):[m [32m+[m[32m # self.wild_entrypoints[key[:-1]] = value[m [32m+[m[32m #else:[m [32m+[m[32m # self.entrypoints[key] = value[m [32m+[m[32m for hostname in self.hostnames:[m [32m+[m[32m self.add_entrypoint('gemini', hostname, key, value)[m [32m+[m [32m+[m[32m # def __getitem__(self, key):[m [32m+[m[32m # if key.endswith('*'):[m [32m+[m[32m # return self.wild_entrypoints[key[:-1]][m [32m+[m[32m # return self.entrypoints[key][m [32m+[m [32m+[m[32m def run(self):[m [32m+[m[32m for worker in self.workers:[m [32m+[m[32m worker.start()[m [32m+[m[32m while True:[m [32m+[m[32m try:[m [32m+[m[32m stream = None[m [32m+[m[32m try:[m [32m+[m[32m stream, from_addr = self.sv_conn.accept()[m [32m+[m[32m #print(stream, from_addr)[m [32m+[m[32m #self.process_request(stream, from_addr)[m [32m+[m[32m self.work_queue.put((stream, from_addr))[m [32m+[m[32m except KeyboardInterrupt:[m [32m+[m[32m print('\nStopping the server...')[m [32m+[m[32m break[m [32m+[m[32m except Exception as ex:[m [32m+[m[32m import traceback[m [32m+[m[32m traceback.print_exc()[m [32m+[m[32m print(ex)[m [32m+[m[32m except Exception as ex:[m [32m+[m[32m print(ex)[m [32m+[m[32m for i in range(len(self.workers)):[m [32m+[m[32m self.work_queue.put(None)[m [32m+[m[32m for worker in self.workers:[m [32m+[m[32m worker.join()[m [32m+[m [32m+[m[32m def find_entrypoint(self, protocol, hostname, path):[m [32m+[m[32m try:[m [32m+[m[32m for entry in self.entrypoints[protocol][hostname]:[m [32m+[m[32m if len(entry[0]) == 0 or fnmatch.fnmatch(path, entry[0]):[m [32m+[m[32m return entry[1][m [32m+[m[32m except:[m [32m+[m[32m return None[m [32m+[m [32m+[m[32m # # Check the more specific virtual host entrypoint first.[m [32m+[m[32m # virt_path = f":{hostname}:{path}"[m [32m+[m[32m # if virt_path in self.entrypoints:[m [32m+[m[32m # return self.entrypoints[virt_path][m [32m+[m[32m # for entry in self.wild_entrypoints:[m [32m+[m[32m # if virt_path.startswith(entry):[m [32m+[m[32m # return self.wild_entrypoints[entry][m [32m+[m [32m+[m[32m # if path in self.entrypoints:[m [32m+[m[32m # return self.entrypoints[path][m [32m+[m[32m # for entry in self.wild_entrypoints:[m [32m+[m[32m # if path.startswith(entry):[m [32m+[m[32m # return self.wild_entrypoints[entry][m [32m+[m [32m+[m[32m return None[m [41m+[m [1mdiff --git a/gmcapsule.py b/gmcapsule.py[m [1mindex 4886141..bc0c2a4 100644[m [1m--- a/gmcapsule.py[m [1m+++ b/gmcapsule.py[m [36m@@ -13,28 +13,43 @@[m [mimport gemini[m [m [m class Config:[m [31m- def __init__(self):[m [31m- # TODO: Get this using configparser[m [31m- self.hostnames = ['localhost'][m [31m- self.address = '0.0.0.0'[m [31m- self.port = 1965[m [31m- self.certs_dir = Path('.certs')[m [31m- self.root_dir = Path('.') # vhosts as subdirs[m [31m- self.mod_dir = Path('modules') # extension modules[m [31m- self.max_upload_size = 10 * 1024 * 1024[m [31m- self.cgi = {[m [31m- 'gemini': {[m [31m- 'localhost': [[m [31m- ('/test', ['/bin/ls', '-l'])[m [31m- ][m [31m- },[m [31m- 'titan': {[m [31m- 'localhost': [[m [31m- ('/test', ['printenv']),[m [31m- ('/test/*', ['printenv'])[m [31m- ][m [31m- }[m [31m- }[m [32m+[m[32m def __init__(self, config_path):[m [32m+[m[32m self.ini = configparser.ConfigParser()[m [32m+[m[32m if os.path.exists(config_path):[m [32m+[m[32m self.ini.read(config_path)[m [32m+[m[32m else:[m [32m+[m[32m print(config_path, 'not found -- using defaults')[m [32m+[m [32m+[m[32m def hostnames(self):[m [32m+[m[32m return self.ini.get('server', 'host', fallback='localhost').split()[m [32m+[m [32m+[m[32m def address(self):[m [32m+[m[32m return self.ini.get('server', 'address', fallback='0.0.0.0')[m [32m+[m [32m+[m[32m def port(self):[m [32m+[m[32m return self.ini.getint('server', 'port', fallback=1965)[m [32m+[m [32m+[m[32m def certs_dir(self):[m [32m+[m[32m return Path(self.ini.get('server', 'certs', fallback='.certs'))[m [32m+[m [32m+[m[32m def root_dir(self):[m [32m+[m[32m return Path(self.ini.get('server', 'root', fallback='.'))[m [32m+[m [32m+[m[32m def mod_dir(self):[m [32m+[m[32m return Path(self.ini.get('server', 'modules', fallback='modules'))[m [32m+[m [32m+[m[32m def num_threads(self):[m [32m+[m[32m return self.ini.getint('server', 'threads', fallback=5)[m [32m+[m [32m+[m[32m def max_upload_size(self):[m [32m+[m[32m return self.ini.getint('titan', 'upload_size', fallback=10 * 1024 * 1024)[m [32m+[m [32m+[m[32m def prefixed_sections(self, prefix):[m [32m+[m[32m sects = {}[m [32m+[m[32m for name in self.ini.sections():[m [32m+[m[32m if not name.startswith(prefix): continue[m [32m+[m[32m sects[name[len(prefix):]] = self.ini[name][m [32m+[m[32m return sects[m [m [m class Capsule:[m [36m@@ -44,13 +59,14 @@[m [mclass Capsule:[m Capsule._capsule = self[m self.cfg = cfg[m self.sv = gemini.Server([m [31m- cfg.hostnames,[m [31m- cfg.certs_dir / 'cert.pem',[m [31m- cfg.certs_dir / 'key.pem',[m [31m- address=cfg.address,[m [31m- port=cfg.port,[m [31m- session_id=f'GmCapsule:{cfg.port}'.encode('utf-8'),[m [31m- max_upload_size=cfg.max_upload_size[m [32m+[m[32m cfg.hostnames(),[m [32m+[m[32m cfg.certs_dir() / 'cert.pem',[m [32m+[m[32m cfg.certs_dir() / 'key.pem',[m [32m+[m[32m address=cfg.address(),[m [32m+[m[32m port=cfg.port(),[m [32m+[m[32m session_id=f'GmCapsule:{cfg.port()}'.encode('utf-8'),[m [32m+[m[32m max_upload_size=cfg.max_upload_size(),[m [32m+[m[32m num_threads=cfg.num_threads()[m )[m # Modules define the entrypoints.[m self.load_modules()[m [36m@@ -63,21 +79,21 @@[m [mclass Capsule:[m if hostname:[m self.sv.add_entrypoint(protocol, hostname, path, entrypoint)[m else:[m [31m- for hostname in self.cfg.hostnames:[m [32m+[m[32m for hostname in self.cfg.hostnames():[m if not hostname:[m raise Exception(f'invalid hostname: "{hostname}"')[m self.sv.add_entrypoint(protocol, hostname, path, entrypoint)[m [m def load_modules(self):[m [31m- for mod_file in sorted(os.listdir(self.cfg.mod_dir)):[m [32m+[m[32m for mod_file in sorted(os.listdir(self.cfg.mod_dir())):[m if mod_file.endswith('.py'):[m [31m- path = (self.cfg.mod_dir / mod_file).resolve()[m [32m+[m[32m path = (self.cfg.mod_dir() / mod_file).resolve()[m name = mod_file[:-3][m [31m- print('Module:', name)[m loader = importlib.machinery.SourceFileLoader(name, str(path))[m spec = importlib.util.spec_from_loader(name, loader)[m mod = importlib.util.module_from_spec(spec)[m loader.exec_module(mod)[m [32m+[m[32m print('Module:', mod.__doc__)[m mod.init(self)[m [m def run(self):[m [1mdiff --git a/gmcapsuled b/gmcapsuled[m [1mindex cab942e..212401d 100755[m [1m--- a/gmcapsuled[m [1m+++ b/gmcapsuled[m [36m@@ -5,17 +5,21 @@[m # License: BSD 2-Clause[m [m import argparse[m [32m+[m[32mimport os[m import sys[m import threading[m [m from gmcapsule import *[m [32m+[m[32mfrom pathlib import Path[m [m VERSION = '0.1'[m [m print(f"GmCapsule {VERSION}")[m [m [31m-# TODO: Parse command arguments.[m [32m+[m[32margp = argparse.ArgumentParser(description='GmCapsule is an extensible server for Gemini and Titan.')[m [32m+[m[32margp.add_argument('-C', '--config', dest='config_file', default=Path.home() / '.gmcapsulerc')[m [32m+[m[32margs = argp.parse_args()[m [m [31m-cfg = Config()[m [32m+[m[32mcfg = Config(args.config_file)[m capsule = Capsule(cfg)[m capsule.run()[m [1mdiff --git a/modules/400_cgi.py b/modules/40_cgi.py[m [1msimilarity index 54%[m [1mrename from modules/400_cgi.py[m [1mrename to modules/40_cgi.py[m [1mindex c635a85..70bd04a 100644[m [1m--- a/modules/400_cgi.py[m [1m+++ b/modules/40_cgi.py[m [36m@@ -1,5 +1,7 @@[m [32m+[m[32m"""CGI commands"""[m [m import os[m [32m+[m[32mimport shlex[m import subprocess[m import urllib.parse[m [m [36m@@ -15,7 +17,6 @@[m [mclass CgiContext:[m [m def __call__(self, req):[m try:[m [31m- cfg = Capsule.config()[m query = urllib.parse.unquote(req.query)[m env_vars = dict(os.environ)[m [m [36m@@ -24,21 +25,29 @@[m [mclass CgiContext:[m env_vars['QUERY_STRING'] = req.query[m assert req.path.startswith(self.base_path)[m env_vars['PATH_INFO'] = req.path[len(self.base_path):][m [32m+[m[32m env_vars['SERVER_PROTOCOL'] = req.scheme[m [32m+[m[32m env_vars['SERVER_NAME'] = req.hostname[m [32m+[m[32m env_vars['SERVER_PORT'] = str(Capsule.config().port())[m [m [32m+[m[32m # TLS client certificate.[m if req.identity:[m [31m- env_vars['REMOTE_IDENT'] = str(req.identity)[m [31m- env_vars['REMOTE_USER'] = req.identity.subject()[m [32m+[m[32m env_vars['AUTH_TYPE'] = 'TLS'[m [32m+[m[32m env_vars['REMOTE_IDENT'] = str(req.identity) # cert fingerprints[m [32m+[m[32m env_vars['REMOTE_USER'] = req.identity.subject() # "/CN=name"[m [32m+[m[32m else:[m [32m+[m[32m env_vars['AUTH_TYPE'] = ''[m [m [32m+[m[32m # Titan metadata.[m if req.content:[m [31m- env_vars['TITAN_TOKEN'] = req.content_token if req.content_token is not None else ''[m [31m- env_vars['TITAN_MIME'] = req.content_mime if req.content_mime is not None else ''[m [32m+[m[32m env_vars['CONTENT_LENGTH'] = str(len(req.content))[m [32m+[m[32m env_vars['CONTENT_TYPE'] = req.content_mime if req.content_mime is not None else ''[m [32m+[m[32m env_vars['CONTENT_TOKEN'] = req.content_token if req.content_token is not None else ''[m [m [31m- print(req.content)[m [31m-[m [31m- result = subprocess.run(self.args, check=True,[m [31m- input=req.content,[m [31m- stdout=subprocess.PIPE,[m [31m- env=env_vars).stdout[m [32m+[m[32m result = subprocess.run(self.args,[m [32m+[m[32m check=True,[m [32m+[m[32m input=req.content,[m [32m+[m[32m stdout=subprocess.PIPE,[m [32m+[m[32m env=env_vars).stdout[m try:[m # Parse response header.[m crlf_pos = result.find(b'\r\n')[m [36m@@ -62,7 +71,10 @@[m [mclass CgiContext:[m [m def init(capsule):[m cfg = Capsule.config()[m [31m- for protocol in cfg.cgi:[m [31m- for hostname in cfg.cgi[protocol]:[m [31m- for entry in cfg.cgi[protocol][hostname]:[m [31m- capsule.add(entry[0], CgiContext(entry[0], entry[1]), hostname, protocol)[m [32m+[m[32m default_host = cfg.hostnames()[0][m [32m+[m[32m for section in Capsule.config().prefixed_sections('cgi.').values():[m [32m+[m[32m protocol = section.get('protocol', fallback='gemini')[m [32m+[m[32m host = section.get('host', fallback=default_host)[m [32m+[m[32m path = section.get('path', fallback='/*')[m [32m+[m[32m args = shlex.split(section.get('command'))[m [32m+[m[32m capsule.add(path, CgiContext(path, args), host, protocol)[m [1mdiff --git a/modules/500_static.py b/modules/50_static.py[m [1msimilarity index 91%[m [1mrename from modules/500_static.py[m [1mrename to modules/50_static.py[m [1mindex a525993..7a9f95f 100644[m [1m--- a/modules/500_static.py[m [1m+++ b/modules/50_static.py[m [36m@@ -1,3 +1,5 @@[m [32m+[m[32m"""Static files from the host content directory"""[m [32m+[m import fnmatch[m import os.path[m import string[m [36m@@ -10,7 +12,7 @@[m [mMETA = '.meta'[m [m def check_meta_rules(path, hostname):[m cfg = Capsule.config()[m [31m- root = (cfg.root_dir / hostname).resolve()[m [32m+[m[32m root = (cfg.root_dir() / hostname).resolve()[m dir = path.parent[m while True:[m if not str(dir.resolve()).startswith(str(root)):[m [36m@@ -44,7 +46,7 @@[m [mdef serve_file(req):[m if seg != '.' and seg != '..' and seg.startswith('.'):[m return 51, "Not found"[m [m [31m- host_root = (cfg.root_dir / req.hostname).resolve()[m [32m+[m[32m host_root = (cfg.root_dir() / req.hostname).resolve()[m path = (host_root / req.path[1:]).resolve()[m if not str(path).startswith(str(host_root)):[m return 51, "Not found"[m
text/gemini; charset=utf-8
This content has been proxied by September (ba2dc).