开发者用 Python 构建 Gemini 协议服务器
来源标题:Building a Gemini Server in Python
一位开发者在使用 Gemini 协议后,用 Python 为其搭建了服务器。Gemini 是七年前发明的、类似 Web 的极简替代协议,不支持 Cookie、脚本和样式,站点称为 capsule,文档格式为 gemtext。作者认为这种极简设计催生了一个专注内容分享、没有机器人、追踪器和商业目标的社区。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
发明七年之后,我发现了 Gemini 协议。它像是回归基本原理的 Web 替代方案,旨在为独立内容创作者提供价值,让他们远离主流的噪音、机器人和追踪器。想象一下这样的 Web:HTTP 没有 cookie,甚至除了 GET 之外没有任何 header 或方法;HTML 不能添加脚本、样式,也不能获取其他资源(没有 <link>、<script>,甚至没有 <img>、<audio> 标签);除了标题和列表之外没有任何文本装饰;TLS 不需要受信任的第三方证书颁发机构(CA)来认证每一方。这种极简主义听起来可能非常基础,对许多应用来说甚至毫无用处,但它带来的结果是一个专注于创作和分享内容、背后没有商业目的的写作者和开发者社区——没有网红,没有机器人,没有电商,没有订阅。
在 Gemini 中,网站被称为 capsule,网络被称为 geminispace。标准文档格式是一种类似精简版 markdown 的格式,叫做 gemtext(相当于 Gemini 的 HTML)。要浏览 geminispace,你需要一个 Gemini 客户端(相当于 Web 浏览器)。在尝试了 deedum 等几个选项之后,我发现了 Lagrange 客户端,这是由就职于 Nokia 的 Jaakko Keränen @skyjake 编写的一款出色软件。该客户端支持所有平台(移动端和桌面端),使用体验非常愉悦。Gemini 上没有大公司。Lagrange 附带了一个指向“快速入门”页面的书签,该页面列出了一些搜索引擎,比如 keneddy——据我了解,它完全运行在某人的一台 Mac mini 上,包括爬虫。
在 geminispace 里探索了一番之后,我想尝试实现自己的服务器。Python 可能不是实现服务器的理想语言,但我越来越多地使用 Python 进行开发,所以用它来做这个项目似乎很有趣。本文接下来的内容将按照我实现 Python Gemini 服务器时走过的类似步骤展开,尽量少用外部依赖。
首先要做的是创建一个 TCP socket,监听 Gemini 端口(1965,即 Gemini 5 火箭发射的年份)。为此我们这样做:
import socket as Socket port = 1965 socket = Socket.socket(Socket.AF_INET, Socket.SOCK_STREAM, 0) socket.setsockopt(Socket.SOL_SOCKET, Socket.SO_REUSEADDR, 1) socket.bind(('0.0.0.0', port)) socket.listen(5) while True: connection, client_address = socket.accept() connection.sendall(b'hello')
这些都是相当底层的 API,封装了操作系统的系统调用,让我们解释其中一些。我们将 socket 绑定到 0.0.0.0,这是一个特殊地址,表示我们希望监听所有网络接口。你也可以使用 127.0.0.1,但例如在 docker 容器内运行时,这会导致问题。如果你运行上面给出的代码,可以使用 TCP 客户端测试你的服务器。我推荐使用 telnet 或 nc,因为它们很可能已经安装好了。运行 Python 脚本,然后在另一个终端窗口中运行其中之一:
nc 0.0.0.0 1965
telnet 0.0.0.0 1965
如果一切正常,你应该会看到打印出 hello。
目前,我们的 socket 服务器只返回 hello。为了返回一个有效的 Gemini 响应,我们需要返回类似这样的内容:
20 text/gemini
Hello
其中 20 是“成功”状态码(类似于 HTTP 中的 200),text/gemini 是响应数据的 MIME 类型(更多关于 gemtext 的内容稍后再说),下面一行是实际的响应数据。
一个有效的 Gemini 请求始终只包含一个绝对 URL,且不能超过 1024 字节的数据。我们可以从 connection 对象读取 1024 字节。让我们在路径为 /hello 时返回 Hello,在路径为 /goodbye 时返回 Goodbye。对于其他情况,我们可以发送 51 “Not found”。
from urllib.parse import SplitResult, urlsplit data = connection.recv(1024) url = urlsplit(data.split("\r\n")[0]) if url.path == b"/hello": connection.send(b"20 text/gemini\r\n# Hello") elif url.path == b"/goodbye": connection.send(b"20 text/gemini\r\n# Goodbye") else: connection.send(b"51 Not Found\r\n")
此时,如果你用 TCP 客户端重新运行测试,应该能够发送一个 URL 并根据路径观察到不同的响应。
到目前为止,我们通过 socket 发送的是未加密的数据(类似于明文 HTTP 请求)。Gemini 强制要求使用 TLS,因此为了让 Gemini 客户端理解我们的请求,我们需要用 TLS 证书加密数据。
与 Web 上几乎强制要求服务器证书由特定的一组证书颁发机构(CA)签发不同,在 Gemini 中使用自签名证书相当常见。Gemini 对 TLS 采用“首次使用信任”(TOFU)策略。这类似于 SSH 连接:第一次连接到某台主机时,你的客户端会将其证书与该主机关联。如果后续请求中证书发生变化,你会收到一个 TLS 错误(至少是警告),提示服务器身份已改变。
让我们生成一个自签名证书来标识我们的服务器。为此我们将使用 openssl 工具,它很可能也已经安装在你的系统上。
#!/bin/sh
set -eu
HOST="${1:-my-gemini-host.com}"
DAYS="${2:-365}"
openssl req -x509 -newkey rsa:2048 -nodes -days "$DAYS"
-keyout key.pem -out certificate.pem
-subj "/CN=$HOST"
-addext "subjectAltName=DNS:$HOST,DNS:localhost"
chmod 600 key.pem
openssl x509 -in certificate.pem -noout -subject -ext subjectAltName -dates
这将生成自签名的 certificate.pem 和 key.pem 文件,我们可以用它们向 Gemini 客户端标识我们的服务器。使用 ssl 模块创建一个上下文,并加载前一个脚本生成的证书和密钥文件。ssl 库允许你包装一个未加密的 TCP socket,这样你就可以像之前一样使用它,但数据是加密的。将你当前的代码改为使用包装后的 tls_connection。
import ssl context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain("./certificate.pem", "./key.pem")
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。