136 lines
3.5 KiB
Go
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()
|
|
}
|