001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2012-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.json.resource.http; 018 019import static org.forgerock.http.protocol.Responses.newInternalServerError; 020import static org.forgerock.http.routing.Version.version; 021import static org.forgerock.json.resource.ActionRequest.ACTION_ID_CREATE; 022import static org.forgerock.util.Utils.closeSilently; 023import static org.forgerock.util.promise.Promises.newResultPromise; 024 025import java.io.ByteArrayOutputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.OutputStream; 029import java.util.ArrayDeque; 030import java.util.Arrays; 031import java.util.Collection; 032import java.util.Iterator; 033import java.util.LinkedHashMap; 034import java.util.List; 035import java.util.Map; 036import java.util.regex.Matcher; 037import java.util.regex.Pattern; 038import javax.activation.DataSource; 039import javax.mail.BodyPart; 040import javax.mail.MessagingException; 041import javax.mail.internet.ContentDisposition; 042import javax.mail.internet.ContentType; 043import javax.mail.internet.MimeBodyPart; 044import javax.mail.internet.MimeMultipart; 045import javax.mail.internet.ParseException; 046 047import org.forgerock.http.header.AcceptApiVersionHeader; 048import org.forgerock.http.header.ContentTypeHeader; 049import org.forgerock.http.protocol.Response; 050import org.forgerock.http.protocol.Status; 051import org.forgerock.http.routing.Version; 052import org.forgerock.json.JsonValue; 053import org.forgerock.json.resource.ActionRequest; 054import org.forgerock.json.resource.BadRequestException; 055import org.forgerock.json.resource.InternalServerErrorException; 056import org.forgerock.json.resource.NotSupportedException; 057import org.forgerock.json.resource.PatchOperation; 058import org.forgerock.json.resource.PreconditionFailedException; 059import org.forgerock.json.resource.QueryRequest; 060import org.forgerock.json.resource.Request; 061import org.forgerock.json.resource.RequestType; 062import org.forgerock.json.resource.ResourceException; 063import org.forgerock.util.encode.Base64url; 064import org.forgerock.util.promise.NeverThrowsException; 065import org.forgerock.util.promise.Promise; 066 067import com.fasterxml.jackson.core.JsonGenerator; 068import com.fasterxml.jackson.core.JsonParseException; 069import com.fasterxml.jackson.core.JsonParser; 070import com.fasterxml.jackson.databind.JsonMappingException; 071import com.fasterxml.jackson.databind.ObjectMapper; 072 073/** 074 * HTTP utility methods and constants. 075 */ 076public final class HttpUtils { 077 static final String CACHE_CONTROL = "no-cache"; 078 static final String CHARACTER_ENCODING = "UTF-8"; 079 static final Pattern CONTENT_TYPE_REGEX = Pattern.compile( 080 "^application/json([ ]*;[ ]*charset=utf-8)?$", Pattern.CASE_INSENSITIVE); 081 static final String CRLF = "\r\n"; 082 static final String ETAG_ANY = "*"; 083 084 static final String MIME_TYPE_APPLICATION_JSON = "application/json"; 085 static final String MIME_TYPE_MULTIPART_FORM_DATA = "multipart/form-data"; 086 static final String MIME_TYPE_TEXT_PLAIN = "text/plain"; 087 088 static final String HEADER_CACHE_CONTROL = "Cache-Control"; 089 static final String HEADER_ETAG = "ETag"; 090 static final String HEADER_IF_MATCH = "If-Match"; 091 static final String HEADER_IF_NONE_MATCH = "If-None-Match"; 092 static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; 093 static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; 094 static final String HEADER_LOCATION = "Location"; 095 static final String HEADER_X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override"; 096 /** the HTTP header for {@literal Content-Disposition}. */ 097 public static final String CONTENT_DISPOSITION = "Content-Disposition"; 098 static final Collection<String> RESTRICTED_HEADER_NAMES = Arrays.asList( 099 ContentTypeHeader.NAME, 100 AcceptApiVersionHeader.NAME, 101 HEADER_IF_MODIFIED_SINCE, 102 HEADER_IF_UNMODIFIED_SINCE, 103 HEADER_IF_MATCH, 104 HEADER_IF_NONE_MATCH, 105 HEADER_CACHE_CONTROL, 106 HEADER_ETAG, 107 HEADER_LOCATION, 108 HEADER_X_HTTP_METHOD_OVERRIDE, 109 CONTENT_DISPOSITION 110 ); 111 112 static final String METHOD_DELETE = "DELETE"; 113 static final String METHOD_GET = "GET"; 114 static final String METHOD_HEAD = "HEAD"; 115 static final String METHOD_OPTIONS = "OPTIONS"; 116 static final String METHOD_PATCH = "PATCH"; 117 static final String METHOD_POST = "POST"; 118 static final String METHOD_PUT = "PUT"; 119 static final String METHOD_TRACE = "TRACE"; 120 121 /** the HTTP request parameter for an action. */ 122 public static final String PARAM_ACTION = param(ActionRequest.FIELD_ACTION); 123 /** the HTTP request parameter to specify which fields to return. */ 124 public static final String PARAM_FIELDS = param(Request.FIELD_FIELDS); 125 /** the HTTP request parameter to request a certain mimetype for a filed. */ 126 public static final String PARAM_MIME_TYPE = param("mimeType"); 127 /** the HTTP request parameter to request a certain page size. */ 128 public static final String PARAM_PAGE_SIZE = param(QueryRequest.FIELD_PAGE_SIZE); 129 /** the HTTP request parameter to specify a paged results cookie. */ 130 public static final String PARAM_PAGED_RESULTS_COOKIE = 131 param(QueryRequest.FIELD_PAGED_RESULTS_COOKIE); 132 /** the HTTP request parameter to specify a paged results offset. */ 133 public static final String PARAM_PAGED_RESULTS_OFFSET = 134 param(QueryRequest.FIELD_PAGED_RESULTS_OFFSET); 135 /** the HTTP request parameter to request pretty printing. */ 136 public static final String PARAM_PRETTY_PRINT = "_prettyPrint"; 137 /** the HTTP request parameter to specify a query expression. */ 138 public static final String PARAM_QUERY_EXPRESSION = param(QueryRequest.FIELD_QUERY_EXPRESSION); 139 /** the HTTP request parameter to specify a query filter. */ 140 public static final String PARAM_QUERY_FILTER = param(QueryRequest.FIELD_QUERY_FILTER); 141 /** the HTTP request parameter to specify a query id. */ 142 public static final String PARAM_QUERY_ID = param(QueryRequest.FIELD_QUERY_ID); 143 /** the HTTP request parameter to specify the sort keys. */ 144 public static final String PARAM_SORT_KEYS = param(QueryRequest.FIELD_SORT_KEYS); 145 /** The policy used for counting total paged results. */ 146 public static final String PARAM_TOTAL_PAGED_RESULTS_POLICY = param(QueryRequest.FIELD_TOTAL_PAGED_RESULTS_POLICY); 147 148 /** Protocol Version 1. */ 149 public static final Version PROTOCOL_VERSION_1 = version(1); 150 /** Protocol Version 2 - supports upsert on PUT. */ 151 public static final Version PROTOCOL_VERSION_2 = version(2); 152 /** 153 * Protocol Version 2.1 - supports defacto standard for create requests when the ID of the created resource is 154 * to be allocated by the server, which are represented as a POST to the collection endpoint without an 155 * {@code _action} query parameter. 156 */ 157 public static final Version PROTOCOL_VERSION_2_1 = version(2, 1); 158 /** The default version of the named protocol. */ 159 public static final Version DEFAULT_PROTOCOL_VERSION = PROTOCOL_VERSION_2_1; 160 static final String FIELDS_DELIMITER = ","; 161 static final String SORT_KEYS_DELIMITER = ","; 162 163 private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); 164 165 private static final String FILENAME = "filename"; 166 private static final String MIME_TYPE = "mimetype"; 167 private static final String CONTENT = "content"; 168 private static final String NAME = "name"; 169 private static final Pattern MULTIPART_FIELD_REGEX = Pattern.compile("^cid:(.*)#(" + FILENAME 170 + "|" + MIME_TYPE + "|" + CONTENT + ")$", Pattern.CASE_INSENSITIVE); 171 private static final int PART_NAME = 1; 172 private static final int PART_DATA_TYPE = 2; 173 private static final String REFERENCE_TAG = "$ref"; 174 175 private static final int BUFFER_SIZE = 1_024; 176 private static final int EOF = -1; 177 178 /** 179 * Adapts an {@code Exception} to a {@code ResourceException}. 180 * 181 * @param t 182 * The exception which caused the request to fail. 183 * @return The equivalent resource exception. 184 */ 185 static ResourceException adapt(final Throwable t) { 186 if (t instanceof ResourceException) { 187 return (ResourceException) t; 188 } else { 189 return new InternalServerErrorException(t); 190 } 191 } 192 193 /** 194 * Parses a header or request parameter as a boolean value. 195 * 196 * @param name 197 * The name of the header or parameter. 198 * @param values 199 * The header or parameter values. 200 * @return The boolean value. 201 * @throws ResourceException 202 * If the value could not be parsed as a boolean. 203 */ 204 static boolean asBooleanValue(final String name, final List<String> values) 205 throws ResourceException { 206 final String value = asSingleValue(name, values); 207 return Boolean.parseBoolean(value); 208 } 209 210 /** 211 * Parses a header or request parameter as an integer value. 212 * 213 * @param name 214 * The name of the header or parameter. 215 * @param values 216 * The header or parameter values. 217 * @return The integer value. 218 * @throws ResourceException 219 * If the value could not be parsed as a integer. 220 */ 221 static int asIntValue(final String name, final List<String> values) throws ResourceException { 222 final String value = asSingleValue(name, values); 223 try { 224 return Integer.parseInt(value); 225 } catch (final NumberFormatException e) { 226 // FIXME: i18n. 227 throw new BadRequestException("The value \'" + value + "\' for parameter '" + name 228 + "' could not be parsed as a valid integer"); 229 } 230 } 231 232 /** 233 * Parses a header or request parameter as a single string value. 234 * 235 * @param name 236 * The name of the header or parameter. 237 * @param values 238 * The header or parameter values. 239 * @return The single string value. 240 * @throws ResourceException 241 * If the value could not be parsed as a single string. 242 */ 243 static String asSingleValue(final String name, final List<String> values) throws ResourceException { 244 if (values == null || values.isEmpty()) { 245 // FIXME: i18n. 246 throw new BadRequestException("No values provided for the request parameter \'" + name 247 + "\'"); 248 } else if (values.size() > 1) { 249 // FIXME: i18n. 250 throw new BadRequestException( 251 "Multiple values provided for the single-valued request parameter \'" + name 252 + "\'"); 253 } 254 return values.get(0); 255 } 256 257 /** 258 * Safely fail an HTTP request using the provided {@code Exception}. 259 * 260 * @param req 261 * The HTTP request. 262 * @param t 263 * The resource exception indicating why the request failed. 264 */ 265 static Promise<Response, NeverThrowsException> fail(org.forgerock.http.protocol.Request req, final Throwable t) { 266 return fail0(req, null, t); 267 } 268 269 /** 270 * Safely fail an HTTP request using the provided {@code Exception}. 271 * 272 * @param req 273 * The HTTP request. 274 * @param resp 275 * The HTTP response. 276 * @param t 277 * The resource exception indicating why the request failed. 278 */ 279 static Promise<Response, NeverThrowsException> fail(org.forgerock.http.protocol.Request req, 280 org.forgerock.http.protocol.Response resp, final Throwable t) { 281 return fail0(req, resp, t); 282 } 283 284 private static Promise<Response, NeverThrowsException> fail0(org.forgerock.http.protocol.Request req, 285 org.forgerock.http.protocol.Response resp, Throwable t) { 286 final ResourceException re = adapt(t); 287 try { 288 if (resp == null) { 289 resp = prepareResponse(req); 290 } else { 291 resp = prepareResponse(req, resp); 292 } 293 resp.setStatus(Status.valueOf(re.getCode())); 294 final JsonGenerator writer = getJsonGenerator(req, resp); 295 writer.writeObject(re.toJsonValue().getObject()); 296 closeSilently(writer); 297 return newResultPromise(resp); 298 } catch (final IOException ignored) { 299 // Ignore the error since this was probably the cause. 300 return newResultPromise(newInternalServerError()); 301 } 302 } 303 304 /** 305 * Determines which CREST operation (CRUDPAQ) of the incoming request. 306 * 307 * @param request The request. 308 * @return The Operation. 309 * @throws ResourceException If the request operation could not be 310 * determined or is not supported. 311 */ 312 public static RequestType determineRequestType(org.forgerock.http.protocol.Request request) 313 throws ResourceException { 314 // Dispatch the request based on method, taking into account 315 // method override header. 316 final String method = getMethod(request); 317 if (METHOD_DELETE.equals(method)) { 318 return RequestType.DELETE; 319 } else if (METHOD_GET.equals(method)) { 320 if (hasParameter(request, PARAM_QUERY_ID) 321 || hasParameter(request, PARAM_QUERY_EXPRESSION) 322 || hasParameter(request, PARAM_QUERY_FILTER)) { 323 return RequestType.QUERY; 324 } else { 325 return RequestType.READ; 326 } 327 } else if (METHOD_PATCH.equals(method)) { 328 return RequestType.PATCH; 329 } else if (METHOD_POST.equals(method)) { 330 return determinePostRequestType(request); 331 } else if (METHOD_PUT.equals(method)) { 332 return determinePutRequestType(request); 333 } else { 334 // TODO: i18n 335 throw new NotSupportedException("Method " + method + " not supported"); 336 } 337 } 338 339 private static RequestType determinePostRequestType(org.forgerock.http.protocol.Request request) 340 throws ResourceException { 341 List<String> parameter = getParameter(request, PARAM_ACTION); 342 343 boolean defactoCreate = getRequestedProtocolVersion(request).compareTo(PROTOCOL_VERSION_2_1) >= 0 344 && (parameter == null || parameter.isEmpty()); 345 346 return defactoCreate || asSingleValue(PARAM_ACTION, parameter).equalsIgnoreCase(ACTION_ID_CREATE) 347 ? RequestType.CREATE 348 : RequestType.ACTION; 349 } 350 351 /** 352 * Determine whether the PUT request should be interpreted as a CREATE or an UPDATE depending on 353 * If-None-Match header, If-Match header, and protocol version. 354 * 355 * @param request The request. 356 * @return true if request is interpreted as a create; false if interpreted as an update 357 */ 358 private static RequestType determinePutRequestType(org.forgerock.http.protocol.Request request) 359 throws BadRequestException { 360 361 final Version protocolVersion = getRequestedProtocolVersion(request); 362 final String ifNoneMatch = getIfNoneMatch(request); 363 final String ifMatch = getIfMatch(request, protocolVersion); 364 365 /* CREST-100 366 * For protocol version 1: 367 * 368 * - "If-None-Match: x" is present, where 'x' is any non-* value: this is a bad request 369 * - "If-None-Match: *" is present: this is a create which will fail if the object already exists. 370 * - "If-None-Match: *" is not present: 371 * This is an update which will fail if the object does not exist. There are two ways to 372 * perform the update, using the value of the If-Match header: 373 * - "If-Match: <rev>" : update the object if its revision matches the header value 374 * - "If-Match: * : update the object regardless of the object's revision 375 * - "If-Match:" header is not present : same as "If-Match: *"; update regardless of object revision 376 * 377 * For protocol version 2 onward: 378 * 379 * Two methods of create are implied by PUT: 380 * 381 * - "If-None-Match: x" is present, where 'x' is any non-* value: this is a bad request 382 * - "If-None-Match: *" is present, this is a create which will fail if the object already exists. 383 * - "If-Match" is present; this is an update only: 384 * - "If-Match: <rev>" : update the object if its revision matches the header value 385 * - "If-Match: * : update the object regardless of the object's revision 386 * - Neither "If-None-Match" nor "If-Match" are present, this is either a create or an update ("upsert"): 387 * Attempt a create; if it fails, attempt an update. If the update fails, return an error 388 * (the record could have been deleted between the create-failure and the update, for example). 389 */ 390 391 /* CREST-346 */ 392 if (ifNoneMatch != null && !ETAG_ANY.equals(ifNoneMatch)) { 393 throw new BadRequestException("\"" + ifNoneMatch + "\" is not a supported value for If-None-Match on PUT"); 394 } 395 396 if (ETAG_ANY.equals(ifNoneMatch)) { 397 return RequestType.CREATE; 398 } else if (ifNoneMatch == null && ifMatch == null && protocolVersion.getMajor() >= 2) { 399 return RequestType.CREATE; 400 } else { 401 return RequestType.UPDATE; 402 } 403 } 404 405 /** 406 * Attempts to parse the version header and return a corresponding resource {@link Version} representation. 407 * Further validates that the specified versions are valid. That being not in the future and no earlier 408 * that the current major version. 409 * 410 * @param req 411 * The HTTP servlet request 412 * 413 * @return A non-null resource {@link Version} instance 414 * 415 * @throws BadRequestException 416 * If an invalid version is requested 417 */ 418 static Version getRequestedResourceVersion(org.forgerock.http.protocol.Request req) throws BadRequestException { 419 return getAcceptApiVersionHeader(req).getResourceVersion(); 420 } 421 422 /** 423 * Attempts to parse the version header and return a corresponding protocol {@link Version} representation. 424 * Further validates that the specified versions are valid. That being not in the future and no earlier 425 * that the current major version. 426 * 427 * @param req 428 * The HTTP servlet request 429 * 430 * @return A non-null resource {@link Version} instance 431 * 432 * @throws BadRequestException 433 * If an invalid version is requested 434 */ 435 static Version getRequestedProtocolVersion(org.forgerock.http.protocol.Request req) throws BadRequestException { 436 Version protocolVersion = getAcceptApiVersionHeader(req).getProtocolVersion(); 437 return protocolVersion != null ? protocolVersion : DEFAULT_PROTOCOL_VERSION; 438 } 439 440 /** 441 * Validate and return the AcceptApiVersionHeader. 442 * 443 * @param req 444 * The HTTP servlet request 445 * 446 * @return A non-null resource {@link Version} instance 447 * 448 * @throws BadRequestException 449 * If an invalid version is requested 450 */ 451 private static AcceptApiVersionHeader getAcceptApiVersionHeader(org.forgerock.http.protocol.Request req) 452 throws BadRequestException { 453 AcceptApiVersionHeader apiVersionHeader; 454 try { 455 apiVersionHeader = AcceptApiVersionHeader.valueOf(req); 456 } catch (IllegalArgumentException e) { 457 throw new BadRequestException(e); 458 } 459 validateProtocolVersion(apiVersionHeader.getProtocolVersion()); 460 return apiVersionHeader; 461 } 462 463 /** 464 * Validate the Protocol version as not in the future. 465 * 466 * @param protocolVersion the protocol version from the request 467 * @throws BadRequestException if the request marks a protocol version greater than the current version 468 */ 469 private static void validateProtocolVersion(Version protocolVersion) throws BadRequestException { 470 if (protocolVersion != null && protocolVersion.getMajor() > DEFAULT_PROTOCOL_VERSION.getMajor()) { 471 throw new BadRequestException("Unsupported major version: " + protocolVersion); 472 } 473 if (protocolVersion != null && protocolVersion.getMinor() > DEFAULT_PROTOCOL_VERSION.getMinor()) { 474 throw new BadRequestException("Unsupported minor version: " + protocolVersion); 475 } 476 } 477 478 static String getIfMatch(org.forgerock.http.protocol.Request req, Version protocolVersion) { 479 final String etag = req.getHeaders().getFirst(HEADER_IF_MATCH); 480 if (etag != null) { 481 if (etag.length() >= 2) { 482 // Remove quotes. 483 if (etag.charAt(0) == '"') { 484 return etag.substring(1, etag.length() - 1); 485 } 486 } else if (etag.equals(ETAG_ANY) && protocolVersion.getMajor() < 2) { 487 // If-Match * is implied prior to version 2 488 return null; 489 } 490 } 491 return etag; 492 } 493 494 static String getIfNoneMatch(org.forgerock.http.protocol.Request req) { 495 final String etag = req.getHeaders().getFirst(HEADER_IF_NONE_MATCH); 496 if (etag != null) { 497 if (etag.length() >= 2) { 498 // Remove quotes. 499 if (etag.charAt(0) == '"') { 500 return etag.substring(1, etag.length() - 1); 501 } 502 } else if (etag.equals(ETAG_ANY)) { 503 // If-None-Match *. 504 return ETAG_ANY; 505 } 506 } 507 return etag; 508 } 509 510 /** 511 * Returns the content of the provided HTTP request decoded as a JSON 512 * object. The content is allowed to be empty, in which case an empty JSON 513 * object is returned. 514 * 515 * @param req 516 * The HTTP request. 517 * @return The content of the provided HTTP request decoded as a JSON 518 * object. 519 * @throws ResourceException 520 * If the content could not be read or if the content was not 521 * valid JSON. 522 */ 523 static JsonValue getJsonContentIfPresent(org.forgerock.http.protocol.Request req) throws ResourceException { 524 return getJsonContent0(req, true); 525 } 526 527 /** 528 * Returns the content of the provided HTTP request decoded as a JSON 529 * object. If there is no content then a {@link BadRequestException} will be 530 * thrown. 531 * 532 * @param req 533 * The HTTP request. 534 * @return The content of the provided HTTP request decoded as a JSON 535 * object. 536 * @throws ResourceException 537 * If the content could not be read or if the content was not 538 * valid JSON. 539 */ 540 static JsonValue getJsonContent(org.forgerock.http.protocol.Request req) throws ResourceException { 541 return getJsonContent0(req, false); 542 } 543 544 /** 545 * Creates a JSON generator which can be used for serializing JSON content 546 * in HTTP responses. 547 * 548 * @param req 549 * The HTTP request. 550 * @param resp 551 * The HTTP response. 552 * @return A JSON generator which can be used to write out a JSON response. 553 * @throws IOException 554 * If an error occurred while obtaining an output stream. 555 */ 556 static JsonGenerator getJsonGenerator(org.forgerock.http.protocol.Request req, 557 Response resp) throws IOException { 558 559 PipeBufferedStream pipeStream = new PipeBufferedStream(); 560 resp.setEntity(pipeStream.getOut()); 561 562 final JsonGenerator writer = 563 JSON_MAPPER.getFactory().createGenerator(pipeStream.getIn()); 564 writer.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); 565 566 // Enable pretty printer if requested. 567 final List<String> values = getParameter(req, PARAM_PRETTY_PRINT); 568 if (values != null) { 569 try { 570 if (asBooleanValue(PARAM_PRETTY_PRINT, values)) { 571 writer.useDefaultPrettyPrinter(); 572 } 573 } catch (final ResourceException e) { 574 // Ignore because we may be trying to obtain a generator in 575 // order to output an error. 576 } 577 } 578 return writer; 579 } 580 581 /** 582 * Returns the content of the provided HTTP request decoded as a JSON patch 583 * object. 584 * 585 * @param req 586 * The HTTP request. 587 * @return The content of the provided HTTP request decoded as a JSON patch 588 * object. 589 * @throws ResourceException 590 * If the content could not be read or if the content was not a 591 * valid JSON patch. 592 */ 593 static List<PatchOperation> getJsonPatchContent(org.forgerock.http.protocol.Request req) 594 throws ResourceException { 595 return PatchOperation.valueOfList(new JsonValue(parseJsonBody(req, false))); 596 } 597 598 /** 599 * Returns the content of the provided HTTP request decoded as a JSON action 600 * content. 601 * 602 * @param req 603 * The HTTP request. 604 * @return The content of the provided HTTP request decoded as a JSON action 605 * content. 606 * @throws ResourceException 607 * If the content could not be read or if the content was not 608 * valid JSON. 609 */ 610 static JsonValue getJsonActionContent(org.forgerock.http.protocol.Request req) throws ResourceException { 611 return new JsonValue(parseJsonBody(req, true)); 612 } 613 614 /** 615 * Returns the effective method name for an HTTP request taking into account 616 * the "X-HTTP-Method-Override" header. 617 * 618 * @param req 619 * The HTTP request. 620 * @return The effective method name. 621 */ 622 static String getMethod(org.forgerock.http.protocol.Request req) { 623 String method = req.getMethod(); 624 if (HttpUtils.METHOD_POST.equals(method) 625 && req.getHeaders().getFirst(HttpUtils.HEADER_X_HTTP_METHOD_OVERRIDE) != null) { 626 method = req.getHeaders().getFirst(HttpUtils.HEADER_X_HTTP_METHOD_OVERRIDE); 627 } 628 return method; 629 } 630 631 /** 632 * Returns the named parameter from the provided HTTP request using case 633 * insensitive matching. 634 * 635 * @param req 636 * The HTTP request. 637 * @param parameter 638 * The parameter to return. 639 * @return The parameter values or {@code null} if it wasn't present. 640 */ 641 static List<String> getParameter(org.forgerock.http.protocol.Request req, String parameter) { 642 // Need to do case-insensitive matching. 643 for (final Map.Entry<String, List<String>> p : req.getForm().entrySet()) { 644 if (p.getKey().equalsIgnoreCase(parameter)) { 645 return p.getValue(); 646 } 647 } 648 return null; 649 } 650 651 /** 652 * Returns {@code true} if the named parameter is present in the provided 653 * HTTP request using case insensitive matching. 654 * 655 * @param req 656 * The HTTP request. 657 * @param parameter 658 * The parameter to return. 659 * @return {@code true} if the named parameter is present. 660 */ 661 static boolean hasParameter(org.forgerock.http.protocol.Request req, String parameter) { 662 return getParameter(req, parameter) != null; 663 } 664 665 static Response prepareResponse(org.forgerock.http.protocol.Request req) throws ResourceException { 666 return prepareResponse(req, new Response()); 667 } 668 669 static Response prepareResponse(org.forgerock.http.protocol.Request req, org.forgerock.http.protocol.Response resp) 670 throws ResourceException { 671 //get content type from req path 672 try { 673 resp.setStatus(Status.OK); 674 String mimeType = req.getForm().getFirst(PARAM_MIME_TYPE); 675 if (METHOD_GET.equalsIgnoreCase(getMethod(req)) && mimeType != null && !mimeType.isEmpty()) { 676 ContentType contentType = new ContentType(mimeType); 677 resp.getHeaders().put(new ContentTypeHeader(contentType.toString(), CHARACTER_ENCODING, null)); 678 } else { 679 resp.getHeaders().put(new ContentTypeHeader(MIME_TYPE_APPLICATION_JSON, CHARACTER_ENCODING, null)); 680 } 681 682 resp.getHeaders().put(HEADER_CACHE_CONTROL, CACHE_CONTROL); 683 return resp; 684 } catch (ParseException e) { 685 throw new BadRequestException("The mime type parameter '" + req.getForm().getFirst(PARAM_MIME_TYPE) 686 + "' can't be parsed", e); 687 } 688 } 689 690 static void rejectIfMatch(org.forgerock.http.protocol.Request req) throws ResourceException { 691 if (req.getHeaders().getFirst(HEADER_IF_MATCH) != null) { 692 // FIXME: i18n 693 throw new PreconditionFailedException("If-Match not supported for " + getMethod(req) + " requests"); 694 } 695 } 696 697 static void rejectIfNoneMatch(org.forgerock.http.protocol.Request req) throws ResourceException, 698 PreconditionFailedException { 699 if (req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) { 700 // FIXME: i18n 701 throw new PreconditionFailedException("If-None-Match not supported for " 702 + getMethod(req) + " requests"); 703 } 704 } 705 706 private static JsonValue getJsonContent0(org.forgerock.http.protocol.Request req, boolean allowEmpty) 707 throws ResourceException { 708 final Object body = parseJsonBody(req, allowEmpty); 709 if (body == null) { 710 return new JsonValue(new LinkedHashMap<>(0)); 711 } else if (!(body instanceof Map)) { 712 throw new BadRequestException( 713 "The request could not be processed because the provided " 714 + "content is not a JSON object"); 715 } else { 716 return new JsonValue(body); 717 } 718 } 719 720 private static BodyPart getJsonRequestPart(final MimeMultipart mimeMultiparts) 721 throws BadRequestException, ResourceException { 722 try { 723 for (int i = 0; i < mimeMultiparts.getCount(); i++) { 724 BodyPart part = mimeMultiparts.getBodyPart(i); 725 ContentType contentType = new ContentType(part.getContentType()); 726 if (contentType.match(MIME_TYPE_APPLICATION_JSON)) { 727 return part; 728 } 729 } 730 throw new BadRequestException( 731 "The request could not be processed because the multipart request " 732 + "does not include Content-Type: " + MIME_TYPE_APPLICATION_JSON); 733 } catch (final MessagingException e) { 734 throw new BadRequestException( 735 "The request could not be processed because the request cant be parsed", e); 736 } catch (final IOException e) { 737 throw adapt(e); 738 } 739 740 } 741 742 private static String getRequestPartData(final MimeMultipart mimeMultiparts, 743 final String partName, final String partDataType) throws IOException, MessagingException { 744 if (mimeMultiparts == null) { 745 throw new BadRequestException( 746 "The request parameter is null when retrieving part data for part name: " 747 + partName); 748 } 749 750 if (partDataType == null || partDataType.isEmpty()) { 751 throw new BadRequestException("The request is requesting an unknown part field"); 752 } 753 MimeBodyPart part = null; 754 for (int i = 0; i < mimeMultiparts.getCount(); i++) { 755 part = (MimeBodyPart) mimeMultiparts.getBodyPart(i); 756 ContentDisposition disposition = 757 new ContentDisposition(part.getHeader(CONTENT_DISPOSITION, null)); 758 if (disposition.getParameter(NAME).equalsIgnoreCase(partName)) { 759 break; 760 } 761 } 762 763 if (part == null) { 764 throw new BadRequestException( 765 "The request is missing a referenced part for part name: " + partName); 766 } 767 768 if (MIME_TYPE.equalsIgnoreCase(partDataType)) { 769 return new ContentType(part.getContentType()).toString(); 770 } else if (FILENAME.equalsIgnoreCase(partDataType)) { 771 return part.getFileName(); 772 } else if (CONTENT.equalsIgnoreCase(partDataType)) { 773 return Base64url.encode(toByteArray(part.getInputStream())); 774 } else { 775 throw new BadRequestException( 776 "The request could not be processed because the multipart request " 777 + "requests data from the part that isn't supported. Data requested: " 778 + partDataType); 779 } 780 } 781 782 private static boolean isAReferenceJsonObject(JsonValue node) { 783 return node.keys() != null && node.keys().size() == 1 784 && REFERENCE_TAG.equalsIgnoreCase(node.keys().iterator().next()); 785 } 786 787 private static Object swapRequestPartsIntoContent(final MimeMultipart mimeMultiparts, 788 Object content) throws ResourceException { 789 try { 790 JsonValue root = new JsonValue(content); 791 792 ArrayDeque<JsonValue> stack = new ArrayDeque<>(); 793 stack.push(root); 794 795 while (!stack.isEmpty()) { 796 JsonValue node = stack.pop(); 797 if (isAReferenceJsonObject(node)) { 798 Matcher matcher = 799 MULTIPART_FIELD_REGEX.matcher(node.get(REFERENCE_TAG).asString()); 800 if (matcher.matches()) { 801 String partName = matcher.group(PART_NAME); 802 String requestPartData = 803 getRequestPartData(mimeMultiparts, partName, matcher 804 .group(PART_DATA_TYPE)); 805 root.put(node.getPointer(), requestPartData); 806 } else { 807 throw new BadRequestException("Invalid reference tag '" + node.toString() 808 + "'"); 809 } 810 } else { 811 Iterator<JsonValue> iter = node.iterator(); 812 while (iter.hasNext()) { 813 stack.push(iter.next()); 814 } 815 } 816 } 817 return root; 818 } catch (final IOException e) { 819 throw adapt(e); 820 } catch (final MessagingException e) { 821 throw new BadRequestException( 822 "The request could not be processed because the request is not a valid multipart request"); 823 } 824 } 825 826 static boolean isMultiPartRequest(final String unknownContentType) throws BadRequestException { 827 try { 828 if (unknownContentType == null) { 829 return false; 830 } 831 ContentType contentType = new ContentType(unknownContentType); 832 return contentType.match(MIME_TYPE_MULTIPART_FORM_DATA); 833 } catch (final ParseException e) { 834 throw new BadRequestException("The request content type can't be parsed.", e); 835 } 836 } 837 838 private static Object parseJsonBody(org.forgerock.http.protocol.Request req, boolean allowEmpty) 839 throws ResourceException { 840 try { 841 String contentType = req.getHeaders().getFirst(ContentTypeHeader.class); 842 if (contentType == null && !allowEmpty) { 843 throw new BadRequestException("The request could not be processed because the " 844 + " content-type was not specified and is required"); 845 } 846 boolean isMultiPartRequest = isMultiPartRequest(contentType); 847 MimeMultipart mimeMultiparts = null; 848 JsonParser jsonParser; 849 if (isMultiPartRequest) { 850 mimeMultiparts = new MimeMultipart(new HttpServletRequestDataSource(req)); 851 BodyPart jsonPart = getJsonRequestPart(mimeMultiparts); 852 jsonParser = JSON_MAPPER.getFactory().createParser(jsonPart.getInputStream()); 853 } else { 854 jsonParser = JSON_MAPPER.getFactory().createParser(req.getEntity().getRawContentInputStream()); 855 } 856 try (JsonParser parser = jsonParser) { 857 Object content = parser.readValueAs(Object.class); 858 859 // Ensure that there is no trailing data following the JSON resource. 860 boolean hasTrailingGarbage; 861 try { 862 hasTrailingGarbage = parser.nextToken() != null; 863 } catch (JsonParseException e) { 864 hasTrailingGarbage = true; 865 } 866 if (hasTrailingGarbage) { 867 throw new BadRequestException( 868 "The request could not be processed because there is " 869 + "trailing data after the JSON content"); 870 } 871 872 if (isMultiPartRequest) { 873 swapRequestPartsIntoContent(mimeMultiparts, content); 874 } 875 876 return content; 877 } 878 } catch (final JsonParseException e) { 879 throw new BadRequestException( 880 "The request could not be processed because the provided " 881 + "content is not valid JSON", e) 882 .setDetail(new JsonValue(e.getMessage())); 883 } catch (final JsonMappingException e) { 884 if (allowEmpty) { 885 return null; 886 } else { 887 throw new BadRequestException("The request could not be processed " 888 + "because it did not contain any JSON content", e); 889 } 890 } catch (final IOException e) { 891 throw adapt(e); 892 } catch (final MessagingException e) { 893 throw new BadRequestException( 894 "The request could not be processed because it can't be parsed", e); 895 } 896 } 897 898 private static String param(final String field) { 899 return "_" + field; 900 } 901 902 private HttpUtils() { 903 // Prevent instantiation. 904 } 905 906 private static class HttpServletRequestDataSource implements DataSource { 907 private org.forgerock.http.protocol.Request request; 908 909 HttpServletRequestDataSource(org.forgerock.http.protocol.Request request) { 910 this.request = request; 911 } 912 913 public InputStream getInputStream() throws IOException { 914 return request.getEntity().getRawContentInputStream(); 915 } 916 917 public OutputStream getOutputStream() throws IOException { 918 return null; 919 } 920 921 public String getContentType() { 922 return request.getHeaders().getFirst(ContentTypeHeader.class); 923 } 924 925 public String getName() { 926 return "HttpServletRequestDataSource"; 927 } 928 } 929 930 private static byte[] toByteArray(final InputStream inputStream) throws IOException { 931 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 932 final byte[] data = new byte[BUFFER_SIZE]; 933 int size; 934 while ((size = inputStream.read(data)) != EOF) { 935 byteArrayOutputStream.write(data, 0, size); 936 } 937 byteArrayOutputStream.flush(); 938 return byteArrayOutputStream.toByteArray(); 939 } 940}