]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | // Package rest provides RESTful serialization of AWS requests and responses. |
2 | package rest | |
3 | ||
4 | import ( | |
5 | "bytes" | |
6 | "encoding/base64" | |
bae9f6d2 JC |
7 | "fmt" |
8 | "io" | |
9 | "net/http" | |
10 | "net/url" | |
11 | "path" | |
12 | "reflect" | |
13 | "strconv" | |
14 | "strings" | |
15 | "time" | |
16 | ||
17 | "github.com/aws/aws-sdk-go/aws" | |
18 | "github.com/aws/aws-sdk-go/aws/awserr" | |
19 | "github.com/aws/aws-sdk-go/aws/request" | |
15c0b25d | 20 | "github.com/aws/aws-sdk-go/private/protocol" |
bae9f6d2 JC |
21 | ) |
22 | ||
bae9f6d2 JC |
23 | // Whether the byte value can be sent without escaping in AWS URLs |
24 | var noEscape [256]bool | |
25 | ||
26 | var errValueNotSet = fmt.Errorf("value not set") | |
27 | ||
28 | func init() { | |
29 | for i := 0; i < len(noEscape); i++ { | |
30 | // AWS expects every character except these to be escaped | |
31 | noEscape[i] = (i >= 'A' && i <= 'Z') || | |
32 | (i >= 'a' && i <= 'z') || | |
33 | (i >= '0' && i <= '9') || | |
34 | i == '-' || | |
35 | i == '.' || | |
36 | i == '_' || | |
37 | i == '~' | |
38 | } | |
39 | } | |
40 | ||
41 | // BuildHandler is a named request handler for building rest protocol requests | |
42 | var BuildHandler = request.NamedHandler{Name: "awssdk.rest.Build", Fn: Build} | |
43 | ||
44 | // Build builds the REST component of a service request. | |
45 | func Build(r *request.Request) { | |
46 | if r.ParamsFilled() { | |
47 | v := reflect.ValueOf(r.Params).Elem() | |
48 | buildLocationElements(r, v, false) | |
49 | buildBody(r, v) | |
50 | } | |
51 | } | |
52 | ||
53 | // BuildAsGET builds the REST component of a service request with the ability to hoist | |
54 | // data from the body. | |
55 | func BuildAsGET(r *request.Request) { | |
56 | if r.ParamsFilled() { | |
57 | v := reflect.ValueOf(r.Params).Elem() | |
58 | buildLocationElements(r, v, true) | |
59 | buildBody(r, v) | |
60 | } | |
61 | } | |
62 | ||
63 | func buildLocationElements(r *request.Request, v reflect.Value, buildGETQuery bool) { | |
64 | query := r.HTTPRequest.URL.Query() | |
65 | ||
66 | // Setup the raw path to match the base path pattern. This is needed | |
67 | // so that when the path is mutated a custom escaped version can be | |
68 | // stored in RawPath that will be used by the Go client. | |
69 | r.HTTPRequest.URL.RawPath = r.HTTPRequest.URL.Path | |
70 | ||
71 | for i := 0; i < v.NumField(); i++ { | |
72 | m := v.Field(i) | |
73 | if n := v.Type().Field(i).Name; n[0:1] == strings.ToLower(n[0:1]) { | |
74 | continue | |
75 | } | |
76 | ||
77 | if m.IsValid() { | |
78 | field := v.Type().Field(i) | |
79 | name := field.Tag.Get("locationName") | |
80 | if name == "" { | |
81 | name = field.Name | |
82 | } | |
83 | if kind := m.Kind(); kind == reflect.Ptr { | |
84 | m = m.Elem() | |
85 | } else if kind == reflect.Interface { | |
86 | if !m.Elem().IsValid() { | |
87 | continue | |
88 | } | |
89 | } | |
90 | if !m.IsValid() { | |
91 | continue | |
92 | } | |
93 | if field.Tag.Get("ignore") != "" { | |
94 | continue | |
95 | } | |
96 | ||
97 | var err error | |
98 | switch field.Tag.Get("location") { | |
99 | case "headers": // header maps | |
100 | err = buildHeaderMap(&r.HTTPRequest.Header, m, field.Tag) | |
101 | case "header": | |
102 | err = buildHeader(&r.HTTPRequest.Header, m, name, field.Tag) | |
103 | case "uri": | |
104 | err = buildURI(r.HTTPRequest.URL, m, name, field.Tag) | |
105 | case "querystring": | |
106 | err = buildQueryString(query, m, name, field.Tag) | |
107 | default: | |
108 | if buildGETQuery { | |
109 | err = buildQueryString(query, m, name, field.Tag) | |
110 | } | |
111 | } | |
112 | r.Error = err | |
113 | } | |
114 | if r.Error != nil { | |
115 | return | |
116 | } | |
117 | } | |
118 | ||
119 | r.HTTPRequest.URL.RawQuery = query.Encode() | |
120 | if !aws.BoolValue(r.Config.DisableRestProtocolURICleaning) { | |
121 | cleanPath(r.HTTPRequest.URL) | |
122 | } | |
123 | } | |
124 | ||
125 | func buildBody(r *request.Request, v reflect.Value) { | |
126 | if field, ok := v.Type().FieldByName("_"); ok { | |
127 | if payloadName := field.Tag.Get("payload"); payloadName != "" { | |
128 | pfield, _ := v.Type().FieldByName(payloadName) | |
129 | if ptag := pfield.Tag.Get("type"); ptag != "" && ptag != "structure" { | |
130 | payload := reflect.Indirect(v.FieldByName(payloadName)) | |
131 | if payload.IsValid() && payload.Interface() != nil { | |
132 | switch reader := payload.Interface().(type) { | |
133 | case io.ReadSeeker: | |
134 | r.SetReaderBody(reader) | |
135 | case []byte: | |
136 | r.SetBufferBody(reader) | |
137 | case string: | |
138 | r.SetStringBody(reader) | |
139 | default: | |
140 | r.Error = awserr.New("SerializationError", | |
141 | "failed to encode REST request", | |
142 | fmt.Errorf("unknown payload type %s", payload.Type())) | |
143 | } | |
144 | } | |
145 | } | |
146 | } | |
147 | } | |
148 | } | |
149 | ||
150 | func buildHeader(header *http.Header, v reflect.Value, name string, tag reflect.StructTag) error { | |
151 | str, err := convertType(v, tag) | |
152 | if err == errValueNotSet { | |
153 | return nil | |
154 | } else if err != nil { | |
155 | return awserr.New("SerializationError", "failed to encode REST request", err) | |
156 | } | |
157 | ||
107c1cdb ND |
158 | name = strings.TrimSpace(name) |
159 | str = strings.TrimSpace(str) | |
160 | ||
bae9f6d2 JC |
161 | header.Add(name, str) |
162 | ||
163 | return nil | |
164 | } | |
165 | ||
166 | func buildHeaderMap(header *http.Header, v reflect.Value, tag reflect.StructTag) error { | |
167 | prefix := tag.Get("locationName") | |
168 | for _, key := range v.MapKeys() { | |
169 | str, err := convertType(v.MapIndex(key), tag) | |
170 | if err == errValueNotSet { | |
171 | continue | |
172 | } else if err != nil { | |
173 | return awserr.New("SerializationError", "failed to encode REST request", err) | |
174 | ||
175 | } | |
107c1cdb ND |
176 | keyStr := strings.TrimSpace(key.String()) |
177 | str = strings.TrimSpace(str) | |
bae9f6d2 | 178 | |
107c1cdb | 179 | header.Add(prefix+keyStr, str) |
bae9f6d2 JC |
180 | } |
181 | return nil | |
182 | } | |
183 | ||
184 | func buildURI(u *url.URL, v reflect.Value, name string, tag reflect.StructTag) error { | |
185 | value, err := convertType(v, tag) | |
186 | if err == errValueNotSet { | |
187 | return nil | |
188 | } else if err != nil { | |
189 | return awserr.New("SerializationError", "failed to encode REST request", err) | |
190 | } | |
191 | ||
192 | u.Path = strings.Replace(u.Path, "{"+name+"}", value, -1) | |
193 | u.Path = strings.Replace(u.Path, "{"+name+"+}", value, -1) | |
194 | ||
195 | u.RawPath = strings.Replace(u.RawPath, "{"+name+"}", EscapePath(value, true), -1) | |
196 | u.RawPath = strings.Replace(u.RawPath, "{"+name+"+}", EscapePath(value, false), -1) | |
197 | ||
198 | return nil | |
199 | } | |
200 | ||
201 | func buildQueryString(query url.Values, v reflect.Value, name string, tag reflect.StructTag) error { | |
202 | switch value := v.Interface().(type) { | |
203 | case []*string: | |
204 | for _, item := range value { | |
205 | query.Add(name, *item) | |
206 | } | |
207 | case map[string]*string: | |
208 | for key, item := range value { | |
209 | query.Add(key, *item) | |
210 | } | |
211 | case map[string][]*string: | |
212 | for key, items := range value { | |
213 | for _, item := range items { | |
214 | query.Add(key, *item) | |
215 | } | |
216 | } | |
217 | default: | |
218 | str, err := convertType(v, tag) | |
219 | if err == errValueNotSet { | |
220 | return nil | |
221 | } else if err != nil { | |
222 | return awserr.New("SerializationError", "failed to encode REST request", err) | |
223 | } | |
224 | query.Set(name, str) | |
225 | } | |
226 | ||
227 | return nil | |
228 | } | |
229 | ||
230 | func cleanPath(u *url.URL) { | |
231 | hasSlash := strings.HasSuffix(u.Path, "/") | |
232 | ||
233 | // clean up path, removing duplicate `/` | |
234 | u.Path = path.Clean(u.Path) | |
235 | u.RawPath = path.Clean(u.RawPath) | |
236 | ||
237 | if hasSlash && !strings.HasSuffix(u.Path, "/") { | |
238 | u.Path += "/" | |
239 | u.RawPath += "/" | |
240 | } | |
241 | } | |
242 | ||
243 | // EscapePath escapes part of a URL path in Amazon style | |
244 | func EscapePath(path string, encodeSep bool) string { | |
245 | var buf bytes.Buffer | |
246 | for i := 0; i < len(path); i++ { | |
247 | c := path[i] | |
248 | if noEscape[c] || (c == '/' && !encodeSep) { | |
249 | buf.WriteByte(c) | |
250 | } else { | |
251 | fmt.Fprintf(&buf, "%%%02X", c) | |
252 | } | |
253 | } | |
254 | return buf.String() | |
255 | } | |
256 | ||
15c0b25d | 257 | func convertType(v reflect.Value, tag reflect.StructTag) (str string, err error) { |
bae9f6d2 JC |
258 | v = reflect.Indirect(v) |
259 | if !v.IsValid() { | |
260 | return "", errValueNotSet | |
261 | } | |
262 | ||
bae9f6d2 JC |
263 | switch value := v.Interface().(type) { |
264 | case string: | |
265 | str = value | |
266 | case []byte: | |
267 | str = base64.StdEncoding.EncodeToString(value) | |
268 | case bool: | |
269 | str = strconv.FormatBool(value) | |
270 | case int64: | |
271 | str = strconv.FormatInt(value, 10) | |
272 | case float64: | |
273 | str = strconv.FormatFloat(value, 'f', -1, 64) | |
274 | case time.Time: | |
15c0b25d AP |
275 | format := tag.Get("timestampFormat") |
276 | if len(format) == 0 { | |
277 | format = protocol.RFC822TimeFormatName | |
278 | if tag.Get("location") == "querystring" { | |
279 | format = protocol.ISO8601TimeFormatName | |
280 | } | |
281 | } | |
282 | str = protocol.FormatTime(format, value) | |
bae9f6d2 | 283 | case aws.JSONValue: |
15c0b25d AP |
284 | if len(value) == 0 { |
285 | return "", errValueNotSet | |
bae9f6d2 | 286 | } |
15c0b25d | 287 | escaping := protocol.NoEscape |
bae9f6d2 | 288 | if tag.Get("location") == "header" { |
15c0b25d AP |
289 | escaping = protocol.Base64Escape |
290 | } | |
291 | str, err = protocol.EncodeJSONValue(value, escaping) | |
292 | if err != nil { | |
293 | return "", fmt.Errorf("unable to encode JSONValue, %v", err) | |
bae9f6d2 JC |
294 | } |
295 | default: | |
15c0b25d | 296 | err := fmt.Errorf("unsupported value for param %v (%s)", v.Interface(), v.Type()) |
bae9f6d2 JC |
297 | return "", err |
298 | } | |
299 | return str, nil | |
300 | } |