fix(kook): KOOK 机器人鉴权用 Authorization: Bot <token> 而非 Bearer
httpclient 硬编码 Bearer,导致 KOOK bot token 被当 OAuth2 用户令牌发,
/ gateway/index 返回 401『登录环境异常』。按 KOOK reference 鉴权规范,
给共享 httpclient 增加可配置 authScheme(默认 Bearer),KOOK 用 Bot。
- httpclient: 新增 WithAuthScheme 选项 + authScheme 字段,do() 按 scheme 组装
- kook/adapter: 装配时传 WithAuthScheme("Bot")
- 同步修正 config.go 注释与 send/identity 测试断言
This commit is contained in:
1 parent
e9df928072
commit
c1bd740f7c
5 files changed
+36
-18
No files matched your search
@@ -1,12 +1,13 @@
|
|||||||
// Package httpclient provides a small, reusable HTTP (/JSON) client for IM
|
// Package httpclient provides a small, reusable HTTP (/JSON) client for IM
|
||||||
// channel adapters that talk to a REST API with bearer-token auth and a
|
// channel adapters that talk to a REST API with token auth and a
|
||||||
// common `{code,message,data}` response envelope (KOOK, and similarly shaped
|
// common `{code,message,data}` response envelope (KOOK, and similarly shaped
|
||||||
// platforms). It is used by the KOOK adapter and is intended to be shared by
|
// platforms). It is used by the KOOK adapter and is intended to be shared by
|
||||||
// future platforms; existing adapters (Telegram, OneBot) that already carry
|
// future platforms; existing adapters (Telegram, OneBot) that already carry
|
||||||
// their own transport are left untouched.
|
// their own transport are left untouched.
|
||||||
//
|
//
|
||||||
// It depends only on the standard library and internal/logger. The bearer token
|
// It depends only on the standard library and internal/logger. The auth token is
|
||||||
// is never logged or surfaced in errors.
|
// never logged or surfaced in errors. The Authorization scheme is configurable
|
||||||
|
// (default "Bearer" for OAuth2-style tokens; KOOK robots use "Bot").
|
||||||
package httpclient
|
package httpclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -31,12 +32,13 @@ var ErrAPI = errors.New("httpclient: api error")
|
|||||||
// maxBodyBytes caps how large a response body the client will read.
|
// maxBodyBytes caps how large a response body the client will read.
|
||||||
const maxBodyBytes = 1 << 20 // 1 MiB
|
const maxBodyBytes = 1 << 20 // 1 MiB
|
||||||
|
|
||||||
// Client is a bearer-auth JSON HTTP client bound to a base URL.
|
// Client is a token-auth JSON HTTP client bound to a base URL.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
http *http.Client
|
http *http.Client
|
||||||
baseURL string
|
baseURL string
|
||||||
token string
|
token string
|
||||||
log *slog.Logger
|
authScheme string
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option customizes a Client at construction time.
|
// Option customizes a Client at construction time.
|
||||||
@@ -54,12 +56,19 @@ func WithBaseURL(base string) Option {
|
|||||||
return func(c *Client) { c.baseURL = strings.TrimRight(base, "/") }
|
return func(c *Client) { c.baseURL = strings.TrimRight(base, "/") }
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithToken permits an Authorization: Bearer header on every request. The token
|
// WithToken permits an Authorization header on every request. The token is
|
||||||
// is stored in memory only and never logged.
|
// stored in memory only and never logged.
|
||||||
func WithToken(token string) Option {
|
func WithToken(token string) Option {
|
||||||
return func(c *Client) { c.token = token }
|
return func(c *Client) { c.token = token }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithAuthScheme sets the scheme used in the `Authorization: <scheme> <token>`
|
||||||
|
// header. The default is "Bearer" (OAuth2-style tokens). KOOK robots require
|
||||||
|
// "Bot". Passing an empty string keeps the default.
|
||||||
|
func WithAuthScheme(scheme string) Option {
|
||||||
|
return func(c *Client) { c.authScheme = strings.TrimSpace(scheme) }
|
||||||
|
}
|
||||||
|
|
||||||
// WithLogger injects the logger. A nil logger stays silent.
|
// WithLogger injects the logger. A nil logger stays silent.
|
||||||
func WithLogger(l *slog.Logger) Option {
|
func WithLogger(l *slog.Logger) Option {
|
||||||
return func(c *Client) {
|
return func(c *Client) {
|
||||||
@@ -75,8 +84,9 @@ func WithLogger(l *slog.Logger) Option {
|
|||||||
// the Do/Get helpers' path being an absolute URL).
|
// the Do/Get helpers' path being an absolute URL).
|
||||||
func New(opts ...Option) *Client {
|
func New(opts ...Option) *Client {
|
||||||
c := &Client{
|
c := &Client{
|
||||||
http: &http.Client{},
|
http: &http.Client{},
|
||||||
log: logger.New(logger.Options{Out: io.Discard}),
|
authScheme: "Bearer",
|
||||||
|
log: logger.New(logger.Options{Out: io.Discard}),
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(c)
|
opt(c)
|
||||||
@@ -133,7 +143,11 @@ func (c *Client) do(ctx context.Context, method, target string, body any) (json.
|
|||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
if c.token != "" {
|
if c.token != "" {
|
||||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
scheme := c.authScheme
|
||||||
|
if scheme == "" {
|
||||||
|
scheme = "Bearer"
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", scheme+" "+c.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.http.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
|
|||||||
@@ -105,9 +105,13 @@ func New(cfg Config, opts ...Option) *Adapter {
|
|||||||
a.baseURL = defaultBaseURL
|
a.baseURL = defaultBaseURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// KOOK authenticates bots with `Authorization: Bot <token>` (not "Bearer",
|
||||||
|
// which is reserved for OAuth2 user tokens). This is mandated by the KOOK
|
||||||
|
// reference: /doc/reference §鉴权.
|
||||||
a.http = httpclient.New(
|
a.http = httpclient.New(
|
||||||
httpclient.WithBaseURL(a.baseURL),
|
httpclient.WithBaseURL(a.baseURL),
|
||||||
httpclient.WithToken(cfg.BotToken),
|
httpclient.WithToken(cfg.BotToken),
|
||||||
|
httpclient.WithAuthScheme("Bot"),
|
||||||
httpclient.WithHTTPClient(a.client),
|
httpclient.WithHTTPClient(a.client),
|
||||||
httpclient.WithLogger(a.log),
|
httpclient.WithLogger(a.log),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// modes: a WebSocket-gateway mode (events over a gateway WebSocket with
|
// modes: a WebSocket-gateway mode (events over a gateway WebSocket with
|
||||||
// hello/heartbeat/event/resume signaling) and a Webhook mode (KOOK POSTs event
|
// hello/heartbeat/event/resume signaling) and a Webhook mode (KOOK POSTs event
|
||||||
// callbacks to a Cyrene gateway route). Outbound messages are always sent over a
|
// callbacks to a Cyrene gateway route). Outbound messages are always sent over a
|
||||||
// bearer-authenticated REST API.
|
// bot-token-authenticated REST API (`Authorization: Bot <token>`).
|
||||||
//
|
//
|
||||||
// The adapter conforms to internal/channel/adapter.PlatformAdapter, translating
|
// The adapter conforms to internal/channel/adapter.PlatformAdapter, translating
|
||||||
// native KOOK events into the normalized adapter.UnifiedMessage model and
|
// native KOOK events into the normalized adapter.UnifiedMessage model and
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ func TestFetchBotIdentity(t *testing.T) {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if got := r.Header.Get("Authorization"); got != "Bearer testtoken" {
|
if got := r.Header.Get("Authorization"); got != "Bot testtoken" {
|
||||||
t.Errorf("Authorization = %q, want Bearer testtoken", got)
|
t.Errorf("Authorization = %q, want Bot testtoken", got)
|
||||||
}
|
}
|
||||||
kookOK(w, map[string]any{"id": "bot-1", "username": "bot", "nickname": "小K"})
|
kookOK(w, map[string]any{"id": "bot-1", "username": "bot", "nickname": "小K"})
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ func TestSendMessage_GroupText(t *testing.T) {
|
|||||||
}
|
}
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
if auth != "Bearer testtoken" {
|
if auth != "Bot testtoken" {
|
||||||
t.Errorf("Authorization = %q, want Bearer testtoken", auth)
|
t.Errorf("Authorization = %q, want Bot testtoken", auth)
|
||||||
}
|
}
|
||||||
if body["target_id"] != "chan-1" {
|
if body["target_id"] != "chan-1" {
|
||||||
t.Errorf("target_id = %v, want chan-1", body["target_id"])
|
t.Errorf("target_id = %v, want chan-1", body["target_id"])
|
||||||
|
|||||||
Reference in new issue
Block a user