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