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