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