1 package http
2 3 import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "strings"
10 11 iointernal "github.com/aws/smithy-go/transport/http/internal/io"
12 )
13 14 // Request provides the HTTP specific request structure for HTTP specific
15 // middleware steps to use to serialize input, and send an operation's request.
16 type Request struct {
17 *http.Request
18 stream io.Reader
19 isStreamSeekable bool
20 streamStartPos int64
21 }
22 23 // NewStackRequest returns an initialized request ready to be populated with the
24 // HTTP request details. Returns empty interface so the function can be used as
25 // a parameter to the Smithy middleware Stack constructor.
26 func NewStackRequest() interface{} {
27 return &Request{
28 Request: &http.Request{
29 URL: &url.URL{},
30 Header: http.Header{},
31 ContentLength: -1, // default to unknown length
32 },
33 }
34 }
35 36 // IsHTTPS returns if the request is HTTPS. Returns false if no endpoint URL is set.
37 func (r *Request) IsHTTPS() bool {
38 if r.URL == nil {
39 return false
40 }
41 return strings.EqualFold(r.URL.Scheme, "https")
42 }
43 44 // Clone returns a deep copy of the Request for the new context. A reference to
45 // the Stream is copied, but the underlying stream is not copied.
46 func (r *Request) Clone() *Request {
47 rc := *r
48 rc.Request = rc.Request.Clone(context.TODO())
49 return &rc
50 }
51 52 // StreamLength returns the number of bytes of the serialized stream attached
53 // to the request and ok set. If the length cannot be determined, an error will
54 // be returned.
55 func (r *Request) StreamLength() (size int64, ok bool, err error) {
56 return streamLength(r.stream, r.isStreamSeekable, r.streamStartPos)
57 }
58 59 func streamLength(stream io.Reader, seekable bool, startPos int64) (size int64, ok bool, err error) {
60 if stream == nil {
61 return 0, true, nil
62 }
63 64 if l, ok := stream.(interface{ Len() int }); ok {
65 return int64(l.Len()), true, nil
66 }
67 68 if !seekable {
69 return 0, false, nil
70 }
71 72 s := stream.(io.Seeker)
73 endOffset, err := s.Seek(0, io.SeekEnd)
74 if err != nil {
75 return 0, false, err
76 }
77 78 // The reason to seek to streamStartPos instead of 0 is to ensure that the
79 // SDK only sends the stream from the starting position the user's
80 // application provided it to the SDK at. For example application opens a
81 // file, and wants to skip the first N bytes uploading the rest. The
82 // application would move the file's offset N bytes, then hand it off to
83 // the SDK to send the remaining. The SDK should respect that initial offset.
84 _, err = s.Seek(startPos, io.SeekStart)
85 if err != nil {
86 return 0, false, err
87 }
88 89 return endOffset - startPos, true, nil
90 }
91 92 // RewindStream will rewind the io.Reader to the relative start position if it
93 // is an io.Seeker.
94 func (r *Request) RewindStream() error {
95 // If there is no stream there is nothing to rewind.
96 if r.stream == nil {
97 return nil
98 }
99 100 if !r.isStreamSeekable {
101 return fmt.Errorf("request stream is not seekable")
102 }
103 _, err := r.stream.(io.Seeker).Seek(r.streamStartPos, io.SeekStart)
104 return err
105 }
106 107 // GetStream returns the request stream io.Reader if a stream is set. If no
108 // stream is present nil will be returned.
109 func (r *Request) GetStream() io.Reader {
110 return r.stream
111 }
112 113 // IsStreamSeekable returns whether the stream is seekable.
114 func (r *Request) IsStreamSeekable() bool {
115 return r.isStreamSeekable
116 }
117 118 // SetStream returns a clone of the request with the stream set to the provided
119 // reader. May return an error if the provided reader is seekable but returns
120 // an error.
121 func (r *Request) SetStream(reader io.Reader) (rc *Request, err error) {
122 rc = r.Clone()
123 124 if reader == http.NoBody {
125 reader = nil
126 }
127 128 var isStreamSeekable bool
129 var streamStartPos int64
130 switch v := reader.(type) {
131 case io.Seeker:
132 n, err := v.Seek(0, io.SeekCurrent)
133 if err != nil {
134 return r, err
135 }
136 isStreamSeekable = true
137 streamStartPos = n
138 default:
139 // If the stream length can be determined, and is determined to be empty,
140 // use a nil stream to prevent confusion between empty vs not-empty
141 // streams.
142 length, ok, err := streamLength(reader, false, 0)
143 if err != nil {
144 return nil, err
145 } else if ok && length == 0 {
146 reader = nil
147 }
148 }
149 150 rc.stream = reader
151 rc.isStreamSeekable = isStreamSeekable
152 rc.streamStartPos = streamStartPos
153 154 return rc, err
155 }
156 157 // Build returns a build standard HTTP request value from the Smithy request.
158 // The request's stream is wrapped in a safe container that allows it to be
159 // reused for subsequent attempts.
160 func (r *Request) Build(ctx context.Context) *http.Request {
161 req := r.Request.Clone(ctx)
162 163 if r.stream == nil && req.ContentLength == -1 {
164 req.ContentLength = 0
165 }
166 167 switch stream := r.stream.(type) {
168 case *io.PipeReader:
169 req.Body = io.NopCloser(stream)
170 req.ContentLength = -1
171 default:
172 // HTTP Client Request must only have a non-nil body if the
173 // ContentLength is explicitly unknown (-1) or non-zero. The HTTP
174 // Client will interpret a non-nil body and ContentLength 0 as
175 // "unknown". This is unwanted behavior.
176 if req.ContentLength != 0 && r.stream != nil {
177 req.Body = iointernal.NewSafeReadCloser(io.NopCloser(stream))
178 }
179 }
180 181 return req
182 }
183 184 // RequestCloner is a function that can take an input request type and clone the request
185 // for use in a subsequent retry attempt.
186 func RequestCloner(v interface{}) interface{} {
187 return v.(*Request).Clone()
188 }
189