package alexa import ( "context" "encoding/json" "io" "net/http" "gitea.nik4nao.com/nik/home-services/alexa-bridge/internal/logger" ) // Dispatcher handles one already-validated Alexa request and produces a // response. internal/directive.Router implements this. type Dispatcher interface { Dispatch(ctx context.Context, req Request) (Response, error) } // Handler is the HTTP entrypoint Alexa's servers call into: it verifies the // request signature, timestamp, and skill ID (in that order — cheapest and // most identity-establishing checks first), then hands the decoded envelope // to a Dispatcher. type Handler struct { validator Validator skillID string dispatcher Dispatcher } // NewHandler constructs the HTTP handler for the Alexa endpoint. func NewHandler(validator Validator, skillID string, dispatcher Dispatcher) *Handler { return &Handler{validator: validator, skillID: skillID, dispatcher: dispatcher} } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := logger.FromContext(ctx) body, err := io.ReadAll(r.Body) if err != nil { log.Warn("read request body failed", "err", err) http.Error(w, "bad request", http.StatusBadRequest) return } if err := h.validator.Validate(ctx, r, body); err != nil { log.Warn("signature validation failed", "err", err) http.Error(w, "signature verification failed", http.StatusUnauthorized) return } var req Request if err := json.Unmarshal(body, &req); err != nil { log.Warn("decode request body failed", "err", err) http.Error(w, "bad request", http.StatusBadRequest) return } if err := ValidateTimestamp(req.Request.Timestamp); err != nil { log.Warn("timestamp validation failed", "err", err) http.Error(w, "request timestamp outside tolerance", http.StatusUnauthorized) return } if err := ValidateApplicationID(&req, h.skillID); err != nil { log.Warn("application id validation failed", "err", err) http.Error(w, "application id mismatch", http.StatusUnauthorized) return } resp, err := h.dispatcher.Dispatch(ctx, req) if err != nil { log.Error("dispatch failed", "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { log.Error("encode response failed", "err", err) } }