1 /*
2  * Copyright (c) 2016-2019. 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.http.apache.utils;
16
17 import com.amazonaws.Request;
18 import com.amazonaws.SdkClientException;
19 import com.amazonaws.http.HttpResponse;
20 import com.amazonaws.http.settings.HttpClientSettings;
21 import com.amazonaws.util.FakeIOException;
22 import com.amazonaws.util.ReflectionMethodInvoker;
23
24 import org.apache.commons.logging.Log;
25 import org.apache.commons.logging.LogFactory;
26 import org.apache.http.Header;
27 import org.apache.http.HttpEntity;
28 import org.apache.http.HttpHost;
29 import org.apache.http.HttpStatus;
30 import org.apache.http.auth.AuthScope;
31 import org.apache.http.auth.Credentials;
32 import org.apache.http.auth.NTCredentials;
33 import org.apache.http.client.AuthCache;
34 import org.apache.http.client.CredentialsProvider;
35 import org.apache.http.client.config.RequestConfig;
36 import org.apache.http.client.methods.HttpRequestBase;
37 import org.apache.http.client.protocol.HttpClientContext;
38 import org.apache.http.entity.BufferedHttpEntity;
39 import org.apache.http.entity.StringEntity;
40 import org.apache.http.impl.auth.BasicScheme;
41 import org.apache.http.impl.client.BasicAuthCache;
42 import org.apache.http.impl.client.BasicCredentialsProvider;
43
44 import java.io.IOException;
45 import java.io.UnsupportedEncodingException;
46 import java.util.Map;
47 import org.apache.http.protocol.HttpContext;
48
49 public class ApacheUtils {
50     private static final Log log = LogFactory.getLog(ApacheUtils.class);
51
52     private static final ReflectionMethodInvoker<RequestConfig.Builder, RequestConfig.Builder> normalizeUriInvoker;
53
54     static {
55         // Attempt to initialize the invoker once on class-load. If it fails, it will not be attempted again, but we'll
56         // use that opportunity to log a warning.
57         normalizeUriInvoker =
58             new ReflectionMethodInvoker<RequestConfig.Builder, RequestConfig.Builder>(RequestConfig.Builder.class,
59                                                                                       RequestConfig.Builder.class,
60                                                                                       "setNormalizeUri",
61                                                                                       boolean.class);
62
63         try {
64             normalizeUriInvoker.initialize();
65         } catch (NoSuchMethodException ignored) {
66             noSuchMethodThrownByNormalizeUriInvoker();
67         }
68     }
69
70     private final boolean normalizeUriMethodNotFound = false;
71
72     /**
73      * Checks if the request was successful or not based on the status code.
74      *
75      * @param response HTTP response
76      * @return True if the request was successful (i.e. has a 2xx status code), false otherwise.
77      */

78     public static boolean isRequestSuccessful(org.apache.http.HttpResponse response) {
79         int status = response.getStatusLine().getStatusCode();
80         return status / 100 == HttpStatus.SC_OK / 100;
81     }
82
83     /**
84      * Creates and initializes an HttpResponse object suitable to be passed to an HTTP response
85      * handler object.
86      *
87      * @param request Marshalled request object.
88      * @param method  The HTTP method that was invoked to get the response.
89      * @param context The HTTP context associated with the request and response.
90      * @return The new, initialized HttpResponse object ready to be passed to an HTTP response
91      * handler object.
92      * @throws IOException If there were any problems getting any response information from the
93      *                     HttpClient method object.
94      */

95     public static HttpResponse createResponse(Request<?> request,
96                                         HttpRequestBase method,
97                                         org.apache.http.HttpResponse apacheHttpResponse,
98                                         HttpContext context) throws IOException {
99         HttpResponse httpResponse = new HttpResponse(request, method, context);
100
101         if (apacheHttpResponse.getEntity() != null) {
102             httpResponse.setContent(apacheHttpResponse.getEntity().getContent());
103         }
104
105         httpResponse.setStatusCode(apacheHttpResponse.getStatusLine().getStatusCode());
106         httpResponse.setStatusText(apacheHttpResponse.getStatusLine().getReasonPhrase());
107         for (Header header : apacheHttpResponse.getAllHeaders()) {
108             httpResponse.addHeader(header.getName(), header.getValue());
109         }
110
111         return httpResponse;
112     }
113
114     /**
115      * Utility function for creating a new StringEntity and wrapping any errors
116      * as a SdkClientException.
117      *
118      * @param s The string contents of the returned HTTP entity.
119      * @return A new StringEntity with the specified contents.
120      */

121     public static HttpEntity newStringEntity(String s) {
122         try {
123             return new StringEntity(s);
124         } catch (UnsupportedEncodingException e) {
125             throw new SdkClientException("Unable to create HTTP entity: " + e.getMessage(), e);
126         }
127     }
128
129     /**
130      * Utility function for creating a new BufferedEntity and wrapping any errors
131      * as a SdkClientException.
132      *
133      * @param entity The HTTP entity to wrap with a buffered HTTP entity.
134      * @return A new BufferedHttpEntity wrapping the specified entity.
135      * @throws FakeIOException only for test simulation
136      */

