Files
Cyrene_v2/plugins/channels/qq_obv11/http_server.go
T
AskaEth 9253fef92a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(qq_obv11): OneBot v11 四端齐备(裁决 1.4 / 12.4.1)
- 四端=ws×client / ws×server(原有,补语义注释)+ **http×client**(新增 http_client.go:
  调 OneBot HTTP API、请求-响应、retcode 判成败、token 只走 Authorization 头)+
  **http×server**(新增 http_server.go:监听 listen_addr 收上报、fail-closed 鉴权、10MiB 体上限)
- 配置面:适配器新增 transport / api_base_url / report_path(json tag 沿用既有口径);
  env 兜底 CHANNEL_QQ_OBV11_{TRANSPORT,API_BASE_URL,REPORT_PATH};**默认值=现状**(ws+server)
- 出站分流:session.go 发送路径按 transport 走 WS 写帧或 HTTP POST(不改它则 HTTP 端无法出站)
- 端口豁免边界:http+server 沿用 AGENTS §9 已登记的同一豁免(design/13 §0 原文即含 HTTP server),
  且 listen_addr 为空时报错、**绝不开端口**(与 ws 侧同名校验成对)
- 关键取舍(已上报):OneBot 的 HTTP 是「动作 + 上报」两半,mode 决定**哪一半必需**、
  另一半给齐字段就一并启用 ⇒ 单个适配器即可「收上报 + 回消息」,四端仍各自可独立选择与测试
- 复核:build/vet 绿 + qq_obv11/cmd 包测试绿 + `-race` 干净;10 条新用例(HTTP 两端全部用本地
  httptest 假 OneBot,未连真实 QQ);变异 3 条(伪造 retcode 成功 / 去掉上报鉴权门 / 默认值翻 http)已还原
- 未接:get_group_list(后续)、无 token 时放行(与既有 ws_server 一致,如需 fail-closed 请裁决)
2026-10-04 13:58:18 +08:00

88 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package qq_obv11
// 本文件实现 OneBot v11 的 HTTP **server 端**(裁决 1.4 的四个端之一):Cyrene 监听一个
// HTTP 端口,接收 OneBot 实现主动 POST 上来的事件上报(OneBot→Cyrene 方向)。这是四端里
// 唯一新增的监听端口,受 AGENTS §9 的登记豁免约束:**必须**由配置 `listen_addr` 显式给出,
// 空值直接让 Connect 失败、绝不默认开(design/13 §0)。鉴权 fail-closed:配置了
// access_token 时,Authorization: Bearer 或 ?access_token= 必须匹配,否则 403 直接拒绝。
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
)
// maxReportBodyBytes 限制单条上报体大小:事件 JSON 很小,超过上限的一律拒收,避免
// 恶意对端把上报路径当内存放大器(与 WS 帧上限同一口径)。
const maxReportBodyBytes = 10 << 20 // 10 MiB
// connectHTTPServer 在 listen_addr 上启动 OneBot HTTP 上报接收端。返回时监听已绑定;
// 真正的事件在 OneBot POST 上来时经 handlePayload 进入消息流。
func (a *Adapter) connectHTTPServer(_ context.Context) error {
if a.cfg.ListenAddr == "" {
return errors.New("qq-obv11: http server mode requires listenAddr")
}
mux := http.NewServeMux()
mux.HandleFunc(a.cfg.ReportPath, a.httpReportHandler)
ln, err := net.Listen("tcp", a.cfg.ListenAddr)
if err != nil {
return fmt.Errorf("qq-obv11: listen %s: %w", a.cfg.ListenAddr, err)
}
srv := &http.Server{Handler: mux}
a.mu.Lock()
a.ln = ln
a.srv = srv
a.mu.Unlock()
go func() {
if serveErr := srv.Serve(ln); serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
a.log.Warn("onebot http report server stopped", "err", serveErr)
}
}()
a.log.Info("onebot http report server listening (http server mode)", "addr", ln.Addr().String())
return nil
}
// httpReportHandler 接收 OneBot v11 的 HTTP POST 上报。处理是同步的——与 WS 读循环
// 一致,保证同一账号的事件顺序;桥接器的入站只是入队,所以这里能立刻回 204,OneBot
// 侧不会因超时而重发。
func (a *Adapter) httpReportHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if a.cfg.AccessToken != "" && !a.checkAuth(r) {
http.Error(w, "Forbidden: invalid access_token", http.StatusForbidden)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, maxReportBodyBytes+1))
if err != nil {
http.Error(w, "cannot read report body", http.StatusBadRequest)
return
}
if len(body) > maxReportBodyBytes {
http.Error(w, "report body too large", http.StatusRequestEntityTooLarge)
return
}
// 必须用适配器生命周期 context:r.Context() 在响应写完时就被取消,会连带掐断
// 入站流水线(桥接器的处理是异步派发的)。
a.handlePayload(a.reportContext(), body)
w.WriteHeader(http.StatusNoContent)
}
// reportContext 返回适配器生命周期 context(Connect 时建立);适配器尚未就绪时退回
// 后台 context,保证事件仍能派发而不是被静默丢弃。
func (a *Adapter) reportContext() context.Context {
a.mu.RLock()
defer a.mu.RUnlock()
if a.lifeCtx != nil {
return a.lifeCtx
}
return context.Background()
}