feat: add im kafka admin integration
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package iminternal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
const (
|
||||
headerService = "X-Service-Name"
|
||||
headerTimestamp = "X-Timestamp"
|
||||
headerNonce = "X-Nonce"
|
||||
headerSignature = "X-Signature"
|
||||
)
|
||||
|
||||
var nonces sync.Map
|
||||
|
||||
func Middleware(r *ghttp.Request) {
|
||||
if !Authorize(r) {
|
||||
return
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func Authorize(r *ghttp.Request) bool {
|
||||
ctx := r.GetCtx()
|
||||
serviceName := strings.TrimSpace(r.Header.Get(headerService))
|
||||
timestampRaw := strings.TrimSpace(r.Header.Get(headerTimestamp))
|
||||
nonce := strings.TrimSpace(r.Header.Get(headerNonce))
|
||||
signature := strings.TrimSpace(r.Header.Get(headerSignature))
|
||||
expectedService := g.Cfg().MustGet(ctx, "imConfigApi.serviceName", "im-server").String()
|
||||
secret := configuredSecret(r)
|
||||
if serviceName == "" || timestampRaw == "" || nonce == "" || signature == "" || serviceName != expectedService || secret == "" {
|
||||
writeAuthError(r, http.StatusUnauthorized, "internal signature required")
|
||||
return false
|
||||
}
|
||||
timestamp, err := strconv.ParseInt(timestampRaw, 10, 64)
|
||||
if err != nil {
|
||||
writeAuthError(r, http.StatusUnauthorized, "invalid internal timestamp")
|
||||
return false
|
||||
}
|
||||
window := g.Cfg().MustGet(ctx, "imConfigApi.timeWindow", "300s").Duration()
|
||||
if window <= 0 {
|
||||
window = 5 * time.Minute
|
||||
}
|
||||
delta := time.Since(time.Unix(timestamp, 0))
|
||||
if delta > window || delta < -window {
|
||||
writeAuthError(r, http.StatusUnauthorized, "internal signature expired")
|
||||
return false
|
||||
}
|
||||
body, err := readBody(r)
|
||||
if err != nil {
|
||||
writeAuthError(r, http.StatusInternalServerError, "read internal body failed")
|
||||
return false
|
||||
}
|
||||
bodyHash := sha256.Sum256(body)
|
||||
signingText := strings.Join([]string{
|
||||
r.Method,
|
||||
r.URL.RequestURI(),
|
||||
hex.EncodeToString(bodyHash[:]),
|
||||
timestampRaw,
|
||||
nonce,
|
||||
}, "\n")
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = mac.Write([]byte(signingText))
|
||||
if !hmac.Equal([]byte(strings.ToLower(signature)), []byte(hex.EncodeToString(mac.Sum(nil)))) {
|
||||
writeAuthError(r, http.StatusUnauthorized, "invalid internal signature")
|
||||
return false
|
||||
}
|
||||
if !claimNonce(serviceName, nonce, time.Now().Add(window)) {
|
||||
writeAuthError(r, http.StatusUnauthorized, "internal nonce replayed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func configuredSecret(r *ghttp.Request) string {
|
||||
ctx := r.GetCtx()
|
||||
secret := g.Cfg().MustGet(ctx, "imConfigApi.serviceSecret").String()
|
||||
if strings.HasPrefix(secret, "${") && strings.HasSuffix(secret, "}") {
|
||||
secret = os.Getenv(strings.TrimSuffix(strings.TrimPrefix(secret, "${"), "}"))
|
||||
}
|
||||
if secret == "" {
|
||||
secret = os.Getenv("IM_CONFIG_API_SECRET")
|
||||
}
|
||||
if secret == "" {
|
||||
secret = g.Cfg().MustGet(ctx, "imInternalApi.serviceSecret").String()
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
func readBody(r *ghttp.Request) ([]byte, error) {
|
||||
if r.Body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func claimNonce(serviceName string, nonce string, expiresAt time.Time) bool {
|
||||
now := time.Now()
|
||||
nonces.Range(func(key, value any) bool {
|
||||
if expires, ok := value.(time.Time); ok && expires.Before(now) {
|
||||
nonces.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
key := serviceName + ":" + nonce
|
||||
_, loaded := nonces.LoadOrStore(key, expiresAt)
|
||||
return !loaded
|
||||
}
|
||||
|
||||
func writeAuthError(r *ghttp.Request, status int, message string) {
|
||||
r.Response.WriteStatus(status)
|
||||
r.Response.WriteJson(g.Map{
|
||||
"code": status,
|
||||
"message": message,
|
||||
})
|
||||
r.Exit()
|
||||
}
|
||||
Reference in New Issue
Block a user