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