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