feat: add kafka admin integration
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/logs"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
const (
|
||||
internalCapabilityAdminQuery = "admin_query"
|
||||
|
||||
internalHeaderService = "X-Service-Name"
|
||||
internalHeaderTimestamp = "X-Timestamp"
|
||||
internalHeaderNonce = "X-Nonce"
|
||||
internalHeaderSignature = "X-Signature"
|
||||
)
|
||||
|
||||
type internalAPIConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
TimeWindowSeconds int `json:"time_window_seconds"`
|
||||
Services map[string]internalAPIServiceConfig `json:"services"`
|
||||
}
|
||||
|
||||
type internalAPIServiceConfig struct {
|
||||
Secret string `json:"secret"`
|
||||
Allow []string `json:"allow"`
|
||||
}
|
||||
|
||||
type internalAPIRuntime struct {
|
||||
config internalAPIConfig
|
||||
nonces sync.Map
|
||||
}
|
||||
|
||||
var internalAPI internalAPIRuntime
|
||||
|
||||
func initInternalAPI(raw json.RawMessage) error {
|
||||
internalAPI = internalAPIRuntime{}
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var cfg internalAPIConfig
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.TimeWindowSeconds <= 0 {
|
||||
cfg.TimeWindowSeconds = 300
|
||||
}
|
||||
if cfg.Services == nil {
|
||||
cfg.Services = map[string]internalAPIServiceConfig{}
|
||||
}
|
||||
internalAPI.config = cfg
|
||||
if cfg.Enabled {
|
||||
logs.Info.Printf("Internal API enabled for %d service(s)", len(cfg.Services))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix string) {
|
||||
if req.Method != http.MethodGet {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
if !authorizeInternalAPI(wrt, req, internalCapabilityAdminQuery) {
|
||||
return
|
||||
}
|
||||
|
||||
tail := strings.Trim(strings.TrimPrefix(req.URL.Path, prefix), "/")
|
||||
parts := splitInternalPath(tail)
|
||||
if len(parts) < 3 || parts[0] != "tenants" {
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "resource not found", nil)
|
||||
return
|
||||
}
|
||||
tenant, ok := resolveInternalTenant(wrt, req, parts[1])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(parts) == 3 && parts[2] == "users":
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "users", "")
|
||||
case len(parts) == 4 && parts[2] == "users":
|
||||
serveInternalUserQuery(wrt, req, tenant.ID, parts[3])
|
||||
case len(parts) == 5 && parts[2] == "users" && parts[4] == "devices":
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "user_devices", parts[3])
|
||||
case len(parts) == 3 && parts[2] == "groups":
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "groups", "")
|
||||
case len(parts) == 4 && parts[2] == "groups":
|
||||
serveInternalGroupQuery(wrt, req, tenant.ID, parts[3])
|
||||
case len(parts) == 5 && parts[2] == "groups" && parts[4] == "members":
|
||||
serveInternalGroupMembersQuery(wrt, req, tenant.ID, parts[3])
|
||||
case len(parts) == 5 && parts[2] == "groups" && internalGroupResourceSpecs[parts[4]] != nil:
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "group_"+parts[4], parts[3])
|
||||
case len(parts) == 5 && parts[2] == "messages":
|
||||
serveInternalMessageQuery(wrt, req, tenant.ID, parts[3], parts[4])
|
||||
case len(parts) == 3 && parts[2] == "files":
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "files", "")
|
||||
case len(parts) == 4 && parts[2] == "files":
|
||||
serveInternalFileQuery(wrt, req, tenant.ID, parts[3])
|
||||
case len(parts) == 5 && parts[2] == "files" && parts[4] == "links":
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "file_links", parts[3])
|
||||
case len(parts) == 3 && internalTenantResourceSpecs[parts[2]] != nil:
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, parts[2], "")
|
||||
case len(parts) == 4 && internalTenantResourceSpecs[parts[2]] != nil:
|
||||
serveInternalAdminResourceDetail(wrt, req, tenant.ID, parts[2], parts[3])
|
||||
case len(parts) == 4 && parts[2] == "reports" && parts[3] == "abuse":
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "resource not found", nil)
|
||||
case len(parts) == 6 && parts[2] == "reports" && parts[3] == "abuse" && parts[5] == "target":
|
||||
serveInternalAbuseTargetQuery(wrt, req, tenant.ID, parts[4])
|
||||
case len(parts) == 4 && parts[2] == "outbox" && internalOutboxResourceSpecs[parts[3]] != nil:
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "outbox_"+parts[3], "")
|
||||
case len(parts) == 5 && parts[2] == "outbox" && internalOutboxResourceSpecs[parts[3]] != nil:
|
||||
serveInternalAdminResourceDetail(wrt, req, tenant.ID, "outbox_"+parts[3], parts[4])
|
||||
case len(parts) == 4 && parts[2] == "org" && internalOrgResourceSpecs[parts[3]] != nil:
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "org_"+parts[3], "")
|
||||
case len(parts) == 4 && parts[2] == "workbench" && parts[3] == "apps":
|
||||
serveInternalAdminResourceList(wrt, req, tenant.ID, "workbench/apps", "")
|
||||
case len(parts) == 4 && parts[2] == "dashboard" && parts[3] == "summary":
|
||||
serveInternalDashboardSummary(wrt, req, tenant.ID)
|
||||
case len(parts) == 4 && parts[2] == "kafka" && parts[3] == "dead-letters":
|
||||
serveInternalKafkaDeadLettersQuery(wrt, req, tenant.ID)
|
||||
case len(parts) == 5 && parts[2] == "kafka" && parts[3] == "dead-letters":
|
||||
serveInternalKafkaDeadLetterQuery(wrt, req, tenant.ID, parts[4])
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "resource not found", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeInternalAPI(wrt http.ResponseWriter, req *http.Request, capability string) bool {
|
||||
if !internalAPI.config.Enabled {
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "resource not found", nil)
|
||||
return false
|
||||
}
|
||||
serviceName := strings.TrimSpace(req.Header.Get(internalHeaderService))
|
||||
timestampRaw := strings.TrimSpace(req.Header.Get(internalHeaderTimestamp))
|
||||
nonce := strings.TrimSpace(req.Header.Get(internalHeaderNonce))
|
||||
signature := strings.TrimSpace(req.Header.Get(internalHeaderSignature))
|
||||
service, ok := internalAPI.config.Services[serviceName]
|
||||
if !ok || service.Secret == "" || timestampRaw == "" || nonce == "" || signature == "" {
|
||||
writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "internal signature required", nil)
|
||||
return false
|
||||
}
|
||||
if !internalServiceAllows(service, capability) {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
|
||||
return false
|
||||
}
|
||||
timestamp, err := strconv.ParseInt(timestampRaw, 10, 64)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "invalid internal timestamp", nil)
|
||||
return false
|
||||
}
|
||||
window := time.Duration(internalAPI.config.TimeWindowSeconds) * time.Second
|
||||
if delta := time.Since(time.Unix(timestamp, 0)); delta > window || delta < -window {
|
||||
writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "internal signature expired", nil)
|
||||
return false
|
||||
}
|
||||
bodyHash, err := hashInternalRequestBody(req)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return false
|
||||
}
|
||||
signingText := strings.Join([]string{
|
||||
req.Method,
|
||||
req.URL.RequestURI(),
|
||||
bodyHash,
|
||||
timestampRaw,
|
||||
nonce,
|
||||
}, "\n")
|
||||
if !verifyInternalSignature(service.Secret, signingText, signature) {
|
||||
writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "invalid internal signature", nil)
|
||||
return false
|
||||
}
|
||||
if !claimInternalNonce(serviceName, nonce, time.Now().Add(window)) {
|
||||
writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "internal nonce replayed", nil)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func splitInternalPath(tail string) []string {
|
||||
if tail == "" {
|
||||
return nil
|
||||
}
|
||||
rawParts := strings.Split(tail, "/")
|
||||
parts := make([]string, 0, len(rawParts))
|
||||
for _, part := range rawParts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
decoded, err := url.PathUnescape(part)
|
||||
if err != nil {
|
||||
decoded = part
|
||||
}
|
||||
parts = append(parts, decoded)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func resolveInternalTenant(wrt http.ResponseWriter, req *http.Request, tenantCode string) (*types.Tenant, bool) {
|
||||
if tenantCode == "" || store.Tenants == nil {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "tenant_code"})
|
||||
return nil, false
|
||||
}
|
||||
tenant, err := store.Tenants.GetByCode(tenantCode)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrNotFound) {
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30010, imReasonTenantUnavailable, "tenant unavailable", nil)
|
||||
return nil, false
|
||||
}
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return nil, false
|
||||
}
|
||||
if !tenant.IsActive() {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30010, imReasonTenantUnavailable, "tenant unavailable", nil)
|
||||
return nil, false
|
||||
}
|
||||
return tenant, true
|
||||
}
|
||||
|
||||
func serveInternalUserQuery(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID, userID string) {
|
||||
userDBID, ok := parseInternalAdminDBID(userID)
|
||||
if !ok {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "user_id"})
|
||||
return
|
||||
}
|
||||
if spec, ok := internalAdminQuerySpec("users"); ok {
|
||||
query := spec.Select + " " + spec.From + " WHERE u.id=?"
|
||||
if row, err := store.InternalAdminQueries.QueryOne(tenantID, query, []any{userDBID}); err == nil {
|
||||
writeIMSuccess(wrt, req, normalizeInternalAdminRowForAPI(row))
|
||||
return
|
||||
}
|
||||
}
|
||||
uid := store.EncodeUid(userDBID)
|
||||
user, err := store.Users.Get(tenantID, uid)
|
||||
if err != nil {
|
||||
writeInternalQueryError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"user_id": userDBID,
|
||||
"state": user.State,
|
||||
"state_at": user.StateAt,
|
||||
"username": user.Username,
|
||||
"phone": user.Phone,
|
||||
"public": user.Public,
|
||||
"trusted": user.Trusted,
|
||||
"last_seen": user.LastSeen,
|
||||
"user_agent": user.UserAgent,
|
||||
"tags": user.Tags,
|
||||
"created_at": user.CreatedAt,
|
||||
"updated_at": user.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func serveInternalGroupQuery(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID, topic string) {
|
||||
if topic == "" || store.GroupBusiness == nil {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
}
|
||||
if spec, ok := internalAdminQuerySpec("groups"); ok {
|
||||
query := spec.Select + " " + spec.From + " WHERE t.name=?"
|
||||
if row, err := store.InternalAdminQueries.QueryOne(tenantID, query, []any{topic}); err == nil {
|
||||
writeIMSuccess(wrt, req, normalizeInternalAdminRowForAPI(row))
|
||||
return
|
||||
}
|
||||
}
|
||||
settings, err := store.GroupBusiness.GetSettings(tenantID, topic)
|
||||
if err != nil {
|
||||
writeInternalQueryError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, internalGroupSettingsPayload(settings))
|
||||
}
|
||||
|
||||
func serveInternalGroupMembersQuery(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID, topic string) {
|
||||
if topic == "" || store.GroupBusiness == nil {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
}
|
||||
spec := &internalQuerySpec{
|
||||
Select: `SELECT gm.id,gm.topic,gm.user_id,u.state,u.public,p.nickname,p.public_account,s.modewant AS mode_want,s.modegiven AS mode_given,s.recvseqid AS recv_seq_id,s.readseqid AS read_seq_id,s.delid AS del_id,
|
||||
gm.role,gm.member_status,gm.nickname AS group_nickname,gm.pinned,gm.mute_notifications,gm.join_source,gm.joined_by,gm.joined_at,gm.left_at,gm.removed_by,gm.version,gm.created_at,gm.updated_at`,
|
||||
From: "FROM im_group_members AS gm LEFT JOIN im_users AS u ON u.id=gm.user_id LEFT JOIN im_user_profiles AS p ON p.user_id=gm.user_id LEFT JOIN im_subscriptions AS s ON s.topic=gm.topic AND s.userid=gm.user_id",
|
||||
DefaultSort: "ORDER BY gm.id DESC",
|
||||
ScopeColumn: "gm.topic",
|
||||
Filters: []internalQueryFilter{
|
||||
{"user_id", "gm.user_id", "=", "uid"},
|
||||
{"role", "gm.role", "=", "string"},
|
||||
{"member_status", "gm.member_status", "=", "string"},
|
||||
{"q", "gm.nickname", "LIKE", "string"},
|
||||
{"eligible_role", "gm.role", "=", "string"},
|
||||
},
|
||||
}
|
||||
query, countQuery, args, page, perPage, err := buildInternalAdminListQuery(req, spec, topic)
|
||||
if err == nil {
|
||||
if result, queryErr := store.InternalAdminQueries.Query(tenantID, query, countQuery, args, page, perPage); queryErr == nil {
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"items": normalizeInternalAdminRowsForAPI(result.Items),
|
||||
"total": result.Total,
|
||||
"page": result.Page,
|
||||
"per_page": result.PerPage,
|
||||
"next_cursor": "",
|
||||
"has_more": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
limit := parseIMLimit(req.URL.Query().Get("limit"), 50, 200)
|
||||
members, nextCursor, err := store.GroupBusiness.ListMembers(tenantID, topic, types.GroupMemberListOptions{
|
||||
Role: strings.TrimSpace(req.URL.Query().Get("role")),
|
||||
EligibleRole: strings.TrimSpace(req.URL.Query().Get("eligible_role")),
|
||||
Query: strings.TrimSpace(req.URL.Query().Get("q")),
|
||||
Cursor: strings.TrimSpace(req.URL.Query().Get("cursor")),
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
writeInternalQueryError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
items := make([]any, 0, len(members))
|
||||
for _, member := range members {
|
||||
items = append(items, map[string]any{
|
||||
"user_id": internalAdminUIDRaw(member.UserID),
|
||||
"role": member.Role,
|
||||
"member_status": member.MemberStatus,
|
||||
"nickname": member.Nickname,
|
||||
"pinned": member.Pinned,
|
||||
"mute_notifications": member.MuteNotifications,
|
||||
"join_source": member.JoinSource,
|
||||
"joined_by": internalAdminUIDRaw(member.JoinedBy),
|
||||
"joined_at": member.JoinedAt,
|
||||
"left_at": member.LeftAt,
|
||||
"removed_by": internalAdminUIDRaw(member.RemovedBy),
|
||||
"version": member.Version,
|
||||
"created_at": member.CreatedAt,
|
||||
"updated_at": member.UpdatedAt,
|
||||
})
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"items": items, "next_cursor": nextCursor})
|
||||
}
|
||||
|
||||
func serveInternalMessageQuery(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID, topic, seqRaw string) {
|
||||
seq, err := strconv.ParseInt(seqRaw, 10, 64)
|
||||
if topic == "" || err != nil || seq <= 0 {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
return
|
||||
}
|
||||
msg, err := store.Messages.GetOne(tenantID, topic, seq)
|
||||
if err != nil {
|
||||
writeInternalQueryError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"topic": msg.Topic,
|
||||
"seq_id": msg.SeqId,
|
||||
"from": internalAdminUIDStringRaw(msg.From),
|
||||
"client_msg_id": msg.ClientMsgID,
|
||||
"version": msg.Version,
|
||||
"head": msg.Head,
|
||||
"content": msg.Content,
|
||||
"deleted_at": msg.DeletedAt,
|
||||
"recalled_at": msg.RecalledAt,
|
||||
"recalled_by": internalAdminUIDStringRaw(msg.RecalledBy),
|
||||
"created_at": msg.CreatedAt,
|
||||
"updated_at": msg.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func serveInternalFileQuery(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID, fileID string) {
|
||||
if fileID == "" || store.Files == nil {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "file_id"})
|
||||
return
|
||||
}
|
||||
file, err := store.Files.Get(tenantID, fileID)
|
||||
if err != nil {
|
||||
writeInternalQueryError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"file_id": file.Id,
|
||||
"status": file.Status,
|
||||
"user_id": internalAdminUIDStringRaw(file.User),
|
||||
"mime_type": file.MimeType,
|
||||
"size": file.Size,
|
||||
"location": file.Location,
|
||||
"etag": file.ETag,
|
||||
"created_at": file.CreatedAt,
|
||||
"updated_at": file.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func serveInternalKafkaDeadLettersQuery(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID) {
|
||||
if store.HTTPInfrastructure == nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
limit := parseIMLimit(req.URL.Query().Get("limit"), 50, 200)
|
||||
page, err := store.HTTPInfrastructure.ListKafkaDeadLetters(tenantID, types.KafkaDeadLetterListOptions{
|
||||
Status: strings.TrimSpace(req.URL.Query().Get("status")),
|
||||
BizType: strings.TrimSpace(req.URL.Query().Get("biz_type")),
|
||||
BizID: strings.TrimSpace(req.URL.Query().Get("biz_id")),
|
||||
EventType: strings.TrimSpace(req.URL.Query().Get("event_type")),
|
||||
Cursor: strings.TrimSpace(req.URL.Query().Get("cursor")),
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
writeInternalQueryError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
items := make([]any, 0, len(page.Items))
|
||||
for _, dead := range page.Items {
|
||||
items = append(items, internalKafkaDeadLetterPayload(&dead, false))
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"items": items,
|
||||
"next_cursor": page.NextCursor,
|
||||
"has_more": page.NextCursor != "",
|
||||
})
|
||||
}
|
||||
|
||||
func serveInternalKafkaDeadLetterQuery(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID, deadID string) {
|
||||
if store.HTTPInfrastructure == nil || strings.TrimSpace(deadID) == "" {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "dead_id"})
|
||||
return
|
||||
}
|
||||
dead, err := store.HTTPInfrastructure.GetKafkaDeadLetter(tenantID, deadID)
|
||||
if err != nil {
|
||||
writeInternalQueryError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, internalKafkaDeadLetterPayload(dead, true))
|
||||
}
|
||||
|
||||
func internalGroupSettingsPayload(settings *types.GroupSettings) map[string]any {
|
||||
if settings == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"topic": settings.Topic,
|
||||
"owner_user_id": internalAdminUIDRaw(settings.OwnerUserID),
|
||||
"name": settings.Name,
|
||||
"avatar_file_id": settings.AvatarFileID,
|
||||
"description": settings.Description,
|
||||
"group_status": settings.GroupStatus,
|
||||
"join_approval": settings.JoinApproval,
|
||||
"mute_all": settings.MuteAll,
|
||||
"member_upload_files": settings.MemberUploadFiles,
|
||||
"member_private_chat": settings.MemberPrivateChat,
|
||||
"member_invite": settings.MemberInvite,
|
||||
"max_member_count": settings.MaxMemberCount,
|
||||
"max_admin_count": settings.MaxAdminCount,
|
||||
"member_count": settings.MemberCount,
|
||||
"version": settings.Version,
|
||||
"created_by": internalAdminUIDRaw(settings.CreatedBy),
|
||||
"dissolved_by": internalAdminUIDRaw(settings.DissolvedBy),
|
||||
"dissolved_at": settings.DissolvedAt,
|
||||
"created_at": settings.CreatedAt,
|
||||
"updated_at": settings.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func internalKafkaDeadLetterPayload(dead *types.KafkaDeadLetter, includeEnvelope bool) map[string]any {
|
||||
if dead == nil {
|
||||
return nil
|
||||
}
|
||||
payload := map[string]any{
|
||||
"id": dead.ID,
|
||||
"dead_id": dead.DeadID,
|
||||
"event_id": dead.EventID,
|
||||
"parent_event_id": dead.ParentEventID,
|
||||
"source_topic": dead.SourceTopic,
|
||||
"consumer_group": dead.ConsumerGroup,
|
||||
"event_type": dead.EventType,
|
||||
"biz_type": dead.BizType,
|
||||
"biz_id": dead.BizID,
|
||||
"retry_count": dead.RetryCount,
|
||||
"last_reason": dead.LastReason,
|
||||
"last_error": dead.LastError,
|
||||
"status": dead.Status,
|
||||
"replay_count": dead.ReplayCount,
|
||||
"created_at": dead.CreatedAt,
|
||||
"updated_at": dead.UpdatedAt,
|
||||
}
|
||||
if includeEnvelope {
|
||||
var envelope any
|
||||
if len(dead.Envelope) > 0 && json.Unmarshal(dead.Envelope, &envelope) == nil {
|
||||
payload["envelope"] = envelope
|
||||
} else {
|
||||
payload["envelope_raw"] = string(dead.Envelope)
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func writeInternalQueryError(wrt http.ResponseWriter, req *http.Request, err error) {
|
||||
switch {
|
||||
case errors.Is(err, types.ErrUserNotFound), errors.Is(err, types.ErrTopicNotFound), errors.Is(err, types.ErrNotFound):
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "resource not found", nil)
|
||||
case errors.Is(err, types.ErrMalformed):
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
case errors.Is(err, types.ErrPermissionDenied):
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func parseInternalUid(raw string) types.Uid {
|
||||
if uid := types.ParseUserId(raw); !uid.IsZero() {
|
||||
return uid
|
||||
}
|
||||
id, ok := parseInternalAdminDBID(raw)
|
||||
if !ok {
|
||||
return types.ZeroUid
|
||||
}
|
||||
return store.EncodeUid(id)
|
||||
}
|
||||
|
||||
func internalServiceAllows(service internalAPIServiceConfig, capability string) bool {
|
||||
for _, allowed := range service.Allow {
|
||||
allowed = strings.TrimSpace(allowed)
|
||||
if allowed == "*" || allowed == capability {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hashInternalRequestBody(req *http.Request) (string, error) {
|
||||
if req.Body == nil {
|
||||
sum := sha256.Sum256(nil)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(req.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := req.Body.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func verifyInternalSignature(secret, signingText, signature string) bool {
|
||||
signature = strings.TrimPrefix(signature, "sha256=")
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = mac.Write([]byte(signingText))
|
||||
expected := mac.Sum(nil)
|
||||
if decoded, err := hex.DecodeString(signature); err == nil {
|
||||
return hmac.Equal(decoded, expected)
|
||||
}
|
||||
if decoded, err := base64.StdEncoding.DecodeString(signature); err == nil {
|
||||
return hmac.Equal(decoded, expected)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func claimInternalNonce(serviceName, nonce string, expiresAt time.Time) bool {
|
||||
ttl := time.Until(expiresAt)
|
||||
if ok, enabled, err := store.ClaimShortState("internal_api_nonce", fmt.Sprintf("%s:%s", serviceName, nonce), ttl); enabled {
|
||||
if err != nil {
|
||||
logs.Warn.Println("internal api: failed to claim nonce in redis", err)
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
now := time.Now()
|
||||
internalAPI.nonces.Range(func(key, value any) bool {
|
||||
if expires, ok := value.(time.Time); ok && expires.Before(now) {
|
||||
internalAPI.nonces.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
nonceKey := fmt.Sprintf("%s:%s", serviceName, nonce)
|
||||
_, loaded := internalAPI.nonces.LoadOrStore(nonceKey, expiresAt)
|
||||
return !loaded
|
||||
}
|
||||
Reference in New Issue
Block a user