172 lines
6.1 KiB
Go
172 lines
6.1 KiB
Go
// app/router/router.go
|
||
// HTTP router для shared-sqs
|
||
// Updated: 2026-04-09 — добавлены TenantStore, admin API, auth middleware
|
||
package router
|
||
|
||
import (
|
||
"encoding/json"
|
||
"encoding/xml"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"shared-sqs/app/admin"
|
||
"shared-sqs/app/auth"
|
||
sqs "shared-sqs/app/gosqs"
|
||
"shared-sqs/app/interfaces"
|
||
"shared-sqs/app/tenant"
|
||
"shared-sqs/app/ui"
|
||
|
||
"github.com/gorilla/mux"
|
||
log "github.com/sirupsen/logrus"
|
||
)
|
||
|
||
// New — создаёт HTTP router с tenant auth и admin API
|
||
func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler {
|
||
r := mux.NewRouter()
|
||
|
||
// Fix #3.5: глобальный лимит размера тела HTTP запроса — 3MB
|
||
// Защита от OOM при отправке гигантских batch/message requests
|
||
r.Use(requestBodyLimitMiddleware)
|
||
|
||
// /health — публичный, без auth
|
||
r.HandleFunc("/health", health).Methods("GET")
|
||
|
||
// Admin API — Bearer token auth, регистрируется через AdminHandler
|
||
adminHandler := admin.NewHandler(tenantStore, adminToken)
|
||
adminHandler.RegisterRoutes(r)
|
||
|
||
// UI public API — JWT auth, для встроенной console
|
||
// ВАЖНО: RegisterPublicRoutes ПЕРЕД static handler — иначе PathPrefix("/ui") перехватит /ui/api/*
|
||
adminHandler.RegisterPublicRoutes(r)
|
||
|
||
// UI console — встроенный SPA, публичный доступ
|
||
// НЕ ловит /ui/api/* — mux сначала проверит более специфичные маршруты выше
|
||
r.PathPrefix("/ui").Handler(http.StripPrefix("/ui", ui.Handler()))
|
||
|
||
// SQS API — tenant auth middleware оборачивает каждый handler отдельно.
|
||
// r.NewRoute().Subrouter() с Use() некорректно работает в gorilla/mux v1.8.0
|
||
// при пустом prefix — ответы теряются. Поэтому используем явную обёртку.
|
||
sqsAuth := auth.AuthMiddleware(tenantStore)
|
||
r.Handle("/", sqsAuth(http.HandlerFunc(actionHandler))).Methods("GET", "POST")
|
||
r.Handle("/{account}", sqsAuth(http.HandlerFunc(actionHandler))).Methods("GET", "POST")
|
||
r.Handle("/queue/{queueName}", sqsAuth(http.HandlerFunc(actionHandler))).Methods("GET", "POST")
|
||
r.Handle("/{account}/{queueName}", sqsAuth(http.HandlerFunc(actionHandler))).Methods("GET", "POST")
|
||
|
||
return r
|
||
}
|
||
|
||
// requestBodyLimitMiddleware — ограничивает размер тела HTTP запроса до 3MB.
|
||
// Защита от OOM при отправке гигантских messages/batch (Fix #3.5).
|
||
func requestBodyLimitMiddleware(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Body != nil {
|
||
r.Body = http.MaxBytesReader(w, r.Body, 3*1024*1024) // 3MB
|
||
}
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
|
||
func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, body interfaces.AbstractResponseBody) {
|
||
protocol := resolveProtocol(req)
|
||
switch protocol {
|
||
case AwsJsonProtocol:
|
||
w.Header().Set("x-amzn-RequestId", body.GetRequestId())
|
||
w.Header().Set("Content-Type", "application/x-amz-json-1.0")
|
||
w.WriteHeader(statusCode)
|
||
if body.GetResult() == nil {
|
||
return
|
||
}
|
||
err := json.NewEncoder(w).Encode(body.GetResult())
|
||
if err != nil {
|
||
log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body)
|
||
http.Error(w, "General Error", http.StatusInternalServerError)
|
||
}
|
||
case AwsQueryProtocol:
|
||
w.Header().Set("Content-Type", "application/xml")
|
||
w.WriteHeader(statusCode)
|
||
result, err := xml.Marshal(body)
|
||
if err != nil {
|
||
log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body)
|
||
http.Error(w, "General Error", http.StatusInternalServerError)
|
||
}
|
||
_, _ = w.Write(result)
|
||
}
|
||
}
|
||
|
||
// routingTableV1 — только SQS actions (SNS удалён)
|
||
var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){
|
||
"CreateQueue": sqs.CreateQueueV1,
|
||
"ListQueues": sqs.ListQueuesV1,
|
||
"GetQueueAttributes": sqs.GetQueueAttributesV1,
|
||
"SetQueueAttributes": sqs.SetQueueAttributesV1,
|
||
"SendMessage": sqs.SendMessageV1,
|
||
"ReceiveMessage": sqs.ReceiveMessageV1,
|
||
"ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1,
|
||
"DeleteMessage": sqs.DeleteMessageV1,
|
||
"GetQueueUrl": sqs.GetQueueUrlV1,
|
||
"PurgeQueue": sqs.PurgeQueueV1,
|
||
"DeleteQueue": sqs.DeleteQueueV1,
|
||
"SendMessageBatch": sqs.SendMessageBatchV1,
|
||
"DeleteMessageBatch": sqs.DeleteMessageBatchV1,
|
||
"ChangeMessageVisibilityBatch": sqs.ChangeMessageVisibilityBatchV1,
|
||
"TagQueue": sqs.TagQueueV1,
|
||
"UntagQueue": sqs.UntagQueueV1,
|
||
"ListQueueTags": sqs.ListQueueTagsV1,
|
||
}
|
||
|
||
func health(w http.ResponseWriter, req *http.Request) {
|
||
w.WriteHeader(200)
|
||
fmt.Fprint(w, "OK")
|
||
}
|
||
|
||
func actionHandler(w http.ResponseWriter, req *http.Request) {
|
||
action := extractAction(req)
|
||
log.WithFields(log.Fields{
|
||
"action": action,
|
||
"url": req.URL,
|
||
}).Debug("Handling URL request")
|
||
jsonFn, ok := routingTableV1[action]
|
||
if ok {
|
||
statusCode, responseBody := jsonFn(req)
|
||
encodeResponse(w, req, statusCode, responseBody)
|
||
return
|
||
}
|
||
log.Warnf("Bad Request - Action: %s", action)
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
io.WriteString(w, "Bad Request")
|
||
}
|
||
|
||
type AwsProtocol int
|
||
|
||
const (
|
||
AwsJsonProtocol AwsProtocol = iota
|
||
AwsQueryProtocol AwsProtocol = iota
|
||
)
|
||
|
||
// extractAction — извлекает Action из запроса (Query Protocol или JSON Protocol)
|
||
func extractAction(req *http.Request) string {
|
||
protocol := resolveProtocol(req)
|
||
switch protocol {
|
||
case AwsJsonProtocol:
|
||
action := req.Header.Get("X-Amz-Target")
|
||
parts := strings.SplitN(action, ".", 2)
|
||
if len(parts) != 2 || parts[1] == "" {
|
||
return ""
|
||
}
|
||
return parts[1]
|
||
case AwsQueryProtocol:
|
||
return req.FormValue("Action")
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// resolveProtocol — определяет протокол по Content-Type
|
||
func resolveProtocol(req *http.Request) AwsProtocol {
|
||
if req.Header.Get("Content-Type") == "application/x-amz-json-1.0" {
|
||
return AwsJsonProtocol
|
||
}
|
||
return AwsQueryProtocol
|
||
}
|