Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : #include "BodyUtil.h"
8 :
9 : #include "nsError.h"
10 : #include "nsString.h"
11 : #include "nsIGlobalObject.h"
12 : #include "mozilla/Encoding.h"
13 :
14 : #include "nsCharSeparatedTokenizer.h"
15 : #include "nsDOMString.h"
16 : #include "nsNetUtil.h"
17 : #include "nsReadableUtils.h"
18 : #include "nsStreamUtils.h"
19 : #include "nsStringStream.h"
20 :
21 : #include "mozilla/ErrorResult.h"
22 : #include "mozilla/dom/Exceptions.h"
23 : #include "mozilla/dom/FetchUtil.h"
24 : #include "mozilla/dom/File.h"
25 : #include "mozilla/dom/FormData.h"
26 : #include "mozilla/dom/Headers.h"
27 : #include "mozilla/dom/Promise.h"
28 : #include "mozilla/dom/URLSearchParams.h"
29 :
30 : namespace mozilla {
31 : namespace dom {
32 :
33 : namespace {
34 :
35 : // Reads over a CRLF and positions start after it.
36 : static bool
37 0 : PushOverLine(nsACString::const_iterator& aStart,
38 : const nsACString::const_iterator& aEnd)
39 : {
40 0 : if (*aStart == nsCRT::CR && (aEnd - aStart > 1) && *(++aStart) == nsCRT::LF) {
41 0 : ++aStart; // advance to after CRLF
42 0 : return true;
43 : }
44 :
45 : return false;
46 : }
47 :
48 : class MOZ_STACK_CLASS FillFormIterator final
49 : : public URLParams::ForEachIterator
50 : {
51 : public:
52 0 : explicit FillFormIterator(FormData* aFormData)
53 0 : : mFormData(aFormData)
54 : {
55 0 : MOZ_ASSERT(aFormData);
56 0 : }
57 :
58 0 : bool URLParamsIterator(const nsAString& aName,
59 : const nsAString& aValue) override
60 : {
61 0 : ErrorResult rv;
62 0 : mFormData->Append(aName, aValue, rv);
63 0 : MOZ_ASSERT(!rv.Failed());
64 0 : return true;
65 : }
66 :
67 : private:
68 : FormData* mFormData;
69 : };
70 :
71 : /**
72 : * A simple multipart/form-data parser as defined in RFC 2388 and RFC 2046.
73 : * This does not respect any encoding specified per entry, using UTF-8
74 : * throughout. This is as the Fetch spec states in the consume body algorithm.
75 : * Borrows some things from Necko's nsMultiMixedConv, but is simpler since
76 : * unlike Necko we do not have to deal with receiving incomplete chunks of data.
77 : *
78 : * This parser will fail the entire parse on any invalid entry, so it will
79 : * never return a partially filled FormData.
80 : * The content-disposition header is used to figure out the name and filename
81 : * entries. The inclusion of the filename parameter decides if the entry is
82 : * inserted into the FormData as a string or a File.
83 : *
84 : * File blobs are copies of the underlying data string since we cannot adopt
85 : * char* chunks embedded within the larger body without significant effort.
86 : * FIXME(nsm): Bug 1127552 - We should add telemetry to calls to formData() and
87 : * friends to figure out if Fetch ends up copying big blobs to see if this is
88 : * worth optimizing.
89 : */
90 0 : class MOZ_STACK_CLASS FormDataParser
91 : {
92 : private:
93 : RefPtr<FormData> mFormData;
94 : nsCString mMimeType;
95 : nsCString mData;
96 :
97 : // Entry state, reset in START_PART.
98 : nsCString mName;
99 : nsCString mFilename;
100 : nsCString mContentType;
101 :
102 : enum
103 : {
104 : START_PART,
105 : PARSE_HEADER,
106 : PARSE_BODY,
107 : } mState;
108 :
109 : nsIGlobalObject* mParentObject;
110 :
111 : // Reads over a boundary and sets start to the position after the end of the
112 : // boundary. Returns false if no boundary is found immediately.
113 : bool
114 0 : PushOverBoundary(const nsACString& aBoundaryString,
115 : nsACString::const_iterator& aStart,
116 : nsACString::const_iterator& aEnd)
117 : {
118 : // We copy the end iterator to keep the original pointing to the real end
119 : // of the string.
120 0 : nsACString::const_iterator end(aEnd);
121 0 : const char* beginning = aStart.get();
122 0 : if (FindInReadable(aBoundaryString, aStart, end)) {
123 : // We either should find the body immediately, or after 2 chars with the
124 : // 2 chars being '-', everything else is failure.
125 0 : if ((aStart.get() - beginning) == 0) {
126 0 : aStart.advance(aBoundaryString.Length());
127 : return true;
128 : }
129 :
130 0 : if ((aStart.get() - beginning) == 2) {
131 0 : if (*(--aStart) == '-' && *(--aStart) == '-') {
132 0 : aStart.advance(aBoundaryString.Length() + 2);
133 : return true;
134 : }
135 : }
136 : }
137 :
138 : return false;
139 : }
140 :
141 : bool
142 0 : ParseHeader(nsACString::const_iterator& aStart,
143 : nsACString::const_iterator& aEnd,
144 : bool* aWasEmptyHeader)
145 : {
146 0 : nsAutoCString headerName, headerValue;
147 0 : if (!FetchUtil::ExtractHeader(aStart, aEnd,
148 : headerName, headerValue,
149 : aWasEmptyHeader)) {
150 : return false;
151 : }
152 0 : if (*aWasEmptyHeader) {
153 : return true;
154 : }
155 :
156 0 : if (headerName.LowerCaseEqualsLiteral("content-disposition")) {
157 0 : nsCCharSeparatedTokenizer tokenizer(headerValue, ';');
158 0 : bool seenFormData = false;
159 0 : while (tokenizer.hasMoreTokens()) {
160 0 : const nsDependentCSubstring& token = tokenizer.nextToken();
161 0 : if (token.IsEmpty()) {
162 : continue;
163 : }
164 :
165 0 : if (token.EqualsLiteral("form-data")) {
166 : seenFormData = true;
167 : continue;
168 : }
169 :
170 0 : if (seenFormData &&
171 0 : StringBeginsWith(token, NS_LITERAL_CSTRING("name="))) {
172 0 : mName = StringTail(token, token.Length() - 5);
173 0 : mName.Trim(" \"");
174 0 : continue;
175 : }
176 :
177 0 : if (seenFormData &&
178 0 : StringBeginsWith(token, NS_LITERAL_CSTRING("filename="))) {
179 0 : mFilename = StringTail(token, token.Length() - 9);
180 0 : mFilename.Trim(" \"");
181 0 : continue;
182 : }
183 : }
184 :
185 0 : if (mName.IsVoid()) {
186 : // Could not parse a valid entry name.
187 0 : return false;
188 : }
189 0 : } else if (headerName.LowerCaseEqualsLiteral("content-type")) {
190 0 : mContentType = headerValue;
191 : }
192 :
193 : return true;
194 : }
195 :
196 : // The end of a body is marked by a CRLF followed by the boundary. So the
197 : // CRLF is part of the boundary and not the body, but any prior CRLFs are
198 : // part of the body. This will position the iterator at the beginning of the
199 : // boundary (after the CRLF).
200 : bool
201 0 : ParseBody(const nsACString& aBoundaryString,
202 : nsACString::const_iterator& aStart,
203 : nsACString::const_iterator& aEnd)
204 : {
205 0 : const char* beginning = aStart.get();
206 :
207 : // Find the boundary marking the end of the body.
208 0 : nsACString::const_iterator end(aEnd);
209 0 : if (!FindInReadable(aBoundaryString, aStart, end)) {
210 : return false;
211 : }
212 :
213 : // We found a boundary, strip the just prior CRLF, and consider
214 : // everything else the body section.
215 0 : if (aStart.get() - beginning < 2) {
216 : // Only the first entry can have a boundary right at the beginning. Even
217 : // an empty body will have a CRLF before the boundary. So this is
218 : // a failure.
219 : return false;
220 : }
221 :
222 : // Check that there is a CRLF right before the boundary.
223 0 : aStart.advance(-2);
224 :
225 : // Skip optional hyphens.
226 0 : if (*aStart == '-' && *(aStart.get()+1) == '-') {
227 0 : if (aStart.get() - beginning < 2) {
228 : return false;
229 : }
230 :
231 0 : aStart.advance(-2);
232 : }
233 :
234 0 : if (*aStart != nsCRT::CR || *(aStart.get()+1) != nsCRT::LF) {
235 : return false;
236 : }
237 :
238 0 : nsAutoCString body(beginning, aStart.get() - beginning);
239 :
240 : // Restore iterator to after the \r\n as we promised.
241 : // We do not need to handle the extra hyphens case since our boundary
242 : // parser in PushOverBoundary()
243 0 : aStart.advance(2);
244 :
245 0 : if (!mFormData) {
246 0 : mFormData = new FormData();
247 : }
248 :
249 0 : NS_ConvertUTF8toUTF16 name(mName);
250 :
251 0 : if (mFilename.IsVoid()) {
252 0 : ErrorResult rv;
253 0 : mFormData->Append(name, NS_ConvertUTF8toUTF16(body), rv);
254 0 : MOZ_ASSERT(!rv.Failed());
255 : } else {
256 : // Unfortunately we've to copy the data first since all our strings are
257 : // going to free it. We also need fallible alloc, so we can't just use
258 : // ToNewCString().
259 0 : char* copy = static_cast<char*>(moz_xmalloc(body.Length()));
260 0 : if (!copy) {
261 0 : NS_WARNING("Failed to copy File entry body.");
262 0 : return false;
263 : }
264 0 : nsCString::const_iterator bodyIter, bodyEnd;
265 0 : body.BeginReading(bodyIter);
266 0 : body.EndReading(bodyEnd);
267 0 : char *p = copy;
268 0 : while (bodyIter != bodyEnd) {
269 0 : *p++ = *bodyIter++;
270 : }
271 0 : p = nullptr;
272 :
273 : RefPtr<Blob> file =
274 0 : File::CreateMemoryFile(mParentObject,
275 0 : reinterpret_cast<void *>(copy), body.Length(),
276 0 : NS_ConvertUTF8toUTF16(mFilename),
277 0 : NS_ConvertUTF8toUTF16(mContentType), /* aLastModifiedDate */ 0);
278 0 : Optional<nsAString> dummy;
279 0 : ErrorResult rv;
280 0 : mFormData->Append(name, *file, dummy, rv);
281 0 : if (NS_WARN_IF(rv.Failed())) {
282 0 : rv.SuppressException();
283 0 : return false;
284 : }
285 : }
286 :
287 : return true;
288 : }
289 :
290 : public:
291 0 : FormDataParser(const nsACString& aMimeType, const nsACString& aData, nsIGlobalObject* aParent)
292 0 : : mMimeType(aMimeType), mData(aData), mState(START_PART), mParentObject(aParent)
293 : {
294 0 : }
295 :
296 : bool
297 0 : Parse()
298 : {
299 0 : if (mData.IsEmpty()) {
300 : return false;
301 : }
302 :
303 : // Determine boundary from mimetype.
304 0 : const char* boundaryId = nullptr;
305 0 : boundaryId = strstr(mMimeType.BeginWriting(), "boundary");
306 0 : if (!boundaryId) {
307 : return false;
308 : }
309 :
310 0 : boundaryId = strchr(boundaryId, '=');
311 0 : if (!boundaryId) {
312 : return false;
313 : }
314 :
315 : // Skip over '='.
316 0 : boundaryId++;
317 :
318 0 : char *attrib = (char *) strchr(boundaryId, ';');
319 0 : if (attrib) *attrib = '\0';
320 :
321 0 : nsAutoCString boundaryString(boundaryId);
322 0 : if (attrib) *attrib = ';';
323 :
324 0 : boundaryString.Trim(" \"");
325 :
326 0 : if (boundaryString.Length() == 0) {
327 : return false;
328 : }
329 :
330 0 : nsACString::const_iterator start, end;
331 0 : mData.BeginReading(start);
332 : // This should ALWAYS point to the end of data.
333 : // Helpers make copies.
334 0 : mData.EndReading(end);
335 :
336 0 : while (start != end) {
337 0 : switch(mState) {
338 : case START_PART:
339 0 : mName.SetIsVoid(true);
340 0 : mFilename.SetIsVoid(true);
341 0 : mContentType = NS_LITERAL_CSTRING("text/plain");
342 :
343 : // MUST start with boundary.
344 0 : if (!PushOverBoundary(boundaryString, start, end)) {
345 0 : return false;
346 : }
347 :
348 0 : if (start != end && *start == '-') {
349 : // End of data.
350 0 : if (!mFormData) {
351 0 : mFormData = new FormData();
352 : }
353 : return true;
354 : }
355 :
356 0 : if (!PushOverLine(start, end)) {
357 : return false;
358 : }
359 0 : mState = PARSE_HEADER;
360 0 : break;
361 :
362 : case PARSE_HEADER:
363 : bool emptyHeader;
364 0 : if (!ParseHeader(start, end, &emptyHeader)) {
365 : return false;
366 : }
367 :
368 0 : if (emptyHeader && !PushOverLine(start, end)) {
369 : return false;
370 : }
371 :
372 0 : mState = emptyHeader ? PARSE_BODY : PARSE_HEADER;
373 0 : break;
374 :
375 : case PARSE_BODY:
376 0 : if (mName.IsVoid()) {
377 : NS_WARNING("No content-disposition header with a valid name was "
378 0 : "found. Failing at body parse.");
379 0 : return false;
380 : }
381 :
382 0 : if (!ParseBody(boundaryString, start, end)) {
383 : return false;
384 : }
385 :
386 0 : mState = START_PART;
387 0 : break;
388 :
389 : default:
390 0 : MOZ_CRASH("Invalid case");
391 : }
392 : }
393 :
394 0 : MOZ_ASSERT_UNREACHABLE("Should never reach here.");
395 : return false;
396 : }
397 :
398 : already_AddRefed<FormData> GetFormData()
399 : {
400 0 : return mFormData.forget();
401 : }
402 : };
403 : }
404 :
405 : // static
406 : void
407 10 : BodyUtil::ConsumeArrayBuffer(JSContext* aCx,
408 : JS::MutableHandle<JSObject*> aValue,
409 : uint32_t aInputLength, uint8_t* aInput,
410 : ErrorResult& aRv)
411 : {
412 1 : JS::Rooted<JSObject*> arrayBuffer(aCx);
413 1 : arrayBuffer = JS_NewArrayBufferWithContents(aCx, aInputLength,
414 1 : reinterpret_cast<void *>(aInput));
415 10 : if (!arrayBuffer) {
416 0 : JS_ClearPendingException(aCx);
417 0 : aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
418 0 : return;
419 : }
420 10 : aValue.set(arrayBuffer);
421 : }
422 :
423 : // static
424 : already_AddRefed<Blob>
425 0 : BodyUtil::ConsumeBlob(nsISupports* aParent, const nsString& aMimeType,
426 : uint32_t aInputLength, uint8_t* aInput,
427 : ErrorResult& aRv)
428 : {
429 : RefPtr<Blob> blob =
430 0 : Blob::CreateMemoryBlob(aParent,
431 : reinterpret_cast<void *>(aInput), aInputLength,
432 0 : aMimeType);
433 :
434 0 : if (!blob) {
435 0 : aRv.Throw(NS_ERROR_DOM_UNKNOWN_ERR);
436 : return nullptr;
437 : }
438 : return blob.forget();
439 : }
440 :
441 : // static
442 : already_AddRefed<FormData>
443 0 : BodyUtil::ConsumeFormData(nsIGlobalObject* aParent, const nsCString& aMimeType,
444 : const nsCString& aStr, ErrorResult& aRv)
445 : {
446 0 : NS_NAMED_LITERAL_CSTRING(formDataMimeType, "multipart/form-data");
447 :
448 : // Allow semicolon separated boundary/encoding suffix like multipart/form-data; boundary=
449 : // but disallow multipart/form-datafoobar.
450 0 : bool isValidFormDataMimeType = StringBeginsWith(aMimeType, formDataMimeType);
451 :
452 0 : if (isValidFormDataMimeType && aMimeType.Length() > formDataMimeType.Length()) {
453 0 : isValidFormDataMimeType = aMimeType[formDataMimeType.Length()] == ';';
454 : }
455 :
456 0 : if (isValidFormDataMimeType) {
457 0 : FormDataParser parser(aMimeType, aStr, aParent);
458 0 : if (!parser.Parse()) {
459 0 : aRv.ThrowTypeError<MSG_BAD_FORMDATA>();
460 : return nullptr;
461 : }
462 :
463 0 : RefPtr<FormData> fd = parser.GetFormData();
464 0 : MOZ_ASSERT(fd);
465 0 : return fd.forget();
466 : }
467 :
468 0 : NS_NAMED_LITERAL_CSTRING(urlDataMimeType, "application/x-www-form-urlencoded");
469 0 : bool isValidUrlEncodedMimeType = StringBeginsWith(aMimeType, urlDataMimeType);
470 :
471 0 : if (isValidUrlEncodedMimeType && aMimeType.Length() > urlDataMimeType.Length()) {
472 0 : isValidUrlEncodedMimeType = aMimeType[urlDataMimeType.Length()] == ';';
473 : }
474 :
475 0 : if (isValidUrlEncodedMimeType) {
476 0 : RefPtr<FormData> fd = new FormData(aParent);
477 0 : FillFormIterator iterator(fd);
478 0 : DebugOnly<bool> status = URLParams::Parse(aStr, iterator);
479 0 : MOZ_ASSERT(status);
480 :
481 0 : return fd.forget();
482 : }
483 :
484 0 : aRv.ThrowTypeError<MSG_BAD_FORMDATA>();
485 : return nullptr;
486 : }
487 :
488 : // static
489 : nsresult
490 0 : BodyUtil::ConsumeText(uint32_t aInputLength, uint8_t* aInput,
491 : nsString& aText)
492 : {
493 : nsresult rv =
494 0 : UTF_8_ENCODING->DecodeWithBOMRemoval(MakeSpan(aInput, aInputLength), aText);
495 9 : if (NS_FAILED(rv)) {
496 : return rv;
497 : }
498 9 : return NS_OK;
499 : }
500 :
501 : // static
502 : void
503 2 : BodyUtil::ConsumeJson(JSContext* aCx, JS::MutableHandle<JS::Value> aValue,
504 : const nsString& aStr, ErrorResult& aRv)
505 : {
506 0 : aRv.MightThrowJSException();
507 :
508 1 : JS::Rooted<JS::Value> json(aCx);
509 2 : if (!JS_ParseJSON(aCx, aStr.get(), aStr.Length(), &json)) {
510 0 : if (!JS_IsExceptionPending(aCx)) {
511 0 : aRv.Throw(NS_ERROR_DOM_UNKNOWN_ERR);
512 0 : return;
513 : }
514 :
515 0 : JS::Rooted<JS::Value> exn(aCx);
516 0 : DebugOnly<bool> gotException = JS_GetPendingException(aCx, &exn);
517 0 : MOZ_ASSERT(gotException);
518 :
519 0 : JS_ClearPendingException(aCx);
520 0 : aRv.ThrowJSException(aCx, exn);
521 : return;
522 : }
523 :
524 : aValue.set(json);
525 : }
526 :
527 : } // namespace dom
528 : } // namespace mozilla
|