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 \"/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. func WriteErrorResponse(w http.ResponseWriter, reqInfo *ReqInfo, err error) int { 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 := EncodeResponse(errorResponse) WriteResponse(w, code, encodedErrorResponse, MimeXML) return code } // 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) { 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 } WriteResponseBody(w, response) } // WriteResponseBody writes response into w. func WriteResponseBody(w http.ResponseWriter, response []byte) { _, _ = w.Write(response) if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } } // EncodeResponse encodes the response headers into XML format. func EncodeResponse(response interface{}) []byte { var bytesBuffer bytes.Buffer bytesBuffer.WriteString(xml.Header) _ = xml. NewEncoder(&bytesBuffer). Encode(response) return bytesBuffer.Bytes() } // 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 { var bytesBuffer bytes.Buffer _ = xml.NewEncoder(&bytesBuffer).Encode(response) return bytesBuffer.Bytes() } // 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) { 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...) }) } }