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