├── .gitattributes ├── .eslintignore ├── test ├── fixture │ ├── file │ │ ├── plain.txt │ │ ├── funkyfilename.txt │ │ ├── blank.gif │ │ ├── pf1y5.png │ │ ├── beta-sticker-1.png │ │ ├── binaryfile.tar.gz │ │ └── menu_separator.png │ ├── http │ │ ├── filename │ │ │ ├── empty.http │ │ │ ├── generic.http │ │ │ ├── quotes.http │ │ │ ├── unquoted.http │ │ │ └── filename-name.http │ │ ├── special-chars-in-filename │ │ │ ├── info.md │ │ │ ├── issue-252-chrome.http │ │ │ ├── xp-ie-7.http │ │ │ ├── xp-ie-8.http │ │ │ ├── xp-safari-5.http │ │ │ ├── osx-safari-5.http │ │ │ ├── xp-chrome-12.http │ │ │ ├── osx-firefox-3.6.http │ │ │ └── osx-chrome-13.http │ │ ├── preamble │ │ │ ├── crlf.http │ │ │ └── preamble.http │ │ ├── content-type │ │ │ ├── custom-equal-sign.http │ │ │ ├── charset.http │ │ │ ├── custom.http │ │ │ └── charset-last.http │ │ └── encoding │ │ │ ├── plain.txt.http │ │ │ ├── blank.gif.http │ │ │ ├── binaryfile.tar.gz.http │ │ │ ├── menu_seperator.png.http │ │ │ └── beta-sticker-1.png.http │ ├── js │ │ ├── preamble.js │ │ ├── filename.js │ │ ├── content-type.js │ │ ├── special-chars-in-filename.js │ │ └── encoding.js │ └── multipart.js └── test.js ├── .gitignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── scorecard.yml │ └── ci.yml ├── .eslintrc.yml ├── LICENSE ├── package.json ├── examples ├── upload.js ├── azureblobstorage.js ├── progress.js └── s3.js ├── tool ├── record.js └── bench-multipart-parser.js ├── HISTORY.md ├── README.md └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.http binary 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /test/fixture/file/plain.txt: -------------------------------------------------------------------------------- 1 | I am a plain text file 2 | -------------------------------------------------------------------------------- /test/fixture/file/funkyfilename.txt: -------------------------------------------------------------------------------- 1 | I am a text file with a funky name! 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | coverage/ 4 | test/tmp/ 5 | npm-debug.log 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /test/fixture/file/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pillarjs/multiparty/HEAD/test/fixture/file/blank.gif -------------------------------------------------------------------------------- /test/fixture/file/pf1y5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pillarjs/multiparty/HEAD/test/fixture/file/pf1y5.png -------------------------------------------------------------------------------- /test/fixture/file/beta-sticker-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pillarjs/multiparty/HEAD/test/fixture/file/beta-sticker-1.png -------------------------------------------------------------------------------- /test/fixture/file/binaryfile.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pillarjs/multiparty/HEAD/test/fixture/file/binaryfile.tar.gz -------------------------------------------------------------------------------- /test/fixture/file/menu_separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pillarjs/multiparty/HEAD/test/fixture/file/menu_separator.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [{*.js,*.json,*.yml}] 10 | indent_size = 2 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /test/fixture/http/filename/empty.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryi3Xz4TKrYpgIdIpf 4 | Content-Length: 43 5 | 6 | ------WebKitFormBoundaryi3Xz4TKrYpgIdIpf-- 7 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/info.md: -------------------------------------------------------------------------------- 1 | * Opera does not allow submitting this file, it shows a warning to the 2 | user that the file could not be found instead. Tested in 9.8, 11.51 on OSX. 3 | Reported to Opera on 08.09.2011 (tracking email DSK-346009@bugs.opera.com). 4 | -------------------------------------------------------------------------------- /test/fixture/http/preamble/crlf.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----TLV0SrKD4z1TRxRhAPUvZ 4 | Content-Length: 184 5 | 6 | 7 | ------TLV0SrKD4z1TRxRhAPUvZ 8 | Content-Disposition: form-data; name="upload"; filename="plain.txt" 9 | Content-Type: text/plain 10 | 11 | I am a plain text file 12 | 13 | ------TLV0SrKD4z1TRxRhAPUvZ-- 14 | -------------------------------------------------------------------------------- /test/fixture/http/content-type/custom-equal-sign.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----=test; charset=utf-8 4 | Content-Length: 182 5 | 6 | ------=test 7 | Content-Disposition: form-data; name="file"; filename="plain.txt" 8 | Content-Type: text/plain 9 | Content-Transfer-Encoding: 7bit 10 | 11 | I am a plain text file 12 | 13 | ------=test-- 14 | -------------------------------------------------------------------------------- /test/fixture/js/preamble.js: -------------------------------------------------------------------------------- 1 | module.exports['crlf.http'] = [{ 2 | type: 'file', 3 | name: 'upload', 4 | filename: 'plain.txt', 5 | fixture: 'plain.txt', 6 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872' 7 | }] 8 | 9 | module.exports['preamble.http'] = [{ 10 | type: 'file', 11 | name: 'upload', 12 | filename: 'plain.txt', 13 | fixture: 'plain.txt', 14 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872' 15 | }] 16 | -------------------------------------------------------------------------------- /test/fixture/http/filename/generic.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytyE4wkKlZ5CQJVTG 4 | Content-Length: 200 5 | 6 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 7 | Content-Disposition: form-data; name="upload"; filename="" 8 | Content-Type: text/plain 9 | 10 | I am a plain text file 11 | 12 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG-- 13 | 14 | -------------------------------------------------------------------------------- /test/fixture/http/encoding/plain.txt.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----TLV0SrKD4z1TRxRhAPUvZ 4 | Content-Length: 213 5 | 6 | ------TLV0SrKD4z1TRxRhAPUvZ 7 | Content-Disposition: form-data; name="file"; filename="plain.txt" 8 | Content-Type: text/plain 9 | Content-Transfer-Encoding: 7bit 10 | 11 | I am a plain text file 12 | 13 | ------TLV0SrKD4z1TRxRhAPUvZ-- 14 | -------------------------------------------------------------------------------- /test/fixture/http/filename/quotes.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytyE4wkKlZ5CQJVTG 4 | Content-Length: 211 5 | 6 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 7 | Content-Disposition: form-data; name="upload"; filename="foo \"bar\"" 8 | Content-Type: text/plain 9 | 10 | I am a plain text file 11 | 12 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG-- 13 | 14 | -------------------------------------------------------------------------------- /test/fixture/http/filename/unquoted.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytyE4wkKlZ5CQJVTG 4 | Content-Length: 207 5 | 6 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 7 | Content-Disposition: form-data; name=upload; filename=foo_bar.txt 8 | Content-Type: text/plain 9 | 10 | I am a plain text file 11 | 12 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG-- 13 | 14 | -------------------------------------------------------------------------------- /test/fixture/http/filename/filename-name.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytyE4wkKlZ5CQJVTG 4 | Content-Length: 209 5 | 6 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 7 | Content-Disposition: form-data; filename="plain.txt"; name="upload" 8 | Content-Type: text/plain 9 | 10 | I am a plain text file 11 | 12 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG-- 13 | 14 | -------------------------------------------------------------------------------- /test/fixture/http/preamble/preamble.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----TLV0SrKD4z1TRxRhAPUvZ 4 | Content-Length: 226 5 | 6 | This is a preamble which should be ignored 7 | ------TLV0SrKD4z1TRxRhAPUvZ 8 | Content-Disposition: form-data; name="upload"; filename="plain.txt" 9 | Content-Type: text/plain 10 | 11 | I am a plain text file 12 | 13 | ------TLV0SrKD4z1TRxRhAPUvZ-- 14 | -------------------------------------------------------------------------------- /test/fixture/http/content-type/charset.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; charset=utf-8; boundary=----TLV0SrKD4z1TRxRhAPUvZ 4 | Content-Length: 213 5 | 6 | ------TLV0SrKD4z1TRxRhAPUvZ 7 | Content-Disposition: form-data; name="file"; filename="plain.txt" 8 | Content-Type: text/plain 9 | Content-Transfer-Encoding: 7bit 10 | 11 | I am a plain text file 12 | 13 | ------TLV0SrKD4z1TRxRhAPUvZ-- 14 | -------------------------------------------------------------------------------- /test/fixture/http/content-type/custom.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; custom=stuff; boundary=----TLV0SrKD4z1TRxRhAPUvZ 4 | Content-Length: 213 5 | 6 | ------TLV0SrKD4z1TRxRhAPUvZ 7 | Content-Disposition: form-data; name="file"; filename="plain.txt" 8 | Content-Type: text/plain 9 | Content-Transfer-Encoding: 7bit 10 | 11 | I am a plain text file 12 | 13 | ------TLV0SrKD4z1TRxRhAPUvZ-- 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | time: "23:00" 13 | timezone: Europe/London 14 | open-pull-requests-limit: 10 15 | ignore: 16 | - dependency-name: "*" 17 | update-types: ["version-update:semver-major"] 18 | -------------------------------------------------------------------------------- /test/fixture/http/content-type/charset-last.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----TLV0SrKD4z1TRxRhAPUvZ; charset=utf-8 4 | Content-Length: 213 5 | 6 | ------TLV0SrKD4z1TRxRhAPUvZ 7 | Content-Disposition: form-data; name="file"; filename="plain.txt" 8 | Content-Type: text/plain 9 | Content-Transfer-Encoding: 7bit 10 | 11 | I am a plain text file 12 | 13 | ------TLV0SrKD4z1TRxRhAPUvZ-- 14 | -------------------------------------------------------------------------------- /test/fixture/http/encoding/blank.gif.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ 4 | Content-Length: 323 5 | 6 | --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ 7 | Content-Disposition: form-data; name="file"; filename="blank.gif" 8 | Content-Type: image/gif 9 | Content-Transfer-Encoding: base64 10 | 11 | R0lGODlhAQABAJH/AP///wAAAMDAwAAAACH5BAEAAAIALAAAAAABAAEAAAICVAEAOw== 12 | --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/-- 13 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/issue-252-chrome.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Length: 363 4 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytyE4wkKlZ5CQJVTG 5 | 6 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 7 | Content-Disposition: form-data; name="title" 8 | 9 | Weird filename 10 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 11 | Content-Disposition: form-data; name="upload"; filename="JΛ̊KE_2023-02-25T16:44:24.129Z.txt" 12 | Content-Type: text/plain 13 | 14 | I am a text file with a funky name! 15 | 16 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG-- 17 | -------------------------------------------------------------------------------- /test/fixture/js/filename.js: -------------------------------------------------------------------------------- 1 | module.exports['empty.http'] = [ 2 | ]; 3 | 4 | module.exports['generic.http'] = [{ 5 | type: 'file', 6 | name: 'upload', 7 | filename: '', 8 | fixture: 'plain.txt', 9 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872' 10 | }] 11 | 12 | module.exports['filename-name.http'] = [{ 13 | type: 'file', 14 | name: 'upload', 15 | filename: 'plain.txt', 16 | fixture: 'plain.txt', 17 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872' 18 | }] 19 | 20 | module.exports['quotes.http'] = [{ 21 | type: 'file', 22 | name: 'upload', 23 | filename: 'foo "bar"', 24 | fixture: 'plain.txt', 25 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872' 26 | }] 27 | 28 | module.exports['unquoted.http'] = [{ 29 | type: 'file', 30 | name: 'upload', 31 | filename: 'foo_bar.txt', 32 | fixture: 'plain.txt', 33 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872' 34 | }] 35 | -------------------------------------------------------------------------------- /test/fixture/http/encoding/binaryfile.tar.gz.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ 4 | Content-Length: 676 5 | 6 | --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ 7 | Content-Disposition: form-data; name="file"; filename="binaryfile.tar.gz" 8 | Content-Type: application/x-gzip 9 | Content-Transfer-Encoding: base64 10 | 11 | H4sIAGiNIU8AA+3R0W6CMBQGYK59iobLZantRDG73osUOGqnFNJWM2N897UghG1ZdmWWLf93U/jP4bRAq8q92hJ/dY1J7kQEqyyLq8yXYrp2ltkqkTKXYiEykYc++ZTLVcLEvQ40dXReWcYSV1pdnL/v+6n+R11mjKVG1ZQ+s3TT2FpXqjhQ+hjzE1mnGxNLkgu+7tOKWjIVmVKTC6XL9ZaeXj4VQhwKWzL+cI4zwgQuuhkh3mhTad/Hkssh3im3027X54JnQ360R/M19OT8kC7SEN7Ooi2VvrEfznHQRWzl83gxttZKmzGehzPRW/+W8X+3fvL8sFet9sS6m3EIma02071MU3Uf9KHrmV1/+y8DAAAAAAAAAAAAAAAAAAAAAMB/9A6txIuJACgAAA== 12 | --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/-- 13 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/xp-ie-7.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, */* 3 | Referer: http://192.168.56.1:8080/ 4 | Accept-Language: de 5 | User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1) 6 | Content-Type: multipart/form-data; boundary=---------------------------7db1fe232017c 7 | Accept-Encoding: gzip, deflate 8 | Host: 192.168.56.1:8080 9 | Content-Length: 368 10 | Connection: Keep-Alive 11 | Cache-Control: no-cache 12 | 13 | -----------------------------7db1fe232017c 14 | Content-Disposition: form-data; name="title" 15 | 16 | Weird filename 17 | -----------------------------7db1fe232017c 18 | Content-Disposition: form-data; name="upload"; filename=" ? % * | " < > . ☃ ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 19 | Content-Type: application/octet-stream 20 | 21 | 22 | -----------------------------7db1fe232017c-- 23 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/xp-ie-8.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, */* 3 | Referer: http://192.168.56.1:8080/ 4 | Accept-Language: de 5 | User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0) 6 | Content-Type: multipart/form-data; boundary=---------------------------7db3a8372017c 7 | Accept-Encoding: gzip, deflate 8 | Host: 192.168.56.1:8080 9 | Content-Length: 368 10 | Connection: Keep-Alive 11 | Cache-Control: no-cache 12 | 13 | -----------------------------7db3a8372017c 14 | Content-Disposition: form-data; name="title" 15 | 16 | Weird filename 17 | -----------------------------7db3a8372017c 18 | Content-Disposition: form-data; name="upload"; filename=" ? % * | " < > . ☃ ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 19 | Content-Type: application/octet-stream 20 | 21 | 22 | -----------------------------7db3a8372017c-- 23 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - plugin:markdown/recommended 4 | plugins: 5 | - markdown 6 | overrides: 7 | - files: '**/*.md' 8 | processor: 'markdown/markdown' 9 | rules: 10 | brace-style: 11 | - error 12 | - 1tbs 13 | - allowSingleLine: true 14 | comma-dangle: error 15 | comma-style: 16 | - error 17 | - last 18 | eol-last: error 19 | indent: 20 | - error 21 | - 2 22 | - SwitchCase: 1 23 | no-multi-spaces: error 24 | no-param-reassign: error 25 | no-trailing-spaces: error 26 | no-unused-vars: 27 | - error 28 | - vars: all 29 | args: none 30 | ignoreRestSiblings: true 31 | no-useless-escape: error 32 | object-curly-spacing: 33 | - error 34 | - always 35 | padded-blocks: 36 | - error 37 | - blocks: never 38 | switches: never 39 | classes: never 40 | quotes: 41 | - error 42 | - single 43 | - avoidEscape: true 44 | allowTemplateLiterals: false 45 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/xp-safari-5.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: 192.168.56.1:8080 3 | Referer: http://192.168.56.1:8080/ 4 | Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 5 | Accept-Language: en-US 6 | Origin: http://192.168.56.1:8080 7 | User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 8 | Accept-Encoding: gzip, deflate 9 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykmaWSUbu697WN9TM 10 | Content-Length: 344 11 | Connection: keep-alive 12 | 13 | ------WebKitFormBoundarykmaWSUbu697WN9TM 14 | Content-Disposition: form-data; name="title" 15 | 16 | Weird filename 17 | ------WebKitFormBoundarykmaWSUbu697WN9TM 18 | Content-Disposition: form-data; name="upload"; filename=" ? % * | %22 < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 19 | Content-Type: text/plain 20 | 21 | 22 | ------WebKitFormBoundarykmaWSUbu697WN9TM-- 23 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/osx-safari-5.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Origin: http://localhost:8080 4 | Content-Length: 383 5 | User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1 6 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQJZ1gvhvdgfisJPJ 7 | Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 8 | Referer: http://localhost:8080/ 9 | Accept-Language: en-us 10 | Accept-Encoding: gzip, deflate 11 | Connection: keep-alive 12 | 13 | ------WebKitFormBoundaryQJZ1gvhvdgfisJPJ 14 | Content-Disposition: form-data; name="title" 15 | 16 | Weird filename 17 | ------WebKitFormBoundaryQJZ1gvhvdgfisJPJ 18 | Content-Disposition: form-data; name="upload"; filename=": \ ? % * | %22 < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 19 | Content-Type: text/plain 20 | 21 | I am a text file with a funky name! 22 | 23 | ------WebKitFormBoundaryQJZ1gvhvdgfisJPJ-- 24 | -------------------------------------------------------------------------------- /test/fixture/js/content-type.js: -------------------------------------------------------------------------------- 1 | module.exports['charset.http'] = [ 2 | { 3 | type: 'file', 4 | name: 'file', 5 | filename: 'plain.txt', 6 | fixture: 'plain.txt', 7 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872', 8 | size: 23 9 | } 10 | ]; 11 | 12 | module.exports['charset-last.http'] = [ 13 | { 14 | type: 'file', 15 | name: 'file', 16 | filename: 'plain.txt', 17 | fixture: 'plain.txt', 18 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872', 19 | size: 23 20 | } 21 | ]; 22 | 23 | module.exports['custom.http'] = [ 24 | { 25 | type: 'file', 26 | name: 'file', 27 | filename: 'plain.txt', 28 | fixture: 'plain.txt', 29 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872', 30 | size: 23 31 | } 32 | ]; 33 | 34 | 35 | // to test the regexp modification : it should accepts equal sign in boundary 36 | module.exports['custom-equal-sign.http'] = [ 37 | { 38 | type: 'file', 39 | name: 'file', 40 | filename: 'plain.txt', 41 | fixture: 'plain.txt', 42 | size: 24 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/xp-chrome-12.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: 192.168.56.1:8080 3 | Connection: keep-alive 4 | Referer: http://192.168.56.1:8080/ 5 | Content-Length: 344 6 | Cache-Control: max-age=0 7 | Origin: http://192.168.56.1:8080 8 | User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.122 Safari/534.30 9 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryEvqBNplR3ByrwQPa 10 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 11 | Accept-Encoding: gzip,deflate,sdch 12 | Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 13 | Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 14 | 15 | ------WebKitFormBoundaryEvqBNplR3ByrwQPa 16 | Content-Disposition: form-data; name="title" 17 | 18 | Weird filename 19 | ------WebKitFormBoundaryEvqBNplR3ByrwQPa 20 | Content-Disposition: form-data; name="upload"; filename=" ? % * | %22 < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 21 | Content-Type: text/plain 22 | 23 | 24 | ------WebKitFormBoundaryEvqBNplR3ByrwQPa-- 25 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/osx-firefox-3.6.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.22) Gecko/20110902 Firefox/3.6.22 4 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 5 | Accept-Language: en-us,en;q=0.5 6 | Accept-Encoding: gzip,deflate 7 | Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 8 | Keep-Alive: 115 9 | Connection: keep-alive 10 | Referer: http://localhost:8080/ 11 | Content-Type: multipart/form-data; boundary=---------------------------9849436581144108930470211272 12 | Content-Length: 438 13 | 14 | -----------------------------9849436581144108930470211272 15 | Content-Disposition: form-data; name="title" 16 | 17 | Weird filename 18 | -----------------------------9849436581144108930470211272 19 | Content-Disposition: form-data; name="upload"; filename=": \ ? % * | " < > . ☃ ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 20 | Content-Type: text/plain 21 | 22 | I am a text file with a funky name! 23 | 24 | -----------------------------9849436581144108930470211272-- 25 | -------------------------------------------------------------------------------- /test/fixture/js/special-chars-in-filename.js: -------------------------------------------------------------------------------- 1 | var properFilename = 'funkyfilename.txt'; 2 | 3 | function expect(filename) { 4 | return [ 5 | { 6 | type: 'field', 7 | name: 'title', 8 | value: 'Weird filename' 9 | }, 10 | { 11 | type: 'file', 12 | name: 'upload', 13 | filename: filename, 14 | fixture: properFilename 15 | } 16 | ]; 17 | } 18 | 19 | var webkit = " ? % * | \" < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt"; 20 | var ffOrIe = " ? % * | \" < > . ☃ ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt"; 21 | 22 | module.exports = { 23 | 'issue-252-chrome.http' : [ 24 | { 25 | type: 'field', 26 | name: 'title', 27 | value: 'Weird filename' 28 | }, 29 | { 30 | type: 'file', 31 | name: 'upload', 32 | filename: 'JΛ̊KE_2023-02-25T16:44:24.129Z.txt' 33 | } 34 | ], 35 | 'osx-chrome-13.http' : expect(webkit), 36 | 'osx-firefox-3.6.http' : expect(ffOrIe), 37 | 'osx-safari-5.http' : expect(webkit), 38 | 'xp-chrome-12.http' : expect(webkit), 39 | 'xp-ie-7.http' : expect(ffOrIe), 40 | 'xp-ie-8.http' : expect(ffOrIe), 41 | 'xp-safari-5.http' : expect(webkit) 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/osx-chrome-13.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Connection: keep-alive 4 | Referer: http://localhost:8080/ 5 | Content-Length: 383 6 | Cache-Control: max-age=0 7 | Origin: http://localhost:8080 8 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1 9 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytyE4wkKlZ5CQJVTG 10 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 11 | Accept-Encoding: gzip,deflate,sdch 12 | Accept-Language: en-US,en;q=0.8 13 | Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 14 | Cookie: jqCookieJar_tablesorter=%7B%22showListTable%22%3A%5B%5B5%2C1%5D%2C%5B1%2C0%5D%5D%7D 15 | 16 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 17 | Content-Disposition: form-data; name="title" 18 | 19 | Weird filename 20 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 21 | Content-Disposition: form-data; name="upload"; filename=": \ ? % * | %22 < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 22 | Content-Type: text/plain 23 | 24 | I am a text file with a funky name! 25 | 26 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG-- 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Felix Geisendörfer 4 | Copyright (c) 2014 Andrew Kelley 5 | Copyright (c) 2014 Douglas Christopher Wilson 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiparty", 3 | "description": "multipart/form-data parser which supports streaming", 4 | "version": "4.2.3", 5 | "author": "Andrew Kelley ", 6 | "contributors": [ 7 | "Douglas Christopher Wilson ", 8 | "Felix Geisendörfer " 9 | ], 10 | "license": "MIT", 11 | "keywords": [ 12 | "file", 13 | "upload", 14 | "formidable", 15 | "stream", 16 | "s3" 17 | ], 18 | "repository": "pillarjs/multiparty", 19 | "dependencies": { 20 | "http-errors": "2.0.0", 21 | "safe-buffer": "5.2.1", 22 | "uid-safe": "2.1.5" 23 | }, 24 | "devDependencies": { 25 | "eslint": "8.42.0", 26 | "eslint-plugin-markdown": "3.0.0", 27 | "mocha": "10.2.0", 28 | "nyc": "15.1.0", 29 | "pend": "1.2.0", 30 | "require-all": "3.0.0", 31 | "rimraf": "2.6.3", 32 | "superagent": "3.8.3" 33 | }, 34 | "files": [ 35 | "HISTORY.md", 36 | "LICENSE", 37 | "README.md", 38 | "index.js" 39 | ], 40 | "engines": { 41 | "node": ">= 0.10" 42 | }, 43 | "scripts": { 44 | "lint": "eslint .", 45 | "test": "mocha --reporter spec --bail --check-leaks test/", 46 | "test-ci": "nyc --reporter=lcov --reporter=text npm test", 47 | "test-cov": "nyc --reporter=html --reporter=text npm test" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/fixture/js/encoding.js: -------------------------------------------------------------------------------- 1 | module.exports['menu_seperator.png.http'] = [ 2 | { 3 | type: 'file', 4 | name: 'image', 5 | filename: 'menu_separator.png', 6 | fixture: 'menu_separator.png', 7 | sha1: 'c845ca3ea794be298f2a1b79769b71939eaf4e54', 8 | size: 931 9 | } 10 | ]; 11 | 12 | module.exports['beta-sticker-1.png.http'] = [ 13 | { 14 | type: 'file', 15 | name: 'sticker', 16 | filename: 'beta-sticker-1.png', 17 | fixture: 'beta-sticker-1.png', 18 | sha1: '6abbcffd12b4ada5a6a084fe9e4584f846331bc4', 19 | size: 1660 20 | } 21 | ]; 22 | 23 | module.exports['blank.gif.http'] = [ 24 | { 25 | type: 'file', 26 | name: 'file', 27 | filename: 'blank.gif', 28 | fixture: 'blank.gif', 29 | sha1: 'a1fdee122b95748d81cee426d717c05b5174fe96', 30 | size: 49 31 | } 32 | ]; 33 | 34 | module.exports['binaryfile.tar.gz.http'] = [ 35 | { 36 | type: 'file', 37 | name: 'file', 38 | filename: 'binaryfile.tar.gz', 39 | fixture: 'binaryfile.tar.gz', 40 | sha1: 'cfabe13b348e5e69287d677860880c52a69d2155', 41 | size: 301 42 | } 43 | ]; 44 | 45 | module.exports['plain.txt.http'] = [ 46 | { 47 | type: 'file', 48 | name: 'file', 49 | filename: 'plain.txt', 50 | fixture: 'plain.txt', 51 | sha1: 'b31d07bac24ac32734de88b3687dddb10e976872', 52 | size: 23 53 | } 54 | ]; 55 | -------------------------------------------------------------------------------- /examples/upload.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var multiparty = require('../') 3 | var util = require('util') 4 | 5 | var PORT = process.env.PORT || 27372 6 | 7 | var server = http.createServer(function(req, res) { 8 | if (req.url === '/') { 9 | res.writeHead(200, { 'content-type': 'text/html' }) 10 | res.end( 11 | '
'+ 12 | '
'+ 13 | '
'+ 14 | ''+ 15 | '
' 16 | ); 17 | } else if (req.url === '/upload') { 18 | var form = new multiparty.Form(); 19 | 20 | form.parse(req, function(err, fields, files) { 21 | if (err) { 22 | res.writeHead(400, { 'content-type': 'text/plain' }) 23 | res.end('invalid request: ' + err.message) 24 | return; 25 | } 26 | res.writeHead(200, { 'content-type': 'text/plain' }) 27 | res.write('received fields:\n\n '+util.inspect(fields)); 28 | res.write('\n\n'); 29 | res.end('received files:\n\n '+util.inspect(files)); 30 | }); 31 | } else { 32 | res.writeHead(404, { 'content-type': 'text/plain' }) 33 | res.end('404'); 34 | } 35 | }); 36 | server.listen(PORT, function() { 37 | console.info('listening on http://0.0.0.0:'+PORT+'/'); 38 | }); 39 | -------------------------------------------------------------------------------- /examples/azureblobstorage.js: -------------------------------------------------------------------------------- 1 | var azure = require('azure') 2 | var http = require('http') 3 | var multiparty = require('../') 4 | 5 | var PORT = process.env.PORT || 27372; 6 | 7 | var server = http.createServer(function(req, res) { 8 | if (req.url === '/') { 9 | res.writeHead(200, { 'content-type': 'text/html' }) 10 | res.end( 11 | '
'+ 12 | '
'+ 13 | '
'+ 14 | ''+ 15 | '
' 16 | ); 17 | } else if (req.url === '/upload') { 18 | var blobService = azure.createBlobService(); 19 | var form = new multiparty.Form(); 20 | 21 | form.on('part', function(part) { 22 | if (!part.filename) return; 23 | 24 | var size = part.byteCount; 25 | var name = part.filename; 26 | var container = 'blobContainerName'; 27 | 28 | blobService.createBlockBlobFromStream(container, name, part, size, function(error) { 29 | if (error) { 30 | // error handling 31 | res.status(500).send('Error uploading file'); 32 | } 33 | res.send('File uploaded successfully'); 34 | }); 35 | }); 36 | 37 | form.parse(req); 38 | } 39 | }); 40 | 41 | server.listen(PORT, function() { 42 | console.info('listening on http://0.0.0.0:' + PORT + '/'); 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /tool/record.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var fs = require('fs'); 3 | var connections = 0; 4 | 5 | var server = http.createServer(function(req, res) { 6 | var socket = req.socket; 7 | console.log('Request: %s %s -> %s', req.method, req.url, socket.filename); 8 | 9 | req.on('end', function() { 10 | if (req.url !== '/') { 11 | res.end(JSON.stringify({ 12 | method: req.method, 13 | url: req.url, 14 | filename: socket.filename 15 | })); 16 | return; 17 | } 18 | 19 | res.writeHead(200, { 'content-type': 'text/html' }) 20 | res.end( 21 | '
'+ 22 | '
'+ 23 | '
'+ 24 | ''+ 25 | '
' 26 | ); 27 | }); 28 | }); 29 | 30 | server.on('connection', function(socket) { 31 | connections++; 32 | 33 | socket.id = connections; 34 | socket.filename = 'connection-' + socket.id + '.http'; 35 | socket.file = fs.createWriteStream(socket.filename); 36 | socket.pipe(socket.file); 37 | 38 | console.log('--> %s', socket.filename); 39 | socket.on('close', function() { 40 | console.log('<-- %s', socket.filename); 41 | }); 42 | }); 43 | 44 | var port = process.env.PORT || 8080; 45 | server.listen(port, function() { 46 | console.log('Recording connections on port %s', port); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/progress.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var multiparty = require('../') 3 | var util = require('util') 4 | 5 | var PORT = process.env.PORT || 8080 6 | 7 | var server = http.createServer(function (req, res) { 8 | if (req.url === '/') { 9 | res.writeHead(200, { 'content-type': 'text/html' }) 10 | res.end( 11 | '
' + 12 | '
' + 13 | '
' + 14 | '' + 15 | '
' 16 | ) 17 | } else if (req.url === '/upload') { 18 | var form = new multiparty.Form() 19 | 20 | form.on('progress', function (bytesReceived, bytesExpected) { 21 | if (bytesExpected === null) { 22 | return 23 | } 24 | 25 | var percentComplete = (bytesReceived / bytesExpected) * 100 26 | console.log('the form is ' + Math.floor(percentComplete) + '%' + ' complete') 27 | }) 28 | 29 | form.parse(req, function (err, fields, files) { 30 | if (err) { 31 | res.writeHead(400, { 'content-type': 'text/plain' }) 32 | res.end('invalid request: ' + err.message) 33 | return 34 | } 35 | res.writeHead(200, { 'content-type': 'text/plain' }) 36 | res.write('received fields:\n\n ' + util.inspect(fields)) 37 | res.write('\n\n') 38 | res.end('received files:\n\n ' + util.inspect(files)) 39 | }) 40 | } else { 41 | res.writeHead(404, { 'content-type': 'text/plain' }) 42 | res.end('404') 43 | } 44 | }) 45 | server.listen(PORT, function () { 46 | console.info('listening on http://0.0.0.0:' + PORT + '/') 47 | }) 48 | -------------------------------------------------------------------------------- /test/fixture/http/encoding/menu_seperator.png.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ 4 | Content-Length: 1509 5 | 6 | --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ 7 | Content-Disposition: form-data; name="image"; filename="menu_separator.png" 8 | Content-Type: image/png 9 | Content-Transfer-Encoding: base64 10 | 11 | iVBORw0KGgoAAAANSUhEUgAAAAIAAAAYCAIAAABfmbuOAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDcxODNBNzJERDcyMTFFMUFBOEVFNDQzOTA0MDJDMjQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDcxODNBNzNERDcyMTFFMUFBOEVFNDQzOTA0MDJDMjQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowNzE4M0E3MERENzIxMUUxQUE4RUU0NDM5MDQwMkMyNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowNzE4M0E3MURENzIxMUUxQUE4RUU0NDM5MDQwMkMyNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pmvhbb8AAAAXSURBVHjaYnHk9PON8WJiAIPBSwEEGAAPrgG+VozFWgAAAABJRU5ErkJggg== 12 | --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/-- 13 | -------------------------------------------------------------------------------- /tool/bench-multipart-parser.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var Buffer = require('safe-buffer').Buffer 3 | var Form = require('../').Form 4 | 5 | var BOUNDARY = '-----------------------------168072824752491622650073' 6 | var SIZE_MB = 100 7 | var BUFFER = createMultipartBuffer(BOUNDARY, SIZE_MB * 1024 * 1024) 8 | 9 | var callbacks = { 10 | partBegin: -1, 11 | partEnd: -1, 12 | headerField: -1, 13 | headerValue: -1, 14 | partData: -1, 15 | end: -1 16 | }; 17 | 18 | var form = new Form({ boundary: BOUNDARY }); 19 | 20 | hijack('onParseHeaderField', function() { 21 | callbacks.headerField++; 22 | }); 23 | 24 | hijack('onParseHeaderValue', function() { 25 | callbacks.headerValue++; 26 | }); 27 | 28 | hijack('onParsePartBegin', function() { 29 | callbacks.partBegin++; 30 | }); 31 | 32 | hijack('onParsePartData', function() { 33 | callbacks.partData++; 34 | }); 35 | 36 | hijack('onParsePartEnd', function() { 37 | callbacks.partEnd++; 38 | }); 39 | 40 | form.on('finish', function() { 41 | callbacks.end++; 42 | }); 43 | 44 | var start = new Date(); 45 | form.write(BUFFER, function(err) { 46 | var duration = new Date() - start; 47 | assert.ifError(err); 48 | var mbPerSec = (SIZE_MB / (duration / 1000)).toFixed(2); 49 | console.log(mbPerSec+' mb/sec'); 50 | }); 51 | 52 | process.on('exit', function() { 53 | for (var k in callbacks) { 54 | assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]); 55 | } 56 | }); 57 | 58 | function createMultipartBuffer(boundary, size) { 59 | var buffer = Buffer.alloc(size) 60 | var head = 61 | '--'+boundary+'\r\n' + 62 | 'content-disposition: form-data; name="field1"\r\n' + 63 | '\r\n' 64 | var tail = '\r\n--'+boundary+'--\r\n' 65 | 66 | buffer.write(head, 'ascii', 0); 67 | buffer.write(tail, 'ascii', buffer.length - tail.length); 68 | return buffer; 69 | } 70 | 71 | function hijack(name, fn) { 72 | var oldFn = form[name]; 73 | form[name] = function() { 74 | fn(); 75 | return oldFn.apply(this, arguments); 76 | }; 77 | } 78 | 79 | -------------------------------------------------------------------------------- /examples/s3.js: -------------------------------------------------------------------------------- 1 | if (!process.env.S3_BUCKET || !process.env.S3_KEY || !process.env.S3_SECRET) { 2 | console.log('To run this example, do this:') 3 | console.log('npm install aws-sdk') 4 | console.log('S3_BUCKET="(your s3 bucket)" S3_KEY="(your s3 key)" S3_SECRET="(your s3 secret) node examples/s3.js"'); 5 | process.exit(1); 6 | } 7 | 8 | var http = require('http'); 9 | var multiparty = require('../'); 10 | var AWS = require('aws-sdk'); 11 | var PORT = process.env.PORT || 27372; 12 | 13 | var bucket = process.env.S3_BUCKET; 14 | var s3Client = new AWS.S3({ 15 | accessKeyId: process.env.S3_KEY, 16 | secretAccessKey: process.env.S3_SECRET 17 | // See: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#constructor-property 18 | }); 19 | 20 | var server = http.createServer(function(req, res) { 21 | if (req.url === '/') { 22 | res.writeHead(200, { 'content-type': 'text/html' }) 23 | res.end( 24 | '
'+ 25 | '
'+ 26 | '
'+ 27 | ''+ 28 | '
' 29 | ); 30 | } else if (req.url === '/upload') { 31 | var form = new multiparty.Form(); 32 | var destPath; 33 | form.on('field', function(name, value) { 34 | if (name === 'path') { 35 | destPath = value; 36 | } 37 | }); 38 | form.on('part', function(part) { 39 | s3Client.putObject({ 40 | Bucket: bucket, 41 | Key: destPath, 42 | ACL: 'public-read', 43 | Body: part, 44 | ContentLength: part.byteCount 45 | }, function(err, data) { 46 | if (err) throw err; 47 | console.log('done', data) 48 | res.end('OK') 49 | console.log('https://s3.amazonaws.com/' + bucket + '/' + destPath) 50 | }); 51 | }); 52 | form.parse(req); 53 | } else { 54 | res.writeHead(404, { 'content-type': 'text/plain' }) 55 | res.end('404'); 56 | } 57 | }); 58 | server.listen(PORT, function() { 59 | console.info('listening on http://0.0.0.0:'+PORT+'/'); 60 | }); 61 | -------------------------------------------------------------------------------- /test/fixture/multipart.js: -------------------------------------------------------------------------------- 1 | exports['rfc1867'] = 2 | { boundary: 'AaB03x', 3 | raw: 4 | '--AaB03x\r\n'+ 5 | 'content-disposition: form-data; name="field1"\r\n'+ 6 | '\r\n'+ 7 | 'Joe Blow\r\nalmost tricked you!\r\n'+ 8 | '--AaB03x\r\n'+ 9 | 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+ 10 | 'Content-Type: text/plain\r\n'+ 11 | '\r\n'+ 12 | '... contents of file1.txt ...\r\r\n'+ 13 | '--AaB03x--\r\n', 14 | parts: 15 | [ 16 | { 17 | headers: { 18 | 'content-disposition': 'form-data; name="field1"' 19 | }, 20 | data: 'Joe Blow\r\nalmost tricked you!' 21 | }, 22 | { 23 | headers: { 24 | 'content-disposition': 'form-data; name="pics"; filename="file1.txt"', 25 | 'Content-Type': 'text/plain' 26 | }, 27 | data: '... contents of file1.txt ...\r' 28 | } 29 | ] 30 | }; 31 | 32 | exports['noTrailing\r\n'] = 33 | { boundary: 'AaB03x', 34 | raw: 35 | '--AaB03x\r\n'+ 36 | 'content-disposition: form-data; name="field1"\r\n'+ 37 | '\r\n'+ 38 | 'Joe Blow\r\nalmost tricked you!\r\n'+ 39 | '--AaB03x\r\n'+ 40 | 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+ 41 | 'Content-Type: text/plain\r\n'+ 42 | '\r\n'+ 43 | '... contents of file1.txt ...\r\r\n'+ 44 | '--AaB03x--', 45 | parts: 46 | [ 47 | { 48 | headers: { 49 | 'content-disposition': 'form-data; name="field1"' 50 | }, 51 | data: 'Joe Blow\r\nalmost tricked you!' 52 | }, 53 | { 54 | headers: { 55 | 'content-disposition': 'form-data; name="pics"; filename="file1.txt"', 56 | 'Content-Type': 'text/plain' 57 | }, 58 | data: '... contents of file1.txt ...\r' 59 | } 60 | ] 61 | }; 62 | 63 | exports['emptyHeader'] = 64 | { boundary: 'AaB03x', 65 | raw: 66 | '--AaB03x\r\n'+ 67 | 'content-disposition: form-data; name="field1"\r\n'+ 68 | ': foo\r\n'+ 69 | '\r\n'+ 70 | 'Joe Blow\r\nalmost tricked you!\r\n'+ 71 | '--AaB03x\r\n'+ 72 | 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+ 73 | 'Content-Type: text/plain\r\n'+ 74 | '\r\n'+ 75 | '... contents of file1.txt ...\r\r\n'+ 76 | '--AaB03x--\r\n', 77 | expectError: true 78 | }; 79 | -------------------------------------------------------------------------------- /test/fixture/http/encoding/beta-sticker-1.png.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ 4 | Content-Length: 2483 5 | 6 | --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ 7 | Content-Disposition: form-data; name="sticker"; filename="beta-sticker-1.png" 8 | Content-Type: image/png 9 | Content-Transfer-Encoding: base64 10 | 11 | iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABh5JREFUeNrMmHtIHEcYwGfv5SNwaovxEanEiJKqlYCCTRo1f0SvDeof1legEcE/YttQaNOiaQjYFFtpKaJILZU8SCRUWqlJGpoWepGLTXqUEnzFxCrnK9DEelbvvPOe/WacuY7r7HmGFjrwsbNzt7u//V7zfYvQ/2xI/9K1/NyvMP9PgCTuGmmL6/0ckD9UOGmbIExUsqMkAPHJjv5QwKRtgKioqDlh5+w/7IFeCuLlxCeA2zQ0IcCwh2qoaLH09fUdTElJ2e/1elU+n0/y+9fvPz4+fvfYsWN3YOoBcXPiocLghD4mBYHhQTCErqWlZU9FRcXJqKiowyqVSk/uSEH4o8fjWVlYWDB2d3e3d3R0WGB5jYqLg/NyGgsKxMNgkDB4451NTU3vxcXF1SlBKB0tFsuVxsbGjlu3bj2GJQeIk8K5RVBqBTMxrYRfuHAh9/jx4+ejo6MPS9I6f6hHPOC6rOLi4vyVlZXf7t27Z5c5/iZfkgMxxyUwFy9ezC0tLe3V6XRJ/MOCAYjWwsLCni0oKCh98uSJaWhoyMZFn0/uT2qBqYi/1NbWxjc0NJwPFUYExc/B53R5eXk5ZrN5YH5+3slFn5+D2uBDzG90IJETExOtzGdC9RelNf78wYMH3xQWFn4Ep0sgyyCr1NmJP6kEIa5tbW3dEx8fXxeKRoJpT76OR3p6enllZWUKTCOwNalFAglWDkTCvLq6+uR2YYKZSw4GQVKNfZQCafjkqhKYTBsTE3NY/uYi2Q4MP5KTkw9QGB3VEMv6G/YioqFLly5lazQavfytxobnUW+PWTGisIyNPEL3QYLB4PPIyMi4EydO7JUBbTIZ0RDYOFPkE8t/OdHczCK6Y/qdzP8BfUTW8Tj/uQndvT1F5vOzVvTLz1PwX4cQbt++fekURsNpSNLIw16v1z/HLsRRgecsSnovm8nxs5bvUe+NN1Bz47fkfBaAXj2aA2BWEsM/3hhFX1/5Fe3NTEAfvn8NXTO+tSH68IiNjU2Qw/AmCzg2XCQp+YyhJAu9c+pl9GJ+KmhiEt38bhjpoyJQRtYudA60k3dwD6o4mouKjmSiolcy0ArRqnXz3rT+knwFEShhNKLNlmmFP7Kf8XxuehHpj0QQmLdPGch/ioYyCSAe57pMaHnJgcprctDdwUkRjKi8CUTWhipvbm7uvlJo3zFNoHJDOznPeGEXqn+9EBUf+AQZXvqU+BEG/KCpHz2flYh+ALO9++ZX5L/Mj3gfevjw4ZRoP+PzD/b4HadPn844c+aMkb0F1DqIz9byzBvquXytvr6+7vr16+Ow9CfN2njjdfFAWpo9o2FnNmm12kQMw24gcvSnhbHb7Y+huHsNlhapLNHSxK3idlq287qhhrkKlSByOBzIZrPhGyCn04ncbjfRGAMV5ZlQxvDw8E+yYi1Q3qpleYjUQlNTU5aysrJqgNBhIAwGVSDCkFj48BVFULA1eCl7XV3dx1CKYK3YqKnY7u9Ti2royclJ76FDh1YhxefgsoFpCIOtra0RuGBQwYbRaLzc1dVlpjA2ZiqmKbWsDAmEYU9Pz8Tg4OCNoqKixNTU1BQostDq6iqBcrlcRBiYfEff1KBR+OnpabPBYOikWlnhtOOWm0zUffpnZ2ednZ2dJtCYMTs7+xkA2x0eHk6gsMYwFPYr/EC1Wo2LMEWzWa1WC1QRZ8FUVgpj42ohD3umWqHjRFxf5RkZGVkCNQ9CcTWQn5+flpSUtBOiMKAt7Fek/FSAmpmZMVdVVZ0dGxv7g4PhteMVlbBIofv0sh4Lbmhtb2+/Cbv1eFpaWmJCQsJODMO0hGGgUghAAay9v7//i5KSki9lmmG+4+Jg/MHaIH6f0dCkqaNFFc5VkViam5v319TUNEDdvRubEGsNYHGqsAwMDFxta2u7DdpdpA+3c+LgWiHfVkCiFnpDw0iLqwgqO6BVKoPo00K6WIDsOzE6OrpE395FzeLgxMn5jVe0dYTa26s5jfFg4VR0nAuwNtrFda1rgmToD6VzVWq3eTPyYAxOwwH5gvT2PiWY7X4fUgJTywp1fivyyL6E+Lb6XvQ0X9AkBeeXZED+p/k+9LcAAwAXm3hBLzoZPAAAAABJRU5ErkJggg== 12 | --\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/-- 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["javascript"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 72 | with: 73 | category: "/language:${{matrix.language}}" 74 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | 7 | on: 8 | # For Branch-Protection check. Only the default branch is supported. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 10 | branch_protection_rule: 11 | # To guarantee Maintained check is occasionally updated. See 12 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 13 | schedule: 14 | - cron: '16 21 * * 1' 15 | push: 16 | branches: [ "master" ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | # Uncomment the permissions below if installing in a private repository. 31 | # contents: read 32 | # actions: read 33 | 34 | steps: 35 | - name: "Checkout code" 36 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 42 | with: 43 | results_file: results.sarif 44 | results_format: sarif 45 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 46 | # - you want to enable the Branch-Protection check on a *public* repository, or 47 | # - you are installing Scorecard on a *private* repository 48 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 49 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 50 | 51 | # Public repositories: 52 | # - Publish results to OpenSSF REST API for easy access by consumers 53 | # - Allows the repository to include the Scorecard badge. 54 | # - See https://github.com/ossf/scorecard-action#publishing-results. 55 | # For private repositories: 56 | # - `publish_results` will always be set to `false`, regardless 57 | # of the value entered here. 58 | publish_results: true 59 | 60 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 61 | # format to the repository Actions tab. 62 | - name: "Upload artifact" 63 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 64 | with: 65 | name: SARIF file 66 | path: results.sarif 67 | retention-days: 5 68 | 69 | # Upload the results to GitHub's code scanning dashboard. 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | permissions: 13 | checks: write # for coverallsapp/github-action to create new checks 14 | contents: read # for actions/checkout to fetch code 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | name: 19 | - Node.js 0.10 20 | - Node.js 0.12 21 | - Node.js 4.x 22 | - Node.js 6.x 23 | - Node.js 8.x 24 | - Node.js 10.x 25 | - Node.js 12.x 26 | - Node.js 14.x 27 | - Node.js 16.x 28 | - Node.js 18.x 29 | - Node.js 20.x 30 | 31 | include: 32 | - name: Node.js 0.10 33 | node-version: "0.10" 34 | npm-i: mocha@3.5.3 nyc@10.3.2 superagent@2.3.0 35 | 36 | - name: Node.js 0.12 37 | node-version: "0.12" 38 | npm-i: mocha@3.5.3 nyc@10.3.2 superagent@2.3.0 39 | 40 | - name: Node.js 4.x 41 | node-version: "4.9" 42 | npm-i: mocha@5.2.0 nyc@11.9.0 43 | 44 | - name: Node.js 6.x 45 | node-version: "6.17" 46 | npm-i: mocha@6.2.2 nyc@14.1.1 47 | 48 | - name: Node.js 8.x 49 | node-version: "8.17" 50 | npm-i: mocha@7.2.0 51 | 52 | - name: Node.js 10.x 53 | node-version: "10.24" 54 | npm-i: mocha@8.4.0 55 | 56 | - name: Node.js 12.x 57 | node-version: "12.22" 58 | npm-i: mocha@8.4.0 59 | 60 | - name: Node.js 14.x 61 | node-version: "14.21" 62 | 63 | - name: Node.js 16.x 64 | node-version: "16.20" 65 | 66 | - name: Node.js 18.x 67 | node-version: "18.16" 68 | 69 | - name: Node.js 20.x 70 | node-version: "20.2" 71 | 72 | steps: 73 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 74 | 75 | - name: Install Node.js ${{ matrix.node-version }} 76 | shell: bash -eo pipefail -l {0} 77 | run: | 78 | nvm install --default ${{ matrix.node-version }} 79 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 80 | 81 | - name: Configure npm 82 | run: | 83 | if [[ "$(npm config get package-lock)" == "true" ]]; then 84 | npm config set package-lock false 85 | else 86 | npm config set shrinkwrap false 87 | fi 88 | 89 | - name: Install npm module(s) ${{ matrix.npm-i }} 90 | run: npm install --save-dev ${{ matrix.npm-i }} 91 | if: matrix.npm-i != '' 92 | 93 | - name: Setup Node.js version-specific dependencies 94 | shell: bash 95 | run: | 96 | # eslint for linting 97 | # - remove on Node.js < 12 98 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then 99 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 100 | grep -E '^eslint(-|$)' | \ 101 | sort -r | \ 102 | xargs -n1 npm rm --silent --save-dev 103 | fi 104 | 105 | - name: Install Node.js dependencies 106 | run: npm install 107 | 108 | - name: List environment 109 | id: list_env 110 | shell: bash 111 | run: | 112 | echo "node@$(node -v)" 113 | echo "npm@$(npm -v)" 114 | npm -s ls ||: 115 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" 116 | 117 | - name: Run tests 118 | shell: bash 119 | run: npm run test-ci 120 | 121 | - name: Lint code 122 | if: steps.list_env.outputs.eslint != '' 123 | run: npm run lint 124 | 125 | - name: Collect code coverage 126 | uses: coverallsapp/github-action@09b709cf6a16e30b0808ba050c7a6e8a5ef13f8d # master 127 | with: 128 | github-token: ${{ secrets.GITHUB_TOKEN }} 129 | flag-name: run-${{ matrix.test_number }} 130 | parallel: true 131 | 132 | coverage: 133 | permissions: 134 | checks: write # for coverallsapp/github-action to create new checks 135 | needs: test 136 | runs-on: ubuntu-latest 137 | steps: 138 | - name: Uploade code coverage 139 | uses: coverallsapp/github-action@09b709cf6a16e30b0808ba050c7a6e8a5ef13f8d # master 140 | with: 141 | github-token: ${{ secrets.github_token }} 142 | parallel-finished: true 143 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | unreleased 2 | ========== 3 | 4 | * Fix decoding filenames from Chrome/Firefox 5 | * Fix form parsing when no `part` event listener added 6 | * deps: http-errors@2.0.0 7 | - deps: depd@2.0.0 8 | - deps: statuses@2.0.1 9 | 10 | 4.2.3 / 2022-01-20 11 | ================== 12 | 13 | * Fix handling of unquoted values in `Content-Disposition` 14 | * deps: http-errors@~1.8.1 15 | - deps: toidentifier@1.0.1 16 | 17 | 4.2.2 / 2020-07-27 18 | ================== 19 | 20 | * Fix empty files on Node.js 14.x 21 | * Fix form emitting aborted error after close 22 | * Replace `fd-slicer` module with internal transform stream 23 | * deps: http-errors@~1.8.0 24 | - Fix error creating objects in some environments 25 | - deps: inherits@2.0.4 26 | - deps: setprototypeof@1.2.0 27 | * deps: safe-buffer@5.2.1 28 | 29 | 4.2.1 / 2018-08-12 30 | ================== 31 | 32 | * Use `uid-safe` module to for temp file names 33 | * deps: fd-slicer@1.1.0 34 | * deps: http-errors@~1.7.0 35 | 36 | 4.2.0 / 2018-07-30 37 | ================== 38 | 39 | * Use `http-errors` for raised errors 40 | * Use `random-bytes` module for polyfill 41 | * perf: remove parameter reassignment 42 | 43 | 4.1.4 / 2018-05-11 44 | ================== 45 | 46 | * Fix file extension filtering stopping on certain whitespace characters 47 | * Use `safe-buffer` for improved API safety 48 | * perf: enable strict mode 49 | 50 | 4.1.3 / 2017-01-22 51 | ================== 52 | 53 | * Use `os.tmpdir()` instead of `os.tmpDir()` 54 | * deps: fd-slicer@1.0.1 55 | 56 | 4.1.2 / 2015-05-09 57 | ================== 58 | 59 | * Do not emit error on part prior to emitting part 60 | * Fix filename with quotes truncating from certain clients 61 | 62 | 4.1.1 / 2015-01-18 63 | ================== 64 | 65 | * Do not clobber existing temporary files 66 | 67 | 4.1.0 / 2014-12-04 68 | ================== 69 | 70 | * Add `statusCode` field to HTTP-related errors 71 | * deps: fd-slicer@1.0.0 72 | 73 | 4.0.0 / 2014-10-14 74 | ================== 75 | 76 | * `part` events for fields no longer fire if `autoFields` is on 77 | * `part` events for files no longer fire if `autoFiles` is on 78 | * `field`, `file`, and `part` events are guaranteed to emit in the 79 | correct order - the order that the user places the parts in the 80 | request. Each `part` `end` event is guaranteed to emit before the 81 | next `part` event is emitted. 82 | * Drop Node.js 0.8.x support 83 | * Improve random temp file names 84 | - Now using 18 bytes of randomness instead of 8. 85 | * More robust `maxFilesSize` implementation 86 | - Before it was possible for race conditions to cause more than 87 | `maxFilesSize` bytes to get written to disk. That is now fixed. 88 | * Now `part` objects emit `error` events 89 | - This makes streaming work better since the part stream will emit 90 | an error when it is no longer streaming. 91 | * Remove support for generating the hash digest of a part 92 | - If you want this, do it in your own code. 93 | * Remove undocumented `ws` property from `file` objects 94 | * Require the close boundary 95 | - This makes multiparty more RFC-compliant and makes some invalid 96 | requests which used to work, now emit an error instead. 97 | 98 | 3.3.2 / 2014-08-07 99 | ================== 100 | 101 | * Do not invoke callback after close 102 | * Share callback ending logic between error and close 103 | 104 | 3.3.1 / 2014-07-22 105 | ================== 106 | 107 | * Remove problematic test fixtures 108 | 109 | 3.3.0 / 2014-07-03 110 | ================== 111 | 112 | * Always emit close after all parts ended 113 | 114 | 3.2.10 / 2014-07-03 115 | =================== 116 | 117 | * Fix callback hang in node.js 0.8 on errors 118 | * Remove execute bit from files 119 | 120 | 3.2.9 / 2014-06-16 121 | ================== 122 | 123 | * Fix attaching error listeners directly after form.parse 124 | * Fix to not synchronously invoke callback to form.parse on error 125 | 126 | 3.2.8 / 2014-06-01 127 | ================== 128 | 129 | * Fix developer accidentally corrupting data 130 | * Fix handling epilogue in a separate chunk 131 | * Fix initial check errors to use supplied callback 132 | 133 | 3.2.7 / 2014-05-26 134 | ================== 135 | 136 | * Fix errors hanging responses in callback-style 137 | 138 | 3.2.6 / 2014-05-13 139 | ================== 140 | 141 | * Fix `maxFields` to error on field after max 142 | 143 | 3.2.5 / 2014-05-11 144 | ================== 145 | 146 | * Support boundary containing equal sign 147 | 148 | 3.2.4 / 2014-03-26 149 | ================== 150 | 151 | * Keep `part.byteCount` undefined in chunked encoding 152 | * Fix temp files not always cleaned up 153 | 154 | 3.2.3 / 2014-02-20 155 | ================== 156 | 157 | * Improve parsing boundary attribute from `Content-Type` 158 | 159 | 3.2.2 / 2014-01-29 160 | ================== 161 | 162 | * Fix error on empty payloads 163 | 164 | 3.2.1 / 2014-01-27 165 | ================== 166 | 167 | * Fix `maxFilesSize` overcalculation bug 168 | 169 | 3.2.0 / 2014-01-17 170 | ================== 171 | 172 | * Add `maxFilesSize` for `autoFiles` 173 | 174 | 3.1.2 / 2014-01-13 175 | ================== 176 | 177 | * Fix incorrectly using `autoFields` value for `autoFiles` 178 | 179 | 3.1.1 / 2013-12-13 180 | ================== 181 | 182 | * Fix not emitting `close` after all part `end` events 183 | 184 | 3.1.0 / 2013-11-10 185 | ================== 186 | 187 | * Support UTF-8 filename in `Content-Disposition` 188 | 189 | 3.0.0 / 2013-10-25 190 | ================== 191 | 192 | * `form.parse` callback API changed in a compatibility-breaking manner 193 | 194 | 2.2.0 / 2013-10-15 195 | ================== 196 | 197 | * Add callback API to support multiple files with same field name 198 | * Fix assertion crash when max field count is exceeded 199 | * Fix assertion crash when client aborts an invalid request 200 | * Fix assertion crash when `EMFILE` occurrs 201 | * Switch from assertions to only `error` events 202 | * Unpipe the request when an error occurs to save resources 203 | * Update readable-stream to ~1.1.9 204 | 205 | 2.1.9 / 2013-10-06 206 | ================== 207 | 208 | * relax `Content-Type` detection regex 209 | 210 | 2.1.8 / 2013-08-26 211 | ================== 212 | 213 | * Replace deprecated `Buffer.write()` 214 | 215 | 2.1.7 / 2013-05-23 216 | ================== 217 | 218 | * Add repository field to package.json 219 | 220 | 2.1.6 / 2013-04-30 221 | ================== 222 | 223 | * Expose `hash` as an option to `Form` 224 | 225 | 2.1.5 / 2013-04-10 226 | ================== 227 | 228 | * Fix possible `close` event before all temp files are done 229 | 230 | 2.1.4 / 2013-04-09 231 | ================== 232 | 233 | * Fix crash for invalid requests 234 | 235 | 2.1.3 / 2013-04-09 236 | ================== 237 | 238 | * Add `file.size` 239 | 240 | 2.1.2 / 2013-04-08 241 | ================== 242 | 243 | * Add proper backpressure support 244 | 245 | 2.1.1 / 2013-04-05 246 | ================== 247 | 248 | * Add `part.byteCount` and `part.byteOffset` 249 | * Fix uploads larger than 2KB 250 | 251 | 2.1.0 / 2013-04-04 252 | ================== 253 | 254 | * Complete rewrite. See README for changes and new API. 255 | 256 | 2.0.0 / 2013-04-02 257 | ================== 258 | 259 | * Fork and rewrite from `formidable` 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multiparty 2 | 3 | [![NPM Version][npm-version-image]][npm-url] 4 | [![NPM Downloads][npm-downloads-image]][npm-url] 5 | [![Node.js Version][node-version-image]][node-version-url] 6 | [![Build Status][github-actions-ci-image]][github-actions-ci-url] 7 | [![Test Coverage][coveralls-image]][coveralls-url] 8 | 9 | Parse http requests with content-type `multipart/form-data`, also known as file uploads. 10 | 11 | See also [busboy](https://github.com/mscdex/busboy) - a 12 | [faster](https://github.com/mscdex/dicer/wiki/Benchmarks) alternative 13 | which may be worth looking into. 14 | 15 | ## Installation 16 | 17 | This is a [Node.js](https://nodejs.org/en/) module available through the 18 | [npm registry](https://www.npmjs.com/). Installation is done using the 19 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 20 | 21 | ``` 22 | npm install multiparty 23 | ``` 24 | 25 | ## Usage 26 | 27 | * See [examples](examples). 28 | 29 | Parse an incoming `multipart/form-data` request. 30 | 31 | ```js 32 | var multiparty = require('multiparty'); 33 | var http = require('http'); 34 | var util = require('util'); 35 | 36 | http.createServer(function(req, res) { 37 | if (req.url === '/upload' && req.method === 'POST') { 38 | // parse a file upload 39 | var form = new multiparty.Form(); 40 | 41 | form.parse(req, function(err, fields, files) { 42 | res.writeHead(200, { 'content-type': 'text/plain' }); 43 | res.write('received upload:\n\n'); 44 | res.end(util.inspect({ fields: fields, files: files })); 45 | }); 46 | 47 | return; 48 | } 49 | 50 | // show a file upload form 51 | res.writeHead(200, { 'content-type': 'text/html' }); 52 | res.end( 53 | '
'+ 54 | '
'+ 55 | '
'+ 56 | ''+ 57 | '
' 58 | ); 59 | }).listen(8080); 60 | ``` 61 | 62 | ## API 63 | 64 | ### multiparty.Form 65 | 66 | 67 | 68 | ```js 69 | var form = new multiparty.Form(options) 70 | ``` 71 | 72 | Creates a new form. Options: 73 | 74 | * `encoding` - sets encoding for the incoming form fields. Defaults to `utf8`. 75 | * `maxFieldsSize` - Limits the amount of memory all fields (not files) can 76 | allocate in bytes. If this value is exceeded, an `error` event is emitted. 77 | The default size is 2MB. 78 | * `maxFields` - Limits the number of fields that will be parsed before 79 | emitting an `error` event. A file counts as a field in this case. 80 | Defaults to 1000. 81 | * `maxFilesSize` - Only relevant when `autoFiles` is `true`. Limits the 82 | total bytes accepted for all files combined. If this value is exceeded, 83 | an `error` event is emitted. The default is `Infinity`. 84 | * `autoFields` - Enables `field` events and disables `part` events for fields. 85 | This is automatically set to `true` if you add a `field` listener. 86 | * `autoFiles` - Enables `file` events and disables `part` events for files. 87 | This is automatically set to `true` if you add a `file` listener. 88 | * `uploadDir` - Only relevant when `autoFiles` is `true`. The directory for 89 | placing file uploads in. You can move them later using `fs.rename()`. 90 | Defaults to `os.tmpdir()`. 91 | 92 | #### form.parse(request, [cb]) 93 | 94 | Parses an incoming node.js `request` containing form data.This will cause 95 | `form` to emit events based off the incoming request. 96 | 97 | ```js 98 | var count = 0; 99 | var form = new multiparty.Form(); 100 | 101 | // Errors may be emitted 102 | // Note that if you are listening to 'part' events, the same error may be 103 | // emitted from the `form` and the `part`. 104 | form.on('error', function(err) { 105 | console.log('Error parsing form: ' + err.stack); 106 | }); 107 | 108 | // Parts are emitted when parsing the form 109 | form.on('part', function(part) { 110 | // You *must* act on the part by reading it 111 | // NOTE: if you want to ignore it, just call "part.resume()" 112 | 113 | if (part.filename === undefined) { 114 | // filename is not defined when this is a field and not a file 115 | console.log('got field named ' + part.name); 116 | // ignore field's content 117 | part.resume(); 118 | } 119 | 120 | if (part.filename !== undefined) { 121 | // filename is defined when this is a file 122 | count++; 123 | console.log('got file named ' + part.name); 124 | // ignore file's content here 125 | part.resume(); 126 | } 127 | 128 | part.on('error', function(err) { 129 | // decide what to do 130 | }); 131 | }); 132 | 133 | // Close emitted after form parsed 134 | form.on('close', function() { 135 | console.log('Upload completed!'); 136 | res.setHeader('text/plain'); 137 | res.end('Received ' + count + ' files'); 138 | }); 139 | 140 | // Parse req 141 | form.parse(req); 142 | ``` 143 | 144 | If `cb` is provided, `autoFields` and `autoFiles` are set to `true` and all 145 | fields and files are collected and passed to the callback, removing the need to 146 | listen to any events on `form`. This is for convenience when you want to read 147 | everything, but be sure to write cleanup code, as this will write all uploaded 148 | files to the disk, even ones you may not be interested in. 149 | 150 | ```js 151 | form.parse(req, function(err, fields, files) { 152 | Object.keys(fields).forEach(function(name) { 153 | console.log('got field named ' + name); 154 | }); 155 | 156 | Object.keys(files).forEach(function(name) { 157 | console.log('got file named ' + name); 158 | }); 159 | 160 | console.log('Upload completed!'); 161 | res.setHeader('text/plain'); 162 | res.end('Received ' + files.length + ' files'); 163 | }); 164 | ``` 165 | 166 | `fields` is an object where the property names are field names and the values 167 | are arrays of field values. 168 | 169 | `files` is an object where the property names are field names and the values 170 | are arrays of file objects. 171 | 172 | #### form.bytesReceived 173 | 174 | The amount of bytes received for this form so far. 175 | 176 | #### form.bytesExpected 177 | 178 | The expected number of bytes in this form. 179 | 180 | ### Events 181 | 182 | #### 'error' (err) 183 | 184 | Unless you supply a callback to `form.parse`, you definitely want to handle 185 | this event. Otherwise your server *will* crash when users submit bogus 186 | multipart requests! 187 | 188 | Only one 'error' event can ever be emitted, and if an 'error' event is 189 | emitted, then 'close' will not be emitted. 190 | 191 | If the error would correspond to a certain HTTP response code, the `err` object 192 | will have a `statusCode` property with the value of the suggested HTTP response 193 | code to send back. 194 | 195 | Note that an 'error' event will be emitted both from the `form` and from the 196 | current `part`. 197 | 198 | #### 'part' (part) 199 | 200 | Emitted when a part is encountered in the request. `part` is a 201 | `ReadableStream`. It also has the following properties: 202 | 203 | * `headers` - the headers for this part. For example, you may be interested 204 | in `content-type`. 205 | * `name` - the field name for this part 206 | * `filename` - only if the part is an incoming file 207 | * `byteOffset` - the byte offset of this part in the request body 208 | * `byteCount` - assuming that this is the last part in the request, 209 | this is the size of this part in bytes. You could use this, for 210 | example, to set the `Content-Length` header if uploading to S3. 211 | If the part had a `Content-Length` header then that value is used 212 | here instead. 213 | 214 | Parts for fields are not emitted when `autoFields` is on, and likewise parts 215 | for files are not emitted when `autoFiles` is on. 216 | 217 | `part` emits 'error' events! Make sure you handle them. 218 | 219 | #### 'aborted' 220 | 221 | Emitted when the request is aborted. This event will be followed shortly 222 | by an `error` event. In practice you do not need to handle this event. 223 | 224 | #### 'progress' (bytesReceived, bytesExpected) 225 | 226 | Emitted when a chunk of data is received for the form. The `bytesReceived` 227 | argument contains the total count of bytes received for this form so far. The 228 | `bytesExpected` argument contains the total expected bytes if known, otherwise 229 | `null`. 230 | 231 | #### 'close' 232 | 233 | Emitted after all parts have been parsed and emitted. Not emitted if an `error` 234 | event is emitted. 235 | 236 | If you have `autoFiles` on, this is not fired until all the data has been 237 | flushed to disk and the file handles have been closed. 238 | 239 | This is typically when you would send your response. 240 | 241 | #### 'file' (name, file) 242 | 243 | **By default multiparty will not touch your hard drive.** But if you add this 244 | listener, multiparty automatically sets `form.autoFiles` to `true` and will 245 | stream uploads to disk for you. 246 | 247 | **The max bytes accepted per request can be specified with `maxFilesSize`.** 248 | 249 | * `name` - the field name for this file 250 | * `file` - an object with these properties: 251 | - `fieldName` - same as `name` - the field name for this file 252 | - `originalFilename` - the filename that the user reports for the file 253 | - `path` - the absolute path of the uploaded file on disk 254 | - `headers` - the HTTP headers that were sent along with this file 255 | - `size` - size of the file in bytes 256 | 257 | #### 'field' (name, value) 258 | 259 | * `name` - field name 260 | * `value` - string field value 261 | 262 | ## License 263 | 264 | [MIT](LICENSE) 265 | 266 | [coveralls-image]: https://badgen.net/coveralls/c/github/pillarjs/multiparty/master 267 | [coveralls-url]: https://coveralls.io/r/pillarjs/multiparty?branch=master 268 | [github-actions-ci-image]: https://badgen.net/github/checks/pillarjs/multiparty/master?label=ci 269 | [github-actions-ci-url]: https://github.com/pillarjs/multiparty/actions?query=workflow%3Aci 270 | [node-version-image]: https://badgen.net/npm/node/multiparty 271 | [node-version-url]: https://nodejs.org/en/download 272 | [npm-downloads-image]: https://badgen.net/npm/dm/multiparty 273 | [npm-url]: https://npmjs.org/package/multiparty 274 | [npm-version-image]: https://badgen.net/npm/v/multiparty 275 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * multiparty 3 | * Copyright(c) 2013 Felix Geisendörfer 4 | * Copyright(c) 2014 Andrew Kelley 5 | * Copyright(c) 2014 Douglas Christopher Wilson 6 | * MIT Licensed 7 | */ 8 | 9 | 'use strict' 10 | 11 | var createError = require('http-errors') 12 | var uid = require('uid-safe') 13 | var stream = require('stream'); 14 | var util = require('util'); 15 | var fs = require('fs'); 16 | var path = require('path'); 17 | var os = require('os'); 18 | var Buffer = require('safe-buffer').Buffer 19 | var StringDecoder = require('string_decoder').StringDecoder; 20 | 21 | var START = 0; 22 | var START_BOUNDARY = 1; 23 | var HEADER_FIELD_START = 2; 24 | var HEADER_FIELD = 3; 25 | var HEADER_VALUE_START = 4; 26 | var HEADER_VALUE = 5; 27 | var HEADER_VALUE_ALMOST_DONE = 6; 28 | var HEADERS_ALMOST_DONE = 7; 29 | var PART_DATA_START = 8; 30 | var PART_DATA = 9; 31 | var CLOSE_BOUNDARY = 10; 32 | var END = 11; 33 | 34 | var LF = 10; 35 | var CR = 13; 36 | var SPACE = 32; 37 | var HYPHEN = 45; 38 | var COLON = 58; 39 | var A = 97; 40 | var Z = 122; 41 | 42 | var CONTENT_TYPE_RE = /^multipart\/(?:form-data|related)(?:;|$)/i; 43 | var CONTENT_TYPE_PARAM_RE = /;\s*([^=]+)=(?:"([^"]+)"|([^;]+))/gi; 44 | var FILE_EXT_RE = /(\.[_\-a-zA-Z0-9]{0,16})[\S\s]*/; 45 | var FILENAME_PARAM_RE = /\bfilename=(?:"(.*?)"|([!#$%&'*+.0-9A-Z^_`a-z|~-]+))($|; )/i 46 | var LAST_BOUNDARY_SUFFIX_LEN = 4; // --\r\n 47 | var NAME_PARAM_RE = /\bname=(?:"([^"]+)"|([!#$%&'*+.0-9A-Z^_`a-z|~-]+))/i 48 | 49 | exports.Form = Form; 50 | 51 | util.inherits(Form, stream.Writable); 52 | function Form(options) { 53 | var opts = options || {} 54 | var self = this; 55 | stream.Writable.call(self, { emitClose: false }) 56 | 57 | self.error = null; 58 | 59 | self.autoFields = !!opts.autoFields 60 | self.autoFiles = !!opts.autoFiles 61 | 62 | self.maxFields = opts.maxFields || 1000 63 | self.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024 64 | self.maxFilesSize = opts.maxFilesSize || Infinity 65 | self.uploadDir = opts.uploadDir || os.tmpdir() 66 | self.encoding = opts.encoding || 'utf8' 67 | 68 | self.bytesReceived = 0; 69 | self.bytesExpected = null; 70 | 71 | self.openedFiles = []; 72 | self.totalFieldSize = 0; 73 | self.totalFieldCount = 0; 74 | self.totalFileSize = 0; 75 | self.flushing = 0; 76 | 77 | self.backpressure = false; 78 | self.writeCbs = []; 79 | 80 | self.emitQueue = []; 81 | 82 | self.on('newListener', function(eventName) { 83 | if (eventName === 'file') { 84 | self.autoFiles = true; 85 | } else if (eventName === 'field') { 86 | self.autoFields = true; 87 | } 88 | }); 89 | } 90 | 91 | Form.prototype.parse = function(req, cb) { 92 | var called = false; 93 | var self = this; 94 | var waitend = true; 95 | 96 | self.on('close', onClosed) 97 | 98 | if (cb) { 99 | // if the user supplies a callback, this implies autoFields and autoFiles 100 | self.autoFields = true; 101 | self.autoFiles = true; 102 | 103 | // wait for request to end before calling cb 104 | var end = function (done) { 105 | if (called) return; 106 | 107 | called = true; 108 | 109 | // wait for req events to fire 110 | process.nextTick(function() { 111 | if (waitend && req.readable) { 112 | // dump rest of request 113 | req.resume(); 114 | req.once('end', done); 115 | return; 116 | } 117 | 118 | done(); 119 | }); 120 | }; 121 | 122 | var fields = {}; 123 | var files = {}; 124 | self.on('error', function(err) { 125 | end(function() { 126 | cb(err); 127 | }); 128 | }); 129 | self.on('field', function(name, value) { 130 | var fieldsArray = fields[name] || (fields[name] = []); 131 | fieldsArray.push(value); 132 | }); 133 | self.on('file', function(name, file) { 134 | var filesArray = files[name] || (files[name] = []); 135 | filesArray.push(file); 136 | }); 137 | self.on('close', function() { 138 | end(function() { 139 | cb(null, fields, files); 140 | }); 141 | }); 142 | } 143 | 144 | self.handleError = handleError; 145 | self.bytesExpected = getBytesExpected(req.headers); 146 | 147 | req.on('end', onReqEnd); 148 | req.on('error', function(err) { 149 | waitend = false; 150 | handleError(err); 151 | }); 152 | req.on('aborted', onReqAborted); 153 | 154 | var state = req._readableState; 155 | if (req._decoder || (state && (state.encoding || state.decoder))) { 156 | // this is a binary protocol 157 | // if an encoding is set, input is likely corrupted 158 | validationError(new Error('request encoding must not be set')); 159 | return; 160 | } 161 | 162 | var contentType = req.headers['content-type']; 163 | if (!contentType) { 164 | validationError(createError(415, 'missing content-type header')); 165 | return; 166 | } 167 | 168 | var m = CONTENT_TYPE_RE.exec(contentType); 169 | if (!m) { 170 | validationError(createError(415, 'unsupported content-type')); 171 | return; 172 | } 173 | 174 | var boundary; 175 | CONTENT_TYPE_PARAM_RE.lastIndex = m.index + m[0].length - 1; 176 | while ((m = CONTENT_TYPE_PARAM_RE.exec(contentType))) { 177 | if (m[1].toLowerCase() !== 'boundary') continue; 178 | boundary = m[2] || m[3]; 179 | break; 180 | } 181 | 182 | if (!boundary) { 183 | validationError(createError(400, 'content-type missing boundary')); 184 | return; 185 | } 186 | 187 | setUpParser(self, boundary); 188 | req.pipe(self); 189 | 190 | function onClosed () { 191 | req.removeListener('aborted', onReqAborted) 192 | } 193 | 194 | function onReqAborted() { 195 | waitend = false; 196 | self.emit('aborted'); 197 | handleError(new Error('Request aborted')) 198 | } 199 | 200 | function onReqEnd() { 201 | waitend = false; 202 | } 203 | 204 | function handleError(err) { 205 | var first = !self.error; 206 | if (first) { 207 | self.error = err; 208 | req.removeListener('aborted', onReqAborted); 209 | req.removeListener('end', onReqEnd); 210 | if (self.destStream) { 211 | errorEventQueue(self, self.destStream, err); 212 | } 213 | } 214 | 215 | cleanupOpenFiles(self); 216 | 217 | if (first) { 218 | self.emit('error', err); 219 | } 220 | } 221 | 222 | function validationError(err) { 223 | // handle error on next tick for event listeners to attach 224 | process.nextTick(handleError.bind(null, err)) 225 | } 226 | }; 227 | 228 | Form.prototype._write = function(buffer, encoding, cb) { 229 | if (this.error) return; 230 | 231 | var self = this; 232 | var i = 0; 233 | var len = buffer.length; 234 | var prevIndex = self.index; 235 | var index = self.index; 236 | var state = self.state; 237 | var lookbehind = self.lookbehind; 238 | var boundary = self.boundary; 239 | var boundaryChars = self.boundaryChars; 240 | var boundaryLength = self.boundary.length; 241 | var boundaryEnd = boundaryLength - 1; 242 | var bufferLength = buffer.length; 243 | var c; 244 | var cl; 245 | 246 | for (i = 0; i < len; i++) { 247 | c = buffer[i]; 248 | switch (state) { 249 | case START: 250 | index = 0; 251 | state = START_BOUNDARY; 252 | /* falls through */ 253 | case START_BOUNDARY: 254 | if (index === boundaryLength - 2 && c === HYPHEN) { 255 | index = 1; 256 | state = CLOSE_BOUNDARY; 257 | break; 258 | } else if (index === boundaryLength - 2) { 259 | if (c !== CR) return self.handleError(createError(400, 'Expected CR Received ' + c)); 260 | index++; 261 | break; 262 | } else if (index === boundaryLength - 1) { 263 | if (c !== LF) return self.handleError(createError(400, 'Expected LF Received ' + c)); 264 | index = 0; 265 | self.onParsePartBegin(); 266 | state = HEADER_FIELD_START; 267 | break; 268 | } 269 | 270 | if (c !== boundary[index+2]) index = -2; 271 | if (c === boundary[index+2]) index++; 272 | break; 273 | case HEADER_FIELD_START: 274 | state = HEADER_FIELD; 275 | self.headerFieldMark = i; 276 | index = 0; 277 | /* falls through */ 278 | case HEADER_FIELD: 279 | if (c === CR) { 280 | self.headerFieldMark = null; 281 | state = HEADERS_ALMOST_DONE; 282 | break; 283 | } 284 | 285 | index++; 286 | if (c === HYPHEN) break; 287 | 288 | if (c === COLON) { 289 | if (index === 1) { 290 | // empty header field 291 | self.handleError(createError(400, 'Empty header field')); 292 | return; 293 | } 294 | self.onParseHeaderField(buffer.slice(self.headerFieldMark, i)); 295 | self.headerFieldMark = null; 296 | state = HEADER_VALUE_START; 297 | break; 298 | } 299 | 300 | cl = lower(c); 301 | if (cl < A || cl > Z) { 302 | self.handleError(createError(400, 'Expected alphabetic character, received ' + c)); 303 | return; 304 | } 305 | break; 306 | case HEADER_VALUE_START: 307 | if (c === SPACE) break; 308 | 309 | self.headerValueMark = i; 310 | state = HEADER_VALUE; 311 | /* falls through */ 312 | case HEADER_VALUE: 313 | if (c === CR) { 314 | self.onParseHeaderValue(buffer.slice(self.headerValueMark, i)); 315 | self.headerValueMark = null; 316 | self.onParseHeaderEnd(); 317 | state = HEADER_VALUE_ALMOST_DONE; 318 | } 319 | break; 320 | case HEADER_VALUE_ALMOST_DONE: 321 | if (c !== LF) return self.handleError(createError(400, 'Expected LF Received ' + c)); 322 | state = HEADER_FIELD_START; 323 | break; 324 | case HEADERS_ALMOST_DONE: 325 | if (c !== LF) return self.handleError(createError(400, 'Expected LF Received ' + c)); 326 | var err = self.onParseHeadersEnd(i + 1); 327 | if (err) return self.handleError(err); 328 | state = PART_DATA_START; 329 | break; 330 | case PART_DATA_START: 331 | state = PART_DATA; 332 | self.partDataMark = i; 333 | /* falls through */ 334 | case PART_DATA: 335 | prevIndex = index; 336 | 337 | if (index === 0) { 338 | // boyer-moore derrived algorithm to safely skip non-boundary data 339 | i += boundaryEnd; 340 | while (i < bufferLength && !(buffer[i] in boundaryChars)) { 341 | i += boundaryLength; 342 | } 343 | i -= boundaryEnd; 344 | c = buffer[i]; 345 | } 346 | 347 | if (index < boundaryLength) { 348 | if (boundary[index] === c) { 349 | if (index === 0) { 350 | self.onParsePartData(buffer.slice(self.partDataMark, i)); 351 | self.partDataMark = null; 352 | } 353 | index++; 354 | } else { 355 | index = 0; 356 | } 357 | } else if (index === boundaryLength) { 358 | index++; 359 | if (c === CR) { 360 | // CR = part boundary 361 | self.partBoundaryFlag = true; 362 | } else if (c === HYPHEN) { 363 | index = 1; 364 | state = CLOSE_BOUNDARY; 365 | break; 366 | } else { 367 | index = 0; 368 | } 369 | } else if (index - 1 === boundaryLength) { 370 | if (self.partBoundaryFlag) { 371 | index = 0; 372 | if (c === LF) { 373 | self.partBoundaryFlag = false; 374 | self.onParsePartEnd(); 375 | self.onParsePartBegin(); 376 | state = HEADER_FIELD_START; 377 | break; 378 | } 379 | } else { 380 | index = 0; 381 | } 382 | } 383 | 384 | if (index > 0) { 385 | // when matching a possible boundary, keep a lookbehind reference 386 | // in case it turns out to be a false lead 387 | lookbehind[index-1] = c; 388 | } else if (prevIndex > 0) { 389 | // if our boundary turned out to be rubbish, the captured lookbehind 390 | // belongs to partData 391 | self.onParsePartData(lookbehind.slice(0, prevIndex)); 392 | prevIndex = 0; 393 | self.partDataMark = i; 394 | 395 | // reconsider the current character even so it interrupted the sequence 396 | // it could be the beginning of a new sequence 397 | i--; 398 | } 399 | 400 | break; 401 | case CLOSE_BOUNDARY: 402 | if (c !== HYPHEN) return self.handleError(createError(400, 'Expected HYPHEN Received ' + c)); 403 | if (index === 1) { 404 | self.onParsePartEnd(); 405 | state = END; 406 | } else if (index > 1) { 407 | return self.handleError(new Error('Parser has invalid state.')) 408 | } 409 | index++; 410 | break; 411 | case END: 412 | break; 413 | default: 414 | self.handleError(new Error('Parser has invalid state.')) 415 | return; 416 | } 417 | } 418 | 419 | if (self.headerFieldMark != null) { 420 | self.onParseHeaderField(buffer.slice(self.headerFieldMark)); 421 | self.headerFieldMark = 0; 422 | } 423 | if (self.headerValueMark != null) { 424 | self.onParseHeaderValue(buffer.slice(self.headerValueMark)); 425 | self.headerValueMark = 0; 426 | } 427 | if (self.partDataMark != null) { 428 | self.onParsePartData(buffer.slice(self.partDataMark)); 429 | self.partDataMark = 0; 430 | } 431 | 432 | self.index = index; 433 | self.state = state; 434 | 435 | self.bytesReceived += buffer.length; 436 | self.emit('progress', self.bytesReceived, self.bytesExpected); 437 | 438 | if (self.backpressure) { 439 | self.writeCbs.push(cb); 440 | } else { 441 | cb(); 442 | } 443 | }; 444 | 445 | Form.prototype.onParsePartBegin = function() { 446 | clearPartVars(this); 447 | } 448 | 449 | Form.prototype.onParseHeaderField = function(b) { 450 | this.headerField += this.headerFieldDecoder.write(b); 451 | } 452 | 453 | Form.prototype.onParseHeaderValue = function(b) { 454 | this.headerValue += this.headerValueDecoder.write(b); 455 | } 456 | 457 | Form.prototype.onParseHeaderEnd = function() { 458 | this.headerField = this.headerField.toLowerCase(); 459 | this.partHeaders[this.headerField] = this.headerValue; 460 | 461 | var m; 462 | if (this.headerField === 'content-disposition') { 463 | if (m = NAME_PARAM_RE.exec(this.headerValue)) { 464 | this.partName = m[1] || m[2] || '' 465 | } 466 | this.partFilename = parseFilename(this.headerValue); 467 | } else if (this.headerField === 'content-transfer-encoding') { 468 | this.partTransferEncoding = this.headerValue.toLowerCase(); 469 | } 470 | 471 | this.headerFieldDecoder = new StringDecoder(this.encoding); 472 | this.headerField = ''; 473 | this.headerValueDecoder = new StringDecoder(this.encoding); 474 | this.headerValue = ''; 475 | } 476 | 477 | Form.prototype.onParsePartData = function(b) { 478 | if (this.partTransferEncoding === 'base64') { 479 | this.backpressure = ! this.destStream.write(b.toString('ascii'), 'base64'); 480 | } else { 481 | this.backpressure = ! this.destStream.write(b); 482 | } 483 | } 484 | 485 | Form.prototype.onParsePartEnd = function() { 486 | if (this.destStream) { 487 | flushWriteCbs(this); 488 | var s = this.destStream; 489 | process.nextTick(function() { 490 | s.end(); 491 | }); 492 | } 493 | clearPartVars(this); 494 | } 495 | 496 | Form.prototype.onParseHeadersEnd = function(offset) { 497 | var self = this; 498 | switch(self.partTransferEncoding){ 499 | case 'binary': 500 | case '7bit': 501 | case '8bit': 502 | self.partTransferEncoding = 'binary'; 503 | break; 504 | 505 | case 'base64': break; 506 | default: 507 | return createError(400, 'unknown transfer-encoding: ' + self.partTransferEncoding); 508 | } 509 | 510 | self.totalFieldCount += 1; 511 | if (self.totalFieldCount > self.maxFields) { 512 | return createError(413, 'maxFields ' + self.maxFields + ' exceeded.'); 513 | } 514 | 515 | self.destStream = new stream.PassThrough(); 516 | self.destStream.on('drain', function() { 517 | flushWriteCbs(self); 518 | }); 519 | self.destStream.headers = self.partHeaders; 520 | self.destStream.name = self.partName; 521 | self.destStream.filename = self.partFilename; 522 | self.destStream.byteOffset = self.bytesReceived + offset; 523 | var partContentLength = self.destStream.headers['content-length']; 524 | self.destStream.byteCount = partContentLength ? parseInt(partContentLength, 10) : 525 | self.bytesExpected ? (self.bytesExpected - self.destStream.byteOffset - 526 | self.boundary.length - LAST_BOUNDARY_SUFFIX_LEN) : 527 | undefined; 528 | 529 | if (self.destStream.filename == null && self.autoFields) { 530 | handleField(self, self.destStream); 531 | } else if (self.destStream.filename != null && self.autoFiles) { 532 | handleFile(self, self.destStream); 533 | } else { 534 | handlePart(self, self.destStream); 535 | } 536 | } 537 | 538 | util.inherits(LimitStream, stream.Transform) 539 | function LimitStream (limit) { 540 | stream.Transform.call(this) 541 | 542 | this.bytes = 0 543 | this.limit = limit 544 | } 545 | 546 | LimitStream.prototype._transform = function _transform (chunk, encoding, callback) { 547 | var length = !Buffer.isBuffer(chunk) 548 | ? Buffer.byteLength(chunk, encoding) 549 | : chunk.length 550 | 551 | this.bytes += length 552 | 553 | if (this.bytes > this.limit) { 554 | var err = new Error('maximum file length exceeded') 555 | err.code = 'ETOOBIG' 556 | callback(err) 557 | } else { 558 | this.push(chunk) 559 | this.emit('progress', this.bytes, length) 560 | callback() 561 | } 562 | } 563 | 564 | function flushWriteCbs(self) { 565 | self.writeCbs.forEach(function(cb) { 566 | process.nextTick(cb); 567 | }); 568 | self.writeCbs = []; 569 | self.backpressure = false; 570 | } 571 | 572 | function getBytesExpected(headers) { 573 | var contentLength = headers['content-length']; 574 | if (contentLength) { 575 | return parseInt(contentLength, 10); 576 | } else if (headers['transfer-encoding'] == null) { 577 | return 0; 578 | } else { 579 | return null; 580 | } 581 | } 582 | 583 | function beginFlush(self) { 584 | self.flushing += 1; 585 | } 586 | 587 | function endFlush(self) { 588 | self.flushing -= 1; 589 | 590 | if (self.flushing < 0) { 591 | // if this happens this is a critical bug in multiparty and this stack trace 592 | // will help us figure it out. 593 | self.handleError(new Error('unexpected endFlush')) 594 | return; 595 | } 596 | 597 | maybeClose(self); 598 | } 599 | 600 | function maybeClose(self) { 601 | if (self.flushing > 0 || self.error) return; 602 | 603 | // go through the emit queue in case any field, file, or part events are 604 | // waiting to be emitted 605 | holdEmitQueue(self)(function() { 606 | // nextTick because the user is listening to part 'end' events and we are 607 | // using part 'end' events to decide when to emit 'close'. we add our 'end' 608 | // handler before the user gets a chance to add theirs. So we make sure 609 | // their 'end' event fires before we emit the 'close' event. 610 | // this is covered by test/standalone/test-issue-36 611 | process.nextTick(function() { 612 | self.emit('close'); 613 | }); 614 | }); 615 | } 616 | 617 | function cleanupOpenFiles(self) { 618 | self.openedFiles.forEach(function(internalFile) { 619 | // since fd slicer autoClose is true, destroying the only write stream 620 | // is guaranteed by the API to close the fd 621 | internalFile.ws.destroy(); 622 | 623 | fs.unlink(internalFile.publicFile.path, function(err) { 624 | if (err) self.handleError(err); 625 | }); 626 | }); 627 | self.openedFiles = []; 628 | } 629 | 630 | function holdEmitQueue(self, eventEmitter) { 631 | var item = { cb: null, ee: eventEmitter, err: null } 632 | self.emitQueue.push(item); 633 | return function(cb) { 634 | item.cb = cb; 635 | flushEmitQueue(self); 636 | }; 637 | } 638 | 639 | function errorEventQueue(self, eventEmitter, err) { 640 | var items = self.emitQueue.filter(function (item) { 641 | return item.ee === eventEmitter; 642 | }); 643 | 644 | if (items.length === 0) { 645 | eventEmitter.emit('error', err); 646 | return; 647 | } 648 | 649 | items.forEach(function (item) { 650 | item.err = err; 651 | }); 652 | } 653 | 654 | function flushEmitQueue(self) { 655 | while (self.emitQueue.length > 0 && self.emitQueue[0].cb) { 656 | var item = self.emitQueue.shift(); 657 | 658 | // invoke the callback 659 | item.cb(); 660 | 661 | if (item.err) { 662 | // emit the delayed error 663 | item.ee.emit('error', item.err); 664 | } 665 | } 666 | } 667 | 668 | function handlePart(self, partStream) { 669 | beginFlush(self); 670 | var emitAndReleaseHold = holdEmitQueue(self, partStream); 671 | partStream.on('end', function() { 672 | endFlush(self); 673 | }); 674 | emitAndReleaseHold(function() { 675 | if (hasListeners(self, 'part')) { 676 | self.emit('part', partStream) 677 | } else { 678 | partStream.on('error', function (err) { 679 | self.handleError(err) 680 | }) 681 | partStream.resume() 682 | } 683 | }); 684 | } 685 | 686 | function handleFile(self, fileStream) { 687 | if (self.error) return; 688 | var publicFile = { 689 | fieldName: fileStream.name, 690 | originalFilename: fileStream.filename, 691 | path: uploadPath(self.uploadDir, fileStream.filename), 692 | headers: fileStream.headers, 693 | size: 0 694 | }; 695 | var internalFile = { 696 | publicFile: publicFile, 697 | ls: null, 698 | ws: fs.createWriteStream(publicFile.path, { flags: 'wx' }) 699 | }; 700 | self.openedFiles.push(internalFile) 701 | beginFlush(self); // flush to write stream 702 | var emitAndReleaseHold = holdEmitQueue(self, fileStream); 703 | fileStream.on('error', function(err) { 704 | self.handleError(err); 705 | }); 706 | internalFile.ws.on('error', function (err) { 707 | self.handleError(err) 708 | }) 709 | internalFile.ws.on('open', function () { 710 | // end option here guarantees that no more than that amount will be written 711 | // or else an error will be emitted 712 | internalFile.ls = new LimitStream(self.maxFilesSize - self.totalFileSize) 713 | internalFile.ls.pipe(internalFile.ws) 714 | 715 | internalFile.ls.on('error', function (err) { 716 | self.handleError(err.code === 'ETOOBIG' 717 | ? createError(413, err.message, { code: err.code }) 718 | : err) 719 | }); 720 | internalFile.ls.on('progress', function (totalBytes, chunkBytes) { 721 | publicFile.size = totalBytes 722 | self.totalFileSize += chunkBytes 723 | }); 724 | internalFile.ws.on('close', function () { 725 | if (self.error) return; 726 | emitAndReleaseHold(function() { 727 | self.emit('file', fileStream.name, publicFile); 728 | }); 729 | endFlush(self); 730 | }); 731 | fileStream.pipe(internalFile.ls) 732 | }); 733 | } 734 | 735 | function handleField(self, fieldStream) { 736 | var value = ''; 737 | var decoder = new StringDecoder(self.encoding); 738 | 739 | beginFlush(self); 740 | var emitAndReleaseHold = holdEmitQueue(self, fieldStream); 741 | fieldStream.on('error', function(err) { 742 | self.handleError(err); 743 | }); 744 | fieldStream.on('readable', function() { 745 | var buffer = fieldStream.read(); 746 | if (!buffer) return; 747 | 748 | self.totalFieldSize += buffer.length; 749 | if (self.totalFieldSize > self.maxFieldsSize) { 750 | self.handleError(createError(413, 'maxFieldsSize ' + self.maxFieldsSize + ' exceeded')); 751 | return; 752 | } 753 | value += decoder.write(buffer); 754 | }); 755 | 756 | fieldStream.on('end', function() { 757 | emitAndReleaseHold(function() { 758 | self.emit('field', fieldStream.name, value); 759 | }); 760 | endFlush(self); 761 | }); 762 | } 763 | 764 | /** 765 | * Determine if emitter has listeners of a given type. 766 | * 767 | * The way to do this check is done three different ways in Node.js >= 0.10 768 | * so this consolidates them into a minimal set using instance methods. 769 | * 770 | * @param {EventEmitter} emitter 771 | * @param {string} type 772 | * @returns {boolean} 773 | * @private 774 | */ 775 | 776 | function hasListeners (emitter, type) { 777 | var count = typeof emitter.listenerCount !== 'function' 778 | ? emitter.listeners(type).length 779 | : emitter.listenerCount(type) 780 | 781 | return count > 0 782 | } 783 | 784 | function clearPartVars(self) { 785 | self.partHeaders = {}; 786 | self.partName = null; 787 | self.partFilename = null; 788 | self.partTransferEncoding = 'binary'; 789 | self.destStream = null; 790 | 791 | self.headerFieldDecoder = new StringDecoder(self.encoding); 792 | self.headerField = '' 793 | self.headerValueDecoder = new StringDecoder(self.encoding); 794 | self.headerValue = '' 795 | } 796 | 797 | function setUpParser(self, boundary) { 798 | self.boundary = Buffer.alloc(boundary.length + 4) 799 | self.boundary.write('\r\n--', 0, boundary.length + 4, 'ascii'); 800 | self.boundary.write(boundary, 4, boundary.length, 'ascii'); 801 | self.lookbehind = Buffer.alloc(self.boundary.length + 8) 802 | self.state = START; 803 | self.boundaryChars = {}; 804 | for (var i = 0; i < self.boundary.length; i++) { 805 | self.boundaryChars[self.boundary[i]] = true; 806 | } 807 | 808 | self.index = null; 809 | self.partBoundaryFlag = false; 810 | 811 | beginFlush(self); 812 | self.on('finish', function() { 813 | if (self.state !== END) { 814 | self.handleError(createError(400, 'stream ended unexpectedly')); 815 | } 816 | endFlush(self); 817 | }); 818 | } 819 | 820 | function uploadPath(baseDir, filename) { 821 | var ext = path.extname(filename).replace(FILE_EXT_RE, '$1'); 822 | var name = uid.sync(18) + ext 823 | return path.join(baseDir, name); 824 | } 825 | 826 | function parseFilename(headerValue) { 827 | var m = FILENAME_PARAM_RE.exec(headerValue) 828 | if (!m) { 829 | m = headerValue.match(/\bfilename\*=utf-8''(.*?)($|; )/i) 830 | if (m) { 831 | m[1] = decodeURI(m[1]); 832 | } else { 833 | return; 834 | } 835 | } 836 | 837 | var filename = m[1] || m[2] || ''; 838 | filename = filename.replace(/%22|\\"/g, '"'); 839 | filename = filename.replace(/&#([0-9]{1,5});/g, function(m, code) { 840 | return String.fromCharCode(code); 841 | }); 842 | return filename.substr(filename.lastIndexOf('\\') + 1); 843 | } 844 | 845 | function lower(c) { 846 | return c | 0x20; 847 | } 848 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var requireAll = require('require-all') 3 | 4 | var Buffer = require('safe-buffer').Buffer; 5 | var crypto = require('crypto'); 6 | var path = require('path'); 7 | var Pend = require('pend'); 8 | var rimraf = require('rimraf'); 9 | var fs = require('fs'); 10 | var http = require('http'); 11 | var net = require('net'); 12 | var stream = require('stream'); 13 | var assert = require('assert'); 14 | var multiparty = require('../'); 15 | var superagent = require('superagent'); 16 | var FIXTURE_PATH = path.join(__dirname, 'fixture'); 17 | var TMP_PATH = path.join(__dirname, 'tmp'); 18 | 19 | var standaloneTests = [ 20 | { 21 | name: 'chunked', 22 | fn: function(cb) { 23 | var server = http.createServer(function(req, resp) { 24 | var form = new multiparty.Form(); 25 | 26 | var partCount = 0; 27 | form.on('part', function(part) { 28 | part.resume(); 29 | partCount++; 30 | assert.strictEqual(typeof part.byteCount, 'undefined'); 31 | }); 32 | form.on('close', function() { 33 | assert.strictEqual(partCount, 1); 34 | resp.end(); 35 | }); 36 | 37 | form.parse(req); 38 | }); 39 | server.listen(function() { 40 | var socket = net.connect(server.address().port, 'localhost', function () { 41 | socket.write('POST / HTTP/1.1\r\n'); 42 | socket.write('Host: localhost\r\n'); 43 | socket.write('Connection: close\r\n'); 44 | socket.write('Content-Type: multipart/form-data; boundary=foo\r\n'); 45 | socket.write('Transfer-Encoding: chunked\r\n'); 46 | socket.write('\r\n'); 47 | socket.write('7\r\n'); 48 | socket.write('--foo\r\n\r\n'); 49 | socket.write('43\r\n'); 50 | socket.write('Content-Disposition: form-data; name="file"; filename="plain.txt"\r\n\r\n'); 51 | socket.write('12\r\n'); 52 | socket.write('\r\nsome text here\r\n\r\n'); 53 | socket.write('9\r\n'); 54 | socket.write('--foo--\r\n\r\n'); 55 | socket.write('0\r\n\r\n'); 56 | socket.resume(); 57 | socket.on('close', function () { 58 | server.close(cb); 59 | }); 60 | }); 61 | }); 62 | } 63 | }, 64 | { 65 | name: 'connection aborted closed', 66 | fn: function(cb) { 67 | var socket; 68 | var server = http.createServer(function (req, res) { 69 | var called = false; 70 | var form = new multiparty.Form(); 71 | 72 | form.parse(req, function (err, fields, files) { 73 | assert.ok(!called); 74 | called = true; 75 | 76 | assert.ifError(err); 77 | assert.equal(Object.keys(fields).length, 1); 78 | socket.end(); 79 | }); 80 | }); 81 | 82 | server.listen(0, 'localhost', function () { 83 | socket = net.connect(server.address().port, 'localhost', function () { 84 | socket.write('POST / HTTP/1.1\r\n'); 85 | socket.write('Host: localhost\r\n'); 86 | socket.write('Connection: close\r\n'); 87 | socket.write('Content-Type: multipart/form-data; boundary=foo\r\n'); 88 | socket.write('Transfer-Encoding: chunked\r\n'); 89 | socket.write('\r\n'); 90 | socket.write('7\r\n'); 91 | socket.write('--foo\r\n\r\n'); 92 | socket.write('2D\r\n'); 93 | socket.write('Content-Disposition: form-data; name="data"\r\n\r\n'); 94 | socket.write('12\r\n'); 95 | socket.write('\r\nsome text here\r\n\r\n'); 96 | socket.write('7\r\n'); 97 | socket.write('--foo--\r\n'); 98 | socket.write('2\r\n'); 99 | socket.write('\r\n\r\n'); 100 | socket.write('0\r\n\r\n'); 101 | socket.resume(); 102 | socket.on('close', function () { 103 | server.close(cb); 104 | }); 105 | }); 106 | }); 107 | } 108 | }, 109 | { 110 | name: 'connection aborted', 111 | fn: function(cb) { 112 | var server = http.createServer(function (req, res) { 113 | var form = new multiparty.Form(); 114 | var aborted_received = false; 115 | form.on('aborted', function () { 116 | aborted_received = true; 117 | }); 118 | form.on('error', function () { 119 | assert(aborted_received, 'Error event should follow aborted'); 120 | server.close(cb); 121 | }); 122 | form.on('end', function () { 123 | throw new Error('Unexpected "end" event'); 124 | }); 125 | form.on('close', function () { 126 | throw new Error('Unexpected "close" event'); 127 | }); 128 | form.parse(req); 129 | }).listen(0, 'localhost', function () { 130 | var client = net.connect(server.address().port); 131 | client.write( 132 | 'POST / HTTP/1.1\r\n' + 133 | 'Host: localhost\r\n' + 134 | 'Content-Length: 70\r\n' + 135 | 'Content-Type: multipart/form-data; boundary=foo\r\n\r\n') 136 | client.end(); 137 | }); 138 | } 139 | }, 140 | { 141 | name: 'connection aborted after close does error', 142 | fn: function (cb) { 143 | var body = 144 | '--foo\r\n' + 145 | 'Content-Disposition: form-data; name="file1"; filename="file1"\r\n' + 146 | 'Content-Type: application/octet-stream\r\n' + 147 | '\r\nThis is the file\r\n' + 148 | '--foo--\r\n' 149 | var client = null 150 | var error = null 151 | var server = http.createServer(function (req, res) { 152 | var form = new multiparty.Form() 153 | 154 | form.on('close', function () { 155 | client.destroy() 156 | setTimeout(function () { 157 | assert.ifError(error) 158 | server.close(cb) 159 | }, 100) 160 | }) 161 | 162 | form.on('error', function (err) { 163 | error = err 164 | }) 165 | 166 | form.on('part', function (part) { 167 | part.resume() 168 | }) 169 | 170 | form.parse(req) 171 | }).listen(0, 'localhost', function () { 172 | client = net.connect(server.address().port) 173 | client.write( 174 | 'POST / HTTP/1.1\r\n' + 175 | 'Host: localhost\r\n' + 176 | 'Content-Length: ' + Buffer.byteLength(body) + '\r\n' + 177 | 'Content-Type: multipart/form-data; boundary=foo\r\n\r\n' + 178 | body) 179 | }); 180 | } 181 | }, 182 | { 183 | name: 'content transfer encoding', 184 | fn: function(cb) { 185 | var server = http.createServer(function(req, res) { 186 | var form = new multiparty.Form(); 187 | form.uploadDir = TMP_PATH; 188 | form.on('close', function () { 189 | throw new Error('Unexpected "close" event'); 190 | }); 191 | form.on('end', function () { 192 | throw new Error('Unexpected "end" event'); 193 | }); 194 | form.on('error', function (e) { 195 | res.writeHead(e.status || 500); 196 | res.end(e.message); 197 | }); 198 | form.parse(req); 199 | }); 200 | 201 | server.listen(0, function() { 202 | var body = 203 | '--foo\r\n' + 204 | 'Content-Disposition: form-data; name="file1"; filename="file1"\r\n' + 205 | 'Content-Type: application/octet-stream\r\n' + 206 | '\r\nThis is the first file\r\n' + 207 | '--foo\r\n' + 208 | 'Content-Type: application/octet-stream\r\n' + 209 | 'Content-Disposition: form-data; name="file2"; filename="file2"\r\n' + 210 | 'Content-Transfer-Encoding: unknown\r\n' + 211 | '\r\nThis is the second file\r\n' + 212 | '--foo--\r\n'; 213 | 214 | var req = http.request({ 215 | method: 'POST', 216 | port: server.address().port, 217 | headers: { 218 | 'Content-Length': body.length, 219 | 'Content-Type': 'multipart/form-data; boundary=foo' 220 | } 221 | }); 222 | req.on('response', function (res) { 223 | assert.equal(res.statusCode, 400); 224 | res.on('data', function () {}); 225 | res.on('end', function () { 226 | server.close(cb); 227 | }); 228 | }); 229 | req.end(body); 230 | }); 231 | } 232 | }, 233 | { 234 | name: 'emit order', 235 | fn: function(cb) { 236 | var bigFile = path.join(FIXTURE_PATH, 'file', 'pf1y5.png') 237 | 238 | var server = http.createServer(function(req, res) { 239 | assert.strictEqual(req.url, '/upload'); 240 | assert.strictEqual(req.method, 'POST'); 241 | 242 | var fieldsInOrder = [ 243 | 'a', 244 | 'b', 245 | 'myimage.png', 246 | 'c' 247 | ]; 248 | 249 | var form = new multiparty.Form({ 250 | autoFields: true 251 | }); 252 | 253 | form.on('error', function (err) { 254 | assert.ifError(err); 255 | }); 256 | 257 | form.on('part', function(part) { 258 | assert.ok(part.filename); 259 | var expectedFieldName = fieldsInOrder.shift(); 260 | assert.strictEqual(part.name, expectedFieldName); 261 | part.resume(); 262 | }); 263 | 264 | form.on('field', function(name, value) { 265 | var expectedFieldName = fieldsInOrder.shift(); 266 | assert.strictEqual(name, expectedFieldName); 267 | }); 268 | 269 | form.on('close', function() { 270 | assert.strictEqual(fieldsInOrder.length, 0); 271 | res.end('OK') 272 | }); 273 | 274 | form.parse(req); 275 | }); 276 | server.listen(function() { 277 | var url = 'http://localhost:' + server.address().port + '/upload'; 278 | var req = superagent.post(url); 279 | req.field('a', 'a-value'); 280 | req.field('b', 'b-value'); 281 | req.attach('myimage.png', bigFile); 282 | req.field('c', 'hello'); 283 | req.on('error', function(err) { 284 | assert.ifError(err); 285 | }); 286 | req.on('response', function(res) { 287 | assert.equal(res.statusCode, 200); 288 | server.close(cb); 289 | }); 290 | req.end(); 291 | }); 292 | } 293 | }, 294 | { 295 | name: 'epilogue last chunk', 296 | fn: function(cb) { 297 | var server = http.createServer(function(req, res) { 298 | var form = new multiparty.Form(); 299 | 300 | var partCount = 0; 301 | form.on('part', function(part) { 302 | part.resume(); 303 | partCount++; 304 | }); 305 | form.on('close', function() { 306 | assert.strictEqual(partCount, 1); 307 | res.end(); 308 | }); 309 | 310 | form.parse(req); 311 | }); 312 | server.listen(function() { 313 | var socket = net.connect(server.address().port, 'localhost', function () { 314 | socket.write('POST / HTTP/1.1\r\n'); 315 | socket.write('Host: localhost\r\n'); 316 | socket.write('Connection: close\r\n'); 317 | socket.write('Content-Type: multipart/form-data; boundary=foo\r\n'); 318 | socket.write('Transfer-Encoding: chunked\r\n'); 319 | socket.write('\r\n'); 320 | socket.write('7\r\n'); 321 | socket.write('--foo\r\n\r\n'); 322 | socket.write('43\r\n'); 323 | socket.write('Content-Disposition: form-data; name="file"; filename="plain.txt"\r\n\r\n'); 324 | socket.write('12\r\n'); 325 | socket.write('\r\nsome text here\r\n\r\n'); 326 | socket.write('7\r\n'); 327 | socket.write('--foo--\r\n'); 328 | socket.write('2\r\n'); 329 | socket.write('\r\n\r\n'); 330 | socket.write('0\r\n\r\n'); 331 | socket.resume(); 332 | socket.on('close', function () { 333 | server.close(cb); 334 | }); 335 | }); 336 | }); 337 | } 338 | }, 339 | { 340 | name: 'error listen after parse', 341 | fn: function(cb) { 342 | var form = new multiparty.Form(); 343 | var req = new stream.Readable(); 344 | 345 | req.headers = {}; 346 | req._read = function(){ 347 | this.push(Buffer.from('--foo!')); 348 | }; 349 | 350 | form.parse(req); 351 | 352 | form.on('error', function(err){ 353 | // verification that error emitter when attached after form.parse 354 | assert.ok(err); 355 | cb(); 356 | }); 357 | } 358 | }, 359 | { 360 | name: 'error unpipe', 361 | fn: function(cb) { 362 | var err = null; 363 | var form = new multiparty.Form(); 364 | var pend = new Pend(); 365 | var req = new stream.Readable(); 366 | var unpiped = false; 367 | 368 | req.headers = { 369 | 'content-type': 'multipart/form-data; boundary=foo' 370 | }; 371 | req._read = function(){ 372 | this.push(Buffer.from('--foo!')); 373 | }; 374 | 375 | pend.go(function(cb){ 376 | form.on('error', function(e){ 377 | err = e; 378 | cb(); 379 | }); 380 | }); 381 | 382 | pend.go(function(cb){ 383 | form.on('unpipe', function(){ 384 | unpiped = true; 385 | cb(); 386 | }); 387 | }); 388 | 389 | pend.wait(function(){ 390 | // verification that error event implies unpipe call 391 | assert.ok(err); 392 | assert.ok(unpiped, 'req was unpiped'); 393 | 394 | assert.ok(!isReadableStreamFlowing(req), 'req not flowing') 395 | assert.equal(getReadableStreamPipeCount(req), 0, 'req has 0 pipes') 396 | cb(); 397 | }) 398 | 399 | form.parse(req) 400 | 401 | assert.ok(isReadableStreamFlowing(req), 'req flowing') 402 | assert.equal(getReadableStreamPipeCount(req), 1, 'req has 1 pipe') 403 | } 404 | }, 405 | { 406 | name: 'invalid', 407 | fn: function(cb) { 408 | var server = http.createServer(function(req, resp) { 409 | var form = new multiparty.Form(); 410 | 411 | form.on('error', function(err) { 412 | resp.end(); 413 | }); 414 | form.on('file', function(name, file) { 415 | }); 416 | form.on('field', function(name, file) { 417 | }); 418 | 419 | form.parse(req); 420 | }); 421 | server.listen(function() { 422 | var url = 'http://localhost:' + server.address().port + '/' 423 | var req = superagent.post(url) 424 | req.set('Content-Type', 'multipart/form-data; boundary=foo') 425 | req.write('--foo\r\n') 426 | req.write('Content-filename="foo.txt"\r\n') 427 | req.write('\r\n') 428 | req.write('some text here') 429 | req.write('Content-Disposition: form-data; name="text"; filename="bar.txt"\r\n') 430 | req.write('\r\n') 431 | req.write('some more text stuff') 432 | req.write('\r\n--foo--') 433 | req.end(function(err, resp) { 434 | resp.resume() 435 | server.close(cb); 436 | }); 437 | }); 438 | } 439 | }, 440 | { 441 | name: 'issue 15', 442 | fn: function(cb) { 443 | var server = http.createServer(function(req, res) { 444 | assert.strictEqual(req.url, '/upload'); 445 | assert.strictEqual(req.method, 'POST'); 446 | 447 | var form = new multiparty.Form({ autoFields: true, autoFiles: true }) 448 | 449 | form.on('error', function(err) { 450 | console.log(err); 451 | }); 452 | 453 | form.on('close', function() { 454 | }); 455 | 456 | var fileCount = 0; 457 | form.on('file', function(name, file) { 458 | fileCount += 1; 459 | fs.unlink(file.path, function () {}); 460 | }); 461 | 462 | form.parse(req, function(err, fields, files) { 463 | var objFileCount = Object.keys(files).length; 464 | // multiparty does NOT try to do intelligent things based on 465 | // the part name. 466 | assert.strictEqual(fileCount, 2); 467 | assert.strictEqual(objFileCount, 1); 468 | res.end(); 469 | }); 470 | }); 471 | server.listen(function() { 472 | var url = 'http://localhost:' + server.address().port + '/upload'; 473 | var req = superagent.post(url); 474 | req.attach('files[]', fixture('pf1y5.png'), 'SOG2.JPG'); 475 | req.attach('files[]', fixture('binaryfile.tar.gz'), 'BenF364_LIB353.zip'); 476 | 477 | req.end(function(err, resp) { 478 | assert.ifError(err); 479 | resp.resume() 480 | server.close(cb) 481 | }); 482 | 483 | // No space. 484 | createRequest(''); 485 | 486 | // Single space. 487 | createRequest(' '); 488 | 489 | // Multiple spaces. 490 | createRequest(' '); 491 | }); 492 | 493 | function createRequest(separator) { 494 | var url = 'http://localhost:' + server.address().port + '/upload'; 495 | var req = superagent.post(url); 496 | req.attach('files[]', fixture('pf1y5.png'), 'SOG2.JPG'); 497 | req.attach('files[]', fixture('binaryfile.tar.gz'), 'BenF364_LIB353.zip'); 498 | 499 | req.end(function(err, resp) { 500 | assert.ifError(err); 501 | // We don't close the server, to allow other requests to pass. 502 | }); 503 | } 504 | 505 | function fixture(name) { 506 | return path.join(FIXTURE_PATH, 'file', name) 507 | } 508 | } 509 | }, 510 | { 511 | name: 'maxFields error', 512 | fn: function(cb) { 513 | var client; 514 | var server = http.createServer(function (req, res) { 515 | var form = new multiparty.Form({ maxFields: 1 }) 516 | form.on('aborted', function () { 517 | throw new Error('did not expect aborted') 518 | }); 519 | var first = true; 520 | form.on('error', function (err) { 521 | assert.ok(first); 522 | first = false; 523 | client.end(); 524 | assert.ok(/maxFields/.test(err.message)); 525 | assert.equal(err.status, 413); 526 | server.close(cb); 527 | }); 528 | form.on('end', function () { 529 | throw new Error('Unexpected "end" event'); 530 | }); 531 | form.parse(req); 532 | }); 533 | server.listen(function() { 534 | client = net.connect(server.address().port); 535 | 536 | client.write('POST /upload HTTP/1.1\r\n' + 537 | 'Host: localhost\r\n' + 538 | 'Content-Length: 728\r\n' + 539 | 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 540 | '\r\n' + 541 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 542 | 'Content-Disposition: form-data; name="title"\r\n' + 543 | '\r\n' + 544 | 'foofoo' + 545 | '\r\n' + 546 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 547 | 'Content-Disposition: form-data; name="upload"; filename="blah1.txt"\r\n' + 548 | 'Content-Type: text/plain\r\n' + 549 | '\r\n' + 550 | 'hi1\r\n' + 551 | '\r\n' + 552 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n') 553 | }); 554 | } 555 | }, 556 | { 557 | name: 'maxFieldsSize error', 558 | fn: function(cb) { 559 | var client; 560 | var server = http.createServer(function (req, res) { 561 | var form = new multiparty.Form({ maxFieldsSize: 8 }) 562 | form.on('aborted', function () { 563 | throw new Error('did not expect aborted') 564 | }); 565 | var first = true; 566 | form.on('error', function (err) { 567 | assert.ok(first); 568 | first = false; 569 | client.end(); 570 | assert.ok(/maxFieldsSize/.test(err.message)); 571 | assert.equal(err.status, 413); 572 | server.close(cb); 573 | }); 574 | form.on('end', function () { 575 | throw new Error('Unexpected "end" event'); 576 | }); 577 | form.on('field', function () {}); 578 | form.parse(req); 579 | }); 580 | server.listen(function() { 581 | client = net.connect(server.address().port); 582 | 583 | client.write('POST /upload HTTP/1.1\r\n' + 584 | 'Host: localhost\r\n' + 585 | 'Content-Length: 678\r\n' + 586 | 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 587 | '\r\n' + 588 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 589 | 'Content-Disposition: form-data; name="title"\r\n' + 590 | '\r\n' + 591 | 'foofoo' + 592 | '\r\n' + 593 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 594 | 'Content-Disposition: form-data; name="text"\r\n' + 595 | '\r\n' + 596 | 'hi1\r\n' + 597 | '\r\n' + 598 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n') 599 | }); 600 | } 601 | }, 602 | { 603 | name: 'issue 21', 604 | fn: function(cb) { 605 | var client; 606 | var server = http.createServer(function(req, res) { 607 | var form = new multiparty.Form(); 608 | 609 | form.parse(req, function(err, fields, files) { 610 | if (err) { 611 | console.error(err.stack); 612 | return; 613 | } 614 | var nameCount = 0; 615 | var name; 616 | for (name in fields) { 617 | assert.strictEqual(name, 'title') 618 | nameCount += 1; 619 | 620 | var values = fields[name]; 621 | assert.strictEqual(values.length, 1); 622 | assert.strictEqual(values[0], 'foofoo') 623 | } 624 | assert.strictEqual(nameCount, 1); 625 | 626 | nameCount = 0; 627 | for (name in files) { 628 | assert.strictEqual(name, 'upload') 629 | nameCount += 1; 630 | 631 | var filesList = files[name]; 632 | assert.strictEqual(filesList.length, 4); 633 | filesList.forEach(assertAndUnlink); 634 | } 635 | 636 | assert.strictEqual(nameCount, 1); 637 | 638 | res.end(); 639 | client.end(); 640 | server.close(cb); 641 | 642 | function assertAndUnlink(file){ 643 | assert.strictEqual(file.fieldName, 'upload') 644 | fs.unlinkSync(file.path); 645 | } 646 | }); 647 | }); 648 | server.listen(function() { 649 | client = net.connect(server.address().port); 650 | 651 | client.write('POST /upload HTTP/1.1\r\n' + 652 | 'Host: localhost\r\n' + 653 | 'Content-Length: 726\r\n' + 654 | 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 655 | '\r\n' + 656 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 657 | 'Content-Disposition: form-data; name="title"\r\n' + 658 | '\r\n' + 659 | 'foofoo' + 660 | '\r\n' + 661 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 662 | 'Content-Disposition: form-data; name="upload"; filename="blah1.txt"\r\n' + 663 | 'Content-Type: text/plain\r\n' + 664 | '\r\n' + 665 | 'hi1\r\n' + 666 | '\r\n' + 667 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 668 | 'Content-Disposition: form-data; name="upload"; filename="blah2.txt"\r\n' + 669 | 'Content-Type: text/plain\r\n' + 670 | '\r\n' + 671 | 'hi2\r\n' + 672 | '\r\n' + 673 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 674 | 'Content-Disposition: form-data; name="upload"; filename="blah3.txt"\r\n' + 675 | 'Content-Type: text/plain\r\n' + 676 | '\r\n' + 677 | 'hi3\r\n' + 678 | '\r\n' + 679 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 680 | 'Content-Disposition: form-data; name="upload"; filename="blah4.txt"\r\n' + 681 | 'Content-Type: text/plain\r\n' + 682 | '\r\n' + 683 | 'hi4\r\n' + 684 | '\r\n' + 685 | '------WebKitFormBoundaryvfUZhxgsZDO7FXLF--\r\n' 686 | ); 687 | }); 688 | } 689 | }, 690 | { 691 | name: 'issue 32', 692 | fn: function(cb) { 693 | var client; 694 | var server = http.createServer(function(req, res) { 695 | var form = new multiparty.Form(); 696 | 697 | form.parse(req, function(err, fields, files) { 698 | if (err) { 699 | console.error(err.stack); 700 | return; 701 | } 702 | assert.strictEqual(files.image[0].originalFilename, '测试文档') 703 | fs.unlinkSync(files.image[0].path); 704 | res.end(); 705 | client.end(); 706 | server.close(cb); 707 | }); 708 | }); 709 | server.listen(function() { 710 | client = net.connect(server.address().port); 711 | 712 | client.write( 713 | 'POST /upload HTTP/1.1\r\n' + 714 | 'Host: localhost\r\n' + 715 | 'Accept: */*\r\n' + 716 | 'Content-Type: multipart/form-data; boundary="893e5556-f402-4fec-8180-c59333354c6f"\r\n' + 717 | 'Content-Length: 187\r\n' + 718 | '\r\n' + 719 | '--893e5556-f402-4fec-8180-c59333354c6f\r\n' + 720 | "Content-Disposition: form-data; name=\"image\"; filename*=utf-8''%E6%B5%8B%E8%AF%95%E6%96%87%E6%A1%A3\r\n" + 721 | '\r\n' + 722 | '\r\n' + 723 | '--893e5556-f402-4fec-8180-c59333354c6f--\r\n' 724 | ); 725 | }); 726 | } 727 | }, 728 | { 729 | name: 'issue 36', 730 | fn: function(cb) { 731 | var server = http.createServer(function(req, res) { 732 | var form = new multiparty.Form(); 733 | var endCalled = false; 734 | form.on('part', function(part) { 735 | part.on('end', function() { 736 | endCalled = true; 737 | }); 738 | part.resume(); 739 | }); 740 | form.on('close', function() { 741 | assert.ok(endCalled); 742 | res.end(); 743 | }); 744 | form.parse(req); 745 | }); 746 | server.listen(function() { 747 | var url = 'http://localhost:' + server.address().port + '/' 748 | var req = superagent.post(url) 749 | req.set('Content-Type', 'multipart/form-data; boundary=--WebKitFormBoundaryvfUZhxgsZDO7FXLF') 750 | req.set('Content-Length', '186') 751 | req.write('----WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n'); 752 | req.write('Content-Disposition: form-data; name="upload"; filename="blah1.txt"\r\n'); 753 | req.write('Content-Type: plain/text\r\n'); 754 | req.write('\r\n'); 755 | req.write('hi1\r\n'); 756 | req.write('\r\n'); 757 | req.write('----WebKitFormBoundaryvfUZhxgsZDO7FXLF--\r\n'); 758 | req.end(function(err, resp) { 759 | server.close(cb); 760 | }); 761 | }); 762 | } 763 | }, 764 | { 765 | name: 'issue 4', 766 | fn: function(cb) { 767 | var server = http.createServer(function(req, res) { 768 | assert.strictEqual(req.url, '/upload'); 769 | assert.strictEqual(req.method, 'POST'); 770 | 771 | var form = new multiparty.Form({ autoFields: true, autoFiles: true }) 772 | 773 | form.on('error', function(err) { 774 | console.log(err); 775 | }); 776 | 777 | form.on('close', function() { 778 | }); 779 | 780 | var fileCount = 0; 781 | form.on('file', function(name, file) { 782 | fileCount += 1; 783 | fs.unlink(file.path, function () {}); 784 | }); 785 | 786 | form.parse(req, function(err, fields, files) { 787 | var objFileCount = Object.keys(files).length; 788 | // multiparty does NOT try to do intelligent things based on 789 | // the part name. 790 | assert.strictEqual(fileCount, 2); 791 | assert.strictEqual(objFileCount, 1); 792 | res.end(); 793 | }); 794 | }); 795 | server.listen(function() { 796 | var url = 'http://localhost:' + server.address().port + '/upload'; 797 | var req = superagent.post(url); 798 | req.attach('files[]', fixture('pf1y5.png'), 'SOG2.JPG'); 799 | req.attach('files[]', fixture('binaryfile.tar.gz'), 'BenF364_LIB353.zip'); 800 | req.end(function(err, resp) { 801 | assert.ifError(err); 802 | resp.resume() 803 | server.close(cb) 804 | }); 805 | }); 806 | function fixture(name) { 807 | return path.join(FIXTURE_PATH, 'file', name) 808 | } 809 | } 810 | }, 811 | { 812 | name: 'max fields', 813 | fn: function(cb) { 814 | var server = http.createServer(function(req, res) { 815 | assert.strictEqual(req.url, '/upload'); 816 | assert.strictEqual(req.method, 'POST'); 817 | 818 | var form = new multiparty.Form({ autoFiles: true, maxFields: 2 }) 819 | 820 | var first = true; 821 | form.on('error', function (err) { 822 | assert.ok(first); 823 | first = false; 824 | assert.ok(/maxFields/.test(err.message)); 825 | assert.equal(err.status, 413); 826 | }); 827 | 828 | var fieldCount = 0; 829 | form.on('field', function() { 830 | fieldCount += 1; 831 | }); 832 | 833 | form.parse(req, function(err, fields, files) { 834 | assert.ok(!first); 835 | assert.ok(fieldCount <= 2); 836 | res.statusCode = 413; 837 | res.end('too many fields'); 838 | }); 839 | }); 840 | server.listen(function() { 841 | var url = 'http://localhost:' + server.address().port + '/upload'; 842 | var req = superagent.post(url); 843 | var val = Buffer.alloc(10 * 1024); 844 | req.field('a', val); 845 | req.field('b', val); 846 | req.field('c', val); 847 | req.on('error', function(err) { 848 | assert.strictEqual(err.message, 'Payload Too Large') 849 | }); 850 | req.end(); 851 | req.on('response', function(res) { 852 | assert.equal(res.statusCode, 413); 853 | server.close(cb); 854 | }); 855 | }); 856 | } 857 | }, 858 | { 859 | name: 'max files size exact', 860 | fn: function(cb) { 861 | var server = http.createServer(function(req, res) { 862 | assert.strictEqual(req.url, '/upload'); 863 | assert.strictEqual(req.method, 'POST'); 864 | 865 | var form = new multiparty.Form({ autoFiles: true, maxFilesSize: 768323 }) // exact size of pf1y5.png 866 | 867 | var fileCount = 0; 868 | form.on('file', function(name, file) { 869 | fileCount += 1; 870 | fs.unlink(file.path, function() {}); 871 | }); 872 | 873 | form.parse(req, function(err, fields, files) { 874 | assert.ifError(err); 875 | assert.ok(fileCount === 1); 876 | res.end('OK'); 877 | }); 878 | }); 879 | server.listen(function() { 880 | var url = 'http://localhost:' + server.address().port + '/upload'; 881 | var req = superagent.post(url); 882 | req.attach('file0', fixture('pf1y5.png'), 'SOG1.JPG'); 883 | req.on('error', function(err) { 884 | assert.ifError(err); 885 | }); 886 | req.end(); 887 | req.on('response', function(res) { 888 | assert.equal(res.statusCode, 200); 889 | server.close(cb); 890 | }); 891 | }); 892 | 893 | function fixture(name) { 894 | return path.join(FIXTURE_PATH, 'file', name) 895 | } 896 | } 897 | }, 898 | { 899 | name: 'max files size', 900 | fn: function(cb) { 901 | var server = http.createServer(function(req, res) { 902 | assert.strictEqual(req.url, '/upload'); 903 | assert.strictEqual(req.method, 'POST'); 904 | 905 | var form = new multiparty.Form({ autoFiles: true, maxFilesSize: 800 * 1024 }) 906 | 907 | var first = true; 908 | form.on('error', function (err) { 909 | assert.ok(first); 910 | first = false; 911 | assert.strictEqual(err.code, 'ETOOBIG'); 912 | assert.strictEqual(err.status, 413); 913 | }); 914 | 915 | var fileCount = 0; 916 | form.on('file', function(name, file) { 917 | fileCount += 1; 918 | fs.unlinkSync(file.path); 919 | }); 920 | 921 | form.parse(req, function(err, fields, files) { 922 | assert.ok(fileCount <= 1); 923 | res.statusCode = 413; 924 | res.end('files too large'); 925 | }); 926 | }); 927 | server.listen(function() { 928 | var url = 'http://localhost:' + server.address().port + '/upload'; 929 | var req = superagent.post(url); 930 | req.attach('file0', fixture('pf1y5.png'), 'SOG1.JPG'); 931 | req.attach('file1', fixture('pf1y5.png'), 'SOG2.JPG'); 932 | req.on('error', function(err) { 933 | assert.strictEqual(err.message, 'Payload Too Large') 934 | }); 935 | req.end(); 936 | req.on('response', function(res) { 937 | assert.equal(res.statusCode, 413); 938 | server.close(cb); 939 | }); 940 | }); 941 | 942 | function fixture(name) { 943 | return path.join(FIXTURE_PATH, 'file', name) 944 | } 945 | } 946 | }, 947 | { 948 | name: 'max files size edge', 949 | fn: function(cb) { 950 | var server = http.createServer(function(req, res) { 951 | assert.strictEqual(req.url, '/upload'); 952 | assert.strictEqual(req.method, 'POST'); 953 | 954 | var form = new multiparty.Form({ 955 | autoFiles: true, 956 | maxFilesSize: (768323 * 2) - 1 // exact size of 2 x pf1y5.png - 1 957 | }); 958 | 959 | var first = true; 960 | form.on('error', function (err) { 961 | assert.ok(first); 962 | first = false; 963 | assert.strictEqual(err.code, 'ETOOBIG'); 964 | assert.strictEqual(err.status, 413); 965 | }); 966 | 967 | var fileCount = 0; 968 | form.on('file', function(name, file) { 969 | fileCount += 1; 970 | fs.unlinkSync(file.path); 971 | }); 972 | 973 | form.parse(req, function(err, fields, files) { 974 | assert.ok(fileCount <= 2); 975 | res.statusCode = 413; 976 | res.end('files too large'); 977 | }); 978 | }); 979 | server.listen(function() { 980 | var url = 'http://localhost:' + server.address().port + '/upload'; 981 | var req = superagent.post(url); 982 | req.attach('file0', fixture('pf1y5.png'), 'SOG1.JPG'); 983 | req.attach('file1', fixture('pf1y5.png'), 'SOG1.JPG'); 984 | req.on('error', function(err) { 985 | assert.strictEqual(err.message, 'Payload Too Large') 986 | }); 987 | req.end(); 988 | req.on('response', function(res) { 989 | assert.equal(res.statusCode, 413); 990 | server.close(cb); 991 | }); 992 | }); 993 | 994 | function fixture(name) { 995 | return path.join(FIXTURE_PATH, 'file', name) 996 | } 997 | } 998 | }, 999 | { 1000 | name: 'missing boundary end', 1001 | fn: function(cb) { 1002 | var server = http.createServer(function(req, resp) { 1003 | var form = new multiparty.Form(); 1004 | 1005 | var errCount = 0; 1006 | form.on('error', function (err) { 1007 | assert.ok(err); 1008 | assert.equal(err.message, 'stream ended unexpectedly'); 1009 | assert.equal(err.status, 400); 1010 | errCount += 1; 1011 | resp.end(); 1012 | }); 1013 | form.on('part', function (part) { 1014 | part.resume(); 1015 | }); 1016 | form.on('close', function () { 1017 | assert.equal(errCount, 1); 1018 | }) 1019 | 1020 | form.parse(req); 1021 | }); 1022 | server.listen(function() { 1023 | var url = 'http://localhost:' + server.address().port + '/' 1024 | var req = superagent.post(url) 1025 | req.set('Content-Type', 'multipart/form-data; boundary=--WebKitFormBoundaryE19zNvXGzXaLvS5C') 1026 | req.write('----WebKitFormBoundaryE19zNvXGzXaLvS5C\r\n'); 1027 | req.write('Content-Disposition: form-data; name="a[b]"\r\n'); 1028 | req.write('\r\n'); 1029 | req.write('3\r\n'); 1030 | req.write('----WebKitFormBoundaryE19zNvXGzXaLvS5C\r\n'); 1031 | req.write('Content-Disposition: form-data; name="a[c]"\r\n'); 1032 | req.write('\r\n'); 1033 | req.write('4\r\n'); 1034 | req.write('----WebKitFormBoundaryE19zNvXGzXaLvS5C\r\n'); 1035 | req.write('Content-Disposition: form-data; name="file"; filename="test.txt"\r\n'); 1036 | req.write('Content-Type: plain/text\r\n'); 1037 | req.write('\r\n'); 1038 | req.write('and\r\n'); 1039 | req.write('----WebKitFormBoundaryE19zNvXGzXaLvS5C\r\n'); 1040 | req.end(function(err, resp) { 1041 | server.close(cb); 1042 | }); 1043 | }); 1044 | } 1045 | }, 1046 | { 1047 | name: 'missing content-type error', 1048 | fn: function(cb) { 1049 | var server = http.createServer(function(req, res) { 1050 | assert.strictEqual(req.url, '/upload'); 1051 | assert.strictEqual(req.method, 'POST'); 1052 | 1053 | var form = new multiparty.Form(); 1054 | 1055 | form.parse(req, function(err, fields, files) { 1056 | assert.ok(err); 1057 | assert.equal(err.message, 'missing content-type header'); 1058 | assert.equal(err.status, 415); 1059 | res.statusCode = 415; 1060 | res.end(); 1061 | }); 1062 | }); 1063 | server.listen(function() { 1064 | var url = 'http://localhost:' + server.address().port + '/upload'; 1065 | var req = superagent.post(url); 1066 | req.on('error', function(err) { 1067 | assert.strictEqual(err.message, 'Unsupported Media Type') 1068 | }); 1069 | req.end(); 1070 | req.on('response', function(res) { 1071 | assert.equal(res.statusCode, 415); 1072 | server.close(cb); 1073 | }); 1074 | }); 1075 | } 1076 | }, 1077 | { 1078 | name: 'unsupported content-type error', 1079 | fn: function(cb) { 1080 | var server = http.createServer(function(req, res) { 1081 | assert.strictEqual(req.url, '/upload'); 1082 | assert.strictEqual(req.method, 'POST'); 1083 | 1084 | var form = new multiparty.Form(); 1085 | 1086 | form.parse(req, function(err, fields, files) { 1087 | assert.ok(err); 1088 | assert.equal(err.message, 'unsupported content-type'); 1089 | assert.equal(err.status, 415); 1090 | res.statusCode = 415; 1091 | res.end(); 1092 | }); 1093 | }); 1094 | server.listen(function() { 1095 | var url = 'http://localhost:' + server.address().port + '/upload'; 1096 | var req = superagent.post(url); 1097 | req.set('Content-Type', 'application/json'); 1098 | req.write('{}'); 1099 | req.on('error', function(err) { 1100 | assert.strictEqual(err.message, 'Unsupported Media Type') 1101 | }); 1102 | req.end(); 1103 | req.on('response', function(res) { 1104 | assert.equal(res.statusCode, 415); 1105 | server.close(cb); 1106 | }); 1107 | }); 1108 | } 1109 | }, 1110 | { 1111 | name: 'content-type missing boundary error', 1112 | fn: function(cb) { 1113 | var server = http.createServer(function(req, res) { 1114 | assert.strictEqual(req.url, '/upload'); 1115 | assert.strictEqual(req.method, 'POST'); 1116 | 1117 | var form = new multiparty.Form(); 1118 | 1119 | form.parse(req, function(err, fields, files) { 1120 | assert.ok(err); 1121 | assert.equal(err.message, 'content-type missing boundary'); 1122 | assert.equal(err.status, 400); 1123 | res.statusCode = 400; 1124 | res.end(); 1125 | }); 1126 | }); 1127 | server.listen(function() { 1128 | var url = 'http://localhost:' + server.address().port + '/upload'; 1129 | var req = superagent.post(url); 1130 | req.attach('file0', fixture('pf1y5.png'), 'SOG1.JPG'); 1131 | req.on('error', function(err) { 1132 | assert.strictEqual(err.message, 'Bad Request') 1133 | }); 1134 | req.end(); 1135 | req.req.setHeader('Content-Type', 'multipart/form-data') 1136 | req.on('response', function(res) { 1137 | assert.equal(res.statusCode, 400); 1138 | server.close(cb); 1139 | }); 1140 | }); 1141 | 1142 | function fixture(name) { 1143 | return path.join(FIXTURE_PATH, 'file', name) 1144 | } 1145 | } 1146 | }, 1147 | { 1148 | name: 'empty header field error', 1149 | fn: function(cb) { 1150 | var server = http.createServer(function(req, resp) { 1151 | var form = new multiparty.Form(); 1152 | 1153 | var partCount = 0; 1154 | form.on('part', function(part) { 1155 | part.resume(); 1156 | partCount++; 1157 | assert.strictEqual(typeof part.byteCount, 'undefined'); 1158 | }); 1159 | form.on('error', function(err) { 1160 | assert.ok(err); 1161 | assert.equal(err.message, 'Empty header field'); 1162 | assert.equal(err.statusCode, 400); 1163 | assert.equal(partCount, 0); 1164 | server.close(cb); 1165 | }); 1166 | form.on('close', function() { 1167 | throw new Error('Unexpected "close" event'); 1168 | }); 1169 | 1170 | form.parse(req); 1171 | }); 1172 | server.listen(function() { 1173 | var socket = net.connect(server.address().port, 'localhost', function () { 1174 | socket.write('POST / HTTP/1.1\r\n'); 1175 | socket.write('Host: localhost\r\n'); 1176 | socket.write('Connection: close\r\n'); 1177 | socket.write('Content-Type: multipart/form-data; boundary=foo\r\n'); 1178 | socket.write('Transfer-Encoding: chunked\r\n'); 1179 | socket.write('\r\n'); 1180 | socket.write('7\r\n'); 1181 | socket.write('--foo\r\n\r\n'); 1182 | socket.write('46\r\n'); 1183 | socket.write('Content-Disposition: form-data; name="file"; filename="plain.txt"\r\n:\r\n\r\n'); 1184 | socket.write('12\r\n'); 1185 | socket.write('\r\nsome text here\r\n\r\n'); 1186 | socket.write('9\r\n'); 1187 | socket.write('--foo--\r\n\r\n'); 1188 | socket.write('0\r\n\r\n'); 1189 | socket.end(); 1190 | }); 1191 | }); 1192 | } 1193 | }, 1194 | { 1195 | name: 'request encoding', 1196 | fn: function(cb) { 1197 | var server = http.createServer(function(req, res) { 1198 | assert.strictEqual(req.url, '/upload'); 1199 | assert.strictEqual(req.method, 'POST'); 1200 | 1201 | var form = new multiparty.Form(); 1202 | 1203 | // this is invalid 1204 | req.setEncoding('utf8'); 1205 | 1206 | form.parse(req, function(err, fields, files) { 1207 | assert.ok(err); 1208 | assert.equal(err.message, 'request encoding must not be set'); 1209 | res.statusCode = 500; 1210 | res.end(); 1211 | }); 1212 | }); 1213 | server.listen(function() { 1214 | var url = 'http://localhost:' + server.address().port + '/upload'; 1215 | var req = superagent.post(url); 1216 | req.attach('file0', fixture('pf1y5.png'), 'SOG1.JPG'); 1217 | req.on('error', function(err) { 1218 | assert.strictEqual(err.message, 'Internal Server Error') 1219 | }); 1220 | req.end(); 1221 | req.on('response', function(res) { 1222 | assert.equal(res.statusCode, 500); 1223 | server.close(cb); 1224 | }); 1225 | }); 1226 | 1227 | function fixture(name) { 1228 | return path.join(FIXTURE_PATH, 'file', name) 1229 | } 1230 | } 1231 | }, 1232 | { 1233 | name: 'stream error', 1234 | fn: function(cb) { 1235 | var server = http.createServer(function (req, res) { 1236 | var form = new multiparty.Form(); 1237 | var gotPartErr; 1238 | form.on('part', function(part) { 1239 | part.on('error', function(err) { 1240 | gotPartErr = err; 1241 | }); 1242 | part.resume(); 1243 | }); 1244 | form.on('error', function () { 1245 | assert.ok(gotPartErr); 1246 | server.close(cb); 1247 | }); 1248 | form.on('close', function () { 1249 | throw new Error('Unexpected "close" event'); 1250 | }); 1251 | form.parse(req); 1252 | }).listen(0, 'localhost', function () { 1253 | var client = net.connect(server.address().port); 1254 | client.write( 1255 | 'POST / HTTP/1.1\r\n' + 1256 | 'Host: localhost\r\n' + 1257 | 'Content-Length: 186\r\n' + 1258 | 'Content-Type: multipart/form-data; boundary=--WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 1259 | '\r\n' + 1260 | '----WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 1261 | 'Content-Disposition: form-data; name="upload"; filename="blah1.txt"\r\n' + 1262 | 'Content-Type: plain/text\r\n' + 1263 | '\r\n' + 1264 | 'hi1\r\n') 1265 | client.end(); 1266 | }); 1267 | } 1268 | }, 1269 | { 1270 | name: 'queued part error', 1271 | fn: function(cb) { 1272 | var server = http.createServer(function (req, res) { 1273 | var form = new multiparty.Form(); 1274 | var pend = new Pend(); 1275 | 1276 | pend.go(function(cb){ 1277 | form.on('part', function(part){ 1278 | part.on('error', function(err){ 1279 | assert.ok(err); 1280 | assert.equal(err.message, 'stream ended unexpectedly'); 1281 | cb(); 1282 | }); 1283 | part.resume(); 1284 | }); 1285 | }); 1286 | 1287 | pend.go(function(cb){ 1288 | form.on('field', function(){ 1289 | cb(); 1290 | }); 1291 | }); 1292 | 1293 | pend.go(function(cb){ 1294 | form.on('error', function(err){ 1295 | assert.ok(err); 1296 | assert.equal(err.message, 'stream ended unexpectedly'); 1297 | cb(); 1298 | }); 1299 | }); 1300 | 1301 | pend.wait(function(){ 1302 | server.close(cb); 1303 | }); 1304 | 1305 | form.on('close', function () { 1306 | throw new Error('Unexpected "close" event'); 1307 | }); 1308 | 1309 | form.parse(req); 1310 | }).listen(0, 'localhost', function () { 1311 | var client = net.connect(server.address().port); 1312 | client.end( 1313 | 'POST / HTTP/1.1\r\n' + 1314 | 'Host: localhost\r\n' + 1315 | 'Content-Length: 174\r\n' + 1316 | 'Content-Type: multipart/form-data; boundary=--bounds\r\n' + 1317 | '\r\n' + 1318 | '----bounds\r\n' + 1319 | 'Content-Disposition: form-data; name="key"\r\n' + 1320 | '\r\n' + 1321 | 'hi\r\n' + 1322 | '----bounds\r\n' + 1323 | 'Content-Disposition: form-data; name="upload"; filename="blah1.txt"\r\n' + 1324 | 'Content-Type: plain/text\r\n' + 1325 | '\r\n' + 1326 | 'bye') 1327 | }); 1328 | } 1329 | }, 1330 | { 1331 | name: 'issue 198', 1332 | fn: function(cb) { 1333 | var client; 1334 | var server = http.createServer(function(req, res) { 1335 | var form = new multiparty.Form(); 1336 | 1337 | form.parse(req, function(err, fields, files) { 1338 | if (err) { 1339 | console.error(err.stack); 1340 | return; 1341 | } 1342 | assert.strictEqual(path.extname(files.image[0].path), '.y') 1343 | assert.strictEqual(files.image[0].originalFilename, 'x.y\u2028%24(echo subshell)') 1344 | fs.unlinkSync(files.image[0].path); 1345 | res.end(); 1346 | client.end(); 1347 | server.close(cb); 1348 | }); 1349 | }); 1350 | server.listen(function() { 1351 | client = net.connect(server.address().port); 1352 | 1353 | client.write( 1354 | 'POST /upload HTTP/1.1\r\n' + 1355 | 'Host: localhost\r\n' + 1356 | 'Accept: */*\r\n' + 1357 | 'Content-Type: multipart/form-data; boundary="893e5556-f402-4fec-8180-c59333354c6f"\r\n' + 1358 | 'Content-Length: 217\r\n' + 1359 | '\r\n' + 1360 | '--893e5556-f402-4fec-8180-c59333354c6f\r\n' + 1361 | "Content-Disposition: form-data; name=\"image\"; filename*=utf-8''%78%2E%79%E2%80%A8%24%28%65%63%68%6F%20%73%75%62%73%68%65%6C%6C%29\r\n" + 1362 | '\r\n' + 1363 | '\r\n' + 1364 | '--893e5556-f402-4fec-8180-c59333354c6f--\r\n' 1365 | ); 1366 | }); 1367 | } 1368 | }, 1369 | { 1370 | name: 'no part listener', 1371 | fn: function (cb) { 1372 | var server = http.createServer(function (req, res) { 1373 | var form = new multiparty.Form() 1374 | form.on('close', function () { 1375 | res.statusCode = 204 1376 | res.end() 1377 | }) 1378 | form.parse(req) 1379 | }) 1380 | server.listen(function () { 1381 | var url = 'http://localhost:' + server.address().port + '/' 1382 | var req = superagent.post(url) 1383 | req.set('Content-Type', 'multipart/form-data; boundary=--WebKitFormBoundaryvfUZhxgsZDO7FXLF') 1384 | req.set('Content-Length', '186') 1385 | req.write('----WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n') 1386 | req.write('Content-Disposition: form-data; name="upload"; filename="blah1.txt"\r\n') 1387 | req.write('Content-Type: plain/text\r\n') 1388 | req.write('\r\n') 1389 | req.write('hi1\r\n') 1390 | req.write('\r\n') 1391 | req.write('----WebKitFormBoundaryvfUZhxgsZDO7FXLF--\r\n') 1392 | req.end(function (err, resp) { 1393 | server.close(function () { 1394 | if (err) cb(err) 1395 | assert.strictEqual(resp.statusCode, 204) 1396 | cb() 1397 | }) 1398 | }) 1399 | }) 1400 | } 1401 | }, 1402 | { 1403 | name: 'no part listener, stream error', 1404 | fn: function (cb) { 1405 | var server = http.createServer(function (req, res) { 1406 | var form = new multiparty.Form() 1407 | form.on('error', function (err) { 1408 | assert.ok(err) 1409 | server.close(cb) 1410 | }) 1411 | form.on('close', function () { 1412 | throw new Error('Unexpected "close" event') 1413 | }) 1414 | form.parse(req) 1415 | }).listen(0, 'localhost', function () { 1416 | var client = net.connect(server.address().port) 1417 | client.write( 1418 | 'POST / HTTP/1.1\r\n' + 1419 | 'Host: localhost\r\n' + 1420 | 'Content-Length: 186\r\n' + 1421 | 'Content-Type: multipart/form-data; boundary=--WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 1422 | '\r\n' + 1423 | '----WebKitFormBoundaryvfUZhxgsZDO7FXLF\r\n' + 1424 | 'Content-Disposition: form-data; name="upload"; filename="blah1.txt"\r\n' + 1425 | 'Content-Type: plain/text\r\n' + 1426 | '\r\n' + 1427 | 'hi1\r\n') 1428 | client.end() 1429 | }) 1430 | } 1431 | } 1432 | ]; 1433 | 1434 | describe('multiparty', function () { 1435 | before(function (done) { 1436 | rimraf(TMP_PATH, function (err) { 1437 | if (err) return done(err) 1438 | fs.mkdir(TMP_PATH, done) 1439 | }) 1440 | }) 1441 | 1442 | after(function (done) { 1443 | rimraf(TMP_PATH, done) 1444 | }) 1445 | 1446 | describe('fixture tests', function () { 1447 | var fixtureServer = http.createServer() 1448 | var fixtureTests = requireAll(path.join(FIXTURE_PATH, 'js')) 1449 | 1450 | before(function (done) { 1451 | fixtureServer.listen(done) 1452 | }) 1453 | 1454 | after(function (done) { 1455 | fixtureServer.close(done) 1456 | }) 1457 | 1458 | Object.keys(fixtureTests).forEach(function (group) { 1459 | describe(group, function () { 1460 | Object.keys(fixtureTests[group]).forEach(function (name) { 1461 | it(path.basename(name, '.http'), 1462 | createFixtureTest(fixtureServer, (group + '/' + name), fixtureTests[group][name])) 1463 | }) 1464 | }) 1465 | }) 1466 | }) 1467 | 1468 | describe('standalone tests', function () { 1469 | standaloneTests.forEach(function (test) { 1470 | it(test.name, test.fn) 1471 | }) 1472 | }) 1473 | }) 1474 | 1475 | function createFixtureTest(server, name, fixture) { 1476 | return function(cb) { 1477 | uploadFixture(server, path.join(FIXTURE_PATH, 'http', name), function (err, parts) { 1478 | if (err) return cb(err) 1479 | fixture.forEach(function(expectedPart, i) { 1480 | var parsedPart = parts[i]; 1481 | assert.equal(parsedPart.type, expectedPart.type); 1482 | assert.equal(parsedPart.name, expectedPart.name); 1483 | 1484 | if (parsedPart.type === 'file') { 1485 | var file = parsedPart.value; 1486 | assert.equal(file.originalFilename, expectedPart.filename); 1487 | if(expectedPart.sha1) assert.strictEqual(file.hash, expectedPart.sha1); 1488 | if(expectedPart.size) assert.strictEqual(file.size, expectedPart.size); 1489 | } 1490 | }); 1491 | cb(); 1492 | }); 1493 | }; 1494 | } 1495 | 1496 | function computeSha1(o) { 1497 | return function(cb) { 1498 | var file = o.value; 1499 | var hash = fs.createReadStream(file.path).pipe(crypto.createHash('sha1')); 1500 | hash.read(); // work around pre-https://github.com/joyent/node/commit/4bf1d1007fbd249d1d07b662278a5a34c6be12fd 1501 | hash.on('data', function(digest) { 1502 | fs.unlinkSync(file.path); 1503 | file.hash = digest.toString('hex'); 1504 | cb(); 1505 | }); 1506 | }; 1507 | } 1508 | 1509 | function getReadableStreamPipeCount (stream) { 1510 | var count = stream._readableState.pipesCount 1511 | 1512 | return typeof count !== 'number' 1513 | ? stream._readableState.pipes.length 1514 | : count 1515 | } 1516 | 1517 | function isReadableStreamFlowing (stream) { 1518 | return Boolean(stream._readableState.flowing) 1519 | } 1520 | 1521 | function uploadFixture(server, path, cb) { 1522 | server.once('request', function(req, res) { 1523 | var done = false 1524 | var parts = []; 1525 | var form = new multiparty.Form({ 1526 | autoFields: true, 1527 | autoFiles: true 1528 | }); 1529 | form.uploadDir = TMP_PATH; 1530 | var pend = new Pend(); 1531 | 1532 | form.on('error', callback); 1533 | form.on('file', function(name, value) { 1534 | var o = { type: 'file', name: name, value: value } 1535 | parts.push(o); 1536 | pend.go(computeSha1(o)); 1537 | }); 1538 | form.on('field', function(name, value) { 1539 | parts.push({ type: 'field', name: name, value: value }) 1540 | }); 1541 | form.on('close', function() { 1542 | res.end('OK'); 1543 | pend.wait(function(err) { 1544 | if (err) throw err; 1545 | callback(null, parts); 1546 | }); 1547 | }); 1548 | form.parse(req); 1549 | 1550 | function callback() { 1551 | if (done) return 1552 | done = true 1553 | cb.apply(null, arguments) 1554 | } 1555 | }); 1556 | 1557 | var port = server.address().port 1558 | var socket = net.createConnection(port) 1559 | var file = fs.createReadStream(path) 1560 | 1561 | file.pipe(socket, { end: false }) 1562 | socket.on('data', function () { 1563 | socket.end(); 1564 | }); 1565 | } 1566 | --------------------------------------------------------------------------------