Compare commits

..

10 Commits
logger ... v2

Author SHA1 Message Date
Tyler Stewart
6a907294b6 fix(fs): wrap fs methods in try
This handles throwing better than `resolve`
2018-07-05 10:58:32 -06:00
alancnet
1f15af0fb6 fix(cli): resolve authentication bug (#94)
cli would reject all logins with `530 Cannot destructure property `password` of 'undefined' or 'null'.` because the credentials object was being indexed with `[object Object]`. Even with that fixed, if the username was not found, it would produce that error.
2018-06-08 01:52:51 +00:00
Tyler Stewart
1cf1f750f4 chore(readme): update contributors 2018-06-02 18:51:27 +00:00
Diego Rodríguez Baquero
442490d713 chore(readme): correct word and improve events (#91) 2018-06-02 18:51:27 +00:00
Tyler Stewart
58b9ba27d9 test(fs): add fs tests 2018-05-31 14:28:10 +00:00
Tyler Stewart
87a2138cb3 fix(fs): improve path resolution 2018-05-31 14:28:10 +00:00
Tyler Stewart
9fd423c745 docs: fix circle ci badge
Links only to master branch
2018-05-25 17:34:25 -06:00
Tyler Stewart
363839ec8f chore: fix markdown newlines 2018-05-25 22:50:01 +00:00
Tyler Stewart
d9fc0c9cac feat: pasv_url option to send to client
This has passive connections to listen on the same hostname as the server.
But allows this to be customized via the `pasv_url` option.

Hostnames are no longer resolved if given `0.0.0.0`, except when being given to the client via `PASV`
2018-05-25 22:50:01 +00:00
Tyler Stewart
b0463d65b6 fix(passive): listen on server hostname 2018-05-25 22:50:01 +00:00
91 changed files with 689 additions and 551 deletions

109
README.md
View File

@@ -14,8 +14,8 @@
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://circleci.com/gh/trs/ftp-srv">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv.svg?style=for-the-badge" />
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
</a>
</p>
@@ -69,44 +69,50 @@ Supported protocols:
- `ftp` Plain FTP
- `ftps` Implicit FTP over TLS
_Note:_ The hostname must be the external IP address to accept external connections. Setting the hostname to `0.0.0.0` will automatically set the external IP.
_Note:_ The hostname must be the external IP address to accept external connections. `0.0.0.0` will listen on any available hosts for server and passive connections.
__Default:__ `"ftp://127.0.0.1:21"`
#### options
##### `pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
__Default:__ `"127.0.0.1"`
##### `pasv_range`
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
This range is then queried for an available port to use when required.
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
This range is then queried for an available port to use when required.
__Default:__ `22`
##### `greeting`
A human readable array of lines or string to send when a client connects.
A human readable array of lines or string to send when a client connects.
__Default:__ `null`
##### `tls`
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
__Default:__ `false`
##### `anonymous`
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
Can also set as a string which allows users to authenticate using the username provided.
The `login` event is then sent with the provided username and `@anonymous` as the password.
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
Can also set as a string which allows users to authenticate using the username provided.
The `login` event is then sent with the provided username and `@anonymous` as the password.
__Default:__ `false`
##### `blacklist`
Array of commands that are not allowed.
Response code `502` is sent to clients sending one of these commands.
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
Array of commands that are not allowed.
Response code `502` is sent to clients sending one of these commands.
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
__Default:__ `[]`
##### `whitelist`
Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command.
Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command.
__Default:__ `[]`
##### `file_format`
Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"`
Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"`
__Allowable values:__
- `ls` [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
- `ep` [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
@@ -114,7 +120,7 @@ __Allowable values:__
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
##### `log`
A [signale logger](https://github.com/klauscfhq/signale) instance. Created by default.
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
## CLI
@@ -171,15 +177,15 @@ The `FtpSrv` class extends the [node net.Server](https://nodejs.org/api/net.html
### `login`
```js
on('login', ({connection, username, password}, resolve, reject) => { ... });
ftpServer.on('login', ({connection, username, password}, resolve, reject) => { ... });
```
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
`connection` [client class object](src/connection.js)
`username` string of username from `USER` command
`password` string of password from `PASS` command
`resolve` takes an object of arguments:
`connection` [client class object](src/connection.js)
`username` string of username from `USER` command
`password` string of password from `PASS` command
`resolve` takes an object of arguments:
- `fs`
- Set a custom file system class for this connection to use.
- See [File System](#file-system) for implementation details.
@@ -198,45 +204,45 @@ Occurs when a client is attempting to login. Here you can resolve the login requ
### `client-error`
```js
on('client-error', ({connection, context, error}) => { ... });
ftpServer.on('client-error', ({connection, context, error}) => { ... });
```
Occurs when an error arises in the client connection.
`connection` [client class object](src/connection.js)
`context` string of where the error occurred
`connection` [client class object](src/connection.js)
`context` string of where the error occurred
`error` error object
### `RETR`
```js
on('RETR', (error, filePath) => { ... });
connection.on('RETR', (error, filePath) => { ... });
```
Occurs when a file is downloaded.
`error` if successful, will be `null`
`error` if successful, will be `null`
`filePath` location to which file was downloaded
### `STOR`
```js
on('STOR', (error, fileName) => { ... });
connection.on('STOR', (error, fileName) => { ... });
```
Occurs when a file is uploaded.
`error` if successful, will be `null`
`fileName` name of the file that was downloaded
`error` if successful, will be `null`
`fileName` name of the file that was uploaded
## Supported Commands
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
## File System
The default [file system](src/fs.js) can be overwritten to use your own implementation.
This can allow for virtual file systems, and more.
Each connection can set it's own file system based on the user.
The default [file system](src/fs.js) can be overwritten to use your own implementation.
This can allow for virtual file systems, and more.
Each connection can set it's own file system based on the user.
The default file system is exported and can be extended as needed:
The default file system is exported and can be extended as needed:
```js
const {FtpSrv, FileSystem} = require('ftp-srv');
@@ -255,52 +261,52 @@ Custom file systems can implement the following variables depending on the devel
### Methods
#### [`currentDirectory()`](src/fs.js#L29)
Returns a string of the current working directory
Returns a string of the current working directory
__Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L33)
Returns a file stat object of file or directory
Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L39)
Returns array of file and directory stat objects
Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L56)
Returns new directory relative to current directory
Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L96)
Returns a path to a newly created directory
Returns a path to a newly created directory
__Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L68)
Returns a writable stream
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
Returns a writable stream
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L75)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L87)
Delete a file or directory
Delete a file or directory
__Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L102)
Renames a file or directory
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L108)
Modifies a file or directory's permissions
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`getUniqueName()`](src/fs.js#L113)
Returns a unique file name to write to
Returns a unique file name to write to
__Used in:__ `STOU`
<!--[RM_CONTRIBUTING]-->
@@ -319,6 +325,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
- [pkeuter](https://github.com/pkeuter)
- [TimLuq](https://github.com/TimLuq)
- [edin-mg](https://github.com/edin-m)
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
<!--[RM_LICENSE]-->
## License

View File

@@ -62,7 +62,7 @@ function setupState(_args) {
_state.credentials = {};
const setCredentials = (username, password, root = null) => {
_state.credentials[_state.credentials] = {
_state.credentials[username] = {
password,
root
};
@@ -72,7 +72,7 @@ function setupState(_args) {
const credentialsFile = path.resolve(_args.credentials);
const credentials = require(credentialsFile);
for (const cred of Object.entries(credentials)) {
for (const cred of credentials) {
setCredentials(cred.username, cred.password, cred.root);
}
} else if (_args.username) {
@@ -97,9 +97,9 @@ function setupState(_args) {
function startFtpServer(_state) {
function checkLogin(data, resolve, reject) {
const {password, root} = _state.credentials[data.username];
if (_state.anonymous || password === data.password) {
return resolve({root: root || _state.root});
const user = _state.credentials[data.username]
if (_state.anonymous || (user && user.password === data.password)) {
return resolve({root: (user && user.root) || _state.root});
}
return reject(new errors.GeneralError('Invalid username or password', 401));

View File

@@ -53,10 +53,10 @@
},
"dependencies": {
"bluebird": "^3.5.1",
"bunyan": "^1.8.12",
"ip": "^1.1.5",
"lodash": "^4.17.10",
"moment": "^2.22.1",
"signale": "^1.0.1",
"uuid": "^3.2.1",
"yargs": "^11.0.0"
},

View File

@@ -36,16 +36,19 @@ class FtpCommands {
const logCommand = _.clone(command);
if (logCommand.directive === 'PASS') logCommand.arg = '********';
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');
}
if (_.includes(this.blacklist, command.directive)) {
return this.connection.reply(502, 'Command on blacklist');
return this.connection.reply(502, 'Command blacklisted');
}
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
return this.connection.reply(502, 'Command not on whitelist');
return this.connection.reply(502, 'Command not whitelisted');
}
const commandRegister = REGISTRY[command.directive];
@@ -58,7 +61,8 @@ class FtpCommands {
return this.connection.reply(502, 'Handler not set on command');
}
return Promise.try(() => commandRegister.handler.call(this, this.connection, command, this.previousCommand))
const handler = commandRegister.handler.bind(this.connection);
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
.finally(() => {
this.previousCommand = _.clone(command);
});

View File

@@ -1,16 +1,13 @@
module.exports = {
directive: 'ABOR',
handler: function (connection) {
return connection.connector.waitForConnection()
handler: function () {
return this.connector.waitForConnection()
.then(socket => {
return connection.reply(426, {socket})
.then(() => connection.connector.end())
.then(() => connection.reply(226));
return this.reply(426, {socket})
.then(() => this.connector.end())
.then(() => this.reply(226));
})
.catch(err => {
connection.emit('error', err);
return connection.reply(225);
});
.catch(() => this.reply(225));
},
syntax: '{{cmd}}',
description: 'Abort an active file transfer'

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'ALLO',
handler: function (connection) {
return connection.reply(202);
handler: function () {
return this.reply(202);
},
syntax: '{{cmd}}',
description: 'Allocate sufficient disk space to receive a file',

View File

@@ -2,8 +2,8 @@ const stor = require('./stor').handler;
module.exports = {
directive: 'APPE',
handler: function () {
return stor.call(this, ...arguments);
handler: function (args) {
return stor.call(this, args);
},
syntax: '{{cmd}} <path>',
description: 'Append to a file'

View File

@@ -3,12 +3,12 @@ const tls = require('tls');
module.exports = {
directive: 'AUTH',
handler: function (connection, command) {
handler: function ({command} = {}) {
const method = _.upperCase(command.arg);
switch (method) {
case 'TLS': return handleTLS.call(this, connection);
default: return connection.reply(504);
case 'TLS': return handleTLS.call(this);
default: return this.reply(504);
}
},
syntax: '{{cmd}} <type>',
@@ -19,24 +19,24 @@ module.exports = {
}
};
function handleTLS(connection) {
if (!connection.server._tls) return connection.reply(502);
if (connection.secure) return connection.reply(202);
function handleTLS() {
if (!this.server._tls) return this.reply(502);
if (this.secure) return this.reply(202);
return connection.reply(234)
return this.reply(234)
.then(() => {
const secureContext = tls.createSecureContext(connection.server._tls);
const secureSocket = new tls.TLSSocket(connection.commandSocket, {
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true,
secureContext
});
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
function forwardEvent() {
connection.emit.apply(this, arguments);
this.emit.apply(this, arguments);
}
secureSocket.on(event, forwardEvent.bind(connection.commandSocket, event));
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
});
connection.commandSocket = secureSocket;
connection.secure = true;
this.commandSocket = secureSocket;
this.secure = true;
});
}

View File

@@ -2,9 +2,9 @@ const cwd = require('./cwd').handler;
module.exports = {
directive: ['CDUP', 'XCUP'],
handler: function (connection, command, ...args) {
command.arg = '..';
return cwd.call(this, connection, command, ...args);
handler: function (args) {
args.command.arg = '..';
return cwd.call(this, args);
},
syntax: '{{cmd}}',
description: 'Change to Parent Directory'

View File

@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
module.exports = {
directive: ['CWD', 'XCWD'],
handler: function (connection, command) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.chdir) return connection.reply(402, 'Not supported by file system');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
return Promise.resolve(connection.fs.chdir(command.arg))
return Promise.try(() => this.fs.chdir(command.arg))
.then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return connection.reply(250, path);
return this.reply(250, path);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
log.error(err);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} <path>',

View File

@@ -2,17 +2,17 @@ const Promise = require('bluebird');
module.exports = {
directive: 'DELE',
handler: function (connection, command) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.delete) return connection.reply(402, 'Not supported by file system');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
return Promise.resolve(connection.fs.delete(command.arg))
return Promise.try(() => this.fs.delete(command.arg))
.then(() => {
return connection.reply(250);
return this.reply(250);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
log.error(err);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} <path>',

View File

@@ -8,14 +8,14 @@ const FAMILY = {
module.exports = {
directive: 'EPRT',
handler: function (connection, command) {
handler: function ({command} = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol];
if (!family) return connection.reply(504, 'Unknown network protocol');
if (!family) return this.reply(504, 'Unknown network protocol');
connection.connector = new ActiveConnector(connection);
return connection.connector.setupConnection(ip, port, family)
.then(() => connection.reply(200));
this.connector = new ActiveConnector(this);
return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200));
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'

View File

@@ -2,13 +2,13 @@ const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'EPSV',
handler: function (connection) {
connection.connector = new PassiveConnector(connection);
return connection.connector.setupServer()
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then(server => {
const {port} = server.address();
return connection.reply(229, `EPSV OK (|||${port}|)`);
return this.reply(229, `EPSV OK (|||${port}|)`);
});
},
syntax: '{{cmd}} [<protocol>]',

View File

@@ -2,7 +2,7 @@ const _ = require('lodash');
module.exports = {
directive: 'FEAT',
handler: function (connection) {
handler: function () {
const registry = require('../registry');
const features = Object.keys(registry)
.reduce((feats, cmd) => {
@@ -16,8 +16,8 @@ module.exports = {
raw: true
}));
return features.length
? connection.reply(211, 'Extensions supported', ...features, 'End')
: connection.reply(211, 'No features');
? this.reply(211, 'Extensions supported', ...features, 'End')
: this.reply(211, 'No features');
},
syntax: '{{cmd}}',
description: 'Get the feature list implemented by the server',

View File

@@ -2,18 +2,18 @@ const _ = require('lodash');
module.exports = {
directive: 'HELP',
handler: function (connection, command) {
handler: function ({command} = {}) {
const registry = require('../registry');
const directive = _.upperCase(command.arg);
if (directive) {
if (!registry.hasOwnProperty(directive)) return connection.reply(502, `Unknown command ${directive}.`);
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
const {syntax, description} = registry[directive];
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
return connection.reply(214, ...reply);
return this.reply(214, ...reply);
} else {
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
return connection.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
}
},
syntax: '{{cmd}} [<command>]',

View File

@@ -6,22 +6,22 @@ const getFileStat = require('../../helpers/file-stat');
// http://cr.yp.to/ftp/list/eplf.html
module.exports = {
directive: 'LIST',
handler: function (connection, command) {
if (!connection.fs) return this.reply(550, 'File system not instantiated');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
const simple = command.directive === 'NLST';
const path = command.arg || '.';
return connection.connector.waitForConnection()
.tap(() => connection.commandSocket.pause())
.then(() => Promise.resolve(connection.fs.get(path)))
.then(stat => stat.isDirectory() ? Promise.resolve(connection.fs.list(path)) : [stat])
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.get(path)))
.then(stat => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat])
.then(files => {
const getFileMessage = file => {
if (simple) return file.name;
return getFileStat(file, _.get(connection, 'server.options.file_format', 'ls'));
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
};
const fileList = files.map(file => {
@@ -29,26 +29,26 @@ module.exports = {
return {
raw: true,
message,
socket: connection.connector.socket
socket: this.connector.socket
};
});
return connection.reply(150)
return this.reply(150)
.then(() => {
if (fileList.length) return connection.reply({}, ...fileList);
if (fileList.length) return this.reply({}, ...fileList);
});
})
.then(() => connection.reply(226))
.then(() => this.reply(226))
.catch(Promise.TimeoutError, err => {
connection.emit('error', err);
return connection.reply(425, 'No connection established');
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
connection.emit('error', err);
return connection.reply(451, err.message || 'No directory');
log.error(err);
return this.reply(451, err.message || 'No directory');
})
.finally(() => {
connection.connector.end();
connection.commandSocket.resume();
this.connector.end();
this.commandSocket.resume();
});
},
syntax: '{{cmd}} [<path>]',

View File

@@ -3,18 +3,18 @@ const moment = require('moment');
module.exports = {
directive: 'MDTM',
handler: function (connection, command) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return Promise.resolve(connection.fs.get(command.arg))
return Promise.try(() => this.fs.get(command.arg))
.then(fileStat => {
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return connection.reply(213, modificationTime);
return this.reply(213, modificationTime);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
log.error(err);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} <path>',

View File

@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
module.exports = {
directive: ['MKD', 'XMKD'],
handler: function (connection, command) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.mkdir) return connection.reply(402, 'Not supported by file system');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
return Promise.resolve(connection.fs.mkdir(command.arg))
return Promise.try(() => this.fs.mkdir(command.arg))
.then(dir => {
const path = dir ? `"${escapePath(dir)}"` : undefined;
return connection.reply(257, path);
return this.reply(257, path);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
log.error(err);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} <path>',

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'MODE',
handler: function (connection, command) {
return connection.reply(/^S$/i.test(command.arg) ? 200 : 504);
handler: function ({command} = {}) {
return this.reply(/^S$/i.test(command.arg) ? 200 : 504);
},
syntax: '{{cmd}} <mode>',
description: 'Sets the transfer mode (Stream, Block, or Compressed)',

View File

@@ -2,8 +2,8 @@ const list = require('./list').handler;
module.exports = {
directive: 'NLST',
handler: function () {
return list.call(this, ...arguments);
handler: function (args) {
return list.call(this, args);
},
syntax: '{{cmd}} [<path>]',
description: 'Returns a list of file names in a specified directory'

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'NOOP',
handler: function (connection) {
return connection.reply(200);
handler: function () {
return this.reply(200);
},
syntax: '{{cmd}}',
description: 'No operation',

View File

@@ -7,14 +7,14 @@ const OPTIONS = {
module.exports = {
directive: 'OPTS',
handler: function (connection, command) {
if (!_.has(command, 'arg')) return connection.reply(501);
handler: function ({command} = {}) {
if (!_.has(command, 'arg')) return this.reply(501);
const [_option, ...args] = command.arg.split(' ');
const option = _.toUpper(_option);
if (!OPTIONS.hasOwnProperty(option)) return connection.reply(500);
return OPTIONS[option].call(this, ...args);
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
return OPTIONS[option].call(this, args);
},
syntax: '{{cmd}}',
description: 'Select options for a feature'

View File

@@ -1,20 +1,20 @@
module.exports = {
directive: 'PASS',
handler: function (connection, command) {
if (!connection.username) return this.reply(503);
handler: function ({log, command} = {}) {
if (!this.username) return this.reply(503);
if (this.authenticated) return this.reply(202);
// 332 : require account name (ACCT)
const password = command.arg;
if (!password) return this.reply(501, 'Must provide password');
return connection.login(connection.username, password)
return this.login(this.username, password)
.then(() => {
return connection.reply(230);
return this.reply(230);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(530, err.message || 'Authentication failed');
log.error(err);
return this.reply(530, err.message || 'Authentication failed');
});
},
syntax: '{{cmd}} <password>',

View File

@@ -2,17 +2,17 @@ const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'PASV',
handler: function (connection) {
connection.connector = new PassiveConnector(connection);
return connection.connector.setupServer()
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then(server => {
const address = connection.server.url.hostname;
const address = this.server.options.pasv_url;
const {port} = server.address();
const host = address.replace(/\./g, ',');
const portByte1 = port / 256 | 0;
const portByte2 = port % 256;
return connection.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
});
},
syntax: '{{cmd}}',

View File

@@ -1,9 +1,9 @@
module.exports = {
directive: 'PBSZ',
handler: function (connection, command) {
if (!connection.secure) return connection.reply(202, 'Not suppored');
connection.bufferSize = parseInt(command.arg, 10);
return connection.reply(200, connection.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
this.bufferSize = parseInt(command.arg, 10);
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
},
syntax: '{{cmd}}',
description: 'Protection Buffer Size',

View File

@@ -3,18 +3,18 @@ const ActiveConnector = require('../../connector/active');
module.exports = {
directive: 'PORT',
handler: function (connection, command) {
connection.connector = new ActiveConnector(connection);
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = _.get(command, 'arg', '').split(',');
if (rawConnection.length !== 6) return connection.reply(425);
if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).join('.');
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1];
return connection.connector.setupConnection(ip, port)
.then(() => connection.reply(200));
return this.connector.setupConnection(ip, port)
.then(() => this.reply(200));
},
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
description: 'Specifies an address and port to which the server should connect'

View File

@@ -2,16 +2,16 @@ const _ = require('lodash');
module.exports = {
directive: 'PROT',
handler: function (connection, command) {
if (!connection.secure) return connection.reply(202, 'Not suppored');
if (!connection.bufferSize && typeof connection.bufferSize !== 'number') return connection.reply(503);
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not suppored');
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
switch (_.toUpper(command.arg)) {
case 'P': return connection.reply(200, 'OK');
case 'P': return this.reply(200, 'OK');
case 'C':
case 'S':
case 'E': return connection.reply(536, 'Not supported');
default: return connection.reply(504);
case 'E': return this.reply(536, 'Not supported');
default: return this.reply(504);
}
},
syntax: '{{cmd}}',

View File

@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
module.exports = {
directive: ['PWD', 'XPWD'],
handler: function (connection) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.currentDirectory) return connection.reply(402, 'Not supported by file system');
handler: function ({log} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
return Promise.resolve(connection.fs.currentDirectory())
return Promise.try(() => this.fs.currentDirectory())
.then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return connection.reply(257, path);
return this.reply(257, path);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
log.error(err);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}}',

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'QUIT',
handler: function (connection) {
return connection.close(221, 'Client called QUIT');
handler: function () {
return this.close(221, 'Client called QUIT');
},
syntax: '{{cmd}}',
description: 'Disconnect',

View File

@@ -2,14 +2,14 @@ const _ = require('lodash');
module.exports = {
directive: 'REST',
handler: function (connection, command) {
handler: function ({command} = {}) {
const arg = _.get(command, 'arg');
const byteCount = parseInt(arg, 10);
if (isNaN(byteCount) || byteCount < 0) return connection.reply(501, 'Byte count must be 0 or greater');
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
connection.restByteCount = byteCount;
return connection.reply(350, `Restarting next transfer at ${byteCount}`);
this.restByteCount = byteCount;
return this.reply(350, `Restarting next transfer at ${byteCount}`);
},
syntax: '{{cmd}} <byte-count>',
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'

View File

@@ -2,54 +2,54 @@ const Promise = require('bluebird');
module.exports = {
directive: 'RETR',
handler: function (connection, command) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.read) return connection.reply(402, 'Not supported by file system');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
const filePath = command.arg;
return connection.connector.waitForConnection()
.tap(() => connection.commandSocket.pause())
.then(() => Promise.resolve(connection.fs.read(filePath, {start: connection.restByteCount})))
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
.then(stream => {
const destroyConnection = (conn, reject) => err => {
if (conn) conn.destroy(err);
const destroyConnection = (connection, reject) => err => {
if (connection) connection.destroy(err);
reject(err);
};
const eventsPromise = new Promise((resolve, reject) => {
stream.on('data', data => {
if (stream) stream.pause();
if (connection.connector.socket) {
connection.connector.socket.write(data, connection.transferType, () => stream && stream.resume());
if (this.connector.socket) {
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
}
});
stream.once('end', () => resolve());
stream.once('error', destroyConnection(connection.connector.socket, reject));
stream.once('error', destroyConnection(this.connector.socket, reject));
connection.connector.socket.once('error', destroyConnection(stream, reject));
this.connector.socket.once('error', destroyConnection(stream, reject));
});
connection.restByteCount = 0;
this.restByteCount = 0;
return connection.reply(150).then(() => stream.resume() && connection.connector.socket.resume())
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
.then(() => eventsPromise)
.tap(() => connection.emit('RETR', null, filePath))
.tap(() => this.emit('RETR', null, filePath))
.finally(() => stream.destroy && stream.destroy());
})
.then(() => connection.reply(226))
.then(() => this.reply(226))
.catch(Promise.TimeoutError, err => {
connection.emit('error', err);
return connection.reply(425, 'No connection established');
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
connection.emit('error', err);
connection.emit('RETR', err);
return connection.reply(551, err.message);
log.error(err);
this.emit('RETR', err);
return this.reply(551, err.message);
})
.finally(() => {
connection.connector.end();
connection.commandSocket.resume();
this.connector.end();
this.commandSocket.resume();
});
},
syntax: '{{cmd}} <path>',

View File

@@ -2,8 +2,8 @@ const {handler: dele} = require('./dele');
module.exports = {
directive: ['RMD', 'XRMD'],
handler: function (...args) {
return dele.call(this, ...args);
handler: function (args) {
return dele.call(this, args);
},
syntax: '{{cmd}} <path>',
description: 'Remove a directory'

View File

@@ -2,19 +2,19 @@ const Promise = require('bluebird');
module.exports = {
directive: 'RNFR',
handler: function (connection, command) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
const fileName = command.arg;
return Promise.resolve(connection.fs.get(fileName))
return Promise.try(() => this.fs.get(fileName))
.then(() => {
connection.renameFrom = fileName;
return connection.reply(350);
this.renameFrom = fileName;
return this.reply(350);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
log.error(err);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} <name>',

View File

@@ -2,25 +2,25 @@ const Promise = require('bluebird');
module.exports = {
directive: 'RNTO',
handler: function (connection, command) {
if (!connection.renameFrom) return connection.reply(503);
handler: function ({log, command} = {}) {
if (!this.renameFrom) return this.reply(503);
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.rename) return connection.reply(402, 'Not supported by file system');
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
const from = connection.renameFrom;
const from = this.renameFrom;
const to = command.arg;
return Promise.resolve(connection.fs.rename(from, to))
return Promise.try(() => this.fs.rename(from, to))
.then(() => {
return connection.reply(250);
return this.reply(250);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
log.error(err);
return this.reply(550, err.message);
})
.finally(() => {
delete connection.renameFrom;
delete this.renameFrom;
});
},
syntax: '{{cmd}} <name>',

View File

@@ -6,7 +6,7 @@ module.exports = function ({log, command} = {}) {
const [mode, ...fileNameParts] = command.arg.split(' ');
const fileName = fileNameParts.join(' ');
return Promise.resolve(this.fs.chmod(fileName, parseInt(mode, 8)))
return Promise.try(() => this.fs.chmod(fileName, parseInt(mode, 8)))
.then(() => {
return this.reply(200);
})

View File

@@ -8,7 +8,7 @@ module.exports = {
handler: function ({log, command} = {}) {
const rawSubCommand = _.get(command, 'arg', '');
const subCommand = this.commands.parse(rawSubCommand);
const subLog = log.scope(subCommand.directive);
const subLog = log.child({subverb: subCommand.directive});
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);

View File

@@ -2,17 +2,17 @@ const Promise = require('bluebird');
module.exports = {
directive: 'SIZE',
handler: function (connection, command) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return Promise.resolve(connection.fs.get(command.arg))
return Promise.try(() => this.fs.get(command.arg))
.then(fileStat => {
return connection.reply(213, {message: fileStat.size});
return this.reply(213, {message: fileStat.size});
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
log.error(err);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} <path>',

View File

@@ -4,25 +4,26 @@ const getFileStat = require('../../helpers/file-stat');
module.exports = {
directive: 'STAT',
handler: function (connection, command) {
handler: function (args = {}) {
const {log, command} = args;
const path = _.get(command, 'arg');
if (path) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return Promise.resolve(connection.fs.get(path))
return Promise.try(() => this.fs.get(path))
.then(stat => {
if (stat.isDirectory()) {
if (!connection.fs.list) return connection.reply(402, 'Not supported by file system');
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
return Promise.resolve(connection.fs.list(path))
return Promise.try(() => this.fs.list(path))
.then(stats => [213, stats]);
}
return [212, [stat]];
})
.then(([code, fileStats]) => {
return Promise.map(fileStats, file => {
const message = getFileStat(file, _.get(connection, 'server.options.file_format', 'ls'));
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
return {
raw: true,
message
@@ -30,13 +31,13 @@ module.exports = {
})
.then(messages => [code, messages]);
})
.then(([code, messages]) => connection.reply(code, 'Status begin', ...messages, 'Status end'))
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
.catch(err => {
connection.emit('error', err);
return connection.reply(450, err.message);
log.error(err);
return this.reply(450, err.message);
});
} else {
return connection.reply(211, 'Status OK');
return this.reply(211, 'Status OK');
}
},
syntax: '{{cmd}} [<path>]',

View File

@@ -2,62 +2,62 @@ const Promise = require('bluebird');
module.exports = {
directive: 'STOR',
handler: function (connection, command) {
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
if (!connection.fs.write) return connection.reply(402, 'Not supported by file system');
handler: function ({log, command} = {}) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
const append = command.directive === 'APPE';
const fileName = command.arg;
return connection.connector.waitForConnection()
.tap(() => connection.commandSocket.pause())
.then(() => Promise.resolve(connection.fs.write(fileName, {append, start: connection.restByteCount})))
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
.then(stream => {
const destroyConnection = (conn, reject) => err => {
if (conn) conn.destroy(err);
const destroyConnection = (connection, reject) => err => {
if (connection) connection.destroy(err);
reject(err);
};
const streamPromise = new Promise((resolve, reject) => {
stream.once('error', destroyConnection(connection.connector.socket, reject));
stream.once('error', destroyConnection(this.connector.socket, reject));
stream.once('finish', () => resolve());
});
const socketPromise = new Promise((resolve, reject) => {
connection.connector.socket.on('data', data => {
if (connection.connector.socket) connection.connector.socket.pause();
this.connector.socket.on('data', data => {
if (this.connector.socket) this.connector.socket.pause();
if (stream) {
stream.write(data, connection.transferType, () => connection.connector.socket && connection.connector.socket.resume());
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
}
});
connection.connector.socket.once('end', () => {
this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close');
else stream.end();
resolve();
});
connection.connector.socket.once('error', destroyConnection(stream, reject));
this.connector.socket.once('error', destroyConnection(stream, reject));
});
connection.restByteCount = 0;
this.restByteCount = 0;
return connection.reply(150).then(() => connection.connector.socket.resume())
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => Promise.join(streamPromise, socketPromise))
.tap(() => connection.emit('STOR', null, fileName))
.tap(() => this.emit('STOR', null, fileName))
.finally(() => stream.destroy && stream.destroy());
})
.then(() => connection.reply(226, fileName))
.then(() => this.reply(226, fileName))
.catch(Promise.TimeoutError, err => {
connection.emit('error', err);
return connection.reply(425, 'No connection established');
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
connection.emit('error', err);
connection.emit('STOR', err);
return connection.reply(550, err.message);
log.error(err);
this.emit('STOR', err);
return this.reply(550, err.message);
})
.finally(() => {
connection.connector.end();
connection.commandSocket.resume();
this.connector.end();
this.commandSocket.resume();
});
},
syntax: '{{cmd}} <path>',

View File

@@ -3,19 +3,17 @@ const {handler: stor} = require('./stor');
module.exports = {
directive: 'STOU',
handler: function (connection, command, ...args) {
handler: function (args) {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
const fileName = command.arg;
return Promise.try(() => {
return Promise.resolve(this.fs.get(fileName))
.then(() => Promise.resolve(this.fs.getUniqueName()))
.catch(() => Promise.resolve(fileName));
})
const fileName = args.command.arg;
return Promise.try(() => this.fs.get(fileName))
.then(() => Promise.try(() => this.fs.getUniqueName()))
.catch(() => fileName)
.then(name => {
command.arg = name;
return stor.call(this, connection, command, ...args);
args.command.arg = name;
return stor.call(this, args);
});
},
syntax: '{{cmd}}',

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'STRU',
handler: function (connection, command) {
return connection.reply(/^F$/i.test(command.arg) ? 200 : 504);
handler: function ({command} = {}) {
return this.reply(/^F$/i.test(command.arg) ? 200 : 504);
},
syntax: '{{cmd}} <structure>',
description: 'Set file transfer structure',

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'SYST',
handler: function (connection) {
return connection.reply(215);
handler: function () {
return this.reply(215);
},
syntax: '{{cmd}}',
description: 'Return system type',

View File

@@ -1,14 +1,14 @@
module.exports = {
directive: 'TYPE',
handler: function (connection, command) {
handler: function ({command} = {}) {
if (/^A[0-9]?$/i.test(command.arg)) {
connection.transferType = 'ascii';
this.transferType = 'ascii';
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
connection.transferType = 'binary';
this.transferType = 'binary';
} else {
return connection.reply(501);
return this.reply(501);
}
return connection.reply(200, `Switch to "${connection.transferType}" transfer mode.`);
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`);
},
syntax: '{{cmd}} <mode>',
description: 'Set the transfer mode, binary (I) or ascii (A)',

View File

@@ -1,24 +1,24 @@
module.exports = {
directive: 'USER',
handler: function (connection, command) {
if (connection.username) return connection.reply(530, 'Username already set');
if (connection.authenticated) return connection.reply(230);
handler: function ({log, command} = {}) {
if (this.username) return this.reply(530, 'Username already set');
if (this.authenticated) return this.reply(230);
connection.username = command.arg;
if (!connection.username) return connection.reply(501, 'Must provide username');
this.username = command.arg;
if (!this.username) return this.reply(501, 'Must provide username');
if (connection.server.options.anonymous === true && connection.username === 'anonymous' ||
connection.username === connection.server.options.anonymous) {
return connection.login(connection.username, '@anonymous')
if (this.server.options.anonymous === true && this.username === 'anonymous' ||
this.username === this.server.options.anonymous) {
return this.login(this.username, '@anonymous')
.then(() => {
return connection.reply(230);
return this.reply(230);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(530, err.message || 'Authentication failed');
log.error(err);
return this.reply(530, err.message || 'Authentication failed');
});
}
return connection.reply(331);
return this.reply(331);
},
syntax: '{{cmd}} <username>',
description: 'Authentication username',

View File

@@ -10,13 +10,11 @@ const errors = require('./errors');
const DEFAULT_MESSAGE = require('./messages');
class FtpConnection extends EventEmitter {
constructor(server, socket) {
constructor(server, options) {
super();
this.server = server;
this.id = uuid.v4();
this.commandSocket = socket;
this.log = server.log.scope(`client: ${this.ip}`);
// this.log = options.log.child({id: this.id, ip: this.ip});
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
this.encoding = 'utf8';
@@ -26,8 +24,10 @@ class FtpConnection extends EventEmitter {
this.connector = new BaseConnector(this);
this.commandSocket = options.socket;
this.commandSocket.on('error', err => {
this.log.scope('error event').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', () => {});
@@ -40,6 +40,7 @@ class FtpConnection extends EventEmitter {
_handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return Promise.mapSeries(messages, message => this.commands.handle(message));
}
@@ -116,13 +117,12 @@ class FtpConnection extends EventEmitter {
};
const processLetter = letter => {
const log = this.log.scope('reply');
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
log.debug(letter.message, {port: letter.socket.address().port});
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) {
log.error(err);
this.log.error(err);
return reject(err);
}
resolve();

View File

@@ -7,7 +7,6 @@ class Active extends Connector {
constructor(connection) {
super(connection);
this.type = 'active';
this.log = connection.log.scope('active');
}
waitForConnection({timeout = 5000, delay = 250} = {}) {
@@ -30,16 +29,10 @@ class Active extends Connector {
.then(() => {
this.dataSocket = new Socket();
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.connection.emit('error', err));
this.dataSocket.on('close', () => {
this.log.debug('socket closed');
this.end();
});
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.connect({host, port, family}, () => {
this.dataSocket.pause();
this.log.debug('connection', {port, remoteAddress: this.dataSocket.remoteAddress});
if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.dataSocket, {

View File

@@ -8,7 +8,10 @@ class Connector {
this.dataSocket = null;
this.dataServer = null;
this.type = false;
this.log = connection.log.scope('connector');
}
get log() {
return this.connection.log;
}
get socket() {

View File

@@ -11,7 +11,6 @@ class Passive extends Connector {
constructor(connection) {
super(connection);
this.type = 'passive';
this.log = connection.log.scope('passive');
}
waitForConnection({timeout = 5000, delay = 250} = {}) {
@@ -38,16 +37,16 @@ class Passive extends Connector {
.then(port => {
const connectionHandler = socket => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
this.log.error('ip address mismatch', {
this.log.error({
pasv_connection: socket.remoteAddress,
cmd_connection: this.connection.commandSocket.remoteAddress
});
}, 'Connecting addresses do not match');
socket.destroy();
return this.connection.reply(550, 'IP address mismatch')
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
this.log.debug('connection', {port, remoteAddress: socket.remoteAddress});
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server._tls);
@@ -61,9 +60,9 @@ class Passive extends Connector {
}
this.dataSocket.connected = true;
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.connection.emit('error', err));
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.on('close', () => {
this.log.debug('socket closed');
this.log.trace('Passive connection closed');
this.end();
});
};
@@ -71,17 +70,17 @@ class Passive extends Connector {
this.dataSocket = null;
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.connection.emit('error', err));
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('close', () => {
this.log.debug('server closed');
this.log.trace('Passive server closed');
this.dataServer = null;
});
return new Promise((resolve, reject) => {
this.dataServer.listen(port, err => {
this.dataServer.listen(port, this.server.url.hostname, err => {
if (err) reject(err);
else {
this.log.debug('listening', {port});
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
}
});

View File

@@ -8,15 +8,28 @@ const errors = require('./errors');
class FileSystem {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = cwd || nodePath.sep;
this.root = root || process.cwd();
this.cwd = cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep;
this._root = nodePath.resolve(root || process.cwd());
}
_resolvePath(path = '') {
const isFromRoot = _.startsWith(path, '/') || _.startsWith(path, nodePath.sep);
const cwd = isFromRoot ? nodePath.sep : this.cwd || nodePath.sep;
const serverPath = nodePath.join(nodePath.sep, cwd, path);
const fsPath = nodePath.join(this.root, serverPath);
get root() {
return this._root;
}
_resolvePath(path = '.') {
const serverPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
} else {
return nodePath.join(this.cwd, path);
}
})();
const fsPath = (() => {
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${serverPath}`);
return nodePath.join(resolvedPath);
})();
return {
serverPath,

View File

@@ -1,7 +1,7 @@
const _ = require('lodash');
const Promise = require('bluebird');
const nodeUrl = require('url');
const {Signale} = require('signale');
const buyan = require('bunyan');
const net = require('net');
const tls = require('tls');
const fs = require('fs');
@@ -14,11 +14,10 @@ class FtpServer extends EventEmitter {
constructor(url, options = {}) {
super();
this.options = _.merge({
log: new Signale({
scope: 'ftp-srv'
}),
log: buyan.createLogger({name: 'ftp-srv'}),
anonymous: false,
pasv_range: 22,
pasv_url: null,
file_format: 'ls',
blacklist: [],
whitelist: [],
@@ -37,7 +36,7 @@ class FtpServer extends EventEmitter {
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
const serverConnectionHandler = socket => {
let connection = new Connection(this, socket);
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
socket.on('close', () => this.disconnectClient(connection.id));
@@ -50,7 +49,7 @@ class FtpServer extends EventEmitter {
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', err => this.log.scope('error event').error(err));
this.server.on('error', err => this.log.error(err, '[Event] error'));
const quit = _.debounce(this.quit.bind(this), 100);
@@ -64,9 +63,10 @@ class FtpServer extends EventEmitter {
}
listen() {
return resolveHost(this.url.hostname)
.then(hostname => {
this.url.hostname = hostname;
return resolveHost(this.options.pasv_url || this.url.hostname)
.then(pasvUrl => {
this.options.pasv_url = pasvUrl;
return new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.url.port, this.url.hostname, err => {
@@ -124,8 +124,7 @@ class FtpServer extends EventEmitter {
try {
client.close(0);
} catch (err) {
this.log.error('Error disconnecting client', err);
this.log.debug('User ID', {id});
this.log.error(err, 'Error closing connection', {id});
} finally {
resolve('Disconnected');
}
@@ -138,12 +137,12 @@ class FtpServer extends EventEmitter {
}
close() {
this.log.await('Closing server...');
this.log.info('Server closing...');
this.server.maxConnections = 0;
return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => new Promise(resolve => {
this.server.close(err => {
if (err) this.log.error('Error closing server', err);
if (err) this.log.error(err, 'Error closing server');
resolve('Closed');
});
}))

View File

@@ -1,6 +1,6 @@
const {expect} = require('chai');
const Promise = require('bluebird');
const {Signale} = require('signale');
const bunyan = require('bunyan');
const sinon = require('sinon');
const FtpCommands = require('../../src/commands');
@@ -10,7 +10,7 @@ describe('FtpCommands', function () {
let commands;
let mockConnection = {
authenticated: false,
log: new Signale('commands'),
log: bunyan.createLogger({name: 'FtpCommands'}),
reply: () => Promise.resolve({}),
server: {
options: {
@@ -92,7 +92,7 @@ describe('FtpCommands', function () {
.then(() => {
expect(mockConnection.reply.callCount).to.equal(1);
expect(mockConnection.reply.args[0][0]).to.equal(502);
expect(mockConnection.reply.args[0][1]).to.match(/blacklist/);
expect(mockConnection.reply.args[0][1]).to.match(/blacklisted/);
});
});
@@ -102,7 +102,7 @@ describe('FtpCommands', function () {
.then(() => {
expect(mockConnection.reply.callCount).to.equal(1);
expect(mockConnection.reply.args[0][0]).to.equal(502);
expect(mockConnection.reply.args[0][1]).to.match(/whitelist/);
expect(mockConnection.reply.args[0][1]).to.match(/whitelisted/);
});
});

View File

@@ -10,10 +10,9 @@ describe(CMD, function () {
connector: {
waitForConnection: () => Promise.resolve(),
end: () => Promise.resolve()
},
emit: () => Promise.resolve()
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -30,7 +29,7 @@ describe(CMD, function () {
mockClient.connector.waitForConnection.restore();
sandbox.stub(mockClient.connector, 'waitForConnection').rejects();
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
expect(mockClient.connector.end.callCount).to.equal(0);
@@ -39,7 +38,7 @@ describe(CMD, function () {
});
it('// successful | active connection', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
expect(mockClient.connector.end.callCount).to.equal(1);

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,7 +20,7 @@ describe(CMD, function () {
});
it('// successful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
});

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
_tls: {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,7 +23,7 @@ describe(CMD, function () {
});
it('TLS // supported', () => {
return cmdFn(mockClient, {arg: 'TLS', directive: CMD})
return cmdFn({command: {arg: 'TLS', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(234);
expect(mockClient.secure).to.equal(true);
@@ -31,14 +31,14 @@ describe(CMD, function () {
});
it('SSL // not supported', () => {
return cmdFn(mockClient, {arg: 'SSL', directive: CMD})
return cmdFn({command: {arg: 'SSL', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
it('bad // bad', () => {
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});

View File

@@ -1,17 +1,19 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'CDUP';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => Promise.resolve(),
fs: {
chdir: () => Promise.resolve()
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -24,7 +26,7 @@ describe(CMD, function () {
});
it('.. // successful', () => {
return cmdFn(mockClient, {directive: CMD})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('..');

View File

@@ -1,14 +1,16 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'CWD';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {chdir: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,10 +25,10 @@ describe(CMD, function () {
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(mockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
@@ -34,10 +36,10 @@ describe(CMD, function () {
it('fails on no fs chdir command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
@@ -45,7 +47,7 @@ describe(CMD, function () {
});
it('test // successful', () => {
return cmdFn(mockClient, {arg: 'test', directive: CMD})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
@@ -56,7 +58,7 @@ describe(CMD, function () {
mockClient.fs.chdir.restore();
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
return cmdFn(mockClient, {arg: 'test', directive: CMD})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
@@ -67,7 +69,7 @@ describe(CMD, function () {
mockClient.fs.chdir.restore();
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad');

View File

@@ -1,14 +1,16 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'DELE';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {delete: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,10 +25,10 @@ describe(CMD, function () {
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
@@ -37,7 +39,7 @@ describe(CMD, function () {
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
@@ -45,7 +47,7 @@ describe(CMD, function () {
});
it('test // successful', () => {
return cmdFn(mockClient, {arg: 'test', directive: CMD})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.delete.args[0][0]).to.equal('test');
@@ -56,7 +58,7 @@ describe(CMD, function () {
mockClient.fs.delete.restore();
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.delete.args[0][0]).to.equal('bad');

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,21 +23,21 @@ describe(CMD, function () {
});
it('// unsuccessful | no argument', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
it('// unsuccessful | invalid argument', () => {
return cmdFn(mockClient, {arg: 'blah'})
return cmdFn({command: {arg: 'blah'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});
});
it('// successful IPv4', () => {
return cmdFn(mockClient, {arg: '|1|192.168.0.100|35286|'})
return cmdFn({command: {arg: '|1|192.168.0.100|35286|'}})
.then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);
@@ -48,7 +48,7 @@ describe(CMD, function () {
});
it('// successful IPv6', () => {
return cmdFn(mockClient, {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'})
return cmdFn({command: {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'}})
.then(() => {
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -25,7 +25,7 @@ describe(CMD, function () {
});
it('// successful IPv4', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
const [code, message] = mockClient.reply.args[0];
expect(code).to.equal(229);

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,28 +20,28 @@ describe(CMD, function () {
});
it('// successful', () => {
return cmdFn(mockClient, {directive: CMD})
return cmdFn({command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211);
});
});
it('help // successful', () => {
return cmdFn(mockClient, {arg: 'help', directive: CMD})
return cmdFn({command: {arg: 'help', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214);
});
});
it('allo // successful', () => {
return cmdFn(mockClient, {arg: 'allo', directive: CMD})
return cmdFn({command: {arg: 'allo', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(214);
});
});
it('bad // unsuccessful', () => {
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
});

View File

@@ -1,10 +1,12 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'LIST';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {
@@ -20,7 +22,7 @@ describe(CMD, function () {
pause: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -87,10 +89,10 @@ describe(CMD, function () {
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
@@ -98,10 +100,10 @@ describe(CMD, function () {
it('fails on no fs list command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
@@ -109,7 +111,7 @@ describe(CMD, function () {
});
it('. // successful', () => {
return cmdFn(mockClient, {directive: CMD})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(3);
@@ -141,7 +143,7 @@ describe(CMD, function () {
isDirectory: () => false
});
return cmdFn(mockClient, {directive: CMD, arg: 'testfile.txt'})
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(2);
@@ -156,7 +158,7 @@ describe(CMD, function () {
mockClient.fs.list.restore();
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
return cmdFn(mockClient, {directive: CMD})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(451);
});
@@ -165,7 +167,7 @@ describe(CMD, function () {
it('. // unsuccessful (timeout)', () => {
sandbox.stub(mockClient.connector, 'waitForConnection').returns(Promise.reject(new Promise.TimeoutError()));
return cmdFn(mockClient, {directive: CMD})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});

View File

@@ -1,14 +1,16 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'MDTM';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {get: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,10 +25,10 @@ describe(CMD, function () {
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
@@ -34,10 +36,10 @@ describe(CMD, function () {
it('fails on no fs get command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
@@ -45,7 +47,7 @@ describe(CMD, function () {
});
it('. // successful', () => {
return cmdFn(mockClient, {directive: CMD})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
@@ -56,7 +58,7 @@ describe(CMD, function () {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error());
return cmdFn(mockClient, {directive: CMD})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});

View File

@@ -1,14 +1,16 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'MKD';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {mkdir: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,10 +25,10 @@ describe(CMD, function () {
describe('// check', function () {
it('fails on no fs', () => {
const badMockClient = {reply: () => {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
@@ -34,10 +36,10 @@ describe(CMD, function () {
it('fails on no fs mkdir command', () => {
const badMockClient = {reply: () => {}, fs: {}};
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
@@ -45,7 +47,7 @@ describe(CMD, function () {
});
it('test // successful', () => {
return cmdFn(mockClient, {arg: 'test', directive: CMD})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
@@ -56,7 +58,7 @@ describe(CMD, function () {
mockClient.fs.mkdir.restore();
sandbox.stub(mockClient.fs, 'mkdir').resolves('test');
return cmdFn(mockClient, {arg: 'test', directive: CMD})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
@@ -67,7 +69,7 @@ describe(CMD, function () {
mockClient.fs.mkdir.restore();
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'));
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
expect(mockClient.fs.mkdir.args[0][0]).to.equal('bad');

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,14 +20,14 @@ describe(CMD, function () {
});
it('S // successful', () => {
return cmdFn(mockClient, {arg: 'S'})
return cmdFn({command: {arg: 'S'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
it('Q // unsuccessful', () => {
return cmdFn(mockClient, {arg: 'Q'})
return cmdFn({command: {arg: 'Q'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});

View File

@@ -1,10 +1,12 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'NLST';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {
@@ -20,7 +22,7 @@ describe(CMD, function () {
pause: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -85,7 +87,7 @@ describe(CMD, function () {
});
it('. // successful', () => {
return cmdFn(mockClient, {directive: CMD})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(3);
@@ -117,7 +119,7 @@ describe(CMD, function () {
isDirectory: () => false
});
return cmdFn(mockClient, {directive: CMD, arg: 'testfile.txt'})
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(150);
expect(mockClient.reply.args[1].length).to.equal(2);

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,7 +20,7 @@ describe(CMD, function () {
});
it('// successful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,28 +20,28 @@ describe(CMD, function () {
});
it('// unsuccessful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('BAD // unsuccessful', () => {
return cmdFn(mockClient, {arg: 'BAD', directive: CMD})
return cmdFn({command: {arg: 'BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(500);
});
});
it('UTF8 BAD // unsuccessful', () => {
return cmdFn(mockClient, {arg: 'UTF8 BAD', directive: CMD})
return cmdFn({command: {arg: 'UTF8 BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('UTF8 OFF // successful', () => {
return cmdFn(mockClient, {arg: 'UTF8 OFF', directive: CMD})
return cmdFn({command: {arg: 'UTF8 OFF', directive: CMD}})
.then(() => {
expect(mockClient.encoding).to.equal('ascii');
expect(mockClient.reply.args[0][0]).to.equal(200);
@@ -49,7 +49,7 @@ describe(CMD, function () {
});
it('UTF8 ON // successful', () => {
return cmdFn(mockClient, {arg: 'UTF8 ON', directive: CMD})
return cmdFn({command: {arg: 'UTF8 ON', directive: CMD}})
.then(() => {
expect(mockClient.encoding).to.equal('utf8');
expect(mockClient.reply.args[0][0]).to.equal(200);

View File

@@ -1,16 +1,18 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'PASS';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
login: () => {},
server: {options: {anonymous: false}},
username: 'anonymous'
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,7 +25,7 @@ describe(CMD, function () {
});
it('pass // successful', () => {
return cmdFn(mockClient, {arg: 'pass', directive: CMD})
return cmdFn({log, command: {arg: 'pass', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.args[0]).to.eql(['anonymous', 'pass']);
@@ -33,7 +35,7 @@ describe(CMD, function () {
it('// successful (already authenticated)', () => {
mockClient.server.options.anonymous = true;
mockClient.authenticated = true;
return cmdFn(mockClient, {directive: CMD})
return cmdFn({log, command: {directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
expect(mockClient.login.callCount).to.equal(0);
@@ -46,7 +48,7 @@ describe(CMD, function () {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects('bad');
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
});
@@ -56,7 +58,7 @@ describe(CMD, function () {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects({});
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
});
@@ -64,7 +66,7 @@ describe(CMD, function () {
it('bad // unsuccessful', () => {
delete mockClient.username;
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
});

View File

@@ -9,7 +9,7 @@ describe(CMD, function () {
reply: () => Promise.resolve(),
server: {}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -21,7 +21,7 @@ describe(CMD, function () {
});
it('// unsuccessful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
});
@@ -31,7 +31,7 @@ describe(CMD, function () {
mockClient.secure = true;
mockClient.server._tls = {};
return cmdFn(mockClient, {arg: '0'})
return cmdFn({command: {arg: '0'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.bufferSize).to.equal(0);
@@ -42,7 +42,7 @@ describe(CMD, function () {
mockClient.secure = true;
mockClient.server._tls = {};
return cmdFn(mockClient, {arg: '10'})
return cmdFn({command: {arg: '10'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.bufferSize).to.equal(10);

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,21 +23,21 @@ describe(CMD, function () {
});
it('// unsuccessful | no argument', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
});
it('// unsuccessful | invalid argument', () => {
return cmdFn(mockClient, {arg: '1,2,3,4,5'})
return cmdFn({command: {arg: '1,2,3,4,5'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
});
it('// successful', () => {
return cmdFn(mockClient, {arg: '192,168,0,100,137,214'})
return cmdFn({command: {arg: '192,168,0,100,137,214'}})
.then(() => {
const [ip, port] = ActiveConnector.prototype.setupConnection.args[0];
expect(mockClient.reply.args[0][0]).to.equal(200);

View File

@@ -9,7 +9,7 @@ describe(CMD, function () {
reply: () => Promise.resolve(),
server: {}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -21,7 +21,7 @@ describe(CMD, function () {
});
it('// unsuccessful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(202);
});
@@ -31,7 +31,7 @@ describe(CMD, function () {
mockClient.server._tls = {};
mockClient.secure = true;
return cmdFn(mockClient, {arg: 'P'})
return cmdFn({command: {arg: 'P'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
});
@@ -41,7 +41,7 @@ describe(CMD, function () {
mockClient.bufferSize = 0;
mockClient.secure = true;
return cmdFn(mockClient, {arg: 'p'})
return cmdFn({command: {arg: 'p'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
});
@@ -49,7 +49,7 @@ describe(CMD, function () {
it('// unsuccessful - unsupported', () => {
mockClient.secure = true;
return cmdFn(mockClient, {arg: 'C'})
return cmdFn({command: {arg: 'C'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(536);
});
@@ -57,7 +57,7 @@ describe(CMD, function () {
it('// unsuccessful - unknown', () => {
mockClient.secure = true;
return cmdFn(mockClient, {arg: 'QQ'})
return cmdFn({command: {arg: 'QQ'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});

View File

@@ -1,14 +1,16 @@
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'PWD';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
const mockClient = {
reply: () => {},
fs: {currentDirectory: () => {}}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -26,7 +28,7 @@ describe(CMD, function () {
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(550);
});
@@ -37,7 +39,7 @@ describe(CMD, function () {
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
sandbox.stub(badMockClient, 'reply').resolves();
return badCmdFn(badMockClient)
return badCmdFn()
.then(() => {
expect(badMockClient.reply.args[0][0]).to.equal(402);
});
@@ -45,7 +47,7 @@ describe(CMD, function () {
});
it('// successful', () => {
return cmdFn(mockClient, {arg: 'test', directive: CMD})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
});
@@ -55,7 +57,7 @@ describe(CMD, function () {
mockClient.fs.currentDirectory.restore();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves('/test');
return cmdFn(mockClient, {arg: 'test', directive: CMD})
return cmdFn({log, command: {arg: 'test', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(257);
});
@@ -65,7 +67,7 @@ describe(CMD, function () {
mockClient.fs.currentDirectory.restore();
sandbox.stub(mockClient.fs, 'currentDirectory').rejects(new Error('Bad'));
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});

View File

@@ -7,7 +7,7 @@ describe(CMD, function () {
const mockClient = {
close: () => {}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -19,7 +19,7 @@ describe(CMD, function () {
});
it('// successful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.close.callCount).to.equal(1);
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,28 +20,28 @@ describe(CMD, function () {
});
it('// unsuccessful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('-1 // unsuccessful', () => {
return cmdFn(mockClient, {arg: '-1', directive: CMD})
return cmdFn({command: {arg: '-1', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('bad // unsuccessful', () => {
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
return cmdFn({command: {arg: 'bad', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
});
});
it('1 // successful', () => {
return cmdFn(mockClient, {arg: '1', directive: CMD})
return cmdFn({command: {arg: '1', directive: CMD}})
.then(() => {
expect(mockClient.restByteCount).to.equal(1);
expect(mockClient.reply.args[0][0]).to.equal(350);
@@ -49,7 +49,7 @@ describe(CMD, function () {
});
it('0 // successful', () => {
return cmdFn(mockClient, {arg: '0', directive: CMD})
return cmdFn({command: {arg: '0', directive: CMD}})
.then(() => {
expect(mockClient.restByteCount).to.equal(0);
expect(mockClient.reply.args[0][0]).to.equal(350);

View File

@@ -1,4 +1,5 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const EventEmitter = require('events');
@@ -6,6 +7,7 @@ const EventEmitter = require('events');
const CMD = 'RETR';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let emitter;
const mockClient = {
commandSocket: {
@@ -20,7 +22,7 @@ describe(CMD, function () {
end: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -41,7 +43,7 @@ describe(CMD, function () {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
@@ -50,7 +52,7 @@ describe(CMD, function () {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
@@ -62,7 +64,7 @@ describe(CMD, function () {
});
return cmdFn(mockClient, {arg: 'test.txt'})
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
@@ -73,7 +75,7 @@ describe(CMD, function () {
return Promise.reject(new Error('test'));
});
return cmdFn(mockClient, {arg: 'test.txt'})
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(551);
});
@@ -89,7 +91,7 @@ describe(CMD, function () {
errorEmitted = !!err;
});
return cmdFn(mockClient, {arg: 'test.txt'})
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(errorEmitted).to.equal(true);
});

View File

@@ -5,8 +5,9 @@ const sinon = require('sinon');
const CMD = 'RNFR';
describe(CMD, function () {
let sandbox;
const mockLog = {error: () => {}};
const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -26,7 +27,7 @@ describe(CMD, function () {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
@@ -35,7 +36,7 @@ describe(CMD, function () {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
@@ -45,14 +46,14 @@ describe(CMD, function () {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({log: mockLog, command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
});
it('test // successful', () => {
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({log: mockLog, command: {arg: 'test'}})
.then(() => {
expect(mockClient.fs.get.args[0][0]).to.equal('test');
expect(mockClient.reply.args[0][0]).to.equal(350);

View File

@@ -5,8 +5,9 @@ const sinon = require('sinon');
const CMD = 'RNTO';
describe(CMD, function () {
let sandbox;
const mockLog = {error: () => {}};
const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -27,7 +28,7 @@ describe(CMD, function () {
it('// unsuccessful | no renameFrom set', () => {
delete mockClient.renameFrom;
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(503);
});
@@ -36,7 +37,7 @@ describe(CMD, function () {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
@@ -45,7 +46,7 @@ describe(CMD, function () {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
@@ -55,14 +56,14 @@ describe(CMD, function () {
mockClient.fs.rename.restore();
sandbox.stub(mockClient.fs, 'rename').rejects(new Error('test'));
return cmdFn(mockClient, {arg: 'new'})
return cmdFn({log: mockLog, command: {arg: 'new'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
});
it('new // successful', () => {
return cmdFn(mockClient, {arg: 'new'})
return cmdFn({command: {arg: 'new'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(250);
expect(mockClient.fs.rename.args[0]).to.eql(['test', 'new']);

View File

@@ -5,8 +5,9 @@ const sinon = require('sinon');
const CMD = 'CHMOD';
describe(CMD, function () {
let sandbox;
const mockLog = {error: () => {}};
const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`);
const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -25,7 +26,7 @@ describe(CMD, function () {
it('// unsuccessful | no file system', done => {
delete mockClient.fs;
cmdFn(mockClient)
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
@@ -36,7 +37,7 @@ describe(CMD, function () {
it('// unsuccessful | file system does not have functions', done => {
mockClient.fs = {};
cmdFn(mockClient)
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
done();
@@ -48,7 +49,7 @@ describe(CMD, function () {
mockClient.fs.chmod.restore();
sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test'));
cmdFn(mockClient, {arg: '777 test'})
cmdFn({log: mockLog, command: {arg: '777 test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(500);
done();
@@ -57,7 +58,7 @@ describe(CMD, function () {
});
it('777 test // successful', done => {
cmdFn(mockClient, {arg: '777 test'})
cmdFn({log: mockLog, command: {arg: '777 test'}})
.then(() => {
expect(mockClient.fs.chmod.args[0]).to.eql(['test', 511]);
expect(mockClient.reply.args[0][0]).to.equal(200);

View File

@@ -1,6 +1,7 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan');
const siteRegistry = require('../../../../src/commands/registration/site/registry');
const FtpCommands = require('../../../../src/commands');
@@ -8,11 +9,12 @@ const FtpCommands = require('../../../../src/commands');
const CMD = 'SITE';
describe(CMD, function () {
let sandbox;
const log = bunyan.createLogger({name: 'site-test'});
const mockClient = {
reply: () => Promise.resolve(),
commands: new FtpCommands()
};
const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -24,14 +26,14 @@ describe(CMD, function () {
});
it('// unsuccessful', () => {
return cmdFn(mockClient)
return cmdFn({log})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
});
});
it('// unsuccessful', () => {
return cmdFn(mockClient, {arg: 'BAD'})
return cmdFn({log, command: {arg: 'BAD'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(502);
});
@@ -40,7 +42,7 @@ describe(CMD, function () {
it('// successful', () => {
sandbox.stub(siteRegistry.CHMOD, 'handler').resolves();
return cmdFn(mockClient, {arg: 'CHMOD test'})
return cmdFn({log, command: {arg: 'CHMOD test'}})
.then(() => {
const {command} = siteRegistry.CHMOD.handler.args[0][0];
expect(command.directive).to.equal('CHMOD');

View File

@@ -5,8 +5,9 @@ const sinon = require('sinon');
const CMD = 'SIZE';
describe(CMD, function () {
let sandbox;
const mockLog = {error: () => {}};
const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -24,7 +25,7 @@ describe(CMD, function () {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
@@ -33,7 +34,7 @@ describe(CMD, function () {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
@@ -42,14 +43,14 @@ describe(CMD, function () {
it('// unsuccessful | file get fails', () => {
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({log: mockLog, command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
});
it('// successful', () => {
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
});

View File

@@ -5,8 +5,9 @@ const sinon = require('sinon');
const CMD = 'STAT';
describe(CMD, function () {
let sandbox;
const mockLog = {error: () => {}};
const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -23,7 +24,7 @@ describe(CMD, function () {
});
it('// successful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(211);
});
@@ -32,7 +33,7 @@ describe(CMD, function () {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
@@ -41,7 +42,7 @@ describe(CMD, function () {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
@@ -50,7 +51,7 @@ describe(CMD, function () {
it('// unsuccessful | file get fails', () => {
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({log: mockLog, command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(450);
});
@@ -76,7 +77,7 @@ describe(CMD, function () {
isDirectory: () => false
});
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(212);
});
@@ -121,7 +122,7 @@ describe(CMD, function () {
isDirectory: () => true
});
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(213);
});

View File

@@ -1,4 +1,5 @@
const Promise = require('bluebird');
const bunyan = require('bunyan');
const {expect} = require('chai');
const sinon = require('sinon');
const EventEmitter = require('events');
@@ -6,6 +7,7 @@ const EventEmitter = require('events');
const CMD = 'STOR';
describe(CMD, function () {
let sandbox;
let log = bunyan.createLogger({name: CMD});
let emitter;
const mockClient = {
commandSocket: {
@@ -20,7 +22,7 @@ describe(CMD, function () {
end: () => {}
}
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -41,7 +43,7 @@ describe(CMD, function () {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
@@ -50,7 +52,7 @@ describe(CMD, function () {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
@@ -61,7 +63,7 @@ describe(CMD, function () {
return Promise.reject(new Promise.TimeoutError());
});
return cmdFn(mockClient, {arg: 'test.txt'})
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(425);
});
@@ -72,7 +74,7 @@ describe(CMD, function () {
return Promise.reject(new Error('test'));
});
return cmdFn(mockClient, {arg: 'test.txt'})
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
@@ -88,7 +90,7 @@ describe(CMD, function () {
errorEmitted = !!err;
});
return cmdFn(mockClient, {arg: 'test.txt'})
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(errorEmitted).to.equal(true);
});

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -33,7 +33,7 @@ describe(CMD, function () {
it('// unsuccessful | no file system', () => {
delete mockClient.fs;
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
});
@@ -42,7 +42,7 @@ describe(CMD, function () {
it('// unsuccessful | file system does not have functions', () => {
mockClient.fs = {};
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(402);
});
@@ -52,7 +52,7 @@ describe(CMD, function () {
mockClient.fs.get.restore();
sandbox.stub(mockClient.fs, 'get').rejects({});
return cmdFn(mockClient, {arg: 'good'})
return cmdFn({command: {arg: 'good'}})
.then(() => {
const call = stor.handler.call.args[0][1];
expect(call).to.have.property('command');
@@ -63,7 +63,7 @@ describe(CMD, function () {
});
it('// successful | generates unique name', () => {
return cmdFn(mockClient, {arg: 'bad'})
return cmdFn({command: {arg: 'bad'}})
.then(() => {
const call = stor.handler.call.args[0][1];
expect(call).to.have.property('command');

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,14 +20,14 @@ describe(CMD, function () {
});
it('// successful', () => {
return cmdFn(mockClient, {arg: 'F'})
return cmdFn({command: {arg: 'F'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
});
});
it('// unsuccessful', () => {
return cmdFn(mockClient, {arg: 'X'})
return cmdFn({command: {arg: 'X'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -20,7 +20,7 @@ describe(CMD, function () {
});
it('// successful', () => {
return cmdFn(mockClient)
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(215);
});

View File

@@ -8,7 +8,7 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handle;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -21,7 +21,7 @@ describe(CMD, function () {
});
it('A // successful', () => {
return cmdFn(mockClient, {arg: 'A'})
return cmdFn({command: {arg: 'A'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(200);
expect(mockClient.transferType).to.equal('ascii');

View File

@@ -5,12 +5,15 @@ const sinon = require('sinon');
const CMD = 'USER';
describe(CMD, function () {
let sandbox;
const mockLog = {
error: () => {}
};
const mockClient = {
reply: () => Promise.resolve(),
server: {options: {}},
login: () => Promise.resolve()
};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -26,7 +29,7 @@ describe(CMD, function () {
});
it('test // successful | prompt for password', () => {
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(331);
});
@@ -35,7 +38,7 @@ describe(CMD, function () {
it('test // successful | anonymous login', () => {
mockClient.server.options = {anonymous: true};
return cmdFn(mockClient, {arg: 'anonymous'})
return cmdFn({command: {arg: 'anonymous'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.callCount).to.equal(1);
@@ -43,7 +46,7 @@ describe(CMD, function () {
});
it('test // unsuccessful | no username provided', () => {
return cmdFn(mockClient, { })
return cmdFn({command: { }})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
expect(mockClient.login.callCount).to.equal(0);
@@ -53,7 +56,7 @@ describe(CMD, function () {
it('test // unsuccessful | already set username', () => {
mockClient.username = 'test';
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
expect(mockClient.login.callCount).to.equal(0);
@@ -63,7 +66,7 @@ describe(CMD, function () {
it('test // successful | regular login if anonymous is true', () => {
mockClient.server.options = {anonymous: true};
return cmdFn(mockClient, {arg: 'test'})
return cmdFn({log: mockLog, command: {arg: 'test'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(331);
expect(mockClient.login.callCount).to.equal(0);
@@ -73,7 +76,7 @@ describe(CMD, function () {
it('test // successful | anonymous login with set username', () => {
mockClient.server.options = {anonymous: 'sillyrabbit'};
return cmdFn(mockClient, {arg: 'sillyrabbit'})
return cmdFn({log: mockLog, command: {arg: 'sillyrabbit'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.callCount).to.equal(1);
@@ -85,7 +88,7 @@ describe(CMD, function () {
mockClient.login.restore();
sandbox.stub(mockClient, 'login').rejects(new Error('test'));
return cmdFn(mockClient, {arg: 'anonymous'})
return cmdFn({log: mockLog, command: {arg: 'anonymous'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(530);
expect(mockClient.login.callCount).to.equal(1);
@@ -95,7 +98,7 @@ describe(CMD, function () {
it('test // successful | does not login if already authenticated', () => {
mockClient.authenticated = true;
return cmdFn(mockClient, {arg: 'sillyrabbit'})
return cmdFn({log: mockLog, command: {arg: 'sillyrabbit'}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(230);
expect(mockClient.login.callCount).to.equal(0);

View File

@@ -4,6 +4,7 @@ const sinon = require('sinon');
const Promise = require('bluebird');
const net = require('net');
const bunyan = require('bunyan');
const PassiveConnector = require('../../src/connector/passive');
@@ -13,8 +14,9 @@ describe('Connector - Passive //', function () {
reply: () => Promise.resolve({}),
close: () => Promise.resolve({}),
encoding: 'utf8',
log: bunyan.createLogger({name: 'passive-test'}),
commandSocket: {},
server: {options: {}}
server: {options: {}, url: {}}
};
let sandbox;

83
test/fs.spec.js Normal file
View File

@@ -0,0 +1,83 @@
const {expect} = require('chai');
const nodePath = require('path');
const Promise = require('bluebird');
const FileSystem = require('../src/fs');
const errors = require('../src/errors');
describe('FileSystem', function () {
let fs;
before(function () {
fs = new FileSystem({}, {
root: '/tmp/ftp-srv',
cwd: 'file/1/2/3'
});
});
describe('extend', function () {
class FileSystemOV extends FileSystem {
chdir() {
throw new errors.FileSystemError('Not a valid directory');
}
}
let ovFs;
before(function () {
ovFs = new FileSystemOV({});
});
it('handles error', function () {
return Promise.try(() => ovFs.chdir())
.catch(err => {
expect(err).to.be.instanceof(errors.FileSystemError);
});
});
});
describe('#_resolvePath', function () {
it('gets correct relative path', function () {
const result = fs._resolvePath();
expect(result).to.be.an('object');
expect(result.serverPath).to.equal(
nodePath.normalize('/file/1/2/3'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/file/1/2/3'));
});
it('gets correct relative path', function () {
const result = fs._resolvePath('..');
expect(result).to.be.an('object');
expect(result.serverPath).to.equal(
nodePath.normalize('/file/1/2'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/file/1/2'));
});
it('gets correct absolute path', function () {
const result = fs._resolvePath('/other');
expect(result).to.be.an('object');
expect(result.serverPath).to.equal(
nodePath.normalize('/other'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/other'));
});
it('cannot escape root', function () {
const result = fs._resolvePath('../../../../../../../../../../..');
expect(result).to.be.an('object');
expect(result.serverPath).to.equal(
nodePath.normalize('/'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv'));
});
it('resolves to file', function () {
const result = fs._resolvePath('/cool/file.txt');
expect(result).to.be.an('object');
expect(result.serverPath).to.equal(
nodePath.normalize('/cool/file.txt'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/cool/file.txt'));
});
});
});

View File

@@ -1,6 +1,7 @@
/* eslint no-unused-expressions: 0 */
const {expect} = require('chai');
const sinon = require('sinon');
const bunyan = require('bunyan');
const Promise = require('bluebird');
const _ = require('lodash');
const fs = require('fs');
@@ -14,6 +15,7 @@ describe('Integration', function () {
let client;
let sandbox;
let log = bunyan.createLogger({name: 'test-runner'});
let server;
let connection;
@@ -36,6 +38,7 @@ describe('Integration', function () {
function startServer(url, options = {}) {
server = new FtpServer(url, _.assign({
log,
pasv_range: 8881,
greeting: ['hello', 'world'],
anonymous: true
@@ -169,12 +172,12 @@ describe('Integration', function () {
const stream = fs.createWriteStream(fsPath, {flags: 'w+'});
stream.on('error', () => fs.existsSync(fsPath) && fs.unlinkSync(fsPath));
stream.on('close', () => stream.end());
setTimeout(() => stream.emit('error', new Error('STOR fail test')));
setImmediate(() => stream.emit('error', new Error('STOR fail test')));
return stream;
});
client.put(buffer, 'fail.txt', err => {
setTimeout(() => {
setImmediate(() => {
const fileExists = fs.existsSync(fsPath);
expect(err).to.exist;
expect(fileExists).to.equal(false);
@@ -193,7 +196,7 @@ describe('Integration', function () {
client.put(buffer, 'tést.txt', err => {
expect(err).to.not.exist;
setTimeout(() => {
setImmediate(() => {
expect(fs.existsSync(fsPath)).to.equal(true);
fs.readFile(fsPath, (fserr, data) => {
expect(fserr).to.not.exist;
@@ -209,7 +212,7 @@ describe('Integration', function () {
const fsPath = `${clientDirectory}/${name}/tést.txt`;
client.append(buffer, 'tést.txt', err => {
expect(err).to.not.exist;
setTimeout(() => {
setImmediate(() => {
expect(fs.existsSync(fsPath)).to.equal(true);
fs.readFile(fsPath, (fserr, data) => {
expect(fserr).to.not.exist;
@@ -382,7 +385,7 @@ describe('Integration', function () {
runFileSystemTests('binary');
});
describe.skip('#EXPLICIT', function () {
describe('#EXPLICIT', function () {
before(() => {
return server.close()
.then(() => startServer('ftp://127.0.0.1:8880', {

View File

@@ -1,7 +1,12 @@
require('dotenv').load();
const bunyan = require('bunyan');
const FtpServer = require('../src');
const log = bunyan.createLogger({name: 'test'});
log.level('trace');
const server = new FtpServer('ftp://127.0.0.1:8880', {
log,
pasv_range: 8881,
greeting: ['Welcome', 'to', 'the', 'jungle!'],
tls: {