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