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