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