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