Files
im_manager/server/internal/library/iminternal/auth.go
T

136 lines
3.5 KiB
Go

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()
}