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