1 package com.atlassian.sal.core.net;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.UnsupportedEncodingException;
7 import java.net.MalformedURLException;
8 import java.net.SocketException;
9 import java.net.URL;
10 import java.util.ArrayList;
11 import java.util.Arrays;
12 import java.util.Collections;
13 import java.util.HashMap;
14 import java.util.List;
15 import java.util.Map;
16
17 import javax.net.ssl.SSLException;
18
19 import com.atlassian.sal.api.net.Request;
20 import com.atlassian.sal.api.net.RequestFilePart;
21 import com.atlassian.sal.api.net.ResponseConnectTimeoutException;
22 import com.atlassian.sal.api.net.ResponseException;
23 import com.atlassian.sal.api.net.ResponseHandler;
24 import com.atlassian.sal.api.net.ResponseProtocolException;
25 import com.atlassian.sal.api.net.ResponseReadTimeoutException;
26 import com.atlassian.sal.api.net.ResponseStatusException;
27 import com.atlassian.sal.api.net.ResponseTransportException;
28 import com.atlassian.sal.api.net.ReturningResponseHandler;
29 import com.atlassian.sal.api.net.auth.Authenticator;
30 import com.atlassian.sal.api.user.UserManager;
31 import com.atlassian.sal.core.net.auth.BaseAuthenticator;
32 import com.atlassian.sal.core.net.auth.HttpClientAuthenticator;
33 import com.atlassian.sal.core.net.auth.SeraphAuthenticator;
34 import com.atlassian.sal.core.net.auth.TrustedTokenAuthenticator;
35 import com.atlassian.sal.core.trusted.CertificateFactory;
36
37 import com.google.common.annotations.VisibleForTesting;
38 import com.google.common.collect.ImmutableList;
39
40 import org.apache.commons.httpclient.ConnectTimeoutException;
41 import org.apache.commons.httpclient.Header;
42 import org.apache.commons.httpclient.HostConfiguration;
43 import org.apache.commons.httpclient.HttpClient;
44 import org.apache.commons.httpclient.HttpConnectionManager;
45 import org.apache.commons.httpclient.HttpException;
46 import org.apache.commons.httpclient.HttpMethod;
47 import org.apache.commons.httpclient.NoHttpResponseException;
48 import org.apache.commons.httpclient.URI;
49 import org.apache.commons.httpclient.methods.DeleteMethod;
50 import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
51 import org.apache.commons.httpclient.methods.GetMethod;
52 import org.apache.commons.httpclient.methods.HeadMethod;
53 import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
54 import org.apache.commons.httpclient.methods.OptionsMethod;
55 import org.apache.commons.httpclient.methods.PostMethod;
56 import org.apache.commons.httpclient.methods.PutMethod;
57 import org.apache.commons.httpclient.methods.TraceMethod;
58 import org.apache.commons.httpclient.methods.multipart.FilePart;
59 import org.apache.commons.httpclient.methods.multipart.FilePartSource;
60 import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
61 import org.apache.commons.httpclient.params.HttpClientParams;
62 import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
63 import org.apache.commons.httpclient.params.HttpMethodParams;
64 import org.apache.commons.lang.StringUtils;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68
69
70
71 public class HttpClientRequest implements Request<HttpClientRequest, HttpClientResponse>
72 {
73 private static final Logger log = LoggerFactory.getLogger(HttpClientRequest.class);
74
75
76
77
78
79
80
81
82
83 @SuppressWarnings ("JavadocReference")
84 public static final int MAX_REDIRECTS = 20;
85
86
87
88
89 private final int maxRedirects = Integer.getInteger(HttpClientParams.MAX_REDIRECTS, MAX_REDIRECTS);
90
91 private final Request.MethodType methodType;
92
93
94
95 private String absoluteUrl;
96
97
98
99
100 private String url;
101 private final Map<String, List<String>> parameters = new HashMap<String, List<String>>();
102 private final Map<String, List<String>> headers = new HashMap<String, List<String>>();
103 private final List<HttpClientAuthenticator> authenticators = new ArrayList<HttpClientAuthenticator>();
104 private final CertificateFactory certificateFactory;
105
106 private final HttpClient httpClient;
107 private final UserManager userManager;
108 private String requestBody;
109 private String requestContentType;
110 private boolean followRedirects = true;
111 private List<RequestFilePart> files;
112
113 public HttpClientRequest(final HttpClient httpClient, final MethodType methodType, final String url,
114 final CertificateFactory certificateFactory, final UserManager userManager)
115 {
116 this.httpClient = httpClient;
117 this.methodType = methodType;
118
119 this.absoluteUrl = url;
120 this.url = selectUrlFormat(httpClient, this.absoluteUrl);
121 this.certificateFactory = certificateFactory;
122 this.userManager = userManager;
123 if (isEntityEnclosingMethod())
124 {
125 followRedirects = false;
126 }
127 }
128
129 public HttpClientRequest setUrl(final String url)
130 {
131 this.url = selectUrlFormat(httpClient, url);
132
133
134
135 configureProtocol(url);
136
137
138
139 configureProxy();
140 return this;
141 }
142
143
144
145 public HttpClientRequest addAuthentication(final Authenticator authenticator)
146 {
147 if (authenticator instanceof HttpClientAuthenticator)
148 {
149 this.authenticators.add((HttpClientAuthenticator) authenticator);
150 }
151 else
152 {
153 log.warn("Authenticator '" + authenticator + "'is not instance of " + HttpClientAuthenticator.class.getName());
154 }
155 return this;
156 }
157
158 public HttpClientRequest addTrustedTokenAuthentication()
159 {
160 return this.addTrustedTokenAuthentication(userManager.getRemoteUsername());
161 }
162
163 public HttpClientRequest addTrustedTokenAuthentication(final String username)
164 {
165 final TrustedTokenAuthenticator trustedTokenAuthenticator = new TrustedTokenAuthenticator(username, this.absoluteUrl,
166 certificateFactory);
167
168 this.authenticators.add(trustedTokenAuthenticator);
169 return this;
170 }
171
172 public HttpClientRequest addBasicAuthentication(final String username, final String password)
173 {
174 this.authenticators.add(new BaseAuthenticator(username, password));
175 return this;
176 }
177
178 public HttpClientRequest addSeraphAuthentication(final String username, final String password)
179 {
180 this.authenticators.add(new SeraphAuthenticator(username, password));
181 return this;
182 }
183
184 @VisibleForTesting
185 protected ImmutableList<HttpClientAuthenticator> getAuthenticators()
186 {
187 return ImmutableList.copyOf(authenticators);
188 }
189
190
191
192 public HttpClientRequest setConnectionTimeout(final int connectionTimeout)
193 {
194 final HttpConnectionManagerParams params = httpClient.getHttpConnectionManager().getParams();
195 params.setConnectionTimeout(connectionTimeout);
196 return this;
197 }
198
199 public HttpClientRequest setSoTimeout(final int soTimeout)
200 {
201 final HttpConnectionManagerParams params = httpClient.getHttpConnectionManager().getParams();
202 params.setSoTimeout(soTimeout);
203 return this;
204 }
205
206 public HttpClientRequest setRequestBody(final String requestBody)
207 {
208 if (!isEntityEnclosingMethod())
209 {
210 throw new IllegalArgumentException("Only POST and PUT methods can have request body");
211 }
212 if (files != null)
213 {
214 throw new IllegalStateException("This request contains already file parts! The request body can only be set, if the request does not contain file parts.");
215 }
216 this.requestBody = requestBody;
217 return this;
218 }
219
220 public HttpClientRequest setFiles(final List<RequestFilePart> files)
221 {
222 if (files == null)
223 {
224 throw new IllegalArgumentException("Files cannot be null");
225 }
226 if (!isEntityEnclosingMethod())
227 {
228 throw new IllegalArgumentException("Only POST and PUT methods can have a multi part body with file parts.");
229 }
230 if (requestBody != null)
231 {
232 throw new IllegalStateException("The request body is not empty! The request can only have file parts if the request is empty.");
233 }
234 this.files = files;
235 return this;
236 }
237
238 private boolean isEntityEnclosingMethod()
239 {
240 return (methodType == MethodType.POST || methodType == MethodType.PUT);
241 }
242
243 public HttpClientRequest setEntity(Object entity)
244 {
245 throw new UnsupportedOperationException("This SAL request does not support object marshaling. Use the RequestFactory component instead.");
246 }
247
248 public HttpClientRequest setRequestContentType(final String requestContentType)
249 {
250 this.requestContentType = requestContentType;
251 return this;
252 }
253
254 public HttpClientRequest addRequestParameters(final String... params)
255 {
256 if (methodType != MethodType.POST)
257 {
258 throw new UnsupportedOperationException("Only POST methods accept request parameters. For all other HTTP methods types http parameters have to be part of the URL string.");
259 }
260
261 if (params.length % 2 != 0)
262 {
263 throw new IllegalArgumentException("You must enter an even number of arguments");
264 }
265
266 for (int i = 0; i < params.length; i += 2)
267 {
268 final String name = params[i];
269 final String value = params[i + 1];
270 List<String> list = parameters.get(name);
271 if (list == null)
272 {
273 list = new ArrayList<String>();
274 parameters.put(name, list);
275 }
276 list.add(value);
277 }
278 return this;
279 }
280
281 public HttpClientRequest addHeader(final String headerName, final String headerValue)
282 {
283 List<String> list = headers.get(headerName);
284 if (list == null)
285 {
286 list = new ArrayList<String>();
287 headers.put(headerName, list);
288 }
289 list.add(headerValue);
290 return this;
291 }
292
293 public HttpClientRequest setHeader(final String headerName, final String headerValue)
294 {
295 headers.put(headerName, new ArrayList<String>(Arrays.asList(headerValue)));
296 return this;
297 }
298
299 public HttpClientRequest setFollowRedirects(boolean follow)
300 {
301 if (isEntityEnclosingMethod() && follow)
302 {
303 throw new IllegalStateException("Entity enclosing requests cannot be redirected without user intervention!");
304 }
305 this.followRedirects = follow;
306 return this;
307 }
308
309 public HttpClientRequest addHeaders(final String... params)
310 {
311 if (params.length % 2 != 0)
312 {
313 throw new IllegalArgumentException("You must enter even number of arguments");
314 }
315
316 for (int i = 0; i < params.length; i += 2)
317 {
318 final String name = params[i];
319 final String value = params[i + 1];
320 List<String> list = headers.get(name);
321 if (list == null)
322 {
323 list = new ArrayList<String>();
324 headers.put(name, list);
325 }
326 list.add(value);
327 }
328 return this;
329 }
330
331 public <E> E executeAndReturn(ReturningResponseHandler<HttpClientResponse, E> httpClientResponseResponseHandler)
332 throws ResponseException
333 {
334 final HttpMethod method = makeMethod();
335 method.setFollowRedirects(false);
336 processHeaders(method);
337 processAuthenticator(method);
338 processParameters(method);
339 if (log.isDebugEnabled())
340 {
341
342 final Header[] requestHeaders = method.getRequestHeaders();
343 String headerStr = requestHeaders == null ? "none" : Arrays.asList(requestHeaders).toString();
344 log.debug("Calling {} {} with headers: {}", new Object[]{ method.getName(), this.url, headerStr });
345 }
346 method.setRequestHeader("Connection", "close");
347 ResponseException rethrown = null;
348 try
349 {
350 executeMethod(method, 0);
351 return httpClientResponseResponseHandler.handle(new HttpClientResponse(method));
352 }
353 catch (ConnectTimeoutException cte)
354 {
355
356 rethrown = new ResponseConnectTimeoutException(cte.getMessage(), cte);
357 throw rethrown;
358 }
359 catch (SocketException se)
360 {
361 rethrown = new ResponseTransportException(se.getMessage(), se);
362 throw rethrown;
363 }
364 catch (NoHttpResponseException nhre)
365 {
366 rethrown = new ResponseReadTimeoutException(nhre.getMessage(), nhre);
367 throw rethrown;
368 }
369 catch (HttpException he)
370 {
371 rethrown = new ResponseProtocolException(he.getMessage(), he);
372 throw rethrown;
373 }
374 catch (IOException ioe)
375 {
376 rethrown = new ResponseException(ioe);
377 throw rethrown;
378 }
379 finally
380 {
381 if (rethrown != null)
382 {
383 log.debug("Call to {} {} failed with {}, message: {}", new Object[]{ method.getName(), this.url, rethrown.getClass().getSimpleName(), rethrown.getMessage() });
384 }
385 else
386 {
387 log.debug("Called {} {} with response status: {}", new Object[]{ method.getName(), this.url, method.getStatusLine() });
388 }
389
390 exhaustResponseContents(method);
391 method.releaseConnection();
392
393 final HttpConnectionManager httpConnectionManager = httpClient.getHttpConnectionManager();
394 if (httpConnectionManager != null)
395 {
396 httpConnectionManager.closeIdleConnections(0);
397 }
398 }
399 }
400
401
402
403
404 public void execute(final ResponseHandler<HttpClientResponse> responseHandler)
405 throws ResponseException
406 {
407 executeAndReturn(new ReturningResponseHandler<HttpClientResponse, Void>()
408 {
409 public Void handle(final HttpClientResponse response) throws ResponseException
410 {
411 responseHandler.handle(response);
412 return null;
413 }
414 });
415 }
416
417 private static void exhaustResponseContents(final HttpMethod response)
418 {
419 InputStream body = null;
420 try
421 {
422 body = response.getResponseBodyAsStream();
423 if (body == null)
424 {
425 return;
426 }
427 final byte[] buf = new byte[512];
428 @SuppressWarnings("unused")
429 int bytesRead = 0;
430 while ((bytesRead = body.read(buf)) != -1)
431 {
432
433
434
435 }
436 }
437 catch (final IOException e)
438 {
439
440 }
441 finally
442 {
443 shutdownStream(body);
444 }
445 }
446
447
448
449
450
451
452
453 public static void shutdownStream(final InputStream input)
454 {
455 if (null == input)
456 {
457 return;
458 }
459
460 try
461 {
462 input.close();
463 }
464 catch (final IOException ioe)
465 {
466
467 }
468 }
469
470 public String execute() throws ResponseException
471 {
472 return executeAndReturn(new ReturningResponseHandler<HttpClientResponse, String>()
473 {
474 public String handle(final HttpClientResponse response) throws ResponseException
475 {
476 if (!response.isSuccessful())
477 {
478 throw new ResponseStatusException("Unexpected response received. Status code: " + response.getStatusCode(),
479 response);
480 }
481 return response.getResponseBodyAsString();
482 }
483 });
484 }
485
486
487
488
489 protected HttpMethod makeMethod()
490 {
491 final HttpMethod method;
492 switch (methodType)
493 {
494 case POST:
495 method = new PostMethod(url);
496 break;
497 case PUT:
498 method = new PutMethod(url);
499 break;
500 case DELETE:
501 method = new DeleteMethod(url);
502 break;
503 case OPTIONS:
504 method = new OptionsMethod(url);
505 break;
506 case HEAD:
507 method = new HeadMethod(url);
508 break;
509 case TRACE:
510 method = new TraceMethod(url);
511 break;
512 default:
513 method = new GetMethod(url);
514 break;
515 }
516 return method;
517 }
518
519
520
521
522 protected void configureProxy()
523 {
524 new HttpClientProxyConfig().configureProxy(this.httpClient, this.url);
525 }
526
527
528
529
530 protected void configureProtocol(final String url)
531 {
532 HttpClientProtocolConfig.configureProtocol(this.httpClient, url);
533 }
534
535 private void executeMethod(final HttpMethod method, int redirectCounter) throws IOException, ResponseProtocolException
536 {
537
538 if (redirectCounter > 0 && redirectCounter > this.getMaxRedirects())
539 {
540
541 throw new ResponseProtocolException(new IOException("Maximum number of redirects (" + this.getMaxRedirects() + ") reached."));
542 }
543 else
544 {
545
546 int statusCode = executeMethod(method);
547
548 if(statusCode >= 300 && statusCode <= 399)
549 {
550
551 redirectCounter++;
552
553 if (followRedirects)
554 {
555 String redirectLocation;
556 final Header locationHeader = method.getResponseHeader("location");
557 if (locationHeader != null)
558 {
559 redirectLocation = locationHeader.getValue();
560 HttpClientProtocolConfig.configureProtocol(httpClient, redirectLocation);
561 method.setURI(new URI(selectUrlFormat(httpClient, redirectLocation), true));
562 executeMethod(method, redirectCounter);
563 }
564 else
565 {
566
567
568
569
570 throw new ResponseProtocolException(new IOException("HTTP response returned redirect code " + statusCode + " but did not provide a location header"));
571 }
572 }
573 }
574 }
575 }
576
577
578
579
580 public int getMaxRedirects()
581 {
582 return maxRedirects;
583 }
584
585 private int executeMethod(HttpMethod method) throws IOException
586 {
587 int statusCode;
588 try
589 {
590 statusCode = httpClient.executeMethod(method);
591 }
592 catch (SSLException e)
593 {
594
595 HttpClientProtocolConfig.changeHostConfigurationProtocol(httpClient, "SSLv3");
596 statusCode = httpClient.executeMethod(method);
597 }
598
599 return statusCode;
600 }
601
602 private void processHeaders(final HttpMethod method)
603 {
604 for (final String headerName : this.headers.keySet())
605 {
606 for (final String headerValue : this.headers.get(headerName))
607 {
608 method.addRequestHeader(headerName, headerValue);
609 }
610 }
611 }
612
613 private void processParameters(final HttpMethod method)
614 {
615 if (!(method instanceof EntityEnclosingMethod))
616 {
617 return;
618 }
619 EntityEnclosingMethod entityEnclosingMethod = (EntityEnclosingMethod) method;
620
621 if ((entityEnclosingMethod instanceof PostMethod) && !this.parameters.isEmpty())
622 {
623 final PostMethod postMethod = (PostMethod) method;
624 postMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
625 for (final String parameterName : this.parameters.keySet())
626 {
627 for (final String parameterValue : this.parameters.get(parameterName))
628 {
629 postMethod.addParameter(parameterName, parameterValue);
630 }
631 }
632 return;
633 }
634
635
636 if (this.requestBody != null)
637 {
638 setRequestBody(entityEnclosingMethod);
639 }
640
641 if (files != null && !files.isEmpty())
642 {
643 setFileParts(entityEnclosingMethod);
644 }
645 }
646
647 private void setFileParts(final EntityEnclosingMethod entityEnclosingMethod)
648 {
649 final List<FilePart> fileParts = new ArrayList<FilePart>();
650 for (RequestFilePart file : files)
651 {
652 try
653 {
654 FilePartSource partSource = new FilePartSource(file.getFileName(), file.getFile());
655 FilePart filePart = new FilePart(file.getParameterName(), partSource, file.getContentType(), null);
656 fileParts.add(filePart);
657 }
658 catch (IOException e)
659 {
660 throw new RuntimeException(e);
661 }
662 }
663 MultipartRequestEntity entity = new MultipartRequestEntity(fileParts.toArray(new FilePart[fileParts.size()]), new HttpMethodParams());
664 entityEnclosingMethod.setRequestEntity(entity);
665 }
666
667 private void setRequestBody(final EntityEnclosingMethod method)
668 {
669 final String contentType = requestContentType + "; charset=UTF-8";
670 ByteArrayInputStream inputStream;
671 try
672 {
673 inputStream = new ByteArrayInputStream(requestBody.getBytes("UTF-8"));
674 }
675 catch (final UnsupportedEncodingException e)
676 {
677 throw new RuntimeException(e);
678 }
679 method.setRequestEntity(new InputStreamRequestEntity(inputStream, contentType));
680 }
681
682 private void processAuthenticator(final HttpMethod method)
683 {
684 for (final HttpClientAuthenticator authenticator : authenticators)
685 {
686 authenticator.process(httpClient, method);
687 }
688 }
689
690
691
692
693
694
695
696
697
698
699 private String selectUrlFormat(final HttpClient client, String url)
700 {
701 HostConfiguration hostConfiguration = client.getHostConfiguration();
702 if (hostConfiguration != null && hostConfiguration.getHost() != null)
703 {
704
705 return getPathQueryAndFragment(url);
706 }
707
708 return url;
709 }
710
711
712
713
714
715
716
717 private static String getPathQueryAndFragment(final String url)
718 {
719 try
720 {
721 return new URL(url).getFile();
722 }
723 catch (MalformedURLException e)
724 {
725 throw new IllegalArgumentException("The URL is not valid", e);
726 }
727 }
728
729 public Map<String, List<String>> getHeaders()
730 {
731 return Collections.unmodifiableMap(headers);
732 }
733
734 public MethodType getMethodType()
735 {
736 return methodType;
737 }
738
739 @Override
740 public String toString()
741 {
742 return methodType + " " + url + ", Parameters: " + parameters +
743 (StringUtils.isBlank(requestBody) ? "" : "\nRequest body:\n" + requestBody);
744 }
745 }