mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-16 23:01:30 +00:00
* fix(s3): return 403 on POST policy violation instead of 307 redirect CheckPostPolicy failures previously responded with HTTP 307 Temporary Redirect to the request URL, which causes clients to re-POST and obscures the failure. Return 403 AccessDenied so the client surfaces the error. * test(s3): exercise PostPolicyBucketHandler end-to-end for 403 mapping Replace the shallow ErrAccessDenied tautology test with one that builds a signed POST multipart request whose policy conditions cannot be satisfied, calls PostPolicyBucketHandler directly, and asserts HTTP 403 with no Location redirect header. Addresses gemini-code-assist review on PR #9122. * fix(s3): surface POST policy failure reason in AccessDenied response Add s3err.WriteErrorResponseWithMessage so a caller can keep the standard error code mapping while providing a specific Message. Use it from PostPolicyBucketHandler so the XML body carries the CheckPostPolicy error (e.g. which condition failed or that the policy expired) rather than the generic "Access Denied." description. Addresses gemini-code- assist review on PR #9122. * refactor(s3err): delegate WriteErrorResponse to WriteErrorResponseWithMessage The two helpers shared every line except the Message override. Fold WriteErrorResponse into a one-line delegation that passes an empty message, so the request-id/mux/apiError logic lives in exactly one place. Addresses gemini-code-assist review on PR #9122.
148 lines
4.8 KiB
Go
148 lines
4.8 KiB
Go
package s3err
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil"
|
|
"github.com/gorilla/mux"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/request_id"
|
|
)
|
|
|
|
type mimeType string
|
|
|
|
const (
|
|
mimeNone mimeType = ""
|
|
MimeXML mimeType = "application/xml"
|
|
)
|
|
|
|
func WriteAwsXMLResponse(w http.ResponseWriter, r *http.Request, statusCode int, result interface{}) {
|
|
var bytesBuffer bytes.Buffer
|
|
err := xmlutil.BuildXML(result, xml.NewEncoder(&bytesBuffer))
|
|
if err != nil {
|
|
WriteErrorResponse(w, r, ErrInternalError)
|
|
return
|
|
}
|
|
WriteResponse(w, r, statusCode, bytesBuffer.Bytes(), MimeXML)
|
|
}
|
|
|
|
func WriteXMLResponse(w http.ResponseWriter, r *http.Request, statusCode int, response interface{}) {
|
|
WriteResponse(w, r, statusCode, EncodeXMLResponse(response), MimeXML)
|
|
}
|
|
|
|
func WriteEmptyResponse(w http.ResponseWriter, r *http.Request, statusCode int) {
|
|
WriteResponse(w, r, statusCode, []byte{}, mimeNone)
|
|
PostLog(r, statusCode, ErrNone)
|
|
}
|
|
|
|
func WriteErrorResponse(w http.ResponseWriter, r *http.Request, errorCode ErrorCode) {
|
|
WriteErrorResponseWithMessage(w, r, errorCode, "")
|
|
}
|
|
|
|
// WriteErrorResponseWithMessage writes an S3 error response that uses the
|
|
// standard error code mapping (status + Code). When message is non-empty,
|
|
// it overrides the default Message field so the caller can surface why the
|
|
// request was rejected (e.g. which POST policy condition failed) instead
|
|
// of the generic APIError Description.
|
|
func WriteErrorResponseWithMessage(w http.ResponseWriter, r *http.Request, errorCode ErrorCode, message string) {
|
|
r, reqID := request_id.Ensure(r)
|
|
vars := mux.Vars(r)
|
|
bucket := vars["bucket"]
|
|
object := vars["object"]
|
|
if strings.HasPrefix(object, "/") {
|
|
object = object[1:]
|
|
}
|
|
|
|
apiError := GetAPIError(errorCode)
|
|
errorResponse := getRESTErrorResponse(apiError, r.URL.Path, bucket, object, reqID)
|
|
if message != "" {
|
|
errorResponse.Message = message
|
|
}
|
|
WriteXMLResponse(w, r, apiError.HTTPStatusCode, errorResponse)
|
|
PostLog(r, apiError.HTTPStatusCode, errorCode)
|
|
}
|
|
|
|
func getRESTErrorResponse(err APIError, resource string, bucket, object, requestID string) RESTErrorResponse {
|
|
return RESTErrorResponse{
|
|
Code: err.Code,
|
|
BucketName: bucket,
|
|
Key: object,
|
|
Message: err.Description,
|
|
Resource: resource,
|
|
RequestID: requestID,
|
|
}
|
|
}
|
|
|
|
// Encodes the response headers into XML format.
|
|
func EncodeXMLResponse(response interface{}) []byte {
|
|
var bytesBuffer bytes.Buffer
|
|
bytesBuffer.WriteString(xml.Header)
|
|
e := xml.NewEncoder(&bytesBuffer)
|
|
e.Encode(response)
|
|
return bytesBuffer.Bytes()
|
|
}
|
|
|
|
func setCommonHeaders(w http.ResponseWriter, r *http.Request) {
|
|
_, reqID := request_id.Ensure(r)
|
|
w.Header().Set(request_id.AmzRequestIDHeader, reqID)
|
|
w.Header().Set("Accept-Ranges", "bytes")
|
|
|
|
// Handle CORS headers for requests with Origin header
|
|
if r.Header.Get("Origin") != "" {
|
|
// Use mux.Vars to detect bucket-specific requests more reliably
|
|
vars := mux.Vars(r)
|
|
bucket := vars["bucket"]
|
|
isBucketRequest := bucket != ""
|
|
|
|
if !isBucketRequest {
|
|
// Service-level request (like OPTIONS /) - apply static CORS if none set
|
|
if w.Header().Get("Access-Control-Allow-Origin") == "" {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "*")
|
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
|
w.Header().Set("Access-Control-Expose-Headers", "*")
|
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
}
|
|
} else {
|
|
// Bucket-specific request - preserve existing CORS headers or set default
|
|
// This handles cases where CORS middleware set headers but auth failed
|
|
if w.Header().Get("Access-Control-Allow-Origin") == "" {
|
|
// No CORS headers were set by middleware, so this request doesn't match any CORS rule
|
|
// According to CORS spec, we should not set CORS headers for non-matching requests
|
|
// However, if the bucket has CORS config but request doesn't match,
|
|
// we still should not set headers here as it would be incorrect
|
|
}
|
|
// If CORS headers were already set by middleware, preserve them
|
|
}
|
|
}
|
|
}
|
|
|
|
func WriteResponse(w http.ResponseWriter, r *http.Request, statusCode int, response []byte, mType mimeType) {
|
|
setCommonHeaders(w, r)
|
|
if response != nil {
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
|
}
|
|
if mType != mimeNone {
|
|
w.Header().Set("Content-Type", string(mType))
|
|
}
|
|
w.WriteHeader(statusCode)
|
|
if response != nil {
|
|
glog.V(4).Infof("status %d %s: %s", statusCode, mType, string(response))
|
|
_, err := w.Write(response)
|
|
if err != nil {
|
|
glog.V(1).Infof("write err: %v", err)
|
|
}
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
}
|
|
|
|
// If none of the http routes match respond with MethodNotAllowed
|
|
func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
|
|
glog.V(2).Infof("unsupported %s %s", r.Method, r.RequestURI)
|
|
WriteErrorResponse(w, r, ErrMethodNotAllowed)
|
|
}
|