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