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 return httpClient.executeMethod(method);
588 }
589
590 private void processHeaders(final HttpMethod method)
591 {
592 for (final String headerName : this.headers.keySet())
593 {
594 for (final String headerValue : this.headers.get(headerName))
595 {
596 method.addRequestHeader(headerName, headerValue);
597 }
598 }
599 }
600
601 private void processParameters(final HttpMethod method)
602 {
603 if (!(method instanceof EntityEnclosingMethod))
604 {
605 return;
606 }
607 EntityEnclosingMethod entityEnclosingMethod = (EntityEnclosingMethod) method;
608
609 if ((entityEnclosingMethod instanceof PostMethod) && !this.parameters.isEmpty())
610 {
611 final PostMethod postMethod = (PostMethod) method;
612 postMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
613 for (final String parameterName : this.parameters.keySet())
614 {
615 for (final String parameterValue : this.parameters.get(parameterName))
616 {
617 postMethod.addParameter(parameterName, parameterValue);
618 }
619 }
620 return;
621 }
622
623
624 if (this.requestBody != null)
625 {
626 setRequestBody(entityEnclosingMethod);
627 }
628
629 if (files != null && !files.isEmpty())
630 {
631 setFileParts(entityEnclosingMethod);
632 }
633 }
634
635 private void setFileParts(final EntityEnclosingMethod entityEnclosingMethod)
636 {
637 final List<FilePart> fileParts = new ArrayList<FilePart>();
638 for (RequestFilePart file : files)
639 {
640 try
641 {
642 FilePartSource partSource = new FilePartSource(file.getFileName(), file.getFile());
643 FilePart filePart = new FilePart(file.getParameterName(), partSource, file.getContentType(), null);
644 fileParts.add(filePart);
645 }
646 catch (IOException e)
647 {
648 throw new RuntimeException(e);
649 }
650 }
651 MultipartRequestEntity entity = new MultipartRequestEntity(fileParts.toArray(new FilePart[fileParts.size()]), new HttpMethodParams());
652 entityEnclosingMethod.setRequestEntity(entity);
653 }
654
655 private void setRequestBody(final EntityEnclosingMethod method)
656 {
657 final String contentType = requestContentType + "; charset=UTF-8";
658 ByteArrayInputStream inputStream;
659 try
660 {
661 inputStream = new ByteArrayInputStream(requestBody.getBytes("UTF-8"));
662 }
663 catch (final UnsupportedEncodingException e)
664 {
665 throw new RuntimeException(e);
666 }
667 method.setRequestEntity(new InputStreamRequestEntity(inputStream, contentType));
668 }
669
670 private void processAuthenticator(final HttpMethod method)
671 {
672 for (final HttpClientAuthenticator authenticator : authenticators)
673 {
674 authenticator.process(httpClient, method);
675 }
676 }
677
678
679
680
681
682
683
684
685
686
687 private String selectUrlFormat(final HttpClient client, String url)
688 {
689 HostConfiguration hostConfiguration = client.getHostConfiguration();
690 if (hostConfiguration != null && hostConfiguration.getHost() != null)
691 {
692
693 return getPathQueryAndFragment(url);
694 }
695
696 return url;
697 }
698
699
700
701
702
703
704
705 private static String getPathQueryAndFragment(final String url)
706 {
707 try
708 {
709 return new URL(url).getFile();
710 }
711 catch (MalformedURLException e)
712 {
713 throw new IllegalArgumentException("The URL is not valid", e);
714 }
715 }
716
717 public Map<String, List<String>> getHeaders()
718 {
719 return Collections.unmodifiableMap(headers);
720 }
721
722 public MethodType getMethodType()
723 {
724 return methodType;
725 }
726
727 @Override
728 public String toString()
729 {
730 return methodType + " " + url + ", Parameters: " + parameters +
731 (StringUtils.isBlank(requestBody) ? "" : "\nRequest body:\n" + requestBody);
732 }
733 }