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