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() }