Files
im_manager/server/internal/library/imworkbench/client.go
T

200 lines
5.3 KiB
Go

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 := firstSecret(
g.Cfg().MustGet(ctx, "imWorkbenchApi.serviceSecret").String(),
os.Getenv("IM_WORKBENCH_API_SECRET"),
g.Cfg().MustGet(ctx, "imInternalApi.serviceSecret").String(),
os.Getenv("IM_INTERNAL_API_SECRET"),
g.Cfg().MustGet(ctx, "imAdminApi.serviceSecret").String(),
os.Getenv("IM_ADMIN_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
}
func firstSecret(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") {
value = strings.TrimSpace(os.Getenv(strings.TrimSuffix(strings.TrimPrefix(value, "${"), "}")))
}
if value != "" {
return value
}
}
return ""
}