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.
		
		
		
		
		
			
		
			
				
					
					
						
							485 lines
						
					
					
						
							9.7 KiB
						
					
					
				
			
		
		
	
	
							485 lines
						
					
					
						
							9.7 KiB
						
					
					
				/**
 | 
						|
 * Parted (https://github.com/chjj/parted)
 | 
						|
 * A streaming multipart state parser.
 | 
						|
 * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed)
 | 
						|
 */
 | 
						|
 | 
						|
var fs = require('fs')
 | 
						|
  , path = require('path')
 | 
						|
  , EventEmitter = require('events').EventEmitter
 | 
						|
  , StringDecoder = require('string_decoder').StringDecoder
 | 
						|
  , set = require('qs').set
 | 
						|
  , each = Array.prototype.forEach;
 | 
						|
 | 
						|
/**
 | 
						|
 * Character Constants
 | 
						|
 */
 | 
						|
 | 
						|
var DASH = '-'.charCodeAt(0)
 | 
						|
  , CR = '\r'.charCodeAt(0)
 | 
						|
  , LF = '\n'.charCodeAt(0)
 | 
						|
  , COLON = ':'.charCodeAt(0)
 | 
						|
  , SPACE = ' '.charCodeAt(0);
 | 
						|
 | 
						|
/**
 | 
						|
 * Parser
 | 
						|
 */
 | 
						|
 | 
						|
