Compare commits

...

23 Commits

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

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

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

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

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

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

* Update list.js

Tidy up of the List function.

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

* refactor(list): chained promises with less depth

* refactor: fixup formatting

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

fixes #120

* refactor: upper directive before checking flag parse

* test: add test for command option parse

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

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

Reuse the Port Number Generator

* feat(passive): initialize port factory in server

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

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

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

If there was a data socket, this would never resolve

* test: use bluebird promises for tests, fix stubs

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

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

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

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

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

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

* test: stop skipping explicit tests
2018-08-08 19:07:06 -06:00
Tyler Stewart
c970a42132 chore(package): update package lock (#107)
Fixes vulnerabilities in packages
2018-08-08 19:04:38 -06:00
Tim Lundqvist
30ae54a952 fix: better typings for EventEmitter inheritance (#105) 2018-07-24 11:46:03 -06:00
74 changed files with 7241 additions and 5023 deletions

View File

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

View File

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

View File

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

6
ftp-srv.d.ts vendored
View File

@@ -78,7 +78,7 @@ export class FtpServer extends EventEmitter {
emitPromise(action: any, ...data: any[]): Promise<any>;
emit(action: any, ...data: any[]): void;
// emit is exported from super class
setupTLS(_tls: boolean): boolean | {
cert: string;
@@ -108,7 +108,7 @@ export class FtpServer extends EventEmitter {
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void): EventEmitter;
) => void): this;
on(event: "client-error", listener: (
data: {
@@ -116,7 +116,7 @@ export class FtpServer extends EventEmitter {
context: string,
error: Error,
}
) => void): EventEmitter;
) => void): this;
}
export {FtpServer as FtpSrv};

