You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

217 lines
5.4 KiB

var sys = require("sys");
var events = require('events');
exports.parse = function(options) {
var promise = new events.Promise();
try {
var stream = new exports.Stream(options);
} catch (e) {
process.nextTick(function() {
promise.emitError(e);
});
return promise;
}
var parts = {};
stream.addListener('part', function(part) {
var name = part.name;
var buffer = '';
part.addListener('body', function(chunk) {
buffer = buffer + chunk;
});
part.addListener('complete', function() {
parts[name] = buffer;
});
});
stream.addListener('complete', function() {
promise.emitSuccess(parts);
});
return promise;
};
exports.Stream = function(options) {
events.EventEmitter.call(this);
this.init(options);
};
sys.inherits(exports.Stream, events.EventEmitter);
var proto = exports.Stream.prototype;
proto.init = function(options) {
this.buffer = '';
this.bytesReceived = 0;
this.bytesTotal = 0;
this.part = null;
if ('headers' in options) {
var req = options, contentType = req.headers['content-type'];
if (!contentType) {
throw new Error('Content-Type header not set');
}
if (!contentType.match(/^multipart\/form-data/i)) {
throw new Error('Content-Type is not multipart: "'+contentType+'"');
}
var boundary = contentType.match(/boundary=([^;]+)/i)
if (!boundary) {
throw new Error('No boundary in Content-Type header: "'+contentType+'"');
}
this.boundary = '--'+boundary[1];
this.bytesTotal = req.headers['content-length'];
var self = this;
req
.addListener('body', function(chunk) {
self.write(chunk);
})
.addListener('complete', function() {
self.emit('complete');
});
} else {
if (!options.boundary) {
throw new Error('No boundary option given');
}
this.boundary = options.boundary;
this.write(options.data || '');
}
};
proto.write = function(chunk) {
this.bytesReceived = this.bytesReceived + chunk.length;
this.buffer = this.buffer + chunk;
while (this.buffer.length) {
var offset = this.buffer.indexOf(this.boundary);
if (offset === 0) {
this.buffer = this.buffer.substr(offset + this.boundary.length + 2);
} else if (offset == -1) {
if (this.buffer === "\r\n") {
this.buffer = '';
} else {
this.part = (this.part || new Part(this));
this.part.write(this.buffer);
this.buffer = [];
}
} else if (offset > 0) {
this.part = (this.part || new Part(this));
this.part.write(this.buffer.substr(0, offset - 2));
this.part.emit('complete');
this.part = new Part(this);
this.buffer = this.buffer.substr(offset + this.boundary.length + 2);
}
}
};
function Part(stream) {
events.EventEmitter.call(this);
this.headers = {};
this.name = null;
this.filename = null;
this.buffer = '';
this.bytesReceived = 0;
// Avoids turning Part into a circular JSON object
this.getStream = function() {
return stream;
};
this._headersComplete = false;
}
sys.inherits(Part, events.EventEmitter);
Part.prototype.parsedHeaders = function() {
for (var header in this.headers) {
var parts = this.headers[header].split(/; ?/), parsedHeader = {};
for (var i = 0; i < parts.length; i++) {
var pair = parts[i].split('=');
if (pair.length < 2) {
continue;
}
var key = pair[0].toLowerCase(), val = pair[1] || '';
val = stripslashes(val).substr(1);
val = val.substr(0, val.length - 1);
parsedHeader[key] = val;
}
if (header == 'content-disposition') {
this.name = parsedHeader.name || null;
this.filename = parsedHeader.filename || null;
}
this.headers[header] = parsedHeader;
}
};
Part.prototype.write = function(chunk) {
if (this._headersComplete) {
this.bytesReceived = this.bytesReceived + chunk.length;
this.emit('body', chunk);
return;
}
this.buffer = this.buffer + chunk;
while (this.buffer.length) {
var offset = this.buffer.indexOf("\r\n");
if (offset === 0) {
this._headersComplete = true;
this.parsedHeaders();
this.getStream().emit('part', this);
this.buffer = this.buffer.substr(2);
this.bytesReceived = this.bytesReceived + this.buffer.length;
this.emit('body', this.buffer);
this.buffer = '';
return;
} else if (offset > 0) {
var header = this.buffer.substr(0, offset).split(/: ?/);
this.headers[header[0].toLowerCase()] = header[1];
this.buffer = this.buffer.substr(offset+2);
} else if (offset === false) {
return;
}
}
};
function stripslashes(str) {
// + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + improved by: Ates Goral (http://magnetiq.com)
// + fixed by: Mick@el
// + improved by: marrtins
// + bugfixed by: Onno Marsman
// + improved by: rezna
// + input by: Rick Waldron
// + reimplemented by: Brett Zamir (http://brett-zamir.me)
// * example 1: stripslashes('Kevin\'s code');
// * returns 1: "Kevin's code"
// * example 2: stripslashes('Kevin\\\'s code');
// * returns 2: "Kevin\'s code"
return (str+'').replace(/\\(.?)/g, function (s, n1) {
switch(n1) {
case '\\':
return '\\';
case '0':
return '\0';
case '':
return '';
default:
return n1;
}
});
}