Compare commits

..

45 Commits

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

* fix: correct tls connection

* fix(connector): dont prematurely destroy socket

* fix(passive): set connected if not tls

* refactor: dont return promises on connector end

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

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

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

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

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

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

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

* Update list.js

Tidy up of the List function.

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

* refactor(list): chained promises with less depth

* refactor: fixup formatting

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

fixes #120

* refactor: upper directive before checking flag parse

* test: add test for command option parse

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

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

Reuse the Port Number Generator

* feat(passive): initialize port factory in server

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

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

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

If there was a data socket, this would never resolve

* test: use bluebird promises for tests, fix stubs

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

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

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

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

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

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

* test: stop skipping explicit tests
2018-08-08 19:07:06 -06:00
Tyler Stewart
c970a42132 chore(package): update package lock (#107)
Fixes vulnerabilities in packages
2018-08-08 19:04:38 -06:00
Tim Lundqvist
30ae54a952 fix: better typings for EventEmitter inheritance (#105) 2018-07-24 11:46:03 -06:00
Tyler Stewart
91be338ebd fix: expose errors as export 2018-07-22 18:47:23 -06:00
Tyler Stewart
2a5013447c fix: fix event emitter extension typing 2018-07-22 18:44:15 -06:00
alancnet
1f15af0fb6 fix(cli): resolve authentication bug (#94)
cli would reject all logins with `530 Cannot destructure property `password` of 'undefined' or 'null'.` because the credentials object was being indexed with `[object Object]`. Even with that fixed, if the username was not found, it would produce that error.
2018-06-08 01:52:51 +00:00
Tyler Stewart
1cf1f750f4 chore(readme): update contributors 2018-06-02 18:51:27 +00:00
Diego Rodríguez Baquero
442490d713 chore(readme): correct word and improve events (#91) 2018-06-02 18:51:27 +00:00
Tyler Stewart
58b9ba27d9 test(fs): add fs tests 2018-05-31 14:28:10 +00:00
Tyler Stewart
87a2138cb3 fix(fs): improve path resolution 2018-05-31 14:28:10 +00:00
Tyler Stewart
9fd423c745 docs: fix circle ci badge
Links only to master branch
2018-05-25 17:34:25 -06:00
Tyler Stewart
363839ec8f chore: fix markdown newlines 2018-05-25 22:50:01 +00:00
Tyler Stewart
d9fc0c9cac feat: pasv_url option to send to client
This has passive connections to listen on the same hostname as the server.
But allows this to be customized via the `pasv_url` option.

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

View File

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

163
README.md
View File

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

View File

@@ -19,7 +19,8 @@ function setupYargs() {
})
.option('username', {
describe: 'Blank for anonymous',
type: 'string'
type: 'string',
default: ''
})
.option('password', {
describe: 'Password for given username',
@@ -36,6 +37,20 @@ function setupYargs() {
boolean: true,
default: false
})
.option('pasv_url', {
describe: 'URL to provide for passive connections',
type: 'string'
})
.option('pasv_min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024
})
.option('pasv_max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535
})
.parse();
}
@@ -46,15 +61,18 @@ function setupState(_args) {
if (_args._ && _args._.length > 0) {
_state.url = _args._[0];
}
_state.pasv_url = _args.pasv_url;
_state.pasv_min = _args.pasv_min;
_state.pasv_max = _args.pasv_max;
_state.anonymous = _args.username === '';
}
function setupRoot() {
const dirPath = _args.root;
if (dirPath) {
_state.root = process.cwd();
} else {
_state.root = dirPath;
} else {
_state.root = process.cwd();
}
}
@@ -62,7 +80,7 @@ function setupState(_args) {
_state.credentials = {};
const setCredentials = (username, password, root = null) => {
_state.credentials[_state.credentials] = {
_state.credentials[username] = {
password,
root
};
@@ -72,7 +90,7 @@ function setupState(_args) {
const credentialsFile = path.resolve(_args.credentials);
const credentials = require(credentialsFile);
for (const cred of Object.entries(credentials)) {
for (const cred of credentials) {
setCredentials(cred.username, cred.password, cred.root);
}
} else if (_args.username) {
@@ -97,15 +115,19 @@ function setupState(_args) {
function startFtpServer(_state) {
function checkLogin(data, resolve, reject) {
const {password, root} = _state.credentials[data.username];
if (_state.anonymous || password === data.password) {
return resolve({root: root || _state.root});
const user = _state.credentials[data.username]
if (_state.anonymous || (user && user.password === data.password)) {
return resolve({root: (user && user.root) || _state.root});
}
return reject(new errors.GeneralError('Invalid username or password', 401));
}
const ftpServer = new FtpSrv(_state.url, {
const ftpServer = new FtpSrv({
url: _state.url,
pasv_url: _state.pasv_url,
pasv_min: _state.pasv_min,
pasv_max: _state.pasv_max,
anonymous: _state.anonymous,
blacklist: _state.blacklist
});

View File

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

View File

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

19
ftp-srv.d.ts vendored
View File

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

View File

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

12662
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -2,12 +2,12 @@ module.exports = {
directive: 'ABOR',
handler: function () {
return this.connector.waitForConnection()
.then(socket => {
.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,17 +20,17 @@ 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
});
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
function forwardEvent() {
this.emit.apply(this, arguments);
}

View File

@@ -7,12 +7,12 @@ 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))
.then(cwd => {
return Promise.try(() => this.fs.chdir(command.arg))
.then((cwd) => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(250, path);
})
.catch(err => {
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ module.exports = {
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
return this.reply(214, ...reply);
} 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.');
}
},

View File

@@ -16,33 +16,33 @@ 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(files => {
const getFileMessage = file => {
.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))
.catch(Promise.TimeoutError, err => {
.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');
})
.catch(err => {
.catch((err) => {
log.error(err);
return this.reply(451, err.message || 'No directory');
})

View File

@@ -7,12 +7,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(command.arg))
.then(fileStat => {
return Promise.try(() => this.fs.get(command.arg))
.then((fileStat) => {
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
return this.reply(213, modificationTime);
})
.catch(err => {
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});

View File

@@ -7,12 +7,12 @@ 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))
.then(dir => {
return Promise.try(() => this.fs.mkdir(command.arg))
.then((dir) => {
const path = dir ? `"${escapePath(dir)}"` : undefined;
return this.reply(257, path);
})
.catch(err => {
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});

View File

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

View File

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

View File

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

View File

@@ -7,12 +7,12 @@ 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())
.then(cwd => {
return Promise.try(() => this.fs.currentDirectory())
.then((cwd) => {
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
return this.reply(257, path);
})
.catch(err => {
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,11 +6,11 @@ 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))
.then(fileStat => {
return Promise.try(() => this.fs.get(command.arg))
.then((fileStat) => {
return this.reply(213, {message: fileStat.size});
})
.catch(err => {
.catch((err) => {
log.error(err);
return this.reply(550, err.message);
});

View File

@@ -11,28 +11,28 @@ 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))
.then(stat => {
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))
.then(stats => [213, stats]);
return Promise.try(() => this.fs.list(path))
.then((stats) => [213, stats]);
}
return [212, [stat]];
})
.then(([code, fileStats]) => {
return Promise.map(fileStats, file => {
return Promise.map(fileStats, (file) => {
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
return {
raw: true,
message
};
})
.then(messages => [code, messages]);
.then((messages) => [code, messages]);
})
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
.catch(err => {
.catch((err) => {
log.error(err);
return this.reply(450, err.message);
});

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 => {
const destroyConnection = (connection, reject) => err => {
if (connection) connection.destroy(err);
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
.then((fsResponse) => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = fileName;
}
const serverPath = stream.path || fileName;
const destroyConnection = (connection, reject) => (err) => {
if (connection) {
if (connection.writeable) connection.end();
connection.destroy(err);
}
reject(err);
};
@@ -24,7 +34,7 @@ module.exports = {
});
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 (stream) {
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
@@ -41,16 +51,16 @@ 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 => {
.catch(Promise.TimeoutError, (err) => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch(err => {
.catch((err) => {
log.error(err);
this.emit('STOR', err);
return this.reply(550, err.message);

View File

@@ -8,12 +8,10 @@ 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));
})
.then(name => {
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

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

View File

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

View File

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

View File

@@ -29,12 +29,12 @@ class Active extends Connector {
.then(() => {
this.dataSocket = new Socket();
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.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

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

View File

@@ -4,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 {
@@ -13,7 +12,7 @@ class Passive extends Connector {
this.type = 'passive';
}
waitForConnection({timeout = 5000, delay = 250} = {}) {
waitForConnection({timeout = 5000, delay = 50} = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
const checkSocket = () => {
@@ -28,14 +27,10 @@ class Passive extends Connector {
}
setupServer() {
const closeExistingServer = () => this.dataServer ?
new Promise(resolve => this.dataServer.close(() => resolve())) :
Promise.resolve();
return closeExistingServer()
.then(() => this.getPort())
.then(port => {
const connectionHandler = socket => {
this.closeServer();
return this.server.getNextPasvPort()
.then((port) => {
const connectionHandler = (socket) => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
this.log.error({
pasv_connection: socket.remoteAddress,
@@ -48,36 +43,35 @@ 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.connected = true;
this.dataSocket = socket;
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.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
if (!this.connection.secure) {
this.dataSocket.connected = true;
}
};
this.dataSocket = null;
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('close', () => {
this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.once('close', () => {
this.log.trace('Passive server closed');
this.dataServer = null;
this.end();
});
if (this.connection.secure) {
this.dataServer.on('secureConnection', (socket) => {
socket.connected = true;
});
}
return new Promise((resolve, reject) => {
this.dataServer.listen(port, err => {
this.dataServer.listen(port, this.server.url.hostname, (err) => {
if (err) reject(err);
else {
this.log.debug({port}, 'Passive connection listening');
@@ -88,15 +82,5 @@ class Passive extends Connector {
});
}
getPort() {
if (this.server.options.pasv_range) {
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
[this.server.options.pasv_range];
return findPort(min, max);
}
throw new errors.ConnectorError('Invalid pasv_range');
}
}
module.exports = Passive;

View File

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

View File

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

View File

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

View File

@@ -4,37 +4,42 @@ 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'}),
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
anonymous: false,
pasv_range: 22,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false
}, options);
this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage();
this._tls = this.setupTLS(this.options.tls);
delete this.options.greeting;
delete this.options.tls;
this.connections = {};
this.log = this.options.log;
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
this.url = nodeUrl.parse(this.options.url);
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max'));
const serverConnectionHandler = socket => {
const serverConnectionHandler = (socket) => {
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
@@ -45,10 +50,10 @@ class FtpServer extends EventEmitter {
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
};
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', err => this.log.error(err, '[Event] error'));
this.server.on('error', (err) => this.log.error(err, '[Event] error'));
const quit = _.debounce(this.quit.bind(this), 100);
@@ -58,16 +63,17 @@ class FtpServer extends EventEmitter {
}
get isTLS() {
return this.url.protocol === 'ftps:' && this._tls;
return this.url.protocol === 'ftps:' && this.options.tls;
}
listen() {
return resolveHost(this.url.hostname)
.then(hostname => {
this.url.hostname = hostname;
return resolveHost(this.options.pasv_url || this.url.hostname)
.then((pasvUrl) => {
this.options.pasv_url = pasvUrl;
return new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.url.port, this.url.hostname, err => {
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject);
if (err) return reject(err);
this.log.info({
@@ -88,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');
@@ -115,7 +112,7 @@ class FtpServer extends EventEmitter {
}
disconnectClient(id) {
return new Promise(resolve => {
return new Promise((resolve) => {
const client = this.connections[id];
if (!client) return resolve();
delete this.connections[id];
@@ -137,9 +134,9 @@ class FtpServer extends EventEmitter {
close() {
this.log.info('Server closing...');
this.server.maxConnections = 0;
return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => new Promise(resolve => {
this.server.close(err => {
return Promise.map(Object.keys(this.connections), (id) => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => new Promise((resolve) => {
this.server.close((err) => {
if (err) this.log.error(err, 'Error closing server');
resolve('Closed');
});

View File

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

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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().usingPromise(Promise);
mockClient.fs = {
read: () => {}
@@ -87,7 +87,7 @@ describe(CMD, function () {
});
let errorEmitted = false;
emitter.once('RETR', err => {
emitter.once('RETR', (err) => {
errorEmitted = !!err;
});

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.createSandbox().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.createSandbox().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.createSandbox().usingPromise(Promise);
mockClient.fs = {
chmod: () => Promise.resolve()
@@ -23,7 +23,7 @@ describe(CMD, function () {
sandbox.restore();
});
it('// unsuccessful | no file system', done => {
it('// unsuccessful | no file system', (done) => {
delete mockClient.fs;
cmdFn()
@@ -34,7 +34,7 @@ describe(CMD, function () {
.catch(done);
});
it('// unsuccessful | file system does not have functions', done => {
it('// unsuccessful | file system does not have functions', (done) => {
mockClient.fs = {};
cmdFn()
@@ -45,7 +45,7 @@ describe(CMD, function () {
.catch(done);
});
it('777 test // unsuccessful | file chmod fails', done => {
it('777 test // unsuccessful | file chmod fails', (done) => {
mockClient.fs.chmod.restore();
sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test'));
@@ -57,7 +57,7 @@ describe(CMD, function () {
.catch(done);
});
it('777 test // successful', done => {
it('777 test // successful', (done) => {
cmdFn({log: mockLog, command: {arg: '777 test'}})
.then(() => {
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);
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox = sinon.createSandbox().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.createSandbox().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.createSandbox().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.createSandbox().usingPromise(Promise);
mockClient.fs = {
write: () => {}
@@ -86,7 +86,7 @@ describe(CMD, function () {
});
let errorEmitted = false;
emitter.once('STOR', err => {
emitter.once('STOR', (err) => {
errorEmitted = !!err;
});

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

View File

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

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

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

View File

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

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.createSandbox().usingPromise(Promise);
sandbox.spy(Server.prototype, 'listen');
getNextPort = getNextPortFactory(1, 2);
});
afterEach(() => {
sandbox.restore();
});
it('finds a port', () => {
return findPort(1)
.then(port => {
expect(Server.prototype.listen.callCount).to.be.above(1);
expect(port).to.be.above(1);
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);
});
});
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,14 +7,14 @@ describe('helpers //resolve-host', function () {
let sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox = sinon.createSandbox().usingPromise(Promise);
});
afterEach(() => sandbox.restore());
it('fetches ip address', () => {
const hostname = '0.0.0.0';
return resolveHost(hostname)
.then(resolvedHostname => {
.then((resolvedHostname) => {
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
});
});
@@ -22,7 +22,7 @@ describe('helpers //resolve-host', function () {
it('fetches ip address', () => {
const hostname = null;
return resolveHost(hostname)
.then(resolvedHostname => {
.then((resolvedHostname) => {
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
});
});
@@ -30,7 +30,7 @@ describe('helpers //resolve-host', function () {
it('does nothing', () => {
const hostname = '127.0.0.1';
return resolveHost(hostname)
.then(resolvedHostname => {
.then((resolvedHostname) => {
expect(resolvedHostname).to.equal(hostname);
});
});
@@ -44,7 +44,7 @@ describe('helpers //resolve-host', function () {
return resolveHost(null)
.then(() => expect(1).to.equal(2))
.catch(err => {
.catch((err) => {
expect(err.code).to.equal(420);
});
});

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.createSandbox().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));
@@ -55,7 +64,7 @@ describe('Integration', function () {
return new Promise((resolve, reject) => {
client = new FtpClient();
client.once('ready', () => resolve(client));
client.once('error', err => reject(err));
client.once('error', (err) => reject(err));
client.connect(_.assign({
host: server.url.hostname,
port: server.url.port,
@@ -63,7 +72,7 @@ describe('Integration', function () {
password: 'test'
}, options));
})
.then(instance => {
.then((instance) => {
client = instance;
});
}
@@ -71,8 +80,8 @@ describe('Integration', function () {
function closeClient() {
return new Promise((resolve, reject) => {
client.once('close', () => resolve());
client.once('error', err => reject(err));
client.logout(err => {
client.once('error', (err) => reject(err));
client.logout((err) => {
expect(err).to.be.undefined;
});
});
@@ -83,7 +92,7 @@ describe('Integration', function () {
if (!dirExists) return;
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);
if (!itemExists) return;
@@ -104,7 +113,7 @@ describe('Integration', function () {
after(() => directoryPurge(`${clientDirectory}/${name}/`));
it('STAT', done => {
it('STAT', (done) => {
client.status((err, status) => {
expect(err).to.not.exist;
expect(status).to.equal('Status OK');
@@ -112,7 +121,7 @@ describe('Integration', function () {
});
});
it('SYST', done => {
it('SYST', (done) => {
client.system((err, os) => {
expect(err).to.not.exist;
expect(os).to.equal('UNIX');
@@ -120,7 +129,7 @@ describe('Integration', function () {
});
});
it('CWD ..', done => {
it('CWD ..', (done) => {
client.cwd('..', (err, data) => {
expect(err).to.not.exist;
expect(data).to.equal('/');
@@ -128,7 +137,7 @@ describe('Integration', function () {
});
});
it(`CWD ${name}`, done => {
it(`CWD ${name}`, (done) => {
client.cwd(`${name}`, (err, data) => {
expect(err).to.not.exist;
expect(data).to.equal(`/${name}`);
@@ -136,7 +145,7 @@ describe('Integration', function () {
});
});
it('PWD', done => {
it('PWD', (done) => {
client.pwd((err, data) => {
expect(err).to.not.exist;
expect(data).to.equal(`/${name}`);
@@ -144,7 +153,7 @@ describe('Integration', function () {
});
});
it('LIST .', done => {
it('LIST .', (done) => {
client.list('.', (err, data) => {
expect(err).to.not.exist;
expect(data).to.be.an('array');
@@ -154,7 +163,7 @@ describe('Integration', function () {
});
});
it('LIST fake.txt', done => {
it('LIST fake.txt', (done) => {
client.list('fake.txt', (err, data) => {
expect(err).to.not.exist;
expect(data).to.be.an('array');
@@ -164,7 +173,7 @@ describe('Integration', function () {
});
});
it('STOR fail.txt', done => {
it('STOR fail.txt', (done) => {
const buffer = Buffer.from('test text file');
const fsPath = `${clientDirectory}/${name}/fail.txt`;
@@ -172,12 +181,12 @@ describe('Integration', function () {
const stream = fs.createWriteStream(fsPath, {flags: 'w+'});
stream.on('error', () => fs.existsSync(fsPath) && fs.unlinkSync(fsPath));
stream.on('close', () => stream.end());
setTimeout(() => stream.emit('error', new Error('STOR fail test')));
setImmediate(() => stream.emit('error', new Error('STOR fail test')));
return stream;
});
client.put(buffer, 'fail.txt', err => {
setTimeout(() => {
client.put(buffer, 'fail.txt', (err) => {
setImmediate(() => {
const fileExists = fs.existsSync(fsPath);
expect(err).to.exist;
expect(fileExists).to.equal(false);
@@ -186,17 +195,17 @@ describe('Integration', function () {
});
});
it('STOR tést.txt', done => {
it('STOR tést.txt', (done) => {
const buffer = Buffer.from('test text file');
const fsPath = `${clientDirectory}/${name}/tést.txt`;
connection.once('STOR', err => {
connection.once('STOR', (err) => {
expect(err).to.not.exist;
});
client.put(buffer, 'tést.txt', err => {
client.put(buffer, 'tést.txt', (err) => {
expect(err).to.not.exist;
setTimeout(() => {
setImmediate(() => {
expect(fs.existsSync(fsPath)).to.equal(true);
fs.readFile(fsPath, (fserr, data) => {
expect(fserr).to.not.exist;
@@ -207,12 +216,12 @@ describe('Integration', function () {
});
});
it('APPE tést.txt', done => {
it('APPE tést.txt', (done) => {
const buffer = Buffer.from(', awesome!');
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;
setTimeout(() => {
setImmediate(() => {
expect(fs.existsSync(fsPath)).to.equal(true);
fs.readFile(fsPath, (fserr, data) => {
expect(fserr).to.not.exist;
@@ -223,15 +232,15 @@ describe('Integration', function () {
});
});
it('RETR tést.txt', done => {
connection.once('RETR', err => {
it('RETR tést.txt', (done) => {
connection.once('RETR', (err) => {
expect(err).to.not.exist;
});
client.get('tést.txt', (err, stream) => {
expect(err).to.not.exist;
let text = '';
stream.on('data', data => {
stream.on('data', (data) => {
text += data.toString();
});
stream.on('end', () => {
@@ -242,8 +251,8 @@ describe('Integration', function () {
});
});
it('RNFR tést.txt, RNTO awesome.txt', done => {
client.rename('tést.txt', 'awesome.txt', err => {
it('RNFR tést.txt, RNTO awesome.txt', (done) => {
client.rename('tést.txt', 'awesome.txt', (err) => {
expect(err).to.not.exist;
expect(fs.existsSync(`${clientDirectory}/${name}/tést.txt`)).to.equal(false);
expect(fs.existsSync(`${clientDirectory}/${name}/awesome.txt`)).to.equal(true);
@@ -255,7 +264,7 @@ describe('Integration', function () {
});
});
it('SIZE awesome.txt', done => {
it('SIZE awesome.txt', (done) => {
client.size('awesome.txt', (err, size) => {
expect(err).to.not.exist;
expect(size).to.be.a('number');
@@ -263,7 +272,7 @@ describe('Integration', function () {
});
});
it('MDTM awesome.txt', done => {
it('MDTM awesome.txt', (done) => {
client.lastMod('awesome.txt', (err, modTime) => {
expect(err).to.not.exist;
expect(modTime).to.be.instanceOf(Date);
@@ -272,14 +281,14 @@ describe('Integration', function () {
});
});
it.skip('MLSD .', done => {
it.skip('MLSD .', (done) => {
client.mlsd('.', () => {
done();
});
});
it('SITE CHMOD 700 awesome.txt', done => {
client.site('CHMOD 600 awesome.txt', err => {
it('SITE CHMOD 700 awesome.txt', (done) => {
client.site('CHMOD 600 awesome.txt', (err) => {
expect(err).to.not.exist;
fs.stat(`${clientDirectory}/${name}/awesome.txt`, (fserr, stats) => {
expect(fserr).to.not.exist;
@@ -290,27 +299,27 @@ describe('Integration', function () {
});
});
it('DELE awesome.txt', done => {
client.delete('awesome.txt', err => {
it('DELE awesome.txt', (done) => {
client.delete('awesome.txt', (err) => {
expect(err).to.not.exist;
expect(fs.existsSync(`${clientDirectory}/${name}/awesome.txt`)).to.equal(false);
done();
});
});
it('MKD témp', done => {
it('MKD témp', (done) => {
const path = `${clientDirectory}/${name}/témp`;
if (fs.existsSync(path)) {
fs.rmdirSync(path);
}
client.mkdir('témp', err => {
client.mkdir('témp', (err) => {
expect(err).to.not.exist;
expect(fs.existsSync(path)).to.equal(true);
done();
});
});
it('CWD témp', done => {
it('CWD témp', (done) => {
client.cwd('témp', (err, data) => {
expect(err).to.not.exist;
expect(data).to.to.be.a('string');
@@ -318,23 +327,23 @@ describe('Integration', function () {
});
});
it('CDUP', done => {
client.cdup(err => {
it('CDUP', (done) => {
client.cdup((err) => {
expect(err).to.not.exist;
done();
});
});
it('RMD témp', done => {
client.rmdir('témp', err => {
it('RMD témp', (done) => {
client.rmdir('témp', (err) => {
expect(err).to.not.exist;
expect(fs.existsSync(`${clientDirectory}/${name}/témp`)).to.equal(false);
done();
});
});
it('CDUP', done => {
client.cdup(err => {
it('CDUP', (done) => {
client.cdup((err) => {
expect(err).to.not.exist;
done();
});
@@ -353,8 +362,8 @@ describe('Integration', function () {
after(() => closeClient(client));
it('TYPE A', done => {
client.ascii(err => {
it('TYPE A', (done) => {
client.ascii((err) => {
expect(err).to.not.exist;
done();
});
@@ -375,8 +384,8 @@ describe('Integration', function () {
after(() => closeClient(client));
it('TYPE I', done => {
client.binary(err => {
it('TYPE I', (done) => {
client.binary((err) => {
expect(err).to.not.exist;
done();
});
@@ -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,16 @@
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: 'ftp://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'