This repository has been archived on 2022-07-06. You can view files and clone it, but cannot push or open issues or pull requests.
websocket-webcam/node_modules/jsftp/lib/jsftp.js

651 lines
18 KiB
JavaScript

/* vim:set ts=2 sw=2 sts=2 expandtab */
/*global require: true module: true */
/*
* @package jsftp
* @copyright Copyright(c) 2012 Ajax.org B.V. <info@c9.io>
* @author Sergi Mansilla <sergi.mansilla@gmail.com>
* @license https://github.com/sergi/jsFTP/blob/master/LICENSE MIT License
*/
var Net = require("net");
var EventEmitter = require("events").EventEmitter;
var es = require("event-stream");
var responseHandler = require("./response");
var Utils = require("./utils");
var util = require("util");
var fs = require("fs");
var FTP_PORT = 21;
var DEBUG_MODE = false;
var TIMEOUT = 10 * 60 * 1000;
var IDLE_TIME = 30000;
var COMMANDS = [
// Commands without parameters
"abor", "pwd", "cdup", "feat", "noop", "quit", "pasv", "syst",
// Commands with one or more parameters
"cwd", "dele", "list", "mdtm", "mkd", "mode", "nlst", "pass", "retr", "rmd",
"rnfr", "rnto", "site", "stat", "stor", "type", "user", "pass", "xrmd", "opts",
// Extended features
"chmod", "size"
];
var Cmds = {};
COMMANDS.forEach(function(cmd) {
cmd = cmd.toLowerCase();
Cmds[cmd] = function() {
var callback = function() {};
var completeCmd = cmd;
if (arguments.length) {
var args = Array.prototype.slice.call(arguments);
if (typeof args[args.length - 1] === "function")
callback = args.pop();
completeCmd += " " + args.join(" ");
}
this.execute(completeCmd.trim(), callback);
};
});
function once(fn) {
var returnValue, called = false;
return function() {
if (!called) {
called = true;
returnValue = fn.apply(this, arguments);
}
return returnValue;
};
}
var Ftp = module.exports = function(cfg) {
"use strict";
Object.keys(cfg).forEach(function(opt) {
if (!this[opt]) this[opt] = cfg[opt];
}, this);
EventEmitter.call(this);
// True if the server doesn't support the `stat` command. Since listing a
// directory or retrieving file properties is quite a common operation, it is
// more efficient to avoid the round-trip to the server.
this.useList = false;
this.port = this.port || FTP_PORT;
this.pending = []; // Pending requests
this.cmdBuffer_ = [];
this.responseHandler = responseHandler();
// Generate generic methods from parameter names. they can easily be
// overriden if we need special behavior. they accept any parameters given,
// it is the responsability of the user to validate the parameters.
var raw = this.raw = {};
COMMANDS.forEach(function(cmd) { raw[cmd] = Cmds[cmd].bind(this); }, this);
this.socket = this._createSocket(this.port, this.host);
};
util.inherits(Ftp, EventEmitter);
Ftp.prototype.reemit = function(event) {
var self = this;
return function(data) { self.emit(event, data); }
};
Ftp.prototype._createSocket = function(port, host, firstAction) {
if (this.socket && this.socket.destroy) this.socket.destroy();
this.authenticated = false;
var socket = Net.createConnection(port, host);
socket.on("connect", this.reemit("connect"));
socket.on("timeout", this.reemit("timeout"));
if (firstAction)
socket.once("connect", firstAction);
this._createStreams(socket);
return socket;
};
Ftp.prototype._createStreams = function(socket) {
this.pipeline = es.pipeline(
socket,
es.split(),
es.mapSync(this.responseHandler));
var self = this;
this.pipeline.on('data', function(data) {
self.emit('data', data);
self.parseResponse.call(self, data)
});
this.pipeline.on("error", this.reemit("error"));
};
Ftp.prototype.parseResponse = function(data) {
if (!this.cmdBuffer_.length)
return;
if ([220].indexOf(data.code) > -1)
return;
var next = this.cmdBuffer_[0][1];
if (Utils.isMark(data.code)) {
// If we receive a Mark and it is not expected, we ignore
// that command
if (!next.expectsMark || next.expectsMark.marks.indexOf(data.code) === -1)
return;
// We might have to ignore the command that comes after the
// mark.
if (next.expectsMark.ignore)
this.ignoreCmdCode = next.expectsMark.ignore;
}
if (this.ignoreCmdCode && this.ignoreCmdCode === data.code) {
this.ignoreCmdCode = null;
return;
}
this.parse(data, this.cmdBuffer_.shift());
};
/**
* Writes a new command to the server.
*
* @param {String} command Command to write in the FTP socket
* @returns void
*/
Ftp.prototype.send = function(command) {
if (!command || typeof command !== "string")
return;
this.emit("cmdSend", command);
this.pipeline.write(command + "\r\n");
};
Ftp.prototype.nextCmd = function() {
if (!this.inProgress && this.cmdBuffer_[0]) {
this.send(this.cmdBuffer_[0][0]);
this.inProgress = true;
}
};
/**
* Check whether the ftp user is authenticated at the moment of the
* enqueing. ideally this should happen in the `push` method, just
* before writing to the socket, but that would be complicated,
* since we would have to 'unshift' the auth chain into the queue
* or play the raw auth commands (that is, without enqueuing in
* order to not mess up the queue order. ideally, that would be
* built into the queue object. all this explanation to justify a
* slight slopiness in the code flow.
*
* @param {string} action
* @param {function} callback
* @return void
*/
Ftp.prototype.execute = function(action, callback) {
if (!callback) callback = function() {};
if (this.socket && this.socket.writable) {
this._executeCommand(action, callback);
} else {
var self = this;
this.authenticated = false;
this.socket = this._createSocket(this.port, this.host, function() {
self._executeCommand(action, callback);
});
}
};
Ftp.prototype._executeCommand = function(action, callback) {
var self = this;
function executeCmd() {
self.cmdBuffer_.push([action, callback]);
self.nextCmd();
}
if (self.authenticated || /feat|syst|user|pass/.test(action)) {
executeCmd();
} else {
this.getFeatures(function() {
self.auth(self.user, self.pass, executeCmd);
});
}
};
/**
* Parse is called each time that a comand and a request are paired
* together. That is, each time that there is a round trip of actions
* between the client and the server. The `action` param contains an array
* with the response from the server as a first element (text) and an array
* with the command executed and the callback (if any) as the second
* element.
*
* @param action {Array} Contains server response and client command info.
*/
Ftp.prototype.parse = function(response, command) {
// In FTP every response code above 399 means error in some way.
// Since the RFC is not respected by many servers, we are going to
// overgeneralize and consider every value above 399 as an error.
var err = null;
if (response.code > 399) {
err = new Error(response.text || "Unknown FTP error.");
err.code = response.code;
}
command[1](err, response);
this.inProgress = false;
this.nextCmd();
};
/**
* Returns true if the current server has the requested feature. False otherwise.
*
* @param {String} feature Feature to look for
* @returns {Boolean} Whether the current server has the feature
*/
Ftp.prototype.hasFeat = function(feature) {
if (feature)
return this.features.indexOf(feature.toLowerCase()) > -1;
};
/**
* Returns an array of features supported by the current FTP server
*
* @param {String} features Server response for the 'FEAT' command
* @returns {String[]} Array of feature names
*/
Ftp.prototype._parseFeats = function(features) {
// Ignore header and footer
return features.split(/\r\n|\n/).slice(1, -1).map(function(feat) {
return (/^\s*(\w*)\s*/).exec(feat)[1].trim().toLowerCase();
});
};
// Below this point all the methods are action helpers for FTP that compose
// several actions in one command
Ftp.prototype.getFeatures = function(callback) {
var self = this;
if (!this.features)
this.raw.feat(function(err, response) {
self.features = err ? [] : self._parseFeats(response.text);
self.raw.syst(function(err, res) {
if (!err && res.code === 215)
self.system = res.text.toLowerCase();
callback(null, self.features);
});
});
else
callback(null, self.features);
};
/**
* Authenticates the user.
*
* @param user {String} Username
* @param pass {String} Password
* @param callback {Function} Follow-up function.
*/
Ftp.prototype.auth = function(user, pass, callback) {
this.pending.push(callback);
var self = this;
function notifyAll(err, res) {
var cb;
while (cb = self.pending.shift())
cb(err, res);
}
if (this.authenticating) return;
if (!user) user = "anonymous";
if (!pass) pass = "@anonymous";
this.authenticating = true;
self.raw.user(user, function(err, res) {
if (!err && [230, 331, 332].indexOf(res.code) > -1) {
self.raw.pass(pass, function(err, res) {
self.authenticating = false;
if (err)
notifyAll(new Error("Login not accepted"));
if ([230, 202].indexOf(res.code) > -1) {
self.authenticated = true;
self.user = user;
self.pass = pass;
self.raw.type("I", function() {
notifyAll(null, res);
});
} else if (res.code === 332) {
self.raw.acct(""); // ACCT not really supported
}
});
} else {
self.authenticating = false;
notifyAll(new Error("Login not accepted"));
}
});
};
Ftp.prototype.setType = function(type, callback) {
if (this.type === type)
callback(null);
var self = this;
this.raw.type(type, function(err, data) {
if (!err) self.type = type;
callback(err, data);
});
};
/**
* Lists a folder's contents using a passive connection.
*
* @param {String} [path] Remote path for the file/folder to retrieve
* @param {Function} callback Function to call with errors or results
*/
Ftp.prototype.list = function(path, callback) {
if (arguments.length === 1) {
callback = arguments[0];
path = "";
}
var self = this;
var cb = function(err, listing) {
self.setType("I", once(function() {
callback(err, listing);
}));
};
cb.expectsMark = {
marks: [125, 150],
ignore: 226
};
var listing = "";
this.setType("A", function() {
self.getPasvSocket(function(err, socket) {
socket.on("data", function(data) {
listing += data;
});
socket.on("close", function(err) {
cb(err || null, listing);
});
socket.on("error", cb);
self.send("list " + (path || ""));
});
});
};
Ftp.prototype.emitProgress = function(data) {
this.emit('progress', {
filename: data.filename,
action: data.action,
total: data.totalSize || 0,
transferred: data.socket[
data.action === 'get' ? 'bytesRead' : 'bytesWritten']
});
};
/**
* Depending on the number of parameters, returns the content of the specified
* file or directly saves a file into the specified destination. In the latter
* case, an optional callback can be provided, which will receive the error in
* case the operation was not successful.
*
* @param {String} remotePath File to be retrieved from the FTP server
* @param {String} localPath Local path where the new file will be created
* @param {Function} [callback] Gets called on either success or failure
*/
Ftp.prototype.get = function(remotePath, localPath, callback) {
var self = this;
if (arguments.length === 2) {
callback = once(localPath || function() {});
this.getGetSocket(remotePath, callback);
} else {
callback = once(callback || function() {});
this.getGetSocket(remotePath, function(err, socket) {
if (err) {
callback(err);
}
var writeStream = fs.createWriteStream(localPath);
writeStream.on('error', callback);
socket.on('readable', function() {
self.emitProgress({
filename: remotePath,
action: 'get',
socket: this
});
});
socket.on('end', callback);
socket.pipe(writeStream);
socket.resume();
})
}
};
/**
* Returns a socket for a get (RETR) on a path. The socket is ready to be
* streamed, but it is returned in a paused state. It is left to the user to
* resume it.
*
* @param path {String} Path to the file to be retrieved
* @param callback {Function} Function to call when finalized, with the socket as a parameter
*/
Ftp.prototype.getGetSocket = function(path, callback) {
var self = this;
callback = once(callback);
this.getPasvSocket(function(err, socket) {
if (err) return cmdCallback(err);
socket.pause();
function cmdCallback(err, res) {
if (err) return callback(err);
if (res.code === 150)
callback(null, socket);
else
callback(new Error("Unexpected command " + res.text));
}
cmdCallback.expectsMark = {
marks: [125, 150],
ignore: 226
};
self.execute("retr " + path, cmdCallback);
});
};
/**
* Uploads contents on a FTP server. The `from` parameter can be a Buffer or the
* path for a local file to be uploaded.
*
* @param {String|Buffer} from Contents to be uploaded.
* @param {String} to path for the remote destination.
* @param {Function} callback Function to execute on error or success.
*/
Ftp.prototype.put = function(from, to, callback) {
if (from instanceof Buffer) {
this.getPutSocket(to, function(err, socket) {
if (!err) socket.end(from);
}, callback);
} else {
var self = this;
fs.exists(from, function(exists) {
if (!exists)
return callback(new Error("Local file doesn't exist."));
self.getPutSocket(to, function(err, socket) {
if (err) return;
fs.stat(from, function(err, stats) {
var totalSize = err ? 0 : stats.size;
var read = fs.createReadStream(from, {
bufferSize: 4 * 1024
});
read.pipe(socket);
read.on('readable', function() {
self.emitProgress({
filename: to,
action: 'put',
socket: read,
totalSize: totalSize
});
});
});
}, callback);
});
}
};
Ftp.prototype.getPutSocket = function(path, callback, doneCallback) {
if (!callback) throw new Error("A callback argument is required.");
doneCallback = once(doneCallback || function() {});
var _callback = once(function(err, _socket) {
if (err) {
callback(err);
return doneCallback(err);
}
return callback(err, _socket);
});
var self = this;
this.getPasvSocket(function(err, socket) {
if (err) return _callback(err);
var putCallback = once(function putCallback(err, res) {
if (err) return _callback(err);
// Mark 150 indicates that the 'STOR' socket is ready to receive data.
// Anything else is not relevant.
if (res.code === 150) {
socket.on('close', doneCallback);
socket.on('error', doneCallback);
_callback(null, socket);
} else {
return _callback(new Error("Unexpected command " + res.text));
}
});
putCallback.expectsMark = {
marks: [125, 150],
ignore: 226
};
self.execute("stor " + path, putCallback);
});
};
Ftp.prototype.getPasvSocket = function(callback) {
var timeout = this.timeout;
callback = once(callback || function() {});
this.execute("pasv", function(err, res) {
if (err) return callback(err);
var pasvRes = Utils.getPasvPort(res.text);
if (pasvRes === false)
return callback(new Error("PASV: Bad host/port combination"));
var host = pasvRes[0];
var port = pasvRes[1];
var socket = Net.createConnection(port, host);
socket.setTimeout(timeout || TIMEOUT);
callback(null, socket);
});
};
/**
* Provides information about files. It lists a directory contents or
* a single file and yields an array of file objects. The file objects
* contain several properties. The main difference between this method and
* 'list' or 'stat' is that it returns objects with the file properties
* already parsed.
*
* Example of file object:
*
* {
* name: 'README.txt',
* type: 0,
* time: 996052680000,
* size: '2582',
* owner: 'sergi',
* group: 'staff',
* userPermissions: { read: true, write: true, exec: false },
* groupPermissions: { read: true, write: false, exec: false },
* otherPermissions: { read: true, write: false, exec: false }
* }
*
* The constants used in the object are defined in ftpParser.js
*
* @param filePath {String} Path to the file or directory to list
* @param callback {Function} Function to call with the proper data when
* the listing is finished.
*/
Ftp.prototype.ls = function(filePath, callback) {
function entriesToList(err, entries) {
if (err) {
return callback(err);
}
callback(null, Utils.parseEntry(entries.text || entries));
}
if (this.useList) {
this.list(filePath, entriesToList);
} else {
var self = this;
this.raw.stat(filePath, function(err, data) {
// We might be connected to a server that doesn't support the
// 'STAT' command, which is set as default. We use 'LIST' instead,
// and we set the variable `useList` to true, to avoid extra round
// trips to the server to check.
if ((err && (err.code === 502 || err.code === 500)) ||
(self.system && self.system.indexOf("hummingbird") > -1))
// Not sure if the "hummingbird" system check ^^^ is still
// necessary. If they support any standards, the 500 error
// should have us covered. Let's leave it for now.
{
self.useList = true;
self.list(filePath, entriesToList);
} else {
entriesToList(err, data);
}
});
}
};
Ftp.prototype.rename = function(from, to, callback) {
var self = this;
this.raw.rnfr(from, function(err, res) {
if (err) return callback(err);
self.raw.rnto(to, function(err, res) {
callback(err, res);
});
});
};
Ftp.prototype.keepAlive = function() {
var self = this;
if (this._keepAliveInterval)
clearInterval(this._keepAliveInterval);
this._keepAliveInterval = setInterval(self.raw.noop, IDLE_TIME);
};
Ftp.prototype.destroy = function() {
if (this._keepAliveInterval)
clearInterval(this._keepAliveInterval);
this.socket.destroy();
this.features = null;
this.authenticated = false;
};