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