aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoralex <alex@pdp7.net>2026-01-17 18:56:02 +0100
committeralex <alex@pdp7.net>2026-01-17 20:26:52 +0100
commit26a87fd4a71f695e47ff8c5ee143ab1f037fdc9d (patch)
tree75b8aa06f023326b8afc8720440bd3d0c101ab06
Initial add
-rw-r--r--README.md13
-rwxr-xr-xpackage-mod-md-certs3
-rwxr-xr-xproxy.py58
3 files changed, 74 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ed1393c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+# Gemini from http
+
+`proxy.py` is a Gemini server that proxies all content to an http or https server.
+
+## Notes
+
+```
+su -c ./package-mod-md-certs | tar x
+```
+
+```
+./proxy.py domains/
+```
diff --git a/package-mod-md-certs b/package-mod-md-certs
new file mode 100755
index 0000000..304f64d
--- /dev/null
+++ b/package-mod-md-certs
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec tar c -C /etc/apache2/md/ --exclude '*.json' domains/
diff --git a/proxy.py b/proxy.py
new file mode 100755
index 0000000..854446b
--- /dev/null
+++ b/proxy.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+import argparse
+import logging
+import pathlib
+import ssl
+import socketserver
+import urllib.request
+
+
+context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+domains_path = None
+
+class Handler(socketserver.BaseRequestHandler):
+ def handle(self):
+ with context.wrap_socket(self.request, server_side=True) as sock:
+ recv = sock.recv(1024)
+ recv = recv.decode("ASCII")
+ assert recv.endswith("\r\n"), f"Received request {repr(recv)} that does not end in \\r\\n"
+ absolute_uri = recv.removesuffix("\r\n")
+ assert absolute_uri.startswith("gemini://"), f"Request for uri {absolute_uri} does not start with gemini://"
+ logging.info(absolute_uri)
+
+ request = urllib.request.Request("https://" + absolute_uri.removeprefix("gemini://"))
+ request.add_header("Accept", "text/gemini")
+ with urllib.request.urlopen(request) as f:
+ content = f.read().decode("UTF8")
+ response = "20 text/gemini\r\n"
+ response += content
+
+ sock.sendall(response.encode("UTF8"))
+
+
+def sni_callback(socket: ssl.SSLSocket, server_name, _context):
+ context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ domain_path = domains_path / server_name
+ context.load_cert_chain(domain_path / "pubcert.pem", domain_path / "privkey.pem")
+ socket.context = context
+
+
+def main():
+ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(message)s")
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--host", default="0.0.0.0")
+ parser.add_argument("--port", type=int, default=1965)
+ parser.add_argument("domains_path", type=pathlib.Path)
+ args = parser.parse_args()
+
+ global domains_path
+ domains_path = args.domains_path
+
+ context.sni_callback = sni_callback
+
+ with socketserver.TCPServer((args.host, args.port), Handler) as server:
+ server.serve_forever()
+
+
+if __name__ == "__main__":
+ main()