Compare commits

..

1 Commits

Author SHA1 Message Date
Tyler Stewart
6a907294b6 fix(fs): wrap fs methods in try
This handles throwing better than `resolve`
2018-07-05 10:58:32 -06:00
87 changed files with 5508 additions and 7493 deletions

View File

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

View File

@@ -36,8 +36,6 @@
## Overview ## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable. `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 ## Features
- Extensible [file systems](#file-system) per connection - Extensible [file systems](#file-system) per connection
- Passive and active transfers - Passive and active transfers
@@ -53,7 +51,7 @@ You can use `ftp-srv` to traverse the file system on the server, but it's bigges
// Quick start // Quick start
const FtpSrv = require('ftp-srv'); const FtpSrv = require('ftp-srv');
const ftpServer = new FtpSrv({ options ... }); const ftpServer = new FtpSrv('ftp://0.0.0.0:9876', { options ... });
ftpServer.on('login', (data, resolve, reject) => { ... }); ftpServer.on('login', (data, resolve, reject) => { ... });
... ...
@@ -64,7 +62,7 @@ ftpServer.listen()
## API ## API
### `new FtpSrv({options})` ### `new FtpSrv(url, [{options}])`
#### url #### 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. [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: Supported protocols:
@@ -74,47 +72,45 @@ Supported protocols:
_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. _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"` __Default:__ `"ftp://127.0.0.1:21"`
#### `pasv_url` #### options
##### `pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname. The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box. _Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
__Default:__ `"127.0.0.1"` __Default:__ `"127.0.0.1"`
#### `pasv_min` ##### `pasv_range`
Tne starting port to accept passive connections. A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
__Default:__ `1024` This range is then queried for an available port to use when required.
__Default:__ `22`
#### `pasv_max` ##### `greeting`
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. A human readable array of lines or string to send when a client connects.
__Default:__ `null` __Default:__ `null`
#### `tls` ##### `tls`
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections. Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
__Default:__ `false` __Default:__ `false`
#### `anonymous` ##### `anonymous`
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user. 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. 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. The `login` event is then sent with the provided username and `@anonymous` as the password.
__Default:__ `false` __Default:__ `false`
#### `blacklist` ##### `blacklist`
Array of commands that are not allowed. Array of commands that are not allowed.
Response code `502` is sent to clients sending one of these commands. 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. __Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
__Default:__ `[]` __Default:__ `[]`
#### `whitelist` ##### `whitelist`
Array of commands that are only allowed. Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command. Response code `502` is sent to clients sending any other command.
__Default:__ `[]` __Default:__ `[]`
#### `file_format` ##### `file_format`
Sets the format to use for file stat queries such as `LIST`. Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"` __Default:__ `"ls"`
__Allowable values:__ __Allowable values:__
@@ -123,14 +119,9 @@ __Allowable values:__
- `function () {}` A custom function returning a format or promise for one. - `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 - 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` ##### `log`
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default. 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 ## CLI
`ftp-srv` also comes with a builtin CLI. `ftp-srv` also comes with a builtin CLI.
@@ -269,52 +260,52 @@ class MyFileSystem extends FileSystem {
Custom file systems can implement the following variables depending on the developers needs: Custom file systems can implement the following variables depending on the developers needs:
### Methods ### Methods
#### [`currentDirectory()`](src/fs.js#L40) #### [`currentDirectory()`](src/fs.js#L29)
Returns a string of the current working directory Returns a string of the current working directory
__Used in:__ `PWD` __Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L44) #### [`get(fileName)`](src/fs.js#L33)
Returns a file stat object of file or directory Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM` __Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L50) #### [`list(path)`](src/fs.js#L39)
Returns array of file and directory stat objects Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT` __Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L67) #### [`chdir(path)`](src/fs.js#L56)
Returns new directory relative to current directory Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP` __Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L114) #### [`mkdir(path)`](src/fs.js#L96)
Returns a path to a newly created directory Returns a path to a newly created directory
__Used in:__ `MKD` __Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L79) #### [`write(fileName, {append, start})`](src/fs.js#L68)
Returns a writable stream Returns a writable stream
Options: Options:
`append` if true, append to existing file `append` if true, append to existing file
`start` if set, specifies the byte offset to write to `start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE` __Used in:__ `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L90) #### [`read(fileName, {start})`](src/fs.js#L75)
Returns a readable stream Returns a readable stream
Options: Options:
`start` if set, specifies the byte offset to read from `start` if set, specifies the byte offset to read from
__Used in:__ `RETR` __Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L105) #### [`delete(path)`](src/fs.js#L87)
Delete a file or directory Delete a file or directory
__Used in:__ `DELE` __Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L120) #### [`rename(from, to)`](src/fs.js#L102)
Renames a file or directory Renames a file or directory
__Used in:__ `RNFR`, `RNTO` __Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L126) #### [`chmod(path)`](src/fs.js#L108)
Modifies a file or directory's permissions Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD` __Used in:__ `SITE CHMOD`
#### [`getUniqueName()`](src/fs.js#L131) #### [`getUniqueName()`](src/fs.js#L113)
Returns a unique file name to write to Returns a unique file name to write to
__Used in:__ `STOU` __Used in:__ `STOU`
@@ -335,7 +326,6 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
- [TimLuq](https://github.com/TimLuq) - [TimLuq](https://github.com/TimLuq)
- [edin-mg](https://github.com/edin-m) - [edin-mg](https://github.com/edin-m)
- [DiegoRBaquero](https://github.com/DiegoRBaquero) - [DiegoRBaquero](https://github.com/DiegoRBaquero)
- [Johnnyrook777](https://github.com/Johnnyrook777)
<!--[RM_LICENSE]--> <!--[RM_LICENSE]-->
## License ## License

View File

@@ -19,8 +19,7 @@ function setupYargs() {
}) })
.option('username', { .option('username', {
describe: 'Blank for anonymous', describe: 'Blank for anonymous',
type: 'string', type: 'string'
default: ''
}) })
.option('password', { .option('password', {
describe: 'Password for given username', describe: 'Password for given username',
@@ -37,20 +36,6 @@ function setupYargs() {
boolean: true, boolean: true,
default: false 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(); .parse();
} }
@@ -61,18 +46,15 @@ function setupState(_args) {
if (_args._ && _args._.length > 0) { if (_args._ && _args._.length > 0) {
_state.url = _args._[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 === ''; _state.anonymous = _args.username === '';
} }
function setupRoot() { function setupRoot() {
const dirPath = _args.root; const dirPath = _args.root;
if (dirPath) { if (dirPath) {
_state.root = dirPath;
} else {
_state.root = process.cwd(); _state.root = process.cwd();
} else {
_state.root = dirPath;
} }
} }
@@ -123,11 +105,7 @@ function startFtpServer(_state) {
return reject(new errors.GeneralError('Invalid username or password', 401)); return reject(new errors.GeneralError('Invalid username or password', 401));
} }
const ftpServer = new FtpSrv({ const ftpServer = new FtpSrv(_state.url, {
url: _state.url,
pasv_url: _state.pasv_url,
pasv_min: _state.pasv_min,
pasv_max: _state.pasv_max,
anonymous: _state.anonymous, anonymous: _state.anonymous,
blacklist: _state.blacklist blacklist: _state.blacklist
}); });

View File

@@ -1,44 +0,0 @@
# 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, "comma-dangle": 1,
"new-cap": 2, "new-cap": 2,
"new-parens": 2, "new-parens": 2,
"arrow-parens": [2, "always"], "arrow-parens": [2, "as-needed"],
"no-array-constructor": 2, "no-array-constructor": 2,
"array-callback-return": 1, "array-callback-return": 1,
"no-extra-parens": 2, "no-extra-parens": 2,

19
ftp-srv.d.ts vendored
View File

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

View File

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

11892
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -3,30 +3,25 @@ const Promise = require('bluebird');
const REGISTRY = require('./registry'); const REGISTRY = require('./registry');
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
class FtpCommands { class FtpCommands {
constructor(connection) { constructor(connection) {
this.connection = connection; this.connection = connection;
this.previousCommand = {}; this.previousCommand = {};
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).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)); this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
} }
parse(message) { parse(message) {
const strippedMessage = message.replace(/"/g, ''); const strippedMessage = message.replace(/"/g, '');
let [directive, ...args] = strippedMessage.split(' '); const [directive, ...args] = strippedMessage.split(' ');
directive = _.chain(directive).trim().toUpper().value();
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
const params = args.reduce(({arg, flags}, param) => { const params = args.reduce(({arg, flags}, param) => {
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param); if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
else arg.push(param); else arg.push(param);
return {arg, flags}; return {arg, flags};
}, {arg: [], flags: []}); }, {arg: [], flags: []});
const command = { const command = {
directive, directive: _.chain(directive).trim().toUpper().value(),
arg: params.arg.length ? params.arg.join(' ') : null, arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags, flags: params.flags,
raw: message raw: message

View File

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

View File

@@ -20,17 +20,17 @@ module.exports = {
}; };
function handleTLS() { function handleTLS() {
if (!this.server.options.tls) return this.reply(502); if (!this.server._tls) return this.reply(502);
if (this.secure) return this.reply(202); if (this.secure) return this.reply(202);
return this.reply(234) return this.reply(234)
.then(() => { .then(() => {
const secureContext = tls.createSecureContext(this.server.options.tls); const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.commandSocket, { const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true, isServer: true,
secureContext secureContext
}); });
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => { ['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
function forwardEvent() { function forwardEvent() {
this.emit.apply(this, arguments); this.emit.apply(this, arguments);
} }

View File

@@ -8,11 +8,11 @@ module.exports = {
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system'); if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
return Promise.try(() => this.fs.chdir(command.arg)) return Promise.try(() => this.fs.chdir(command.arg))
.then((cwd) => { .then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined; const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(250, path); return this.reply(250, path);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -10,7 +10,7 @@ module.exports = {
.then(() => { .then(() => {
return this.reply(250); return this.reply(250);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -5,7 +5,7 @@ module.exports = {
handler: function () { handler: function () {
this.connector = new PassiveConnector(this); this.connector = new PassiveConnector(this);
return this.connector.setupServer() return this.connector.setupServer()
.then((server) => { .then(server => {
const {port} = server.address(); const {port} = server.address();
return this.reply(229, `EPSV OK (|||${port}|)`); return this.reply(229, `EPSV OK (|||${port}|)`);

View File

@@ -11,7 +11,7 @@ module.exports = {
return feats; return feats;
}, ['UTF8']) }, ['UTF8'])
.sort() .sort()
.map((feat) => ({ .map(feat => ({
message: ` ${feat}`, message: ` ${feat}`,
raw: true raw: true
})); }));

View File

@@ -12,7 +12,7 @@ module.exports = {
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]); const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
return this.reply(214, ...reply); return this.reply(214, ...reply);
} else { } else {
const supportedCommands = _.chunk(Object.keys(registry), 5).map((chunk) => chunk.join('\t')); 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.'); return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
} }
}, },

View File

@@ -17,32 +17,32 @@ module.exports = {
return this.connector.waitForConnection() return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause()) .tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.get(path))) .then(() => Promise.try(() => this.fs.get(path)))
.then((stat) => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat]) .then(stat => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat])
.then((files) => { .then(files => {
const getFileMessage = (file) => { const getFileMessage = file => {
if (simple) return file.name; if (simple) return file.name;
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls')); return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
}; };
return Promise.try(() => files.map((file) => { const fileList = files.map(file => {
const message = getFileMessage(file); const message = getFileMessage(file);
return { return {
raw: true, raw: true,
message, message,
socket: this.connector.socket socket: this.connector.socket
}; };
})); });
return this.reply(150)
.then(() => {
if (fileList.length) return this.reply({}, ...fileList);
});
}) })
.tap(() => this.reply(150)) .then(() => this.reply(226))
.then((fileList) => { .catch(Promise.TimeoutError, err => {
if (fileList.length) return this.reply({}, ...fileList);
})
.tap(() => this.reply(226))
.catch(Promise.TimeoutError, (err) => {
log.error(err); log.error(err);
return this.reply(425, 'No connection established'); return this.reply(425, 'No connection established');
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(451, err.message || 'No directory'); return this.reply(451, err.message || 'No directory');
}) })

View File

@@ -8,11 +8,11 @@ module.exports = {
if (!this.fs.get) return this.reply(402, 'Not supported by file system'); if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return Promise.try(() => this.fs.get(command.arg)) return Promise.try(() => this.fs.get(command.arg))
.then((fileStat) => { .then(fileStat => {
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS'); const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime); return this.reply(213, modificationTime);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -8,11 +8,11 @@ module.exports = {
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system'); if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
return Promise.try(() => this.fs.mkdir(command.arg)) return Promise.try(() => this.fs.mkdir(command.arg))
.then((dir) => { .then(dir => {
const path = dir ? `"${escapePath(dir)}"` : undefined; const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path); return this.reply(257, path);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -12,7 +12,7 @@ module.exports = {
.then(() => { .then(() => {
return this.reply(230); return this.reply(230);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(530, err.message || 'Authentication failed'); return this.reply(530, err.message || 'Authentication failed');
}); });

View File

@@ -5,7 +5,7 @@ module.exports = {
handler: function () { handler: function () {
this.connector = new PassiveConnector(this); this.connector = new PassiveConnector(this);
return this.connector.setupServer() return this.connector.setupServer()
.then((server) => { .then(server => {
const address = this.server.options.pasv_url; const address = this.server.options.pasv_url;
const {port} = server.address(); const {port} = server.address();
const host = address.replace(/\./g, ','); const host = address.replace(/\./g, ',');

View File

@@ -10,7 +10,7 @@ module.exports = {
if (rawConnection.length !== 6) return this.reply(425); if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).join('.'); 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]; const port = portBytes[0] * 256 + portBytes[1];
return this.connector.setupConnection(ip, port) return this.connector.setupConnection(ip, port)

View File

@@ -8,11 +8,11 @@ module.exports = {
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system'); if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
return Promise.try(() => this.fs.currentDirectory()) return Promise.try(() => this.fs.currentDirectory())
.then((cwd) => { .then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined; const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(257, path); return this.reply(257, path);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -11,21 +11,14 @@ module.exports = {
return this.connector.waitForConnection() return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause()) .tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount}))) .then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
.then((fsResponse) => { .then(stream => {
let {stream, clientPath} = fsResponse; const destroyConnection = (connection, reject) => err => {
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = filePath;
}
const serverPath = stream.path || filePath;
const destroyConnection = (connection, reject) => (err) => {
if (connection) connection.destroy(err); if (connection) connection.destroy(err);
reject(err); reject(err);
}; };
const eventsPromise = new Promise((resolve, reject) => { const eventsPromise = new Promise((resolve, reject) => {
stream.on('data', (data) => { stream.on('data', data => {
if (stream) stream.pause(); if (stream) stream.pause();
if (this.connector.socket) { if (this.connector.socket) {
this.connector.socket.write(data, this.transferType, () => stream && stream.resume()); this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
@@ -41,15 +34,15 @@ module.exports = {
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume()) return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
.then(() => eventsPromise) .then(() => eventsPromise)
.tap(() => this.emit('RETR', null, serverPath)) .tap(() => this.emit('RETR', null, filePath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy()); .finally(() => stream.destroy && stream.destroy());
}) })
.catch(Promise.TimeoutError, (err) => { .then(() => this.reply(226))
.catch(Promise.TimeoutError, err => {
log.error(err); log.error(err);
return this.reply(425, 'No connection established'); return this.reply(425, 'No connection established');
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
this.emit('RETR', err); this.emit('RETR', err);
return this.reply(551, err.message); return this.reply(551, err.message);

View File

@@ -12,7 +12,7 @@ module.exports = {
this.renameFrom = fileName; this.renameFrom = fileName;
return this.reply(350); return this.reply(350);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -15,7 +15,7 @@ module.exports = {
.then(() => { .then(() => {
return this.reply(250); return this.reply(250);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}) })

View File

@@ -10,7 +10,7 @@ module.exports = function ({log, command} = {}) {
.then(() => { .then(() => {
return this.reply(200); return this.reply(200);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(500); return this.reply(500);
}); });

View File

@@ -7,10 +7,10 @@ module.exports = {
if (!this.fs.get) return this.reply(402, 'Not supported by file system'); if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return Promise.try(() => this.fs.get(command.arg)) return Promise.try(() => this.fs.get(command.arg))
.then((fileStat) => { .then(fileStat => {
return this.reply(213, {message: fileStat.size}); return this.reply(213, {message: fileStat.size});
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(550, err.message); return this.reply(550, err.message);
}); });

View File

@@ -12,27 +12,27 @@ module.exports = {
if (!this.fs.get) return this.reply(402, 'Not supported by file system'); if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return Promise.try(() => this.fs.get(path)) return Promise.try(() => this.fs.get(path))
.then((stat) => { .then(stat => {
if (stat.isDirectory()) { if (stat.isDirectory()) {
if (!this.fs.list) return this.reply(402, 'Not supported by file system'); if (!this.fs.list) return this.reply(402, 'Not supported by file system');
return Promise.try(() => this.fs.list(path)) return Promise.try(() => this.fs.list(path))
.then((stats) => [213, stats]); .then(stats => [213, stats]);
} }
return [212, [stat]]; return [212, [stat]];
}) })
.then(([code, fileStats]) => { .then(([code, fileStats]) => {
return Promise.map(fileStats, (file) => { return Promise.map(fileStats, file => {
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls')); const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
return { return {
raw: true, raw: true,
message message
}; };
}) })
.then((messages) => [code, messages]); .then(messages => [code, messages]);
}) })
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end')) .then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(450, err.message); return this.reply(450, err.message);
}); });

View File

@@ -12,19 +12,9 @@ module.exports = {
return this.connector.waitForConnection() return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause()) .tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount}))) .then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
.then((fsResponse) => { .then(stream => {
let {stream, clientPath} = fsResponse; const destroyConnection = (connection, reject) => err => {
if (!stream && !clientPath) { if (connection) connection.destroy(err);
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); reject(err);
}; };
@@ -34,7 +24,7 @@ module.exports = {
}); });
const socketPromise = new Promise((resolve, reject) => { const socketPromise = new Promise((resolve, reject) => {
this.connector.socket.on('data', (data) => { this.connector.socket.on('data', data => {
if (this.connector.socket) this.connector.socket.pause(); if (this.connector.socket) this.connector.socket.pause();
if (stream) { if (stream) {
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume()); stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
@@ -51,16 +41,16 @@ module.exports = {
this.restByteCount = 0; this.restByteCount = 0;
return this.reply(150).then(() => this.connector.socket.resume()) return this.reply(150).then(() => this.connector.socket.resume())
.then(() => Promise.all([streamPromise, socketPromise])) .then(() => Promise.join(streamPromise, socketPromise))
.tap(() => this.emit('STOR', null, serverPath)) .tap(() => this.emit('STOR', null, fileName))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy()); .finally(() => stream.destroy && stream.destroy());
}) })
.catch(Promise.TimeoutError, (err) => { .then(() => this.reply(226, fileName))
.catch(Promise.TimeoutError, err => {
log.error(err); log.error(err);
return this.reply(425, 'No connection established'); return this.reply(425, 'No connection established');
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
this.emit('STOR', err); this.emit('STOR', err);
return this.reply(550, err.message); return this.reply(550, err.message);

View File

@@ -11,7 +11,7 @@ module.exports = {
return Promise.try(() => this.fs.get(fileName)) return Promise.try(() => this.fs.get(fileName))
.then(() => Promise.try(() => this.fs.getUniqueName())) .then(() => Promise.try(() => this.fs.getUniqueName()))
.catch(() => fileName) .catch(() => fileName)
.then((name) => { .then(name => {
args.command.arg = name; args.command.arg = name;
return stor.call(this, args); return stor.call(this, args);
}); });

View File

@@ -13,7 +13,7 @@ module.exports = {
.then(() => { .then(() => {
return this.reply(230); return this.reply(230);
}) })
.catch((err) => { .catch(err => {
log.error(err); log.error(err);
return this.reply(530, err.message || 'Authentication failed'); return this.reply(530, err.message || 'Authentication failed');
}); });

View File

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

View File

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

View File

@@ -29,12 +29,12 @@ class Active extends Connector {
.then(() => { .then(() => {
this.dataSocket = new Socket(); this.dataSocket = new Socket();
this.dataSocket.setEncoding(this.connection.transferType); this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err})); 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.connect({host, port, family}, () => {
this.dataSocket.pause(); this.dataSocket.pause();
if (this.connection.secure) { if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server.options.tls); const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.dataSocket, { const secureSocket = new tls.TLSSocket(this.dataSocket, {
isServer: true, isServer: true,
secureContext secureContext

View File

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

View File

@@ -4,6 +4,7 @@ const ip = require('ip');
const Promise = require('bluebird'); const Promise = require('bluebird');
const Connector = require('./base'); const Connector = require('./base');
const findPort = require('../helpers/find-port');
const errors = require('../errors'); const errors = require('../errors');
class Passive extends Connector { class Passive extends Connector {
@@ -12,7 +13,7 @@ class Passive extends Connector {
this.type = 'passive'; this.type = 'passive';
} }
waitForConnection({timeout = 5000, delay = 50} = {}) { waitForConnection({timeout = 5000, delay = 250} = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup')); if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
const checkSocket = () => { const checkSocket = () => {
@@ -27,10 +28,14 @@ class Passive extends Connector {
} }
setupServer() { setupServer() {
this.closeServer(); const closeExistingServer = () => this.dataServer ?
return this.server.getNextPasvPort() new Promise(resolve => this.dataServer.close(() => resolve())) :
.then((port) => { Promise.resolve();
const connectionHandler = (socket) => {
return closeExistingServer()
.then(() => this.getPort())
.then(port => {
const connectionHandler = socket => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) { if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
this.log.error({ this.log.error({
pasv_connection: socket.remoteAddress, pasv_connection: socket.remoteAddress,
@@ -43,35 +48,36 @@ class Passive extends Connector {
} }
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.'); this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
this.dataSocket = socket; if (this.connection.secure) {
this.dataSocket.setEncoding(this.connection.transferType); const secureContext = tls.createSecureContext(this.server._tls);
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err})); const secureSocket = new tls.TLSSocket(socket, {
isServer: true,
if (!this.connection.secure) { secureContext
this.dataSocket.connected = true; });
this.dataSocket = secureSocket;
} else {
this.dataSocket = socket;
} }
this.dataSocket.connected = true;
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.on('close', () => {
this.log.trace('Passive connection closed');
this.end();
});
}; };
this.dataSocket = null; 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.maxConnections = 1;
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err})); this.dataServer.on('close', () => {
this.dataServer.once('close', () => {
this.log.trace('Passive server closed'); this.log.trace('Passive server closed');
this.end(); this.dataServer = null;
}); });
if (this.connection.secure) {
this.dataServer.on('secureConnection', (socket) => {
socket.connected = true;
});
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.dataServer.listen(port, this.server.url.hostname, (err) => { this.dataServer.listen(port, this.server.url.hostname, err => {
if (err) reject(err); if (err) reject(err);
else { else {
this.log.debug({port}, 'Passive connection listening'); this.log.debug({port}, 'Passive connection listening');
@@ -82,5 +88,15 @@ 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; module.exports = Passive;

View File

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

View File

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

View File

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

View File

@@ -4,42 +4,38 @@ const nodeUrl = require('url');
const buyan = require('bunyan'); const buyan = require('bunyan');
const net = require('net'); const net = require('net');
const tls = require('tls'); const tls = require('tls');
const fs = require('fs');
const EventEmitter = require('events'); const EventEmitter = require('events');
const Connection = require('./connection'); const Connection = require('./connection');
const resolveHost = require('./helpers/resolve-host'); const resolveHost = require('./helpers/resolve-host');
const {getNextPortFactory} = require('./helpers/find-port');
class FtpServer extends EventEmitter { class FtpServer extends EventEmitter {
constructor(options = {}) { constructor(url, options = {}) {
super(); super();
this.options = Object.assign({ this.options = _.merge({
log: buyan.createLogger({name: 'ftp-srv'}), log: buyan.createLogger({name: 'ftp-srv'}),
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
anonymous: false, anonymous: false,
pasv_range: 22,
pasv_url: null,
file_format: 'ls', file_format: 'ls',
blacklist: [], blacklist: [],
whitelist: [], whitelist: [],
greeting: null, greeting: null,
tls: false tls: false
}, options); }, options);
this._greeting = this.setupGreeting(this.options.greeting); this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage(); this._features = this.setupFeaturesMessage();
this._tls = this.setupTLS(this.options.tls);
delete this.options.greeting; delete this.options.greeting;
delete this.options.tls;
this.connections = {}; this.connections = {};
this.log = this.options.log; this.log = this.options.log;
this.url = nodeUrl.parse(this.options.url); this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max'));
const serverConnectionHandler = (socket) => { const serverConnectionHandler = socket => {
let connection = new Connection(this, {log: this.log, socket}); let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection; this.connections[connection.id] = connection;
@@ -50,10 +46,10 @@ class FtpServer extends EventEmitter {
return connection.reply(220, ...greeting, features) return connection.reply(220, ...greeting, features)
.finally(() => socket.resume()); .finally(() => socket.resume());
}; };
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true}); const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler); this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', (err) => this.log.error(err, '[Event] error')); this.server.on('error', err => this.log.error(err, '[Event] error'));
const quit = _.debounce(this.quit.bind(this), 100); const quit = _.debounce(this.quit.bind(this), 100);
@@ -63,17 +59,17 @@ class FtpServer extends EventEmitter {
} }
get isTLS() { get isTLS() {
return this.url.protocol === 'ftps:' && this.options.tls; return this.url.protocol === 'ftps:' && this._tls;
} }
listen() { listen() {
return resolveHost(this.options.pasv_url || this.url.hostname) return resolveHost(this.options.pasv_url || this.url.hostname)
.then((pasvUrl) => { .then(pasvUrl => {
this.options.pasv_url = pasvUrl; this.options.pasv_url = pasvUrl;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.server.once('error', 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); this.server.removeListener('error', reject);
if (err) return reject(err); if (err) return reject(err);
this.log.info({ this.log.info({
@@ -94,6 +90,15 @@ 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) { setupGreeting(greet) {
if (!greet) return []; if (!greet) return [];
const greeting = Array.isArray(greet) ? greet : greet.split('\n'); const greeting = Array.isArray(greet) ? greet : greet.split('\n');
@@ -112,7 +117,7 @@ class FtpServer extends EventEmitter {
} }
disconnectClient(id) { disconnectClient(id) {
return new Promise((resolve) => { return new Promise(resolve => {
const client = this.connections[id]; const client = this.connections[id];
if (!client) return resolve(); if (!client) return resolve();
delete this.connections[id]; delete this.connections[id];
@@ -134,9 +139,9 @@ class FtpServer extends EventEmitter {
close() { close() {
this.log.info('Server closing...'); this.log.info('Server closing...');
this.server.maxConnections = 0; this.server.maxConnections = 0;
return Promise.map(Object.keys(this.connections), (id) => Promise.try(this.disconnectClient.bind(this, id))) return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => new Promise((resolve) => { .then(() => new Promise(resolve => {
this.server.close((err) => { this.server.close(err => {
if (err) this.log.error(err, 'Error closing server'); if (err) this.log.error(err, 'Error closing server');
resolve('Closed'); resolve('Closed');
}); });

View File

@@ -20,7 +20,7 @@ describe('FtpCommands', function () {
}; };
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
commands = new FtpCommands(mockConnection); commands = new FtpCommands(mockConnection);
@@ -64,8 +64,8 @@ describe('FtpCommands', function () {
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => { it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A'); const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
expect(cmd.directive).to.equal('TEST'); expect(cmd.directive).to.equal('TEST');
expect(cmd.arg).to.equal('arg1 arg2 --zz88A'); expect(cmd.arg).to.equal('arg1 arg2');
expect(cmd.flags).to.deep.equal(['-l', '-A']); expect(cmd.flags).to.deep.equal(['-l', '-A', '--zz88A']);
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A'); expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
}); });
@@ -76,13 +76,6 @@ describe('FtpCommands', function () {
expect(cmd.flags).to.deep.equal(['-l']); expect(cmd.flags).to.deep.equal(['-l']);
expect(cmd.raw).to.equal('list -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 () { describe('handle', function () {

View File

@@ -3,7 +3,7 @@ const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
const CMD = 'ABOR'; const CMD = 'ABOR';
describe.skip(CMD, function () { describe(CMD, function () {
let sandbox; let sandbox;
const mockClient = { const mockClient = {
reply: () => Promise.resolve(), reply: () => Promise.resolve(),
@@ -15,7 +15,7 @@ describe.skip(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.connector, 'waitForConnection'); sandbox.spy(mockClient.connector, 'waitForConnection');

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -8,15 +8,13 @@ describe(CMD, function () {
const mockClient = { const mockClient = {
reply: () => Promise.resolve(), reply: () => Promise.resolve(),
server: { server: {
options: { _tls: {}
tls: {}
}
} }
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -16,7 +16,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'chdir'); sandbox.spy(mockClient.fs, 'chdir');

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'chdir').resolves(); sandbox.stub(mockClient.fs, 'chdir').resolves();

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'delete').resolves(); sandbox.stub(mockClient.fs, 'delete').resolves();

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves(); sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({ sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -15,7 +15,7 @@ describe(CMD, function () {
}, },
connector: { connector: {
waitForConnection: () => Promise.resolve({}), waitForConnection: () => Promise.resolve({}),
end: () => Promise.resolve({}) end: () => {}
}, },
commandSocket: { commandSocket: {
resume: () => {}, resume: () => {},
@@ -25,7 +25,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({ sandbox.stub(mockClient.fs, 'get').resolves({

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'}); sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'mkdir').resolves(); sandbox.stub(mockClient.fs, 'mkdir').resolves();

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -15,7 +15,7 @@ describe(CMD, function () {
}, },
connector: { connector: {
waitForConnection: () => Promise.resolve({}), waitForConnection: () => Promise.resolve({}),
end: () => Promise.resolve({}) end: () => {}
}, },
commandSocket: { commandSocket: {
resume: () => {}, resume: () => {},
@@ -25,7 +25,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'get').resolves({ sandbox.stub(mockClient.fs, 'get').resolves({

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -15,7 +15,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient, 'login').resolves(); sandbox.stub(mockClient, 'login').resolves();

View File

@@ -12,7 +12,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves(); sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();

View File

@@ -12,7 +12,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
sandbox.stub(mockClient.fs, 'currentDirectory').resolves(); sandbox.stub(mockClient.fs, 'currentDirectory').resolves();

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'close').resolves(); sandbox.stub(mockClient, 'close').resolves();
}); });

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -19,13 +19,13 @@ describe(CMD, function () {
waitForConnection: () => Promise.resolve({ waitForConnection: () => Promise.resolve({
resume: () => {} resume: () => {}
}), }),
end: () => Promise.resolve({}) end: () => {}
} }
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.fs = { mockClient.fs = {
read: () => {} read: () => {}
@@ -87,7 +87,7 @@ describe(CMD, function () {
}); });
let errorEmitted = false; let errorEmitted = false;
emitter.once('RETR', (err) => { emitter.once('RETR', err => {
errorEmitted = !!err; errorEmitted = !!err;
}); });

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.renameFrom = 'test'; mockClient.renameFrom = 'test';
mockClient.fs = { mockClient.fs = {

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.renameFrom = 'test'; mockClient.renameFrom = 'test';
mockClient.fs = { mockClient.fs = {

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient); const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.fs = { mockClient.fs = {
chmod: () => Promise.resolve() chmod: () => Promise.resolve()
@@ -23,7 +23,7 @@ describe(CMD, function () {
sandbox.restore(); sandbox.restore();
}); });
it('// unsuccessful | no file system', (done) => { it('// unsuccessful | no file system', done => {
delete mockClient.fs; delete mockClient.fs;
cmdFn() cmdFn()
@@ -34,7 +34,7 @@ describe(CMD, function () {
.catch(done); .catch(done);
}); });
it('// unsuccessful | file system does not have functions', (done) => { it('// unsuccessful | file system does not have functions', done => {
mockClient.fs = {}; mockClient.fs = {};
cmdFn() cmdFn()
@@ -45,7 +45,7 @@ describe(CMD, function () {
.catch(done); .catch(done);
}); });
it('777 test // unsuccessful | file chmod fails', (done) => { it('777 test // unsuccessful | file chmod fails', done => {
mockClient.fs.chmod.restore(); mockClient.fs.chmod.restore();
sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test')); sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test'));
@@ -57,7 +57,7 @@ describe(CMD, function () {
.catch(done); .catch(done);
}); });
it('777 test // successful', (done) => { it('777 test // successful', done => {
cmdFn({log: mockLog, command: {arg: '777 test'}}) cmdFn({log: mockLog, command: {arg: '777 test'}})
.then(() => { .then(() => {
expect(mockClient.fs.chmod.args[0]).to.eql(['test', 511]); expect(mockClient.fs.chmod.args[0]).to.eql(['test', 511]);

View File

@@ -17,7 +17,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.stub(mockClient, 'reply').resolves(); sandbox.stub(mockClient, 'reply').resolves();
}); });

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.fs = { mockClient.fs = {
get: () => Promise.resolve({size: 1}) get: () => Promise.resolve({size: 1})

View File

@@ -10,7 +10,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.fs = { mockClient.fs = {
get: () => Promise.resolve({}), get: () => Promise.resolve({}),

View File

@@ -19,13 +19,13 @@ describe(CMD, function () {
waitForConnection: () => Promise.resolve({ waitForConnection: () => Promise.resolve({
resume: () => {} resume: () => {}
}), }),
end: () => Promise.resolve({}) end: () => {}
} }
}; };
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.fs = { mockClient.fs = {
write: () => {} write: () => {}
@@ -86,7 +86,7 @@ describe(CMD, function () {
}); });
let errorEmitted = false; let errorEmitted = false;
emitter.once('STOR', (err) => { emitter.once('STOR', err => {
errorEmitted = !!err; errorEmitted = !!err;
}); });

View File

@@ -13,7 +13,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.fs = { mockClient.fs = {
get: () => Promise.resolve(), get: () => Promise.resolve(),

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');
}); });

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
mockClient.transferType = null; mockClient.transferType = null;
sandbox.spy(mockClient, 'reply'); sandbox.spy(mockClient, 'reply');

View File

@@ -16,7 +16,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient); const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
delete mockClient.username; delete mockClient.username;
mockClient.server.options = {}; mockClient.server.options = {};

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ describe('FileSystem', function () {
it('handles error', function () { it('handles error', function () {
return Promise.try(() => ovFs.chdir()) return Promise.try(() => ovFs.chdir())
.catch((err) => { .catch(err => {
expect(err).to.be.instanceof(errors.FileSystemError); expect(err).to.be.instanceof(errors.FileSystemError);
}); });
}); });
@@ -38,7 +38,7 @@ describe('FileSystem', function () {
it('gets correct relative path', function () { it('gets correct relative path', function () {
const result = fs._resolvePath(); const result = fs._resolvePath();
expect(result).to.be.an('object'); expect(result).to.be.an('object');
expect(result.clientPath).to.equal( expect(result.serverPath).to.equal(
nodePath.normalize('/file/1/2/3')); nodePath.normalize('/file/1/2/3'));
expect(result.fsPath).to.equal( expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/file/1/2/3')); nodePath.resolve('/tmp/ftp-srv/file/1/2/3'));
@@ -47,7 +47,7 @@ describe('FileSystem', function () {
it('gets correct relative path', function () { it('gets correct relative path', function () {
const result = fs._resolvePath('..'); const result = fs._resolvePath('..');
expect(result).to.be.an('object'); expect(result).to.be.an('object');
expect(result.clientPath).to.equal( expect(result.serverPath).to.equal(
nodePath.normalize('/file/1/2')); nodePath.normalize('/file/1/2'));
expect(result.fsPath).to.equal( expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/file/1/2')); nodePath.resolve('/tmp/ftp-srv/file/1/2'));
@@ -56,7 +56,7 @@ describe('FileSystem', function () {
it('gets correct absolute path', function () { it('gets correct absolute path', function () {
const result = fs._resolvePath('/other'); const result = fs._resolvePath('/other');
expect(result).to.be.an('object'); expect(result).to.be.an('object');
expect(result.clientPath).to.equal( expect(result.serverPath).to.equal(
nodePath.normalize('/other')); nodePath.normalize('/other'));
expect(result.fsPath).to.equal( expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/other')); nodePath.resolve('/tmp/ftp-srv/other'));
@@ -65,7 +65,7 @@ describe('FileSystem', function () {
it('cannot escape root', function () { it('cannot escape root', function () {
const result = fs._resolvePath('../../../../../../../../../../..'); const result = fs._resolvePath('../../../../../../../../../../..');
expect(result).to.be.an('object'); expect(result).to.be.an('object');
expect(result.clientPath).to.equal( expect(result.serverPath).to.equal(
nodePath.normalize('/')); nodePath.normalize('/'));
expect(result.fsPath).to.equal( expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv')); nodePath.resolve('/tmp/ftp-srv'));
@@ -74,7 +74,7 @@ describe('FileSystem', function () {
it('resolves to file', function () { it('resolves to file', function () {
const result = fs._resolvePath('/cool/file.txt'); const result = fs._resolvePath('/cool/file.txt');
expect(result).to.be.an('object'); expect(result).to.be.an('object');
expect(result.clientPath).to.equal( expect(result.serverPath).to.equal(
nodePath.normalize('/cool/file.txt')); nodePath.normalize('/cool/file.txt'));
expect(result.fsPath).to.equal( expect(result.fsPath).to.equal(
nodePath.resolve('/tmp/ftp-srv/cool/file.txt')); nodePath.resolve('/tmp/ftp-srv/cool/file.txt'));

View File

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

View File

@@ -1,52 +1,35 @@
/* eslint no-unused-expressions: 0 */ /* eslint no-unused-expressions: 0 */
const {expect} = require('chai'); const {expect} = require('chai');
const net = require('net'); const {Server} = require('net');
const sinon = require('sinon'); const sinon = require('sinon');
const {getNextPortFactory} = require('../../src/helpers/find-port'); const findPort = require('../../src/helpers/find-port');
describe('helpers // find-port', function () { describe('helpers // find-port', function () {
let sandbox; let sandbox;
let getNextPort;
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
getNextPort = getNextPortFactory(1, 2); sandbox.spy(Server.prototype, 'listen');
}); });
afterEach(() => { afterEach(() => {
sandbox.restore(); sandbox.restore();
}); });
it('finds a port', () => { it('finds a port', () => {
sandbox.stub(net.Server.prototype, 'listen').callsFake(function (port) { return findPort(1)
this.address = () => ({port}); .then(port => {
setImmediate(() => this.emit('listening')); expect(Server.prototype.listen.callCount).to.be.above(1);
}); expect(port).to.be.above(1);
return getNextPort()
.then((port) => {
expect(port).to.equal(1);
}); });
}); });
it('restarts count', () => { it('does not find a port', () => {
sandbox.stub(net.Server.prototype, 'listen').callsFake(function (port) { return findPort(1, 2)
this.address = () => ({port}); .then(() => expect(1).to.equal(2)) // should not happen
setImmediate(() => this.emit('listening')); .catch(err => {
}); expect(err).to.exist;
return getNextPort()
.then((port) => {
expect(port).to.equal(1);
})
.then(() => getNextPort())
.then((port) => {
expect(port).to.equal(2);
})
.then(() => getNextPort())
.then((port) => {
expect(port).to.equal(1);
}); });
}); });
}); });

View File

@@ -7,14 +7,14 @@ describe('helpers //resolve-host', function () {
let sandbox; let sandbox;
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
}); });
afterEach(() => sandbox.restore()); afterEach(() => sandbox.restore());
it('fetches ip address', () => { it('fetches ip address', () => {
const hostname = '0.0.0.0'; const hostname = '0.0.0.0';
return resolveHost(hostname) return resolveHost(hostname)
.then((resolvedHostname) => { .then(resolvedHostname => {
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/); expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
}); });
}); });
@@ -22,7 +22,7 @@ describe('helpers //resolve-host', function () {
it('fetches ip address', () => { it('fetches ip address', () => {
const hostname = null; const hostname = null;
return resolveHost(hostname) return resolveHost(hostname)
.then((resolvedHostname) => { .then(resolvedHostname => {
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/); expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
}); });
}); });
@@ -30,7 +30,7 @@ describe('helpers //resolve-host', function () {
it('does nothing', () => { it('does nothing', () => {
const hostname = '127.0.0.1'; const hostname = '127.0.0.1';
return resolveHost(hostname) return resolveHost(hostname)
.then((resolvedHostname) => { .then(resolvedHostname => {
expect(resolvedHostname).to.equal(hostname); expect(resolvedHostname).to.equal(hostname);
}); });
}); });
@@ -44,7 +44,7 @@ describe('helpers //resolve-host', function () {
return resolveHost(null) return resolveHost(null)
.then(() => expect(1).to.equal(2)) .then(() => expect(1).to.equal(2))
.catch((err) => { .catch(err => {
expect(err.code).to.equal(420); expect(err.code).to.equal(420);
}); });
}); });

View File

@@ -22,10 +22,10 @@ describe('Integration', function () {
const clientDirectory = `${process.cwd()}/test_tmp`; const clientDirectory = `${process.cwd()}/test_tmp`;
before(() => { before(() => {
return startServer({url: 'ftp://127.0.0.1:8880'}); return startServer('ftp://127.0.0.1:8880');
}); });
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox().usingPromise(Promise); sandbox = sinon.sandbox.create();
}); });
afterEach(() => sandbox.restore()); afterEach(() => sandbox.restore());
after(() => server.close()); after(() => server.close());
@@ -36,19 +36,10 @@ describe('Integration', function () {
}); });
after(() => directoryPurge(clientDirectory)); after(() => directoryPurge(clientDirectory));
function readFile(path) { function startServer(url, options = {}) {
return new Promise((resolve, reject) => { server = new FtpServer(url, _.assign({
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
function startServer(options = {}) {
server = new FtpServer(_.assign({
log, log,
pasv_min: 8881, pasv_range: 8881,
greeting: ['hello', 'world'], greeting: ['hello', 'world'],
anonymous: true anonymous: true
}, options)); }, options));
@@ -64,7 +55,7 @@ describe('Integration', function () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
client = new FtpClient(); client = new FtpClient();
client.once('ready', () => resolve(client)); client.once('ready', () => resolve(client));
client.once('error', (err) => reject(err)); client.once('error', err => reject(err));
client.connect(_.assign({ client.connect(_.assign({
host: server.url.hostname, host: server.url.hostname,
port: server.url.port, port: server.url.port,
@@ -72,7 +63,7 @@ describe('Integration', function () {
password: 'test' password: 'test'
}, options)); }, options));
}) })
.then((instance) => { .then(instance => {
client = instance; client = instance;
}); });
} }
@@ -80,8 +71,8 @@ describe('Integration', function () {
function closeClient() { function closeClient() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
client.once('close', () => resolve()); client.once('close', () => resolve());
client.once('error', (err) => reject(err)); client.once('error', err => reject(err));
client.logout((err) => { client.logout(err => {
expect(err).to.be.undefined; expect(err).to.be.undefined;
}); });
}); });
@@ -92,7 +83,7 @@ describe('Integration', function () {
if (!dirExists) return; if (!dirExists) return;
const list = fs.readdirSync(dir); const list = fs.readdirSync(dir);
list.map((item) => nodePath.resolve(dir, item)).forEach((item) => { list.map(item => nodePath.resolve(dir, item)).forEach(item => {
const itemExists = fs.existsSync(dir); const itemExists = fs.existsSync(dir);
if (!itemExists) return; if (!itemExists) return;
@@ -113,7 +104,7 @@ describe('Integration', function () {
after(() => directoryPurge(`${clientDirectory}/${name}/`)); after(() => directoryPurge(`${clientDirectory}/${name}/`));
it('STAT', (done) => { it('STAT', done => {
client.status((err, status) => { client.status((err, status) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(status).to.equal('Status OK'); expect(status).to.equal('Status OK');
@@ -121,7 +112,7 @@ describe('Integration', function () {
}); });
}); });
it('SYST', (done) => { it('SYST', done => {
client.system((err, os) => { client.system((err, os) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(os).to.equal('UNIX'); expect(os).to.equal('UNIX');
@@ -129,7 +120,7 @@ describe('Integration', function () {
}); });
}); });
it('CWD ..', (done) => { it('CWD ..', done => {
client.cwd('..', (err, data) => { client.cwd('..', (err, data) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(data).to.equal('/'); expect(data).to.equal('/');
@@ -137,7 +128,7 @@ describe('Integration', function () {
}); });
}); });
it(`CWD ${name}`, (done) => { it(`CWD ${name}`, done => {
client.cwd(`${name}`, (err, data) => { client.cwd(`${name}`, (err, data) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(data).to.equal(`/${name}`); expect(data).to.equal(`/${name}`);
@@ -145,7 +136,7 @@ describe('Integration', function () {
}); });
}); });
it('PWD', (done) => { it('PWD', done => {
client.pwd((err, data) => { client.pwd((err, data) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(data).to.equal(`/${name}`); expect(data).to.equal(`/${name}`);
@@ -153,7 +144,7 @@ describe('Integration', function () {
}); });
}); });
it('LIST .', (done) => { it('LIST .', done => {
client.list('.', (err, data) => { client.list('.', (err, data) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(data).to.be.an('array'); expect(data).to.be.an('array');
@@ -163,7 +154,7 @@ describe('Integration', function () {
}); });
}); });
it('LIST fake.txt', (done) => { it('LIST fake.txt', done => {
client.list('fake.txt', (err, data) => { client.list('fake.txt', (err, data) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(data).to.be.an('array'); expect(data).to.be.an('array');
@@ -173,7 +164,7 @@ describe('Integration', function () {
}); });
}); });
it('STOR fail.txt', (done) => { it('STOR fail.txt', done => {
const buffer = Buffer.from('test text file'); const buffer = Buffer.from('test text file');
const fsPath = `${clientDirectory}/${name}/fail.txt`; const fsPath = `${clientDirectory}/${name}/fail.txt`;
@@ -185,7 +176,7 @@ describe('Integration', function () {
return stream; return stream;
}); });
client.put(buffer, 'fail.txt', (err) => { client.put(buffer, 'fail.txt', err => {
setImmediate(() => { setImmediate(() => {
const fileExists = fs.existsSync(fsPath); const fileExists = fs.existsSync(fsPath);
expect(err).to.exist; expect(err).to.exist;
@@ -195,15 +186,15 @@ describe('Integration', function () {
}); });
}); });
it('STOR tést.txt', (done) => { it('STOR tést.txt', done => {
const buffer = Buffer.from('test text file'); const buffer = Buffer.from('test text file');
const fsPath = `${clientDirectory}/${name}/tést.txt`; const fsPath = `${clientDirectory}/${name}/tést.txt`;
connection.once('STOR', (err) => { connection.once('STOR', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
}); });
client.put(buffer, 'tést.txt', (err) => { client.put(buffer, 'tést.txt', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
setImmediate(() => { setImmediate(() => {
expect(fs.existsSync(fsPath)).to.equal(true); expect(fs.existsSync(fsPath)).to.equal(true);
@@ -216,10 +207,10 @@ describe('Integration', function () {
}); });
}); });
it('APPE tést.txt', (done) => { it('APPE tést.txt', done => {
const buffer = Buffer.from(', awesome!'); const buffer = Buffer.from(', awesome!');
const fsPath = `${clientDirectory}/${name}/tést.txt`; const fsPath = `${clientDirectory}/${name}/tést.txt`;
client.append(buffer, 'tést.txt', (err) => { client.append(buffer, 'tést.txt', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
setImmediate(() => { setImmediate(() => {
expect(fs.existsSync(fsPath)).to.equal(true); expect(fs.existsSync(fsPath)).to.equal(true);
@@ -232,15 +223,15 @@ describe('Integration', function () {
}); });
}); });
it('RETR tést.txt', (done) => { it('RETR tést.txt', done => {
connection.once('RETR', (err) => { connection.once('RETR', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
}); });
client.get('tést.txt', (err, stream) => { client.get('tést.txt', (err, stream) => {
expect(err).to.not.exist; expect(err).to.not.exist;
let text = ''; let text = '';
stream.on('data', (data) => { stream.on('data', data => {
text += data.toString(); text += data.toString();
}); });
stream.on('end', () => { stream.on('end', () => {
@@ -251,8 +242,8 @@ describe('Integration', function () {
}); });
}); });
it('RNFR tést.txt, RNTO awesome.txt', (done) => { it('RNFR tést.txt, RNTO awesome.txt', done => {
client.rename('tést.txt', 'awesome.txt', (err) => { client.rename('tést.txt', 'awesome.txt', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(fs.existsSync(`${clientDirectory}/${name}/tést.txt`)).to.equal(false); expect(fs.existsSync(`${clientDirectory}/${name}/tést.txt`)).to.equal(false);
expect(fs.existsSync(`${clientDirectory}/${name}/awesome.txt`)).to.equal(true); expect(fs.existsSync(`${clientDirectory}/${name}/awesome.txt`)).to.equal(true);
@@ -264,7 +255,7 @@ describe('Integration', function () {
}); });
}); });
it('SIZE awesome.txt', (done) => { it('SIZE awesome.txt', done => {
client.size('awesome.txt', (err, size) => { client.size('awesome.txt', (err, size) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(size).to.be.a('number'); expect(size).to.be.a('number');
@@ -272,7 +263,7 @@ describe('Integration', function () {
}); });
}); });
it('MDTM awesome.txt', (done) => { it('MDTM awesome.txt', done => {
client.lastMod('awesome.txt', (err, modTime) => { client.lastMod('awesome.txt', (err, modTime) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(modTime).to.be.instanceOf(Date); expect(modTime).to.be.instanceOf(Date);
@@ -281,14 +272,14 @@ describe('Integration', function () {
}); });
}); });
it.skip('MLSD .', (done) => { it.skip('MLSD .', done => {
client.mlsd('.', () => { client.mlsd('.', () => {
done(); done();
}); });
}); });
it('SITE CHMOD 700 awesome.txt', (done) => { it('SITE CHMOD 700 awesome.txt', done => {
client.site('CHMOD 600 awesome.txt', (err) => { client.site('CHMOD 600 awesome.txt', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
fs.stat(`${clientDirectory}/${name}/awesome.txt`, (fserr, stats) => { fs.stat(`${clientDirectory}/${name}/awesome.txt`, (fserr, stats) => {
expect(fserr).to.not.exist; expect(fserr).to.not.exist;
@@ -299,27 +290,27 @@ describe('Integration', function () {
}); });
}); });
it('DELE awesome.txt', (done) => { it('DELE awesome.txt', done => {
client.delete('awesome.txt', (err) => { client.delete('awesome.txt', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(fs.existsSync(`${clientDirectory}/${name}/awesome.txt`)).to.equal(false); expect(fs.existsSync(`${clientDirectory}/${name}/awesome.txt`)).to.equal(false);
done(); done();
}); });
}); });
it('MKD témp', (done) => { it('MKD témp', done => {
const path = `${clientDirectory}/${name}/témp`; const path = `${clientDirectory}/${name}/témp`;
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
fs.rmdirSync(path); fs.rmdirSync(path);
} }
client.mkdir('témp', (err) => { client.mkdir('témp', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(fs.existsSync(path)).to.equal(true); expect(fs.existsSync(path)).to.equal(true);
done(); done();
}); });
}); });
it('CWD témp', (done) => { it('CWD témp', done => {
client.cwd('témp', (err, data) => { client.cwd('témp', (err, data) => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(data).to.to.be.a('string'); expect(data).to.to.be.a('string');
@@ -327,23 +318,23 @@ describe('Integration', function () {
}); });
}); });
it('CDUP', (done) => { it('CDUP', done => {
client.cdup((err) => { client.cdup(err => {
expect(err).to.not.exist; expect(err).to.not.exist;
done(); done();
}); });
}); });
it('RMD témp', (done) => { it('RMD témp', done => {
client.rmdir('témp', (err) => { client.rmdir('témp', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(fs.existsSync(`${clientDirectory}/${name}/témp`)).to.equal(false); expect(fs.existsSync(`${clientDirectory}/${name}/témp`)).to.equal(false);
done(); done();
}); });
}); });
it('CDUP', (done) => { it('CDUP', done => {
client.cdup((err) => { client.cdup(err => {
expect(err).to.not.exist; expect(err).to.not.exist;
done(); done();
}); });
@@ -362,8 +353,8 @@ describe('Integration', function () {
after(() => closeClient(client)); after(() => closeClient(client));
it('TYPE A', (done) => { it('TYPE A', done => {
client.ascii((err) => { client.ascii(err => {
expect(err).to.not.exist; expect(err).to.not.exist;
done(); done();
}); });
@@ -384,8 +375,8 @@ describe('Integration', function () {
after(() => closeClient(client)); after(() => closeClient(client));
it('TYPE I', (done) => { it('TYPE I', done => {
client.binary((err) => { client.binary(err => {
expect(err).to.not.exist; expect(err).to.not.exist;
done(); done();
}); });
@@ -397,14 +388,12 @@ describe('Integration', function () {
describe('#EXPLICIT', function () { describe('#EXPLICIT', function () {
before(() => { before(() => {
return server.close() return server.close()
.then(() => Promise.all([ .then(() => startServer('ftp://127.0.0.1:8880', {
readFile(`${process.cwd()}/test/cert/server.key`), tls: {
readFile(`${process.cwd()}/test/cert/server.crt`), key: `${process.cwd()}/test/cert/server.key`,
readFile(`${process.cwd()}/test/cert/server.csr`) cert: `${process.cwd()}/test/cert/server.crt`,
])) ca: `${process.cwd()}/test/cert/server.csr`
.then(([key, cert, ca]) => startServer({ }
url: 'ftp://127.0.0.1:8880',
tls: {key, cert, ca}
})) }))
.then(() => { .then(() => {
return connectClient({ return connectClient({
@@ -425,14 +414,12 @@ describe('Integration', function () {
describe.skip('#IMPLICIT', function () { describe.skip('#IMPLICIT', function () {
before(() => { before(() => {
return server.close() return server.close()
.then(() => Promise.all([ .then(() => startServer('ftps://127.0.0.1:8880', {
readFile(`${process.cwd()}/test/cert/server.key`), tls: {
readFile(`${process.cwd()}/test/cert/server.crt`), key: `${process.cwd()}/test/cert/server.key`,
readFile(`${process.cwd()}/test/cert/server.csr`) cert: `${process.cwd()}/test/cert/server.crt`,
])) ca: `${process.cwd()}/test/cert/server.csr`
.then(([key, cert, ca]) => startServer({ }
url: 'ftps://127.0.0.1:8880',
tls: {key, cert, ca}
})) }))
.then(() => { .then(() => {
return connectClient({ return connectClient({

View File

@@ -1,16 +1,18 @@
require('dotenv').load();
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const fs = require('fs');
const FtpServer = require('../src'); const FtpServer = require('../src');
const server = new FtpServer({ const log = bunyan.createLogger({name: 'test'});
log: bunyan.createLogger({name: 'test', level: 'trace'}), log.level('trace');
url: 'ftp://127.0.0.1:8880', const server = new FtpServer('ftp://127.0.0.1:8880', {
pasv_min: 8881, log,
pasv_range: 8881,
greeting: ['Welcome', 'to', 'the', 'jungle!'], greeting: ['Welcome', 'to', 'the', 'jungle!'],
tls: { tls: {
key: fs.readFileSync(`${process.cwd()}/test/cert/server.key`), key: `${process.cwd()}/test/cert/server.key`,
cert: fs.readFileSync(`${process.cwd()}/test/cert/server.crt`), cert: `${process.cwd()}/test/cert/server.crt`,
ca: fs.readFileSync(`${process.cwd()}/test/cert/server.csr`) ca: `${process.cwd()}/test/cert/server.csr`
}, },
file_format: 'ep', file_format: 'ep',
anonymous: 'sillyrabbit' anonymous: 'sillyrabbit'