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