feat: proxy workbench admin through service
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package imworkbench
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
const maxResponseBody = 10 << 20
|
||||
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
ServiceName string
|
||||
Secret string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
conf Config
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
type Page[T any] struct {
|
||||
Items []T `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
}
|
||||
|
||||
func DefaultClient(ctx context.Context) *Client {
|
||||
timeout := g.Cfg().MustGet(ctx, "imWorkbenchApi.timeout", "5s").Duration()
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
secret := g.Cfg().MustGet(ctx, "imWorkbenchApi.serviceSecret").String()
|
||||
if strings.HasPrefix(secret, "${") && strings.HasSuffix(secret, "}") {
|
||||
secret = os.Getenv(strings.TrimSuffix(strings.TrimPrefix(secret, "${"), "}"))
|
||||
}
|
||||
if secret == "" {
|
||||
secret = os.Getenv("IM_WORKBENCH_API_SECRET")
|
||||
}
|
||||
conf := Config{
|
||||
BaseURL: g.Cfg().MustGet(ctx, "imWorkbenchApi.baseUrl").String(),
|
||||
ServiceName: g.Cfg().MustGet(ctx, "imWorkbenchApi.serviceName", "hotgo-admin").String(),
|
||||
Secret: secret,
|
||||
Timeout: timeout,
|
||||
}
|
||||
return &Client{conf: conf, http: &http.Client{Timeout: timeout}}
|
||||
}
|
||||
|
||||
func (c *Client) Get(ctx context.Context, path string, values url.Values, out any) error {
|
||||
if values != nil {
|
||||
if query := values.Encode(); query != "" {
|
||||
path += "?" + query
|
||||
}
|
||||
}
|
||||
return c.doJSON(ctx, http.MethodGet, path, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) Post(ctx context.Context, path string, in any, out any) error {
|
||||
body, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "序列化工作台请求失败")
|
||||
}
|
||||
return c.doJSON(ctx, http.MethodPost, path, body, out)
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method string, path string, body []byte, out any) error {
|
||||
req, err := c.newRequest(ctx, method, path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "IM workbench request failed url:%s err:%+v", req.URL.String(), err)
|
||||
return gerror.New("IM工作台服务暂不可用")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return parseError(resp)
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取IM工作台响应失败")
|
||||
}
|
||||
var envelope struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(responseBody, &envelope); err == nil && len(envelope.Data) > 0 {
|
||||
if envelope.Code != 0 {
|
||||
return stableError(envelope.Message, envelope.Reason)
|
||||
}
|
||||
return json.Unmarshal(envelope.Data, out)
|
||||
}
|
||||
return json.Unmarshal(responseBody, out)
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(ctx context.Context, method string, path string, body []byte) (*http.Request, error) {
|
||||
baseURL := strings.TrimRight(c.conf.BaseURL, "/")
|
||||
if baseURL == "" {
|
||||
return nil, gerror.New("IM工作台API未配置")
|
||||
}
|
||||
if c.conf.Secret == "" {
|
||||
return nil, gerror.New("IM工作台API密钥未配置")
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建IM工作台请求失败")
|
||||
}
|
||||
timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10)
|
||||
nonce, err := randomNonce()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyHash := sha256.Sum256(body)
|
||||
signingText := strings.Join([]string{
|
||||
method,
|
||||
req.URL.RequestURI(),
|
||||
hex.EncodeToString(bodyHash[:]),
|
||||
timestamp,
|
||||
nonce,
|
||||
}, "\n")
|
||||
mac := hmac.New(sha256.New, []byte(c.conf.Secret))
|
||||
_, _ = mac.Write([]byte(signingText))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Name", c.conf.ServiceName)
|
||||
req.Header.Set("X-Timestamp", timestamp)
|
||||
req.Header.Set("X-Nonce", nonce)
|
||||
req.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil)))
|
||||
req.Header.Set("X-Request-ID", gctx.CtxId(ctx))
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func parseError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||
var envelope struct {
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &envelope)
|
||||
return stableError(envelope.Message, envelope.Reason)
|
||||
}
|
||||
|
||||
func stableError(message string, reason string) error {
|
||||
if message != "" {
|
||||
return gerror.New(message)
|
||||
}
|
||||
if reason != "" {
|
||||
return gerror.New(reason)
|
||||
}
|
||||
return gerror.New("IM工作台服务暂不可用")
|
||||
}
|
||||
|
||||
func randomNonce() (string, error) {
|
||||
var b [16]byte
|
||||
if _, err := cryptorand.Read(b[:]); err != nil {
|
||||
return "", gerror.Wrap(err, "生成IM工作台请求随机数失败")
|
||||
}
|
||||
return hex.EncodeToString(b[:]), nil
|
||||
}
|
||||
Reference in New Issue
Block a user