1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
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
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
124 public static final String PARAM_ACTION = param(ActionRequest.FIELD_ACTION);
125
126 public static final String PARAM_FIELDS = param(Request.FIELD_FIELDS);
127
128 public static final String PARAM_MIME_TYPE = param("mimeType");
129
130 public static final String PARAM_PAGE_SIZE = param(QueryRequest.FIELD_PAGE_SIZE);
131
132 public static final String PARAM_PAGED_RESULTS_COOKIE =
133 param(QueryRequest.FIELD_PAGED_RESULTS_COOKIE);
134
135 public static final String PARAM_PAGED_RESULTS_OFFSET =
136 param(QueryRequest.FIELD_PAGED_RESULTS_OFFSET);
137
138 public static final String PARAM_PRETTY_PRINT = "_prettyPrint";
139
140 public static final String PARAM_QUERY_EXPRESSION = param(QueryRequest.FIELD_QUERY_EXPRESSION);
141
142 public static final String PARAM_QUERY_FILTER = param(QueryRequest.FIELD_QUERY_FILTER);
143
144 public static final String PARAM_QUERY_ID = param(QueryRequest.FIELD_QUERY_ID);
145
146 public static final String PARAM_SORT_KEYS = param(QueryRequest.FIELD_SORT_KEYS);
147
148 public static final String PARAM_TOTAL_PAGED_RESULTS_POLICY = param(QueryRequest.FIELD_TOTAL_PAGED_RESULTS_POLICY);
149
150 public static final String PARAM_CREST_API = param("crestapi");
151
152
153 public static final Version PROTOCOL_VERSION_1 = version(1);
154
155 public static final Version PROTOCOL_VERSION_2 = version(2);
156
157
158
159
160
161 public static final Version PROTOCOL_VERSION_2_1 = version(2, 1);
162
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
185
186
187
188
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
200
201
202
203
204
205
206
207
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
217
218
219
220
221
222
223
224
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
232 throw new BadRequestException("The value \'" + value + "\' for parameter '" + name
233 + "' could not be parsed as a valid integer");
234 }
235 }
236
237
238
239
240
241
242
243
244
245
246
247
248 static String asSingleValue(final String name, final List<String> values) throws ResourceException {
249 if (values == null || values.isEmpty()) {
250
251 throw new BadRequestException("No values provided for the request parameter \'" + name
252 + "\'");
253 } else if (values.size() > 1) {
254
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
264
265
266
267
268
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
276
277
278
279
280
281
282
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
306 return newResultPromise(newInternalServerError());
307 } catch (MalformedHeaderException e) {
308 return newResultPromise(new Response(Status.BAD_REQUEST).setEntity("Malformed header"));
309 }
310 }
311
312
313
314
315
316
317
318
319
320 public static RequestType determineRequestType(org.forgerock.http.protocol.Request request)
321 throws ResourceException {
322
323
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
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
363
364
365
366
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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
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
417
418
419
420
421
422
423
424
425
426
427
428 static Version getRequestedResourceVersion(org.forgerock.http.protocol.Request req) throws BadRequestException {
429 return getAcceptApiVersionHeader(req).getResourceVersion();
430 }
431
432
433
434
435
436
437
438
439
440
441
442
443
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
452
453
454
455
456
457
458
459
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
475
476
477
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
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
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
509 if (etag.charAt(0) == '"') {
510 return etag.substring(1, etag.length() - 1);
511 }
512 } else if (etag.equals(ETAG_ANY)) {
513
514 return ETAG_ANY;
515 }
516 }
517 return etag;
518 }
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533 static JsonValue getJsonContentIfPresent(org.forgerock.http.protocol.Request req) throws ResourceException {
534 return getJsonContent0(req, true);
535 }
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550 static JsonValue getJsonContent(org.forgerock.http.protocol.Request req) throws ResourceException {
551 return getJsonContent0(req, false);
552 }
553
554
555
556
557
558
559
560
561
562
563
564
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
576
577 writer.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, true);
578
579
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
588
589 }
590 }
591 return writer;
592 }
593
594
595
596
597
598
599
600
601
602
603
604
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
613
614
615
616
617
618
619
620
621
622
623 static JsonValue getJsonActionContent(org.forgerock.http.protocol.Request req) throws ResourceException {
624 return new JsonValue(parseJsonBody(req, true));
625 }
626
627
628
629
630
631
632
633
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
646
647
648
649
650
651
652
653
654 static List<String> getParameter(org.forgerock.http.protocol.Request req, String parameter) {
655
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
666
667
668
669
670
671
672
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
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
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
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
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 }