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