View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2012-2016 ForgeRock AS.
15   * Portions copyright 2026 Wren Security
16   */
17  package org.forgerock.json.resource.http;
18  
19  import static org.forgerock.http.protocol.Responses.newInternalServerError;
20  import static org.forgerock.http.routing.Version.version;
21  import static org.forgerock.json.resource.ActionRequest.ACTION_ID_CREATE;
22  import static org.forgerock.util.Utils.closeSilently;
23  import static org.forgerock.util.promise.Promises.newResultPromise;
24  
25  import com.fasterxml.jackson.core.JsonGenerator;
26  import com.fasterxml.jackson.core.JsonParseException;
27  import com.fasterxml.jackson.core.JsonParser;
28  import com.fasterxml.jackson.databind.JsonMappingException;
29  import com.fasterxml.jackson.databind.ObjectMapper;
30  import jakarta.activation.DataSource;
31  import jakarta.mail.BodyPart;
32  import jakarta.mail.MessagingException;
33  import jakarta.mail.internet.ContentDisposition;
34  import jakarta.mail.internet.ContentType;
35  import jakarta.mail.internet.MimeBodyPart;
36  import jakarta.mail.internet.MimeMultipart;
37  import jakarta.mail.internet.ParseException;
38  import java.io.ByteArrayOutputStream;
39  import java.io.IOException;
40  import java.io.InputStream;
41  import java.io.OutputStream;
42  import java.util.ArrayDeque;
43  import java.util.Arrays;
44  import java.util.Collection;
45  import java.util.Iterator;
46  import java.util.LinkedHashMap;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.regex.Matcher;
50  import java.util.regex.Pattern;
51  import org.forgerock.http.header.AcceptApiVersionHeader;
52  import org.forgerock.http.header.ContentTypeHeader;
53  import org.forgerock.http.header.MalformedHeaderException;
54  import org.forgerock.http.io.PipeBufferedStream;
55  import org.forgerock.http.protocol.Response;
56  import org.forgerock.http.protocol.Status;
57  import org.forgerock.http.routing.Version;
58  import org.forgerock.http.util.Json;
59  import org.forgerock.json.JsonValue;
60  import org.forgerock.json.resource.ActionRequest;
61  import org.forgerock.json.resource.BadRequestException;
62  import org.forgerock.json.resource.InternalServerErrorException;
63  import org.forgerock.json.resource.NotSupportedException;
64  import org.forgerock.json.resource.PatchOperation;
65  import org.forgerock.json.resource.PreconditionFailedException;
66  import org.forgerock.json.resource.QueryRequest;
67  import org.forgerock.json.resource.Request;
68  import org.forgerock.json.resource.RequestType;
69  import org.forgerock.json.resource.ResourceException;
70  import org.forgerock.services.context.Context;
71  import org.forgerock.util.encode.Base64url;
72  import org.forgerock.util.promise.NeverThrowsException;
73  import org.forgerock.util.promise.Promise;
74  
75  /**
76   * HTTP utility methods and constants.
77   */
78  public final class HttpUtils {
79      static final String CACHE_CONTROL = "no-cache";
80      static final String CHARACTER_ENCODING = "UTF-8";
81      static final Pattern CONTENT_TYPE_REGEX = Pattern.compile(
82              "^application/json([ ]*;[ ]*charset=utf-8)?$", Pattern.CASE_INSENSITIVE);
83      static final String CRLF = "\r\n";
84      static final String ETAG_ANY = "*";
85  
86      static final String MIME_TYPE_APPLICATION_JSON = "application/json";
87      static final String MIME_TYPE_MULTIPART_FORM_DATA = "multipart/form-data";
88      static final String MIME_TYPE_TEXT_PLAIN = "text/plain";
89  
90      static final String HEADER_CACHE_CONTROL = "Cache-Control";
91      static final String HEADER_ETAG = "ETag";
92      static final String HEADER_IF_MATCH = "If-Match";
93      static final String HEADER_IF_NONE_MATCH = "If-None-Match";
94      static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
95      static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
96      static final String HEADER_LOCATION = "Location";
97      static final String HEADER_X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
98      /** the HTTP header for {@literal Content-Disposition}. */
99      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 }