Files
napcat/connect_http.go

193 lines
5.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 napcat
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"sync"
"time"
)
// HTTPReport HTTP POST 上报连接器:框架起一个 HTTP 服务,
// NapCat 把事件 POST 过来;处理函数里调用的 Reply/Approve 会以
// OneBot「快速操作」的形式随 HTTP 响应返回。
type HTTPReport struct {
addr string
srv *http.Server
}
// NewHTTPReport 创建 HTTP 上报连接器,监听 addr(如 ":8081"),
// NapCat 侧上报地址填 http://<本机IP>:8081/(任意路径均可)。
func NewHTTPReport(addr string) *HTTPReport { return &HTTPReport{addr: addr} }
func (h *HTTPReport) Name() string { return "http-report(" + h.addr + ")" }
func (h *HTTPReport) Serve(ctx context.Context, bot *Client) error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
body, err := readAllLimited(r.Body)
if err != nil {
http.Error(w, "read body: "+err.Error(), http.StatusBadRequest)
return
}
if bot.secret != "" && !verifySignature(bot.secret, body, r.Header.Get("X-Signature")) {
http.Error(w, "invalid signature", http.StatusForbidden)
return
}
ev, err := decodeEvent(body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
quick := &quickBox{params: map[string]any{}}
bot.dispatch(ev, bot, quick)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(quick.params)
})
srv := &http.Server{
Addr: h.addr,
Handler: mux,
BaseContext: func(net.Listener) context.Context { return ctx },
}
h.srv = srv
err := srv.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
// Shutdown 关闭监听(Run 的退出流程会调用)。
func (h *HTTPReport) Shutdown(ctx context.Context) error {
if h.srv == nil {
return nil
}
return h.srv.Shutdown(ctx)
}
// quickBox 收集 HTTP 上报模式下随响应返回的快速操作参数。
type quickBox struct {
params map[string]any
}
func (q *quickBox) set(key string, value any) {
if q.params == nil {
q.params = map[string]any{}
}
q.params[key] = value
}
// verifySignature 校验 NapCat HTTP 上报的 X-Signature(HMAC-SHA1)。
func verifySignature(secret string, body []byte, got string) bool {
mac := hmac.New(sha1.New, []byte(secret))
mac.Write(body)
want := "sha1=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(got), []byte(want))
}
func readAllLimited(r io.Reader) ([]byte, error) {
const maxBody = 32 << 20
buf := &bytes.Buffer{}
if _, err := io.Copy(buf, io.LimitReader(r, maxBody+1)); err != nil {
return nil, err
}
if buf.Len() > maxBody {
return nil, errors.New("request body too large")
}
return buf.Bytes(), nil
}
// HTTPAPI HTTP API 连接器:框架作为客户端,POST 到 NapCat 的 HTTP 服务执行 action。
// 仅配置了 HTTPAPI 而没有 WS 会话时,Client.Call 走这条通道。
type HTTPAPI struct {
base string
token atomicString
hc *http.Client
}
type atomicString struct {
mu sync.RWMutex
v string
}
func (a *atomicString) Store(v string) {
a.mu.Lock()
a.v = v
a.mu.Unlock()
}
func (a *atomicString) Load() string {
a.mu.RLock()
defer a.mu.RUnlock()
return a.v
}
// NewHTTPAPI 创建 HTTP API 连接器,base 形如 http://127.0.0.1:3000。
func NewHTTPAPI(base string) *HTTPAPI {
return &HTTPAPI{base: base, hc: &http.Client{Timeout: 30 * time.Second}}
}
func (h *HTTPAPI) Name() string { return "http-api(" + h.base + ")" }
// Serve 对 HTTP API 连接器只是完成鉴权配置并阻塞到框架退出。
func (h *HTTPAPI) Serve(ctx context.Context, bot *Client) error {
h.token.Store(bot.token)
<-ctx.Done()
return nil
}
// Call 实现 Caller:POST <base>/<action>,body 为 params 对象。
func (h *HTTPAPI) Call(ctx context.Context, action string, params any, out any) error {
if ctx == nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
}
body := []byte("{}")
if params != nil {
var err error
if body, err = json.Marshal(params); err != nil {
return fmt.Errorf("napcat: marshal params of %s: %w", action, err)
}
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.base+"/"+action, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if token := h.token.Load(); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := h.hc.Do(req)
if err != nil {
return fmt.Errorf("napcat: http api %s: %w", action, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("napcat: action %s not found (404)", action)
}
var envelope Response
if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
return fmt.Errorf("napcat: http api %s: bad response (http %d): %w", action, resp.StatusCode, err)
}
if err := envelope.Err(); err != nil {
return err
}
if err := envelope.Decode(out); err != nil {
return fmt.Errorf("napcat: decode data of %s: %w", action, err)
}
return nil
}