Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base fork: felixge/node-formidable
base: v0.9.5
...
head fork: felixge/node-formidable
compare: v0.9.6
  • 6 commits
  • 10 files changed
  • 0 commit comments
  • 1 contributor
Commits on Jul 16, 2010
Felix Geisendörfer Revert "Feature: IncomingForm.timeout"
This reverts commit 5e2612c.

There seems to be problems with this, causing non-stalled uploads to
report timeouts as well. Need to investigate.
15f0079
Commits on Jul 17, 2010
Felix Geisendörfer Fix: Handling of empty header values and field data
This patch introduces two new events to the multipart parser:
'onHeaderEnd' and 'onHeadersEnd'. These make it much easier to properly
handle the output of the parser.

Another nice addition is a system test that verifies a complete upload
from start to finish.
99c1354
Felix Geisendörfer Add .npmignore file 3b67d93
Felix Geisendörfer Make clean for test/tmp bf21f0c
Felix Geisendörfer Feature: IncomingForm.maxFieldSize
The parser is now considered safe. A malicious client can no longer
allocate huge amounts of memory by sending a big field.
d99eea0
Felix Geisendörfer Bump version 18de5c6
2  .npmignore
View
@@ -0,0 +1,2 @@
+test/tmp/
+*.upload
7 Makefile
View
@@ -1,4 +1,7 @@
test:
- @find test/{simple,integration}/test-*.js | xargs -n 1 -t node
+ @find test/{simple,integration,system}/test-*.js | xargs -n 1 -t node
-.PHONY: test
+clean:
+ rm test/tmp/*
+
+.PHONY: test clean
13 Readme.md
View
@@ -14,7 +14,6 @@ A node.js module for dealing with web forms.
### Todo
-* Limit buffer size for fields
* Implement QuerystringParser the same way as MultipartParser
## Installation
@@ -74,12 +73,6 @@ Creates a new incoming form.
The encoding to use for incoming form fields.
-#### incomingForm.timeout = 30000
-
-The time in ms after which a stalled upload causes an error. Can be set to
-`false` to disable this feature. The resulting error object will have a
-property `timeout` that is set to `true`.
-
#### incomingForm.uploadDir = '/tmp'
The directory for placing file uploads in. You can later on move them using `fs.rename()`.
@@ -92,6 +85,12 @@ If you want the files written to `incomingForm.uploadDir` to include the extensi
Either 'multipart' or 'urlencoded' depending on the incoming request.
+#### incomingForm.maxFieldSize = 2 * 1024 * 1024
+
+Limits the amount of memory a field (not file) can allocate in bytes.
+I this value is exceeded, an `'error'` event is emitted. The default
+size is 2MB.
+
#### incomingForm.bytesReceived
The amount of bytes received for this form so far.
80 lib/formidable/incoming_form.js
View
@@ -14,10 +14,10 @@ function IncomingForm() {
this.error = null;
this.ended = false;
+ this.maxFieldSize = 2 * 1024 * 1024;
this.keepExtensions = false;
this.uploadDir = '/tmp';
this.encoding = 'utf-8';
- this.timeout = 30000;
this.headers = null;
this.type = null;
@@ -26,6 +26,7 @@ function IncomingForm() {
this._parser = null;
this._flushing = 0;
+ this._fieldsSize = 0;
};
sys.inherits(IncomingForm, EventEmitter);
exports.IncomingForm = IncomingForm;
@@ -63,15 +64,6 @@ IncomingForm.prototype.parse = function(req, cb) {
this.writeHeaders(req.headers);
var self = this;
- if (this.timeout) {
- req.connection.setTimeout(this.timeout);
- req.connection.addListener('timeout', function() {
- var err = new Error('timeout, no data received for '+self.timeout+'ms')
- err.timeout = true;
- self._error(err);
- });
- }
-
req
.addListener('error', function(err) {
self._error(err);
@@ -156,13 +148,16 @@ IncomingForm.prototype.handlePart = function(part) {
, decoder = new StringDecoder(this.encoding);
part.addListener('data', function(buffer) {
+ self._fieldsSize += buffer.length;
+ if (self._fieldsSize > self.maxFieldSize) {
+ self._error(new Error('maxFieldSize exceeded, received '+self._fieldsSize+' bytes of field data'));
+ return;
+ }
value += decoder.write(buffer);
});
part.addListener('end', function() {
- if (value) {
- self.emit('field', part.name, value);
- }
+ self.emit('field', part.name, value);
});
return;
}
@@ -243,29 +238,9 @@ IncomingForm.prototype._initMultipart = function(boundary) {
var parser = new MultipartParser()
, self = this
- , headerField = ''
- , headerValue = ''
- , part
- , addHeader = function() {
- headerField = headerField.toLowerCase();
- part.headers[headerField] = headerValue;
-
- var m;
- if (headerField == 'content-disposition') {
- if (m = headerValue.match(/name="([^"]+)"/i)) {
- part.name = m[1];
- }
-
- if (m = headerValue.match(/filename="([^"]+)"/i)) {
- part.filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
- }
- } else if (headerField == 'content-type') {
- part.mime = headerValue;
- }
-
- headerField = '';
- headerValue = '';
- };
+ , headerField
+ , headerValue
+ , part;
parser.initWithBoundary(boundary);
@@ -275,12 +250,11 @@ IncomingForm.prototype._initMultipart = function(boundary) {
part.name = null;
part.filename = null;
part.mime = null;
+ headerField = '';
+ headerValue = '';
};
parser.onHeaderField = function(b, start, end) {
- if (headerValue) {
- addHeader();
- }
headerField += b.toString(self.encoding, start, end);
};
@@ -288,12 +262,32 @@ IncomingForm.prototype._initMultipart = function(boundary) {
headerValue += b.toString(self.encoding, start, end);
};
- parser.onPartData = function(b, start, end) {
- if (headerValue) {
- addHeader();
- self.onPart(part);
+ parser.onHeaderEnd = function() {
+ headerField = headerField.toLowerCase();
+ part.headers[headerField] = headerValue;
+
+ var m;
+ if (headerField == 'content-disposition') {
+ if (m = headerValue.match(/name="([^"]+)"/i)) {
+ part.name = m[1];
+ }
+
+ if (m = headerValue.match(/filename="([^"]+)"/i)) {
+ part.filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
+ }
+ } else if (headerField == 'content-type') {
+ part.mime = headerValue;
}
+ headerField = '';
+ headerValue = '';
+ };
+
+ parser.onHeadersEnd = function() {
+ self.onPart(part);
+ };
+
+ parser.onPartData = function(b, start, end) {
part.emit('data', b.slice(start, end));
};
2  lib/formidable/multipart_parser.js
View
@@ -181,6 +181,7 @@ MultipartParser.prototype.write = function(buffer) {
case S.HEADER_VALUE:
if (c == CR) {
dataCallback('headerValue', true);
+ callback('headerEnd');
state = S.HEADER_VALUE_ALMOST_DONE;
}
break;
@@ -195,6 +196,7 @@ MultipartParser.prototype.write = function(buffer) {
return i;
}
+ callback('headersEnd');
state = S.PART_DATA_START;
break;
case S.PART_DATA_START:
2  package.json
View
@@ -1,5 +1,5 @@
{ "name" : "formidable"
-, "version": "0.9.5"
+, "version": "0.9.6"
, "dependencies": {"gently": ">=0.7.0"}
, "directories" : { "lib" : "./lib/formidable" }
, "main" : "./lib/formidable/index"
BIN  test/fixture/multi_video.upload
View
Binary file not shown
28 test/integration/test-multipart-parser.js
View
@@ -15,9 +15,9 @@ Object.keys(fixtures).forEach(function(name) {
, parts = []
, part = null
- , headerField = null
- , headerValue = null
- , endCalled = false;
+ , headerField
+ , headerValue
+ , endCalled = '';
parser.initWithBoundary(fixture.boundary);
parser.onPartBegin = function() {
@@ -28,27 +28,21 @@ Object.keys(fixtures).forEach(function(name) {
};
parser.onHeaderField = function(b, start, end) {
- var str = b.toString('ascii', start, end);
- if (headerValue) {
- part.headers[headerField] = headerValue;
- headerField = '';
- headerValue = '';
- }
- headerField += str;
+ headerField += b.toString('ascii', start, end);
};
parser.onHeaderValue = function(b, start, end) {
- var str = b.toString('ascii', start, end);
- headerValue += str;
+ headerValue += b.toString('ascii', start, end);
}
+ parser.onHeaderEnd = function() {
+ part.headers[headerField] = headerValue;
+ headerField = '';
+ headerValue = '';
+ };
+
parser.onPartData = function(b, start, end) {
var str = b.toString('ascii', start, end);
- if (headerField) {
- part.headers[headerField] = headerValue;
- headerValue = '';
- headerField = '';
- }
part.data += b.binarySlice(start, end);
}
49 test/simple/test-incoming-form.js
View
@@ -29,33 +29,24 @@ test(function constructor() {
assert.strictEqual(form.keepExtensions, false);
assert.strictEqual(form.uploadDir, '/tmp');
assert.strictEqual(form.encoding, 'utf-8');
- assert.strictEqual(form.timeout, 30000);
assert.strictEqual(form.bytesReceived, null);
assert.strictEqual(form.bytesExpected, null);
+ assert.strictEqual(form.maxFieldSize, 2 * 1024 * 1024);
assert.strictEqual(form._parser, null);
assert.strictEqual(form._flushing, 0);
+ assert.strictEqual(form._fieldsSize, 0);
assert.ok(form instanceof EventEmitterStub);
assert.equal(form.constructor.name, 'IncomingForm');
});
test(function parse() {
- var REQ = {headers: {}, connection: {}}
- , emit = {}
- , reqEmit = {};
+ var REQ = {headers: {}}
+ , emit = {};
gently.expect(form, 'writeHeaders', function(headers) {
assert.strictEqual(headers, REQ.headers);
});
- gently.expect(REQ.connection, 'setTimeout', function(timeout) {
- assert.equal(timeout, form.timeout);
- });
-
- gently.expect(REQ.connection, 'addListener', function(event, fn) {
- assert.equal(event, 'timeout');
- reqEmit[event] = fn;
- });
-
var events = ['error', 'data', 'end'];
gently.expect(REQ, 'addListener', events.length, function(event, fn) {
assert.equal(event, events.shift());
@@ -126,15 +117,6 @@ test(function parse() {
assert.strictEqual(form.resume(), false);
})();
-
- (function testReqEmitTimeout() {
- gently.expect(form, '_error', function(err) {
- assert.equal(err.message, 'timeout, no data received for '+form.timeout+'ms');
- assert.equal(err.timeout, true);
- });
-
- reqEmit.timeout();
- })();
(function testEmitError() {
var ERR = new Error('something bad happened');
@@ -186,8 +168,6 @@ test(function parse() {
, REQ = {headers: {}}
, parseCalled = 0;
- form.timeout = null;
-
gently.expect(form, 'writeHeaders');
gently.expect(REQ, 'addListener', 3, function() {
return this;
@@ -423,8 +403,11 @@ test(function _initMultipart() {
PARSER.onHeaderField(new Buffer('content-disposition'), 10, 19);
PARSER.onHeaderValue(new Buffer('form-data; name="field1"'), 0, 14);
PARSER.onHeaderValue(new Buffer('form-data; name="field1"'), 14, 24);
+ PARSER.onHeaderEnd();
PARSER.onHeaderField(new Buffer('foo'), 0, 3);
PARSER.onHeaderValue(new Buffer('bar'), 0, 3);
+ PARSER.onHeaderEnd();
+ PARSER.onHeadersEnd();
PARSER.onPartData(new Buffer('hello world'), 0, 5);
PARSER.onPartData(new Buffer('hello world'), 5, 11);
PARSER.onPartEnd();
@@ -460,8 +443,11 @@ test(function _initMultipart() {
PARSER.onPartBegin();
PARSER.onHeaderField(new Buffer('content-disposition'), 0, 19);
PARSER.onHeaderValue(new Buffer('form-data; name="field2"; filename="C:\\Documents and Settings\\IE\\Must\\Die\\Sunset.jpg"'), 0, 85);
+ PARSER.onHeaderEnd();
PARSER.onHeaderField(new Buffer('Content-Type'), 0, 12);
PARSER.onHeaderValue(new Buffer('text/plain'), 0, 10);
+ PARSER.onHeaderEnd();
+ PARSER.onHeadersEnd();
PARSER.onPartData(new Buffer('... contents of file1.txt ...'), 0, 29);
PARSER.onPartEnd();
})();
@@ -566,6 +552,21 @@ test(function handlePart() {
PART.emit('end');
})();
+ (function testFieldSize() {
+ form.maxFieldSize = 8;
+ var PART = new events.EventEmitter();
+ PART.name = 'my_field';
+
+ gently.expect(form, '_error', function(err) {
+ assert.equal(err.message, 'maxFieldSize exceeded, received 9 bytes of field data');
+ });
+
+ form.handlePart(PART);
+ form._fieldsSize = 1;
+ PART.emit('data', new Buffer(7));
+ PART.emit('data', new Buffer(1));
+ })();
+
(function testFilePart() {
var PART = new events.EventEmitter()
, FILE = new events.EventEmitter()
52 test/system/test-multi-video-upload.js
View
@@ -0,0 +1,52 @@
+require('../common');
+var BOUNDARY = '---------------------------10102754414578508781458777923'
+ , FIXTURE = TEST_FIXTURES+'/multi_video.upload'
+ , fs = require('fs')
+ , sys = require('sys')
+ , http = require('http')
+ , formidable = require('formidable')
+ , server = http.createServer();
+
+server.addListener('request', function(req, res) {
+ var form = new formidable.IncomingForm()
+ , files = {};
+
+ form.uploadDir = TEST_TMP;
+ form.parse(req);
+
+ form
+ .addListener('field', function(field, value) {
+ assert.equal(field, 'title');
+ assert.equal(value, '');
+ })
+ .addListener('file', function(field, file) {
+ assert.equal(field, 'upload');
+ files[file.filename] = true;
+ })
+ .addListener('end', function() {
+ assert.deepEqual
+ ( files
+ , { 'shortest_video.flv': true
+ , 'shortest_video.mp4' :true
+ }
+ );
+ server.close();
+ res.writeHead(200);
+ res.end('good');
+ });
+});
+
+server.listen(TEST_PORT, function() {
+ var client = http.createClient(TEST_PORT)
+ , headers = {'content-type': 'multipart/form-data; boundary='+BOUNDARY}
+ , request = client.request('POST', '/', headers)
+ , fixture = new fs.ReadStream(FIXTURE);
+
+ fixture
+ .addListener('data', function(b) {;
+ request.write(b);
+ })
+ .addListener('end', function() {
+ request.end();
+ });
+});

No commit comments for this range

Something went wrong with that request. Please try again.