Compare commits

..

45 Commits

Author SHA1 Message Date
Tyler Stewart
a308a33491 chore: update readme 2018-12-21 16:10:34 -07:00
Tyler Stewart
191ad5507c test: update sinon sandbox creation 2018-12-21 16:07:34 -07:00
Tyler Stewart
281d147b96 chore(package): update scripts 2018-12-21 16:03:13 -07:00
Tyler Stewart
52958ffd9f chore: update packages 2018-12-21 16:00:12 -07:00
Tyler Stewart
3f5d548634 chore(circle): update config 2018-12-21 15:57:04 -07:00
Tyler Stewart
ab085a1bca chore: remove npm-run-all
Late to the party, but removes dev dependency `npm-run-all` to avoid malicious package
2018-12-21 15:49:38 -07:00
Robert Kieffer
a5f26480e5 fix(cli): correct --root flag logic (#135)
Fixes #134
2018-12-21 22:44:13 +00:00
Mike Estes
e41b04be46 fix: add pasv_url to typescript definitions (#131) 2018-11-19 09:49:43 -07:00
Tyler Stewart
7acf861a4d fix(cli): correctly setup server, add pasv options (#130) 2018-11-19 09:48:24 -07:00
Tyler Stewart
4801ecc0cc fix: correct timeouts around TLS data connection (#128)
* fix: timeouts when using tls

* fix: correct tls connection

* fix(connector): dont prematurely destroy socket

* fix(passive): set connected if not tls

* refactor: dont return promises on connector end

Since we're not waiting, we don't need to return promises
2018-11-12 03:19:57 +00:00
Qian.Sicheng
8e34e4c71a fix: correct type definitions (#125)
* fix types of server options

* fix usage of server options
2018-11-07 08:29:37 -07:00
Tyler Stewart
0afd578683 chore: update semantic-release (#124) 2018-10-30 01:48:36 +00:00
Tyler Stewart
46b0d52ff2 fix: improve uploading of files 2018-10-30 01:38:59 +00:00
Tyler Stewart
185e473edc chore: update test example 2018-10-30 01:38:59 +00:00
Tyler Stewart
92a323f3dd chore: update fs method links 2018-10-30 01:38:59 +00:00
Tyler Stewart
f67e487306 test: update tests 2018-10-30 01:38:59 +00:00
Tyler Stewart
2716123da7 chore: add Johnnyrook777 to contributors 2018-10-30 01:38:59 +00:00
Tyler Stewart
ef207f60c1 feat: replace tls options with actual tls SecureContext
https://github.com/trs/ftp-srv/pull/108

BREAKING CHANGE: tls options no longer assume file paths are passed for key, ca, and cert. Options are passed directly to `tls.createServer`. See: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
2018-10-30 01:38:59 +00:00
Tyler Stewart
4d8cf42ad0 chore: update readme with changed options 2018-10-30 01:38:59 +00:00
Tyler Stewart
50c6b92d12 fix: improve connection close events and handlers (#123)
* (fix) base: set datasocket to null once destroyed (#119)

Ensure new sockets get created when uploading files by setting the datasocket to null after its destroyed

*  fix(stor): use the close event (#118)

The 'end' event is not called when using SSL. Should use the 'close' event

* refactor(list): chained promises with less depth (#117)

* Update list.js

Tidy up of the List function.

Makes the intent of the method clearer, as there are less nested promises

* refactor(list): chained promises with less depth

* refactor: fixup formatting

* test: update test file config
2018-10-25 10:25:52 -06:00
Tyler Stewart
a2103e5a3c fix: command parse ignores flags on RETR and SIZE (#122)
* fix: command parse ignores flags on RETR and SIZE (#121)

fixes #120

* refactor: upper directive before checking flag parse

* test: add test for command option parse

* feat: restrict command flags to single dash, single character

Only parse `-x` style flags.
2018-10-25 10:25:32 -06:00
Mark Wheeldon
2302b749fa fix(connector): gracefully end data socket (#116)
Fixes GnuTLS error -110: The TLS connection was non-properly terminated issue
2018-09-21 13:54:59 -06:00
Tyler Stewart
27b43d702b chore: update scripts 2018-09-02 16:20:48 +00:00
Tyler Stewart
fae003e644 test(passive): correctly close test connections 2018-09-02 16:20:48 +00:00
Tyler Stewart
a51678ae70 feat: start pasv port selection from last checked port (#111)
* Update find-port.js

Reuse the Port Number Generator

* feat(passive): initialize port factory in server

Ensures that each passive connection does not have to start from the beginning of the pasv port range

https://github.com/trs/ftp-srv/pull/110

* fix(connector): ensure data socket is destroyed and resolved

If there was a data socket, this would never resolve

* test: use bluebird promises for tests, fix stubs

* fix(commands): ensure connector is ended before resuming
2018-09-02 16:20:48 +00:00
Tyler Stewart
bc26886a0d fix(fs): wrap fs methods in try
This handles throwing better than `resolve`
2018-09-02 16:20:48 +00:00
Tyler Stewart
c9b4371579 feat: respond with full paths in STOR/RETR (#95)
Emit and reply with the full paths to the files.

BREAKING CHANGE: fs expects an object `{stream, clientPath}` in response to `.read()` and `.write()`.

This is implemented in a backwards compatible way, and works with the old `fs` implementation. But since this may break builds in unintented ways.
2018-09-02 16:20:48 +00:00
Tyler Stewart
95471bdd15 test: update tests with refactors 2018-09-02 16:20:48 +00:00
Tyler Stewart
5a36a6685d refactor(passive): increment passive connection ports
Using a generator, will loop through min and max ports in order

This should be faster and more efficient since it starts after the last valid port meaning it has a higher chance of being valid
2018-09-02 16:20:48 +00:00
Tyler Stewart
90a7419661 feat: change server options
BREAKING CHANGE: some options have moved or been renamed
2018-09-02 16:20:48 +00:00
Tyler Stewart
29cb035f66 chore: migration guide v2 to v3 2018-09-02 16:20:48 +00:00
Tyler Stewart
66fc66ed80 fix(connector): build passive server correctly for tls (#106)
* fix(connector): build passive server correctly for tls

Fixes an issue where passive tls connections would never be fulfilled.

This uses `tls` to create a server if the connection is secure, which allows an ftp client to connect correctly

* test: stop skipping explicit tests
2018-08-08 19:07:06 -06:00
Tyler Stewart
c970a42132 chore(package): update package lock (#107)
Fixes vulnerabilities in packages
2018-08-08 19:04:38 -06:00
Tim Lundqvist
30ae54a952 fix: better typings for EventEmitter inheritance (#105) 2018-07-24 11:46:03 -06:00
Tyler Stewart
91be338ebd fix: expose errors as export 2018-07-22 18:47:23 -06:00
Tyler Stewart
2a5013447c fix: fix event emitter extension typing 2018-07-22 18:44:15 -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
104 changed files with 8525 additions and 6402 deletions

View File

@@ -23,10 +23,10 @@ base-build: &base-build
- node_modules
- run:
name: Lint
command: npm run verify:js
command: npm run verify -- --silent
- run:
name: Test
command: npm run test:unit:once
command: npm run test:once
jobs:
test_node_10:
@@ -60,15 +60,10 @@ jobs:
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- run:
name: Update NPM
command: |
npm install npm@5
npm install semantic-release@11
- deploy:
name: Semantic Release
command: |
npm run semantic-release || true
npm run semantic-release
workflows:
version: 2

165
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>
@@ -36,6 +36,8 @@
## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
You can use `ftp-srv` to traverse the file system on the server, but it's biggest strength comes from it's customizable file system. This allows you to serve a custom, dynamic, or unique file system to users. You can even server a different system depending on the user connecting.
## Features
- Extensible [file systems](#file-system) per connection
- Passive and active transfers
@@ -51,7 +53,7 @@
// Quick start
const FtpSrv = require('ftp-srv');
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876', { options ... });
const ftpServer = new FtpSrv({ options ... });
ftpServer.on('login', (data, resolve, reject) => { ... });
...
@@ -62,59 +64,72 @@ ftpServer.listen()
## API
### `new FtpSrv(url, [{options}])`
### `new FtpSrv({options})`
#### url
[URL string](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) indicating the protocol, hostname, and port to listen on for connections.
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.
##### `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.
__Default:__ `22`
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
__Default:__ `"127.0.0.1"`
##### `greeting`
A human readable array of lines or string to send when a client connects.
#### `pasv_min`
Tne starting port to accept passive connections.
__Default:__ `1024`
#### `pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
#### `greeting`
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.
#### `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.
__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.
#### `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.
__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.
#### `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.
__Default:__ `[]`
##### `whitelist`
Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command.
#### `whitelist`
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"`
#### `file_format`
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)
- `function () {}` A custom function returning a format or promise for one.
- 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.
#### `log`
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
Piping the output into bunyan will format logs nicely, eg:
```
$ node ./test/start.js | npx bunyan
```
## CLI
@@ -171,15 +186,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 +213,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');
@@ -254,53 +269,53 @@ class MyFileSystem extends FileSystem {
Custom file systems can implement the following variables depending on the developers needs:
### Methods
#### [`currentDirectory()`](src/fs.js#L29)
Returns a string of the current working directory
#### [`currentDirectory()`](src/fs.js#L40)
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
#### [`get(fileName)`](src/fs.js#L44)
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
#### [`list(path)`](src/fs.js#L50)
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
#### [`chdir(path)`](src/fs.js#L67)
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
#### [`mkdir(path)`](src/fs.js#L114)
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
#### [`write(fileName, {append, start})`](src/fs.js#L79)
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
#### [`read(fileName, {start})`](src/fs.js#L90)
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(path)`](src/fs.js#L105)
Delete a file or directory
__Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L102)
Renames a file or directory
#### [`rename(from, to)`](src/fs.js#L120)
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L108)
Modifies a file or directory's permissions
#### [`chmod(path)`](src/fs.js#L126)
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`getUniqueName()`](src/fs.js#L113)
Returns a unique file name to write to
#### [`getUniqueName()`](src/fs.js#L131)
Returns a unique file name to write to
__Used in:__ `STOU`
<!--[RM_CONTRIBUTING]-->
@@ -319,6 +334,8 @@ 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)
- [Johnnyrook777](https://github.com/Johnnyrook777)
<!--[RM_LICENSE]-->
## License

View File

@@ -19,7 +19,8 @@ function setupYargs() {
})
.option('username', {
describe: 'Blank for anonymous',
type: 'string'
type: 'string',
default: ''
})
.option('password', {
describe: 'Password for given username',
@@ -36,6 +37,20 @@ function setupYargs() {
boolean: true,
default: false
})
.option('pasv_url', {
describe: 'URL to provide for passive connections',
type: 'string'
})
.option('pasv_min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024
})
.option('pasv_max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535
})
.parse();
}
@@ -46,15 +61,18 @@ function setupState(_args) {
if (_args._ && _args._.length > 0) {
_state.url = _args._[0];
}
_state.pasv_url = _args.pasv_url;
_state.pasv_min = _args.pasv_min;
_state.pasv_max = _args.pasv_max;
_state.anonymous = _args.username === '';
}
function setupRoot() {
const dirPath = _args.root;
if (dirPath) {
_state.root = process.cwd();
} else {
_state.root = dirPath;
} else {
_state.root = process.cwd();
}
}
@@ -62,7 +80,7 @@ function setupState(_args) {
_state.credentials = {};
const setCredentials = (username, password, root = null) => {
_state.credentials[_state.credentials] = {
_state.credentials[username] = {
password,
root
};
@@ -72,7 +90,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,15 +115,19 @@ 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));
}
const ftpServer = new FtpSrv(_state.url, {
const ftpServer = new FtpSrv({
url: _state.url,
pasv_url: _state.pasv_url,
pasv_min: _state.pasv_min,
pasv_max: _state.pasv_max,
anonymous: _state.anonymous,
blacklist: _state.blacklist
});

View File

@@ -0,0 +1,44 @@
# Migration Guide - v2 to v3
The `FtpServer` constructor has been changed to only take one object option. Combining the two just made sense.
### From:
```js
const server = new FtpServer('ftp://0.0.0.0:21');
```
### To:
```js
const server = new FtpServer({
url: 'ftp://0.0.0.0:21'
});
```
----
The `pasv_range` option has been changed to separate integer variables: `pasv_min`, `pasv_max`.
### From:
```js
const server = new FtpServer(..., {
pasv_range: '1000-2000'
});
```
### To:
```js
const server = new FtpServer({
pasv_min: 1000,
pasv_max: 2000
})
```
----
The default passive port range has been changed to `1024` - `65535`
----

View File

@@ -136,7 +136,7 @@
"comma-dangle": 1,
"new-cap": 2,
"new-parens": 2,
"arrow-parens": [2, "as-needed"],
"arrow-parens": [2, "always"],
"no-array-constructor": 2,
"array-callback-return": 1,
"no-extra-parens": 2,

19
ftp-srv.d.ts vendored
View File

@@ -41,7 +41,7 @@ export class FileSystem {
getUniqueName(): string;
}
export class FtpConnection {
export class FtpConnection extends EventEmitter {
server: FtpServer;
id: string;
log: any;
@@ -59,18 +59,21 @@ export class FtpConnection {
}
export interface FtpServerOptions {
pasv_range?: number | string,
url?: string,
pasv_min?: number,
pasv_max?: number,
pasv_url?: string,
greeting?: string | string[],
tls?: tls.SecureContext | false,
anonymous?: boolean,
blacklist?: Array<string>,
whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log?: any
log?: any,
}
export class FtpServer {
constructor(url: string, options?: FtpServerOptions);
export class FtpServer extends EventEmitter {
constructor(options?: FtpServerOptions);
readonly isTLS: boolean;
@@ -78,7 +81,7 @@ export class FtpServer {
emitPromise(action: any, ...data: any[]): Promise<any>;
emit(action: any, ...data: any[]): void;
// emit is exported from super class
setupTLS(_tls: boolean): boolean | {
cert: string;
@@ -108,7 +111,7 @@ export class FtpServer {
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void): EventEmitter;
) => void): this;
on(event: "client-error", listener: (
data: {
@@ -116,7 +119,7 @@ export class FtpServer {
context: string,
error: Error,
}
) => void): EventEmitter;
) => void): this;
}
export {FtpServer as FtpSrv};

View File

@@ -1,6 +1,8 @@
const FtpSrv = require('./src');
const FileSystem = require('./src/fs');
const errors = require('./src/errors');
module.exports = FtpSrv;
module.exports.FtpSrv = FtpSrv;
module.exports.FileSystem = FileSystem;
module.exports.ftpErrors = errors;

12662
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,17 +28,12 @@
"pre-release": "npm run verify",
"commitmsg": "cz-customizable-ghooks",
"dev": "cross-env NODE_ENV=development npm run verify:watch",
"prepush": "npm-run-all verify test:unit:once --silent",
"prepush": "npm run verify && npm run test:once --silent",
"semantic-release": "semantic-release",
"start": "npm run dev",
"test": "npm run test:unit",
"test:unit": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
"test:unit:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
"verify": "npm run verify:js --silent",
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
"verify:js:watch": "chokidar 'src/**/*.js' 'test/**/*.js' 'config/**/*.js' -c 'npm run verify:js:fix' --initial --silent",
"verify:watch": "npm run verify:js:watch --silent"
"test": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
"test:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
"verify": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\""
},
"release": {
"verifyConditions": "condition-circle"
@@ -53,36 +48,33 @@
},
"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"
"moment": "^2.22.2",
"uuid": "^3.3.2",
"yargs": "^12.0.1"
},
"devDependencies": {
"@icetee/ftp": "^1.0.2",
"chai": "^4.0.2",
"chokidar-cli": "1.2.0",
"condition-circle": "^1.6.0",
"cross-env": "3.1.4",
"@icetee/ftp": "^1.0.3",
"chai": "^4.1.2",
"condition-circle": "^2.0.1",
"cross-env": "5.2.0",
"cz-customizable": "5.2.0",
"cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0",
"eslint": "4.5.0",
"eslint-config-google": "0.8.0",
"eslint-friendly-formatter": "3.0.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "5.1.1",
"husky": "0.13.3",
"eslint": "5.3.0",
"eslint-config-google": "0.9.1",
"eslint-friendly-formatter": "4.0.1",
"eslint-plugin-mocha": "^5.1.0",
"eslint-plugin-node": "7.0.1",
"husky": "0.14.3",
"istanbul": "0.4.5",
"mocha": "3.5.0",
"mocha-junit-reporter": "1.13.0",
"mocha-multi-reporters": "1.1.5",
"npm-run-all": "^4.1.3",
"rimraf": "2.6.1",
"semantic-release": "^11.0.2",
"sinon": "^2.3.5"
"mocha": "^5.2.0",
"mocha-junit-reporter": "1.18.0",
"mocha-multi-reporters": "1.1.7",
"rimraf": "2.6.2",
"semantic-release": "^15.9.8",
"sinon": "^6.1.5"
},
"engines": {
"node": ">=6.x",

View File

@@ -3,25 +3,30 @@ const Promise = require('bluebird');
const REGISTRY = require('./registry');
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
class FtpCommands {
constructor(connection) {
this.connection = connection;
this.previousCommand = {};
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd));
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd));
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd));
}
parse(message) {
const strippedMessage = message.replace(/"/g, '');
const [directive, ...args] = strippedMessage.split(' ');
let [directive, ...args] = strippedMessage.split(' ');
directive = _.chain(directive).trim().toUpper().value();
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
const params = args.reduce(({arg, flags}, param) => {
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const command = {
directive: _.chain(directive).trim().toUpper().value(),
directive,
arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags,
raw: message
@@ -36,16 +41,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 +66,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()
.then(socket => {
return connection.reply(426, {socket})
.then(() => connection.connector.end())
.then(() => connection.reply(226));
handler: function () {
return this.connector.waitForConnection()
.then((socket) => {
return this.reply(426, {socket})
.then(() => this.reply(226));
})
.catch(err => {
connection.emit('error', err);
return connection.reply(225);
});
.catch(() => this.reply(225))
.finally(() => this.connector.end());
},
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.options.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.options.tls);
const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true,
secureContext
});
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
['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))
.then(cwd => {
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);
.catch((err) => {
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);
.catch((err) => {
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()
.then(server => {
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) => {
@@ -11,13 +11,13 @@ module.exports = {
return feats;
}, ['UTF8'])
.sort()
.map(feat => ({
.map((feat) => ({
message: ` ${feat}`,
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.');
const supportedCommands = _.chunk(Object.keys(registry), 5).map((chunk) => chunk.join('\t'));
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
}
},
syntax: '{{cmd}} [<command>]',

View File

@@ -6,49 +6,49 @@ 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])
.then(files => {
const getFileMessage = file => {
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 => {
return Promise.try(() => files.map((file) => {
const message = getFileMessage(file);
return {
raw: true,
message,
socket: connection.connector.socket
socket: this.connector.socket
};
});
return connection.reply(150)
.then(() => {
if (fileList.length) return connection.reply({}, ...fileList);
});
}));
})
.then(() => connection.reply(226))
.catch(Promise.TimeoutError, err => {
connection.emit('error', err);
return connection.reply(425, 'No connection established');
.tap(() => this.reply(150))
.then((fileList) => {
if (fileList.length) return this.reply({}, ...fileList);
})
.catch(err => {
connection.emit('error', err);
return connection.reply(451, err.message || 'No directory');
.tap(() => this.reply(226))
.catch(Promise.TimeoutError, (err) => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch((err) => {
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))
.then(fileStat => {
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);
.catch((err) => {
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))
.then(dir => {
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);
.catch((err) => {
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');
.catch((err) => {
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()
.then(server => {
const address = connection.server.url.hostname;
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
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 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())
.then(cwd => {
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);
.catch((err) => {
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,61 @@ 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})))
.then(stream => {
const destroyConnection = (conn, reject) => err => {
if (conn) conn.destroy(err);
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
.then((fsResponse) => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = filePath;
}
const serverPath = stream.path || filePath;
const destroyConnection = (connection, reject) => (err) => {
if (connection) connection.destroy(err);
reject(err);
};
const eventsPromise = new Promise((resolve, reject) => {
stream.on('data', data => {
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, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
})
.then(() => connection.reply(226))
.catch(Promise.TimeoutError, err => {
connection.emit('error', err);
return connection.reply(425, 'No connection established');
.catch(Promise.TimeoutError, (err) => {
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);
.catch((err) => {
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);
.catch((err) => {
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);
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
})
.finally(() => {
delete connection.renameFrom;
delete this.renameFrom;
});
},
syntax: '{{cmd}} <name>',

View File

@@ -6,11 +6,11 @@ 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);
})
.catch(err => {
.catch((err) => {
log.error(err);
return this.reply(500);
});

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))
.then(fileStat => {
return connection.reply(213, {message: fileStat.size});
return Promise.try(() => this.fs.get(command.arg))
.then((fileStat) => {
return this.reply(213, {message: fileStat.size});
})
.catch(err => {
connection.emit('error', err);
return connection.reply(550, err.message);
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} <path>',

View File

@@ -4,39 +4,40 @@ 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))
.then(stat => {
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))
.then(stats => [213, stats]);
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'));
return Promise.map(fileStats, (file) => {
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
return {
raw: true,
message
};
})
.then(messages => [code, messages]);
.then((messages) => [code, messages]);
})
.then(([code, messages]) => connection.reply(code, 'Status begin', ...messages, 'Status end'))
.catch(err => {
connection.emit('error', err);
return connection.reply(450, err.message);
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
.catch((err) => {
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,72 @@ 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})))
.then(stream => {
const destroyConnection = (conn, reject) => err => {
if (conn) conn.destroy(err);
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
.then((fsResponse) => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = fileName;
}
const serverPath = stream.path || fileName;
const destroyConnection = (connection, reject) => (err) => {
if (connection) {
if (connection.writeable) connection.end();
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())
.then(() => Promise.join(streamPromise, socketPromise))
.tap(() => connection.emit('STOR', null, fileName))
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => Promise.all([streamPromise, socketPromise]))
.tap(() => this.emit('STOR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
})
.then(() => connection.reply(226, fileName))
.catch(Promise.TimeoutError, err => {
connection.emit('error', err);
return connection.reply(425, 'No connection established');
.catch(Promise.TimeoutError, (err) => {
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);
.catch((err) => {
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));
})
.then(name => {
command.arg = name;
return stor.call(this, connection, command, ...args);
const fileName = args.command.arg;
return Promise.try(() => this.fs.get(fileName))
.then(() => Promise.try(() => this.fs.getUniqueName()))
.catch(() => fileName)
.then((name) => {
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');
.catch((err) => {
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

@@ -43,7 +43,7 @@ const commands = [
const registry = commands.reduce((result, cmd) => {
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
aliases.forEach(alias => result[alias] = cmd);
aliases.forEach((alias) => result[alias] = cmd);
return result;
}, {});

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.on('error', err => {
this.log.scope('error event').error(err);
this.commandSocket = options.socket;
this.commandSocket.on('error', (err) => {
this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
});
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', () => {});
@@ -40,7 +40,8 @@ class FtpConnection extends EventEmitter {
_handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
return Promise.mapSeries(messages, message => this.commands.handle(message));
this.log.trace(messages);
return Promise.mapSeries(messages, (message) => this.commands.handle(message));
}
get ip() {
@@ -67,7 +68,7 @@ class FtpConnection extends EventEmitter {
close(code = 421, message = 'Closing connection') {
return Promise.resolve(code)
.then(_code => _code && this.reply(_code, message))
.then((_code) => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
}
@@ -95,7 +96,7 @@ class FtpConnection extends EventEmitter {
if (!letters.length) letters = [{}];
return Promise.map(letters, (promise, index) => {
return Promise.resolve(promise)
.then(letter => {
.then((letter) => {
if (!letter) letter = {};
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
@@ -103,7 +104,7 @@ class FtpConnection extends EventEmitter {
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding;
return Promise.resolve(letter.message) // allow passing in a promise as a message
.then(message => {
.then((message) => {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
@@ -115,14 +116,13 @@ class FtpConnection extends EventEmitter {
});
};
const processLetter = letter => {
const log = this.log.scope('reply');
const processLetter = (letter) => {
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
log.debug(letter.message, {port: letter.socket.address().port});
letter.socket.write(letter.message + '\r\n', letter.encoding, err => {
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();
@@ -132,10 +132,10 @@ class FtpConnection extends EventEmitter {
};
return satisfyParameters()
.then(satisfiedLetters => Promise.mapSeries(satisfiedLetters, (letter, index) => {
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch(err => {
.catch((err) => {
this.log.error(err);
});
}

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,18 +29,12 @@ 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 secureContext = tls.createSecureContext(this.server.options.tls);
const secureSocket = new tls.TLSSocket(this.dataSocket, {
isServer: true,
secureContext

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() {
@@ -23,22 +26,28 @@ class Connector {
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
}
end() {
const closeDataSocket = new Promise(resolve => {
if (this.dataSocket) this.dataSocket.end();
else resolve();
});
const closeDataServer = new Promise(resolve => {
if (this.dataServer) this.dataServer.close(() => resolve());
else resolve();
});
return Promise.all([closeDataSocket, closeDataServer])
.then(() => {
closeSocket() {
if (this.dataSocket) {
const socket = this.dataSocket;
this.dataSocket.end(() => socket.destroy());
this.dataSocket = null;
}
}
closeServer() {
if (this.dataServer) {
this.dataServer.close();
this.dataServer = null;
this.type = false;
});
}
}
end() {
this.closeSocket();
this.closeServer();
this.type = false;
this.connection.connector = new Connector(this);
}
}
module.exports = Connector;

View File

@@ -4,17 +4,15 @@ const ip = require('ip');
const Promise = require('bluebird');
const Connector = require('./base');
const findPort = require('../helpers/find-port');
const errors = require('../errors');
class Passive extends Connector {
constructor(connection) {
super(connection);
this.type = 'passive';
this.log = connection.log.scope('passive');
}
waitForConnection({timeout = 5000, delay = 250} = {}) {
waitForConnection({timeout = 5000, delay = 50} = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
const checkSocket = () => {
@@ -29,59 +27,54 @@ class Passive extends Connector {
}
setupServer() {
const closeExistingServer = () => this.dataServer ?
new Promise(resolve => this.dataServer.close(() => resolve())) :
Promise.resolve();
return closeExistingServer()
.then(() => this.getPort())
.then(port => {
const connectionHandler = socket => {
this.closeServer();
return this.server.getNextPasvPort()
.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);
const secureSocket = new tls.TLSSocket(socket, {
isServer: true,
secureContext
});
this.dataSocket = secureSocket;
} else {
this.dataSocket = socket;
}
this.dataSocket.connected = true;
this.dataSocket = 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}));
if (!this.connection.secure) {
this.dataSocket.connected = true;
}
};
this.dataSocket = null;
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.connection.emit('error', err));
this.dataServer.on('close', () => {
this.log.debug('server closed');
this.dataServer = null;
this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.once('close', () => {
this.log.trace('Passive server closed');
this.end();
});
if (this.connection.secure) {
this.dataServer.on('secureConnection', (socket) => {
socket.connected = true;
});
}
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);
}
});
@@ -89,15 +82,5 @@ class Passive extends Connector {
});
}
getPort() {
if (this.server.options.pasv_range) {
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
[this.server.options.pasv_range];
return findPort(min, max);
}
throw new errors.ConnectorError('Invalid pasv_range');
}
}
module.exports = Passive;

View File

@@ -8,18 +8,31 @@ 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 clientPath = (() => {
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}${clientPath}`);
return nodePath.join(resolvedPath);
})();
return {
serverPath,
clientPath,
fsPath
};
}
@@ -31,19 +44,19 @@ class FileSystem {
get(fileName) {
const {fsPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.then(stat => _.set(stat, 'name', fileName));
.then((stat) => _.set(stat, 'name', fileName));
}
list(path = '.') {
const {fsPath} = this._resolvePath(path);
return fs.readdirAsync(fsPath)
.then(fileNames => {
return Promise.map(fileNames, fileName => {
.then((fileNames) => {
return Promise.map(fileNames, (fileName) => {
const filePath = nodePath.join(fsPath, fileName);
return fs.accessAsync(filePath, fs.constants.F_OK)
.then(() => {
return fs.statAsync(filePath)
.then(stat => _.set(stat, 'name', fileName));
.then((stat) => _.set(stat, 'name', fileName));
})
.catch(() => null);
});
@@ -52,41 +65,47 @@ class FileSystem {
}
chdir(path = '.') {
const {fsPath, serverPath} = this._resolvePath(path);
const {fsPath, clientPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.tap(stat => {
.tap((stat) => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = serverPath;
this.cwd = clientPath;
return this.currentDirectory();
});
}
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
const {fsPath, clientPath} = this._resolvePath(fileName);
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlinkAsync(fsPath));
stream.once('close', () => stream.end());
return stream;
return {
stream,
clientPath
};
}
read(fileName, {start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
const {fsPath, clientPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.tap(stat => {
.tap((stat) => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
return stream;
return {
stream,
clientPath
};
});
}
delete(path) {
const {fsPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.then(stat => {
.then((stat) => {
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
else return fs.unlinkAsync(fsPath);
});

View File

@@ -2,26 +2,35 @@ const net = require('net');
const Promise = require('bluebird');
const errors = require('../errors');
module.exports = function (min = 1, max = undefined) {
return new Promise((resolve, reject) => {
let checkPort = min;
let portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
portCheckServer.on('error', () => {
if (checkPort < 65535 && (!max || checkPort < max)) {
checkPort = checkPort + 1;
portCheckServer.listen(checkPort);
} else {
reject(new errors.GeneralError('Unable to find open port', 500));
}
});
portCheckServer.on('listening', () => {
const {port} = portCheckServer.address();
portCheckServer.close(() => {
portCheckServer = null;
resolve(port);
});
});
portCheckServer.listen(checkPort);
function* portNumberGenerator(min, max) {
let current = min;
while (true) {
if (current > 65535 || current > max) {
current = min;
}
yield current++;
}
}
function getNextPortFactory(min, max = Infinity) {
const nextPortNumber = portNumberGenerator(min, max);
const portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
portCheckServer.on('error', () => {
portCheckServer.listen(nextPortNumber.next().value);
});
return () => new Promise((resolve) => {
portCheckServer.once('listening', () => {
const {port} = portCheckServer.address();
portCheckServer.close(() => resolve(port));
});
portCheckServer.listen(nextPortNumber.next().value);
})
.catch(RangeError, (err) => Promise.reject(new errors.ConnectorError(err.message)));
}
module.exports = {
getNextPortFactory,
portNumberGenerator
};

View File

@@ -8,12 +8,12 @@ module.exports = function (hostname) {
return new Promise((resolve, reject) => {
if (!hostname || hostname === '0.0.0.0') {
let ip = '';
http.get(IP_WEBSITE, response => {
http.get(IP_WEBSITE, (response) => {
if (response.statusCode !== 200) {
return reject(new errors.GeneralError('Unable to resolve hostname', response.statusCode));
}
response.setEncoding('utf8');
response.on('data', chunk => {
response.on('data', (chunk) => {
ip += chunk;
});
response.on('end', () => {

View File

@@ -1,43 +1,46 @@
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');
const EventEmitter = require('events');
const Connection = require('./connection');
const resolveHost = require('./helpers/resolve-host');
const {getNextPortFactory} = require('./helpers/find-port');
class FtpServer extends EventEmitter {
constructor(url, options = {}) {
constructor(options = {}) {
super();
this.options = _.merge({
log: new Signale({
scope: 'ftp-srv'
}),
this.options = Object.assign({
log: buyan.createLogger({name: 'ftp-srv'}),
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
anonymous: false,
pasv_range: 22,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false
}, options);
this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage();
this._tls = this.setupTLS(this.options.tls);
delete this.options.greeting;
delete this.options.tls;
this.connections = {};
this.log = this.options.log;
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
this.url = nodeUrl.parse(this.options.url);
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max'));
const serverConnectionHandler = socket => {
let connection = new Connection(this, socket);
const serverConnectionHandler = (socket) => {
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
socket.on('close', () => this.disconnectClient(connection.id));
@@ -47,10 +50,10 @@ class FtpServer extends EventEmitter {
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
};
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
const serverOptions = Object.assign({}, this.isTLS ? this.options.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);
@@ -60,16 +63,17 @@ class FtpServer extends EventEmitter {
}
get isTLS() {
return this.url.protocol === 'ftps:' && this._tls;
return this.url.protocol === 'ftps:' && this.options.tls;
}
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 => {
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject);
if (err) return reject(err);
this.log.info({
@@ -90,15 +94,6 @@ class FtpServer extends EventEmitter {
});
}
setupTLS(_tls) {
if (!_tls) return false;
return _.assign({}, _tls, {
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
});
}
setupGreeting(greet) {
if (!greet) return [];
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
@@ -117,15 +112,14 @@ class FtpServer extends EventEmitter {
}
disconnectClient(id) {
return new Promise(resolve => {
return new Promise((resolve) => {
const client = this.connections[id];
if (!client) return resolve();
delete this.connections[id];
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 +132,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);
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(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: {
@@ -20,7 +20,7 @@ describe('FtpCommands', function () {
};
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox = sinon.createSandbox().usingPromise(Promise);
commands = new FtpCommands(mockConnection);
@@ -64,8 +64,8 @@ describe('FtpCommands', function () {
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('arg1 arg2');
expect(cmd.flags).to.deep.equal(['-l', '-A', '--zz88A']);
expect(cmd.arg).to.equal('arg1 arg2 --zz88A');
expect(cmd.flags).to.deep.equal(['-l', '-A']);
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
});
@@ -76,6 +76,13 @@ describe('FtpCommands', function () {
expect(cmd.flags).to.deep.equal(['-l']);
expect(cmd.raw).to.equal('list -l');
});
it('does not check for option flags', () => {
const cmd = commands.parse('retr -test');
expect(cmd.directive).to.equal('RETR');
expect(cmd.arg).to.equal('-test');
expect(cmd.flags).to.deep.equal([]);
});
});
describe('handle', function () {
@@ -92,7 +99,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 +109,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

@@ -3,20 +3,19 @@ const {expect} = require('chai');
const sinon = require('sinon');
const CMD = 'ABOR';
describe(CMD, function () {
describe.skip(CMD, function () {
let sandbox;
const mockClient = {
reply: () => Promise.resolve(),
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.connector, 'waitForConnection');
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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

@@ -8,13 +8,15 @@ describe(CMD, function () {
const mockClient = {
reply: () => Promise.resolve(),
server: {
_tls: {}
options: {
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -23,7 +25,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 +33,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,20 +1,22 @@
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'chdir');
@@ -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,17 +1,19 @@
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'chdir').resolves();
@@ -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,17 +1,19 @@
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'delete').resolves();
@@ -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,10 +10,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
@@ -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,10 +10,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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: {
@@ -13,17 +15,17 @@ describe(CMD, function () {
},
connector: {
waitForConnection: () => Promise.resolve({}),
end: () => {}
end: () => Promise.resolve({})
},
commandSocket: {
resume: () => {},
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({
@@ -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,17 +1,19 @@
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});
@@ -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,17 +1,19 @@
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'mkdir').resolves();
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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: {
@@ -13,17 +15,17 @@ describe(CMD, function () {
},
connector: {
waitForConnection: () => Promise.resolve({}),
end: () => {}
end: () => Promise.resolve({})
},
commandSocket: {
resume: () => {},
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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,19 +1,21 @@
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient, 'login').resolves();
@@ -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,10 +9,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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,10 +10,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
@@ -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,10 +9,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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,17 +1,19 @@
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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves();
@@ -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,10 +7,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'close').resolves();
});
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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: {
@@ -17,13 +19,13 @@ describe(CMD, function () {
waitForConnection: () => Promise.resolve({
resume: () => {}
}),
end: () => {}
end: () => 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = {
read: () => {}
@@ -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);
});
@@ -85,11 +87,11 @@ describe(CMD, function () {
});
let errorEmitted = false;
emitter.once('RETR', err => {
emitter.once('RETR', (err) => {
errorEmitted = !!err;
});
return cmdFn(mockClient, {arg: 'test.txt'})
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(errorEmitted).to.equal(true);
});

View File

@@ -5,11 +5,12 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.renameFrom = 'test';
mockClient.fs = {
@@ -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,11 +5,12 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.renameFrom = 'test';
mockClient.fs = {
@@ -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,11 +5,12 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = {
chmod: () => Promise.resolve()
@@ -22,10 +23,10 @@ describe(CMD, function () {
sandbox.restore();
});
it('// unsuccessful | no file system', done => {
it('// unsuccessful | no file system', (done) => {
delete mockClient.fs;
cmdFn(mockClient)
cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(550);
done();
@@ -33,10 +34,10 @@ describe(CMD, function () {
.catch(done);
});
it('// unsuccessful | file system does not have functions', done => {
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();
@@ -44,11 +45,11 @@ describe(CMD, function () {
.catch(done);
});
it('777 test // unsuccessful | file chmod fails', done => {
it('777 test // unsuccessful | file chmod fails', (done) => {
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();
@@ -56,8 +57,8 @@ describe(CMD, function () {
.catch(done);
});
it('777 test // successful', done => {
cmdFn(mockClient, {arg: '777 test'})
it('777 test // successful', (done) => {
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,14 +9,15 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.stub(mockClient, 'reply').resolves();
});
@@ -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,11 +5,12 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = {
get: () => Promise.resolve({size: 1})
@@ -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,11 +5,12 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = {
get: () => Promise.resolve({}),
@@ -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: {
@@ -17,13 +19,13 @@ describe(CMD, function () {
waitForConnection: () => Promise.resolve({
resume: () => {}
}),
end: () => {}
end: () => 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = {
write: () => {}
@@ -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);
});
@@ -84,11 +86,11 @@ describe(CMD, function () {
});
let errorEmitted = false;
emitter.once('STOR', err => {
emitter.once('STOR', (err) => {
errorEmitted = !!err;
});
return cmdFn(mockClient, {arg: 'test.txt'})
return cmdFn({log, command: {arg: 'test.txt'}})
.then(() => {
expect(errorEmitted).to.equal(true);
});

View File

@@ -10,10 +10,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.fs = {
get: () => Promise.resolve(),
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
sandbox.spy(mockClient, 'reply');
});
@@ -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,10 +8,10 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
mockClient.transferType = null;
sandbox.spy(mockClient, 'reply');
@@ -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,15 +5,18 @@ 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();
sandbox = sinon.createSandbox().usingPromise(Promise);
delete mockClient.username;
mockClient.server.options = {};
@@ -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

@@ -6,9 +6,10 @@ const net = require('net');
const tls = require('tls');
const ActiveConnector = require('../../src/connector/active');
const findPort = require('../../src/helpers/find-port');
const {getNextPortFactory} = require('../../src/helpers/find-port');
describe('Connector - Active //', function () {
let getNextPort = getNextPortFactory(1024);
let PORT;
let active;
let mockConnection = {};
@@ -18,18 +19,18 @@ describe('Connector - Active //', function () {
before(() => {
active = new ActiveConnector(mockConnection);
});
beforeEach(done => {
sandbox = sinon.sandbox.create();
beforeEach((done) => {
sandbox = sinon.createSandbox().usingPromise(Promise);
findPort()
.then(port => {
getNextPort()
.then((port) => {
PORT = port;
server = net.createServer()
.on('connection', socket => socket.destroy())
.on('connection', (socket) => socket.destroy())
.listen(PORT, () => done());
});
});
afterEach(done => {
afterEach((done) => {
sandbox.restore();
server.close(done);
});
@@ -57,7 +58,7 @@ describe('Connector - Active //', function () {
expect(active.dataSocket).to.exist;
return active.waitForConnection();
})
.then(dataSocket => {
.then((dataSocket) => {
expect(dataSocket.connected).to.equal(true);
expect(dataSocket instanceof net.Socket).to.equal(true);
expect(dataSocket instanceof tls.TLSSocket).to.equal(false);
@@ -66,14 +67,18 @@ describe('Connector - Active //', function () {
it('upgrades to a secure connection', function () {
mockConnection.secure = true;
mockConnection.server = {_tls: {}};
mockConnection.server = {
options: {
tls: {}
}
};
return active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(active.dataSocket).to.exist;
return active.waitForConnection();
})
.then(dataSocket => {
.then((dataSocket) => {
expect(dataSocket.connected).to.equal(true);
expect(dataSocket instanceof net.Socket).to.equal(true);
expect(dataSocket instanceof tls.TLSSocket).to.equal(true);

View File

@@ -4,88 +4,120 @@ const sinon = require('sinon');
const Promise = require('bluebird');
const net = require('net');
const bunyan = require('bunyan');
const PassiveConnector = require('../../src/connector/passive');
const {getNextPortFactory} = require('../../src/helpers/find-port');
describe('Connector - Passive //', function () {
let passive;
let mockConnection = {
reply: () => Promise.resolve({}),
close: () => Promise.resolve({}),
encoding: 'utf8',
commandSocket: {},
server: {options: {}}
log: bunyan.createLogger({name: 'passive-test'}),
commandSocket: {
remoteAddress: '::ffff:127.0.0.1'
},
server: {
url: '',
getNextPasvPort: getNextPortFactory(1024)
}
};
let sandbox;
function shouldNotResolve() {
throw new Error('Should not resolve');
}
before(() => {
passive = new PassiveConnector(mockConnection);
sandbox = sinon.sandbox.create().usingPromise(Promise);
});
beforeEach(() => {
sandbox = sinon.sandbox.create();
beforeEach(() => {
sandbox.spy(mockConnection, 'reply');
sandbox.spy(mockConnection, 'close');
mockConnection.commandSocket.remoteAddress = '::ffff:127.0.0.1';
mockConnection.server.options.pasv_range = '8000';
});
afterEach(() => {
sandbox.restore();
});
it('cannot wait for connection with no server', function () {
return passive.waitForConnection()
.then(shouldNotResolve)
.catch(err => {
it('cannot wait for connection with no server', function (done) {
let passive = new PassiveConnector(mockConnection);
passive.waitForConnection()
.catch((err) => {
expect(err.name).to.equal('ConnectorError');
done();
});
});
it('no pasv range provided', function () {
delete mockConnection.server.options.pasv_range;
describe('setup', function () {
before(function () {
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory());
});
return passive.setupServer()
.then(shouldNotResolve)
.catch(err => {
expect(err.name).to.equal('ConnectorError');
it('no pasv range provided', function (done) {
let passive = new PassiveConnector(mockConnection);
passive.setupServer()
.catch((err) => {
try {
expect(err.name).to.equal('ConnectorError');
done();
} catch (ex) {
done(ex);
}
});
});
});
it('has invalid pasv range', function () {
mockConnection.server.options.pasv_range = -1;
describe('setup', function () {
let connection;
before(function () {
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory(-1, -1));
return passive.setupServer()
.then(shouldNotResolve)
.catch(err => {
expect(err).to.be.instanceOf(RangeError);
connection = new PassiveConnector(mockConnection);
});
it('has invalid pasv range', function (done) {
connection.setupServer()
.catch((err) => {
expect(err.name).to.equal('ConnectorError');
done();
});
});
});
it('sets up a server', function () {
let passive = new PassiveConnector(mockConnection);
return passive.setupServer()
.then(() => {
expect(passive.dataServer).to.exist;
return passive.end();
});
});
it('destroys existing server, then sets up a server', function () {
const closeFnSpy = sandbox.spy(passive.dataServer, 'close');
describe('setup', function () {
let passive;
let closeFnSpy;
beforeEach(function () {
passive = new PassiveConnector(mockConnection);
return passive.setupServer()
.then(() => {
closeFnSpy = sandbox.spy(passive.dataServer, 'close');
});
});
afterEach(function () {
return passive.end();
});
return passive.setupServer()
.then(() => {
expect(closeFnSpy.callCount).to.equal(1);
expect(passive.dataServer).to.exist;
it('destroys existing server, then sets up a server', function () {
return passive.setupServer()
.then(() => {
expect(closeFnSpy.callCount).to.equal(1);
expect(passive.dataServer).to.exist;
});
});
});
it('refuses connection with different remote address', function (done) {
mockConnection.commandSocket.remoteAddress = 'bad';
sandbox.stub(mockConnection.commandSocket, 'remoteAddress').value('bad');
let passive = new PassiveConnector(mockConnection);
passive.setupServer()
.then(() => {
expect(passive.dataServer).to.exist;
@@ -96,6 +128,8 @@ describe('Connector - Passive //', function () {
setTimeout(() => {
expect(passive.connection.reply.callCount).to.equal(1);
expect(passive.connection.reply.args[0][0]).to.equal(550);
passive.end();
done();
}, 100);
});
@@ -104,6 +138,7 @@ describe('Connector - Passive //', function () {
});
it('accepts connection', function () {
let passive = new PassiveConnector(mockConnection);
return passive.setupServer()
.then(() => {
expect(passive.dataServer).to.exist;

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.clientPath).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.clientPath).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.clientPath).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.clientPath).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.clientPath).to.equal(
nodePath.normalize('/cool/file.txt'));
expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/cool/file.txt'));
});
});
});

View File

@@ -9,7 +9,7 @@ describe('helpers // file-stat', function () {
let sandbox;
before(function () {
sandbox = sinon.sandbox.create();
sandbox = sinon.createSandbox().usingPromise(Promise);
});
afterEach(function () {
sandbox.restore();

Some files were not shown because too many files have changed in this diff Show More