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