001package com.nimbusds.oauth2.sdk.http; 002 003 004import java.io.*; 005import java.net.*; 006import java.util.List; 007import java.util.Map; 008 009import javax.net.ssl.HostnameVerifier; 010import javax.net.ssl.HttpsURLConnection; 011import javax.net.ssl.SSLSocketFactory; 012 013import net.jcip.annotations.ThreadSafe; 014 015import net.minidev.json.JSONObject; 016 017import com.nimbusds.oauth2.sdk.ParseException; 018import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 019import com.nimbusds.oauth2.sdk.util.URLUtils; 020 021 022/** 023 * HTTP request with support for the parameters required to construct an 024 * {@link com.nimbusds.oauth2.sdk.Request OAuth 2.0 request message}. 025 * 026 * <p>Supported HTTP methods: 027 * 028 * <ul> 029 * <li>{@link Method#GET HTTP GET} 030 * <li>{@link Method#POST HTTP POST} 031 * <li>{@link Method#POST HTTP PUT} 032 * <li>{@link Method#POST HTTP DELETE} 033 * </ul> 034 * 035 * <p>Supported request headers: 036 * 037 * <ul> 038 * <li>Content-Type 039 * <li>Authorization 040 * <li>Accept 041 * <li>Etc. 042 * </ul> 043 * 044 * <p>Supported timeouts: 045 * 046 * <ul> 047 * <li>On HTTP connect 048 * <li>On HTTP response read 049 * </ul> 050 * 051 * <p>HTTP 3xx redirection: follow (default) / don't follow 052 */ 053@ThreadSafe 054public class HTTPRequest extends HTTPMessage { 055 056 057 /** 058 * Enumeration of the HTTP methods used in OAuth 2.0 requests. 059 */ 060 public enum Method { 061 062 /** 063 * HTTP GET. 064 */ 065 GET, 066 067 068 /** 069 * HTTP POST. 070 */ 071 POST, 072 073 074 /** 075 * HTTP PUT. 076 */ 077 PUT, 078 079 080 /** 081 * HTTP DELETE. 082 */ 083 DELETE 084 } 085 086 087 /** 088 * The request method. 089 */ 090 private final Method method; 091 092 093 /** 094 * The request URL. 095 */ 096 private final URL url; 097 098 099 /** 100 * The query string / post body. 101 */ 102 private String query = null; 103 104 105 /** 106 * The fragment. 107 */ 108 private String fragment = 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 * Controls HTTP 3xx redirections. 126 */ 127 private boolean followRedirects = true; 128 129 130 /** 131 * The default hostname verifier for all HTTPS requests. 132 */ 133 private static HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); 134 135 136 /** 137 * The default socket factory for all HTTPS requests. 138 */ 139 private static SSLSocketFactory defaultSSLSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); 140 141 142 /** 143 * Creates a new minimally specified HTTP request. 144 * 145 * @param method The HTTP request method. Must not be {@code null}. 146 * @param url The HTTP request URL. Must not be {@code null}. 147 */ 148 public HTTPRequest(final Method method, final URL url) { 149 150 if (method == null) 151 throw new IllegalArgumentException("The HTTP method must not be null"); 152 153 this.method = method; 154 155 156 if (url == null) 157 throw new IllegalArgumentException("The HTTP URL must not be null"); 158 159 this.url = url; 160 } 161 162 163 /** 164 * Gets the request method. 165 * 166 * @return The request method. 167 */ 168 public Method getMethod() { 169 170 return method; 171 } 172 173 174 /** 175 * Gets the request URL. 176 * 177 * @return The request URL. 178 */ 179 public URL getURL() { 180 181 return url; 182 } 183 184 185 /** 186 * Ensures this HTTP request has the specified method. 187 * 188 * @param expectedMethod The expected method. Must not be {@code null}. 189 * 190 * @throws ParseException If the method doesn't match the expected. 191 */ 192 public void ensureMethod(final Method expectedMethod) 193 throws ParseException { 194 195 if (method != expectedMethod) 196 throw new ParseException("The HTTP request method must be " + expectedMethod); 197 } 198 199 200 /** 201 * Gets the {@code Authorization} header value. 202 * 203 * @return The {@code Authorization} header value, {@code null} if not 204 * specified. 205 */ 206 public String getAuthorization() { 207 208 return getHeader("Authorization"); 209 } 210 211 212 /** 213 * Sets the {@code Authorization} header value. 214 * 215 * @param authz The {@code Authorization} header value, {@code null} if 216 * not specified. 217 */ 218 public void setAuthorization(final String authz) { 219 220 setHeader("Authorization", authz); 221 } 222 223 224 /** 225 * Gets the {@code Accept} header value. 226 * 227 * @return The {@code Accept} header value, {@code null} if not 228 * specified. 229 */ 230 public String getAccept() { 231 232 return getHeader("Accept"); 233 } 234 235 236 /** 237 * Sets the {@code Accept} header value. 238 * 239 * @param accept The {@code Accept} header value, {@code null} if not 240 * specified. 241 */ 242 public void setAccept(final String accept) { 243 244 setHeader("Accept", accept); 245 } 246 247 248 /** 249 * Gets the raw (undecoded) query string if the request is HTTP GET or 250 * the entity body if the request is HTTP POST. 251 * 252 * <p>Note that the '?' character preceding the query string in GET 253 * requests is not included in the returned string. 254 * 255 * <p>Example query string (line breaks for clarity): 256 * 257 * <pre> 258 * response_type=code 259 * &client_id=s6BhdRkqt3 260 * &state=xyz 261 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 262 * </pre> 263 * 264 * @return For HTTP GET requests the URL query string, for HTTP POST 265 * requests the body. {@code null} if not specified. 266 */ 267 public String getQuery() { 268 269 return query; 270 } 271 272 273 /** 274 * Sets the raw (undecoded) query string if the request is HTTP GET or 275 * the entity body if the request is HTTP POST. 276 * 277 * <p>Note that the '?' character preceding the query string in GET 278 * requests must not be included. 279 * 280 * <p>Example query string (line breaks for clarity): 281 * 282 * <pre> 283 * response_type=code 284 * &client_id=s6BhdRkqt3 285 * &state=xyz 286 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 287 * </pre> 288 * 289 * @param query For HTTP GET requests the URL query string, for HTTP 290 * POST requests the body. {@code null} if not specified. 291 */ 292 public void setQuery(final String query) { 293 294 this.query = query; 295 } 296 297 298 /** 299 * Ensures this HTTP response has a specified query string or entity 300 * body. 301 * 302 * @throws ParseException If the query string or entity body is missing 303 * or empty. 304 */ 305 private void ensureQuery() 306 throws ParseException { 307 308 if (query == null || query.trim().isEmpty()) 309 throw new ParseException("Missing or empty HTTP query string / entity body"); 310 } 311 312 313 /** 314 * Gets the request query as a parameter map. The parameters are 315 * decoded according to {@code application/x-www-form-urlencoded}. 316 * 317 * @return The request query parameters, decoded. If none the map will 318 * be empty. 319 */ 320 public Map<String,String> getQueryParameters() { 321 322 return URLUtils.parseParameters(query); 323 } 324 325 326 /** 327 * Gets the request query or entity body as a JSON Object. 328 * 329 * @return The request query or entity body as a JSON object. 330 * 331 * @throws ParseException If the Content-Type header isn't 332 * {@code application/json}, the request query 333 * or entity body is {@code null}, empty or 334 * couldn't be parsed to a valid JSON object. 335 */ 336 public JSONObject getQueryAsJSONObject() 337 throws ParseException { 338 339 ensureContentType(CommonContentTypes.APPLICATION_JSON); 340 341 ensureQuery(); 342 343 return JSONObjectUtils.parse(query); 344 } 345 346 347 /** 348 * Gets the raw (undecoded) request fragment. 349 * 350 * @return The request fragment, {@code null} if not specified. 351 */ 352 public String getFragment() { 353 354 return fragment; 355 } 356 357 358 /** 359 * Sets the raw (undecoded) request fragment. 360 * 361 * @param fragment The request fragment, {@code null} if not specified. 362 */ 363 public void setFragment(final String fragment) { 364 365 this.fragment = fragment; 366 } 367 368 369 /** 370 * Gets the HTTP connect timeout. 371 * 372 * @return The HTTP connect read timeout, in milliseconds. Zero implies 373 * no timeout. 374 */ 375 public int getConnectTimeout() { 376 377 return connectTimeout; 378 } 379 380 381 /** 382 * Sets the HTTP connect timeout. 383 * 384 * @param connectTimeout The HTTP connect timeout, in milliseconds. 385 * Zero implies no timeout. Must not be negative. 386 */ 387 public void setConnectTimeout(final int connectTimeout) { 388 389 if (connectTimeout < 0) { 390 throw new IllegalArgumentException("The HTTP connect timeout must be zero or positive"); 391 } 392 393 this.connectTimeout = connectTimeout; 394 } 395 396 397 /** 398 * Gets the HTTP response read timeout. 399 * 400 * @return The HTTP response read timeout, in milliseconds. Zero 401 * implies no timeout. 402 */ 403 public int getReadTimeout() { 404 405 return readTimeout; 406 } 407 408 409 /** 410 * Sets the HTTP response read timeout. 411 * 412 * @param readTimeout The HTTP response read timeout, in milliseconds. 413 * Zero implies no timeout. Must not be negative. 414 */ 415 public void setReadTimeout(final int readTimeout) { 416 417 if (readTimeout < 0) { 418 throw new IllegalArgumentException("The HTTP response read timeout must be zero or positive"); 419 } 420 421 this.readTimeout = readTimeout; 422 } 423 424 425 /** 426 * Gets the boolean setting whether HTTP redirects (requests with 427 * response code 3xx) should be automatically followed. 428 * 429 * @return {@code true} if HTTP redirects are automatically followed, 430 * else {@code false}. 431 */ 432 public boolean getFollowRedirects() { 433 434 return followRedirects; 435 } 436 437 438 /** 439 * Sets whether HTTP redirects (requests with response code 3xx) should 440 * be automatically followed. 441 * 442 * @param follow Whether or not to follow HTTP redirects. 443 */ 444 public void setFollowRedirects(final boolean follow) { 445 446 followRedirects = follow; 447 } 448 449 450 /** 451 * Returns the default hostname verifier for all HTTPS requests. 452 * 453 * @return The hostname verifier. 454 */ 455 public static HostnameVerifier getDefaultHostnameVerifier() { 456 457 return defaultHostnameVerifier; 458 } 459 460 461 /** 462 * Sets the default hostname verifier for all HTTPS requests. May be 463 * overridden on a individual request basis. 464 * 465 * @param defaultHostnameVerifier The hostname verifier. Must not be 466 * {@code null}. 467 */ 468 public static void setDefaultHostnameVerifier(final HostnameVerifier defaultHostnameVerifier) { 469 470 if (defaultHostnameVerifier == null) { 471 throw new IllegalArgumentException("The hostname verifier must not be null"); 472 } 473 474 HTTPRequest.defaultHostnameVerifier = defaultHostnameVerifier; 475 } 476 477 478 /** 479 * Returns the default SSL socket factory for all HTTPS requests. 480 * 481 * @return The SSL socket factory. 482 */ 483 public static SSLSocketFactory getDefaultSSLSocketFactory() { 484 485 return defaultSSLSocketFactory; 486 } 487 488 489 /** 490 * Sets the default SSL socket factory for all HTTPS requests. May be 491 * overridden on a individual request basis. 492 * 493 * @param sslSocketFactory The SSL socket factory. Must not be 494 * {@code null}. 495 */ 496 public static void setDefaultSSLSocketFactory(final SSLSocketFactory sslSocketFactory) { 497 498 if (sslSocketFactory == null) { 499 throw new IllegalArgumentException("The SSL socket factory must not be null"); 500 } 501 502 HTTPRequest.defaultSSLSocketFactory = sslSocketFactory; 503 } 504 505 506 /** 507 * Returns an established HTTP URL connection for this HTTP request. 508 * 509 * @return The HTTP URL connection, with the request sent and ready to 510 * read the response. 511 * 512 * @throws IOException If the HTTP request couldn't be made, due to a 513 * network or other error. 514 */ 515 public HttpURLConnection toHttpURLConnection() 516 throws IOException { 517 518 return toHttpURLConnection(null, null); 519 } 520 521 522 /** 523 * Returns an established HTTP URL connection for this HTTP request. 524 * 525 * @param hostnameVerifier The hostname verifier for HTTPS requests. 526 * Disregarded for plain HTTP requests. If 527 * {@code null} the 528 * {@link #getDefaultHostnameVerifier() default 529 * hostname verifier} will apply. 530 * @param sslSocketFactory The SSL socket factory for HTTPS requests. 531 * Disregarded for plain HTTP requests. If 532 * {@code null} the 533 * {@link #getDefaultSSLSocketFactory() default 534 * SSL socket factory} will apply. 535 * 536 * @return The HTTP URL connection, with the request sent and ready to 537 * read the response. 538 * 539 * @throws IOException If the HTTP request couldn't be made, due to a 540 * network or other error. 541 */ 542 public HttpURLConnection toHttpURLConnection(final HostnameVerifier hostnameVerifier, 543 final SSLSocketFactory sslSocketFactory) 544 throws IOException { 545 546 URL finalURL = url; 547 548 if (query != null && (method.equals(HTTPRequest.Method.GET) || method.equals(Method.DELETE))) { 549 550 // Append query string 551 StringBuilder sb = new StringBuilder(url.toString()); 552 sb.append('?'); 553 sb.append(query); 554 555 try { 556 finalURL = new URL(sb.toString()); 557 558 } catch (MalformedURLException e) { 559 560 throw new IOException("Couldn't append query string: " + e.getMessage(), e); 561 } 562 } 563 564 if (fragment != null) { 565 566 // Append raw fragment 567 StringBuilder sb = new StringBuilder(finalURL.toString()); 568 sb.append('#'); 569 sb.append(fragment); 570 571 try { 572 finalURL = new URL(sb.toString()); 573 574 } catch (MalformedURLException e) { 575 576 throw new IOException("Couldn't append raw fragment: " + e.getMessage(), e); 577 } 578 } 579 580 HttpURLConnection conn = (HttpURLConnection)finalURL.openConnection(); 581 582 if (conn instanceof HttpsURLConnection) { 583 HttpsURLConnection sslConn = (HttpsURLConnection)conn; 584 sslConn.setHostnameVerifier(hostnameVerifier != null ? hostnameVerifier : getDefaultHostnameVerifier()); 585 sslConn.setSSLSocketFactory(sslSocketFactory != null ? sslSocketFactory : getDefaultSSLSocketFactory()); 586 } 587 588 for (Map.Entry<String,String> header: getHeaders().entrySet()) { 589 conn.setRequestProperty(header.getKey(), header.getValue()); 590 } 591 592 conn.setRequestMethod(method.name()); 593 conn.setConnectTimeout(connectTimeout); 594 conn.setReadTimeout(readTimeout); 595 conn.setInstanceFollowRedirects(followRedirects); 596 597 if (method.equals(HTTPRequest.Method.POST) || method.equals(Method.PUT)) { 598 599 conn.setDoOutput(true); 600 601 if (getContentType() != null) 602 conn.setRequestProperty("Content-Type", getContentType().toString()); 603 604 if (query != null) { 605 try { 606 OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream()); 607 writer.write(query); 608 writer.close(); 609 } catch (IOException e) { 610 closeStreams(conn); 611 throw e; // Rethrow 612 } 613 } 614 } 615 616 return conn; 617 } 618 619 620 /** 621 * Sends this HTTP request to the request URL and retrieves the 622 * resulting HTTP response. 623 * 624 * @return The resulting HTTP response. 625 * 626 * @throws IOException If the HTTP request couldn't be made, due to a 627 * network or other error. 628 */ 629 public HTTPResponse send() 630 throws IOException { 631 632 return send(null, null); 633 } 634 635 636 /** 637 * Sends this HTTP request to the request URL and retrieves the 638 * resulting HTTP response. 639 * 640 * @param hostnameVerifier The hostname verifier for HTTPS requests. 641 * Disregarded for plain HTTP requests. If 642 * {@code null} the 643 * {@link #getDefaultHostnameVerifier() default 644 * hostname verifier} will apply. 645 * @param sslSocketFactory The SSL socket factory for HTTPS requests. 646 * Disregarded for plain HTTP requests. If 647 * {@code null} the 648 * {@link #getDefaultSSLSocketFactory() default 649 * SSL socket factory} will apply. 650 * 651 * @return The resulting HTTP response. 652 * 653 * @throws IOException If the HTTP request couldn't be made, due to a 654 * network or other error. 655 */ 656 public HTTPResponse send(final HostnameVerifier hostnameVerifier, 657 final SSLSocketFactory sslSocketFactory) 658 throws IOException { 659 660 HttpURLConnection conn = toHttpURLConnection(hostnameVerifier, sslSocketFactory); 661 662 int statusCode; 663 664 BufferedReader reader; 665 666 try { 667 // Open a connection, then send method and headers 668 reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 669 670 // The next step is to get the status 671 statusCode = conn.getResponseCode(); 672 673 } catch (IOException e) { 674 675 // HttpUrlConnection will throw an IOException if any 676 // 4XX response is sent. If we request the status 677 // again, this time the internal status will be 678 // properly set, and we'll be able to retrieve it. 679 statusCode = conn.getResponseCode(); 680 681 if (statusCode == -1) { 682 throw e; // Rethrow IO exception 683 } else { 684 // HTTP status code indicates the response got 685 // through, read the content but using error stream 686 InputStream errStream = conn.getErrorStream(); 687 688 if (errStream != null) { 689 // We have useful HTTP error body 690 reader = new BufferedReader(new InputStreamReader(errStream)); 691 } else { 692 // No content, set to empty string 693 reader = new BufferedReader(new StringReader("")); 694 } 695 } 696 } 697 698 StringBuilder body = new StringBuilder(); 699 String line; 700 while ((line = reader.readLine()) != null) { 701 body.append(line); 702 body.append(System.getProperty("line.separator")); 703 } 704 reader.close(); 705 706 707 HTTPResponse response = new HTTPResponse(statusCode); 708 709 // Set headers 710 for (Map.Entry<String,List<String>> responseHeader: conn.getHeaderFields().entrySet()) { 711 712 if (responseHeader.getKey() == null) { 713 continue; // skip header 714 } 715 716 List<String> values = responseHeader.getValue(); 717 if (values == null || values.isEmpty() || values.get(0) == null) { 718 continue; // skip header 719 } 720 721 response.setHeader(responseHeader.getKey(), values.get(0)); 722 } 723 724 closeStreams(conn); 725 726 final String bodyContent = body.toString(); 727 if (! bodyContent.isEmpty()) 728 response.setContent(bodyContent); 729 730 return response; 731 } 732 733 734 /** 735 * Closes the input, output and error streams of the specified HTTP URL 736 * connection. No attempt is made to close the underlying socket with 737 * {@code conn.disconnect} so it may be cached (HTTP 1.1 keep live). 738 * See http://techblog.bozho.net/caveats-of-httpurlconnection/ 739 * 740 * @param conn The HTTP URL connection. May be {@code null}. 741 */ 742 private static void closeStreams(final HttpURLConnection conn) { 743 744 if (conn == null) { 745 return; 746 } 747 748 try { 749 if (conn.getInputStream() != null) { 750 conn.getInputStream().close(); 751 } 752 } catch (Exception e) { 753 // ignore 754 } 755 756 try { 757 if (conn.getOutputStream() != null) { 758 conn.getOutputStream().close(); 759 } 760 } catch (Exception e) { 761 // ignore 762 } 763 764 try { 765 if (conn.getErrorStream() != null) { 766 conn.getOutputStream().close(); 767 } 768 } catch (Exception e) { 769 // ignore 770 } 771 } 772}