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