001    package com.nimbusds.oauth2.sdk.http;
002    
003    
004    import java.io.BufferedReader;
005    import java.io.InputStreamReader;
006    import java.io.IOException;
007    import java.io.OutputStreamWriter;
008    import java.net.HttpURLConnection;
009    import java.net.MalformedURLException;
010    import java.net.URL;
011    import java.util.Map;
012    
013    import javax.servlet.http.HttpServletRequest;
014    
015    import net.jcip.annotations.ThreadSafe;
016    
017    import net.minidev.json.JSONObject;
018    
019    import com.nimbusds.oauth2.sdk.ParseException;
020    import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
021    import 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}. This
027     * class is thread-safe.
028     *
029     * <p>Supported HTTP methods:
030     *
031     * <ul>
032     *     <li>{@link Method#GET HTTP GET}
033     *     <li>{@link Method#POST HTTP POST}
034     * </ul>
035     *
036     * <p>Supported request headers:
037     *
038     * <ul>
039     *     <li>Content-Type
040     *     <li>Authorization
041     * </ul>
042     *
043     * @author Vladimir Dzhuvinov
044     * @version $version$ (2013-05-13)
045     */
046    @ThreadSafe
047    public 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            /**
069             * The request method.
070             */
071            private final Method method;
072    
073    
074            /**
075             * The request URL.
076             */
077            private final URL url;
078            
079            
080            /**
081             * Specifies an {@code Authorization} header value.
082             */
083            private String authorization = null;
084            
085            
086            /**
087             * The query string / post body.
088             */
089            private String query = null;
090            
091            
092            /**
093             * Creates a new minimally specified HTTP request.
094             *
095             * @param method The HTTP request method. Must not be {@code null}.
096             * @param url    The HTTP request URL. Must not be {@code null}.
097             */
098            public HTTPRequest(final Method method, final URL url) {
099            
100                    if (method == null)
101                            throw new IllegalArgumentException("The HTTP method must not be null");
102                    
103                    this.method = method;
104    
105    
106                    if (url == null)
107                            throw new IllegalArgumentException("The HTTP URL must not be null");
108    
109                    this.url = url;
110            }
111            
112            
113            /**
114             * Creates a new HTTP request from the specified HTTP servlet request.
115             *
116             * @param sr The servlet request. Must not be {@code null}.
117             *
118             * @throws IllegalArgumentException The the servlet request method is
119             *                                  not GET or POST, or the content type
120             *                                  header value couldn't be parsed.
121             * @throws IOException              For a POST body that couldn't be 
122             *                                  read due to an I/O exception.
123             */
124            public HTTPRequest(final HttpServletRequest sr)
125                    throws IOException {
126            
127                    method = HTTPRequest.Method.valueOf(sr.getMethod().toUpperCase());
128    
129                    try {
130                            url = new URL(sr.getRequestURL().toString());
131    
132                    } catch (MalformedURLException e) {
133    
134                            throw new IllegalArgumentException("Invalid request URL: " + e.getMessage(), e);
135                    }
136                    
137                    String ct = sr.getContentType();
138                    
139                    try {
140                            setContentType(sr.getContentType());
141                    
142                    } catch (ParseException e) {
143                            
144                            throw new IllegalArgumentException("Invalid Content-Type header value: " + e.getMessage(), e);
145                    }
146                    
147                    setAuthorization(sr.getHeader("Authorization"));
148                    
149                    if (method.equals(Method.GET)) {
150                    
151                            setQuery(sr.getQueryString());
152    
153                    } else if (method.equals(Method.POST)) {
154                    
155                            // read body
156                            
157                            StringBuilder body = new StringBuilder(256);
158                            
159                            BufferedReader reader = sr.getReader();
160                            
161                            String line = null;
162                            
163                            while ((line = reader.readLine()) != null) {
164                            
165                                    body.append(line);
166                                    body.append(System.getProperty("line.separator"));
167                            }
168                            
169                            reader.close();
170                            
171                            setQuery(body.toString());
172                    }
173            }
174            
175            
176            /**
177             * Gets the request method.
178             *
179             * @return The request method.
180             */
181            public Method getMethod() {
182            
183                    return method;
184            }
185    
186    
187            /**
188             * Gets the request URL.
189             *
190             * @return The request URL.
191             */
192            public URL getURL() {
193    
194                    return url;
195            }
196            
197            
198            /**
199             * Ensures this HTTP request has the specified method.
200             *
201             * @param expectedMethod The expected method. Must not be {@code null}.
202             *
203             * @throws ParseException If the method doesn't match the expected.
204             */
205            public void ensureMethod(final Method expectedMethod)
206                    throws ParseException {
207                    
208                    if (method != expectedMethod)
209                            throw new ParseException("The HTTP request method must be " + expectedMethod);
210            }
211            
212            
213            /**
214             * Gets the {@code Authorization} header value.
215             *
216             * @return The {@code Authorization} header value, {@code null} if not 
217             *         specified.
218             */
219            public String getAuthorization() {
220            
221                    return authorization;
222            }
223            
224            
225            /**
226             * Sets the {@code Authorization} header value.
227             *
228             * @param authz The {@code Authorization} header value, {@code null} if 
229             *              not specified.
230             */
231            public void setAuthorization(final String authz) {
232            
233                    authorization = authz;
234            }
235            
236            
237            /**
238             * Gets the raw (undecoded) query string if the request is HTTP GET or
239             * the entity body if the request is HTTP POST.
240             *
241             * <p>Note that the '?' character preceding the query string in GET
242             * requests is not included in the returned string.
243             *
244             * <p>Example query string (line breaks for clarity):
245             *
246             * <pre>
247             * response_type=code
248             * &amp;client_id=s6BhdRkqt3
249             * &amp;state=xyz
250             * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
251             * </pre>
252             *
253             * @return For HTTP GET requests the URL query string, for HTTP POST 
254             *         requests the body. {@code null} if not specified.
255             */
256            public String getQuery() {
257            
258                    return query;
259            }
260            
261            
262            /**
263             * Sets the raw (undecoded) query string if the request is HTTP GET or
264             * the entity body if the request is HTTP POST.
265             *
266             * <p>Note that the '?' character preceding the query string in GET
267             * requests must not be included.
268             *
269             * <p>Example query string (line breaks for clarity):
270             *
271             * <pre>
272             * response_type=code
273             * &amp;client_id=s6BhdRkqt3
274             * &amp;state=xyz
275             * &amp;redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
276             * </pre>
277             *
278             * @param query For HTTP GET requests the URL query string, for HTTP 
279             *              POST requests the body. {@code null} if not specified.
280             */
281            public void setQuery(final String query) {
282            
283                    this.query = query;
284            }
285    
286    
287            /**
288             * Ensures this HTTP response has a specified query string or entity
289             * body.
290             *
291             * @throws ParseException If the query string or entity body is missing
292             *                        or empty.
293             */
294            private void ensureQuery()
295                    throws ParseException {
296                    
297                    if (query == null || query.isEmpty())
298                            throw new ParseException("Missing or empty HTTP query string / entity body");
299            }
300            
301            
302            /**
303             * Gets the request query as a parameter map. The parameters are 
304             * decoded according to {@code application/x-www-form-urlencoded}.
305             *
306             * @return The request query parameters, decoded. If none the map will
307             *         be empty.
308             */
309            public Map<String,String> getQueryParameters() {
310            
311                    return URLUtils.parseParameters(query);
312            }
313    
314    
315            /**
316             * Gets the request query or entity body as a JSON Object.
317             *
318             * @return The request query or entity body as a JSON object.
319             *
320             * @throws ParseException If the Content-Type header isn't 
321             *                        {@code application/json}, the request query
322             *                        or entity body is {@code null}, empty or 
323             *                        couldn't be parsed to a valid JSON object.
324             */
325            public JSONObject getQueryAsJSONObject()
326                    throws ParseException {
327    
328                    ensureContentType(CommonContentTypes.APPLICATION_JSON);
329    
330                    ensureQuery();
331    
332                    return JSONObjectUtils.parseJSONObject(query);
333            }
334    
335    
336            /**
337             * Processes this HTTP request by making a connection to the request
338             * URL and retrieving the resulting HTTP response.
339             *
340             * @return The resulting HTTP response.
341             *
342             * @throws IOException If the HTTP request couldn't be processed, due 
343             *                     to a network or other error.
344             */
345            public HTTPResponse process()
346                    throws IOException {
347    
348                    URL finalURL;
349    
350                    if (method.equals(HTTPRequest.Method.GET) && query != null) {
351    
352                            // Append query string
353    
354                            try {
355                                    finalURL = new URL(url, "?" + query);
356    
357                            } catch (MalformedURLException e) {
358    
359                                    throw new IOException("Couldn't append query string: " + e.getMessage(), e);
360                            }
361    
362                    } else {
363    
364                            finalURL = url;
365                    }
366    
367                    HttpURLConnection conn = (HttpURLConnection)finalURL.openConnection();
368    
369                    if (authorization != null)
370                            conn.setRequestProperty("Authorization", authorization);
371    
372                    if (method.equals(HTTPRequest.Method.POST)) {
373    
374                            conn.setDoOutput(true);
375                            
376                            conn.setRequestProperty("Content-Type", CommonContentTypes.APPLICATION_URLENCODED.toString());
377    
378                            if (query != null) {
379    
380                                    OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
381                                    writer.write(query);
382                                    writer.flush(); 
383                            }
384                    }
385    
386    
387                    BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
388           
389                    StringBuilder body = new StringBuilder();
390    
391                    String line = null;
392                            
393                    while ((line = reader.readLine()) != null) {
394                            
395                            body.append(line);
396                            body.append(System.getProperty("line.separator"));
397                    }
398                            
399                    reader.close();
400    
401    
402                    HTTPResponse response = new HTTPResponse(conn.getResponseCode());
403    
404                    String location = conn.getHeaderField("Location");
405    
406                    if (location != null) {
407    
408                            try {
409                                    response.setLocation(new URL(location));
410    
411                            } catch (MalformedURLException e) {
412    
413                                    throw new IOException("Couldn't parse Location header: " + e.getMessage(), e);
414                            }
415                            
416                    }
417    
418    
419                    try {
420                            response.setContentType(conn.getContentType());
421    
422                    } catch (ParseException e) {
423    
424                            throw new IOException("Couldn't parse Content-Type header: " + e.getMessage(), e);
425                    }
426    
427    
428                    response.setCacheControl(conn.getHeaderField("Cache-Control"));
429    
430                    response.setPragma(conn.getHeaderField("Pragma"));
431    
432                    response.setWWWAuthenticate(conn.getHeaderField("WWW-Authenticate"));
433    
434                    String bodyContent = body.toString();
435    
436                    if (! bodyContent.isEmpty())
437                            response.setContent(bodyContent);
438    
439    
440                    return response;
441            }
442    }