- 四端=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 请裁决)
88 lines
3.2 KiB
Go
88 lines
3.2 KiB
Go
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()
|
||
}
|