2023-07-05 14:04:52 +00:00
package middleware
import (
"bytes"
"encoding/xml"
"fmt"
"net/http"
"strconv"
"sync"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
2023-08-23 11:07:52 +00:00
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
2023-07-05 14:04:52 +00:00
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
2023-08-29 09:27:50 +00:00
"go.opentelemetry.io/otel/trace"
2023-07-05 14:04:52 +00:00
"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.
2024-03-04 12:53:00 +00:00
// returns http error code and error in case of failure of response writing.
func WriteErrorResponse ( w http . ResponseWriter , reqInfo * ReqInfo , err error ) ( int , error ) {
2023-07-05 14:04:52 +00:00
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 )
2024-03-04 12:53:00 +00:00
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
2023-07-05 14:04:52 +00:00
}
// 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).
2024-03-04 12:53:00 +00:00
func WriteResponse ( w http . ResponseWriter , statusCode int , response [ ] byte , mType mimeType ) error {
2023-07-05 14:04:52 +00:00
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 {
2024-03-04 12:53:00 +00:00
return nil
2023-07-05 14:04:52 +00:00
}
2024-03-04 12:53:00 +00:00
return WriteResponseBody ( w , response )
2023-07-05 14:04:52 +00:00
}
// WriteResponseBody writes response into w.
2024-03-04 12:53:00 +00:00
func WriteResponseBody ( w http . ResponseWriter , response [ ] byte ) error {
if _ , err := w . Write ( response ) ; err != nil {
return err
}
2023-07-05 14:04:52 +00:00
if flusher , ok := w . ( http . Flusher ) ; ok {
flusher . Flush ( )
}
2024-03-04 12:53:00 +00:00
return nil
2023-07-05 14:04:52 +00:00
}
// EncodeResponse encodes the response headers into XML format.
2024-03-04 12:53:00 +00:00
func EncodeResponse ( response interface { } ) ( [ ] byte , error ) {
2023-07-05 14:04:52 +00:00
var bytesBuffer bytes . Buffer
bytesBuffer . WriteString ( xml . Header )
2024-03-04 12:53:00 +00:00
if err := xml . NewEncoder ( & bytesBuffer ) . Encode ( response ) ; err != nil {
return nil , err
}
return bytesBuffer . Bytes ( ) , nil
2023-07-05 14:04:52 +00:00
}
// 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.
2024-03-04 12:53:00 +00:00
func EncodeResponseNoHeader ( response interface { } ) ( [ ] byte , error ) {
2023-07-05 14:04:52 +00:00
var bytesBuffer bytes . Buffer
2024-03-04 12:53:00 +00:00
if err := xml . NewEncoder ( & bytesBuffer ) . Encode ( response ) ; err != nil {
return nil , err
}
return bytesBuffer . Bytes ( ) , nil
2023-07-05 14:04:52 +00:00
}
// 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.
2024-03-04 12:53:00 +00:00
func WriteSuccessResponseHeadersOnly ( w http . ResponseWriter ) error {
return WriteResponse ( w , http . StatusOK , nil , MimeNone )
2023-07-05 14:04:52 +00:00
}
// 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 )
2024-02-21 14:32:17 +00:00
fields := make ( [ ] zap . Field , 0 , 6 )
fields = append ( fields ,
2023-07-05 14:04:52 +00:00
zap . Int ( "status" , lw . statusCode ) ,
2024-02-21 14:32:17 +00:00
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 ) )
}
2023-08-29 09:27:50 +00:00
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 ... )
2023-07-05 14:04:52 +00:00
} )
}
}