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}. 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
048public 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}