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 getHeaderValue("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 getHeaderValue("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,List<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,List<String>> header: getHeaderMap().entrySet()) {
781                        for (String headerValue: header.getValue()) {
782                                conn.addRequestProperty(header.getKey(), headerValue);
783                        }
784                }
785
786                conn.setRequestMethod(method.name());
787                conn.setConnectTimeout(connectTimeout);
788                conn.setReadTimeout(readTimeout);
789                conn.setInstanceFollowRedirects(followRedirects);
790
791                if (method.equals(HTTPRequest.Method.POST) || method.equals(Method.PUT)) {
792
793                        conn.setDoOutput(true);
794
795                        if (getContentType() != null)
796                                conn.setRequestProperty("Content-Type", getContentType().toString());
797
798                        if (query != null) {
799                                try {
800                                        OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
801                                        writer.write(query);
802                                        writer.close();
803                                } catch (IOException e) {
804                                        closeStreams(conn);
805                                        throw e; // Rethrow
806                                }
807                        }
808                }
809
810                return conn;
811        }
812
813
814        /**
815         * Sends this HTTP request to the request URL and retrieves the
816         * resulting HTTP response. Deprecated as of v5.31, use
817         * {@link #toHttpURLConnection()} with {@link #setHostnameVerifier} and
818         * {@link #setSSLSocketFactory} instead.
819         *
820         * @param hostnameVerifier The hostname verifier for outgoing HTTPS
821         *                         requests, {@code null} implies use of the
822         *                         {@link #getDefaultHostnameVerifier() default
823         *                         one}.
824         * @param sslSocketFactory The SSL socket factory for HTTPS requests,
825         *                         {@code null} implies use of the
826         *                         {@link #getDefaultSSLSocketFactory() default
827         *                         one}.
828         *
829         * @return The resulting HTTP response.
830         *
831         * @throws IOException If the HTTP request couldn't be made, due to a
832         *                     network or other error.
833         */
834        @Deprecated
835        public HTTPResponse send(final HostnameVerifier hostnameVerifier,
836                                 final SSLSocketFactory sslSocketFactory)
837                throws IOException {
838                
839                HostnameVerifier savedHostnameVerifier = getHostnameVerifier();
840                SSLSocketFactory savedSSLFactory = getSSLSocketFactory();
841                
842                try {
843                        // Set for this HTTP URL connection only
844                        setHostnameVerifier(hostnameVerifier);
845                        setSSLSocketFactory(sslSocketFactory);
846                        
847                        return send();
848                        
849                } finally {
850                        setHostnameVerifier(savedHostnameVerifier);
851                        setSSLSocketFactory(savedSSLFactory);
852                }
853        }
854
855
856        /**
857         * Sends this HTTP request to the request URL and retrieves the
858         * resulting HTTP response.
859         *
860         * @return The resulting HTTP response.
861         *
862         * @throws IOException If the HTTP request couldn't be made, due to a
863         *                     network or other error.
864         */
865        public HTTPResponse send()
866                throws IOException {
867
868                HttpURLConnection conn = toHttpURLConnection();
869
870                int statusCode;
871
872                BufferedReader reader;
873
874                try {
875                        // Open a connection, then send method and headers
876                        reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), Charset.forName("UTF-8")));
877
878                        // The next step is to get the status
879                        statusCode = conn.getResponseCode();
880
881                } catch (IOException e) {
882
883                        // HttpUrlConnection will throw an IOException if any
884                        // 4XX response is sent. If we request the status
885                        // again, this time the internal status will be
886                        // properly set, and we'll be able to retrieve it.
887                        statusCode = conn.getResponseCode();
888
889                        if (statusCode == -1) {
890                                throw e; // Rethrow IO exception
891                        } else {
892                                // HTTP status code indicates the response got
893                                // through, read the content but using error stream
894                                InputStream errStream = conn.getErrorStream();
895
896                                if (errStream != null) {
897                                        // We have useful HTTP error body
898                                        reader = new BufferedReader(new InputStreamReader(errStream));
899                                } else {
900                                        // No content, set to empty string
901                                        reader = new BufferedReader(new StringReader(""));
902                                }
903                        }
904                }
905
906                StringBuilder body = new StringBuilder();
907                String line;
908                while ((line = reader.readLine()) != null) {
909                        body.append(line);
910                        body.append(System.getProperty("line.separator"));
911                }
912                reader.close();
913
914
915                HTTPResponse response = new HTTPResponse(statusCode);
916                
917                response.setStatusMessage(conn.getResponseMessage());
918
919                // Set headers
920                for (Map.Entry<String,List<String>> responseHeader: conn.getHeaderFields().entrySet()) {
921
922                        if (responseHeader.getKey() == null) {
923                                continue; // skip header
924                        }
925
926                        List<String> values = responseHeader.getValue();
927                        if (values == null || values.isEmpty() || values.get(0) == null) {
928                                continue; // skip header
929                        }
930
931                        response.setHeader(responseHeader.getKey(), values.toArray(new String[]{}));
932                }
933
934                closeStreams(conn);
935
936                final String bodyContent = body.toString();
937                if (! bodyContent.isEmpty())
938                        response.setContent(bodyContent);
939
940                return response;
941        }
942
943
944        /**
945         * Closes the input, output and error streams of the specified HTTP URL
946         * connection. No attempt is made to close the underlying socket with
947         * {@code conn.disconnect} so it may be cached (HTTP 1.1 keep live).
948         * See http://techblog.bozho.net/caveats-of-httpurlconnection/
949         *
950         * @param conn The HTTP URL connection. May be {@code null}.
951         */
952        private static void closeStreams(final HttpURLConnection conn) {
953
954                if (conn == null) {
955                        return;
956                }
957
958                try {
959                        if (conn.getInputStream() != null) {
960                                conn.getInputStream().close();
961                        }
962                } catch (Exception e) {
963                        // ignore
964                }
965
966                try {
967                        if (conn.getOutputStream() != null) {
968                                conn.getOutputStream().close();
969                        }
970                } catch (Exception e) {
971                        // ignore
972                }
973
974                try {
975                        if (conn.getErrorStream() != null) {
976                                conn.getOutputStream().close();
977                        }
978                } catch (Exception e) {
979                        // ignore
980                }
981        }
982}