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