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