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