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