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 == "" || !isAllowedService(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 isAllowedService(serviceName, expectedServices string) bool { serviceName = strings.TrimSpace(serviceName) if serviceName == "" { return false } for _, expected := range strings.Split(expectedServices, ",") { if serviceName == strings.TrimSpace(expected) { return true } } return false } 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() }