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