Files
seaweedfs/weed/s3api/s3err/error_handler.go
Chris Lu 2da24cc230 fix(s3): return 403 on POST policy violation instead of 307 redirect (#9122)
* 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.
2026-04-17 14:54:58 -07:00

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)
}