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