v0.1.35: query-mode XML-ответы, TOCTOU receive, валидации AWS, purge 60s, сужение локов

This commit is contained in:
“Naeel”
2026-08-14 15:53:08 +04:00
parent 8c40ca592f
commit ec10acc99e
15 changed files with 919 additions and 172 deletions
+63 -15
View File
@@ -7,7 +7,6 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"strings"
@@ -103,10 +102,35 @@ func requestBodyLimitMiddleware(next http.Handler) http.Handler {
})
}
// encodeResponse — сериализует ответ в формате протокола клиента.
//
// Логическая схема:
// - AwsJsonProtocol (чистый JSON-протокол): успешный ответ — JSON тела;
// ОШИБКА — спец-формат AWS-JSON {"__type": "com.amazonaws.sqs#<Code>",
// "message": "..."}. Раньше для ErrorResponse писался голый ErrorResult
// ({"Type":...,"Code":...,"Message":...}) — SDK не распознавал это как ошибку
// и возвращал Code=None.
// - AwsQueryProtocol: ответ всегда XML (в т.ч. <ErrorResponse> для ошибок).
func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, body interfaces.AbstractResponseBody) {
protocol := resolveProtocol(req)
switch protocol {
case AwsJsonProtocol:
// Ошибка в JSON-протоколе: __type + message — единственный формат,
// который botocore распознаёт как исключение с кодом.
if errResp, ok := body.(models.ErrorResponse); ok {
w.Header().Set("x-amzn-RequestId", errResp.RequestId)
w.Header().Set("Content-Type", "application/x-amz-json-1.0")
w.WriteHeader(statusCode)
err := json.NewEncoder(w).Encode(map[string]string{
"__type": "com.amazonaws.sqs#" + errResp.Result.Code,
"message": errResp.Result.Message,
})
if err != nil {
log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body)
}
return
}
// Успешный ответ: JSON тела (Result).
w.Header().Set("x-amzn-RequestId", body.GetRequestId())
w.Header().Set("Content-Type", "application/x-amz-json-1.0")
w.WriteHeader(statusCode)
@@ -196,9 +220,16 @@ func actionHandler(w http.ResponseWriter, req *http.Request) {
}
return
}
// Неизвестный Action — отвечаем ошибкой в ФОРМАТЕ ПРОТОКОЛА клиента
// (XML ErrorResponse для Query, AWS-JSON для JSON-протокола).
// Раньше возвращался голый текст "Bad Request" (200/400 без AWS-XML) —
// SDK получал ClientError с Code=None и не мог понять причину.
log.Warnf("Bad Request - Action: %s", action)
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, "Bad Request")
errType := models.SqsErrors["GeneralError"]
encodeResponse(w, req, errType.StatusCode(), models.ErrorResponse{
Result: errType.Response(),
RequestId: "00000000-0000-0000-0000-000000000000",
})
}
type AwsProtocol int
@@ -208,25 +239,42 @@ const (
AwsQueryProtocol AwsProtocol = iota
)
// extractAction — извлекает Action из запроса (Query Protocol или JSON Protocol)
// extractAction — извлекает имя операции SQS из запроса.
//
// Логическая схема (не зависит от resolveProtocol):
// 1. Заголовок X-Amz-Target ("AmazonSQS.SendMessage") — используют клиенты
// JSON-протокола И AWS CLI v2 в query-mode. Извлекаем часть после точки.
// 2. Иначе — form-параметр Action (классический query-протокол).
//
// ВАЖНО: определение по X-Amz-Target в первую очередь обязательно — AWS CLI v2
// шлёт Content-Type: application/x-amz-json-1.0 + x-amzn-query-mode: true, но
// Action у него в заголовке, а НЕ в form-параметрах.
func extractAction(req *http.Request) string {
protocol := resolveProtocol(req)
switch protocol {
case AwsJsonProtocol:
action := req.Header.Get("X-Amz-Target")
if action := req.Header.Get("X-Amz-Target"); action != "" {
parts := strings.SplitN(action, ".", 2)
if len(parts) != 2 || parts[1] == "" {
return ""
if len(parts) == 2 && parts[1] != "" {
return parts[1]
}
return parts[1]
case AwsQueryProtocol:
return req.FormValue("Action")
}
return ""
return req.FormValue("Action")
}
// resolveProtocol — определяет протокол по Content-Type
// resolveProtocol — определяет протокол ОТВЕТА по заголовкам запроса.
//
// Логическая схема:
// 1. Заголовок x-amzn-query-mode: true (шлют AWS CLI v2 и boto3) означает
// «query-compatible JSON»: тело запроса сериализовано как JSON, но клиент
// ЖДЁТ ответ в XML (query-протокол). Если на такой запрос ответить JSON —
// botocore не распарсит тело ошибки (ClientError с Code=None — именно это
// наблюдалось: «сырые 400/404» без AWS-XML). Поэтому для query-mode
// принудительно отвечаем XML.
// 2. Content-Type: application/x-amz-json-1.0 БЕЗ query-mode — чистый
// JSON-протокол (SDK): и запрос, и ответ в JSON.
// 3. Всё остальное (form-urlencoded от старых клиентов) — query/XML.
func resolveProtocol(req *http.Request) AwsProtocol {
if req.Header.Get("x-amzn-query-mode") == "true" {
return AwsQueryProtocol
}
if req.Header.Get("Content-Type") == "application/x-amz-json-1.0" {
return AwsJsonProtocol
}