var Parser = function(type, options) {
 | 
						|
  if (!(this instanceof Parser)) {
 | 
						|
    return new Parser(type, options);
 | 
						|
  }
 | 
						|
 | 
						|
  EventEmitter.call(this);
 | 
						|
 | 
						|
  this.writable = true;
 | 
						|
  this.readable = true;
 | 
						|
 | 
						|
  this.options = options || {};
 | 
						|
 | 
						|
  var key = grab(type, 'boundary');
 | 
						|
  if (!key) {
 | 
						|
    return this._error('No boundary key found.');
 | 
						|
  }
 | 
						|
 | 
						|
  this.key = new Buffer('\r\n--' + key);
 | 
						|
 | 
						|
  this._key = {};
 | 
						|
  each.call(this.key, function(ch) {
 | 
						|
    this._key[ch] = true;
 | 
						|
  }, this);
 | 
						|
 | 
						|
  this.state = 'start';
 | 
						|
  this.pending = 0;
 | 
						|
  this.written = 0;
 | 
						|
  this.writtenDisk = 0;
 | 
						|
  this.buff = new Buffer(200);
 | 
						|
 | 
						|
  this.preamble = true;
 | 
						|
  this.epilogue = false;
 | 
						|
 | 
						|
  this._reset();
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype.__proto__ = EventEmitter.prototype;
 | 
						|
 | 
						|
/**
 | 
						|
 * Parsing
 | 
						|
 */
 | 
						|
 | 
						|
Parser.prototype.write = function(data) {
 | 
						|
  if (!this.writable
 | 
						|
      || this.epilogue) return;
 | 
						|
 | 
						|
  try {
 | 
						|
    this._parse(data);
 | 
						|
  } catch (e) {
 | 
						|
    this._error(e);
 | 
						|
  }
 | 
						|
 | 
						|
  return true;
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype.end = function(data) {
 | 
						|
  if (!this.writable) return;
 | 
						|
 | 
						|
  if (data) this.write(data);
 | 
						|
 | 
						|
  if (!this.epilogue) {
 | 
						|
    return this._error('Message underflow.');
 | 
						|
  }
 | 
						|
 | 
						|
  return true;
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype._parse = function(data) {
 | 
						|
  var i = 0
 | 
						|
    , len = data.length
 | 
						|
    , buff = this.buff
 | 
						|
    , key = this.key
 | 
						|
    , ch
 | 
						|
    , val
 | 
						|
    , j;
 | 
						|
 | 
						|
  for (; i < len; i++) {
 | 
						|
    if (this.pos >= 200) {
 | 
						|
      return this._error('Potential buffer overflow.');
 | 
						|
    }
 | 
						|
 | 
						|
    ch = data[i];
 | 
						|
 | 
						|
    switch (this.state) {
 | 
						|
      case 'start':
 | 
						|
        switch (ch) {
 | 
						|
          case DASH:
 | 
						|
            this.pos = 3;
 | 
						|
            this.state = 'key';
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'key':
 | 
						|
        if (this.pos === key.length) {
 | 
						|
          this.state = 'key_end';
 | 
						|
          i--;
 | 
						|
        } else if (ch !== key[this.pos]) {
 | 
						|
          if (this.preamble) {
 | 
						|
            this.state = 'start';
 | 
						|
            i--;
 | 
						|
          } else {
 | 
						|
            this.state = 'body';
 | 
						|
            val = this.pos - i;
 | 
						|
            if (val > 0) {
 | 
						|
              this._write(key.slice(0, val));
 | 
						|
            }
 | 
						|
            i--;
 | 
						|
          }
 | 
						|
        } else {
 | 
						|
          this.pos++;
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'key_end':
 | 
						|
        switch (ch) {
 | 
						|
          case CR:
 | 
						|
            this.state = 'key_line_end';
 | 
						|
            break;
 | 
						|
          case DASH:
 | 
						|
            this.state = 'key_dash_end';
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            return this._error('Expected CR or DASH.');
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'key_line_end':
 | 
						|
        switch (ch) {
 | 
						|
          case LF:
 | 
						|
            if (this.preamble) {
 | 
						|
              this.preamble = false;
 | 
						|
            } else {
 | 
						|
              this._finish();
 | 
						|
            }
 | 
						|
            this.state = 'header_name';
 | 
						|
            this.pos = 0;
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            return this._error('Expected CR.');
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'key_dash_end':
 | 
						|
        switch (ch) {
 | 
						|
          case DASH:
 | 
						|
            this.epilogue = true;
 | 
						|
            this._finish();
 | 
						|
            return;
 | 
						|
          default:
 | 
						|
            return this._error('Expected DASH.');
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'header_name':
 | 
						|
        switch (ch) {
 | 
						|
          case COLON:
 | 
						|
            this.header = buff.toString('ascii', 0, this.pos);
 | 
						|
            this.pos = 0;
 | 
						|
            this.state = 'header_val';
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            buff[this.pos++] = ch | 32;
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'header_val':
 | 
						|
        switch (ch) {
 | 
						|
          case CR:
 | 
						|
            this.state = 'header_val_end';
 | 
						|
            break;
 | 
						|
          case SPACE:
 | 
						|
            if (this.pos === 0) {
 | 
						|
              break;
 | 
						|
            }
 | 
						|
            ; // FALL-THROUGH
 | 
						|
          default:
 | 
						|
            buff[this.pos++] = ch;
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'header_val_end':
 | 
						|
        switch (ch) {
 | 
						|
          case LF:
 | 
						|
            val = buff.toString('ascii', 0, this.pos);
 | 
						|
            this._header(this.header, val);
 | 
						|
            this.pos = 0;
 | 
						|
            this.state = 'header_end';
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            return this._error('Expected LF.');
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'header_end':
 | 
						|
        switch (ch) {
 | 
						|
          case CR:
 | 
						|
            this.state = 'head_end';
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            this.state = 'header_name';
 | 
						|
            i--;
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'head_end':
 | 
						|
        switch (ch) {
 | 
						|
          case LF:
 | 
						|
            this.state = 'body';
 | 
						|
            i++;
 | 
						|
            if (i >= len) return;
 | 
						|
            data = data.slice(i);
 | 
						|
            i = -1;
 | 
						|
            len = data.length;
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            return this._error('Expected LF.');
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 'body':
 | 
						|
        switch (ch) {
 | 
						|
          case CR:
 | 
						|
            if (i > 0) {
 | 
						|
              this._write(data.slice(0, i));
 | 
						|
            }
 | 
						|
            this.pos = 1;
 | 
						|
            this.state = 'key';
 | 
						|
            data = data.slice(i);
 | 
						|
            i = 0;
 | 
						|
            len = data.length;
 | 
						|
            break;
 | 
						|
          default:
 | 
						|
            // boyer-moore-like algorithm
 | 
						|
            // at felixge's suggestion
 | 
						|
            while ((j = i + key.length - 1) < len) {
 | 
						|
              if (this._key[data[j]]) break;
 | 
						|
              i = j;
 | 
						|
            }
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (this.state === 'body') {
 | 
						|
    this._write(data);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype._header = function(name, val) {
 | 
						|
  /*if (name === 'content-disposition') {
 | 
						|
    this.field = grab(val, 'name');
 | 
						|
    this.file = grab(val, 'filename');
 | 
						|
 | 
						|
    if (this.file) {
 | 
						|
      this.data = stream(this.file, this.options.path);
 | 
						|
    } else {
 | 
						|
      this.decode = new StringDecoder('utf8');
 | 
						|
      this.data = '';
 | 
						|
    }
 | 
						|
  }*/
 | 
						|
 | 
						|
  return this.emit('header', name, val);
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype._write = function(data) {
 | 
						|
  /*if (this.data == null) {
 | 
						|
    return this._error('No disposition.');
 | 
						|
  }
 | 
						|
 | 
						|
  if (this.file) {
 | 
						|
    this.data.write(data);
 | 
						|
    this.writtenDisk += data.length;
 | 
						|
  } else {
 | 
						|
    this.data += this.decode.write(data);
 | 
						|
    this.written += data.length;
 | 
						|
  }*/
 | 
						|
 | 
						|
  this.emit('data', data);
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype._reset = function() {
 | 
						|
  this.pos = 0;
 | 
						|
  this.decode = null;
 | 
						|
  this.field = null;
 | 
						|
  this.data = null;
 | 
						|
  this.file = null;
 | 
						|
  this.header = null;
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype._error = function(err) {
 | 
						|
  this.destroy();
 | 
						|
  this.emit('error', typeof err === 'string'
 | 
						|
    ? new Error(err)
 | 
						|
    : err);
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype.destroy = function(err) {
 | 
						|
  this.writable = false;
 | 
						|
  this.readable = false;
 | 
						|
  this._reset();
 | 
						|
};
 | 
						|
 | 
						|
Parser.prototype._finish = function() {
 | 
						|
  var self = this
 | 
						|
    , field = this.field
 | 
						|
    , data = this.data
 | 
						|
    , file = this.file
 | 
						|
    , part;
 | 
						|
 | 
						|
  this.pending++;
 | 
						|
 | 
						|
  this._reset();
 | 
						|
 | 
						|
  if (data && data.path) {
 | 
						|
    part = data.path;
 | 
						|
    data.end(next);
 | 
						|
  } else {
 | 
						|
    part = data;
 | 
						|
    next();
 | 
						|
  }
 | 
						|
 | 
						|
  function next() {
 | 
						|
    if (!self.readable) return;
 | 
						|
 | 
						|
    self.pending--;
 | 
						|
 | 
						|
    self.emit('part', field, part);
 | 
						|
 | 
						|
    if (data && data.path) {
 | 
						|
      self.emit('file', field, part, file);
 | 
						|
    }
 | 
						|
 | 
						|
    if (self.epilogue && !self.pending) {
 | 
						|
      self.emit('end');
 | 
						|
      self.destroy();
 | 
						|
    }
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Uploads
 | 
						|
 */
 | 
						|
 | 
						|
Parser.root = process.platform === 'win32'
 | 
						|
  ? 'C:/Temp'
 | 
						|
  : '/tmp';
 | 
						|
 | 
						|
/**
 | 
						|
 * Middleware
 | 
						|
 */
 | 
						|
 | 
						|
Parser.middleware = function(options) {
 | 
						|
  options = options || {};
 | 
						|
  return function(req, res, next) {
 | 
						|
    if (options.ensureBody) {
 | 
						|
      req.body = {};
 | 
						|
    }
 | 
						|
 | 
						|
    if (req.method === 'GET'
 | 
						|
        || req.method === 'HEAD'
 | 
						|
        || req._multipart) return next();
 | 
						|
 | 
						|
    req._multipart = true;
 | 
						|
 | 
						|
    var type = req.headers['content-type'];
 | 
						|
 | 
						|
    if (type) type = type.split(';')[0].trim().toLowerCase();
 | 
						|
 | 
						|
    if (type === 'multipart/form-data') {
 | 
						|
      Parser.handle(req, res, next, options);
 | 
						|
    } else {
 | 
						|
      next();
 | 
						|
    }
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Handler
 | 
						|
 */
 | 
						|
 | 
						|
Parser.handle = function(req, res, next, options) {
 | 
						|
  var parser = new Parser(req.headers['content-type'], options)
 | 
						|
    , diskLimit = options.diskLimit
 | 
						|
    , limit = options.limit
 | 
						|
    , parts = {}
 | 
						|
    , files = {};
 | 
						|
 | 
						|
  parser.on('error', function(err) {
 | 
						|
    req.destroy();
 | 
						|
    next(err);
 | 
						|
  });
 | 
						|
 | 
						|
  parser.on('part', function(field, part) {
 | 
						|
    set(parts, field, part);
 | 
						|
  });
 | 
						|
 | 
						|
  parser.on('file', function(field, path, name) {
 | 
						|
    set(files, field, {
 | 
						|
      path: path,
 | 
						|
      name: name,
 | 
						|
      toString: function() {
 | 
						|
        return path;
 | 
						|
      }
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  parser.on('data', function() {
 | 
						|
    if (this.writtenDisk > diskLimit || this.written > limit) {
 | 
						|
      this.emit('error', new Error('Overflow.'));
 | 
						|
      this.destroy();
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  parser.on('end', next);
 | 
						|
 | 
						|
  req.body = parts;
 | 
						|
  req.files = files;
 | 
						|
  req.pipe(parser);
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Helpers
 | 
						|
 */
 | 
						|
 | 
						|
var isWindows = process.platform === 'win32';
 | 
						|
 | 
						|
var stream = function(name, dir) {
 | 
						|
  var ext = path.extname(name) || ''
 | 
						|
    , name = path.basename(name, ext) || ''
 | 
						|
    , dir = dir || Parser.root
 | 
						|
    , tag;
 | 
						|
 | 
						|
  tag = Math.random().toString(36).substring(2);
 | 
						|
 | 
						|
  name = name.substring(0, 200) + '.' + tag;
 | 
						|
  name = path.join(dir, name) + ext.substring(0, 6);
 | 
						|
  name = name.replace(/\0/g, '');
 | 
						|
 | 
						|
  if (isWindows) {
 | 
						|
    name = name.replace(/[:*<>|"?]/g, '');
 | 
						|
  }
 | 
						|
 | 
						|
  return fs.createWriteStream(name);
 | 
						|
};
 | 
						|
 | 
						|
var grab = function(str, name) {
 | 
						|
  if (!str) return;
 | 
						|
 | 
						|
  var rx = new RegExp('\\b' + name + '\\s*=\\s*("[^"]+"|\'[^\']+\'|[^;,]+)', 'i')
 | 
						|
    , cap = rx.exec(str);
 | 
						|
 | 
						|
  if (cap) {
 | 
						|
    return cap[1].trim().replace(/^['"]|['"]$/g, '');
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Expose
 | 
						|
 */
 | 
						|
 | 
						|
module.exports = Parser; |