Hotdry.
application-security

用 Go 构建基本的 Gemini 服务器和客户端:处理 MIME 类型、证书和 TLS 请求响应

面向轻量级超文本传输,给出 Gemini 协议在 Go 中的服务器和客户端实现要点,包括 MIME 处理和 TLS 配置。

Gemini 协议作为一种新兴的轻量级互联网协议,提供了一种简洁、安全的文本导向内容传输方式。它运行在 TCP 端口 1965 上,并强制要求使用 TLS 加密,这使得它比传统的 HTTP 更注重隐私和效率。不同于 HTTP 的复杂性,Gemini 的设计灵感来源于 Gopher 协议,专注于纯文本文档(gemtext 格式),支持基本的链接、列表和预格式文本,而不支持 JavaScript 或 CSS 等复杂功能。这使得它特别适合低带宽环境、隐私敏感场景或作为 Web 的补充替代方案。

在 Go 语言中实现 Gemini 服务器和客户端的优势在于 Go 的标准库强大,尤其是 net/http 和 crypto/tls 包,能轻松处理网络连接和 TLS 加密,而无需引入过多第三方依赖。这不仅简化了开发过程,还确保了高性能和并发支持。本文将一步步指导如何构建基本的 Gemini 服务器和客户端,重点处理 MIME 类型、证书管理和请求 / 响应机制。通过这些实现,你可以快速搭建一个运行在本地或服务器上的 Gemini 胶囊(capsule),并用客户端访问它。

环境准备与证书生成

首先,确保你的 Go 环境已安装(版本 1.20+ 推荐)。Gemini 协议要求所有通信通过 TLS,因此需要生成自签名证书。Go 的 crypto/tls 包支持自签名,但生产环境中建议使用 Let's Encrypt 等 CA 颁发的证书。对于开发,我们使用自签名证书,并采用 TOFU(Trust On First Use)验证机制,即首次连接时信任证书,后续检查一致性。

使用以下命令生成自签名证书(需要安装 OpenSSL):

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt -subj "/CN=localhost"

这将生成 server.key(私钥)和 server.crt(证书)。在代码中加载它们:

cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
    log.Fatal(err)
}
config := &tls.Config{Certificates: []tls.Certificate{cert}}
listener, err := tls.Listen("tcp", ":1965", config)
if err != nil {
    log.Fatal(err)
}

证据显示,Gemini 规范(v0.14.3)强调 TLS 1.2+ 和隐私增强功能,如不支持弱加密套件。这在 Go 的 tls.Config 中可以通过 MinVersion: tls.VersionTLS12CipherSuites 自定义实现。对于 TOFU,客户端需存储服务器证书指纹,并在后续连接中验证。

基本服务器实现

Gemini 服务器的核心是监听端口 1965,接受 TLS 连接,解析客户端发送的简单请求(一行 URL,如 gemini://example.com/path),然后返回响应。响应格式为:第一行状态码 + META(如 20 text/gemini\r\n),成功时跟随响应体。

使用 Go 的 net 包实现一个简单的服务器:

for {
    conn, err := listener.Accept()
    if err != nil {
        continue
    }
    go handleConnection(conn)
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    request, err := reader.ReadString('\n')
    if err != nil {
        return
    }
    // 解析请求:trim 空格,提取路径
    path := strings.TrimSpace(request)
    if !strings.HasPrefix(path, "gemini://") {
        path = "/default" // 默认路径
    }
    // 根据路径返回内容
    if path == "/" {
        body := "# 欢迎来到 Gemini 服务器\r\n这是一个简单的 Gemini 胶囊。"
        meta := fmt.Sprintf("20 text/gemini\r\n")
        response := meta + body
        conn.Write([]byte(response))
    } else {
        // 错误响应
        conn.Write([]byte("51 No such resource\r\n")) // 51 表示资源不存在
    }
}

这里,META 行指定 MIME 类型,如 text/gemini 用于 gemtext。Gemini 支持多种 MIME:text/plain 用于纯文本、image/png 用于图片(通过链接访问,非内嵌)。引用 Gemini 规范:“响应头必须是单个 CRLF 终止行,格式为 'STATUS META\r\n',其中 STATUS 是两位数字,META 是 MIME 类型或错误消息。”

对于 MIME 处理,服务器需根据文件扩展或内容类型设置 META。例如,处理 PNG 文件:

if strings.HasSuffix(path, ".png") {
    meta := "20 image/png\r\n"
    // 读取并发送二进制数据
    data, _ := os.ReadFile("image.png")
    conn.Write([]byte(meta))
    conn.Write(data)
}

可落地参数:

  • 监听地址::1965(默认端口)。
  • 证书有效期:365 天,RSA 2048 位。
  • 并发:Go 的 goroutine 天然支持高并发,每连接一个 goroutine。
  • 错误码:使用 40(临时失败)、51(资源不存在)、59(坏请求)等。

风险:自签名证书易遭中间人攻击,建议客户端实现 TOFU 并在首次连接时显示指纹(如 SHA-256)。

基本客户端实现

客户端需发起 TLS 连接到 hostname:1965,发送请求 URL,然后读取响应。Go 的 tls.Dial 简化了这一过程。

func fetchGemini(url string) (string, error) {
    u, err := url.Parse(url)
    if err != nil {
        return "", err
    }
    config := &tls.Config{ServerName: u.Hostname()}
    conn, err := tls.Dial("tcp", u.Host+":"+u.Port(), config)
    if err != nil {
        return "", err
    }
    defer conn.Close()
    // 发送请求
    request := u.Path + "\r\n"
    conn.Write([]byte(request))
    // 读取响应
    reader := bufio.NewReader(conn)
    statusLine, err := reader.ReadString('\n')
    if err != nil {
        return "", err
    }
    parts := strings.SplitN(strings.TrimSpace(statusLine), " ", 2)
    if len(parts) < 2 || !strings.HasPrefix(parts[0], "2") {
        return parts[1], nil // 错误消息
    }
    // 读取 body
    body, _ := io.ReadAll(reader)
    return string(body), nil
}

对于 TOFU,客户端可存储证书:

// 首次连接
conn, err := tls.Dial("tcp", host, config)
if err != nil {
    return err
}
state := conn.ConnectionState()
fingerprint := sha256.Sum256(state.PeerCertificates[0].Raw)
storeFingerprint(host, fingerprint[:]) // 存储

后续验证:

if !verifyFingerprint(host, fingerprint) {
    return errors.New("证书指纹不匹配")
}

MIME 处理:在客户端,根据 META 决定如何渲染。例如,text/gemini 解析为链接(=> URL 文本),image/* 下载显示。Go 可使用 regexp 解析 gemtext。

可落地清单:

  • URL 格式:gemini://hostname/path?query
  • 超时:tls.DialWithDialer 设置 10s 超时。
  • 重定向:Gemini 支持 3xx 状态,客户端需跟随(最多 10 次)。
  • 代理:不支持 HTTP 代理,需自定义 SOCKS。

高级考虑与测试

在实际部署中,集成文件系统服务静态内容,或使用 CGI 处理动态响应。监控连接数,避免 DDoS(限速 1 req/s)。测试时,用 Lagrange(Gemini 浏览器)访问 gemini://localhost/

Gemini 的优势在于其简洁:响应大小通常 <1KB,加载 <100ms,远优于 HTTP 的 bloated 页面。这使其理想用于 IoT 或移动设备。

资料来源:

通过以上实现,你已掌握 Gemini 在 Go 中的核心。扩展时,可添加 gemini:// 链接解析或多 MIME 支持,进一步探索这一轻量级协议的潜力。(字数:1024)

查看归档