Denis Kirillov
62cc5a04a7
Some checks failed
/ DCO (pull_request) Successful in 3m34s
/ Vulncheck (pull_request) Failing after 4m18s
/ Builds (1.20) (pull_request) Successful in 4m58s
/ Builds (1.21) (pull_request) Successful in 4m24s
/ Lint (pull_request) Successful in 7m27s
/ Tests (1.20) (pull_request) Successful in 5m24s
/ Tests (1.21) (pull_request) Successful in 5m0s
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
351 lines
12 KiB
Go
351 lines
12 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
|
|
"go.opentelemetry.io/otel/trace"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type (
|
|
// ErrorResponse -- error response format.
|
|
ErrorResponse struct {
|
|
XMLName xml.Name `xml:"Error" json:"-"`
|
|
Code string
|
|
Message string
|
|
Key string `xml:"Key,omitempty" json:"Key,omitempty"`
|
|
BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"`
|
|
Resource string
|
|
RequestID string `xml:"RequestId" json:"RequestId"`
|
|
HostID string `xml:"HostId" json:"HostId"`
|
|
|
|
// The region where the bucket is located. This header is returned
|
|
// only in HEAD bucket and ListObjects response.
|
|
Region string `xml:"Region,omitempty" json:"Region,omitempty"`
|
|
|
|
// Captures the server string returned in response header.
|
|
Server string `xml:"-" json:"-"`
|
|
|
|
// Underlying HTTP status code for the returned error.
|
|
StatusCode int `xml:"-" json:"-"`
|
|
}
|
|
|
|
// mimeType represents various MIME types used in API responses.
|
|
mimeType string
|
|
)
|
|
|
|
const (
|
|
|
|
// MimeNone means no response type.
|
|
MimeNone mimeType = ""
|
|
|
|
// MimeXML means response type is XML.
|
|
MimeXML mimeType = "application/xml"
|
|
|
|
hdrServerInfo = "Server"
|
|
hdrAcceptRanges = "Accept-Ranges"
|
|
hdrContentType = "Content-Type"
|
|
hdrContentLength = "Content-Length"
|
|
hdrRetryAfter = "Retry-After"
|
|
|
|
// Response request id.
|
|
|
|
// hdrSSE is the general AWS SSE HTTP header key.
|
|
hdrSSE = "X-Amz-Server-Side-Encryption"
|
|
|
|
// hdrSSECustomerKey is the HTTP header key referencing the
|
|
// SSE-C client-provided key..
|
|
hdrSSECustomerKey = hdrSSE + "-Customer-Key"
|
|
|
|
// hdrSSECopyKey is the HTTP header key referencing the SSE-C
|
|
// client-provided key for SSE-C copy requests.
|
|
hdrSSECopyKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
|
|
)
|
|
|
|
var (
|
|
xmlHeader = []byte(xml.Header)
|
|
)
|
|
|
|
// Non exhaustive list of AWS S3 standard error responses -
|
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
|
|
var s3ErrorResponseMap = map[string]string{
|
|
"AccessDenied": "Access Denied.",
|
|
"BadDigest": "The Content-Md5 you specified did not match what we received.",
|
|
"EntityTooSmall": "Your proposed upload is smaller than the minimum allowed object size.",
|
|
"EntityTooLarge": "Your proposed upload exceeds the maximum allowed object size.",
|
|
"IncompleteBody": "You did not provide the number of bytes specified by the Content-Length HTTP header.",
|
|
"InternalError": "We encountered an internal error, please try again.",
|
|
"InvalidAccessKeyId": "The access key ID you provided does not exist in our records.",
|
|
"InvalidBucketName": "The specified bucket is not valid.",
|
|
"InvalidDigest": "The Content-Md5 you specified is not valid.",
|
|
"InvalidRange": "The requested range is not satisfiable",
|
|
"MalformedXML": "The XML you provided was not well-formed or did not validate against our published schema.",
|
|
"MissingContentLength": "You must provide the Content-Length HTTP header.",
|
|
"MissingContentMD5": "Missing required header for this request: Content-Md5.",
|
|
"MissingRequestBodyError": "Request body is empty.",
|
|
"NoSuchBucket": "The specified bucket does not exist.",
|
|
"NoSuchBucketPolicy": "The bucket policy does not exist",
|
|
"NoSuchKey": "The specified key does not exist.",
|
|
"NoSuchUpload": "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
|
|
"NotImplemented": "A header you provided implies functionality that is not implemented",
|
|
"PreconditionFailed": "At least one of the pre-conditions you specified did not hold",
|
|
"RequestTimeTooSkewed": "The difference between the request time and the server's time is too large.",
|
|
"SignatureDoesNotMatch": "The request signature we calculated does not match the signature you provided. Check your key and signing method.",
|
|
"MethodNotAllowed": "The specified method is not allowed against this resource.",
|
|
"InvalidPart": "One or more of the specified parts could not be found.",
|
|
"InvalidPartOrder": "The list of parts was not in ascending order. The parts list must be specified in order by part number.",
|
|
"InvalidObjectState": "The operation is not valid for the current state of the object.",
|
|
"AuthorizationHeaderMalformed": "The authorization header is malformed; the region is wrong.",
|
|
"MalformedPOSTRequest": "The body of your POST request is not well-formed multipart/form-data.",
|
|
"BucketNotEmpty": "The bucket you tried to delete is not empty",
|
|
"AllAccessDisabled": "All access to this bucket has been disabled.",
|
|
"MalformedPolicy": "Policy has invalid resource.",
|
|
"MissingFields": "Missing fields in request.",
|
|
"AuthorizationQueryParametersError": "Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request\".",
|
|
"MalformedDate": "Invalid date format header, expected to be in ISO8601, RFC1123 or RFC1123Z time format.",
|
|
"BucketAlreadyOwnedByYou": "Your previous request to create the named bucket succeeded and you already own it.",
|
|
"InvalidDuration": "Duration provided in the request is invalid.",
|
|
"XAmzContentSHA256Mismatch": "The provided 'x-amz-content-sha256' header does not match what was computed.",
|
|
// Add new API errors here.
|
|
}
|
|
|
|
// WriteErrorResponse writes error headers.
|
|
// returns http error code and error in case of failure of response writing.
|
|
func WriteErrorResponse(w http.ResponseWriter, reqInfo *ReqInfo, err error) (int, error) {
|
|
code := http.StatusInternalServerError
|
|
|
|
if e, ok := err.(errors.Error); ok {
|
|
code = e.HTTPStatusCode
|
|
|
|
switch e.Code {
|
|
case "SlowDown", "XFrostFSServerNotInitialized", "XFrostFSReadQuorum", "XFrostFSWriteQuorum":
|
|
// Set retry-after header to indicate user-agents to retry request after 120secs.
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
|
w.Header().Set(hdrRetryAfter, "120")
|
|
}
|
|
}
|
|
|
|
// Generates error response.
|
|
errorResponse := getAPIErrorResponse(reqInfo, err)
|
|
encodedErrorResponse, err := EncodeResponse(errorResponse)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("encode response: %w", err)
|
|
}
|
|
if err = WriteResponse(w, code, encodedErrorResponse, MimeXML); err != nil {
|
|
return 0, fmt.Errorf("write response: %w", err)
|
|
}
|
|
return code, nil
|
|
}
|
|
|
|
// Write http common headers.
|
|
func setCommonHeaders(w http.ResponseWriter) {
|
|
w.Header().Set(hdrServerInfo, version.Server)
|
|
w.Header().Set(hdrAcceptRanges, "bytes")
|
|
|
|
// Remove sensitive information
|
|
removeSensitiveHeaders(w.Header())
|
|
}
|
|
|
|
// removeSensitiveHeaders removes confidential encryption
|
|
// information -- e.g. the SSE-C key -- from the HTTP headers.
|
|
// It has the same semantics as RemoveSensitiveEntries.
|
|
func removeSensitiveHeaders(h http.Header) {
|
|
h.Del(hdrSSECustomerKey)
|
|
h.Del(hdrSSECopyKey)
|
|
}
|
|
|
|
// WriteResponse writes given statusCode and response into w (with mType header if set).
|
|
func WriteResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) error {
|
|
setCommonHeaders(w)
|
|
if mType != MimeNone {
|
|
w.Header().Set(hdrContentType, string(mType))
|
|
}
|
|
w.Header().Set(hdrContentLength, strconv.Itoa(len(response)))
|
|
w.WriteHeader(statusCode)
|
|
if response == nil {
|
|
return nil
|
|
}
|
|
|
|
return WriteResponseBody(w, response)
|
|
}
|
|
|
|
// WriteResponseBody writes response into w.
|
|
func WriteResponseBody(w http.ResponseWriter, response []byte) error {
|
|
if _, err := w.Write(response); err != nil {
|
|
return err
|
|
}
|
|
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EncodeResponse encodes the response headers into XML format.
|
|
func EncodeResponse(response interface{}) ([]byte, error) {
|
|
var bytesBuffer bytes.Buffer
|
|
bytesBuffer.WriteString(xml.Header)
|
|
if err := xml.NewEncoder(&bytesBuffer).Encode(response); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return bytesBuffer.Bytes(), nil
|
|
}
|
|
|
|
// EncodeResponseNoHeader encodes response without setting xml.Header.
|
|
// Should be used with periodicXMLWriter which sends xml.Header to the client
|
|
// with whitespaces to keep connection alive.
|
|
func EncodeResponseNoHeader(response interface{}) ([]byte, error) {
|
|
var bytesBuffer bytes.Buffer
|
|
if err := xml.NewEncoder(&bytesBuffer).Encode(response); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return bytesBuffer.Bytes(), nil
|
|
}
|
|
|
|
// EncodeToResponse encodes the response into ResponseWriter.
|
|
func EncodeToResponse(w http.ResponseWriter, response interface{}) error {
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
if _, err := w.Write(xmlHeader); err != nil {
|
|
return fmt.Errorf("write headers: %w", err)
|
|
}
|
|
|
|
return EncodeToResponseNoHeader(w, response)
|
|
}
|
|
|
|
// EncodeToResponseNoHeader encodes the response into ResponseWriter without
|
|
// header status.
|
|
func EncodeToResponseNoHeader(w http.ResponseWriter, response interface{}) error {
|
|
if err := xml.NewEncoder(w).Encode(response); err != nil {
|
|
return fmt.Errorf("encode xml response: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// // WriteSuccessResponseXML writes success headers and response if any,
|
|
// // with content-type set to `application/xml`.
|
|
// func WriteSuccessResponseXML(w http.ResponseWriter, response []byte) {
|
|
// WriteResponse(w, http.StatusOK, response, MimeXML)
|
|
// }
|
|
|
|
// WriteSuccessResponseHeadersOnly writes HTTP (200) OK response with no data
|
|
// to the client.
|
|
func WriteSuccessResponseHeadersOnly(w http.ResponseWriter) error {
|
|
return WriteResponse(w, http.StatusOK, nil, MimeNone)
|
|
}
|
|
|
|
// Error -- Returns S3 error string.
|
|
func (e ErrorResponse) Error() string {
|
|
if e.Message == "" {
|
|
msg, ok := s3ErrorResponseMap[e.Code]
|
|
if !ok {
|
|
msg = fmt.Sprintf("Error response code %s.", e.Code)
|
|
}
|
|
return msg
|
|
}
|
|
return e.Message
|
|
}
|
|
|
|
// getErrorResponse gets in standard error and resource value and
|
|
// provides an encodable populated response values.
|
|
func getAPIErrorResponse(info *ReqInfo, err error) ErrorResponse {
|
|
code := "InternalError"
|
|
desc := err.Error()
|
|
|
|
if e, ok := err.(errors.Error); ok {
|
|
code = e.Code
|
|
desc = e.Description
|
|
}
|
|
|
|
var resource string
|
|
if info.URL != nil {
|
|
resource = info.URL.Path
|
|
}
|
|
|
|
return ErrorResponse{
|
|
Code: code,
|
|
Message: desc,
|
|
BucketName: info.BucketName,
|
|
Key: info.ObjectName,
|
|
Resource: resource,
|
|
RequestID: info.RequestID,
|
|
HostID: info.DeploymentID,
|
|
}
|
|
}
|
|
|
|
type logResponseWriter struct {
|
|
sync.Once
|
|
http.ResponseWriter
|
|
|
|
statusCode int
|
|
}
|
|
|
|
func (lrw *logResponseWriter) WriteHeader(code int) {
|
|
lrw.Do(func() {
|
|
lrw.statusCode = code
|
|
lrw.ResponseWriter.WriteHeader(code)
|
|
})
|
|
}
|
|
|
|
func (lrw *logResponseWriter) Flush() {
|
|
if f, ok := lrw.ResponseWriter.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
|
|
func LogSuccessResponse(l *zap.Logger) Func {
|
|
return func(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
lw := &logResponseWriter{ResponseWriter: w}
|
|
|
|
// here reqInfo doesn't contain bucket name and object name
|
|
|
|
// pass execution:
|
|
h.ServeHTTP(lw, r)
|
|
|
|
// here reqInfo contains bucket name and object name because of
|
|
// addBucketName and addObjectName middlewares
|
|
|
|
// Ignore >400 status codes
|
|
if lw.statusCode >= http.StatusBadRequest {
|
|
return
|
|
}
|
|
|
|
ctx := r.Context()
|
|
reqLogger := reqLogOrDefault(ctx, l)
|
|
reqInfo := GetReqInfo(ctx)
|
|
|
|
fields := make([]zap.Field, 0, 6)
|
|
fields = append(fields,
|
|
zap.Int("status", lw.statusCode),
|
|
zap.String("description", http.StatusText(lw.statusCode)),
|
|
zap.String("method", reqInfo.API),
|
|
)
|
|
|
|
if reqInfo.BucketName != "" {
|
|
fields = append(fields, zap.String("bucket", reqInfo.BucketName))
|
|
}
|
|
if reqInfo.ObjectName != "" {
|
|
fields = append(fields, zap.String("object", reqInfo.ObjectName))
|
|
}
|
|
|
|
if traceID, err := trace.TraceIDFromHex(reqInfo.TraceID); err == nil && traceID.IsValid() {
|
|
fields = append(fields, zap.String("trace_id", reqInfo.TraceID))
|
|
}
|
|
|
|
reqLogger.Info(logs.RequestEnd, fields...)
|
|
})
|
|
}
|
|
}
|