diff --git a/api/api.go b/api/api.go index 6fcaef0..33f9b5c 100644 --- a/api/api.go +++ b/api/api.go @@ -2,6 +2,7 @@ package api import ( "bufio" + "bytes" "context" "encoding/base64" "encoding/xml" @@ -400,13 +401,21 @@ func shouldDecodeAWSChunkedPayload(r *http.Request) bool { return true } signingMode := strings.ToLower(r.Header.Get("x-amz-content-sha256")) - return strings.HasPrefix(signingMode, "streaming-aws4-hmac-sha256-payload") + if strings.HasPrefix(signingMode, "streaming-aws4-hmac-sha256-payload") { + return true + } + return strings.HasPrefix(signingMode, "streaming-unsigned-payload") } func newAWSChunkedDecodingReader(src io.Reader) io.ReadCloser { + probedReader, isAWSChunked := probeAWSChunkedPayload(src) + if !isAWSChunked { + return io.NopCloser(probedReader) + } + pr, pw := io.Pipe() go func() { - if err := decodeAWSChunkedPayload(src, pw); err != nil { + if err := decodeAWSChunkedPayload(probedReader, pw); err != nil { _ = pw.CloseWithError(err) return } @@ -415,6 +424,30 @@ func newAWSChunkedDecodingReader(src io.Reader) io.ReadCloser { return pr } +func probeAWSChunkedPayload(src io.Reader) (io.Reader, bool) { + reader := bufio.NewReaderSize(src, 512) + headerLine, err := reader.ReadSlice('\n') + replay := io.MultiReader(bytes.NewReader(headerLine), reader) + if err != nil { + return replay, false + } + + line := strings.TrimRight(string(headerLine), "\r\n") + chunkSizeToken := line + if idx := strings.IndexByte(chunkSizeToken, ';'); idx >= 0 { + chunkSizeToken = chunkSizeToken[:idx] + } + chunkSizeToken = strings.TrimSpace(chunkSizeToken) + if chunkSizeToken == "" { + return replay, false + } + size, parseErr := strconv.ParseInt(chunkSizeToken, 16, 64) + if parseErr != nil || size < 0 { + return replay, false + } + return replay, true +} + func decodeAWSChunkedPayload(src io.Reader, dst io.Writer) error { reader := bufio.NewReader(src) for { diff --git a/auth/sigv4.go b/auth/sigv4.go index d79b628..8988498 100644 --- a/auth/sigv4.go +++ b/auth/sigv4.go @@ -259,7 +259,57 @@ func canonicalPath(u *url.URL) string { if path == "" { return "/" } - return path + return awsEncodePath(path) +} + +func awsEncodePath(path string) string { + var b strings.Builder + b.Grow(len(path)) + for i := 0; i < len(path); i++ { + ch := path[i] + if ch == '/' || isUnreserved(ch) { + b.WriteByte(ch) + continue + } + if ch == '%' && i+2 < len(path) && isHex(path[i+1]) && isHex(path[i+2]) { + b.WriteByte('%') + b.WriteByte(toUpperHex(path[i+1])) + b.WriteByte(toUpperHex(path[i+2])) + i += 2 + continue + } + b.WriteByte('%') + b.WriteByte(hexUpper(ch >> 4)) + b.WriteByte(hexUpper(ch & 0x0F)) + } + return b.String() +} + +func isUnreserved(ch byte) bool { + return (ch >= 'A' && ch <= 'Z') || + (ch >= 'a' && ch <= 'z') || + (ch >= '0' && ch <= '9') || + ch == '-' || ch == '_' || ch == '.' || ch == '~' +} + +func isHex(ch byte) bool { + return (ch >= '0' && ch <= '9') || + (ch >= 'a' && ch <= 'f') || + (ch >= 'A' && ch <= 'F') +} + +func toUpperHex(ch byte) byte { + if ch >= 'a' && ch <= 'f' { + return ch - ('a' - 'A') + } + return ch +} + +func hexUpper(nibble byte) byte { + if nibble < 10 { + return '0' + nibble + } + return 'A' + (nibble - 10) } type queryPair struct {