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