package qq_obv11 // 本文件实现四个端里的 **ws + server**(裁决 1.4):Cyrene 监听 `listen_addr`,等 // OneBot 实现(NapCat / go-cqhttp 的「反向 WS」)反连上来——连接由 OneBot 发起 // (OneBot→Cyrene),建立后收发都走这条长连接。端口必须由配置显式给出,默认不开 // (AGENTS §9 的登记豁免)。鉴权与 HTTP server 端一致:配置了 access_token 时, // Authorization: Bearer 或 ?access_token= 必须匹配,否则 403。 import ( "context" "errors" "fmt" "net" "net/http" "strings" ) // connectServer starts the HTTP listener in server mode. It returns once the // listener is bound; the session becomes live when a OneBot client connects. func (a *Adapter) connectServer(ctx context.Context) error { if a.cfg.ListenAddr == "" { return errors.New("qq-obv11: server mode requires listenAddr") } mux := http.NewServeMux() mux.HandleFunc("/", a.wsHandler) ln, err := net.Listen("tcp", a.cfg.ListenAddr) if err != nil { return fmt.Errorf("qq-obv11: listen %s: %w", a.cfg.ListenAddr, err) } // 先把 *http.Server 落在局部变量上再交给 goroutine:Disconnect 会把 a.srv 置 nil, // 直接读 a.srv 会与它构成数据竞争。 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 && serveErr != http.ErrServerClosed { a.log.Warn("websocket server stopped", "err", serveErr) } }() a.log.Info("websocket server listening (server mode)", "addr", ln.Addr().String()) return nil } // wsHandler is the HTTP handler that upgrades one inbound OneBot client // connection to a WebSocket session. func (a *Adapter) wsHandler(w http.ResponseWriter, r *http.Request) { if a.cfg.AccessToken != "" && !a.checkAuth(r) { http.Error(w, "Forbidden: invalid access_token", http.StatusForbidden) return } conn, err := upgradeWS(w, r) if err != nil { a.log.Warn("websocket upgrade failed", "err", err) return } a.setConn(conn) a.log.Info("onebot client connected (server mode)", "remote", r.RemoteAddr) } // checkAuth validates the access token carried by an upgrade request. OneBot // implementations pass it either as an access_token query parameter or as an // Authorization: Bearer header. func (a *Adapter) checkAuth(r *http.Request) bool { if got := r.URL.Query().Get("access_token"); got != "" { return got == a.cfg.AccessToken } auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Bearer ") { return strings.TrimPrefix(auth, "Bearer ") == a.cfg.AccessToken } return false }