2018-09-02 14:20:47 -07:00
package s3api
// the related code is copied and modified from minio source code
/ *
* Minio Cloud Storage , ( C ) 2016 Minio , Inc .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
import (
"bufio"
"bytes"
2025-02-12 21:29:13 +01:00
"crypto/sha1"
2020-02-09 17:42:17 -08:00
"crypto/sha256"
2025-02-12 21:29:13 +01:00
"encoding/base64"
2020-02-09 17:42:17 -08:00
"encoding/hex"
2018-09-02 14:20:47 -07:00
"errors"
2025-02-12 21:29:13 +01:00
"fmt"
2020-02-09 17:42:17 -08:00
"hash"
2025-02-12 21:29:13 +01:00
"hash/crc32"
2018-09-02 14:20:47 -07:00
"io"
"net/http"
2020-02-09 17:42:17 -08:00
"time"
2025-02-07 19:54:31 +01:00
"github.com/seaweedfs/seaweedfs/weed/glog"
2023-01-20 13:12:30 +01:00
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
2020-02-09 17:42:17 -08:00
"github.com/dustin/go-humanize"
2025-04-23 19:31:03 +05:00
"github.com/minio/crc64nvme"
2018-09-02 14:20:47 -07:00
)
2020-02-09 17:42:17 -08:00
// calculateSeedSignature - Calculate seed signature in accordance with
2022-09-14 23:06:44 -07:00
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
//
2020-02-09 17:42:17 -08:00
// returns signature, error otherwise if the signature mismatches or any other
// error while parsing and validating.
2025-08-05 22:54:54 -07:00
func ( iam * IdentityAccessManagement ) calculateSeedSignature ( r * http . Request ) ( cred * Credential , signature string , region string , service string , date time . Time , errCode s3err . ErrorCode ) {
2020-02-09 17:42:17 -08:00
// Copy request.
req := * r
// Save authorization header.
v4Auth := req . Header . Get ( "Authorization" )
// Parse signature version '4' header.
signV4Values , errCode := parseSignV4 ( v4Auth )
2020-09-19 14:09:58 -07:00
if errCode != s3err . ErrNone {
2025-08-05 22:54:54 -07:00
return nil , "" , "" , "" , time . Time { } , errCode
2020-02-09 17:42:17 -08:00
}
2025-02-07 19:54:31 +01:00
contentSha256Header := req . Header . Get ( "X-Amz-Content-Sha256" )
2020-02-09 17:42:17 -08:00
2025-02-07 19:54:31 +01:00
switch contentSha256Header {
2020-02-09 17:42:17 -08:00
// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
2025-02-07 19:54:31 +01:00
case streamingContentSHA256 :
glog . V ( 3 ) . Infof ( "streaming content sha256" )
case streamingUnsignedPayload :
glog . V ( 3 ) . Infof ( "streaming unsigned payload" )
default :
2025-08-05 22:54:54 -07:00
return nil , "" , "" , "" , time . Time { } , s3err . ErrContentSHA256Mismatch
2020-02-09 17:42:17 -08:00
}
2025-02-07 19:54:31 +01:00
// Payload streaming.
payload := contentSha256Header
2020-02-09 17:42:17 -08:00
// Extract all the signed headers along with its values.
extractedSignedHeaders , errCode := extractSignedHeaders ( signV4Values . SignedHeaders , r )
2020-09-19 14:09:58 -07:00
if errCode != s3err . ErrNone {
2025-08-05 22:54:54 -07:00
return nil , "" , "" , "" , time . Time { } , errCode
2020-02-09 17:42:17 -08:00
}
// Verify if the access key id matches.
2021-07-03 14:50:53 -07:00
identity , cred , found := iam . lookupByAccessKey ( signV4Values . Credential . accessKey )
2020-02-09 17:42:17 -08:00
if ! found {
2025-08-05 22:54:54 -07:00
return nil , "" , "" , "" , time . Time { } , s3err . ErrInvalidAccessKeyID
2020-02-09 17:42:17 -08:00
}
2022-05-30 22:57:41 -07:00
bucket , object := s3_constants . GetBucketAndObject ( r )
2022-01-03 15:39:36 -08:00
if ! identity . canDo ( s3_constants . ACTION_WRITE , bucket , object ) {
2021-07-03 14:50:53 -07:00
errCode = s3err . ErrAccessDenied
return
}
2020-02-09 17:42:17 -08:00
// Verify if region is valid.
region = signV4Values . Credential . scope . region
// Extract date, if not present throw error.
var dateStr string
if dateStr = req . Header . Get ( http . CanonicalHeaderKey ( "x-amz-date" ) ) ; dateStr == "" {
if dateStr = r . Header . Get ( "Date" ) ; dateStr == "" {
2025-08-05 22:54:54 -07:00
return nil , "" , "" , "" , time . Time { } , s3err . ErrMissingDateHeader
2020-02-09 17:42:17 -08:00
}
}
2025-07-15 10:11:49 -07:00
2020-02-09 17:42:17 -08:00
// Parse date header.
2025-07-15 10:11:49 -07:00
date , err := time . Parse ( iso8601Format , dateStr )
2020-02-09 17:42:17 -08:00
if err != nil {
2025-08-05 22:54:54 -07:00
return nil , "" , "" , "" , time . Time { } , s3err . ErrMalformedDate
2020-02-09 17:42:17 -08:00
}
// Query string.
queryStr := req . URL . Query ( ) . Encode ( )
// Get canonical request.
canonicalRequest := getCanonicalRequest ( extractedSignedHeaders , payload , queryStr , req . URL . Path , req . Method )
// Get string to sign from canonical request.
stringToSign := getStringToSign ( canonicalRequest , date , signV4Values . Credential . getScope ( ) )
2025-07-15 10:11:49 -07:00
// Get hmac signing key.
2025-08-05 22:54:54 -07:00
signingKey := getSigningKey ( cred . SecretKey , signV4Values . Credential . scope . date . Format ( yyyymmdd ) , region , signV4Values . Credential . scope . service )
2025-07-15 10:11:49 -07:00
2020-02-09 17:42:17 -08:00
// Calculate signature.
2025-07-15 10:11:49 -07:00
newSignature := getSignature ( signingKey , stringToSign )
2020-02-09 17:42:17 -08:00
// Verify if signature match.
if ! compareSignatureV4 ( newSignature , signV4Values . Signature ) {
2025-08-05 22:54:54 -07:00
return nil , "" , "" , "" , time . Time { } , s3err . ErrSignatureDoesNotMatch
2020-02-09 17:42:17 -08:00
}
2022-09-15 05:13:21 -05:00
// Return calculated signature.
2025-08-05 22:54:54 -07:00
return cred , newSignature , region , signV4Values . Credential . scope . service , date , s3err . ErrNone
2020-02-09 17:42:17 -08:00
}
2018-09-02 14:20:47 -07:00
const maxLineLength = 4 * humanize . KiByte // assumed <= bufio.defaultBufSize 4KiB
// lineTooLong is generated as chunk header is bigger than 4KiB.
var errLineTooLong = errors . New ( "header line too long" )
// Malformed encoding is generated when chunk header is wrongly formed.
var errMalformedEncoding = errors . New ( "malformed chunked encoding" )
2025-02-12 21:29:13 +01:00
// newChunkedReader returns a new s3ChunkedReader that translates the data read from r
2018-09-02 14:20:47 -07:00
// out of HTTP "chunked" format before returning it.
// The s3ChunkedReader returns io.EOF when the final 0-length chunk is read.
2025-02-12 21:29:13 +01:00
func ( iam * IdentityAccessManagement ) newChunkedReader ( req * http . Request ) ( io . ReadCloser , s3err . ErrorCode ) {
glog . V ( 3 ) . Infof ( "creating a new newSignV4ChunkedReader" )
contentSha256Header := req . Header . Get ( "X-Amz-Content-Sha256" )
authorizationHeader := req . Header . Get ( "Authorization" )
var ident * Credential
2025-08-05 22:54:54 -07:00
var seedSignature , region , service string
2025-02-12 21:29:13 +01:00
var seedDate time . Time
var errCode s3err . ErrorCode
switch contentSha256Header {
// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
case streamingContentSHA256 :
glog . V ( 3 ) . Infof ( "streaming content sha256" )
2025-08-05 22:54:54 -07:00
ident , seedSignature , region , service , seedDate , errCode = iam . calculateSeedSignature ( req )
2025-02-12 21:29:13 +01:00
if errCode != s3err . ErrNone {
return nil , errCode
}
case streamingUnsignedPayload :
glog . V ( 3 ) . Infof ( "streaming unsigned payload" )
if authorizationHeader != "" {
// We do not need to pass the seed signature to the Reader as each chunk is not signed,
// but we do compute it to verify the caller has the correct permissions.
2025-08-05 22:54:54 -07:00
_ , _ , _ , _ , _ , errCode = iam . calculateSeedSignature ( req )
2025-02-12 21:29:13 +01:00
if errCode != s3err . ErrNone {
return nil , errCode
}
}
}
// Get the checksum algorithm from the x-amz-trailer Header.
amzTrailerHeader := req . Header . Get ( "x-amz-trailer" )
checksumAlgorithm , err := extractChecksumAlgorithm ( amzTrailerHeader )
if err != nil {
glog . V ( 3 ) . Infof ( "error extracting checksum algorithm: %v" , err )
return nil , s3err . ErrInvalidRequest
2018-09-02 14:20:47 -07:00
}
2025-02-12 21:29:13 +01:00
checkSumWriter := getCheckSumWriter ( checksumAlgorithm )
2020-02-09 17:42:17 -08:00
return & s3ChunkedReader {
cred : ident ,
reader : bufio . NewReader ( req . Body ) ,
seedSignature : seedSignature ,
seedDate : seedDate ,
region : region ,
2025-08-05 22:54:54 -07:00
service : service ,
2020-02-09 17:42:17 -08:00
chunkSHA256Writer : sha256 . New ( ) ,
2025-02-12 21:29:13 +01:00
checkSumAlgorithm : checksumAlgorithm . String ( ) ,
checkSumWriter : checkSumWriter ,
2020-02-09 17:42:17 -08:00
state : readChunkHeader ,
2023-01-20 13:12:30 +01:00
iam : iam ,
2020-09-19 14:09:58 -07:00
} , s3err . ErrNone
2018-09-02 14:20:47 -07:00
}
2025-02-12 21:29:13 +01:00
func extractChecksumAlgorithm ( amzTrailerHeader string ) ( ChecksumAlgorithm , error ) {
// Extract checksum algorithm from the x-amz-trailer header.
switch amzTrailerHeader {
case "x-amz-checksum-crc32" :
return ChecksumAlgorithmCRC32 , nil
case "x-amz-checksum-crc32c" :
return ChecksumAlgorithmCRC32C , nil
case "x-amz-checksum-crc64nvme" :
return ChecksumAlgorithmCRC64NVMe , nil
case "x-amz-checksum-sha1" :
return ChecksumAlgorithmSHA1 , nil
case "x-amz-checksum-sha256" :
return ChecksumAlgorithmSHA256 , nil
case "" :
return ChecksumAlgorithmNone , nil
default :
return ChecksumAlgorithmNone , errors . New ( "unsupported checksum algorithm '" + amzTrailerHeader + "'" )
}
}
2018-09-02 14:20:47 -07:00
// Represents the overall state that is required for decoding a
// AWS Signature V4 chunked reader.
type s3ChunkedReader struct {
2020-02-09 17:42:17 -08:00
cred * Credential
reader * bufio . Reader
seedSignature string
seedDate time . Time
region string
2025-08-05 22:54:54 -07:00
service string // Service from credential scope (e.g., "s3", "iam")
2020-02-09 17:42:17 -08:00
state chunkState
lastChunk bool
2025-02-12 21:29:13 +01:00
chunkSignature string // Empty string if unsigned streaming upload.
checkSumAlgorithm string // Empty string if no checksum algorithm is specified.
checkSumWriter hash . Hash
2020-02-09 17:42:17 -08:00
chunkSHA256Writer hash . Hash // Calculates sha256 of chunk data.
n uint64 // Unread bytes in chunk
err error
2023-01-20 13:12:30 +01:00
iam * IdentityAccessManagement
2018-09-02 14:20:47 -07:00
}
// Read chunk reads the chunk token signature portion.
func ( cr * s3ChunkedReader ) readS3ChunkHeader ( ) {
// Read the first chunk line until CRLF.
2025-02-12 21:29:13 +01:00
var bytesRead , hexChunkSize , hexChunkSignature [ ] byte
bytesRead , cr . err = readChunkLine ( cr . reader )
// Parse s3 specific chunk extension and fetch the values.
hexChunkSize , hexChunkSignature = parseS3ChunkExtension ( bytesRead )
2018-09-02 14:20:47 -07:00
if cr . err != nil {
return
}
// <hex>;token=value - converts the hex into its uint64 form.
cr . n , cr . err = parseHexUint ( hexChunkSize )
if cr . err != nil {
return
}
if cr . n == 0 {
cr . err = io . EOF
}
2025-02-12 21:29:13 +01:00
2018-09-02 14:20:47 -07:00
// Save the incoming chunk signature.
2025-02-12 21:29:13 +01:00
if hexChunkSignature == nil {
// We are using unsigned streaming upload.
cr . chunkSignature = ""
} else {
cr . chunkSignature = string ( hexChunkSignature )
}
2018-09-02 14:20:47 -07:00
}
type chunkState int
const (
readChunkHeader chunkState = iota
readChunkTrailer
readChunk
2025-02-12 21:29:13 +01:00
readTrailerChunk
2018-09-02 14:20:47 -07:00
verifyChunk
2025-02-12 21:29:13 +01:00
verifyChecksum
2018-09-02 14:20:47 -07:00
eofChunk
)
func ( cs chunkState ) String ( ) string {
stateString := ""
switch cs {
case readChunkHeader :
stateString = "readChunkHeader"
case readChunkTrailer :
stateString = "readChunkTrailer"
case readChunk :
stateString = "readChunk"
2025-02-12 21:29:13 +01:00
case readTrailerChunk :
stateString = "readTrailerChunk"
2018-09-02 14:20:47 -07:00
case verifyChunk :
stateString = "verifyChunk"
2025-02-12 21:29:13 +01:00
case verifyChecksum :
stateString = "verifyChecksum"
2018-09-02 14:20:47 -07:00
case eofChunk :
stateString = "eofChunk"
}
return stateString
}
func ( cr * s3ChunkedReader ) Close ( ) ( err error ) {
return nil
}
// Read - implements `io.Reader`, which transparently decodes
// the incoming AWS Signature V4 streaming signature.
func ( cr * s3ChunkedReader ) Read ( buf [ ] byte ) ( n int , err error ) {
for {
switch cr . state {
case readChunkHeader :
cr . readS3ChunkHeader ( )
// If we're at the end of a chunk.
if cr . n == 0 && cr . err == io . EOF {
cr . state = readChunkTrailer
cr . lastChunk = true
continue
}
if cr . err != nil {
return 0 , cr . err
}
cr . state = readChunk
case readChunkTrailer :
2025-02-12 21:29:13 +01:00
err = peekCRLF ( cr . reader )
isTrailingChunk := cr . n == 0 && cr . lastChunk
if ! isTrailingChunk {
// If we're not in the trailing chunk, we should consume the bytes no matter what.
// The error returned by peekCRLF is the same as the one by readCRLF.
readCRLF ( cr . reader )
cr . err = err
} else if err != nil && err != errMalformedEncoding {
cr . err = err
2018-09-02 14:20:47 -07:00
return 0 , errMalformedEncoding
2025-02-12 21:29:13 +01:00
} else { // equivalent to isTrailingChunk && err == errMalformedEncoding
// FIXME: The "right" structure of the last chunk as provided by the examples in the
// AWS documentation is "0\r\n\r\n" instead of "0\r\n", but some s3 clients when calling with
// streaming-unsigned-payload-trailer omit the last CRLF. To avoid returning an error that, we need to accept both.
// We arrive here when we're at the end of the 0-byte chunk, depending on the client implementation
// the client may or may not send the optional CRLF after the 0-byte chunk.
// If the client sends the optional CRLF, we should consume it.
if err == nil {
readCRLF ( cr . reader )
}
}
// If we're using unsigned streaming upload, there is no signature to verify at each chunk.
if cr . chunkSignature != "" {
cr . state = verifyChunk
} else if cr . lastChunk {
cr . state = readTrailerChunk
} else {
cr . state = readChunkHeader
}
case readTrailerChunk :
// When using unsigned upload, this would be the raw contents of the trailer chunk:
//
// x-amz-checksum-crc32:YABb/g==\n\r\n\r\n // Trailer chunk (note optional \n character)
// \r\n // CRLF
//
// When using signed upload with an additional checksum algorithm, this would be the raw contents of the trailer chunk:
//
// x-amz-checksum-crc32:YABb/g==\n\r\n // Trailer chunk (note optional \n character)
// trailer-signature\r\n
// \r\n // CRLF
//
// This implementation currently only supports the first case.
// TODO: Implement the second case (signed upload with additional checksum computation for each chunk)
extractedCheckSumAlgorithm , extractedChecksum := parseChunkChecksum ( cr . reader )
if extractedCheckSumAlgorithm . String ( ) != cr . checkSumAlgorithm {
errorMessage := fmt . Sprintf ( "checksum algorithm in trailer '%s' does not match the one advertised in the header '%s'" , extractedCheckSumAlgorithm . String ( ) , cr . checkSumAlgorithm )
2025-03-31 21:42:54 -07:00
glog . V ( 3 ) . Info ( errorMessage )
2025-07-07 02:18:57 +05:00
cr . err = errors . New ( s3err . ErrMsgChecksumAlgorithmMismatch )
2025-02-12 21:29:13 +01:00
return 0 , cr . err
}
computedChecksum := cr . checkSumWriter . Sum ( nil )
base64Checksum := base64 . StdEncoding . EncodeToString ( computedChecksum )
if string ( extractedChecksum ) != base64Checksum {
glog . V ( 3 ) . Infof ( "payload checksum '%s' does not match provided checksum '%s'" , base64Checksum , string ( extractedChecksum ) )
2025-07-07 02:18:57 +05:00
cr . err = errors . New ( s3err . ErrMsgPayloadChecksumMismatch )
2025-02-12 21:29:13 +01:00
return 0 , cr . err
2018-09-02 14:20:47 -07:00
}
2025-02-12 21:29:13 +01:00
// TODO: Extract signature from trailer chunk and verify it.
// For now, we just read the trailer chunk and discard it.
// Reading remaining CRLF.
for i := 0 ; i < 2 ; i ++ {
cr . err = readCRLF ( cr . reader )
}
cr . state = eofChunk
2018-09-02 14:20:47 -07:00
case readChunk :
// There is no more space left in the request buffer.
if len ( buf ) == 0 {
return n , nil
}
rbuf := buf
// The request buffer is larger than the current chunk size.
// Read only the current chunk from the underlying reader.
if uint64 ( len ( rbuf ) ) > cr . n {
rbuf = rbuf [ : cr . n ]
}
var n0 int
n0 , cr . err = cr . reader . Read ( rbuf )
if cr . err != nil {
// We have lesser than chunk size advertised in chunkHeader, this is 'unexpected'.
if cr . err == io . EOF {
cr . err = io . ErrUnexpectedEOF
}
return 0 , cr . err
}
2020-02-09 17:42:17 -08:00
// Calculate sha256.
cr . chunkSHA256Writer . Write ( rbuf [ : n0 ] )
2025-02-12 21:29:13 +01:00
// Compute checksum
if cr . checkSumWriter != nil {
cr . checkSumWriter . Write ( rbuf [ : n0 ] )
}
2018-09-02 14:20:47 -07:00
// Update the bytes read into request buffer so far.
n += n0
buf = buf [ n0 : ]
// Update bytes to be read of the current chunk before verifying chunk's signature.
cr . n -= uint64 ( n0 )
// If we're at the end of a chunk.
if cr . n == 0 {
cr . state = readChunkTrailer
continue
}
case verifyChunk :
2025-08-11 10:31:01 -07:00
// Check if we have credentials for signature verification
// This handles the case where we have unsigned streaming (no cred) but chunks contain signatures
//
// BUG FIX for GitHub issue #6847:
// Some AWS SDK versions (Java 3.7.412+, .NET 4.0.0-preview.6+) send mixed format:
// - HTTP headers indicate unsigned streaming (STREAMING-UNSIGNED-PAYLOAD-TRAILER)
// - But chunk data contains chunk-signature headers (normally only for signed streaming)
// This causes a nil pointer dereference when trying to verify signatures without credentials
if cr . cred != nil {
// Normal signed streaming - verify the chunk signature
// Calculate the hashed chunk.
hashedChunk := hex . EncodeToString ( cr . chunkSHA256Writer . Sum ( nil ) )
// Calculate the chunk signature.
newSignature := cr . getChunkSignature ( hashedChunk )
if ! compareSignatureV4 ( cr . chunkSignature , newSignature ) {
// Chunk signature doesn't match we return signature does not match.
cr . err = errors . New ( s3err . ErrMsgChunkSignatureMismatch )
return 0 , cr . err
}
// Newly calculated signature becomes the seed for the next chunk
// this follows the chaining.
cr . seedSignature = newSignature
} else {
// For unsigned streaming, we should not verify chunk signatures even if they are present
// This fixes the bug where AWS SDKs send chunk signatures with unsigned streaming headers
glog . V ( 3 ) . Infof ( "Skipping chunk signature verification for unsigned streaming" )
2020-02-09 17:42:17 -08:00
}
2025-08-11 10:31:01 -07:00
// Common cleanup and state transition for both signed and unsigned streaming
2020-02-09 17:42:17 -08:00
cr . chunkSHA256Writer . Reset ( )
2018-09-02 14:20:47 -07:00
if cr . lastChunk {
cr . state = eofChunk
} else {
cr . state = readChunkHeader
}
case eofChunk :
return n , io . EOF
}
}
}
2023-01-20 13:12:30 +01:00
// getChunkSignature - get chunk signature.
func ( cr * s3ChunkedReader ) getChunkSignature ( hashedChunk string ) string {
// Calculate string to sign.
2025-07-15 10:11:49 -07:00
stringToSign := signV4Algorithm + "-PAYLOAD" + "\n" +
2023-01-20 13:12:30 +01:00
cr . seedDate . Format ( iso8601Format ) + "\n" +
2025-08-05 22:54:54 -07:00
getScope ( cr . seedDate , cr . region , cr . service ) + "\n" +
2023-01-20 13:12:30 +01:00
cr . seedSignature + "\n" +
emptySHA256 + "\n" +
hashedChunk
2025-07-15 10:11:49 -07:00
// Get hmac signing key.
2025-08-05 22:54:54 -07:00
signingKey := getSigningKey ( cr . cred . SecretKey , cr . seedDate . Format ( yyyymmdd ) , cr . region , cr . service )
2025-07-15 10:11:49 -07:00
// Calculate and return signature.
return getSignature ( signingKey , stringToSign )
2023-01-20 13:12:30 +01:00
}
2025-02-12 21:29:13 +01:00
func readCRLF ( reader * bufio . Reader ) error {
2018-09-02 14:20:47 -07:00
buf := make ( [ ] byte , 2 )
2025-07-15 10:11:49 -07:00
_ , err := io . ReadFull ( reader , buf )
2025-02-12 21:29:13 +01:00
if err != nil {
return err
}
return checkCRLF ( buf )
}
func peekCRLF ( reader * bufio . Reader ) error {
2025-07-15 10:11:49 -07:00
buf , err := reader . Peek ( 2 )
2018-09-02 14:20:47 -07:00
if err != nil {
return err
}
2025-07-15 10:11:49 -07:00
if err := checkCRLF ( buf ) ; err != nil {
2025-02-12 21:29:13 +01:00
return err
}
return nil
}
func checkCRLF ( buf [ ] byte ) error {
2025-07-15 10:11:49 -07:00
if len ( buf ) != 2 || buf [ 0 ] != '\r' || buf [ 1 ] != '\n' {
2018-09-02 14:20:47 -07:00
return errMalformedEncoding
}
return nil
}
2025-02-12 21:29:13 +01:00
func readChunkLine ( b * bufio . Reader ) ( [ ] byte , error ) {
2018-09-02 14:20:47 -07:00
buf , err := b . ReadSlice ( '\n' )
if err != nil {
// We always know when EOF is coming.
// If the caller asked for a line, there should be a line.
2025-08-11 10:31:01 -07:00
switch err {
case io . EOF :
2018-09-02 14:20:47 -07:00
err = io . ErrUnexpectedEOF
2025-08-11 10:31:01 -07:00
case bufio . ErrBufferFull :
2018-09-02 14:20:47 -07:00
err = errLineTooLong
}
2025-02-12 21:29:13 +01:00
return nil , err
2018-09-02 14:20:47 -07:00
}
if len ( buf ) >= maxLineLength {
2025-02-12 21:29:13 +01:00
return nil , errLineTooLong
2018-09-02 14:20:47 -07:00
}
2025-07-15 10:11:49 -07:00
return trimTrailingWhitespace ( buf ) , nil
2018-09-02 14:20:47 -07:00
}
// trimTrailingWhitespace - trim trailing white space.
func trimTrailingWhitespace ( b [ ] byte ) [ ] byte {
for len ( b ) > 0 && isASCIISpace ( b [ len ( b ) - 1 ] ) {
b = b [ : len ( b ) - 1 ]
}
return b
}
// isASCIISpace - is ascii space?
func isASCIISpace ( b byte ) bool {
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
}
// Constant s3 chunk encoding signature.
const s3ChunkSignatureStr = ";chunk-signature="
2025-03-07 16:45:47 +08:00
// parseS3ChunkExtension removes any s3 specific chunk-extension from buf.
2018-09-02 14:20:47 -07:00
// For example,
2022-09-14 23:06:44 -07:00
//
// "10000;chunk-signature=..." => "10000", "chunk-signature=..."
2018-09-02 14:20:47 -07:00
func parseS3ChunkExtension ( buf [ ] byte ) ( [ ] byte , [ ] byte ) {
buf = trimTrailingWhitespace ( buf )
semi := bytes . Index ( buf , [ ] byte ( s3ChunkSignatureStr ) )
// Chunk signature not found, return the whole buffer.
2025-02-12 21:29:13 +01:00
// This means we're using unsigned streaming upload.
2018-09-02 14:20:47 -07:00
if semi == - 1 {
return buf , nil
}
return buf [ : semi ] , parseChunkSignature ( buf [ semi : ] )
}
2025-02-12 21:29:13 +01:00
func parseChunkChecksum ( b * bufio . Reader ) ( ChecksumAlgorithm , [ ] byte ) {
// When using unsigned upload, this would be the raw contents of the trailer chunk:
//
// x-amz-checksum-crc32:YABb/g==\n\r\n\r\n // Trailer chunk (note optional \n character)
// \r\n // CRLF
//
// When using signed upload with an additional checksum algorithm, this would be the raw contents of the trailer chunk:
//
// x-amz-checksum-crc32:YABb/g==\n\r\n // Trailer chunk (note optional \n character)
// trailer-signature\r\n
// \r\n // CRLF
//
// x-amz-checksum-crc32:YABb/g==\n
bytesRead , err := readChunkLine ( b )
if err != nil {
return ChecksumAlgorithmNone , nil
}
// Split on ':'
parts := bytes . SplitN ( bytesRead , [ ] byte ( ":" ) , 2 )
checksumKey := string ( parts [ 0 ] )
checksumValue := parts [ 1 ]
// Discard all trailing whitespace characters
checksumValue = trimTrailingWhitespace ( checksumValue )
// If the checksum key is not a supported checksum algorithm, return an error.
// TODO: Bubble that error up to the caller
extractedAlgorithm , err := extractChecksumAlgorithm ( checksumKey )
if err != nil {
return ChecksumAlgorithmNone , nil
}
return extractedAlgorithm , checksumValue
}
2018-09-02 14:20:47 -07:00
func parseChunkSignature ( chunk [ ] byte ) [ ] byte {
2025-07-15 10:11:49 -07:00
chunkSplits := bytes . SplitN ( chunk , [ ] byte ( "=" ) , 2 )
return chunkSplits [ 1 ] // Keep only the signature.
2018-09-02 14:20:47 -07:00
}
func parseHexUint ( v [ ] byte ) ( n uint64 , err error ) {
for i , b := range v {
switch {
case '0' <= b && b <= '9' :
b = b - '0'
case 'a' <= b && b <= 'f' :
b = b - 'a' + 10
case 'A' <= b && b <= 'F' :
b = b - 'A' + 10
default :
return 0 , errors . New ( "invalid byte in chunk length" )
}
if i == 16 {
return 0 , errors . New ( "http chunk length too large" )
}
n <<= 4
n |= uint64 ( b )
}
return
}
2025-02-12 21:29:13 +01:00
2025-07-15 10:11:49 -07:00
// Checksum Algorithm represents the various checksum algorithms supported.
2025-02-12 21:29:13 +01:00
type ChecksumAlgorithm int
const (
ChecksumAlgorithmNone ChecksumAlgorithm = iota
ChecksumAlgorithmCRC32
ChecksumAlgorithmCRC32C
ChecksumAlgorithmCRC64NVMe
ChecksumAlgorithmSHA1
ChecksumAlgorithmSHA256
)
func ( ca ChecksumAlgorithm ) String ( ) string {
switch ca {
2025-07-15 10:11:49 -07:00
case ChecksumAlgorithmNone :
return ""
2025-02-12 21:29:13 +01:00
case ChecksumAlgorithmCRC32 :
2025-07-15 10:11:49 -07:00
return "x-amz-checksum-crc32"
2025-02-12 21:29:13 +01:00
case ChecksumAlgorithmCRC32C :
2025-07-15 10:11:49 -07:00
return "x-amz-checksum-crc32c"
2025-02-12 21:29:13 +01:00
case ChecksumAlgorithmCRC64NVMe :
2025-07-15 10:11:49 -07:00
return "x-amz-checksum-crc64nvme"
2025-02-12 21:29:13 +01:00
case ChecksumAlgorithmSHA1 :
2025-07-15 10:11:49 -07:00
return "x-amz-checksum-sha1"
2025-02-12 21:29:13 +01:00
case ChecksumAlgorithmSHA256 :
2025-07-15 10:11:49 -07:00
return "x-amz-checksum-sha256"
2025-02-12 21:29:13 +01:00
}
return ""
}
// getCheckSumWriter - get checksum writer.
func getCheckSumWriter ( checksumAlgorithm ChecksumAlgorithm ) hash . Hash {
switch checksumAlgorithm {
case ChecksumAlgorithmCRC32 :
return crc32 . NewIEEE ( )
case ChecksumAlgorithmCRC32C :
return crc32 . New ( crc32 . MakeTable ( crc32 . Castagnoli ) )
case ChecksumAlgorithmCRC64NVMe :
2025-04-23 19:31:03 +05:00
return crc64nvme . New ( )
2025-02-12 21:29:13 +01:00
case ChecksumAlgorithmSHA1 :
return sha1 . New ( )
case ChecksumAlgorithmSHA256 :
return sha256 . New ( )
}
return nil
}