001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.http;
019
020
021import java.io.*;
022import java.net.*;
023import java.nio.charset.Charset;
024import java.security.cert.X509Certificate;
025import java.util.List;
026import java.util.Map;
027import javax.net.ssl.HostnameVerifier;
028import javax.net.ssl.HttpsURLConnection;
029import javax.net.ssl.SSLSocketFactory;
030
031import com.nimbusds.oauth2.sdk.ParseException;
032import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
033import com.nimbusds.oauth2.sdk.util.URLUtils;
034import net.jcip.annotations.ThreadSafe;
035import net.minidev.json.JSONObject;
036
037
038/**
039 * HTTP request with support for the parameters required to construct an 
040 * {@link com.nimbusds.oauth2.sdk.Request OAuth 2.0 request message}.
041 *
042 * <p>Supported HTTP methods:
043 *
044 * <ul>
045 *     <li>{@link Method#GET HTTP GET}
046 *     <li>{@link Method#POST HTTP POST}
047 *     <li>{@link Method#POST HTTP PUT}
048 *     <li>{@link Method#POST HTTP DELETE}
049 * </ul>
050 *
051 * <p>Supported request headers:
052 *
053 * <ul>
054 *     <li>Content-Type
055 *     <li>Authorization
056 *     <li>Accept
057 *     <li>Etc.
058 * </ul>
059 *
060 * <p>Supported timeouts:
061 *
062 * <ul>
063 *     <li>On HTTP connect
064 *     <li>On HTTP response read
065 * </ul>
066 *
067 * <p>HTTP 3xx redirection: follow (default) / don't follow
068 */
069@ThreadSafe
070public class HTTPRequest extends HTTPMessage {
071
072
073        /**
074         * Enumeration of the HTTP methods used in OAuth 2.0 requests.
075         */
076        public enum Method {
077        
078                /**
079                 * HTTP GET.
080                 */
081                GET,
082                
083                
084                /**
085                 * HTTP POST.
086                 */
087                POST,
088                
089                
090                /**
091                 * HTTP PUT.
092                 */
093                PUT,
094                
095                
096                /**
097                 * HTTP DELETE.
098                 */
099                DELETE
100        }
101        
102        
103        /**
104         * The request method.
105         */
106        private final Method method;
107
108
109        /**
110         * The request URL.
111         */
112        private final URL url;
113        
114        
115        /**
116         * The query string / post body.
117         */
118        private String query = null;
119
120
121        /**
122         * The fragment.
123         */
124        private String fragment = null;
125
126
127        /**
128         * The HTTP connect timeout, in milliseconds. Zero implies none.
129         */
130        private int connectTimeout = 0;
131
132
133        /**
134         * The HTTP response read timeout, in milliseconds. Zero implies none.
135
136         */
137        private int readTimeout = 0;
138
139
140        /**
141         * Controls HTTP 3xx redirections.
142         */
143        private boolean followRedirects = true;
144        
145        
146        /**
147         * The received validated client X.509 certificate for a received HTTPS
148         * request, {@code null} if not specified.
149         */
150        private X509Certificate clientX509Certificate = null;
151        
152        
153        /**
154         * The subject DN of a received client X.509 certificate for a received
155         * HTTPS request, {@code null} if not specified.
156         */
157        private String clientX509CertificateSubjectDN = null;
158        
159        
160        /**
161         * The root issuer DN of a received client X.509 certificate for a
162         * received HTTPS request, {@code null} if not specified.
163         */
164        private String clientX509CertificateRootDN = null;
165        
166        
167        /**
168         * The hostname verifier to use for outgoing HTTPS requests,
169         * {@code null} implies the default one.
170         */
171        private HostnameVerifier hostnameVerifier = null;
172        
173        
174        /**
175         * The SSL socket factory to use for outgoing HTTPS requests,
176         * {@code null} implies the default one.
177         */
178        private SSLSocketFactory sslSocketFactory = null;
179
180
181        /**
182         * The default hostname verifier for all outgoing HTTPS requests.
183         */
184        private static HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
185
186
187        /**
188         * The default socket factory for all outgoing HTTPS requests.
189         */
190        private static SSLSocketFactory defaultSSLSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
191        
192        
193        /**
194         * Creates a new minimally specified HTTP request.
195         *
196         * @param method The HTTP request method. Must not be {@code null}.
197         * @param url    The HTTP request URL. Must not be {@code null}.
198         */
199        public HTTPRequest(final Method method, final URL url) {
200        
201                if (method == null)
202                        throw new IllegalArgumentException("The HTTP method must not be null");
203                
204                this.method = method;
205
206
207                if (url == null)
208                        throw new IllegalArgumentException("The HTTP URL must not be null");
209
210                this.url = url;
211        }
212        
213        
214        /**
215         * Gets the request method.
216         *
217         * @return The request method.
218         */
219        public Method getMethod() {
220        
221                return method;
222        }
223
224
225        /**
226         * Gets the request URL.
227         *
228         * @return The request URL.
229         */
230        public URL getURL() {
231
232                return url;
233        }
234
235
236        /**
237         * Gets the request URL as URI.
238         *
239         * @return The request URL as URI.
240         */
241        public URI getURI() {
242                
243                try {
244                        return url.toURI();
245                } catch (URISyntaxException e) {
246                        // Should never happen
247                        throw new IllegalStateException(e.getMessage(), e);
248                }
249        }
250        
251        
252        /**
253         * Ensures this HTTP request has the specified method.
254         *
255         * @param expectedMethod The expected method. Must not be {@code null}.
256         *
257         * @throws ParseException If the method doesn't match the expected.
258         */
259        public void ensureMethod(final Method expectedMethod)
260                throws ParseException {
261                
262                if (method != expectedMethod)
263                        throw new ParseException("The HTTP request method must be " + expectedMethod);
264        }
265        
266        
267        /**
268         * Gets the {@code Authorization} header value.
269         *
270         * @return The {@code Authorization} header value, {@code null} if not 
271         *         specified.
272         */
273        public String getAuthorization() {
274        
275                return getHeaderValue("Authorization");
276        }
277        
278        
279        /**
280         * Sets the {@code Authorization} header value.
281         *
282         * @param authz The {@code Authorization} header value, {@code null} if 
283         *              not specified.
284         */
285        public void setAuthorization(final String authz) {
286        
287                setHeader("Authorization", authz);
288        }
289
290
291        /**
292         * Gets the {@code Accept} header value.
293         *
294         * @return The {@code Accept} header value, {@code null} if not
295         *         specified.
296         */
297        public String getAccept() {
298
299                return getHeaderValue("Accept");
300        }
301
302
303        /**
304         * Sets the {@code Accept} header value.
305         *
306         * @param accept The {@code Accept} header value, {@code null} if not
307         *               specified.
308         */
309        public void setAccept(final String accept) {
310
311                setHeader("Accept", accept);
312        }
313        
314        
315        /**
316         * Gets the raw (undecoded) query string if the request is HTTP GET or
317         * the entity body if the request is HTTP POST.
318         *
319         * <p>Note that the '?' character preceding the query string in GET
320         * requests is not included in the returned string.
321         *
322         * <p>Example query string (line breaks for clarity):
323         *
324         * <pre>
325         * response_type=code
326         * &amp;client_id=s6BhdRkqt3
327         * &amp;state=xyz
328         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
329         * </pre>
330         *
331         * @return For HTTP GET requests the URL query string, for HTTP POST 
332         *         requests the body. {@code null} if not specified.
333         */
334        public String getQuery() {
335        
336                return query;
337        }
338        
339        
340        /**
341         * Sets the raw (undecoded) query string if the request is HTTP GET or
342         * the entity body if the request is HTTP POST.
343         *
344         * <p>Note that the '?' character preceding the query string in GET
345         * requests must not be included.
346         *
347         * <p>Example query string (line breaks for clarity):
348         *
349         * <pre>
350         * response_type=code
351         * &amp;client_id=s6BhdRkqt3
352         * &amp;state=xyz
353         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
354         * </pre>
355         *
356         * @param query For HTTP GET requests the URL query string, for HTTP 
357         *              POST requests the body. {@code null} if not specified.
358         */
359        public void setQuery(final String query) {
360        
361                this.query = query;
362        }
363
364
365        /**
366         * Ensures this HTTP response has a specified query string or entity
367         * body.
368         *
369         * @throws ParseException If the query string or entity body is missing
370         *                        or empty.
371         */
372        private void ensureQuery()
373                throws ParseException {
374                
375                if (query == null || query.trim().isEmpty())
376                        throw new ParseException("Missing or empty HTTP query string / entity body");
377        }
378        
379        
380        /**
381         * Gets the request query as a parameter map. The parameters are 
382         * decoded according to {@code application/x-www-form-urlencoded}.
383         *
384         * @return The request query parameters, decoded. If none the map will
385         *         be empty.
386         */
387        public Map<String,List<String>> getQueryParameters() {
388        
389                return URLUtils.parseParameters(query);
390        }
391
392
393        /**
394         * Gets the request query or entity body as a JSON Object.
395         *
396         * @return The request query or entity body as a JSON object.
397         *
398         * @throws ParseException If the Content-Type header isn't 
399         *                        {@code application/json}, the request query
400         *                        or entity body is {@code null}, empty or 
401         *                        couldn't be parsed to a valid JSON object.
402         */
403        public JSONObject getQueryAsJSONObject()
404                throws ParseException {
405
406                ensureContentType(CommonContentTypes.APPLICATION_JSON);
407
408                ensureQuery();
409
410                return JSONObjectUtils.parse(query);
411        }
412
413
414        /**
415         * Gets the raw (undecoded) request fragment.
416         *
417         * @return The request fragment, {@code null} if not specified.
418         */
419        public String getFragment() {
420
421                return fragment;
422        }
423
424
425        /**
426         * Sets the raw (undecoded) request fragment.
427         *
428         * @param fragment The request fragment, {@code null} if not specified.
429         */
430        public void setFragment(final String fragment) {
431
432                this.fragment = fragment;
433        }
434
435
436        /**
437         * Gets the HTTP connect timeout.
438         *
439         * @return The HTTP connect timeout, in milliseconds. Zero implies no
440         *         timeout.
441         */
442        public int getConnectTimeout() {
443
444                return connectTimeout;
445        }
446
447
448        /**
449         * Sets the HTTP connect timeout.
450         *
451         * @param connectTimeout The HTTP connect timeout, in milliseconds.
452         *                       Zero implies no timeout. Must not be negative.
453         */
454        public void setConnectTimeout(final int connectTimeout) {
455
456                if (connectTimeout < 0) {
457                        throw new IllegalArgumentException("The HTTP connect timeout must be zero or positive");
458                }
459
460                this.connectTimeout = connectTimeout;
461        }
462
463
464        /**
465         * Gets the HTTP response read timeout.
466         *
467         * @return The HTTP response read timeout, in milliseconds. Zero
468         *         implies no timeout.
469         */
470        public int getReadTimeout() {
471
472                return readTimeout;
473        }
474
475
476        /**
477         * Sets the HTTP response read timeout.
478         *
479         * @param readTimeout The HTTP response read timeout, in milliseconds.
480         *                    Zero implies no timeout. Must not be negative.
481         */
482        public void setReadTimeout(final int readTimeout) {
483
484                if (readTimeout < 0) {
485                        throw new IllegalArgumentException("The HTTP response read timeout must be zero or positive");
486                }
487
488                this.readTimeout = readTimeout;
489        }
490
491
492        /**
493         * Gets the boolean setting whether HTTP redirects (requests with
494         * response code 3xx) should be automatically followed.
495         *
496         * @return {@code true} if HTTP redirects are automatically followed,
497         *         else {@code false}.
498         */
499        public boolean getFollowRedirects() {
500
501                return followRedirects;
502        }
503
504
505        /**
506         * Sets whether HTTP redirects (requests with response code 3xx) should
507         * be automatically followed.
508         *
509         * @param follow Whether or not to follow HTTP redirects.
510         */
511        public void setFollowRedirects(final boolean follow) {
512
513                followRedirects = follow;
514        }
515        
516        
517        /**
518         * Gets the received validated client X.509 certificate for a received
519         * HTTPS request.
520         *
521         * @return The client X.509 certificate, {@code null} if not specified.
522         */
523        public X509Certificate getClientX509Certificate() {
524                
525                return clientX509Certificate;
526        }
527        
528        
529        /**
530         * Sets the received validated client X.509 certificate for a received
531         * HTTPS request.
532         *
533         * @param clientX509Certificate The client X.509 certificate,
534         *                              {@code null} if not specified.
535         */
536        public void setClientX509Certificate(final X509Certificate clientX509Certificate) {
537                
538                this.clientX509Certificate = clientX509Certificate;
539        }
540        
541        
542        /**
543         * Gets the subject DN of a received validated client X.509 certificate
544         * for a received HTTPS request.
545         *
546         * @return The subject DN, {@code null} if not specified.
547         */
548        public String getClientX509CertificateSubjectDN() {
549                
550                return clientX509CertificateSubjectDN;
551        }
552        
553        
554        /**
555         * Sets the subject DN of a received validated client X.509 certificate
556         * for a received HTTPS request.
557         *
558         * @param subjectDN The subject DN, {@code null} if not specified.
559         */
560        public void setClientX509CertificateSubjectDN(final String subjectDN) {
561                
562                this.clientX509CertificateSubjectDN = subjectDN;
563        }
564        
565        
566        /**
567         * Gets the root issuer DN of a received validated client X.509
568         * certificate for a received HTTPS request.
569         *
570         * @return The root DN, {@code null} if not specified.
571         */
572        public String getClientX509CertificateRootDN() {
573                
574                return clientX509CertificateRootDN;
575        }
576        
577        
578        /**
579         * Sets the root issuer DN of a received validated client X.509
580         * certificate for a received HTTPS request.
581         *
582         * @param rootDN The root DN, {@code null} if not specified.
583         */
584        public void setClientX509CertificateRootDN(final String rootDN) {
585                
586                this.clientX509CertificateRootDN = rootDN;
587        }
588        
589        
590        /**
591         * Gets the hostname verifier for outgoing HTTPS requests.
592         *
593         * @return The hostname verifier, {@code null} implies use of the
594         *         {@link #getDefaultHostnameVerifier() default one}.
595         */
596        public HostnameVerifier getHostnameVerifier() {
597                
598                return hostnameVerifier;
599        }
600        
601        
602        /**
603         * Sets the hostname verifier for outgoing HTTPS requests.
604         *
605         * @param hostnameVerifier The hostname verifier, {@code null} implies
606         *                         use of the
607         *                         {@link #getDefaultHostnameVerifier() default
608         *                         one}.
609         */
610        public void setHostnameVerifier(final HostnameVerifier hostnameVerifier) {
611                
612                this.hostnameVerifier = hostnameVerifier;
613        }
614        
615        
616        /**
617         * Gets the SSL factory for outgoing HTTPS requests.
618         *
619         * @return The SSL factory, {@code null} implies of the default one.
620         */
621        public SSLSocketFactory getSSLSocketFactory() {
622                
623                return sslSocketFactory;
624        }
625        
626        
627        /**
628         * Sets the SSL factory for outgoing HTTPS requests.
629         *
630         * @param sslSocketFactory The SSL factory, {@code null} implies use of
631         *                         the default one.
632         */
633        public void setSSLSocketFactory(final SSLSocketFactory sslSocketFactory) {
634                
635                this.sslSocketFactory = sslSocketFactory;
636        }
637        
638        
639        /**
640         * Returns the default hostname verifier for all outgoing HTTPS
641         * requests.
642         *
643         * @return The hostname verifier.
644         */
645        public static HostnameVerifier getDefaultHostnameVerifier() {
646
647                return defaultHostnameVerifier;
648        }
649
650
651        /**
652         * Sets the default hostname verifier for all outgoing HTTPS requests.
653         * May be overridden on a individual request basis.
654         *
655         * @param defaultHostnameVerifier The hostname verifier. Must not be
656         *                         {@code null}.
657         */
658        public static void setDefaultHostnameVerifier(final HostnameVerifier defaultHostnameVerifier) {
659
660                if (defaultHostnameVerifier == null) {
661                        throw new IllegalArgumentException("The hostname verifier must not be null");
662                }
663
664                HTTPRequest.defaultHostnameVerifier = defaultHostnameVerifier;
665        }
666
667
668        /**
669         * Returns the default SSL socket factory for all outgoing HTTPS
670         * requests.
671         *
672         * @return The SSL socket factory.
673         */
674        public static SSLSocketFactory getDefaultSSLSocketFactory() {
675
676                return defaultSSLSocketFactory;
677        }
678
679
680        /**
681         * Sets the default SSL socket factory for all outgoing HTTPS requests.
682         * May be overridden on a individual request basis.
683         *
684         * @param sslSocketFactory The SSL socket factory. Must not be
685         *                         {@code null}.
686         */
687        public static void setDefaultSSLSocketFactory(final SSLSocketFactory sslSocketFactory) {
688
689                if (sslSocketFactory == null) {
690                        throw new IllegalArgumentException("The SSL socket factory must not be null");
691                }
692
693                HTTPRequest.defaultSSLSocketFactory = sslSocketFactory;
694        }
695
696
697        /**
698         * Returns an established HTTP URL connection for this HTTP request.
699         * Deprecated as of v5.31, use {@link #toHttpURLConnection()} with
700         * {@link #setHostnameVerifier} and {@link #setSSLSocketFactory}
701         * instead.
702         *
703         * @param hostnameVerifier The hostname verifier for outgoing HTTPS
704         *                         requests, {@code null} implies use of the
705         *                         {@link #getDefaultHostnameVerifier() default
706         *                         one}.
707         * @param sslSocketFactory The SSL socket factory for HTTPS requests,
708         *                         {@code null} implies use of the
709         *                         {@link #getDefaultSSLSocketFactory() default
710         *                         one}.
711         *
712         * @return The HTTP URL connection, with the request sent and ready to
713         *         read the response.
714         *
715         * @throws IOException If the HTTP request couldn't be made, due to a
716         *                     network or other error.
717         */
718        @Deprecated
719        public HttpURLConnection toHttpURLConnection(final HostnameVerifier hostnameVerifier,
720                                                     final SSLSocketFactory sslSocketFactory)
721                throws IOException {
722                
723                HostnameVerifier savedHostnameVerifier = getHostnameVerifier();
724                SSLSocketFactory savedSSLFactory = getSSLSocketFactory();
725                
726                try {
727                        // Set for this HTTP URL connection only
728                        setHostnameVerifier(hostnameVerifier);
729                        setSSLSocketFactory(sslSocketFactory);
730                        
731                        return toHttpURLConnection();
732                        
733                } finally {
734                        setHostnameVerifier(savedHostnameVerifier);
735                        setSSLSocketFactory(savedSSLFactory);
736                }
737        }
738
739
740        /**
741         * Returns an established HTTP URL connection for this HTTP request.
742         *
743         * @return The HTTP URL connection, with the request sent and ready to
744         *         read the response.
745         *
746         * @throws IOException If the HTTP request couldn't be made, due to a
747         *                     network or other error.
748         */
749        public HttpURLConnection toHttpURLConnection()
750                throws IOException {
751
752                URL finalURL = url;
753
754                if (query != null && (method.equals(HTTPRequest.Method.GET) || method.equals(Method.DELETE))) {
755
756                        // Append query string
757                        StringBuilder sb = new StringBuilder(url.toString());
758                        sb.append('?');
759                        sb.append(query);
760
761                        try {
762                                finalURL = new URL(sb.toString());
763
764                        } catch (MalformedURLException e) {
765
766                                throw new IOException("Couldn't append query string: " + e.getMessage(), e);
767                        }
768                }
769
770                if (fragment != null) {
771
772                        // Append raw fragment
773                        StringBuilder sb = new StringBuilder(finalURL.toString());
774                        sb.append('#');
775                        sb.append(fragment);
776
777                        try {
778                                finalURL = new URL(sb.toString());
779
780                        } catch (MalformedURLException e) {
781
782                                throw new IOException("Couldn't append raw fragment: " + e.getMessage(), e);
783                        }
784                }
785
786                HttpURLConnection conn = (HttpURLConnection)finalURL.openConnection();
787
788                if (conn instanceof HttpsURLConnection) {
789                        HttpsURLConnection sslConn = (HttpsURLConnection)conn;
790                        sslConn.setHostnameVerifier(hostnameVerifier != null ? hostnameVerifier : getDefaultHostnameVerifier());
791                        sslConn.setSSLSocketFactory(sslSocketFactory != null ? sslSocketFactory : getDefaultSSLSocketFactory());
792                }
793
794                for (Map.Entry<String,List<String>> header: getHeaderMap().entrySet()) {
795                        for (String headerValue: header.getValue()) {
796                                conn.addRequestProperty(header.getKey(), headerValue);
797                        }
798                }
799
800                conn.setRequestMethod(method.name());
801                conn.setConnectTimeout(connectTimeout);
802                conn.setReadTimeout(readTimeout);
803                conn.setInstanceFollowRedirects(followRedirects);
804
805                if (method.equals(HTTPRequest.Method.POST) || method.equals(Method.PUT)) {
806
807                        conn.setDoOutput(true);
808
809                        if (getContentType() != null)
810                                conn.setRequestProperty("Content-Type", getContentType().toString());
811
812                        if (query != null) {
813                                try {
814                                        OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
815                                        writer.write(query);
816                                        writer.close();
817                                } catch (IOException e) {
818                                        closeStreams(conn);
819                                        throw e; // Rethrow
820                                }
821                        }
822                }
823
824                return conn;
825        }
826
827
828        /**
829         * Sends this HTTP request to the request URL and retrieves the
830         * resulting HTTP response. Deprecated as of v5.31, use
831         * {@link #toHttpURLConnection()} with {@link #setHostnameVerifier} and
832         * {@link #setSSLSocketFactory} instead.
833         *
834         * @param hostnameVerifier The hostname verifier for outgoing HTTPS
835         *                         requests, {@code null} implies use of the
836         *                         {@link #getDefaultHostnameVerifier() default
837         *                         one}.
838         * @param sslSocketFactory The SSL socket factory for HTTPS requests,
839         *                         {@code null} implies use of the
840         *                         {@link #getDefaultSSLSocketFactory() default
841         *                         one}.
842         *
843         * @return The resulting HTTP response.
844         *
845         * @throws IOException If the HTTP request couldn't be made, due to a
846         *                     network or other error.
847         */
848        @Deprecated
849        public HTTPResponse send(final HostnameVerifier hostnameVerifier,
850                                 final SSLSocketFactory sslSocketFactory)
851                throws IOException {
852                
853                HostnameVerifier savedHostnameVerifier = getHostnameVerifier();
854                SSLSocketFactory savedSSLFactory = getSSLSocketFactory();
855                
856                try {
857                        // Set for this HTTP URL connection only
858                        setHostnameVerifier(hostnameVerifier);
859                        setSSLSocketFactory(sslSocketFactory);
860                        
861                        return send();
862                        
863                } finally {
864                        setHostnameVerifier(savedHostnameVerifier);
865                        setSSLSocketFactory(savedSSLFactory);
866                }
867        }
868
869
870        /**
871         * Sends this HTTP request to the request URL and retrieves the
872         * resulting HTTP response.
873         *
874         * @return The resulting HTTP response.
875         *
876         * @throws IOException If the HTTP request couldn't be made, due to a
877         *                     network or other error.
878         */
879        public HTTPResponse send()
880                throws IOException {
881
882                HttpURLConnection conn = toHttpURLConnection();
883
884                int statusCode;
885
886                BufferedReader reader;
887
888                try {
889                        // Open a connection, then send method and headers
890                        reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), Charset.forName("UTF-8")));
891
892                        // The next step is to get the status
893                        statusCode = conn.getResponseCode();
894
895                } catch (IOException e) {
896
897                        // HttpUrlConnection will throw an IOException if any
898                        // 4XX response is sent. If we request the status
899                        // again, this time the internal status will be
900                        // properly set, and we'll be able to retrieve it.
901                        statusCode = conn.getResponseCode();
902
903                        if (statusCode == -1) {
904                                throw e; // Rethrow IO exception
905                        } else {
906                                // HTTP status code indicates the response got
907                                // through, read the content but using error stream
908                                InputStream errStream = conn.getErrorStream();
909
910                                if (errStream != null) {
911                                        // We have useful HTTP error body
912                                        reader = new BufferedReader(new InputStreamReader(errStream));
913                                } else {
914                                        // No content, set to empty string
915                                        reader = new BufferedReader(new StringReader(""));
916                                }
917                        }
918                }
919
920                StringBuilder body = new StringBuilder();
921                String line;
922                while ((line = reader.readLine()) != null) {
923                        body.append(line);
924                        body.append(System.getProperty("line.separator"));
925                }
926                reader.close();
927
928
929                HTTPResponse response = new HTTPResponse(statusCode);
930                
931                response.setStatusMessage(conn.getResponseMessage());
932
933                // Set headers
934                for (Map.Entry<String,List<String>> responseHeader: conn.getHeaderFields().entrySet()) {
935
936                        if (responseHeader.getKey() == null) {
937                                continue; // skip header
938                        }
939
940                        List<String> values = responseHeader.getValue();
941                        if (values == null || values.isEmpty() || values.get(0) == null) {
942                                continue; // skip header
943                        }
944
945                        response.setHeader(responseHeader.getKey(), values.toArray(new String[]{}));
946                }
947
948                closeStreams(conn);
949
950                final String bodyContent = body.toString();
951                if (! bodyContent.isEmpty())
952                        response.setContent(bodyContent);
953
954                return response;
955        }
956
957
958        /**
959         * Closes the input, output and error streams of the specified HTTP URL
960         * connection. No attempt is made to close the underlying socket with
961         * {@code conn.disconnect} so it may be cached (HTTP 1.1 keep live).
962         * See http://techblog.bozho.net/caveats-of-httpurlconnection/
963         *
964         * @param conn The HTTP URL connection. May be {@code null}.
965         */
966        private static void closeStreams(final HttpURLConnection conn) {
967
968                if (conn == null) {
969                        return;
970                }
971
972                try {
973                        if (conn.getInputStream() != null) {
974                                conn.getInputStream().close();
975                        }
976                } catch (Exception e) {
977                        // ignore
978                }
979
980                try {
981                        if (conn.getOutputStream() != null) {
982                                conn.getOutputStream().close();
983                        }
984                } catch (Exception e) {
985                        // ignore
986                }
987
988                try {
989                        if (conn.getErrorStream() != null) {
990                                conn.getOutputStream().close();
991                        }
992                } catch (Exception e) {
993                        // ignore
994                }
995        }
996}