11495
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,17 +28,14 @@
"pre-release": "npm run verify",
"commitmsg": "cz-customizable-ghooks",
"dev": "cross-env NODE_ENV=development npm run verify:watch",
"prepush": "npm-run-all verify test:unit:once --silent",
"prepush": "npm-run-all verify test:once --silent",
"semantic-release": "semantic-release",
"start": "npm run dev",
"test": "npm run test:unit",
"test:unit": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
"test:unit:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
"test": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
"test:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
"verify": "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"
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success"
},
"release": {
"verifyConditions": "condition-circle"
@@ -63,7 +60,6 @@
"devDependencies": {
"@icetee/ftp": "^1.0.2",
"chai": "^4.0.2",
"chokidar-cli": "1.2.0",
"condition-circle": "^1.6.0",
"cross-env": "3.1.4",
"cz-customizable": "5.2.0",
@@ -76,12 +72,12 @@
"eslint-plugin-node": "5.1.1",
"husky": "0.13.3",
"istanbul": "0.4.5",
"mocha": "3.5.0",
"mocha": "^5.2.0",
"mocha-junit-reporter": "1.13.0",
"mocha-multi-reporters": "1.1.5",
"npm-run-all": "^4.1.3",
"rimraf": "2.6.1",
"semantic-release": "^11.0.2",
"semantic-release": "^15.10.6",
"sinon": "^2.3.5"
},
"engines": {

View File

@@ -3,6 +3,8 @@ const Promise = require('bluebird');
const REGISTRY = require('./registry');
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
class FtpCommands {
constructor(connection) {
this.connection = connection;
@@ -13,15 +15,18 @@ class FtpCommands {
parse(message) {
const strippedMessage = message.replace(/"/g, '');
const [directive, ...args] = strippedMessage.split(' ');
let [directive, ...args] = strippedMessage.split(' ');
directive = _.chain(directive).trim().toUpper().value();
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
const params = args.reduce(({arg, flags}, param) => {
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const command = {
directive: _.chain(directive).trim().toUpper().value(),
directive,
arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags,
raw: message

View File

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

View File

@@ -20,12 +20,12 @@ module.exports = {
};
function handleTLS() {
if (!this.server._tls) return this.reply(502);
if (!this.server.options.tls) return this.reply(502);
if (this.secure) return this.reply(202);
return this.reply(234)
.then(() => {
const secureContext = tls.createSecureContext(this.server._tls);
const secureContext = tls.createSecureContext(this.server.options.tls);
const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true,
secureContext

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
return Promise.resolve(this.fs.chdir(command.arg))
return Promise.try(() => this.fs.chdir(command.arg))
.then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(250, path);

View File

@@ -6,7 +6,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
return Promise.resolve(this.fs.delete(command.arg))
return Promise.try(() => this.fs.delete(command.arg))
.then(() => {
return this.reply(250);
})

View File

@@ -16,28 +16,28 @@ module.exports = {
const path = command.arg || '.';
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.resolve(this.fs.get(path)))
.then(stat => stat.isDirectory() ? Promise.resolve(this.fs.list(path)) : [stat])
.then(() => Promise.try(() => this.fs.get(path)))
.then(stat => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat])
.then(files => {
const getFileMessage = file => {
if (simple) return file.name;
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
};
const fileList = files.map(file => {
return Promise.try(() => files.map(file => {
const message = getFileMessage(file);
return {
raw: true,
message,
socket: this.connector.socket
};
});
return this.reply(150)
.then(() => {
if (fileList.length) return this.reply({}, ...fileList);
});
}));
})
.then(() => this.reply(226))
.tap(() => this.reply(150))
.then(fileList => {
if (fileList.length) return this.reply({}, ...fileList);
})
.tap(() => this.reply(226))
.catch(Promise.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
@@ -46,10 +46,7 @@ module.exports = {
log.error(err);
return this.reply(451, err.message || 'No directory');
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
.finally(() => this.connector.end().then(() => this.commandSocket.resume()));
},
syntax: '{{cmd}} [<path>]',
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return Promise.resolve(this.fs.get(command.arg))
return Promise.try(() => this.fs.get(command.arg))
.then(fileStat => {
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime);

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
return Promise.resolve(this.fs.mkdir(command.arg))
return Promise.try(() => this.fs.mkdir(command.arg))
.then(dir => {
const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path);

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
return Promise.resolve(this.fs.currentDirectory())
return Promise.try(() => this.fs.currentDirectory())
.then(cwd => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(257, path);

View File

@@ -10,8 +10,15 @@ module.exports = {
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.resolve(this.fs.read(filePath, {start: this.restByteCount})))
.then(stream => {
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
.then(fsResponse => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = filePath;
}
const serverPath = stream.path || filePath;
const destroyConnection = (connection, reject) => err => {
if (connection) connection.destroy(err);
reject(err);
@@ -34,10 +41,10 @@ module.exports = {
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
.then(() => eventsPromise)
.tap(() => this.emit('RETR', null, filePath))
.tap(() => this.emit('RETR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
})
.then(() => this.reply(226))
.catch(Promise.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
@@ -47,10 +54,7 @@ module.exports = {
this.emit('RETR', err);
return this.reply(551, err.message);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
.finally(() => this.connector.end().then(() => this.commandSocket.resume()));
},
syntax: '{{cmd}} <path>',
description: 'Retrieve a copy of the file'

View File

@@ -7,7 +7,7 @@ module.exports = {
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
const fileName = command.arg;
return Promise.resolve(this.fs.get(fileName))
return Promise.try(() => this.fs.get(fileName))
.then(() => {
this.renameFrom = fileName;
return this.reply(350);

View File

@@ -11,7 +11,7 @@ module.exports = {
const from = this.renameFrom;
const to = command.arg;
return Promise.resolve(this.fs.rename(from, to))
return Promise.try(() => this.fs.rename(from, to))
.then(() => {
return this.reply(250);
})

View File

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

View File

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

View File

@@ -11,12 +11,12 @@ module.exports = {
if (!this.fs) return this.reply(550, 'File system not instantiated');
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
return Promise.resolve(this.fs.get(path))
return Promise.try(() => this.fs.get(path))
.then(stat => {
if (stat.isDirectory()) {
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
return Promise.resolve(this.fs.list(path))
return Promise.try(() => this.fs.list(path))
.then(stats => [213, stats]);
}
return [212, [stat]];

View File

@@ -11,10 +11,20 @@ module.exports = {
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.resolve(this.fs.write(fileName, {append, start: this.restByteCount})))
.then(stream => {
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
.then(fsResponse => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = fileName;
}
const serverPath = stream.path || fileName;
const destroyConnection = (connection, reject) => err => {
if (connection) connection.destroy(err);
if (connection) {
if (connection.writeable) connection.end();
connection.destroy(err);
}
reject(err);
};
@@ -30,7 +40,7 @@ module.exports = {
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
}
});
this.connector.socket.once('end', () => {
this.connector.socket.once('close', () => {
if (stream.listenerCount('close')) stream.emit('close');
else stream.end();
resolve();
@@ -41,11 +51,11 @@ module.exports = {
this.restByteCount = 0;
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => Promise.join(streamPromise, socketPromise))
.tap(() => this.emit('STOR', null, fileName))
.then(() => Promise.all([streamPromise, socketPromise]))
.tap(() => this.emit('STOR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
})
.then(() => this.reply(226, fileName))
.catch(Promise.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
@@ -55,10 +65,7 @@ module.exports = {
this.emit('STOR', err);
return this.reply(550, err.message);
})
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});
.finally(() => this.connector.end().then(() => this.commandSocket.resume()));
},
syntax: '{{cmd}} <path>',
description: 'Store data as a file at the server site'

View File

@@ -8,11 +8,9 @@ module.exports = {
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
const fileName = args.command.arg;
return Promise.try(() => {
return Promise.resolve(this.fs.get(fileName))
.then(() => Promise.resolve(this.fs.getUniqueName()))
.catch(() => Promise.resolve(fileName));
})
return Promise.try(() => this.fs.get(fileName))
.then(() => Promise.try(() => this.fs.getUniqueName()))
.catch(() => fileName)
.then(name => {
args.command.arg = name;
return stor.call(this, args);

View File

@@ -34,7 +34,7 @@ class Active extends Connector {
this.dataSocket.pause();
if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server._tls);
const secureContext = tls.createSecureContext(this.server.options.tls);
const secureSocket = new tls.TLSSocket(this.dataSocket, {
isServer: true,
secureContext

View File

@@ -28,12 +28,16 @@ class Connector {
end() {
const closeDataSocket = new Promise(resolve => {
if (this.dataSocket) this.dataSocket.end();
else resolve();
if (this.dataSocket) {
this.dataSocket.end().destroy();
this.dataSocket = null;
}
resolve();
});
const closeDataServer = new Promise(resolve => {
if (this.dataServer) this.dataServer.close(() => resolve());
else resolve();
if (this.dataServer) {
this.dataServer.close(() => resolve());
} else resolve();
});
return Promise.all([closeDataSocket, closeDataServer])
@@ -41,6 +45,8 @@ class Connector {
this.dataSocket = null;
this.dataServer = null;
this.type = false;
this.connection.connector = new Connector(this);
});
}
}

View File

@@ -4,7 +4,6 @@ const ip = require('ip');
const Promise = require('bluebird');
const Connector = require('./base');
const findPort = require('../helpers/find-port');
const errors = require('../errors');
class Passive extends Connector {
@@ -33,7 +32,7 @@ class Passive extends Connector {
Promise.resolve();
return closeExistingServer()
.then(() => this.getPort())
.then(() => this.server.getNextPasvPort())
.then(port => {
const connectionHandler = socket => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
@@ -48,32 +47,26 @@ class Passive extends Connector {
}
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
if (this.connection.secure) {
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(socket, {
isServer: true,
secureContext
});
this.dataSocket = secureSocket;
} else {
this.dataSocket = socket;
}
this.dataSocket = 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.dataSocket.once('close', () => {
this.log.trace('Passive connection closed');
this.end();
});
};
this.dataSocket = null;
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.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.dataServer = null;
this.end();
});
return new Promise((resolve, reject) => {
@@ -88,15 +81,5 @@ class Passive extends Connector {
});
}
getPort() {
if (this.server.options.pasv_range) {
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
[this.server.options.pasv_range];
return findPort(min, max);
}
throw new errors.ConnectorError('Invalid pasv_range');
}
}
module.exports = Passive;

View File

@@ -17,7 +17,7 @@ class FileSystem {
}
_resolvePath(path = '.') {
const serverPath = (() => {
const clientPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
@@ -27,12 +27,12 @@ class FileSystem {
})();
const fsPath = (() => {
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${serverPath}`);
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${clientPath}`);
return nodePath.join(resolvedPath);
})();
return {
serverPath,
clientPath,
fsPath
};
}
@@ -65,34 +65,40 @@ class FileSystem {
}
chdir(path = '.') {
const {fsPath, serverPath} = this._resolvePath(path);
const {fsPath, clientPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.tap(stat => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = serverPath;
this.cwd = clientPath;
return this.currentDirectory();
});
}
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
const {fsPath, clientPath} = this._resolvePath(fileName);
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlinkAsync(fsPath));
stream.once('close', () => stream.end());
return stream;
return {
stream,
clientPath
};
}
read(fileName, {start = undefined} = {}) {
const {fsPath} = this._resolvePath(fileName);
const {fsPath, clientPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.tap(stat => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
return stream;
return {
stream,
clientPath
};
});
}

View File

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

View File

@@ -4,36 +4,40 @@ const nodeUrl = require('url');
const buyan = require('bunyan');
const net = require('net');
const tls = require('tls');
const fs = require('fs');
const EventEmitter = require('events');
const Connection = require('./connection');
const resolveHost = require('./helpers/resolve-host');
const {getNextPortFactory} = require('./helpers/find-port');
class FtpServer extends EventEmitter {
constructor(url, options = {}) {
constructor(options = {}) {
super();
this.options = _.merge({
this.options = Object.assign({
log: buyan.createLogger({name: 'ftp-srv'}),
anonymous: false,
pasv_range: 22,
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
anonymous: false,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false
}, options);
this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage();
this._tls = this.setupTLS(this.options.tls);
delete this.options.greeting;
delete this.options.tls;
this.connections = {};
this.log = this.options.log;
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
this.url = nodeUrl.parse(this.options.url);
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max'));
const serverConnectionHandler = socket => {
let connection = new Connection(this, {log: this.log, socket});
@@ -46,7 +50,7 @@ class FtpServer extends EventEmitter {
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
};
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', err => this.log.error(err, '[Event] error'));
@@ -59,7 +63,7 @@ class FtpServer extends EventEmitter {
}
get isTLS() {
return this.url.protocol === 'ftps:' && this._tls;
return this.url.protocol === 'ftps:' && this.options.tls;
}
listen() {
@@ -90,15 +94,6 @@ class FtpServer extends EventEmitter {
});
}
setupTLS(_tls) {
if (!_tls) return false;
return _.assign({}, _tls, {
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
});
}
setupGreeting(greet) {
if (!greet) return [];
const greeting = Array.isArray(greet) ? greet : greet.split('\n');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,10 @@ const net = require('net');
const tls = require('tls');
const ActiveConnector = require('../../src/connector/active');
const findPort = require('../../src/helpers/find-port');
const {getNextPortFactory} = require('../../src/helpers/find-port');
describe('Connector - Active //', function () {
let getNextPort = getNextPortFactory(1024);
let PORT;
let active;
let mockConnection = {};
@@ -19,9 +20,9 @@ describe('Connector - Active //', function () {
active = new ActiveConnector(mockConnection);
});
beforeEach(done => {
sandbox = sinon.sandbox.create();
sandbox = sinon.sandbox.create().usingPromise(Promise);
findPort()
getNextPort()
.then(port => {
PORT = port;
server = net.createServer()
@@ -66,7 +67,11 @@ describe('Connector - Active //', function () {
it('upgrades to a secure connection', function () {
mockConnection.secure = true;
mockConnection.server = {_tls: {}};
mockConnection.server = {
options: {
tls: {}
}
};
return active.setupConnection('127.0.0.1', PORT)
.then(() => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,10 +22,10 @@ describe('Integration', function () {
const clientDirectory = `${process.cwd()}/test_tmp`;
before(() => {
return startServer('ftp://127.0.0.1:8880');
return startServer({url: 'ftp://127.0.0.1:8880'});
});
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox = sinon.sandbox.create().usingPromise(Promise);
});
afterEach(() => sandbox.restore());
after(() => server.close());
@@ -36,10 +36,19 @@ describe('Integration', function () {
});
after(() => directoryPurge(clientDirectory));
function startServer(url, options = {}) {
server = new FtpServer(url, _.assign({
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
function startServer(options = {}) {
server = new FtpServer(_.assign({
log,
pasv_range: 8881,
pasv_min: 8881,
greeting: ['hello', 'world'],
anonymous: true
}, options));
@@ -385,15 +394,17 @@ describe('Integration', function () {
runFileSystemTests('binary');
});
describe.skip('#EXPLICIT', function () {
describe('#EXPLICIT', function () {
before(() => {
return server.close()
.then(() => startServer('ftp://127.0.0.1:8880', {
tls: {
key: `${process.cwd()}/test/cert/server.key`,
cert: `${process.cwd()}/test/cert/server.crt`,
ca: `${process.cwd()}/test/cert/server.csr`
}
.then(() => Promise.all([
readFile(`${process.cwd()}/test/cert/server.key`),
readFile(`${process.cwd()}/test/cert/server.crt`),
readFile(`${process.cwd()}/test/cert/server.csr`)
]))
.then(([key, cert, ca]) => startServer({
url: 'ftp://127.0.0.1:8880',
tls: {key, cert, ca}
}))
.then(() => {
return connectClient({
@@ -414,12 +425,14 @@ describe('Integration', function () {
describe.skip('#IMPLICIT', function () {
before(() => {
return server.close()
.then(() => startServer('ftps://127.0.0.1:8880', {
tls: {
key: `${process.cwd()}/test/cert/server.key`,
cert: `${process.cwd()}/test/cert/server.crt`,
ca: `${process.cwd()}/test/cert/server.csr`
}
.then(() => Promise.all([
readFile(`${process.cwd()}/test/cert/server.key`),
readFile(`${process.cwd()}/test/cert/server.crt`),
readFile(`${process.cwd()}/test/cert/server.csr`)
]))
.then(([key, cert, ca]) => startServer({
url: 'ftps://127.0.0.1:8880',
tls: {key, cert, ca}
}))
.then(() => {
return connectClient({

View File

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