request-signature-streaming-unsigned-trailer.go (6805B)
1 /* 2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage 3 * Copyright 2022 MinIO, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package signer 19 20 import ( 21 "bytes" 22 "fmt" 23 "io" 24 "net/http" 25 "strconv" 26 "strings" 27 "time" 28 ) 29 30 // getUnsignedChunkLength - calculates the length of chunk metadata 31 func getUnsignedChunkLength(chunkDataSize int64) int64 { 32 return int64(len(fmt.Sprintf("%x", chunkDataSize))) + 33 crlfLen + 34 chunkDataSize + 35 crlfLen 36 } 37 38 // getUSStreamLength - calculates the length of the overall stream (data + metadata) 39 func getUSStreamLength(dataLen, chunkSize int64, trailers http.Header) int64 { 40 if dataLen <= 0 { 41 return 0 42 } 43 44 chunksCount := int64(dataLen / chunkSize) 45 remainingBytes := int64(dataLen % chunkSize) 46 streamLen := int64(0) 47 streamLen += chunksCount * getUnsignedChunkLength(chunkSize) 48 if remainingBytes > 0 { 49 streamLen += getUnsignedChunkLength(remainingBytes) 50 } 51 streamLen += getUnsignedChunkLength(0) 52 if len(trailers) > 0 { 53 for name, placeholder := range trailers { 54 if len(placeholder) > 0 { 55 streamLen += int64(len(name) + len(trailerKVSeparator) + len(placeholder[0]) + 1) 56 } 57 } 58 streamLen += crlfLen 59 } 60 61 return streamLen 62 } 63 64 // prepareStreamingRequest - prepares a request with appropriate 65 // headers before computing the seed signature. 66 func prepareUSStreamingRequest(req *http.Request, sessionToken string, dataLen int64, timestamp time.Time) { 67 req.TransferEncoding = []string{"aws-chunked"} 68 if sessionToken != "" { 69 req.Header.Set("X-Amz-Security-Token", sessionToken) 70 } 71 72 req.Header.Set("X-Amz-Date", timestamp.Format(iso8601DateFormat)) 73 // Set content length with streaming signature for each chunk included. 74 req.ContentLength = getUSStreamLength(dataLen, int64(payloadChunkSize), req.Trailer) 75 } 76 77 // StreamingUSReader implements chunked upload signature as a reader on 78 // top of req.Body's ReaderCloser chunk header;data;... repeat 79 type StreamingUSReader struct { 80 contentLen int64 // Content-Length from req header 81 baseReadCloser io.ReadCloser // underlying io.Reader 82 bytesRead int64 // bytes read from underlying io.Reader 83 buf bytes.Buffer // holds signed chunk 84 chunkBuf []byte // holds raw data read from req Body 85 chunkBufLen int // no. of bytes read so far into chunkBuf 86 done bool // done reading the underlying reader to EOF 87 chunkNum int 88 totalChunks int 89 lastChunkSize int 90 trailer http.Header 91 } 92 93 // writeChunk - signs a chunk read from s.baseReader of chunkLen size. 94 func (s *StreamingUSReader) writeChunk(chunkLen int, addCrLf bool) { 95 s.buf.WriteString(strconv.FormatInt(int64(chunkLen), 16) + "\r\n") 96 97 // Write chunk data into streaming buffer 98 s.buf.Write(s.chunkBuf[:chunkLen]) 99 100 // Write the chunk trailer. 101 if addCrLf { 102 s.buf.Write([]byte("\r\n")) 103 } 104 105 // Reset chunkBufLen for next chunk read. 106 s.chunkBufLen = 0 107 s.chunkNum++ 108 } 109 110 // addSignedTrailer - adds a trailer with the provided headers, 111 // then signs a chunk and adds it to output. 112 func (s *StreamingUSReader) addTrailer(h http.Header) { 113 olen := len(s.chunkBuf) 114 s.chunkBuf = s.chunkBuf[:0] 115 for k, v := range h { 116 s.chunkBuf = append(s.chunkBuf, []byte(strings.ToLower(k)+trailerKVSeparator+v[0]+"\n")...) 117 } 118 119 s.buf.Write(s.chunkBuf) 120 s.buf.WriteString("\r\n\r\n") 121 122 // Reset chunkBufLen for next chunk read. 123 s.chunkBuf = s.chunkBuf[:olen] 124 s.chunkBufLen = 0 125 s.chunkNum++ 126 } 127 128 // StreamingUnsignedV4 - provides chunked upload 129 func StreamingUnsignedV4(req *http.Request, sessionToken string, dataLen int64, reqTime time.Time) *http.Request { 130 // Set headers needed for streaming signature. 131 prepareUSStreamingRequest(req, sessionToken, dataLen, reqTime) 132 133 if req.Body == nil { 134 req.Body = io.NopCloser(bytes.NewReader([]byte(""))) 135 } 136 137 stReader := &StreamingUSReader{ 138 baseReadCloser: req.Body, 139 chunkBuf: make([]byte, payloadChunkSize), 140 contentLen: dataLen, 141 chunkNum: 1, 142 totalChunks: int((dataLen+payloadChunkSize-1)/payloadChunkSize) + 1, 143 lastChunkSize: int(dataLen % payloadChunkSize), 144 } 145 if len(req.Trailer) > 0 { 146 stReader.trailer = req.Trailer 147 // Remove... 148 req.Trailer = nil 149 } 150 151 req.Body = stReader 152 153 return req 154 } 155 156 // Read - this method performs chunk upload signature providing a 157 // io.Reader interface. 158 func (s *StreamingUSReader) Read(buf []byte) (int, error) { 159 switch { 160 // After the last chunk is read from underlying reader, we 161 // never re-fill s.buf. 162 case s.done: 163 164 // s.buf will be (re-)filled with next chunk when has lesser 165 // bytes than asked for. 166 case s.buf.Len() < len(buf): 167 s.chunkBufLen = 0 168 for { 169 n1, err := s.baseReadCloser.Read(s.chunkBuf[s.chunkBufLen:]) 170 // Usually we validate `err` first, but in this case 171 // we are validating n > 0 for the following reasons. 172 // 173 // 1. n > 0, err is one of io.EOF, nil (near end of stream) 174 // A Reader returning a non-zero number of bytes at the end 175 // of the input stream may return either err == EOF or err == nil 176 // 177 // 2. n == 0, err is io.EOF (actual end of stream) 178 // 179 // Callers should always process the n > 0 bytes returned 180 // before considering the error err. 181 if n1 > 0 { 182 s.chunkBufLen += n1 183 s.bytesRead += int64(n1) 184 185 if s.chunkBufLen == payloadChunkSize || 186 (s.chunkNum == s.totalChunks-1 && 187 s.chunkBufLen == s.lastChunkSize) { 188 // Sign the chunk and write it to s.buf. 189 s.writeChunk(s.chunkBufLen, true) 190 break 191 } 192 } 193 if err != nil { 194 if err == io.EOF { 195 // No more data left in baseReader - last chunk. 196 // Done reading the last chunk from baseReader. 197 s.done = true 198 199 // bytes read from baseReader different than 200 // content length provided. 201 if s.bytesRead != s.contentLen { 202 return 0, fmt.Errorf("http: ContentLength=%d with Body length %d", s.contentLen, s.bytesRead) 203 } 204 205 // Sign the chunk and write it to s.buf. 206 s.writeChunk(0, len(s.trailer) == 0) 207 if len(s.trailer) > 0 { 208 // Trailer must be set now. 209 s.addTrailer(s.trailer) 210 } 211 break 212 } 213 return 0, err 214 } 215 216 } 217 } 218 return s.buf.Read(buf) 219 } 220 221 // Close - this method makes underlying io.ReadCloser's Close method available. 222 func (s *StreamingUSReader) Close() error { 223 return s.baseReadCloser.Close() 224 }