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