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