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