chore: initial import from sless/shared-sqs (v0.1.14)
- Standalone SQS-service repository - Multi-tenant message queue service, AWS SQS compatible - Based on GoAws, with mutable tenants, auth, WebUI, Redis persistence - Ready for independent development and deployment - See doc/ and README.md for architecture and usage
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// 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()
|
||||
|
||||
// /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 — без auth, для встроенной console
|
||||
adminHandler.RegisterPublicRoutes(r)
|
||||
|
||||
// UI console — встроенный SPA, публичный доступ
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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")
|
||||
return strings.Split(action, ".")[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
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
af "shared-sqs/app/fixtures"
|
||||
|
||||
"shared-sqs/app/mocks"
|
||||
|
||||
"shared-sqs/app/interfaces"
|
||||
|
||||
sqs "shared-sqs/app/gosqs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"shared-sqs/app/test"
|
||||
)
|
||||
|
||||
func TestIndexServerhandler_POST_BadRequest(t *testing.T) {
|
||||
// Create a request to pass to our handler. We don't have any query parameters for now, so we'll
|
||||
// pass 'nil' as the third parameter.
|
||||
req, err := http.NewRequest("POST", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("Action", "BadRequest")
|
||||
req.PostForm = form
|
||||
|
||||
// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
|
||||
// directly and pass in our Request and ResponseRecorder.
|
||||
New().ServeHTTP(rr, req)
|
||||
|
||||
// Check the status code is what we expect.
|
||||
if status := rr.Code; status != http.StatusBadRequest {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexServerhandler_POST_GoodRequest(t *testing.T) {
|
||||
// Create a request to pass to our handler. We don't have any query parameters for now, so we'll
|
||||
// pass 'nil' as the third parameter.
|
||||
req, err := http.NewRequest("POST", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("Action", "ListTopics")
|
||||
req.PostForm = form
|
||||
|
||||
// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
|
||||
// directly and pass in our Request and ResponseRecorder.
|
||||
New().ServeHTTP(rr, req)
|
||||
|
||||
// Check the status code is what we expect.
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexServerhandler_POST_GoodRequest_With_URL(t *testing.T) {
|
||||
req, err := http.NewRequest("POST", "/100010001000/local-queue1", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("Action", "CreateQueue")
|
||||
form.Add("QueueName", "local-queue1")
|
||||
req.PostForm = form
|
||||
rr := httptest.NewRecorder()
|
||||
New().ServeHTTP(rr, req)
|
||||
|
||||
form = url.Values{}
|
||||
form.Add("Action", "GetQueueAttributes")
|
||||
form.Add("QueueUrl", fmt.Sprintf("%s/local-queue1", af.BASE_URL))
|
||||
req.PostForm = form
|
||||
|
||||
// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
|
||||
rr = httptest.NewRecorder()
|
||||
|
||||
// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
|
||||
// directly and pass in our Request and ResponseRecorder.
|
||||
New().ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestIndexServerhandler_POST_GoodRequest_With_URL_And_Aws_Json_Protocol(t *testing.T) {
|
||||
json, _ := json.Marshal(map[string]string{
|
||||
"QueueName": "local-queue1",
|
||||
})
|
||||
req, err := http.NewRequest("POST", "/100010001000/local-queue1", bytes.NewBuffer(json))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("X-Amz-Target", "AmazonSQS.CreateQueue")
|
||||
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
New().ServeHTTP(rr, req)
|
||||
|
||||
// Check the status code is what we expect.
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexServerhandler_GET_GoodRequest_Pem_cert(t *testing.T) {
|
||||
|
||||
req, err := http.NewRequest("GET", "/SimpleNotificationService/100010001000.pem", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
New().ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeResponse_success_xml(t *testing.T) {
|
||||
w, r := test.GenerateRequestInfo("POST", "/url", nil, false)
|
||||
|
||||
encodeResponse(w, r, http.StatusOK, mocks.BaseResponse{Message: "test"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
tmp := mocks.BaseResponse{}
|
||||
xml.Unmarshal(w.Body.Bytes(), &tmp)
|
||||
assert.Equal(t, mocks.BaseResponse{Message: "test"}, tmp)
|
||||
}
|
||||
|
||||
func TestEncodeResponse_success_skips_nil_body_xml(t *testing.T) {
|
||||
w, r := test.GenerateRequestInfo("POST", "/url", nil, false)
|
||||
|
||||
encodeResponse(w, r, http.StatusOK, nil)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, &bytes.Buffer{}, w.Body)
|
||||
}
|
||||
|
||||
func TestEncodeResponse_success_json(t *testing.T) {
|
||||
w, r := test.GenerateRequestInfo("POST", "/url", nil, true)
|
||||
|
||||
encodeResponse(w, r, http.StatusOK, mocks.BaseResponse{Message: "test"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
tmp := mocks.BaseResponse{}
|
||||
json.Unmarshal(w.Body.Bytes(), &tmp)
|
||||
assert.Equal(t, mocks.BaseResponse{Message: "test"}, tmp)
|
||||
}
|
||||
|
||||
func TestEncodeResponse_success_skips_malformed_body_json(t *testing.T) {
|
||||
mock := mocks.BaseResponse{
|
||||
Message: "test",
|
||||
}
|
||||
mock.MockGetResult = func() interface{} {
|
||||
return make(chan int)
|
||||
}
|
||||
w, r := test.GenerateRequestInfo("POST", "/url", nil, true)
|
||||
|
||||
encodeResponse(w, r, http.StatusOK, mock)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "General Error", strings.TrimSpace(string(w.Body.Bytes())))
|
||||
}
|
||||
|
||||
func TestActionHandler_v1_json(t *testing.T) {
|
||||
defer func() {
|
||||
routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){
|
||||
"CreateQueue": sqs.CreateQueueV1,
|
||||
}
|
||||
}()
|
||||
|
||||
mockCalled := false
|
||||
mockFunction := func(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
mockCalled = true
|
||||
return http.StatusOK, mocks.BaseResponse{Message: "response-body"}
|
||||
}
|
||||
routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){
|
||||
"CreateQueue": mockFunction,
|
||||
}
|
||||
|
||||
w, r := test.GenerateRequestInfo("POST", "/url", nil, true)
|
||||
r.Header.Set("X-Amz-Target", "QueueService.CreateQueue")
|
||||
|
||||
actionHandler(w, r)
|
||||
|
||||
assert.True(t, mockCalled)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
tmp := mocks.BaseResponse{}
|
||||
json.Unmarshal(w.Body.Bytes(), &tmp)
|
||||
assert.Equal(t, mocks.BaseResponse{Message: "response-body"}, tmp)
|
||||
}
|
||||
|
||||
func TestActionHandler_v1_xml(t *testing.T) {
|
||||
defer func() {
|
||||
routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){
|
||||
"CreateQueue": sqs.CreateQueueV1,
|
||||
}
|
||||
}()
|
||||
|
||||
mockCalled := false
|
||||
mockFunction := func(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
mockCalled = true
|
||||
return http.StatusOK, mocks.BaseResponse{Message: "response-body"}
|
||||
}
|
||||
routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){
|
||||
"CreateQueue": mockFunction,
|
||||
}
|
||||
|
||||
w, r := test.GenerateRequestInfo("POST", "/url", nil, false)
|
||||
form := url.Values{}
|
||||
form.Add("Action", "CreateQueue")
|
||||
r.PostForm = form
|
||||
|
||||
actionHandler(w, r)
|
||||
|
||||
assert.True(t, mockCalled)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
tmp := mocks.BaseResponse{}
|
||||
xml.Unmarshal(w.Body.Bytes(), &tmp)
|
||||
assert.Equal(t, mocks.BaseResponse{Message: "response-body"}, tmp)
|
||||
}
|
||||
Reference in New Issue
Block a user