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