Compare commits

...

18 Commits

Author SHA1 Message Date
Tyler Stewart
cf3d543f1a fix(commands): correctly clone command for log 2017-05-05 18:04:54 -06:00
Tyler Stewart
69bec2b01c fix(fs): normalize fs paths
- Attempting to fix compatability on windows
2017-05-04 17:43:46 -06:00
Tyler Stewart
2eac41d127 test: update test setup
master
2017-05-04 17:43:41 -06:00
Tyler Stewart
eb32f93fc6 feat: close server on SIGINT (ctrl+c) 2017-05-04 17:42:47 -06:00
Tyler Stewart
095423606e fix(find-port): stop check at 65535 2017-05-04 17:42:10 -06:00
Tyler Stewart
61cf1bda39 feat(connection): add helper get for socket remote address 2017-05-04 17:40:37 -06:00
Tyler Stewart
75f847ed5d feat(commands): obfuscate password from logs 2017-05-04 17:40:01 -06:00
Tyler Stewart
ad4b32fc13 chore: add .env to github for tests 2017-05-04 17:39:15 -06:00
Tyler Stewart
be3c57bed0 Merge pull request #10 from stewarttylerr/sandbotorg-master
Sandbotorg master
2017-04-27 13:27:46 -06:00
Tyler Stewart
dc7dd1075c test(passive): merge master 2017-04-27 13:22:57 -06:00
salper
543e6cc1cc chore(readme): fix grammar 2017-04-27 13:22:39 -06:00
salper
5c1f8f7a65 fix: plug QUIT command
master
2017-04-27 13:19:29 -06:00
Tyler Stewart
557995a1a9 test(travis): add env variables 2017-04-27 13:17:01 -06:00
Tyler Stewart
45eca5afe0 test(passive): fix test 2017-04-27 12:49:55 -06:00
Tyler Stewart
695e594d97 Merge pull request #6 from stewarttylerr/migate-to-moment
feat: migrate to moment from date-fns, fix ls format
2017-03-31 17:14:44 -06:00
Tyler Stewart
97b55fc92c feat: migrate to moment from date-fns, fix ls format
Date-fns is great, but too early for use
2017-03-31 17:12:57 -06:00
Tyler Stewart
577066850b fix: improve getting current directory 2017-03-30 12:26:04 -06:00
Tyler Stewart
0ec989cf1e docs: update login event for new root option 2017-03-29 10:20:22 -06:00
17 changed files with 120 additions and 72 deletions

3
.env Executable file
View File

@@ -0,0 +1,3 @@
FTP_URL=ftp://127.0.0.1:8880
PASV_RANGE=8881
LOG_LEVEL=fatal

1
.gitignore vendored
View File

@@ -2,5 +2,4 @@ node_modules/
dist/
reports/
.env
npm-debug.log

View File

@@ -2,6 +2,10 @@ language: node_js
node_js:
- "6"
env:
FTP_URL: ftp://127.0.0.1:8880
PASV_RANGE: 8881
install: npm install
script:

View File

@@ -89,10 +89,13 @@ ftpServer.listen()
- __password__
- Password provided in the `PASS` command
- Only provided if `anonymous` is set to `false`
- __resolve ({fs, cwd, blacklist, whitelist})__
- __resolve ({fs, root, cwd, blacklist, whitelist})__
- __fs__ _[optional]_
- Optional file system class for connection to use
- See [File System](#file-system) for implementation details
- __root__ _[optional]_
- If `fs` not provided, will set the root directory for the connection
- The user cannot traverse lower than this directory
- __cwd__ _[optional]_
- If `fs` not provided, will set the starting directory for the connection
- __blacklist__ _[optional]_
@@ -129,7 +132,7 @@ Returns new directory relative to cwd
> Used in `CWD`, `CDUP`
`mkdir(path)`
Return a path to a newly created directory
Returns a path to a newly created directory
> Used in `MKD`
@@ -161,7 +164,7 @@ Modify a file or directory's permissions
> Used in `SITE CHMOD`
`getUniqueName()`
Return a unique file name to write to
Returns a unique file name to write to
> Used in `STOU`

View File

@@ -47,9 +47,9 @@
},
"dependencies": {
"bunyan": "^1.8.9",
"date-fns": "^1.28.2",
"lodash": "^4.17.4",
"minimist-string": "^1.0.2",
"moment": "^2.18.1",
"uuid": "^3.0.1",
"when": "^3.7.8"
},

