1 /*
2  * Copyright 2015-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */

15 package com.amazonaws.util;
16
17 import java.io.UnsupportedEncodingException;
18 import java.net.URI;
19 import java.net.URLDecoder;
20 import java.net.URLEncoder;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27
28 import com.amazonaws.SignableRequest;
29 import com.amazonaws.http.HttpMethodName;
30
31 public class SdkHttpUtils {
32
33     private static final String DEFAULT_ENCODING = "UTF-8";
34
35     /**
36      * Regex which matches any of the sequences that we need to fix up after
37      * URLEncoder.encode().
38      */

39     private static final Pattern ENCODED_CHARACTERS_PATTERN;
40     static {
41         StringBuilder pattern = new StringBuilder();
42
43         pattern
44             .append(Pattern.quote("+"))
45             .append("|")
46             .append(Pattern.quote("*"))
47             .append("|")
48             .append(Pattern.quote("%7E"))
49             .append("|")
50             .append(Pattern.quote("%2F"));
51
52         ENCODED_CHARACTERS_PATTERN = Pattern.compile(pattern.toString());
53     }
54
55     /**
56      * Encode a string for use in the path of a URL; uses URLEncoder.encode,
57      * (which encodes a string for use in the query portion of a URL), then
58      * applies some postfilters to fix things up per the RFC. Can optionally
59      * handle strings which are meant to encode a path (ie include '/'es
60      * which should NOT be escaped).
61      *
62      * @param value the value to encode
63      * @param path true if the value is intended to represent a path
64      * @return the encoded value
65      */

66     public static String urlEncode(final String value, final boolean path) {
67         if (value == null) {
68             return "";
69         }
70
71         try {
72             String encoded = URLEncoder.encode(value, DEFAULT_ENCODING);
73
74             Matcher matcher = ENCODED_CHARACTERS_PATTERN.matcher(encoded);
75             StringBuffer buffer = new StringBuffer(encoded.length());
76
77             while (matcher.find()) {
78                 String replacement = matcher.group(0);
79
80                 if ("+".equals(replacement)) {
81                     replacement = "%20";
82                 } else if ("*".equals(replacement)) {
83                     replacement = "%2A";
84                 } else if ("%7E".equals(replacement)) {
85                     replacement = "~";
86                 } else if (path && "%2F".equals(replacement)) {
87                     replacement = "/";
88                 }
89
90                 matcher.appendReplacement(buffer, replacement);
91             }
92
93             matcher.appendTail(buffer);
94             return buffer.toString();
95
96         } catch (UnsupportedEncodingException ex) {
97             throw new RuntimeException(ex);
98         }
99     }
100
101     /**
102      * Decode a string for use in the path of a URL; uses URLDecoder.decode,
103      * which decodes a string for use in the query portion of a URL.
104      *
105      * @param value The value to decode
106      * @return The decoded value if parameter is not null, otherwise, null is returned.
107      */

108     public static String urlDecode(final String value) {
109         if (value == null) {
110             return null;
111         }
112
113         try {
114             return URLDecoder.decode(value, DEFAULT_ENCODING);
115
116         } catch (UnsupportedEncodingException ex) {
117             throw new RuntimeException(ex);
118         }
119     }
120
121     /**
122      * Returns true if the specified URI is using a non-standard port (i.e. any
123      * port other than 80 for HTTP URIs or any port other than 443 for HTTPS
124      * URIs).
125      *
126      * @param uri
127      *
128      * @return True if the specified URI is using a non-standard port, otherwise
129      *         false.
130      */

131     public static boolean isUsingNonDefaultPort(URI uri) {
132         String scheme = StringUtils.lowerCase(uri.getScheme());
133         int port = uri.getPort();
134
135         if (port <= 0) return false;
136         if (scheme.equals("http") && port == 80) return false;
137         if (scheme.equals("https") && port == 443) return false;
138
139         return true;
140     }
141
142     public static boolean usePayloadForQueryParameters(SignableRequest<?> request) {
143         boolean requestIsPOST = HttpMethodName.POST.equals(request.getHttpMethod());
144         boolean requestHasNoPayload = (request.getContent() == null);
145
146         return requestIsPOST && requestHasNoPayload;
147     }
148
149     /**
150      * Creates an encoded query string from all the parameters in the specified
151      * request.
152      *
153      * @param request
154      *            The request containing the parameters to encode.
155      *
156      * @return Null if no parameters were present, otherwise the encoded query
157      *         string for the parameters present in the specified request.
158      */

159     public static String encodeParameters(SignableRequest<?> request) {
160
161         final Map<String, List<String>> requestParams = request.getParameters();
162
163         if (requestParams.isEmpty()) return null;
164
165         final List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
166
167         for (Entry<String, List<String>> entry : requestParams.entrySet()) {
168             String parameterName = entry.getKey();
169             for (String value : entry.getValue()) {
170                 nameValuePairs
171                     .add(new BasicNameValuePair(parameterName, value));
172             }
173         }
174
175         return URLEncodedUtils.format(nameValuePairs, DEFAULT_ENCODING);
176     }
177
178     /**
179      * Append the given path to the given baseUri.
180      * By default, all slash characters in path will not be url-encoded.
181      */

182     public static String appendUri(String baseUri, String path) {
183         return appendUri(baseUri, path, false);
184     }
185
186     /**
187      * Append the given path to the given baseUri.
188      *
189      * @param baseUri The URI to append to (required, may be relative)
190      * @param path The path to append (may be null or empty).  Path should be pre-encoded.
191      * @param escapeDoubleSlash Whether double-slash in the path should be escaped to "/%2F"
192      * @return The baseUri with the path appended
193      */

194     public static String appendUri(final String baseUri, String path, final boolean escapeDoubleSlash) {
195         String resultUri = baseUri;
196         if (path != null && path.length() > 0) {
197             if (path.startsWith("/")) {
198                 // trim the trailing slash in baseUri, since the path already starts with a slash
199                 if (resultUri.endsWith("/")) {
200                     resultUri = resultUri.substring(0, resultUri.length() - 1);
201                 }
202             } else if (!resultUri.endsWith("/")) {
203                 resultUri += "/";
204             }
205             if (escapeDoubleSlash) {
206                 resultUri += path.replace("//""/%2F");
207             } else {
208                 resultUri += path;
209             }
210         } else if (!resultUri.endsWith("/")) {
211             resultUri += "/";
212         }
213
214         return resultUri;
215     }
216 }