View Javadoc

1   package com.atlassian.sal.core.net;
2   
3   
4   import com.atlassian.sal.api.net.Request;
5   import com.atlassian.sal.api.net.ResponseException;
6   import com.google.common.base.Throwables;
7   import org.apache.commons.codec.binary.Base64;
8   import org.apache.http.Header;
9   import org.apache.http.HttpClientConnection;
10  import org.apache.http.HttpEntityEnclosingRequest;
11  import org.apache.http.HttpException;
12  import org.apache.http.HttpHost;
13  import org.apache.http.HttpRequest;
14  import org.apache.http.HttpResponse;
15  import org.apache.http.HttpStatus;
16  import org.apache.http.NameValuePair;
17  import org.apache.http.ProtocolVersion;
18  import org.apache.http.auth.AUTH;
19  import org.apache.http.client.RedirectException;
20  import org.apache.http.client.config.AuthSchemes;
21  import org.apache.http.client.utils.URLEncodedUtils;
22  import org.apache.http.conn.ConnectionRequest;
23  import org.apache.http.conn.HttpClientConnectionManager;
24  import org.apache.http.conn.routing.HttpRoute;
25  import org.apache.http.entity.StringEntity;
26  import org.apache.http.message.BasicHttpResponse;
27  import org.apache.http.message.BasicNameValuePair;
28  import org.apache.http.protocol.HttpContext;
29  import org.apache.http.protocol.HttpRequestExecutor;
30  import org.hamcrest.FeatureMatcher;
31  import org.hamcrest.Matcher;
32  import org.hamcrest.Matchers;
33  import org.junit.Before;
34  import org.junit.Rule;
35  import org.junit.Test;
36  import org.junit.rules.ExpectedException;
37  import org.junit.runner.RunWith;
38  import org.mockito.ArgumentCaptor;
39  import org.mockito.Mock;
40  import org.mockito.invocation.InvocationOnMock;
41  import org.mockito.runners.MockitoJUnitRunner;
42  import org.mockito.stubbing.Answer;
43  
44  import java.io.IOException;
45  import java.nio.charset.StandardCharsets;
46  import java.text.MessageFormat;
47  import java.util.List;
48  import java.util.concurrent.ExecutionException;
49  import java.util.concurrent.TimeUnit;
50  
51  import static org.hamcrest.CoreMatchers.instanceOf;
52  import static org.hamcrest.CoreMatchers.is;
53  import static org.hamcrest.CoreMatchers.notNullValue;
54  import static org.hamcrest.MatcherAssert.assertThat;
55  import static org.hamcrest.Matchers.arrayContaining;
56  import static org.hamcrest.Matchers.contains;
57  import static org.hamcrest.Matchers.typeCompatibleWith;
58  import static org.hamcrest.core.IsEqual.equalTo;
59  import static org.junit.Assert.fail;
60  import static org.mockito.Matchers.any;
61  import static org.mockito.Matchers.anyInt;
62  import static org.mockito.Matchers.anyObject;
63  import static org.mockito.Mockito.mock;
64  import static org.mockito.Mockito.times;
65  import static org.mockito.Mockito.verify;
66  import static org.mockito.Mockito.when;
67  
68  @RunWith (MockitoJUnitRunner.class)
69  public class TestHttpClientRequest
70  {
71  
72      private static final String DUMMY_HOST = "dummy.atlassian.test";
73      private static final String DUMMY_HTTP_URL = MessageFormat.format("http://{0}/", DUMMY_HOST);
74  
75      private static final String DUMMY_PROXY_HOST = "dummy.proxy.atlassian.test";
76      private static final String DUMMY_PROXY_PORT = "12345";
77      private static final String DUMMY_PROXY_USER = "dummyproxyuser";
78      private static final String DUMMY_PROXY_PASSWORD = "dummyproxypassword";
79  
80      @Rule
81      public ExpectedException thrown = ExpectedException.none();
82  
83      @Mock
84      private HttpRequestExecutor mockRequestExecutor;
85  
86      @Mock
87      private HttpClientConnectionManager mockConnectionManager;
88  
89      private HttpClientRequestFactory requestFactory;
90      private ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
91      private ArgumentCaptor<HttpRoute> routeCaptor = ArgumentCaptor.forClass(HttpRoute.class);
92  
93      @Before
94      public void setup() throws InterruptedException, ExecutionException, IOException, HttpException
95      {
96          // Clear all system proxy settings
97          clearProxySystemProperties();
98          requestFactory = new HttpClientWithMockConnectionRequestFactory(mockConnectionManager, mockRequestExecutor);
99  
100         // Always respond with a 200/OK message
101         when(mockRequestExecutor.execute(any(HttpRequest.class), any(HttpClientConnection.class),
102                 any(HttpContext.class))).thenReturn(createOkResponse());
103 
104         // This allows us to hook in to the connection details that HttpClient would have made
105         final HttpClientConnection conn = mock(HttpClientConnection.class);
106         final ConnectionRequest connRequest = mock(ConnectionRequest.class);
107         when(connRequest.get(anyInt(), any(TimeUnit.class))).thenReturn(conn);
108         when(mockConnectionManager.requestConnection(any(HttpRoute.class), anyObject())).thenReturn(connRequest);
109     }
110 
111     private static HttpResponse createOkResponse()
112     {
113         final BasicHttpResponse response = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), HttpStatus.SC_OK, "OK");
114         response.setEntity(new StringEntity("test body", StandardCharsets.UTF_8));
115         return response;
116     }
117 
118     private static HttpResponse createRedirectResponse(String location)
119     {
120         final BasicHttpResponse response = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), HttpStatus.SC_MOVED_PERMANENTLY, "Redirect");
121         response.setEntity(new StringEntity("Redirect", StandardCharsets.UTF_8));
122         response.setHeader("Location", location);
123         return response;
124     }
125 
126     private void clearProxySystemProperties()
127     {
128         System.clearProperty(SystemPropertiesProxyConfig.PROXY_HOST_PROPERTY_NAME);
129         System.clearProperty(SystemPropertiesProxyConfig.PROXY_PORT_PROPERTY_NAME);
130         System.clearProperty(SystemPropertiesProxyConfig.PROXY_USER_PROPERTY_NAME);
131         System.clearProperty(SystemPropertiesProxyConfig.PROXY_PASSWORD_PROPERTY_NAME);
132         System.clearProperty(SystemPropertiesProxyConfig.PROXY_NON_HOSTS_PROPERTY_NAME);
133     }
134 
135     @Test
136     public void assertThatHeaderIsAddedToRequest() throws ResponseException, IOException, HttpException
137     {
138         final HttpClientRequest request = requestFactory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
139 
140         final String testHeaderName = "foo";
141         final String testHeaderValue = "bar";
142         request.addHeader(testHeaderName, testHeaderValue);
143         request.execute();
144 
145         verify(mockRequestExecutor).execute(requestCaptor.capture(), any(HttpClientConnection.class), any(HttpContext.class));
146         final HttpRequest lastRequest = requestCaptor.getValue();
147         assertThat(lastRequest, notNullValue());
148 
149         final Header[] headers = lastRequest.getHeaders(testHeaderName);
150         //noinspection unchecked
151         assertThat(headers, arrayContaining(headerWithValue(equalTo(testHeaderValue))));
152     }
153 
154     @Test
155     public void assertThatMultiValuedHeadersAreAddedToRequest() throws ResponseException, IOException, HttpException
156     {
157         final HttpClientRequest request = requestFactory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
158 
159         final String testHeaderName = "foo";
160         final String testHeaderValue1 = "bar1";
161         final String testHeaderValue2 = "bar2";
162         request.addHeader(testHeaderName, testHeaderValue1);
163         request.addHeader(testHeaderName, testHeaderValue2);
164         request.execute();
165 
166         verify(mockRequestExecutor).execute(requestCaptor.capture(), any(HttpClientConnection.class), any(HttpContext.class));
167         final HttpRequest lastRequest = requestCaptor.getValue();
168         assertThat(lastRequest, notNullValue());
169 
170         final Header[] headers = lastRequest.getHeaders(testHeaderName);
171         //noinspection unchecked
172         assertThat(headers, arrayContaining(
173                 headerWithValue(equalTo(testHeaderValue1)),
174                 headerWithValue(equalTo(testHeaderValue2))));
175     }
176 
177     @Test
178     public void assertThatParameterIsAddedToRequest() throws IOException, ResponseException, HttpException
179     {
180         final HttpClientRequest request = requestFactory.createRequest(Request.MethodType.POST, DUMMY_HTTP_URL);
181 
182         final String testParameterName = "foo";
183         final String testParameterValue = "bar";
184 
185         request.addRequestParameters(testParameterName, testParameterValue);
186         request.execute();
187 
188         verify(mockRequestExecutor).execute(requestCaptor.capture(), any(HttpClientConnection.class), any(HttpContext.class));
189         final HttpRequest lastRequest = requestCaptor.getValue();
190 
191         assertThat(lastRequest, notNullValue());
192         assertThat(lastRequest.getClass(), is(typeCompatibleWith(HttpEntityEnclosingRequest.class)));
193         assertThat(lastRequest, requestParameters(contains(parameterWithNameAndValue(
194                 testParameterName, testParameterValue))));
195     }
196 
197     @Test
198     public void assertThatMultiValuedParametersAreAddedToRequest() throws IOException, ResponseException, HttpException
199     {
200         final HttpClientRequest request = requestFactory.createRequest(Request.MethodType.POST, DUMMY_HTTP_URL);
201 
202         final String testParameterName = "foo";
203         final String testParameterValue1 = "bar1";
204         final String testParameterValue2 = "bar2";
205 
206         request.addRequestParameters(testParameterName, testParameterValue1, testParameterName, testParameterValue2);
207         request.execute();
208 
209         verify(mockRequestExecutor).execute(requestCaptor.capture(), any(HttpClientConnection.class), any(HttpContext.class));
210         final HttpRequest lastRequest = requestCaptor.getValue();
211 
212         assertThat(lastRequest, notNullValue());
213         assertThat(lastRequest.getClass(), is(typeCompatibleWith(HttpEntityEnclosingRequest.class)));
214         //noinspection unchecked
215         assertThat(lastRequest, requestParameters(contains(
216                 parameterWithNameAndValue(testParameterName, testParameterValue1),
217                 parameterWithNameAndValue(testParameterName, testParameterValue2)
218         )));
219     }
220 
221     @Test
222     public void assertThatAddingParametersToGetRequestThrowsException()
223     {
224         final HttpClientRequest request = requestFactory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
225         thrown.expect(IllegalStateException.class);
226         request.addRequestParameters("foo", "bar");
227     }
228 
229     @Test
230     public void assertThatSystemProxyHostSettingHonoured() throws IOException, ResponseException, HttpException
231     {
232         System.setProperty(SystemPropertiesProxyConfig.PROXY_HOST_PROPERTY_NAME, DUMMY_PROXY_HOST);
233         System.setProperty(SystemPropertiesProxyConfig.PROXY_PORT_PROPERTY_NAME, DUMMY_PROXY_PORT);
234 
235         final HttpClientRequestFactory factory = new HttpClientWithMockConnectionRequestFactory(mockConnectionManager, mockRequestExecutor);
236         final HttpClientRequest request = factory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
237         request.execute();
238 
239         verify(mockConnectionManager).connect(any(HttpClientConnection.class), routeCaptor.capture(), anyInt(), any(HttpContext.class));
240         HttpRoute route = routeCaptor.getValue();
241         assertThat(route, proxyHostIs(hostWithNameAndPort(DUMMY_PROXY_HOST, Integer.parseInt(DUMMY_PROXY_PORT))));
242     }
243 
244     @Test
245     public void assertThatProxyNotUsedByDefault() throws IOException, ResponseException, HttpException
246     {
247         final HttpClientRequestFactory factory = new HttpClientWithMockConnectionRequestFactory(mockConnectionManager, mockRequestExecutor);
248         final HttpClientRequest request = factory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
249         request.execute();
250 
251         verify(mockConnectionManager).connect(any(HttpClientConnection.class), routeCaptor.capture(), anyInt(), any(HttpContext.class));
252         final HttpRoute route = routeCaptor.getValue();
253         assertThat(route, proxyHostIs(Matchers.nullValue(HttpHost.class)));
254     }
255 
256     @Test
257     public void assertThatNonProxyHostsSystemPropertyHonoured() throws IOException, ResponseException, HttpException
258     {
259         System.setProperty(SystemPropertiesProxyConfig.PROXY_HOST_PROPERTY_NAME, DUMMY_PROXY_HOST);
260         System.setProperty(SystemPropertiesProxyConfig.PROXY_PORT_PROPERTY_NAME, DUMMY_PROXY_PORT);
261         System.setProperty(SystemPropertiesProxyConfig.PROXY_NON_HOSTS_PROPERTY_NAME, "*.notproxied.test|localhost");
262 
263         final HttpClientRequestFactory factory = new HttpClientWithMockConnectionRequestFactory(mockConnectionManager, mockRequestExecutor);
264         final HttpClientRequest request = factory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
265 
266         request.execute();
267         request.setUrl("http://www.notproxied.test").execute();
268 
269         verify(mockConnectionManager, times(2)).connect(any(HttpClientConnection.class), routeCaptor.capture(), anyInt(), any(HttpContext.class));
270 
271         final List<HttpRoute> routes = routeCaptor.getAllValues();
272 
273         //noinspection unchecked
274         assertThat(routes, contains(
275                 proxyHostIs(hostWithNameAndPort(DUMMY_PROXY_HOST, Integer.parseInt(DUMMY_PROXY_PORT))),
276                 proxyHostIs(Matchers.nullValue(HttpHost.class))
277         ));
278     }
279 
280     @Test
281     public void assertThatProxyAuthenticationSystemPropertyHonoured() throws IOException, ResponseException, HttpException
282     {
283         System.setProperty(SystemPropertiesProxyConfig.PROXY_HOST_PROPERTY_NAME, DUMMY_PROXY_HOST);
284         System.setProperty(SystemPropertiesProxyConfig.PROXY_PORT_PROPERTY_NAME, DUMMY_PROXY_PORT);
285         System.setProperty(SystemPropertiesProxyConfig.PROXY_USER_PROPERTY_NAME, DUMMY_PROXY_USER);
286         System.setProperty(SystemPropertiesProxyConfig.PROXY_PASSWORD_PROPERTY_NAME, DUMMY_PROXY_PASSWORD);
287 
288         final HttpClientRequestFactory factory = new HttpClientWithMockConnectionRequestFactory(mockConnectionManager, mockRequestExecutor);
289         final HttpClientRequest request = factory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
290         final String username = "charlie";
291         final String password = "password123";
292         request.addBasicAuthentication(DUMMY_HOST, username, password);
293         request.execute();
294         verify(mockConnectionManager).connect(any(HttpClientConnection.class), routeCaptor.capture(), anyInt(), any(HttpContext.class));
295         final HttpRoute route = routeCaptor.getValue();
296         assertThat(route, proxyHostIs(hostWithNameAndPort(DUMMY_PROXY_HOST, Integer.parseInt(DUMMY_PROXY_PORT))));
297 
298         verify(mockRequestExecutor).execute(requestCaptor.capture(), any(HttpClientConnection.class), any(HttpContext.class));
299         final HttpRequest lastRequest = requestCaptor.getValue();
300         final Header[] headers = lastRequest.getHeaders(AUTH.PROXY_AUTH_RESP);
301 
302         //noinspection unchecked
303         assertThat(headers, arrayContaining(headerWithValue(
304                 basicAuthWithUsernameAndPassword(DUMMY_PROXY_USER, DUMMY_PROXY_PASSWORD))));
305     }
306 
307     @Test
308     public void assertThatBasicAuthenticationHeadersAdded() throws ResponseException, IOException, HttpException
309     {
310         final HttpClientRequest request = requestFactory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
311 
312         final String username = "charlie";
313         final String password = "password123";
314         request.addBasicAuthentication(DUMMY_HOST, username, password);
315         request.execute();
316 
317         verify(mockRequestExecutor).execute(requestCaptor.capture(), any(HttpClientConnection.class), any(HttpContext.class));
318         final HttpRequest lastRequest = requestCaptor.getValue();
319         final Header[] headers = lastRequest.getHeaders(AUTH.WWW_AUTH_RESP);
320 
321         //noinspection unchecked
322         assertThat(headers, arrayContaining(headerWithValue(basicAuthWithUsernameAndPassword(username, password))));
323     }
324 
325     @Test
326     public void assertThatExceptionThrownAfterDefaultMaximumRedirects() throws ResponseException, IOException, HttpException
327     {
328 
329         when(mockRequestExecutor.execute(any(HttpRequest.class), any(HttpClientConnection.class),
330                 any(HttpContext.class))).thenAnswer(new Answer<HttpResponse>()
331         {
332             int redirectCount = 0;
333 
334             @Override
335             public HttpResponse answer(final InvocationOnMock invocationOnMock) throws Throwable
336             {
337                 // We just add a changing query string parameter here to avoid a circular redirect
338                 return createRedirectResponse(DUMMY_HTTP_URL + "?redirect_count=" + redirectCount++);
339             }
340         });
341 
342         final HttpClientRequest request = requestFactory.createRequest(Request.MethodType.GET, DUMMY_HTTP_URL);
343 
344         try
345         {
346             request.execute();
347             fail("An exception should be thrown when maximum redirects is exceeded.");
348         }
349         catch (ResponseException expectedException)
350         {
351             // Although JUnit has an ExpectedException rule, we need to catch this both to ensure that a
352             // RedirectException is the cause, and to subsequently verify the execution method call count.
353             assertThat(Throwables.getCausalChain(expectedException), Matchers.<Throwable>hasItem(instanceOf(RedirectException.class)));
354         }
355 
356         verify(mockRequestExecutor, times(SystemPropertiesConnectionConfig.DEFAULT_MAX_REDIRECTS + 1))
357                 .execute(any(HttpRequest.class), any(HttpClientConnection.class), any(HttpContext.class));
358     }
359 
360     private static Matcher<Header> headerWithValue(final Matcher<String> valueMatcher)
361     {
362         return new FeatureMatcher<Header, String>(valueMatcher, "header with value", "header value")
363         {
364             @Override
365             protected String featureValueOf(final Header header)
366             {
367                 return header.getValue();
368             }
369         };
370     }
371 
372     private static Matcher<HttpRequest> requestParameters(final Matcher<Iterable<? extends NameValuePair>> parametersMatcher)
373     {
374         return new FeatureMatcher<HttpRequest, Iterable<? extends NameValuePair>>(
375                 parametersMatcher, "parameters with value", "parameters value")
376         {
377             @Override
378             protected Iterable<? extends NameValuePair> featureValueOf(final HttpRequest httpRequest)
379             {
380                 try
381                 {
382                     return URLEncodedUtils.parse(((HttpEntityEnclosingRequest) httpRequest).getEntity());
383                 }
384                 catch (IOException e)
385                 {
386                     throw new RuntimeException(e);
387                 }
388             }
389         };
390     }
391 
392     private static Matcher<NameValuePair> parameterWithNameAndValue(final String name, final String value)
393     {
394 
395         final NameValuePair expectedParameter = new BasicNameValuePair(name, value);
396         return Matchers.equalTo(expectedParameter);
397     }
398 
399     private static Matcher<HttpRoute> proxyHostIs(final Matcher<HttpHost> routeMatcher)
400     {
401         return new FeatureMatcher<HttpRoute, HttpHost>(routeMatcher, "proxy host for route", "proxy host")
402         {
403             @Override
404             protected HttpHost featureValueOf(final HttpRoute route)
405             {
406                 return route.getProxyHost();
407             }
408         };
409     }
410 
411     private static Matcher<HttpHost> hostWithNameAndPort(final String name, final int port)
412     {
413 
414         final HttpHost expectedHost = new HttpHost(name, port);
415         return Matchers.equalTo(expectedHost);
416     }
417 
418     private static Matcher<String> basicAuthWithUsernameAndPassword(final String username, final String password)
419     {
420 
421         final String basicAuthCreds = MessageFormat.format("{0}:{1}", username, password);
422         final String encodedBasicAuthCreds = Base64.encodeBase64String(basicAuthCreds.getBytes());
423         final String expectedBasicAuthHeader = MessageFormat.format("{0} {1}", AuthSchemes.BASIC, encodedBasicAuthCreds);
424 
425         return Matchers.equalTo(expectedBasicAuthHeader);
426     }
427 
428 }