feat: add kafka admin integration
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/logs"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
type adminInternalAPIConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
BaseURL string `json:"base_url"`
|
||||
ServiceName string `json:"service_name"`
|
||||
Secret string `json:"secret"`
|
||||
TimeoutMS int `json:"timeout_ms"`
|
||||
}
|
||||
|
||||
type adminInternalClient struct {
|
||||
config adminInternalAPIConfig
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
type adminInternalTenantDTO struct {
|
||||
ID types.TenantID `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
TenantDesc *string `json:"tenant_desc,omitempty"`
|
||||
DBName string `json:"db_name"`
|
||||
DBDSN string `json:"db_dsn"`
|
||||
State types.TenantState `json:"state"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UpdatedBy int64 `json:"updated_by"`
|
||||
}
|
||||
|
||||
func initAdminInternalAPI(raw json.RawMessage) error {
|
||||
store.SetAdminInternalConfigProvider(nil)
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var cfg adminInternalAPIConfig
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
|
||||
cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
|
||||
if cfg.ServiceName == "" {
|
||||
cfg.ServiceName = "im-server"
|
||||
}
|
||||
if cfg.TimeoutMS <= 0 {
|
||||
cfg.TimeoutMS = 3000
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
if cfg.BaseURL == "" || strings.TrimSpace(cfg.Secret) == "" {
|
||||
return errors.New("admin_internal_api enabled but base_url or secret is empty")
|
||||
}
|
||||
store.SetAdminInternalConfigProvider(&adminInternalClient{
|
||||
config: cfg,
|
||||
http: &http.Client{Timeout: time.Duration(cfg.TimeoutMS) * time.Millisecond},
|
||||
})
|
||||
logs.Info.Printf("Admin internal config client enabled: base_url=%s service=%s", cfg.BaseURL, cfg.ServiceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *adminInternalClient) GetTenantByCode(code string) (*types.Tenant, error) {
|
||||
var tenant adminInternalTenantDTO
|
||||
if err := c.getJSON("/internal/im-config/tenants/by-code/"+url.PathEscape(strings.TrimSpace(code)), nil, &tenant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenant.toStoreTenant(), nil
|
||||
}
|
||||
|
||||
func (c *adminInternalClient) GetTenantByID(id types.TenantID) (*types.Tenant, error) {
|
||||
if !id.IsValid() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
var tenant adminInternalTenantDTO
|
||||
if err := c.getJSON("/internal/im-config/tenants/by-id/"+strconv.FormatInt(int64(id), 10), nil, &tenant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenant.toStoreTenant(), nil
|
||||
}
|
||||
|
||||
func (c *adminInternalClient) ListActiveTenants(limit int) ([]types.Tenant, error) {
|
||||
query := url.Values{}
|
||||
query.Set("state", "active")
|
||||
if limit > 0 {
|
||||
query.Set("limit", strconv.Itoa(limit))
|
||||
}
|
||||
var wrapper struct {
|
||||
Items []adminInternalTenantDTO `json:"items"`
|
||||
}
|
||||
if err := c.getJSON("/internal/im-config/tenants", query, &wrapper); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenants := make([]types.Tenant, 0, len(wrapper.Items))
|
||||
for _, item := range wrapper.Items {
|
||||
if tenant := item.toStoreTenant(); tenant != nil {
|
||||
tenants = append(tenants, *tenant)
|
||||
}
|
||||
}
|
||||
return tenants, nil
|
||||
}
|
||||
|
||||
func (c *adminInternalClient) RegistrationSMSEnabled() (bool, error) {
|
||||
var wrapper struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.getJSON("/internal/im-config/registration/sms", nil, &wrapper); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return wrapper.Enabled, nil
|
||||
}
|
||||
|
||||
func (c *adminInternalClient) ThirdPartyConfig(group string, keys ...string) (map[string]string, error) {
|
||||
query := url.Values{}
|
||||
normalized := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if key = strings.TrimSpace(key); key != "" {
|
||||
normalized = append(normalized, key)
|
||||
}
|
||||
}
|
||||
if len(normalized) > 0 {
|
||||
query.Set("keys", strings.Join(normalized, ","))
|
||||
}
|
||||
var wrapper struct {
|
||||
Values map[string]string `json:"values"`
|
||||
}
|
||||
if err := c.getJSON("/internal/im-config/third-party/"+url.PathEscape(strings.TrimSpace(group)), query, &wrapper); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wrapper.Values == nil {
|
||||
wrapper.Values = map[string]string{}
|
||||
}
|
||||
return wrapper.Values, nil
|
||||
}
|
||||
|
||||
func (c *adminInternalClient) getJSON(path string, query url.Values, out any) error {
|
||||
if c == nil || c.http == nil {
|
||||
return types.ErrUnsupported
|
||||
}
|
||||
path = "/" + strings.TrimLeft(path, "/")
|
||||
u, err := url.Parse(c.config.BaseURL + path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(query) > 0 {
|
||||
u.RawQuery = query.Encode()
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.signRequest(req, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("admin internal api status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
var parsed struct {
|
||||
Code int `json:"code"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(body, &parsed); err != nil {
|
||||
return err
|
||||
}
|
||||
if parsed.Code != 0 {
|
||||
return fmt.Errorf("admin internal api code=%d", parsed.Code)
|
||||
}
|
||||
if out == nil || len(parsed.Data) == 0 || string(parsed.Data) == "null" {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(parsed.Data, out)
|
||||
}
|
||||
|
||||
func (c *adminInternalClient) signRequest(req *http.Request, body []byte) error {
|
||||
nonce, err := randomInternalNonce()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
bodyHash := sha256.Sum256(body)
|
||||
signingText := strings.Join([]string{
|
||||
req.Method,
|
||||
req.URL.RequestURI(),
|
||||
hex.EncodeToString(bodyHash[:]),
|
||||
timestamp,
|
||||
nonce,
|
||||
}, "\n")
|
||||
req.Header.Set(internalHeaderService, c.config.ServiceName)
|
||||
req.Header.Set(internalHeaderTimestamp, timestamp)
|
||||
req.Header.Set(internalHeaderNonce, nonce)
|
||||
req.Header.Set(internalHeaderSignature, signInternalRequest(c.config.Secret, signingText))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if len(body) > 0 {
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
req.ContentLength = int64(len(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomInternalNonce() (string, error) {
|
||||
var raw [16]byte
|
||||
if _, err := rand.Read(raw[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(raw[:]), nil
|
||||
}
|
||||
|
||||
func signInternalRequest(secret, signingText string) string {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = mac.Write([]byte(signingText))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func (t adminInternalTenantDTO) toStoreTenant() *types.Tenant {
|
||||
if t.ID.IsZero() && strings.TrimSpace(t.Code) == "" {
|
||||
return nil
|
||||
}
|
||||
return &types.Tenant{
|
||||
ID: t.ID,
|
||||
Code: strings.TrimSpace(t.Code),
|
||||
Name: t.Name,
|
||||
TenantDesc: t.TenantDesc,
|
||||
DBName: t.DBName,
|
||||
DBDSN: t.DBDSN,
|
||||
State: t.State,
|
||||
CreatedAt: t.CreatedAt,
|
||||
CreatedBy: t.CreatedBy,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
UpdatedBy: t.UpdatedBy,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user