Files
olons_server/internal/managerconfig/client.go
T

171 lines
4.1 KiB
Go

package managerconfig
import (
"bytes"
"context"
"crypto/hmac"
cryptorand "crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"git.118im.com/im_team/im_workbench/internal/config"
)
const maxResponseBody = 1 << 20
type Client struct {
cfg config.ManagerConfig
http *http.Client
}
func ResolveUIDKey(ctx context.Context, cfg config.ManagerConfig, fallback string) string {
key, err := New(cfg).UIDKey(ctx)
if err != nil || strings.TrimSpace(key) == "" {
return fallback
}
return key
}
func New(cfg config.ManagerConfig) *Client {
timeout := time.Duration(cfg.TimeoutMS) * time.Millisecond
if timeout <= 0 {
timeout = 3 * time.Second
}
return &Client{
cfg: cfg,
http: &http.Client{Timeout: timeout},
}
}
func (c *Client) UIDKey(ctx context.Context) (string, error) {
baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.BaseURL), "/")
if baseURL == "" {
return "", fmt.Errorf("manager base url is empty")
}
if strings.TrimSpace(c.cfg.Secret) == "" {
return "", fmt.Errorf("manager secret is empty")
}
keys := cleanList(c.cfg.UIDKeyKeys)
if len(keys) == 0 {
keys = []string{"uid_encryption_key", "UID_ENCRYPTION_KEY", "uid_key"}
}
group := strings.TrimSpace(c.cfg.UIDKeyGroup)
if group == "" {
group = "im"
}
values := url.Values{}
values.Set("keys", strings.Join(keys, ","))
path := "/internal/im-config/third-party/" + url.PathEscape(group) + "?" + values.Encode()
req, err := c.newRequest(ctx, http.MethodGet, baseURL+path, nil)
if err != nil {
return "", err
}
resp, err := c.http.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("manager status %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
if err != nil {
return "", err
}
valuesMap, err := parseValues(body)
if err != nil {
return "", err
}
for _, key := range keys {
if value := strings.TrimSpace(valuesMap[key]); value != "" {
return value, nil
}
}
return "", fmt.Errorf("uid key is empty")
}
func (c *Client) newRequest(ctx context.Context, method string, rawURL string, body []byte) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
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.cfg.Secret))
_, _ = mac.Write([]byte(signingText))
serviceName := strings.TrimSpace(c.cfg.ServiceName)
if serviceName == "" {
serviceName = "im-server"
}
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Service-Name", serviceName)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil)))
return req, nil
}
func parseValues(body []byte) (map[string]string, error) {
var envelope struct {
Code int `json:"code"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(body, &envelope); err == nil && len(envelope.Data) > 0 {
if envelope.Code != 0 {
return nil, fmt.Errorf("manager code %d", envelope.Code)
}
body = envelope.Data
}
var payload struct {
Values map[string]string `json:"values"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
if payload.Values == nil {
return map[string]string{}, nil
}
return payload.Values, nil
}
func cleanList(values []string) []string {
items := make([]string, 0, len(values))
seen := make(map[string]bool, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || seen[value] {
continue
}
seen[value] = true
items = append(items, value)
}
return items
}
func randomNonce() (string, error) {
var b [16]byte
if _, err := cryptorand.Read(b[:]); err != nil {
return "", err
}
return hex.EncodeToString(b[:]), nil
}