feat: Implement virtual hosting for multi-domain Gemini server

- Add hostname-based request routing for multiple capsules per server
- Parse virtual host configs from TOML sections ([hostname])
- Implement per-host certificate and content isolation
- Add comprehensive virtual host testing and validation
- Update docs and examples for multi-host deployments

This enables Pollux to serve multiple Gemini domains from one instance,
providing the foundation for multi-tenant Gemini hosting.
This commit is contained in:
Jeena 2026-01-22 02:38:09 +00:00
parent c193d831ed
commit 0459cb6220
22 changed files with 2296 additions and 406 deletions

View file

@ -8,6 +8,7 @@ Used by integration tests for rate limiting validation.
Usage: python3 tests/gemini_test_client.py gemini://host:port/path
"""
import os
import sys
import socket
import ssl
@ -19,51 +20,65 @@ def main():
url = sys.argv[1]
# Parse URL (basic parsing)
if not url.startswith('gemini://'):
print("Error: URL must start with gemini://", file=sys.stderr)
sys.exit(1)
url_parts = url[9:].split('/', 1) # Remove gemini://
host_port = url_parts[0]
path = '/' + url_parts[1] if len(url_parts) > 1 else '/'
if ':' in host_port:
host, port = host_port.rsplit(':', 1)
port = int(port)
# Parse URL (basic parsing) - allow any protocol for testing
if url.startswith('gemini://'):
url_parts = url[9:].split('/', 1) # Remove gemini://
host = url_parts[0]
path = '/' + url_parts[1] if len(url_parts) > 1 else '/'
else:
host = host_port
port = 1965
# For non-gemini URLs, try to extract host anyway for testing
if '://' in url:
protocol, rest = url.split('://', 1)
url_parts = rest.split('/', 1)
host = url_parts[0]
path = '/' + url_parts[1] if len(url_parts) > 1 else '/'
else:
# No protocol, assume it's host/path
url_parts = url.split('/', 1)
host = url_parts[0]
path = '/' + url_parts[1] if len(url_parts) > 1 else '/'
# Get port from environment or use default
port = int(os.environ.get('GEMINI_PORT', '1965'))
# Allow overriding the connection host (useful for testing with localhost)
connect_host = os.environ.get('GEMINI_CONNECT_HOST', host)
try:
# Create SSL connection
context = ssl.create_default_context()
# Create SSL connection with permissive settings for self-signed certs
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
sock = socket.create_connection((host, port), timeout=5.0)
# Load default certificates to avoid some SSL issues
context.load_default_certs()
sock = socket.create_connection((connect_host, port), timeout=5.0)
ssl_sock = context.wrap_socket(sock, server_hostname=host)
# Send request
# Send request (full URL for Gemini protocol over TLS)
request = f"{url}\r\n"
ssl_sock.send(request.encode('utf-8'))
# Read response header
# Read full response (header + body)
response = b''
while b'\r\n' not in response and len(response) < 1024:
data = ssl_sock.recv(1)
if not data:
while len(response) < 1024: # Read up to 1KB for test responses
try:
data = ssl_sock.recv(1024)
if not data:
break
response += data
except:
break
response += data
ssl_sock.close()
if response:
status_line = response.decode('utf-8', errors='ignore').split('\r\n')[0]
print(status_line)
# Decode and return the full response
full_response = response.decode('utf-8', errors='ignore')
print(full_response.strip())
else:
print("Error: No response")
except Exception as e:
print(f"Error: {e}")