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