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 method = HTTPRequest.Method.valueOf(sr.getMethod().toUpperCase()); 215 216 String urlString = reconstructRequestURLString(sr); 217 218 try { 219 url = new URL(urlString); 220 221 } catch (MalformedURLException e) { 222 223 throw new IllegalArgumentException("Invalid request URL: " + e.getMessage() + ": " + urlString, e); 224 } 225 226 try { 227 setContentType(sr.getContentType()); 228 229 } catch (ParseException e) { 230 231 throw new IllegalArgumentException("Invalid Content-Type header value: " + e.getMessage(), e); 232 } 233 234 setAuthorization(sr.getHeader("Authorization")); 235 setAccept(sr.getHeader("Accept")); 236 237 if (method.equals(Method.GET) || method.equals(Method.DELETE)) { 238 239 setQuery(sr.getQueryString()); 240 241 } else if (method.equals(Method.POST) || method.equals(Method.PUT)) { 242 243 // read body 244 StringBuilder body = new StringBuilder(256); 245 246 BufferedReader reader = sr.getReader(); 247 248 String line; 249 250 boolean firstLine = true; 251 252 while ((line = reader.readLine()) != null) { 253 254 if (firstLine) 255 firstLine = false; 256 else 257 body.append(System.getProperty("line.separator")); 258 body.append(line); 259 } 260 261 reader.close(); 262 263 setQuery(body.toString()); 264 } 265 } 266 267 268 /** 269 * Gets the request method. 270 * 271 * @return The request method. 272 */ 273 public Method getMethod() { 274 275 return method; 276 } 277 278 279 /** 280 * Gets the request URL. 281 * 282 * @return The request URL. 283 */ 284 public URL getURL() { 285 286 return url; 287 } 288 289 290 /** 291 * Ensures this HTTP request has the specified method. 292 * 293 * @param expectedMethod The expected method. Must not be {@code null}. 294 * 295 * @throws ParseException If the method doesn't match the expected. 296 */ 297 public void ensureMethod(final Method expectedMethod) 298 throws ParseException { 299 300 if (method != expectedMethod) 301 throw new ParseException("The HTTP request method must be " + expectedMethod); 302 } 303 304 305 /** 306 * Gets the {@code Authorization} header value. 307 * 308 * @return The {@code Authorization} header value, {@code null} if not 309 * specified. 310 */ 311 public String getAuthorization() { 312 313 return authorization; 314 } 315 316 317 /** 318 * Sets the {@code Authorization} header value. 319 * 320 * @param authz The {@code Authorization} header value, {@code null} if 321 * not specified. 322 */ 323 public void setAuthorization(final String authz) { 324 325 authorization = authz; 326 } 327 328 329 /** 330 * Gets the {@code Accept} header value. 331 * 332 * @return The {@code Accept} header value, {@code null} if not 333 * specified. 334 */ 335 public String getAccept() { 336 337 return accept; 338 } 339 340 341 /** 342 * Sets the {@code Accept} header value. 343 * 344 * @param accept The {@code Accept} header value, {@code null} if not 345 * specified. 346 */ 347 public void setAccept(final String accept) { 348 349 this.accept = accept; 350 } 351 352 353 /** 354 * Gets the raw (undecoded) query string if the request is HTTP GET or 355 * the entity body if the request is HTTP POST. 356 * 357 * <p>Note that the '?' character preceding the query string in GET 358 * requests is not included in the returned string. 359 * 360 * <p>Example query string (line breaks for clarity): 361 * 362 * <pre> 363 * response_type=code 364 * &client_id=s6BhdRkqt3 365 * &state=xyz 366 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 367 * </pre> 368 * 369 * @return For HTTP GET requests the URL query string, for HTTP POST 370 * requests the body. {@code null} if not specified. 371 */ 372 public String getQuery() { 373 374 return query; 375 } 376 377 378 /** 379 * Sets the raw (undecoded) query string if the request is HTTP GET or 380 * the entity body if the request is HTTP POST. 381 * 382 * <p>Note that the '?' character preceding the query string in GET 383 * requests must not be included. 384 * 385 * <p>Example query string (line breaks for clarity): 386 * 387 * <pre> 388 * response_type=code 389 * &client_id=s6BhdRkqt3 390 * &state=xyz 391 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 392 * </pre> 393 * 394 * @param query For HTTP GET requests the URL query string, for HTTP 395 * POST requests the body. {@code null} if not specified. 396 */ 397 public void setQuery(final String query) { 398 399 this.query = query; 400 } 401 402 403 /** 404 * Ensures this HTTP response has a specified query string or entity 405 * body. 406 * 407 * @throws ParseException If the query string or entity body is missing 408 * or empty. 409 */ 410 private void ensureQuery() 411 throws ParseException { 412 413 if (query == null || query.trim().isEmpty()) 414 throw new ParseException("Missing or empty HTTP query string / entity body"); 415 } 416 417 418 /** 419 * Gets the request query as a parameter map. The parameters are 420 * decoded according to {@code application/x-www-form-urlencoded}. 421 * 422 * @return The request query parameters, decoded. If none the map will 423 * be empty. 424 */ 425 public Map<String,String> getQueryParameters() { 426 427 return URLUtils.parseParameters(query); 428 } 429 430 431 /** 432 * Gets the request query or entity body as a JSON Object. 433 * 434 * @return The request query or entity body as a JSON object. 435 * 436 * @throws ParseException If the Content-Type header isn't 437 * {@code application/json}, the request query 438 * or entity body is {@code null}, empty or 439 * couldn't be parsed to a valid JSON object. 440 */ 441 public JSONObject getQueryAsJSONObject() 442 throws ParseException { 443 444 ensureContentType(CommonContentTypes.APPLICATION_JSON); 445 446 ensureQuery(); 447 448 return JSONObjectUtils.parseJSONObject(query); 449 } 450 451 452 /** 453 * Gets the HTTP connect timeout. 454 * 455 * @return The HTTP connect read timeout, in milliseconds. Zero implies 456 * no timeout. 457 */ 458 public int getConnectTimeout() { 459 460 return connectTimeout; 461 } 462 463 464 /** 465 * Sets the HTTP connect timeout. 466 * 467 * @param connectTimeout The HTTP connect timeout, in milliseconds. 468 * Zero implies no timeout. Must not be negative. 469 */ 470 public void setConnectTimeout(final int connectTimeout) { 471 472 if (connectTimeout < 0) { 473 throw new IllegalArgumentException("The HTTP connect timeout must be zero or positive"); 474 } 475 476 this.connectTimeout = connectTimeout; 477 } 478 479 480 /** 481 * Gets the HTTP response read timeout. 482 * 483 * @return The HTTP response read timeout, in milliseconds. Zero 484 * implies no timeout. 485 */ 486 public int getReadTimeout() { 487 488 return readTimeout; 489 } 490 491 492 /** 493 * Sets the HTTP response read timeout. 494 * 495 * @param readTimeout The HTTP response read timeout, in milliseconds. 496 * Zero implies no timeout. Must not be negative. 497 */ 498 public void setReadTimeout(final int readTimeout) { 499 500 if (readTimeout < 0) { 501 throw new IllegalArgumentException("The HTTP response read timeout must be zero or positive"); 502 } 503 504 this.readTimeout = readTimeout; 505 } 506 507 508 /** 509 * Returns an established HTTP URL connection for this HTTP request. 510 * 511 * @return The HTTP URL connection, with the request sent and ready to 512 * read the response. 513 * 514 * @throws IOException If the HTTP request couldn't be made, due to a 515 * network or other error. 516 */ 517 public HttpURLConnection toHttpURLConnection() 518 throws IOException { 519 520 URL finalURL = url; 521 522 if (query != null && (method.equals(HTTPRequest.Method.GET) || method.equals(Method.DELETE))) { 523 524 // Append query string 525 StringBuilder sb = new StringBuilder(url.toString()); 526 sb.append('?'); 527 sb.append(query); 528 529 try { 530 finalURL = new URL(sb.toString()); 531 532 } catch (MalformedURLException e) { 533 534 throw new IOException("Couldn't append query string: " + e.getMessage(), e); 535 } 536 } 537 538 HttpURLConnection conn = (HttpURLConnection)finalURL.openConnection(); 539 540 if (authorization != null) 541 conn.setRequestProperty("Authorization", authorization); 542 543 if (accept != null) 544 conn.setRequestProperty("Accept", accept); 545 546 conn.setRequestMethod(method.name()); 547 conn.setConnectTimeout(connectTimeout); 548 conn.setReadTimeout(readTimeout); 549 550 if (method.equals(HTTPRequest.Method.POST) || method.equals(Method.PUT)) { 551 552 conn.setDoOutput(true); 553 554 if (getContentType() != null) 555 conn.setRequestProperty("Content-Type", getContentType().toString()); 556 557 if (query != null) { 558 OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream()); 559 writer.write(query); 560 writer.close(); 561 } 562 } 563 564 return conn; 565 } 566 567 568 /** 569 * Sends this HTTP request to the request URL and retrieves the 570 * resulting HTTP response. 571 * 572 * @return The resulting HTTP response. 573 * 574 * @throws IOException If the HTTP request couldn't be made, due to a 575 * network or other error. 576 */ 577 public HTTPResponse send() 578 throws IOException { 579 580 HttpURLConnection conn = toHttpURLConnection(); 581 582 int statusCode; 583 584 BufferedReader reader; 585 586 try { 587 // Open a connection, then send method and headers 588 reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 589 590 // The next step is to get the status 591 statusCode = conn.getResponseCode(); 592 593 } catch (IOException e) { 594 595 // HttpUrlConnection will throw an IOException if any 596 // 4XX response is sent. If we request the status 597 // again, this time the internal status will be 598 // properly set, and we'll be able to retrieve it. 599 statusCode = conn.getResponseCode(); 600 601 if (statusCode == -1) { 602 // Rethrow IO exception 603 throw e; 604 } else { 605 // HTTP status code indicates the response got 606 // through, read the content but using error stream 607 InputStream errStream = conn.getErrorStream(); 608 609 if (errStream != null) { 610 // We have useful HTTP error body 611 reader = new BufferedReader(new InputStreamReader(errStream)); 612 } else { 613 // No content, set to empty string 614 reader = new BufferedReader(new StringReader("")); 615 } 616 } 617 } 618 619 StringBuilder body = new StringBuilder(); 620 621 try { 622 String line; 623 624 while ((line = reader.readLine()) != null) { 625 body.append(line); 626 body.append(System.getProperty("line.separator")); 627 } 628 629 reader.close(); 630 631 } finally { 632 conn.disconnect(); 633 } 634 635 636 HTTPResponse response = new HTTPResponse(statusCode); 637 638 String location = conn.getHeaderField("Location"); 639 640 if (location != null) { 641 642 try { 643 response.setLocation(new URI(location)); 644 645 } catch (URISyntaxException e) { 646 throw new IOException("Couldn't parse Location header: " + e.getMessage(), e); 647 } 648 } 649 650 651 try { 652 response.setContentType(conn.getContentType()); 653 654 } catch (ParseException e) { 655 656 throw new IOException("Couldn't parse Content-Type header: " + e.getMessage(), e); 657 } 658 659 660 response.setCacheControl(conn.getHeaderField("Cache-Control")); 661 662 response.setPragma(conn.getHeaderField("Pragma")); 663 664 response.setWWWAuthenticate(conn.getHeaderField("WWW-Authenticate")); 665 666 String bodyContent = body.toString(); 667 668 if (! bodyContent.isEmpty()) 669 response.setContent(bodyContent); 670 671 672 return response; 673 } 674}