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                        while ((line = reader.readLine()) != null) {
228                        
229                                body.append(line);
230                                body.append(System.getProperty("line.separator"));
231                        }
232                        
233                        reader.close();
234                        
235                        setQuery(body.toString());
236                }
237        }
238        
239        
240        /**
241         * Gets the request method.
242         *
243         * @return The request method.
244         */
245        public Method getMethod() {
246        
247                return method;
248        }
249
250
251        /**
252         * Gets the request URL.
253         *
254         * @return The request URL.
255         */
256        public URL getURL() {
257
258                return url;
259        }
260        
261        
262        /**
263         * Ensures this HTTP request has the specified method.
264         *
265         * @param expectedMethod The expected method. Must not be {@code null}.
266         *
267         * @throws ParseException If the method doesn't match the expected.
268         */
269        public void ensureMethod(final Method expectedMethod)
270                throws ParseException {
271                
272                if (method != expectedMethod)
273                        throw new ParseException("The HTTP request method must be " + expectedMethod);
274        }
275        
276        
277        /**
278         * Gets the {@code Authorization} header value.
279         *
280         * @return The {@code Authorization} header value, {@code null} if not 
281         *         specified.
282         */
283        public String getAuthorization() {
284        
285                return authorization;
286        }
287        
288        
289        /**
290         * Sets the {@code Authorization} header value.
291         *
292         * @param authz The {@code Authorization} header value, {@code null} if 
293         *              not specified.
294         */
295        public void setAuthorization(final String authz) {
296        
297                authorization = authz;
298        }
299        
300        
301        /**
302         * Gets the raw (undecoded) query string if the request is HTTP GET or
303         * the entity body if the request is HTTP POST.
304         *
305         * <p>Note that the '?' character preceding the query string in GET
306         * requests is not included in the returned string.
307         *
308         * <p>Example query string (line breaks for clarity):
309         *
310         * <pre>
311         * response_type=code
312         * &amp;client_id=s6BhdRkqt3
313         * &amp;state=xyz
314         * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
315         * </pre>
316         *
317         * @return For HTTP GET requests the URL query string, for HTTP POST 
318         *         requests the body. {@code null} if not specified.
319         */
320        public String getQuery() {
321        
322                return query;
323        }
324        
325        
326        /**
327         * Sets 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 must not be included.
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         * @param query For HTTP GET requests the URL query string, for HTTP 
343         *              POST requests the body. {@code null} if not specified.
344         */
345        public void setQuery(final String query) {
346        
347                this.query = query;
348        }
349
350
351        /**
352         * Ensures this HTTP response has a specified query string or entity
353         * body.
354         *
355         * @throws ParseException If the query string or entity body is missing
356         *                        or empty.
357         */
358        private void ensureQuery()
359                throws ParseException {
360                
361                if (query == null || query.isEmpty())
362                        throw new ParseException("Missing or empty HTTP query string / entity body");
363        }
364        
365        
366        /**
367         * Gets the request query as a parameter map. The parameters are 
368         * decoded according to {@code application/x-www-form-urlencoded}.
369         *
370         * @return The request query parameters, decoded. If none the map will
371         *         be empty.
372         */
373        public Map<String,String> getQueryParameters() {
374        
375                return URLUtils.parseParameters(query);
376        }
377
378
379        /**
380         * Gets the request query or entity body as a JSON Object.
381         *
382         * @return The request query or entity body as a JSON object.
383         *
384         * @throws ParseException If the Content-Type header isn't 
385         *                        {@code application/json}, the request query
386         *                        or entity body is {@code null}, empty or 
387         *                        couldn't be parsed to a valid JSON object.
388         */
389        public JSONObject getQueryAsJSONObject()
390                throws ParseException {
391
392                ensureContentType(CommonContentTypes.APPLICATION_JSON);
393
394                ensureQuery();
395
396                return JSONObjectUtils.parseJSONObject(query);
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                HttpURLConnection conn = (HttpURLConnection)finalURL.openConnection();
431
432                if (authorization != null)
433                        conn.setRequestProperty("Authorization", authorization);
434
435                conn.setRequestMethod(method.name());
436
437                if (method.equals(HTTPRequest.Method.POST) || method.equals(Method.PUT)) {
438
439                        conn.setDoOutput(true);
440
441                        if (getContentType() != null)
442                                conn.setRequestProperty("Content-Type", getContentType().toString());
443
444                        if (query != null) {
445                                OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
446                                writer.write(query);
447                                writer.close();
448                        }
449                }
450
451                return conn;
452        }
453
454
455        /**
456         * Sends this HTTP request to the request URL and retrieves the 
457         * resulting HTTP response.
458         *
459         * @return The resulting HTTP response.
460         *
461         * @throws IOException If the HTTP request couldn't be made, due to a 
462         *                     network or other error.
463         */
464        public HTTPResponse send()
465                throws IOException {
466
467                HttpURLConnection conn = toHttpURLConnection();
468
469                int statusCode;
470
471                BufferedReader reader;
472
473                try {
474                        // Open a connection, then send method and headers
475                        reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
476
477                        // The next step is to get the status
478                        statusCode = conn.getResponseCode();
479
480                } catch (IOException e) {
481
482                        // HttpUrlConnection will throw an IOException if any
483                        // 4XX response is sent. If we request the status
484                        // again, this time the internal status will be
485                        // properly set, and we'll be able to retrieve it.
486                        statusCode = conn.getResponseCode();
487
488                        if (statusCode == -1) {
489                                // Rethrow IO exception
490                                throw e;
491                        } else {
492                                // HTTP status code indicates the response got
493                                // through, read the content but using error
494                                // stream
495                                reader = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
496                        }
497                }
498
499                StringBuilder body = new StringBuilder();
500
501
502                try {
503                        String line;
504
505                        while ((line = reader.readLine()) != null) {
506
507                                body.append(line);
508                                body.append(System.getProperty("line.separator"));
509                        }
510
511                        reader.close();
512
513                } finally {
514                        conn.disconnect();
515                }
516
517
518                HTTPResponse response = new HTTPResponse(statusCode);
519
520                String location = conn.getHeaderField("Location");
521
522                if (location != null) {
523
524                        try {
525                                response.setLocation(new URL(location));
526
527                        } catch (MalformedURLException e) {
528
529                                throw new IOException("Couldn't parse Location header: " + e.getMessage(), e);
530                        }
531                }
532
533
534                try {
535                        response.setContentType(conn.getContentType());
536
537                } catch (ParseException e) {
538
539                        throw new IOException("Couldn't parse Content-Type header: " + e.getMessage(), e);
540                }
541
542
543                response.setCacheControl(conn.getHeaderField("Cache-Control"));
544
545                response.setPragma(conn.getHeaderField("Pragma"));
546
547                response.setWWWAuthenticate(conn.getHeaderField("WWW-Authenticate"));
548
549                String bodyContent = body.toString();
550
551                if (! bodyContent.isEmpty())
552                        response.setContent(bodyContent);
553
554
555                return response;
556        }
557}