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