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
98 clearProxySystemProperties();
99 requestFactory = new HttpClientWithMockConnectionRequestFactory(mockConnectionManager, mockRequestExecutor);
100
101
102 when(mockRequestExecutor.execute(any(HttpRequest.class), any(HttpClientConnection.class),
103 any(HttpContext.class))).thenReturn(createOkResponse());
104
105
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
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
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
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
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
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
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
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
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
402
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 }