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}. This 027 * class is thread-safe. 028 * 029 * <p>Supported HTTP methods: 030 * 031 * <ul> 032 * <li>{@link Method#GET HTTP GET} 033 * <li>{@link Method#POST HTTP POST} 034 * <li>{@link Method#POST HTTP PUT} 035 * <li>{@link Method#POST HTTP DELETE} 036 * </ul> 037 * 038 * <p>Supported request headers: 039 * 040 * <ul> 041 * <li>Content-Type 042 * <li>Authorization 043 * </ul> 044 * 045 * @author Vladimir Dzhuvinov 046 */ 047@ThreadSafe 048public final class HTTPRequest extends HTTPMessage { 049 050 051 /** 052 * Enumeration of the HTTP methods used in OAuth 2.0 requests. 053 */ 054 public static enum Method { 055 056 /** 057 * HTTP GET. 058 */ 059 GET, 060 061 062 /** 063 * HTTP POST. 064 */ 065 POST, 066 067 068 /** 069 * HTTP PUT. 070 */ 071 PUT, 072 073 074 /** 075 * HTTP DELETE. 076 */ 077 DELETE 078 } 079 080 081 /** 082 * The request method. 083 */ 084 private final Method method; 085 086 087 /** 088 * The request URL. 089 */ 090 private final URL url; 091 092 093 /** 094 * Specifies an {@code Authorization} header value. 095 */ 096 private String authorization = null; 097 098 099 /** 100 * The query string / post body. 101 */ 102 private String query = null; 103 104 105 /** 106 * Creates a new minimally specified HTTP request. 107 * 108 * @param method The HTTP request method. Must not be {@code null}. 109 * @param url The HTTP request URL. Must not be {@code null}. 110 */ 111 public HTTPRequest(final Method method, final URL url) { 112 113 if (method == null) 114 throw new IllegalArgumentException("The HTTP method must not be null"); 115 116 this.method = method; 117 118 119 if (url == null) 120 throw new IllegalArgumentException("The HTTP URL must not be null"); 121 122 this.url = url; 123 } 124 125 126 /** 127 * Creates a new HTTP request from the specified HTTP servlet request. 128 * 129 * @param sr The servlet request. Must not be {@code null}. 130 * 131 * @throws IllegalArgumentException The the servlet request method is 132 * not GET or POST, or the content type 133 * header value couldn't be parsed. 134 * @throws IOException For a POST body that couldn't be 135 * read due to an I/O exception. 136 */ 137 public HTTPRequest(final HttpServletRequest sr) 138 throws IOException { 139 140 method = HTTPRequest.Method.valueOf(sr.getMethod().toUpperCase()); 141 142 try { 143 url = new URL(sr.getRequestURL().toString()); 144 145 } catch (MalformedURLException e) { 146 147 throw new IllegalArgumentException("Invalid request URL: " + e.getMessage(), e); 148 } 149 150 String ct = sr.getContentType(); 151 152 try { 153 setContentType(sr.getContentType()); 154 155 } catch (ParseException e) { 156 157 throw new IllegalArgumentException("Invalid Content-Type header value: " + e.getMessage(), e); 158 } 159 160 setAuthorization(sr.getHeader("Authorization")); 161 162 if (method.equals(Method.GET)) { 163 164 setQuery(sr.getQueryString()); 165 166 } else if (method.equals(Method.POST)) { 167 168 // read body 169 170 StringBuilder body = new StringBuilder(256); 171 172 BufferedReader reader = sr.getReader(); 173 174 String line; 175 176 while ((line = reader.readLine()) != null) { 177 178 body.append(line); 179 body.append(System.getProperty("line.separator")); 180 } 181 182 reader.close(); 183 184 setQuery(body.toString()); 185 } 186 } 187 188 189 /** 190 * Gets the request method. 191 * 192 * @return The request method. 193 */ 194 public Method getMethod() { 195 196 return method; 197 } 198 199 200 /** 201 * Gets the request URL. 202 * 203 * @return The request URL. 204 */ 205 public URL getURL() { 206 207 return url; 208 } 209 210 211 /** 212 * Ensures this HTTP request has the specified method. 213 * 214 * @param expectedMethod The expected method. Must not be {@code null}. 215 * 216 * @throws ParseException If the method doesn't match the expected. 217 */ 218 public void ensureMethod(final Method expectedMethod) 219 throws ParseException { 220 221 if (method != expectedMethod) 222 throw new ParseException("The HTTP request method must be " + expectedMethod); 223 } 224 225 226 /** 227 * Gets the {@code Authorization} header value. 228 * 229 * @return The {@code Authorization} header value, {@code null} if not 230 * specified. 231 */ 232 public String getAuthorization() { 233 234 return authorization; 235 } 236 237 238 /** 239 * Sets the {@code Authorization} header value. 240 * 241 * @param authz The {@code Authorization} header value, {@code null} if 242 * not specified. 243 */ 244 public void setAuthorization(final String authz) { 245 246 authorization = authz; 247 } 248 249 250 /** 251 * Gets the raw (undecoded) query string if the request is HTTP GET or 252 * the entity body if the request is HTTP POST. 253 * 254 * <p>Note that the '?' character preceding the query string in GET 255 * requests is not included in the returned string. 256 * 257 * <p>Example query string (line breaks for clarity): 258 * 259 * <pre> 260 * response_type=code 261 * &client_id=s6BhdRkqt3 262 * &state=xyz 263 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 264 * </pre> 265 * 266 * @return For HTTP GET requests the URL query string, for HTTP POST 267 * requests the body. {@code null} if not specified. 268 */ 269 public String getQuery() { 270 271 return query; 272 } 273 274 275 /** 276 * Sets the raw (undecoded) query string if the request is HTTP GET or 277 * the entity body if the request is HTTP POST. 278 * 279 * <p>Note that the '?' character preceding the query string in GET 280 * requests must not be included. 281 * 282 * <p>Example query string (line breaks for clarity): 283 * 284 * <pre> 285 * response_type=code 286 * &client_id=s6BhdRkqt3 287 * &state=xyz 288 * &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb 289 * </pre> 290 * 291 * @param query For HTTP GET requests the URL query string, for HTTP 292 * POST requests the body. {@code null} if not specified. 293 */ 294 public void setQuery(final String query) { 295 296 this.query = query; 297 } 298 299 300 /** 301 * Ensures this HTTP response has a specified query string or entity 302 * body. 303 * 304 * @throws ParseException If the query string or entity body is missing 305 * or empty. 306 */ 307 private void ensureQuery() 308 throws ParseException { 309 310 if (query == null || query.isEmpty()) 311 throw new ParseException("Missing or empty HTTP query string / entity body"); 312 } 313 314 315 /** 316 * Gets the request query as a parameter map. The parameters are 317 * decoded according to {@code application/x-www-form-urlencoded}. 318 * 319 * @return The request query parameters, decoded. If none the map will 320 * be empty. 321 */ 322 public Map<String,String> getQueryParameters() { 323 324 return URLUtils.parseParameters(query); 325 } 326 327 328 /** 329 * Gets the request query or entity body as a JSON Object. 330 * 331 * @return The request query or entity body as a JSON object. 332 * 333 * @throws ParseException If the Content-Type header isn't 334 * {@code application/json}, the request query 335 * or entity body is {@code null}, empty or 336 * couldn't be parsed to a valid JSON object. 337 */ 338 public JSONObject getQueryAsJSONObject() 339 throws ParseException { 340 341 ensureContentType(CommonContentTypes.APPLICATION_JSON); 342 343 ensureQuery(); 344 345 return JSONObjectUtils.parseJSONObject(query); 346 } 347 348 349 /** 350 * Sends this HTTP request to the request URL and retrieves the 351 * resulting HTTP response. 352 * 353 * @return The resulting HTTP response. 354 * 355 * @throws IOException If the HTTP request couldn't be made, due to a 356 * network or other error. 357 */ 358 public HTTPResponse send() 359 throws IOException { 360 361 URL finalURL; 362 363 if (method.equals(HTTPRequest.Method.GET) && query != null) { 364 365 // Append query string 366 367 try { 368 finalURL = new URL(url.toString() + "?" + query); 369 370 } catch (MalformedURLException e) { 371 372 throw new IOException("Couldn't append query string: " + e.getMessage(), e); 373 } 374 375 } else { 376 377 finalURL = url; 378 } 379 380 HttpURLConnection conn = (HttpURLConnection)finalURL.openConnection(); 381 382 if (authorization != null) 383 conn.setRequestProperty("Authorization", authorization); 384 385 if (method.equals(HTTPRequest.Method.POST)) { 386 387 conn.setDoOutput(true); 388 389 conn.setRequestProperty("Content-Type", CommonContentTypes.APPLICATION_URLENCODED.toString()); 390 391 if (query != null) { 392 393 OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream()); 394 writer.write(query); 395 writer.flush(); 396 } 397 } 398 399 400 BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 401 402 StringBuilder body = new StringBuilder(); 403 404 String line; 405 406 while ((line = reader.readLine()) != null) { 407 408 body.append(line); 409 body.append(System.getProperty("line.separator")); 410 } 411 412 reader.close(); 413 414 415 HTTPResponse response = new HTTPResponse(conn.getResponseCode()); 416 417 String location = conn.getHeaderField("Location"); 418 419 if (location != null) { 420 421 try { 422 response.setLocation(new URL(location)); 423 424 } catch (MalformedURLException e) { 425 426 throw new IOException("Couldn't parse Location header: " + e.getMessage(), e); 427 } 428 429 } 430 431 432 try { 433 response.setContentType(conn.getContentType()); 434 435 } catch (ParseException e) { 436 437 throw new IOException("Couldn't parse Content-Type header: " + e.getMessage(), e); 438 } 439 440 441 response.setCacheControl(conn.getHeaderField("Cache-Control")); 442 443 response.setPragma(conn.getHeaderField("Pragma")); 444 445 response.setWWWAuthenticate(conn.getHeaderField("WWW-Authenticate")); 446 447 String bodyContent = body.toString(); 448 449 if (! bodyContent.isEmpty()) 450 response.setContent(bodyContent); 451 452 453 return response; 454 } 455}