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