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