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}. 027 * 028 * <p>Supported HTTP methods: 029 * 030 * <ul> 031 * <li>{@link Method#GET HTTP GET} 032 * <li>{@link Method#POST HTTP POST} 033 * <li>{@link Method#POST HTTP PUT} 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@ThreadSafe 045public class HTTPRequest extends HTTPMessage { 046 047 048 /** 049 * Enumeration of the HTTP methods used in OAuth 2.0 requests. 050 */ 051 public static enum Method { 052 053 /** 054 * HTTP GET. 055 */ 056 GET, 057 058 059 /** 060 * HTTP POST. 061 */ 062 POST, 063 064 065 /** 066 * HTTP PUT. 067 */ 068 PUT, 069 070 071 /** 072 * HTTP DELETE. 073 */ 074 DELETE 075 } 076 077 078 /** 079 * The request method. 080 */ 081 private final Method method; 082 083 084 /** 085 * The request URL. 086 */ 087 private final URL url; 088 089 090 /** 091 * Specifies an {@code Authorization} header value. 092 */ 093 private String authorization = null; 094 095 096 /** 097 * The query string / post body. 098 */ 099 private String query = null; 100 101 102 /** 103 * Creates a new minimally specified HTTP request. 104 * 105 * @param method The HTTP request method. Must not be {@code null}. 106 * @param url The HTTP request URL. Must not be {@code null}. 107 */ 108 public HTTPRequest(final Method method, final URL url) { 109 110 if (method == null) 111 throw new IllegalArgumentException("The HTTP method must not be null"); 112 113 this.method = method; 114 115 116 if (url == null) 117 throw new IllegalArgumentException("The HTTP URL must not be null"); 118 119 this.url = url; 120 } 121 122 123 /** 124 * Reconstructs the request URL string for the specified servlet 125 * request. The host part is always the local IP address. The query 126 * string and fragment is always omitted. 127 * 128 * @param request The servlet request. Must not be {@code null}. 129 * 130 * @return The reconstructed request URL string. 131 */ 132 private static String reconstructRequestURLString(final HttpServletRequest request) { 133 134 StringBuilder sb = new StringBuilder("http"); 135 136 if (request.isSecure()) 137 sb.append('s'); 138 139 sb.append("://"); 140 141 String localAddress = request.getLocalAddr(); 142 143 if (localAddress.contains(".")) { 144 // IPv3 address 145 sb.append(localAddress); 146 } else if (localAddress.contains(":")) { 147 // IPv6 address, see RFC 2732 148 sb.append('['); 149 sb.append(localAddress); 150 sb.append(']'); 151 } else { 152 // Don't know what to do 153 } 154 155 if (! request.isSecure() && request.getLocalPort() != 80) { 156 // HTTP plain at port other than 80 157 sb.append(':'); 158 sb.append(request.getLocalPort()); 159 } 160 161 if (request.isSecure() && request.getLocalPort() != 443) { 162 // HTTPS at port other than 443 (default TLS) 163 sb.append(':'); 164 sb.append(request.getLocalPort()); 165 } 166 167 String path = request.getRequestURI(); 168 169 if (path != null) 170 sb.append(path); 171 172 return sb.toString(); 173 } 174 175 176 /** 177 * Creates a new HTTP request from the specified HTTP servlet request. 178 * 179 * @param sr The servlet request. Must not be {@code null}. 180 * 181 * @throws IllegalArgumentException The the servlet request method is 182 * not GET, POST, PUT or DELETE or the 183 * content type header value couldn't 184 * be parsed. 185 * @throws IOException For a POST or PUT body that 186 * couldn't be read due to an I/O 187 * exception. 188 */ 189 public HTTPRequest(final HttpServletRequest sr) 190 throws IOException { 191 192 method = HTTPRequest.Method.valueOf(sr.getMethod().toUpperCase()); 193 194 String urlString = reconstructRequestURLString(sr); 195 196 try { 197 url = new URL(urlString); 198 199 } catch (MalformedURLException e) { 200 201 throw new IllegalArgumentException("Invalid request URL: " + e.getMessage() + ": " + urlString, e); 202 } 203 204 try { 205 setContentType(sr.getContentType()); 206 207 } catch (ParseException e) { 208 209 throw new IllegalArgumentException("Invalid Content-Type header value: " + e.getMessage(), e); 210 } 211 212 setAuthorization(sr.getHeader("Authorization")); 213 214 if (method.equals(Method.GET) || method.equals(Method.DELETE)) { 215 216 setQuery(sr.getQueryString()); 217 218 } else if (method.equals(Method.POST) || method.equals(Method.PUT)) { 219 220 // read body 221 StringBuilder body = new StringBuilder(256); 222 223 BufferedReader reader = sr.getReader(); 224 225 String line; 226 227 while ((line = reader.readLine()) != null) { 228 229 body.append(line); 230 body.append(System.getProperty("line.separator")); 231 } 232 233 reader.close(); 234 235 setQuery(body.toString()); 236 } 237 } 238 239 240 /** 241 * Gets the request method. 242 * 243 * @return The request method. 244 */ 245 public Method getMethod() { 246 247 return method; 248 } 249 250 251 /** 252 * Gets the request URL. 253 * 254 * @return The request URL. 255 */ 256 public URL getURL() { 257 258 return url; 259 } 260 261 262 /** 263 * Ensures this HTTP request has the specified method. 264 * 265 * @param expectedMethod The expected method. Must not be {@code null}. 266 * 267 * @throws ParseException If the method doesn't match the expected. 268 */ 269 public void ensureMethod(final Method expectedMethod) 270 throws ParseException { 271 272 if (method != expectedMethod) 273 throw new ParseException("The HTTP request method must be " + expectedMethod); 274 } 275 276 277 /** 278 * Gets the {@code Authorization} header value. 279 * 280 * @return The {@code Authorization} header value, {@code null} if not 281 * specified. 282 */ 283 public String getAuthorization() { 284 285 return authorization; 286 } 287 288 289 /** 290 * Sets the {@code Authorization} header value. 291 * 292 * @param authz The {@code Authorization} header value, {@code null} if 293 * not specified. 294 */ 295 public void setAuthorization(final String authz) { 296 297 authorization = authz; 298 } 299 300 301 /** 302 * Gets the raw (undecoded) query string if the request is HTTP GET or 303 * the entity body if the request is HTTP POST. 304 * 305 * <p>Note that the '?' character preceding the query string in GET 306 * requests is not included in the returned string. 307 * 308 * <p>Example query string (line breaks for clarity): 309 * 310 * <pre> 311 * response_type=code 312 * &client_id=s6BhdRkqt3 313 * &state=xyz 314 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 315 * </pre> 316 * 317 * @return For HTTP GET requests the URL query string, for HTTP POST 318 * requests the body. {@code null} if not specified. 319 */ 320 public String getQuery() { 321 322 return query; 323 } 324 325 326 /** 327 * Sets the raw (undecoded) query string if the request is HTTP GET or 328 * the entity body if the request is HTTP POST. 329 * 330 * <p>Note that the '?' character preceding the query string in GET 331 * requests must not be included. 332 * 333 * <p>Example query string (line breaks for clarity): 334 * 335 * <pre> 336 * response_type=code 337 * &client_id=s6BhdRkqt3 338 * &state=xyz 339 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 340 * </pre> 341 * 342 * @param query For HTTP GET requests the URL query string, for HTTP 343 * POST requests the body. {@code null} if not specified. 344 */ 345 public void setQuery(final String query) { 346 347 this.query = query; 348 } 349 350 351 /** 352 * Ensures this HTTP response has a specified query string or entity 353 * body. 354 * 355 * @throws ParseException If the query string or entity body is missing 356 * or empty. 357 */ 358 private void ensureQuery() 359 throws ParseException { 360 361 if (query == null || query.isEmpty()) 362 throw new ParseException("Missing or empty HTTP query string / entity body"); 363 } 364 365 366 /** 367 * Gets the request query as a parameter map. The parameters are 368 * decoded according to {@code application/x-www-form-urlencoded}. 369 * 370 * @return The request query parameters, decoded. If none the map will 371 * be empty. 372 */ 373 public Map<String,String> getQueryParameters() { 374 375 return URLUtils.parseParameters(query); 376 } 377 378 379 /** 380 * Gets the request query or entity body as a JSON Object. 381 * 382 * @return The request query or entity body as a JSON object. 383 * 384 * @throws ParseException If the Content-Type header isn't 385 * {@code application/json}, the request query 386 * or entity body is {@code null}, empty or 387 * couldn't be parsed to a valid JSON object. 388 */ 389 public JSONObject getQueryAsJSONObject() 390 throws ParseException { 391 392 ensureContentType(CommonContentTypes.APPLICATION_JSON); 393 394 ensureQuery(); 395 396 return JSONObjectUtils.parseJSONObject(query); 397 } 398 399 400 /** 401 * Returns an established HTTP URL connection for this HTTP request. 402 * 403 * @return The HTTP URL connection, with the request sent and ready to 404 * read the response. 405 * 406 * @throws IOException If the HTTP request couldn't be made, due to a 407 * network or other error. 408 */ 409 public HttpURLConnection toHttpURLConnection() 410 throws IOException { 411 412 URL finalURL = url; 413 414 if (query != null && (method.equals(HTTPRequest.Method.GET) || method.equals(Method.DELETE))) { 415 416 // Append query string 417 StringBuilder sb = new StringBuilder(url.toString()); 418 sb.append('?'); 419 sb.append(query); 420 421 try { 422 finalURL = new URL(sb.toString()); 423 424 } catch (MalformedURLException e) { 425 426 throw new IOException("Couldn't append query string: " + e.getMessage(), e); 427 } 428 } 429 430 HttpURLConnection conn = (HttpURLConnection)finalURL.openConnection(); 431 432 if (authorization != null) 433 conn.setRequestProperty("Authorization", authorization); 434 435 conn.setRequestMethod(method.name()); 436 437 if (method.equals(HTTPRequest.Method.POST) || method.equals(Method.PUT)) { 438 439 conn.setDoOutput(true); 440 441 if (getContentType() != null) 442 conn.setRequestProperty("Content-Type", getContentType().toString()); 443 444 if (query != null) { 445 OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream()); 446 writer.write(query); 447 writer.close(); 448 } 449 } 450 451 return conn; 452 } 453 454 455 /** 456 * Sends this HTTP request to the request URL and retrieves the 457 * resulting HTTP response. 458 * 459 * @return The resulting HTTP response. 460 * 461 * @throws IOException If the HTTP request couldn't be made, due to a 462 * network or other error. 463 */ 464 public HTTPResponse send() 465 throws IOException { 466 467 HttpURLConnection conn = toHttpURLConnection(); 468 469 int statusCode; 470 471 BufferedReader reader = null; 472 473 try { 474 // Open a connection, then send method and headers 475 reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 476 477 // The step is to get the status 478 statusCode = conn.getResponseCode(); 479 480 } catch (IOException e) { 481 482 // HttpUrlConnection will throw an IOException if any 4XX 483 // response is sent. If we request the status again, this 484 // time the internal status will be properly set, and we'll be 485 // able to retrieve it. 486 statusCode = conn.getResponseCode(); 487 488 if (statusCode / 100 != 4) { 489 // Rethrow IO exception 490 throw e; 491 } 492 } 493 494 StringBuilder body = new StringBuilder(); 495 496 if (reader != null) { 497 498 try { 499 String line; 500 501 while ((line = reader.readLine()) != null) { 502 503 body.append(line); 504 body.append(System.getProperty("line.separator")); 505 } 506 507 reader.close(); 508 509 } finally { 510 conn.disconnect(); 511 } 512 } 513 514 HTTPResponse response = new HTTPResponse(statusCode); 515 516 String location = conn.getHeaderField("Location"); 517 518 if (location != null) { 519 520 try { 521 response.setLocation(new URL(location)); 522 523 } catch (MalformedURLException e) { 524 525 throw new IOException("Couldn't parse Location header: " + e.getMessage(), e); 526 } 527 } 528 529 530 try { 531 response.setContentType(conn.getContentType()); 532 533 } catch (ParseException e) { 534 535 throw new IOException("Couldn't parse Content-Type header: " + e.getMessage(), e); 536 } 537 538 539 response.setCacheControl(conn.getHeaderField("Cache-Control")); 540 541 response.setPragma(conn.getHeaderField("Pragma")); 542 543 response.setWWWAuthenticate(conn.getHeaderField("WWW-Authenticate")); 544 545 String bodyContent = body.toString(); 546 547 if (! bodyContent.isEmpty()) 548 response.setContent(bodyContent); 549 550 551 return response; 552 } 553}