package event_ingress import ( "errors" "io" "net/http" "regexp" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "gorm.io/gorm" "go-admin/app/bell/integration/machine_identity" ) const MaxRequestBytes = 64 * 1024 var requestIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{15,127}$`) type Handler struct { DB *gorm.DB Verifier machine_identity.Verifier Enabled bool Resolver EvidenceRefresher } func (h Handler) Post(c *gin.Context) { if !h.Enabled { writeProblem(c, http.StatusServiceUnavailable, "connector_disabled", "event connector is disabled", "") return } requestID := c.GetHeader("X-Request-ID") if requestID == "" { requestID = uuid.NewString() } else if !requestIDPattern.MatchString(requestID) { writeProblem(c, http.StatusBadRequest, "invalid_request_id", "X-Request-ID must be an opaque 16-128 character value", "") return } c.Header("X-Request-ID", requestID) if c.Request.URL.RawQuery != "" || c.Request.URL.Fragment != "" || c.Request.URL.EscapedPath() != "/v1/events" { writeProblem(c, http.StatusBadRequest, "invalid_request_target", "event request target must be the normalized /v1/events path", "") return } if relayHeader := c.GetHeader("X-YoVision-Relay-ID"); relayHeader != "" { relayID := strings.TrimSpace(relayHeader) if relayID != relayHeader || !validID(relayID) { writeProblem(c, http.StatusBadRequest, "invalid_event", "relay identity header is invalid", "") return } } body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, MaxRequestBytes)) if err != nil { writeProblem(c, http.StatusBadRequest, "invalid_event", "event payload is invalid or too large", "") return } token, err := machine_identity.BearerToken(c.GetHeader("Authorization")) if err != nil { writeProblem(c, http.StatusUnauthorized, machineErrorCode(err), "machine identity was rejected", "") return } if _, err = h.Verifier.Verify(token, "yovision-bell", "events:ingest", c.Request.Method, c.Request.URL.EscapedPath(), body); err != nil { status := http.StatusUnauthorized code := machineErrorCode(err) if code == "machine_scope_denied" || code == "machine_audience_denied" { status = http.StatusForbidden } writeProblem(c, status, code, "machine identity was rejected", "") return } parsed, err := ParseEvent(body) if err != nil { if errors.Is(err, ErrUnsupportedSchema) { writeProblem(c, http.StatusUnprocessableEntity, "unsupported_schema_version", "event schema version is unsupported", "") return } writeProblem(c, http.StatusBadRequest, "invalid_event", "event payload failed validation", "") return } result, err := (Service{DB: h.DB, Resolver: h.Resolver}).Ingest(c.Request.Context(), parsed) if errors.Is(err, ErrIdempotencyConflict) { writeProblem(c, http.StatusConflict, "idempotency_conflict", "idempotency key is already bound to another payload", result.EventID) return } if err != nil { writeProblem(c, http.StatusServiceUnavailable, "ingest_unavailable", "event ingest is temporarily unavailable", "") return } status := http.StatusCreated if result.Disposition == "duplicate" { status = http.StatusOK } c.JSON(status, result) } func writeProblem(c *gin.Context, status int, code, message, existing string) { c.Header("Content-Type", "application/problem+json") c.JSON(status, Problem{Code: code, Message: message, ExistingEventID: existing}) } func machineErrorCode(err error) string { var machineErr *machine_identity.Error if errors.As(err, &machineErr) { return machineErr.Code } return "machine_token_invalid" }