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