View File

@@ -12,8 +12,13 @@ class FtpCommands {
}
handle(command) {
const log = this.connection.log.child({command});
log.trace('Handle command');
// Obfuscate password from logs
const logCommand = _.cloneDeep(command);
command.directive = _.upperCase(command._[0]);
if (command.directive === 'PASS') logCommand._[1] = '********';
const log = this.connection.log.child({directive: command.directive});
log.trace({command: logCommand}, 'Handle command');
if (!REGISTRY.hasOwnProperty(command.directive)) {
return this.connection.reply(402, 'Command not allowed');
@@ -40,7 +45,7 @@ class FtpCommands {
const handler = commandRegister.handler.bind(this.connection);
return when.try(handler, { log, command, previous_command: this.previousCommand })
.finally(() => {
this.previousCommand = _.clone(command);
this.previousCommand = _.cloneDeep(command);
});
}
}

View File

@@ -1,5 +1,5 @@
const when = require('when');
const format = require('date-fns/format');
const moment = require('moment');
module.exports = {
directive: 'MDTM',
@@ -9,7 +9,7 @@ module.exports = {
return when.try(this.fs.get.bind(this.fs), command._[1])
.then(fileStat => {
const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS');
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime);
})
.catch(err => {

View File

@@ -20,6 +20,7 @@ const commands = [
require('./registration/pasv'),
require('./registration/port'),
require('./registration/pwd'),
require('./registration/quit'),
require('./registration/retr'),
require('./registration/rmd'),
require('./registration/rnfr'),

View File

@@ -15,7 +15,7 @@ class FtpConnection {
this.server = server;
this.commandSocket = options.socket;
this.id = uuid.v4();
this.log = options.log.child({id: this.id});
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.encoding = 'utf-8';
@@ -28,7 +28,6 @@ class FtpConnection {
const messages = _.compact(data.toString('utf-8').split('\r\n'));
const handleMessage = message => {
const command = parseCommandString(message);
command.directive = _.upperCase(command._[0]);
return this.commands.handle(command);
};
@@ -41,13 +40,19 @@ class FtpConnection {
});
}
get ip() {
try {
return this.commandSocket.remoteAddress;
} catch (ex) {
return null;
}
}
close(code = 421, message = 'Closing connection') {
return when(() => {
if (code) return this.reply(code, message);
})
.then(() => {
if (this.commandSocket) this.commandSocket.end();
});
return when
.resolve(code)
.then(_code => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
}
login(username, password) {

View File

@@ -13,17 +13,24 @@ class FileSystem {
cwd = '/'
} = {}) {
this.connection = connection;
this.cwd = cwd;
this.root = root;
this.cwd = this._normalize(cwd);
this.root = this._normalize(root);
}
_normalize(path) {
return nodePath.normalize(path
.replace(/\\/g, '\/') // replaces \ with /
.replace(/\\\\/g, '\/') // replaces \\ with /
.replace(/\/\//g, '\/') // replaces // with /
);
}
_resolvePath(path) {
const pathParts = {
root: this.root,
base: nodePath.resolve(this.cwd, path)
base: nodePath.join(this.cwd, this._normalize(path))
};
path = nodePath.format(pathParts);
return path;
return nodePath.format(pathParts);
}
currentDirectory() {
@@ -60,7 +67,7 @@ class FileSystem {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = path.replace(new RegExp(`^${this.root}`), '') || '/';
this.cwd = path.substring(this.root.length) || '/';
return this.currentDirectory();
});
}

View File

@@ -1,5 +1,5 @@
const _ = require('lodash');
const dateFns = require('date-fns');
const moment = require('moment');
const errors = require('../errors');
module.exports = function (fileStat, format = 'ls') {
@@ -16,6 +16,10 @@ module.exports = function (fileStat, format = 'ls') {
};
function ls(fileStat) {
const now = moment.utc();
const mtime = moment.utc(new Date(fileStat.mtime));
const dateFormat = now.diff(mtime, 'months') < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
return [
fileStat.mode !== null
? [
@@ -35,7 +39,7 @@ function ls(fileStat) {
fileStat.uid,
fileStat.gid,
_.padStart(fileStat.size, 12),
_.padStart(dateFns.format(fileStat.mtime, 'MMM DD HH:mm'), 12),
_.padStart(mtime.format(dateFormat), 12),
fileStat.name
].join(' ');
}
@@ -44,7 +48,7 @@ function ep(fileStat) {
const facts = [
fileStat.dev && fileStat.ino ? `i${fileStat.dev.toString(16)}.${fileStat.ino.toString(16)}` : null,
fileStat.size ? `s${fileStat.size}` : null,
fileStat.mtime ? `m${dateFns.format(dateFns.parse(fileStat.mtime), 'X')}` : null,
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
fileStat.mode ? `up${fileStat.mode.toString(8).substr(fileStat.mode.toString(8).length - 3)}` : null,
fileStat.isDirectory() ? 'r' : '/'
].join(',');

View File

@@ -2,15 +2,15 @@ const net = require('net');
const when = require('when');
const errors = require('../errors');
module.exports = function (min = 22, max = undefined) {
module.exports = function (min = 1, max = undefined) {
return when.promise((resolve, reject) => {
let port = min;
let checkPort = min;
let portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
portCheckServer.on('error', () => {
if (!max || port < max) {
port = port + 1;
portCheckServer.listen(port);
if (checkPort < 65535 && (!max || checkPort < max)) {
checkPort = checkPort + 1;
portCheckServer.listen(checkPort);
} else {
reject(new errors.GeneralError('Unable to find open port', 500));
}
@@ -22,6 +22,6 @@ module.exports = function (min = 22, max = undefined) {
resolve(port);
});
});
portCheckServer.listen(port);
portCheckServer.listen(checkPort);
});
};

View File

@@ -37,6 +37,8 @@ class FtpServer {
});
this.on = this.server.on.bind(this.server);
this.listeners = this.server.listeners.bind(this.server);
process.on('SIGINT', () => this.close());
}
listen() {

View File

@@ -27,6 +27,7 @@ describe('Connector - Passive //', function () {
sandbox = sinon.sandbox.create();
sandbox.spy(mockConnection, 'reply');
sandbox.spy(mockConnection, 'close');
mockConnection.commandSocket.remoteAddress = '::ffff:127.0.0.1';
mockConnection.server.options.pasv_range = '8000';
@@ -45,7 +46,7 @@ describe('Connector - Passive //', function () {
});
it('has invalid pasv range', function (done) {
delete mockConnection.server.options.pasv_range;
mockConnection.server.options.pasv_range = -1;
passive.setupServer()
.then(() => done('should not happen'))
@@ -84,10 +85,13 @@ describe('Connector - Passive //', function () {
expect(passive.dataServer).to.exist;
const {port} = passive.dataServer.address();
net.createConnection(port, () => {
expect(mockConnection.reply.callCount).to.equal(1);
expect(mockConnection.reply.args[0][0]).to.equal(550);
done();
net.createConnection(port);
passive.dataServer.once('connection', () => {
setTimeout(() => {
expect(passive.connection.reply.callCount).to.equal(1);
expect(passive.connection.reply.args[0][0]).to.equal(550);
done();
}, 100);
});
})
.catch(done);

View File

@@ -1,5 +1,4 @@
const {expect} = require('chai');
const dateFns = require('date-fns');
const fileStat = require('../../src/helpers/file-stat');
const errors = require('../../src/errors');
@@ -17,24 +16,48 @@ describe('helpers // file-stat', function () {
size: 527,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
atime: 'Mon, 10 Oct 2017 23:24:11 GMT',
mtime: 'Mon, 10 Oct 2017 23:24:11 GMT',
ctime: 'Mon, 10 Oct 2017 23:24:11 GMT',
birthtime: 'Mon, 10 Oct 2017 23:24:11 GMT',
isDirectory: () => false
};
describe.skip('format - ls //', function () {
const STAT_OLD = {
name: 'test2',
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 84,
gid: 101,
rdev: 0,
size: 530,
blksize: 4096,
blocks: 8,
atime: 'Mon, 10 Oct 2011 14:05:12 GMT',
mtime: 'Mon, 10 Oct 2011 14:05:12 GMT',
ctime: 'Mon, 10 Oct 2011 14:05:12 GMT',
birthtime: 'Mon, 10 Oct 2011 14:05:12 GMT',
isDirectory: () => false
};
describe('format - ls //', function () {
it('formats correctly', () => {
const format = fileStat(STAT, 'ls');
expect(format).to.equal('-rwxrw-r-- 1 85 100 527 Oct 10 17:24 test1');
expect(format).to.equal('-rwxrw-r-- 1 85 100 527 Oct 10 23:24 test1');
});
it('formats correctly for files over 6 months old', () => {
const format = fileStat(STAT_OLD, 'ls');
expect(format).to.equal('-rwxrw-r-- 1 84 101 530 Oct 10 2011 test2');
});
});
describe.skip('format - ep //', function () {
describe('format - ep //', function () {
it('formats correctly', () => {
const format = fileStat(STAT, 'ep');
expect(format).to.equal('+i842.2dd69c9,s527,m1318289051,up644,/ test1');
expect(format).to.equal('+i842.2dd69c9,s527,m1507677851,up644,/ test1');
});
});

View File

@@ -1,5 +1,4 @@
/* eslint no-unused-expressions: 0 */
require('dotenv').load();
const {expect} = require('chai');
const bunyan = require('bunyan');
const fs = require('fs');
@@ -7,9 +6,12 @@ const fs = require('fs');
const FtpServer = require('../src');
const FtpClient = require('ftp');
before(() => require('dotenv').load());
describe('FtpServer', function () {
this.timeout(2000);
let log = bunyan.createLogger({name: 'test', level: 10});
let log = bunyan.createLogger({name: 'test'});
log.level(process.env.LOG_LEVEL || 'debug');
let server;
let client;
@@ -21,14 +23,9 @@ describe('FtpServer', function () {
server.on('login', (data, resolve) => {
resolve({root: process.cwd()});
});
process.on('SIGINT', function () {
server.close();
});
require('child_process').exec(`sudo kill $(sudo lsof -t -i:${server.url.port})`, () => {
server.listen()
.finally(() => done());
});
server.listen()
.then(() => done());
});
after(() => {
server.close();
@@ -38,6 +35,7 @@ describe('FtpServer', function () {
expect(server).to.exist;
client = new FtpClient();
client.once('ready', () => done());
client.once('error', err => done(err));
client.connect({
host: server.url.hostname,
port: server.url.port,
@@ -235,4 +233,10 @@ describe('FtpServer', function () {
});
});
it('QUIT', done => {
client.once('close', done);
client.logout(err => {
expect(err).to.be.undefined;
});
});
});

View File

@@ -1,16 +0,0 @@
require('dotenv').load();
const bunyan = require('bunyan');
const FtpServer = require('../src');
const log = bunyan.createLogger({name: 'test'});
log.level(process.env.LOG_LEVEL || 'trace');
const server = new FtpServer(process.env.FTP_URL, {
log,
pasv_range: process.env.PASV_RANGE
});
server.on('login', ({username, password}, resolve, reject) => {
if (username === 'test' && password === 'test') resolve({ root: require('os').homedir() });
else reject('Bad username or password');
});
server.listen();