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