Gemini 协议作为一种轻量级超文本传输协议,旨在提供一个简单、安全的替代方案,避免 HTTP 的复杂性和现代 Web 的诸多问题。它运行在 TCP 端口 1965 上,并强制使用 TLS 加密,确保所有传输均为安全通道。与传统 Web 不同,Gemini 不支持 JavaScript、Cookie 或任何状态管理机制,这使得客户端实现极为简洁,同时强调隐私保护。本文将聚焦于在 Go 或 Rust 语言中工程化 Gemini 客户端的核心功能:TLS 安全的请求解析与 6 组响应码处理,从而实现对 Gemini 胶囊(capsules,即 Gemini 站点)的无 JS 导航。我们将从协议基础入手,逐步讨论实现细节、可落地参数、监控要点,并强调协议符合性测试与胶囊发现机制。
Gemini 协议基础概述
Gemini 协议的设计灵感来源于 Gopher 协议,但融入了现代安全特性。请求格式极其简单:客户端向服务器发送一个绝对 URI(如 gemini://example.com/path),后跟 CRLF(回车换行)。URI 长度不得超过 1024 字节,不允许包含用户信息部分(userinfo)或片段(fragment)。服务器响应则以两数字状态码开头,空格分隔元数据(meta),再跟 CRLF,随后是响应体(仅在成功时)。状态码分为 6 组:10-19(输入预期)、20-29(成功)、30-39(重定向)、40-49(临时失败)、50-59(永久失败)、60-69(客户端证书)。这种分组设计允许客户端基于首位数字快速决策,同时第二位提供细粒度控制。
在实现客户端时,TLS 是核心。Gemini 要求 TLS 1.2 或更高版本,并强制使用 Server Name Indication (SNI) 以支持虚拟主机。证书验证推荐采用 Trust on First Use (TOFU) 模式:首次连接时接受服务器证书,保存指纹,后续连接验证匹配。这避免了依赖中心化 CA 的复杂性,但需小心 MITM 攻击。Go 的 crypto/tls 包和 Rust 的 rustls 库均提供原生支持,便于集成。
TLS 安全请求构造与解析
构建请求前,客户端需解析输入 URI,确保符合 Gemini 方案(scheme 为 "gemini")。在 Go 中,可使用 net/url 包解析 URI,提取主机、端口(默认 1965)和路径。若路径为空,建议添加尾随 '/' 以提高兼容性。请求字符串即为完整 URI + "\r\n"。
TLS 连接建立是关键步骤。在 Go 示例中:
import (
"crypto/tls"
"net"
"fmt"
)
func connectGemini(host string, port string) (*tls.Conn, error) {
config := &tls.Config{
ServerName: host,
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
}
conn, err := net.Dial("tcp", host+":"+port)
if err != nil {
return nil, err
}
tlsConn := tls.Client(conn, config)
if err := tlsConn.Handshake(); err != nil {
return nil, err
}
return tlsConn, nil
}
这里,MinVersion 确保 TLS 1.2+,ServerName 启用 SNI。TOFU 验证可扩展:维护一个 map 存储主机-指纹对,首次连接计算 SHA-256 指纹并提示用户确认,后续比较。若不匹配,触发警告或中止。
Rust 中,使用 rustls 和 tokio 等异步库类似:
use rustls::{ClientConfig, RootCertStore};
use std::sync::Arc;
fn create_tls_config(host: &str) -> Arc<ClientConfig> {
let mut root_store = RootCertStore::empty();
let config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
Arc::new(config)
}
连接后,发送请求:tlsConn.Write([]byte(requestURI + "\r\n"))。解析请求侧重错误处理:URI 过长或无效格式时,客户端应本地验证,避免无效连接。参数建议:超时阈值设为 30 秒(Handshake 10s,读写 20s),重试次数限 3 次,指数退避(初始 1s,倍增)。
6 码响应处理与导航逻辑
响应解析从读取头部开始:服务器发送 "XX meta\r\n",客户端需逐字节读取直到 CRLF。Go 的 bufio.Reader 便利:
import "bufio"
status, meta, err := readResponseHeader(tlsConn)
if err != nil { }
code := int(status[0]-'0') * 10 + int(status[1]-'0')
基于首位数字分组处理:
-
1x (10/11 输入预期):显示 meta 作为提示,收集用户输入,URI 编码(空格 %20,换行 %0A)后附加查询,重发请求。11 为敏感输入,不回显。参数:输入缓冲限 1024 字节,支持多行。
-
2x (20 成功):meta 为 MIME 类型(默认 text/gemini; charset=utf-8)。读取剩余体直到 EOF(服务器关闭连接)。导航:解析 gemtext 链接(=> URL),无 JS 故纯文本渲染。清单:支持 text/gemini、text/plain;其他 MIME 保存或外部打开。
-
3x (30/31 重定向):meta 为新 URI,限 5 次跟随。30 临时(保留原 URI),31 永久(更新书签)。解析相对 URI,合并主机/路径。
-
4x (40-44 临时失败):无体,显示 meta 消息。40 通用重试,41 服务器不可用(503 类似),42 CGI 错误,43 代理错误,44 慢速(指数退避)。监控:日志重试计数,阈值 5 次后放弃。
-
5x (50-59 永久失败):无重试。50 通用,51 未找到(404),52 已消失(410),53 代理拒绝,59 坏请求。客户端缓存避免重复。
-
6x (60-62 证书):要求客户端证书。60 需 cert,61 授权失败,62 无效 cert。生成自签名 cert(限路径作用域),用户确认后重发。Rust 的 rcgen 库可动态生成。
导航实现:维护栈式历史,无状态故每个链接新连接。胶囊发现:从根 / 开始爬取 => 链接,构建站点地图。无 Cookie,故无会话;隐私优先,避免指纹。
协议符合性测试与胶囊发现
符合性测试至关重要。使用 gemini.tilde.pw 测试套件验证请求/响应格式、TLS 合规。参数:URI 边缘 case(空路径、长 URI)、状态码模拟(mock 服务器)。Go 测试:httptest 适配 TLS;Rust:tokio-test。
胶囊发现:客户端扫描已知索引(如 gemini://gemini.circumlunar.space/),解析链接。实现 BFS 爬虫,限深度 3,过滤非 gemini://。监控:连接池限 10,速率 1/s 避 DoS。
可落地参数与回滚策略
- TLS 参数:Cipher Suites 优先 ECDHE + AES-GCM;TOFU 数据库 SQLite 存储。
- 解析阈值:头部读超时 5s,体大小限 1MB(用户可调)。
- 错误处理:4x/5x 缓存 TTL 1h;6x cert 过期检查。
- 监控点:Prometheus 指标:连接成功率、状态码分布、重定向链长。
- 回滚:若 TLS 1.3 问题,降 1.2;测试失败回默认 HTTP 代理(非推荐)。
实现 Gemini 客户端强调简洁:Go 约 500 行,Rust 类似。避免复杂依赖,聚焦核心。
资料来源:Gemini 协议规范(https://geminiprotocol.net/docs/protocol-specification.gmi),官方文档(https://geminiprotocol.net/docs/)。
(字数:约 1250 字)