1 package com.github.davidmoten.aws.lw.client;
2
3 import java.io.IOException;
4 import java.io.UnsupportedEncodingException;
5 import java.net.URL;
6 import java.net.URLDecoder;
7 import java.util.ArrayList;
8 import java.util.Collections;
9 import java.util.HashMap;
10 import java.util.List;
11 import java.util.Map;
12 import java.util.Optional;
13 import java.util.stream.Collectors;
14
15 import com.github.davidmoten.aws.lw.client.internal.Clock;
16 import com.github.davidmoten.aws.lw.client.internal.auth.AwsSignatureVersion4;
17 import com.github.davidmoten.aws.lw.client.internal.util.Preconditions;
18 import com.github.davidmoten.aws.lw.client.internal.util.Util;
19
20 final class RequestHelper {
21
22 private RequestHelper() {
23
24 }
25
26 static void put(Map<String, List<String>> map, String name, String value) {
27 Preconditions.checkNotNull(map);
28 Preconditions.checkNotNull(name);
29 Preconditions.checkNotNull(value);
30 List<String> list = map.get(name);
31 if (list == null) {
32 list = new ArrayList<>();
33 map.put(name, list);
34 }
35 list.add(value);
36 }
37
38 static Map<String, String> combineHeaders(Map<String, List<String>> headers) {
39 Preconditions.checkNotNull(headers);
40 return headers.entrySet().stream().collect(Collectors.toMap(x -> x.getKey(),
41 x -> x.getValue().stream().collect(Collectors.joining(","))));
42 }
43
44 static String presignedUrl(Clock clock, String url, String method, Map<String, String> headers,
45 byte[] requestBody, String serviceName, Optional<String> regionName, Credentials credentials,
46 int connectTimeoutMs, int readTimeoutMs, long expirySeconds, boolean signPayload) {
47
48
49 URL endpointUrl = Util.toUrl(url);
50
51 Map<String, String> h = new HashMap<>(headers);
52 final String contentHashString;
53 if (isEmpty(requestBody)) {
54 contentHashString = AwsSignatureVersion4.UNSIGNED_PAYLOAD;
55 h.put("x-amz-content-sha256", "");
56 } else if (!signPayload) {
57 contentHashString = AwsSignatureVersion4.UNSIGNED_PAYLOAD;
58 h.put("x-amz-content-sha256", contentHashString);
59 } else {
60
61 byte[] contentHash = Util.sha256(requestBody);
62 contentHashString = Util.toHex(contentHash);
63 h.put("content-length", "" + requestBody.length);
64 h.put("x-amz-content-sha256", contentHashString);
65 }
66
67 List<Parameter> parameters = extractQueryParameters(endpointUrl);
68
69 Map<String, String> q = new HashMap<>();
70 parameters.forEach(p -> q.put(p.name, p.value));
71
72
73
74
75
76 q.put("X-Amz-Expires", "" + expirySeconds);
77
78 String authorizationQueryParameters = AwsSignatureVersion4.computeSignatureForQueryAuth(
79 endpointUrl, method, serviceName, regionName, clock, h, q, contentHashString,
80 credentials.accessKey(), credentials.secretKey(), credentials.sessionToken());
81
82
83
84 String u = endpointUrl.toString();
85 final String presignedUrl;
86 if (u.contains("?")) {
87 presignedUrl = u + "&" + authorizationQueryParameters;
88 } else {
89 presignedUrl = u + "?" + authorizationQueryParameters;
90 }
91 return presignedUrl;
92 }
93
94 private static void includeTokenIfPresent(Credentials credentials, Map<String, String> h) {
95 if (credentials.sessionToken().isPresent()) {
96 h.put("x-amz-security-token", credentials.sessionToken().get());
97 }
98 }
99
100 static ResponseInputStream request(Clock clock, HttpClient httpClient, String url,
101 HttpMethod method, Map<String, String> headers, byte[] requestBody, String serviceName,
102 Optional<String> regionName, Credentials credentials, int connectTimeoutMs, int readTimeoutMs,
103 boolean signPayload) throws IOException {
104
105
106 URL endpointUrl = Util.toUrl(url);
107
108 Map<String, String> h = new HashMap<>(headers);
109 final String contentHashString;
110 if (isEmpty(requestBody)) {
111 contentHashString = AwsSignatureVersion4.EMPTY_BODY_SHA256;
112 } else {
113 if (!signPayload) {
114 contentHashString = AwsSignatureVersion4.UNSIGNED_PAYLOAD;
115 } else {
116
117 byte[] contentHash = Util.sha256(requestBody);
118 contentHashString = Util.toHex(contentHash);
119 }
120 h.put("content-length", "" + requestBody.length);
121 }
122 h.put("x-amz-content-sha256", contentHashString);
123
124 includeTokenIfPresent(credentials, h);
125
126 List<Parameter> parameters = extractQueryParameters(endpointUrl);
127
128 Map<String, String> q = new HashMap<>();
129 parameters.forEach(p -> q.put(p.name, p.value));
130 String authorization = AwsSignatureVersion4.computeSignatureForAuthorizationHeader(
131 endpointUrl, method.toString(), serviceName, regionName.orElse("us-east-1"), clock, h, q,
132 contentHashString, credentials.accessKey(), credentials.secretKey());
133
134
135
136 h.put("Authorization", authorization);
137 return httpClient.request(endpointUrl, method.toString(), h, requestBody, connectTimeoutMs,
138 readTimeoutMs);
139 }
140
141 private static List<Parameter> extractQueryParameters(URL endpointUrl) {
142 String query = endpointUrl.getQuery();
143 if (query == null) {
144 return Collections.emptyList();
145 } else {
146 return extractQueryParameters(query);
147 }
148 }
149
150 private static final char QUERY_PARAMETER_SEPARATOR = '&';
151 private static final char QUERY_PARAMETER_VALUE_SEPARATOR = '=';
152
153
154
155
156
157
158
159
160
161
162
163 static List<Parameter> extractQueryParameters(String rawQuery) {
164 List<Parameter> results = new ArrayList<>();
165 int endIndex = rawQuery.length() - 1;
166 int index = 0;
167 while (index <= endIndex) {
168
169
170
171
172
173
174
175 String name;
176 String value;
177 int nameValueSeparatorIndex = rawQuery.indexOf(QUERY_PARAMETER_VALUE_SEPARATOR, index);
178 if (nameValueSeparatorIndex < 0) {
179
180 name = rawQuery.substring(index);
181 value = null;
182
183 index = endIndex + 1;
184 } else {
185 int parameterSeparatorIndex = rawQuery.indexOf(QUERY_PARAMETER_SEPARATOR,
186 nameValueSeparatorIndex);
187 if (parameterSeparatorIndex < 0) {
188 parameterSeparatorIndex = endIndex + 1;
189 }
190 name = rawQuery.substring(index, nameValueSeparatorIndex);
191 value = rawQuery.substring(nameValueSeparatorIndex + 1, parameterSeparatorIndex);
192
193 index = parameterSeparatorIndex + 1;
194 }
195
196
197 results.add(parameter(name, value, "UTF-8"));
198 }
199 return results;
200 }
201
202
203 static Parameter parameter(String name, String value, String charset) {
204 try {
205 return new Parameter(URLDecoder.decode(name, charset),
206 value == null ? value : URLDecoder.decode(value, charset));
207 } catch (UnsupportedEncodingException e) {
208 throw new RuntimeException(e);
209 }
210 }
211
212
213 static final class Parameter {
214 final String name;
215 final String value;
216
217 Parameter(String name, String value) {
218 this.name = name;
219 this.value = value;
220 }
221 }
222
223 static boolean isEmpty(byte[] array) {
224 return array == null || array.length == 0;
225 }
226
227 }