137     public static HttpEntity newBufferedHttpEntity(HttpEntity entity) throws
138             FakeIOException {
139         try {
140             return new BufferedHttpEntity(entity);
141         } catch (FakeIOException e) {
142             // Only for test simulation.
143             throw e;
144         } catch (IOException e) {
145             throw new SdkClientException("Unable to create HTTP entity: " + e.getMessage(), e);
146         }
147     }
148
149     /**
150      * Returns a new HttpClientContext used for request execution.
151      */

152     public static HttpClientContext newClientContext(HttpClientSettings settings,
153                                                      Map<String, ? extends Object>
154                                                              attributes) {
155         final HttpClientContext clientContext = new HttpClientContext();
156
157         if (attributes != null && !attributes.isEmpty()) {
158             for (Map.Entry<String, ?> entry : attributes.entrySet()) {
159                 clientContext.setAttribute(entry.getKey(), entry.getValue());
160             }
161         }
162
163         addPreemptiveAuthenticationProxy(clientContext, settings);
164
165         RequestConfig.Builder builder = RequestConfig.custom();
166         disableNormalizeUri(builder);
167
168         clientContext.setRequestConfig(builder.build());
169         clientContext.setAttribute(HttpContextUtils.DISABLE_SOCKET_PROXY_PROPERTY, settings.disableSocketProxy());
170         return clientContext;
171
172     }
173
174     /**
175      * From Apache v4.5.8, normalization should be disabled or AWS requests with special characters in URI path will fail
176      * with Signature Errors.
177      * <p>
178      *    setNormalizeUri is added only in 4.5.8, so customers using the latest version of SDK with old versions (4.5.6 or less)
179      *    of Apache httpclient will see NoSuchMethodError. Hence this method will suppress the error.
180      *
181      *    Do not use Apache version 4.5.7 as it breaks URI paths with special characters and there is no option
182      *    to disable normalization.
183      * </p>
184      *
185      * For more information, See https://github.com/aws/aws-sdk-java/issues/1919
186      */

187     public static void disableNormalizeUri(RequestConfig.Builder requestConfigBuilder) {
188         // For efficiency, do not attempt to call the invoker again if it failed to initialize on class-load
189         if (normalizeUriInvoker.isInitialized()) {
190             try {
191                 normalizeUriInvoker.invoke(requestConfigBuilder, false);
192             } catch (NoSuchMethodException ignored) {
193                 noSuchMethodThrownByNormalizeUriInvoker();
194             }
195         }
196     }
197
198     /**
199      * Returns a new Credentials Provider for use with proxy authentication.
200      */

201     public static CredentialsProvider newProxyCredentialsProvider
202     (HttpClientSettings settings) {
203         final CredentialsProvider provider = new BasicCredentialsProvider();
204         provider.setCredentials(newAuthScope(settings), newNTCredentials(settings));
205         return provider;
206     }
207
208     /**
209      * Returns a new instance of NTCredentials used for proxy authentication.
210      */

211     private static Credentials newNTCredentials(HttpClientSettings settings) {
212         return new NTCredentials(settings.getProxyUsername(),
213                 settings.getProxyPassword(),
214                 settings.getProxyWorkstation(),
215                 settings.getProxyDomain());
216     }
217
218     /**
219      * Returns a new instance of AuthScope used for proxy authentication.
220      */

221     private static AuthScope newAuthScope(HttpClientSettings settings) {
222         return new AuthScope(settings.getProxyHost(), settings.getProxyPort());
223     }
224
225     private static void addPreemptiveAuthenticationProxy(HttpClientContext clientContext,
226                                                          HttpClientSettings settings) {
227
228         if (settings.isPreemptiveBasicProxyAuth()) {
229             HttpHost targetHost = new HttpHost(settings.getProxyHost(), settings
230                     .getProxyPort());
231             final CredentialsProvider credsProvider = newProxyCredentialsProvider(settings);
232             // Create AuthCache instance
233             AuthCache authCache = new BasicAuthCache();
234             // Generate BASIC scheme object and add it to the local auth cache
235             BasicScheme basicAuth = new BasicScheme();
236             authCache.put(targetHost, basicAuth);
237
238             clientContext.setCredentialsProvider(credsProvider);
239             clientContext.setAuthCache(authCache);
240         }
241     }
242
243     // Just log and then swallow the exception
244     private static void noSuchMethodThrownByNormalizeUriInvoker() {
245         // setNormalizeUri method was added in httpclient 4.5.8
246         log.warn("NoSuchMethodException was thrown when disabling normalizeUri. This indicates you are using "
247                  + "an old version (< 4.5.8) of Apache http client. It is recommended to use http client "
248                  + "version >= 4.5.9 to avoid the breaking change introduced in apache client 4.5.7 and "
249                  + "the latency in exception handling. See https://github.com/aws/aws-sdk-java/issues/1919"
250                  + for more information");
251     }
252 }
253