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 * &client_id=s6BhdRkqt3 248 * &state=xyz 249 * &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 * &client_id=s6BhdRkqt3 273 * &state=xyz 274 * &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 }