001package com.nimbusds.oauth2.sdk.http; 002 003 004import java.io.*; 005import java.net.*; 006import java.util.Map; 007 008import javax.servlet.http.HttpServletRequest; 009 010import net.jcip.annotations.ThreadSafe; 011 012import net.minidev.json.JSONObject; 013 014import com.nimbusds.oauth2.sdk.ParseException; 015import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 016import com.nimbusds.oauth2.sdk.util.URLUtils; 017 018 019/** 020 * HTTP request with support for the parameters required to construct an 021 * {@link com.nimbusds.oauth2.sdk.Request OAuth 2.0 request message}. 022 * 023 * <p>Supported HTTP methods: 024 * 025 * <ul> 026 * <li>{@link Method#GET HTTP GET} 027 * <li>{@link Method#POST HTTP POST} 028 * <li>{@link Method#POST HTTP PUT} 029 * <li>{@link Method#POST HTTP DELETE} 030 * </ul> 031 * 032 * <p>Supported request headers: 033 * 034 * <ul> 035 * <li>Content-Type 036 * <li>Authorization 037 * </ul> 038 * 039 * <p>Supported timeouts: 040 * 041 * <ul> 042 * <li>On HTTP connect 043 * <li>On HTTP response read 044 * </ul> 045 */ 046@ThreadSafe 047public 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 PUT. 069 */ 070 PUT, 071 072 073 /** 074 * HTTP DELETE. 075 */ 076 DELETE 077 } 078 079 080 /** 081 * The request method. 082 */ 083 private final Method method; 084 085 086 /** 087 * The request URL. 088 */ 089 private final URL url; 090 091 092 /** 093 * Specifies an {@code Authorization} header value. 094 */ 095 private String authorization = null; 096 097 098 /** 099 * The query string / post body. 100 */ 101 private String query = null; 102 103 104 /** 105 * The HTTP connect timeout, in milliseconds. Zero implies none. 106 */ 107 private int connectTimeout = 0; 108 109 110 /** 111 * The HTTP response read timeout, in milliseconds. Zero implies none. 112 113 */ 114 private int readTimeout = 0; 115 116 117 /** 118 * Creates a new minimally specified HTTP request. 119 * 120 * @param method The HTTP request method. Must not be {@code null}. 121 * @param url The HTTP request URL. Must not be {@code null}. 122 */ 123 public HTTPRequest(final Method method, final URL url) { 124 125 if (method == null) 126 throw new IllegalArgumentException("The HTTP method must not be null"); 127 128 this.method = method; 129 130 131 if (url == null) 132 throw new IllegalArgumentException("The HTTP URL must not be null"); 133 134 this.url = url; 135 } 136 137 138 /** 139 * Reconstructs the request URL string for the specified servlet 140 * request. The host part is always the local IP address. The query 141 * string and fragment is always omitted. 142 * 143 * @param request The servlet request. Must not be {@code null}. 144 * 145 * @return The reconstructed request URL string. 146 */ 147 private static String reconstructRequestURLString(final HttpServletRequest request) { 148 149 StringBuilder sb = new StringBuilder("http"); 150 151 if (request.isSecure()) 152 sb.append('s'); 153 154 sb.append("://"); 155 156 String localAddress = request.getLocalAddr(); 157 158 if (localAddress.contains(".")) { 159 // IPv3 address 160 sb.append(localAddress); 161 } else if (localAddress.contains(":")) { 162 // IPv6 address, see RFC 2732 163 sb.append('['); 164 sb.append(localAddress); 165 sb.append(']'); 166 } else { 167 // Don't know what to do 168 } 169 170 if (! request.isSecure() && request.getLocalPort() != 80) { 171 // HTTP plain at port other than 80 172 sb.append(':'); 173 sb.append(request.getLocalPort()); 174 } 175 176 if (request.isSecure() && request.getLocalPort() != 443) { 177 // HTTPS at port other than 443 (default TLS) 178 sb.append(':'); 179 sb.append(request.getLocalPort()); 180 } 181 182 String path = request.getRequestURI(); 183 184 if (path != null) 185 sb.append(path); 186 187 return sb.toString(); 188 } 189 190 191 /** 192 * Creates a new HTTP request from the specified HTTP servlet request. 193 * 194 * @param sr The servlet request. Must not be {@code null}. 195 * 196 * @throws IllegalArgumentException The the servlet request method is 197 * not GET, POST, PUT or DELETE or the 198 * content type header value couldn't 199 * be parsed. 200 * @throws IOException For a POST or PUT body that 201 * couldn't be read due to an I/O 202 * exception. 203 */ 204 public HTTPRequest(final HttpServletRequest sr) 205 throws IOException { 206 207 method = HTTPRequest.Method.valueOf(sr.getMethod().toUpperCase()); 208 209 String urlString = reconstructRequestURLString(sr); 210 211 try { 212 url = new URL(urlString); 213 214 } catch (MalformedURLException e) { 215 216 throw new IllegalArgumentException("Invalid request URL: " + e.getMessage() + ": " + urlString, e); 217 } 218 219 try { 220 setContentType(sr.getContentType()); 221 222 } catch (ParseException e) { 223 224 throw new IllegalArgumentException("Invalid Content-Type header value: " + e.getMessage(), e); 225 } 226 227 setAuthorization(sr.getHeader("Authorization")); 228 229 if (method.equals(Method.GET) || method.equals(Method.DELETE)) { 230 231 setQuery(sr.getQueryString()); 232 233 } else if (method.equals(Method.POST) || method.equals(Method.PUT)) { 234 235 // read body 236 StringBuilder body = new StringBuilder(256); 237 238 BufferedReader reader = sr.getReader(); 239 240 String line; 241 242 boolean firstLine = true; 243 244 while ((line = reader.readLine()) != null) { 245 246 if (firstLine) 247 firstLine = false; 248 else 249 body.append(System.getProperty("line.separator")); 250 body.append(line); 251 } 252 253 reader.close(); 254 255 setQuery(body.toString()); 256 } 257 } 258 259 260 /** 261 * Gets the request method. 262 * 263 * @return The request method. 264 */ 265 public Method getMethod() { 266 267 return method; 268 } 269 270 271 /** 272 * Gets the request URL. 273 * 274 * @return The request URL. 275 */ 276 public URL getURL() { 277 278 return url; 279 } 280 281 282 /** 283 * Ensures this HTTP request has the specified method. 284 * 285 * @param expectedMethod The expected method. Must not be {@code null}. 286 * 287 * @throws ParseException If the method doesn't match the expected. 288 */ 289 public void ensureMethod(final Method expectedMethod) 290 throws ParseException { 291 292 if (method != expectedMethod) 293 throw new ParseException("The HTTP request method must be " + expectedMethod); 294 } 295 296 297 /** 298 * Gets the {@code Authorization} header value. 299 * 300 * @return The {@code Authorization} header value, {@code null} if not 301 * specified. 302 */ 303 public String getAuthorization() { 304 305 return authorization; 306 } 307 308 309 /** 310 * Sets the {@code Authorization} header value. 311 * 312 * @param authz The {@code Authorization} header value, {@code null} if 313 * not specified. 314 */ 315 public void setAuthorization(final String authz) { 316 317 authorization = authz; 318 } 319 320 321 /** 322 * Gets the raw (undecoded) query string if the request is HTTP GET or 323 * the entity body if the request is HTTP POST. 324 * 325 * <p>Note that the '?' character preceding the query string in GET 326 * requests is not included in the returned string. 327 * 328 * <p>Example query string (line breaks for clarity): 329 * 330 * <pre> 331 * response_type=code 332 * &client_id=s6BhdRkqt3 333 * &state=xyz 334 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 335 * </pre> 336 * 337 * @return For HTTP GET requests the URL query string, for HTTP POST 338 * requests the body. {@code null} if not specified. 339 */ 340 public String getQuery() { 341 342 return query; 343 } 344 345 346 /** 347 * Sets the raw (undecoded) query string if the request is HTTP GET or 348 * the entity body if the request is HTTP POST. 349 * 350 * <p>Note that the '?' character preceding the query string in GET 351 * requests must not be included. 352 * 353 * <p>Example query string (line breaks for clarity): 354 * 355 * <pre> 356 * response_type=code 357 * &client_id=s6BhdRkqt3 358 * &state=xyz 359 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 360 * </pre> 361 * 362 * @param query For HTTP GET requests the URL query string, for HTTP 363 * POST requests the body. {@code null} if not specified. 364 */ 365 public void setQuery(final String query) { 366 367 this.query = query; 368 } 369 370 371 /** 372 * Ensures this HTTP response has a specified query string or entity 373 * body. 374 * 375 * @throws ParseException If the query string or entity body is missing 376 * or empty. 377 */ 378 private void ensureQuery() 379 throws ParseException { 380 381 if (query == null || query.trim().isEmpty()) 382 throw new ParseException("Missing or empty HTTP query string / entity body"); 383 } 384 385 386 /** 387 * Gets the request query as a parameter map. The parameters are 388 * decoded according to {@code application/x-www-form-urlencoded}. 389 * 390 * @return The request query parameters, decoded. If none the map will 391 * be empty. 392 */ 393 public Map<String,String> getQueryParameters() { 394 395 return URLUtils.parseParameters(query); 396 } 397 398 399 /** 400 * Gets the request query or entity body as a JSON Object. 401 * 402 * @return The request query or entity body as a JSON object. 403 * 404 * @throws ParseException If the Content-Type header isn't 405 * {@code application/json}, the request query 406 * or entity body is {@code null}, empty or 407 * couldn't be parsed to a valid JSON object. 408 */ 409 public JSONObject getQueryAsJSONObject() 410 throws ParseException { 411 412 ensureContentType(CommonContentTypes.APPLICATION_JSON); 413 414 ensureQuery(); 415 416 return JSONObjectUtils.parseJSONObject(query); 417 } 418 419 420 /** 421 * Gets the HTTP connect timeout. 422 * 423 * @return The HTTP connect read timeout, in milliseconds. Zero implies 424 * no timeout. 425 */ 426 public int getConnectTimeout() { 427 428 return connectTimeout; 429 } 430 431 432 /** 433 * Sets the HTTP connect timeout. 434 * 435 * @param connectTimeout The HTTP connect timeout, in milliseconds. 436 * Zero implies no timeout. Must not be negative. 437 */ 438 public void setConnectTimeout(final int connectTimeout) { 439 440 if (connectTimeout < 0) { 441 throw new IllegalArgumentException("The HTTP connect timeout must be zero or positive"); 442 } 443 444 this.connectTimeout = connectTimeout; 445 } 446 447 448 /** 449 * Gets the HTTP response read timeout. 450 * 451 * @return The HTTP response read timeout, in milliseconds. Zero 452 * implies no timeout. 453 */ 454 public int getReadTimeout() { 455 456 return readTimeout; 457 } 458 459 460 /** 461 * Sets the HTTP response read timeout. 462 * 463 * @param readTimeout The HTTP response read timeout, in milliseconds. 464 * Zero implies no timeout. Must not be negative. 465 */ 466 public void setReadTimeout(final int readTimeout) { 467 468 if (readTimeout < 0) { 469 throw new IllegalArgumentException("The HTTP response read timeout must be zero or positive"); 470 } 471 472 this.readTimeout = readTimeout; 473 } 474 475 476 /** 477 * Returns an established HTTP URL connection for this HTTP request. 478 * 479 * @return The HTTP URL connection, with the request sent and ready to 480 * read the response. 481 * 482 * @throws IOException If the HTTP request couldn't be made, due to a 483 * network or other error. 484 */ 485 public HttpURLConnection toHttpURLConnection() 486 throws IOException { 487 488 URL finalURL = url; 489 490 if (query != null && (method.equals(HTTPRequest.Method.GET) || method.equals(Method.DELETE))) { 491 492 // Append query string 493 StringBuilder sb = new StringBuilder(url.toString()); 494 sb.append('?'); 495 sb.append(query); 496 497 try { 498 finalURL = new URL(sb.toString()); 499 500 } catch (MalformedURLException e) { 501 502 throw new IOException("Couldn't append query string: " + e.getMessage(), e); 503 } 504 } 505 506 HttpURLConnection conn = (HttpURLConnection)finalURL.openConnection(); 507 508 if (authorization != null) 509 conn.setRequestProperty("Authorization", authorization); 510 511 conn.setRequestMethod(method.name()); 512 conn.setConnectTimeout(connectTimeout); 513 conn.setReadTimeout(readTimeout); 514 515 if (method.equals(HTTPRequest.Method.POST) || method.equals(Method.PUT)) { 516 517 conn.setDoOutput(true); 518 519 if (getContentType() != null) 520 conn.setRequestProperty("Content-Type", getContentType().toString()); 521 522 if (query != null) { 523 OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream()); 524 writer.write(query); 525 writer.close(); 526 } 527 } 528 529 return conn; 530 } 531 532 533 /** 534 * Sends this HTTP request to the request URL and retrieves the 535 * resulting HTTP response. 536 * 537 * @return The resulting HTTP response. 538 * 539 * @throws IOException If the HTTP request couldn't be made, due to a 540 * network or other error. 541 */ 542 public HTTPResponse send() 543 throws IOException { 544 545 HttpURLConnection conn = toHttpURLConnection(); 546 547 int statusCode; 548 549 BufferedReader reader; 550 551 try { 552 // Open a connection, then send method and headers 553 reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 554 555 // The next step is to get the status 556 statusCode = conn.getResponseCode(); 557 558 } catch (IOException e) { 559 560 // HttpUrlConnection will throw an IOException if any 561 // 4XX response is sent. If we request the status 562 // again, this time the internal status will be 563 // properly set, and we'll be able to retrieve it. 564 statusCode = conn.getResponseCode(); 565 566 if (statusCode == -1) { 567 // Rethrow IO exception 568 throw e; 569 } else { 570 // HTTP status code indicates the response got 571 // through, read the content but using error stream 572 InputStream errStream = conn.getErrorStream(); 573 574 if (errStream != null) { 575 // We have useful HTTP error body 576 reader = new BufferedReader(new InputStreamReader(errStream)); 577 } else { 578 // No content, set to empty string 579 reader = new BufferedReader(new StringReader("")); 580 } 581 } 582 } 583 584 StringBuilder body = new StringBuilder(); 585 586 try { 587 String line; 588 589 while ((line = reader.readLine()) != null) { 590 body.append(line); 591 body.append(System.getProperty("line.separator")); 592 } 593 594 reader.close(); 595 596 } finally { 597 conn.disconnect(); 598 } 599 600 601 HTTPResponse response = new HTTPResponse(statusCode); 602 603 String location = conn.getHeaderField("Location"); 604 605 if (location != null) { 606 607 try { 608 response.setLocation(new URI(location)); 609 610 } catch (URISyntaxException e) { 611 throw new IOException("Couldn't parse Location header: " + e.getMessage(), e); 612 } 613 } 614 615 616 try { 617 response.setContentType(conn.getContentType()); 618 619 } catch (ParseException e) { 620 621 throw new IOException("Couldn't parse Content-Type header: " + e.getMessage(), e); 622 } 623 624 625 response.setCacheControl(conn.getHeaderField("Cache-Control")); 626 627 response.setPragma(conn.getHeaderField("Pragma")); 628 629 response.setWWWAuthenticate(conn.getHeaderField("WWW-Authenticate")); 630 631 String bodyContent = body.toString(); 632 633 if (! bodyContent.isEmpty()) 634 response.setContent(bodyContent); 635 636 637 return response; 638 } 639}