@dominiek/ftp-srv
Version:
Modern, extensible FTP Server
203 lines (188 loc) • 6.26 kB
JavaScript
const _ = require("lodash");
const uuid = require("uuid");
const Promise = require("bluebird");
const EventEmitter = require("events");
const BaseConnector = require("./connector/base");
const FileSystem = require("./fs");
const Commands = require("./commands");
const errors = require("./errors");
const DEFAULT_MESSAGE = require("./messages");
class FtpConnection extends EventEmitter {
constructor(server, options) {
super();
this.server = server;
this.id = uuid.v4();
this.log = options.log.child({ id: this.id, ip: this.ip });
this.commands = new Commands(this);
this.transferType = "binary";
this.encoding = "utf8";
this.bufferSize = false;
this._restByteCount = 0;
this._secure = false;
this.connector = new BaseConnector(this);
this._buffer = "";
this.commandSocket = options.socket;
this.commandSocket.on("error", (err) => {
this.log.error(err, "Client error");
this.server.emit("client-error", {
connection: this,
context: "commandSocket",
error: err,
});
});
this.commandSocket.on("data", this._handleData.bind(this));
this.commandSocket.on("timeout", () => {
this.log.trace("Client timeout");
this.close();
});
this.commandSocket.on("close", () => {
if (this.connector) this.connector.end();
if (this.commandSocket && !this.commandSocket.destroyed)
this.commandSocket.destroy();
this.removeAllListeners();
});
}
_handleData(data) {
if (!data.toString(this.encoding).match(/\r\n$/)) {
this._buffer += data.toString(this.encoding);
return;
}
const messages = _.compact(
(this._buffer + data.toString(this.encoding)).split("\r\n")
);
this._buffer = "";
this.log.trace(messages);
return Promise.mapSeries(messages, (message) =>
this.commands.handle(message)
);
}
get ip() {
try {
return this.commandSocket ? this.commandSocket.remoteAddress : undefined;
} catch (ex) {
return null;
}
}
get restByteCount() {
return this._restByteCount > 0 ? this._restByteCount : undefined;
}
set restByteCount(rbc) {
this._restByteCount = rbc;
}
get secure() {
return this.server.isTLS || this._secure;
}
set secure(sec) {
this._secure = sec;
}
close(code = 421, message = "Closing connection") {
return Promise.resolve(code)
.then((_code) => _code && this.reply(_code, message))
.finally(() => this.commandSocket && this.commandSocket.destroy());
}
login(username, password) {
return Promise.try(() => {
const loginListeners = this.server.listeners("login");
if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anonymous)
throw new errors.GeneralError('No "login" listener setup', 500);
} else {
return this.server.emitPromise("login", {
connection: this,
username,
password,
});
}
}).then(({ root, cwd, fs, blacklist = [], whitelist = [] } = {}) => {
this.authenticated = true;
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist);
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist);
this.fs = fs || new FileSystem(this, { root, cwd });
});
}
reply(options = {}, ...letters) {
const satisfyParameters = () => {
if (typeof options === "number") options = { code: options }; // allow passing in code as first param
if (!Array.isArray(letters)) letters = [letters];
if (!letters.length) letters = [{}];
return Promise.map(letters, (promise, index) => {
return Promise.resolve(promise).then((letter) => {
if (!letter) letter = {};
else if (typeof letter === "string") letter = { message: letter }; // allow passing in message as first param
if (!letter.socket)
letter.socket = options.socket
? options.socket
: this.commandSocket;
if (!options.useEmptyMessage) {
if (!letter.message)
letter.message =
DEFAULT_MESSAGE[options.code] || "No information";
if (!letter.encoding) letter.encoding = this.encoding;
}
return Promise.resolve(letter.message) // allow passing in a promise as a message
.then((message) => {
if (!options.useEmptyMessage) {
const seperator = !options.hasOwnProperty("eol")
? letters.length - 1 === index
? " "
: "-"
: options.eol
? " "
: "-";
message = !letter.raw
? _.compact([letter.code || options.code, message]).join(
seperator
)
: message;
letter.message = message;
} else {
letter.message = "";
}
return letter;
});
});
});
};
const processLetter = (letter) => {
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
this.log.trace(
{
port: letter.socket.address().port,
encoding: letter.encoding,
message: letter.message,
},
"Reply"
);
letter.socket.write(
letter.message + "\r\n",
letter.encoding,
(err) => {
if (err) {
this.log.error(err);
return reject(err);
}
resolve();
}
);
} else {
this.log.trace(
{ message: letter.message },
"Could not write message"
);
reject(new errors.SocketError("Socket not writable"));
}
});
};
return satisfyParameters()
.then((satisfiedLetters) =>
Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
})
)
.catch((err) => {
this.log.error(err);
});
}
}
module.exports = FtpConnection;