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 java.io.ByteArrayOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.util.ArrayDeque;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.Iterator;
33 import java.util.LinkedHashMap;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38
39 import javax.activation.DataSource;
40 import javax.mail.BodyPart;
41 import javax.mail.MessagingException;
42 import javax.mail.internet.ContentDisposition;
43 import javax.mail.internet.ContentType;
44 import javax.mail.internet.MimeBodyPart;
45 import javax.mail.internet.MimeMultipart;
46 import javax.mail.internet.ParseException;
47
48 import org.forgerock.http.header.AcceptApiVersionHeader;
49 import org.forgerock.http.header.ContentTypeHeader;
50 import org.forgerock.http.header.MalformedHeaderException;
51 import org.forgerock.http.io.PipeBufferedStream;
52 import org.forgerock.http.protocol.Response;
53 import org.forgerock.http.protocol.Status;
54 import org.forgerock.http.routing.Version;
55 import org.forgerock.http.util.Json;
56 import org.forgerock.json.JsonValue;
57 import org.forgerock.json.resource.ActionRequest;
58 import org.forgerock.json.resource.BadRequestException;
59 import org.forgerock.json.resource.InternalServerErrorException;
60 import org.forgerock.json.resource.NotSupportedException;
61 import org.forgerock.json.resource.PatchOperation;
62 import org.forgerock.json.resource.PreconditionFailedException;
63 import org.forgerock.json.resource.QueryRequest;
64 import org.forgerock.json.resource.Request;
65 import org.forgerock.json.resource.RequestType;
66 import org.forgerock.json.resource.ResourceException;
67 import org.forgerock.services.context.Context;
68 import org.forgerock.util.encode.Base64url;
69 import org.forgerock.util.promise.NeverThrowsException;
70 import org.forgerock.util.promise.Promise;
71
72 import com.fasterxml.jackson.core.JsonGenerator;
73 import com.fasterxml.jackson.core.JsonParseException;
74 import com.fasterxml.jackson.core.JsonParser;
75 import com.fasterxml.jackson.databind.JsonMappingException;
76 import com.fasterxml.jackson.databind.ObjectMapper;
77
78
79
80
81 public final class HttpUtils {
82 static final String CACHE_CONTROL = "no-cache";
83 static final String CHARACTER_ENCODING = "UTF-8";
84 static final Pattern CONTENT_TYPE_REGEX = Pattern.compile(
85 "^application/json([ ]*;[ ]*charset=utf-8)?$", Pattern.CASE_INSENSITIVE);
86 static final String CRLF = "\r\n";
87 static final String ETAG_ANY = "*";
88
89 static final String MIME_TYPE_APPLICATION_JSON = "application/json";
90 static final String MIME_TYPE_MULTIPART_FORM_DATA = "multipart/form-data";
91 static final String MIME_TYPE_TEXT_PLAIN = "text/plain";
92
93 static final String HEADER_CACHE_CONTROL = "Cache-Control";
94 static final String HEADER_ETAG = "ETag";
95 static final String HEADER_IF_MATCH = "If-Match";
96 static final String HEADER_IF_NONE_MATCH = "If-None-Match";
97 static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
98 static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
99 static final String HEADER_LOCATION = "Location";
100 static final String HEADER_X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
101
102 public static final String CONTENT_DISPOSITION = "Content-Disposition";
103 static final Collection<String> RESTRICTED_HEADER_NAMES = Arrays.asList(
104 ContentTypeHeader.NAME,
105 AcceptApiVersionHeader.NAME,
106 HEADER_IF_MODIFIED_SINCE,
107 HEADER_IF_UNMODIFIED_SINCE,
108 HEADER_IF_MATCH,
109 HEADER_IF_NONE_MATCH,
110 HEADER_CACHE_CONTROL,
111 HEADER_ETAG,
112 HEADER_LOCATION,
113 HEADER_X_HTTP_METHOD_OVERRIDE,
114 CONTENT_DISPOSITION
115 );
116
117 static final String METHOD_DELETE = "DELETE";
118 static final String METHOD_GET = "GET";
119 static final String METHOD_HEAD = "HEAD";
120 static final String METHOD_OPTIONS = "OPTIONS";
121 static final String METHOD_PATCH = "PATCH";
122 static final String METHOD_POST = "POST";
123 static final String METHOD_PUT = "PUT";
124 static final String METHOD_TRACE = "TRACE";
125
126
127 public static final String PARAM_ACTION = param(ActionRequest.FIELD_ACTION);
128
129 public static final String PARAM_FIELDS = param(Request.FIELD_FIELDS);
130
131 public static final String PARAM_MIME_TYPE = param("mimeType");
132
133 public static final String PARAM_PAGE_SIZE = param(QueryRequest.FIELD_PAGE_SIZE);
134
135 public static final String PARAM_PAGED_RESULTS_COOKIE =
136 param(QueryRequest.FIELD_PAGED_RESULTS_COOKIE);
137
138 public static final String PARAM_PAGED_RESULTS_OFFSET =
139 param(QueryRequest.FIELD_PAGED_RESULTS_OFFSET);
140
141 public static final String PARAM_PRETTY_PRINT = "_prettyPrint";
142
143 public static final String PARAM_QUERY_EXPRESSION = param(QueryRequest.FIELD_QUERY_EXPRESSION);
144
145 public static final String PARAM_QUERY_FILTER = param(QueryRequest.FIELD_QUERY_FILTER);
146
147 public static final String PARAM_QUERY_ID = param(QueryRequest.FIELD_QUERY_ID);
148
149 public static final String PARAM_SORT_KEYS = param(QueryRequest.FIELD_SORT_KEYS);
150
151 public static final String PARAM_TOTAL_PAGED_RESULTS_POLICY = param(QueryRequest.FIELD_TOTAL_PAGED_RESULTS_POLICY);
152
153 public static final String PARAM_CREST_API = param("crestapi");
154
155
156 public static final Version PROTOCOL_VERSION_1 = version(1);
157
158 public static final Version PROTOCOL_VERSION_2 = version(2);
159
160
161
162
163
164 public static final Version PROTOCOL_VERSION_2_1 = version(2, 1);
165
166 public static final Version DEFAULT_PROTOCOL_VERSION = PROTOCOL_VERSION_2_1;
167 static final String FIELDS_DELIMITER = ",";
168 static final String SORT_KEYS_DELIMITER = ",";
169
170 static final ObjectMapper JSON_MAPPER = new ObjectMapper()
171 .registerModules(new Json.JsonValueModule(), new Json.LocalizableStringModule());
172
173 private static final String FILENAME = "filename";
174 private static final String MIME_TYPE = "mimetype";
175 private static final String CONTENT = "content";
176 private static final String NAME = "name";
177 private static final Pattern MULTIPART_FIELD_REGEX = Pattern.compile("^cid:(.*)#(" + FILENAME
178 + "|" + MIME_TYPE + "|" + CONTENT + ")$", Pattern.CASE_INSENSITIVE);
179 private static final int PART_NAME = 1;
180 private static final int PART_DATA_TYPE = 2;
181 private static final String REFERENCE_TAG = "$ref";
182
183 private static final int BUFFER_SIZE = 1_024;
184 private static final int EOF = -1;
185
186
187
188
189
190
191
192
193 static ResourceException adapt(final Throwable t) {
194 if (t instanceof ResourceException) {
195 return (ResourceException) t;
196 } else {
197 return new InternalServerErrorException(t);
198 }
199 }
200
201
202
203
204
205
206
207
208
209
210
211
212 static boolean asBooleanValue(final String name, final List<String> values)
213 throws ResourceException {
214 final String value = asSingleValue(name, values);
215 return Boolean.parseBoolean(value);
216 }
217
218
219
220
221
222
223
224
225
226
227
228
229 static int asIntValue(final String name, final List<String> values) throws ResourceException {
230 final String value = asSingleValue(name, values);
231 try {
232 return Integer.parseInt(value);
233 } catch (final NumberFormatException e) {
234
235 throw new BadRequestException("The value \'" + value + "\' for parameter '" + name
236 + "' could not be parsed as a valid integer");
237 }
238 }
239
240
241
242
243
244
245
246
247
248
249
250
251 static String asSingleValue(final String name, final List<String> values) throws ResourceException {
252 if (values == null || values.isEmpty()) {
253
254 throw new BadRequestException("No values provided for the request parameter \'" + name
255 + "\'");
256 } else if (values.size() > 1) {
257
258 throw new BadRequestException(
259 "Multiple values provided for the single-valued request parameter \'" + name
260 + "\'");
261 }
262 return values.get(0);
263 }
264
265
266
267
268
269
270
271
272
273 static Promise<Response, NeverThrowsException> fail(org.forgerock.http.protocol.Request req, final Throwable t) {
274 return fail0(req, null, t);
275 }
276
277
278
279
280
281
282
283
284
285
286
287 static Promise<Response, NeverThrowsException> fail(org.forgerock.http.protocol.Request req,
288 org.forgerock.http.protocol.Response resp, final Throwable t) {
289 return fail0(req, resp, t);
290 }
291
292 private static Promise<Response, NeverThrowsException> fail0(org.forgerock.http.protocol.Request req,
293 org.forgerock.http.protocol.Response resp, Throwable t) {
294 final ResourceException re = adapt(t);
295 try {
296 if (resp == null) {
297 resp = prepareResponse(req);
298 } else {
299 resp = prepareResponse(req, resp);
300 }
301 resp.setStatus(Status.valueOf(re.getCode()));
302 final JsonGenerator writer = getJsonGenerator(req, resp);
303 Json.makeLocalizingObjectWriter(JSON_MAPPER, req).writeValue(writer, re.toJsonValue().getObject());
304 closeSilently(writer);
305 return newResultPromise(resp);
306 } catch (final IOException ignored) {
307
308 return newResultPromise(newInternalServerError());
309 } catch (MalformedHeaderException e) {
310 return newResultPromise(new Response(Status.BAD_REQUEST).setEntity("Malformed header"));
311 }
312 }
313
314
315
316
317
318
319
320
321
322 public static RequestType determineRequestType(org.forgerock.http.protocol.Request request)
323 throws ResourceException {
324
325
326 final String method = getMethod(request);
327 if (METHOD_DELETE.equals(method)) {
328 return RequestType.DELETE;
329 } else if (METHOD_GET.equals(method)) {
330 if (hasParameter(request, PARAM_QUERY_ID)
331 || hasParameter(request, PARAM_QUERY_EXPRESSION)
332 || hasParameter(request, PARAM_QUERY_FILTER)) {
333 return RequestType.QUERY;
334 } else if (hasParameter(request, PARAM_CREST_API)) {
335 return RequestType.API;
336 } else {
337 return RequestType.READ;
338 }
339 } else if (METHOD_PATCH.equals(method)) {
340 return RequestType.PATCH;
341 } else if (METHOD_POST.equals(method)) {
342 return determinePostRequestType(request);
343 } else if (METHOD_PUT.equals(method)) {
344 return determinePutRequestType(request);
345 } else {
346
347 throw new NotSupportedException("Method " + method + " not supported");
348 }
349 }
350
351 private static RequestType determinePostRequestType(org.forgerock.http.protocol.Request request)
352 throws ResourceException {
353 List<String> parameter = getParameter(request, PARAM_ACTION);
354
355 boolean defactoCreate = getRequestedProtocolVersion(request).compareTo(PROTOCOL_VERSION_2_1) >= 0
356 && (parameter == null || parameter.isEmpty());
357
358 return defactoCreate || asSingleValue(PARAM_ACTION, parameter).equalsIgnoreCase(ACTION_ID_CREATE)
359 ? RequestType.CREATE
360 : RequestType.ACTION;
361 }
362
363
364
365
366
367
368
369
370 private static RequestType determinePutRequestType(org.forgerock.http.protocol.Request request)
371 throws BadRequestException {
372
373 final Version protocolVersion = getRequestedProtocolVersion(request);
374 final String ifNoneMatch = getIfNoneMatch(request);
375 final String ifMatch = getIfMatch(request, protocolVersion);
376
377
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
403
404 if (ifNoneMatch != null && !ETAG_ANY.equals(ifNoneMatch)) {
405 throw new BadRequestException("\"" + ifNoneMatch + "\" is not a supported value for If-None-Match on PUT");
406 }
407
408 if (ETAG_ANY.equals(ifNoneMatch)) {
409 return RequestType.CREATE;
410 } else if (ifNoneMatch == null && ifMatch == null && protocolVersion.getMajor() >= 2) {
411 return RequestType.CREATE;
412 } else {
413 return RequestType.UPDATE;
414 }
415 }
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430 static Version getRequestedResourceVersion(org.forgerock.http.protocol.Request req) throws BadRequestException {
431 return getAcceptApiVersionHeader(req).getResourceVersion();
432 }
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447 static Version getRequestedProtocolVersion(org.forgerock.http.protocol.Request req) throws BadRequestException {
448 Version protocolVersion = getAcceptApiVersionHeader(req).getProtocolVersion();
449 return protocolVersion != null ? protocolVersion : DEFAULT_PROTOCOL_VERSION;
450 }
451
452
453
454
455
456
457
458
459
460
461
462
463 private static AcceptApiVersionHeader getAcceptApiVersionHeader(org.forgerock.http.protocol.Request req)
464 throws BadRequestException {
465 AcceptApiVersionHeader apiVersionHeader;
466 try {
467 apiVersionHeader = AcceptApiVersionHeader.valueOf(req);
468 } catch (IllegalArgumentException e) {
469 throw new BadRequestException(e);
470 }
471 validateProtocolVersion(apiVersionHeader.getProtocolVersion());
472 return apiVersionHeader;
473 }
474
475
476
477
478
479
480
481 private static void validateProtocolVersion(Version protocolVersion) throws BadRequestException {
482 if (protocolVersion != null && protocolVersion.getMajor() > DEFAULT_PROTOCOL_VERSION.getMajor()) {
483 throw new BadRequestException("Unsupported major version: " + protocolVersion);
484 }
485 if (protocolVersion != null && protocolVersion.getMinor() > DEFAULT_PROTOCOL_VERSION.getMinor()) {
486 throw new BadRequestException("Unsupported minor version: " + protocolVersion);
487 }
488 }
489
490 static String getIfMatch(org.forgerock.http.protocol.Request req, Version protocolVersion) {
491 final String etag = req.getHeaders().getFirst(HEADER_IF_MATCH);
492 if (etag != null) {
493 if (etag.length() >= 2) {
494
495 if (etag.charAt(0) == '"') {
496 return etag.substring(1, etag.length() - 1);
497 }
498 } else if (etag.equals(ETAG_ANY) && protocolVersion.getMajor() < 2) {
499
500 return null;
501 }
502 }
503 return etag;
504 }
505
506 static String getIfNoneMatch(org.forgerock.http.protocol.Request req) {
507 final String etag = req.getHeaders().getFirst(HEADER_IF_NONE_MATCH);
508 if (etag != null) {
509 if (etag.length() >= 2) {
510
511 if (etag.charAt(0) == '"') {
512 return etag.substring(1, etag.length() - 1);
513 }
514 } else if (etag.equals(ETAG_ANY)) {
515
516 return ETAG_ANY;
517 }
518 }
519 return etag;
520 }
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535 static JsonValue getJsonContentIfPresent(org.forgerock.http.protocol.Request req) throws ResourceException {
536 return getJsonContent0(req, true);
537 }
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552 static JsonValue getJsonContent(org.forgerock.http.protocol.Request req) throws ResourceException {
553 return getJsonContent0(req, false);
554 }
555
556
557
558
559
560
561
562
563
564
565
566
567
568 static JsonGenerator getJsonGenerator(org.forgerock.http.protocol.Request req,
569 Response resp) throws IOException {
570
571 PipeBufferedStream pipeStream = new PipeBufferedStream();
572 resp.setEntity(pipeStream.getOut());
573
574 final JsonGenerator writer =
575 JSON_MAPPER.getFactory().createGenerator(pipeStream.getIn());
576
577
578
579 writer.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, true);
580
581
582 final List<String> values = getParameter(req, PARAM_PRETTY_PRINT);
583 if (values != null) {
584 try {
585 if (asBooleanValue(PARAM_PRETTY_PRINT, values)) {
586 writer.useDefaultPrettyPrinter();
587 }
588 } catch (final ResourceException e) {
589
590
591 }
592 }
593 return writer;
594 }
595
596
597
598
599
600
601
602
603
604
605
606
607
608 static List<PatchOperation> getJsonPatchContent(org.forgerock.http.protocol.Request req)
609 throws ResourceException {
610 return PatchOperation.valueOfList(new JsonValue(parseJsonBody(req, false)));
611 }
612
613
614
615
616
617
618
619
620
621
622
623
624
625 static JsonValue getJsonActionContent(org.forgerock.http.protocol.Request req) throws ResourceException {
626 return new JsonValue(parseJsonBody(req, true));
627 }
628
629
630
631
632
633
634
635
636
637 static String getMethod(org.forgerock.http.protocol.Request req) {
638 String method = req.getMethod();
639 if (HttpUtils.METHOD_POST.equals(method)
640 && req.getHeaders().getFirst(HttpUtils.HEADER_X_HTTP_METHOD_OVERRIDE) != null) {
641 method = req.getHeaders().getFirst(HttpUtils.HEADER_X_HTTP_METHOD_OVERRIDE);
642 }
643 return method;
644 }
645
646
647
648
649
650
651
652
653
654
655
656 static List<String> getParameter(org.forgerock.http.protocol.Request req, String parameter) {
657
658 for (final Map.Entry<String, List<String>> p : req.getForm().entrySet()) {
659 if (p.getKey().equalsIgnoreCase(parameter)) {
660 return p.getValue();
661 }
662 }
663 return null;
664 }
665
666
667
668
669
670
671
672
673
674
675
676 static boolean hasParameter(org.forgerock.http.protocol.Request req, String parameter) {
677 return getParameter(req, parameter) != null;
678 }
679
680 static Response prepareResponse(org.forgerock.http.protocol.Request req) throws ResourceException {
681 return prepareResponse(req, new Response(Status.OK));
682 }
683
684 static Response prepareResponse(org.forgerock.http.protocol.Request req, org.forgerock.http.protocol.Response resp)
685 throws ResourceException {
686
687 try {
688 resp.setStatus(Status.OK);
689 String mimeType = req.getForm().getFirst(PARAM_MIME_TYPE);
690 if (METHOD_GET.equalsIgnoreCase(getMethod(req)) && mimeType != null && !mimeType.isEmpty()) {
691 ContentType contentType = new ContentType(mimeType);
692 resp.getHeaders().put(new ContentTypeHeader(contentType.toString(), CHARACTER_ENCODING, null));
693 } else {
694 resp.getHeaders().put(new ContentTypeHeader(MIME_TYPE_APPLICATION_JSON, CHARACTER_ENCODING, null));
695 }
696
697 resp.getHeaders().put(HEADER_CACHE_CONTROL, CACHE_CONTROL);
698 return resp;
699 } catch (ParseException e) {
700 throw new BadRequestException("The mime type parameter '" + req.getForm().getFirst(PARAM_MIME_TYPE)
701 + "' can't be parsed", e);
702 }
703 }
704
705 static void rejectIfMatch(org.forgerock.http.protocol.Request req) throws ResourceException {
706 if (req.getHeaders().getFirst(HEADER_IF_MATCH) != null) {
707
708 throw new PreconditionFailedException("If-Match not supported for " + getMethod(req) + " requests");
709 }
710 }
711
712 static void rejectIfNoneMatch(org.forgerock.http.protocol.Request req) throws ResourceException,
713 PreconditionFailedException {
714 if (req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) {
715
716 throw new PreconditionFailedException("If-None-Match not supported for "
717 + getMethod(req) + " requests");
718 }
719 }
720
721 private static JsonValue getJsonContent0(org.forgerock.http.protocol.Request req, boolean allowEmpty)
722 throws ResourceException {
723 final Object body = parseJsonBody(req, allowEmpty);
724 if (body == null) {
725 return new JsonValue(new LinkedHashMap<>(0));
726 } else if (!(body instanceof Map)) {
727 throw new BadRequestException(
728 "The request could not be processed because the provided "
729 + "content is not a JSON object");
730 } else {
731 return new JsonValue(body);
732 }
733 }
734
735 private static BodyPart getJsonRequestPart(final MimeMultipart mimeMultiparts)
736 throws BadRequestException, ResourceException {
737 try {
738 for (int i = 0; i < mimeMultiparts.getCount(); i++) {
739 BodyPart part = mimeMultiparts.getBodyPart(i);
740 ContentType contentType = new ContentType(part.getContentType());
741 if (contentType.match(MIME_TYPE_APPLICATION_JSON)) {
742 return part;
743 }
744 }
745 throw new BadRequestException(
746 "The request could not be processed because the multipart request "
747 + "does not include Content-Type: " + MIME_TYPE_APPLICATION_JSON);
748 } catch (final MessagingException e) {
749 throw new BadRequestException(
750 "The request could not be processed because the request cant be parsed", e);
751 } catch (final IOException e) {
752 throw adapt(e);
753 }
754
755 }
756
757 private static String getRequestPartData(final MimeMultipart mimeMultiparts,
758 final String partName, final String partDataType) throws IOException, MessagingException {
759 if (mimeMultiparts == null) {
760 throw new BadRequestException(
761 "The request parameter is null when retrieving part data for part name: "
762 + partName);
763 }
764
765 if (partDataType == null || partDataType.isEmpty()) {
766 throw new BadRequestException("The request is requesting an unknown part field");
767 }
768 MimeBodyPart part = null;
769 for (int i = 0; i < mimeMultiparts.getCount(); i++) {
770 part = (MimeBodyPart) mimeMultiparts.getBodyPart(i);
771 ContentDisposition disposition =
772 new ContentDisposition(part.getHeader(CONTENT_DISPOSITION, null));
773 if (disposition.getParameter(NAME).equalsIgnoreCase(partName)) {
774 break;
775 }
776 }
777
778 if (part == null) {
779 throw new BadRequestException(
780 "The request is missing a referenced part for part name: " + partName);
781 }
782
783 if (MIME_TYPE.equalsIgnoreCase(partDataType)) {
784 return new ContentType(part.getContentType()).toString();
785 } else if (FILENAME.equalsIgnoreCase(partDataType)) {
786 return part.getFileName();
787 } else if (CONTENT.equalsIgnoreCase(partDataType)) {
788 return Base64url.encode(toByteArray(part.getInputStream()));
789 } else {
790 throw new BadRequestException(
791 "The request could not be processed because the multipart request "
792 + "requests data from the part that isn't supported. Data requested: "
793 + partDataType);
794 }
795 }
796
797 private static boolean isAReferenceJsonObject(JsonValue node) {
798 return node.keys() != null && node.keys().size() == 1
799 && REFERENCE_TAG.equalsIgnoreCase(node.keys().iterator().next());
800 }
801
802 private static Object swapRequestPartsIntoContent(final MimeMultipart mimeMultiparts,
803 Object content) throws ResourceException {
804 try {
805 JsonValue root = new JsonValue(content);
806
807 ArrayDeque<JsonValue> stack = new ArrayDeque<>();
808 stack.push(root);
809
810 while (!stack.isEmpty()) {
811 JsonValue node = stack.pop();
812 if (isAReferenceJsonObject(node)) {
813 Matcher matcher =
814 MULTIPART_FIELD_REGEX.matcher(node.get(REFERENCE_TAG).asString());
815 if (matcher.matches()) {
816 String partName = matcher.group(PART_NAME);
817 String requestPartData =
818 getRequestPartData(mimeMultiparts, partName, matcher
819 .group(PART_DATA_TYPE));
820 root.put(node.getPointer(), requestPartData);
821 } else {
822 throw new BadRequestException("Invalid reference tag '" + node.toString()
823 + "'");
824 }
825 } else {
826 Iterator<JsonValue> iter = node.iterator();
827 while (iter.hasNext()) {
828 stack.push(iter.next());
829 }
830 }
831 }
832 return root;
833 } catch (final IOException e) {
834 throw adapt(e);
835 } catch (final MessagingException e) {
836 throw new BadRequestException(
837 "The request could not be processed because the request is not a valid multipart request");
838 }
839 }
840
841 static boolean isMultiPartRequest(final String unknownContentType) throws BadRequestException {
842 try {
843 if (unknownContentType == null) {
844 return false;
845 }
846 ContentType contentType = new ContentType(unknownContentType);
847 return contentType.match(MIME_TYPE_MULTIPART_FORM_DATA);
848 } catch (final ParseException e) {
849 throw new BadRequestException("The request content type can't be parsed.", e);
850 }
851 }
852
853 private static Object parseJsonBody(org.forgerock.http.protocol.Request req, boolean allowEmpty)
854 throws ResourceException {
855 try {
856 String contentType = req.getHeaders().getFirst(ContentTypeHeader.class);
857 if (contentType == null && !allowEmpty) {
858 throw new BadRequestException("The request could not be processed because the "
859 + " content-type was not specified and is required");
860 }
861 boolean isMultiPartRequest = isMultiPartRequest(contentType);
862 MimeMultipart mimeMultiparts = null;
863 JsonParser jsonParser;
864 if (isMultiPartRequest) {
865 mimeMultiparts = new MimeMultipart(new HttpServletRequestDataSource(req));
866 BodyPart jsonPart = getJsonRequestPart(mimeMultiparts);
867 jsonParser = JSON_MAPPER.getFactory().createParser(jsonPart.getInputStream());
868 } else {
869 jsonParser = JSON_MAPPER.getFactory().createParser(req.getEntity().getRawContentInputStream());
870 }
871 try (JsonParser parser = jsonParser) {
872 Object content = parser.readValueAs(Object.class);
873
874
875 boolean hasTrailingGarbage;
876 try {
877 hasTrailingGarbage = parser.nextToken() != null;
878 } catch (JsonParseException e) {
879 hasTrailingGarbage = true;
880 }
881 if (hasTrailingGarbage) {
882 throw new BadRequestException(
883 "The request could not be processed because there is "
884 + "trailing data after the JSON content");
885 }
886
887 if (isMultiPartRequest) {
888 swapRequestPartsIntoContent(mimeMultiparts, content);
889 }
890
891 return content;
892 }
893 } catch (final JsonParseException e) {
894 throw new BadRequestException(
895 "The request could not be processed because the provided "
896 + "content is not valid JSON", e)
897 .setDetail(new JsonValue(e.getMessage()));
898 } catch (final JsonMappingException e) {
899 if (allowEmpty) {
900 return null;
901 } else {
902 throw new BadRequestException("The request could not be processed "
903 + "because it did not contain any JSON content", e);
904 }
905 } catch (final IOException e) {
906 throw adapt(e);
907 } catch (final MessagingException e) {
908 throw new BadRequestException(
909 "The request could not be processed because it can't be parsed", e);
910 }
911 }
912
913 private static String param(final String field) {
914 return "_" + field;
915 }
916
917 private HttpUtils() {
918
919 }
920
921 private static class HttpServletRequestDataSource implements DataSource {
922 private org.forgerock.http.protocol.Request request;
923
924 HttpServletRequestDataSource(org.forgerock.http.protocol.Request request) {
925 this.request = request;
926 }
927
928 public InputStream getInputStream() throws IOException {
929 return request.getEntity().getRawContentInputStream();
930 }
931
932 public OutputStream getOutputStream() throws IOException {
933 return null;
934 }
935
936 public String getContentType() {
937 return request.getHeaders().getFirst(ContentTypeHeader.class);
938 }
939
940 public String getName() {
941 return "HttpServletRequestDataSource";
942 }
943 }
944
945 private static byte[] toByteArray(final InputStream inputStream) throws IOException {
946 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
947 final byte[] data = new byte[BUFFER_SIZE];
948 int size;
949 while ((size = inputStream.read(data)) != EOF) {
950 byteArrayOutputStream.write(data, 0, size);
951 }
952 byteArrayOutputStream.flush();
953 return byteArrayOutputStream.toByteArray();
954 }
955
956 static HttpContextFactory staticContextFactory(final Context parentContext) {
957 return new HttpContextFactory() {
958 @Override
959 public Context createContext(Context parent, org.forgerock.http.protocol.Request request) {
960 return parentContext;
961 }
962 };
963 }
964 }