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