Compare commits
45 Commits
logger
...
update-pac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a308a33491 | ||
|
|
191ad5507c | ||
|
|
281d147b96 | ||
|
|
52958ffd9f | ||
|
|
3f5d548634 | ||
|
|
ab085a1bca | ||
|
|
a5f26480e5 | ||
|
|
e41b04be46 | ||
|
|
7acf861a4d | ||
|
|
4801ecc0cc | ||
|
|
8e34e4c71a | ||
|
|
0afd578683 | ||
|
|
46b0d52ff2 | ||
|
|
185e473edc | ||
|
|
92a323f3dd | ||
|
|
f67e487306 | ||
|
|
2716123da7 | ||
|
|
ef207f60c1 | ||
|
|
4d8cf42ad0 | ||
|
|
50c6b92d12 | ||
|
|
a2103e5a3c | ||
|
|
2302b749fa | ||
|
|
27b43d702b | ||
|
|
fae003e644 | ||
|
|
a51678ae70 | ||
|
|
bc26886a0d | ||
|
|
c9b4371579 | ||
|
|
95471bdd15 | ||
|
|
5a36a6685d | ||
|
|
90a7419661 | ||
|
|
29cb035f66 | ||
|
|
66fc66ed80 | ||
|
|
c970a42132 | ||
|
|
30ae54a952 | ||
|
|
91be338ebd | ||
|
|
2a5013447c | ||
|
|
1f15af0fb6 | ||
|
|
1cf1f750f4 | ||
|
|
442490d713 | ||
|
|
58b9ba27d9 | ||
|
|
87a2138cb3 | ||
|
|
9fd423c745 | ||
|
|
363839ec8f | ||
|
|
d9fc0c9cac | ||
|
|
b0463d65b6 |
@@ -23,10 +23,10 @@ base-build: &base-build
|
||||
- node_modules
|
||||
- run:
|
||||
name: Lint
|
||||
command: npm run verify:js
|
||||
command: npm run verify -- --silent
|
||||
- run:
|
||||
name: Test
|
||||
command: npm run test:unit:once
|
||||
command: npm run test:once
|
||||
|
||||
jobs:
|
||||
test_node_10:
|
||||
@@ -60,15 +60,10 @@ jobs:
|
||||
- <<: *create-cache-file
|
||||
- restore_cache:
|
||||
<<: *package-json-cache
|
||||
- run:
|
||||
name: Update NPM
|
||||
command: |
|
||||
npm install npm@5
|
||||
npm install semantic-release@11
|
||||
- deploy:
|
||||
name: Semantic Release
|
||||
command: |
|
||||
npm run semantic-release || true
|
||||
npm run semantic-release
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
165
README.md
165
README.md
@@ -14,8 +14,8 @@
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
|
||||
</a>
|
||||
|
||||
<a href="https://circleci.com/gh/trs/ftp-srv">
|
||||
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv.svg?style=for-the-badge" />
|
||||
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
|
||||
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
## Overview
|
||||
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
|
||||
|
||||
You can use `ftp-srv` to traverse the file system on the server, but it's biggest strength comes from it's customizable file system. This allows you to serve a custom, dynamic, or unique file system to users. You can even server a different system depending on the user connecting.
|
||||
|
||||
## Features
|
||||
- Extensible [file systems](#file-system) per connection
|
||||
- Passive and active transfers
|
||||
@@ -51,7 +53,7 @@
|
||||
// Quick start
|
||||
|
||||
const FtpSrv = require('ftp-srv');
|
||||
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876', { options ... });
|
||||
const ftpServer = new FtpSrv({ options ... });
|
||||
|
||||
ftpServer.on('login', (data, resolve, reject) => { ... });
|
||||
...
|
||||
@@ -62,59 +64,72 @@ ftpServer.listen()
|
||||
|
||||
## API
|
||||
|
||||
### `new FtpSrv(url, [{options}])`
|
||||
### `new FtpSrv({options})`
|
||||
#### url
|
||||
[URL string](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) indicating the protocol, hostname, and port to listen on for connections.
|
||||
Supported protocols:
|
||||
- `ftp` Plain FTP
|
||||
- `ftps` Implicit FTP over TLS
|
||||
|
||||
_Note:_ The hostname must be the external IP address to accept external connections. Setting the hostname to `0.0.0.0` will automatically set the external IP.
|
||||
_Note:_ The hostname must be the external IP address to accept external connections. `0.0.0.0` will listen on any available hosts for server and passive connections.
|
||||
__Default:__ `"ftp://127.0.0.1:21"`
|
||||
|
||||
#### options
|
||||
#### `pasv_url`
|
||||
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
|
||||
|
||||
##### `pasv_range`
|
||||
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
|
||||
This range is then queried for an available port to use when required.
|
||||
__Default:__ `22`
|
||||
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
|
||||
__Default:__ `"127.0.0.1"`
|
||||
|
||||
##### `greeting`
|
||||
A human readable array of lines or string to send when a client connects.
|
||||
#### `pasv_min`
|
||||
Tne starting port to accept passive connections.
|
||||
__Default:__ `1024`
|
||||
|
||||
#### `pasv_max`
|
||||
The ending port to accept passive connections.
|
||||
The range is then queried for an available port to use when required.
|
||||
__Default:__ `65535`
|
||||
|
||||
#### `greeting`
|
||||
A human readable array of lines or string to send when a client connects.
|
||||
__Default:__ `null`
|
||||
|
||||
##### `tls`
|
||||
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
|
||||
#### `tls`
|
||||
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
|
||||
__Default:__ `false`
|
||||
|
||||
##### `anonymous`
|
||||
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
|
||||
Can also set as a string which allows users to authenticate using the username provided.
|
||||
The `login` event is then sent with the provided username and `@anonymous` as the password.
|
||||
#### `anonymous`
|
||||
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
|
||||
Can also set as a string which allows users to authenticate using the username provided.
|
||||
The `login` event is then sent with the provided username and `@anonymous` as the password.
|
||||
__Default:__ `false`
|
||||
|
||||
##### `blacklist`
|
||||
Array of commands that are not allowed.
|
||||
Response code `502` is sent to clients sending one of these commands.
|
||||
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
|
||||
#### `blacklist`
|
||||
Array of commands that are not allowed.
|
||||
Response code `502` is sent to clients sending one of these commands.
|
||||
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
|
||||
__Default:__ `[]`
|
||||
|
||||
##### `whitelist`
|
||||
Array of commands that are only allowed.
|
||||
Response code `502` is sent to clients sending any other command.
|
||||
#### `whitelist`
|
||||
Array of commands that are only allowed.
|
||||
Response code `502` is sent to clients sending any other command.
|
||||
__Default:__ `[]`
|
||||
|
||||
##### `file_format`
|
||||
Sets the format to use for file stat queries such as `LIST`.
|
||||
__Default:__ `"ls"`
|
||||
#### `file_format`
|
||||
Sets the format to use for file stat queries such as `LIST`.
|
||||
__Default:__ `"ls"`
|
||||
__Allowable values:__
|
||||
- `ls` [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
|
||||
- `ep` [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
|
||||
- `function () {}` A custom function returning a format or promise for one.
|
||||
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
|
||||
|
||||
##### `log`
|
||||
A [signale logger](https://github.com/klauscfhq/signale) instance. Created by default.
|
||||
#### `log`
|
||||
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
|
||||
|
||||
Piping the output into bunyan will format logs nicely, eg:
|
||||
```
|
||||
$ node ./test/start.js | npx bunyan
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -171,15 +186,15 @@ The `FtpSrv` class extends the [node net.Server](https://nodejs.org/api/net.html
|
||||
|
||||
### `login`
|
||||
```js
|
||||
on('login', ({connection, username, password}, resolve, reject) => { ... });
|
||||
ftpServer.on('login', ({connection, username, password}, resolve, reject) => { ... });
|
||||
```
|
||||
|
||||
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
|
||||
|
||||
`connection` [client class object](src/connection.js)
|
||||
`username` string of username from `USER` command
|
||||
`password` string of password from `PASS` command
|
||||
`resolve` takes an object of arguments:
|
||||
`connection` [client class object](src/connection.js)
|
||||
`username` string of username from `USER` command
|
||||
`password` string of password from `PASS` command
|
||||
`resolve` takes an object of arguments:
|
||||
- `fs`
|
||||
- Set a custom file system class for this connection to use.
|
||||
- See [File System](#file-system) for implementation details.
|
||||
@@ -198,45 +213,45 @@ Occurs when a client is attempting to login. Here you can resolve the login requ
|
||||
|
||||
### `client-error`
|
||||
```js
|
||||
on('client-error', ({connection, context, error}) => { ... });
|
||||
ftpServer.on('client-error', ({connection, context, error}) => { ... });
|
||||
```
|
||||
|
||||
Occurs when an error arises in the client connection.
|
||||
|
||||
`connection` [client class object](src/connection.js)
|
||||
`context` string of where the error occurred
|
||||
`connection` [client class object](src/connection.js)
|
||||
`context` string of where the error occurred
|
||||
`error` error object
|
||||
|
||||
### `RETR`
|
||||
```js
|
||||
on('RETR', (error, filePath) => { ... });
|
||||
connection.on('RETR', (error, filePath) => { ... });
|
||||
```
|
||||
|
||||
Occurs when a file is downloaded.
|
||||
|
||||
`error` if successful, will be `null`
|
||||
`error` if successful, will be `null`
|
||||
`filePath` location to which file was downloaded
|
||||
|
||||
### `STOR`
|
||||
```js
|
||||
on('STOR', (error, fileName) => { ... });
|
||||
connection.on('STOR', (error, fileName) => { ... });
|
||||
```
|
||||
|
||||
Occurs when a file is uploaded.
|
||||
|
||||
`error` if successful, will be `null`
|
||||
`fileName` name of the file that was downloaded
|
||||
`error` if successful, will be `null`
|
||||
`fileName` name of the file that was uploaded
|
||||
|
||||
## Supported Commands
|
||||
|
||||
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
|
||||
|
||||
## File System
|
||||
The default [file system](src/fs.js) can be overwritten to use your own implementation.
|
||||
This can allow for virtual file systems, and more.
|
||||
Each connection can set it's own file system based on the user.
|
||||
The default [file system](src/fs.js) can be overwritten to use your own implementation.
|
||||
This can allow for virtual file systems, and more.
|
||||
Each connection can set it's own file system based on the user.
|
||||
|
||||
The default file system is exported and can be extended as needed:
|
||||
The default file system is exported and can be extended as needed:
|
||||
```js
|
||||
const {FtpSrv, FileSystem} = require('ftp-srv');
|
||||
|
||||
@@ -254,53 +269,53 @@ class MyFileSystem extends FileSystem {
|
||||
Custom file systems can implement the following variables depending on the developers needs:
|
||||
|
||||
### Methods
|
||||
#### [`currentDirectory()`](src/fs.js#L29)
|
||||
Returns a string of the current working directory
|
||||
#### [`currentDirectory()`](src/fs.js#L40)
|
||||
Returns a string of the current working directory
|
||||
__Used in:__ `PWD`
|
||||
|
||||
#### [`get(fileName)`](src/fs.js#L33)
|
||||
Returns a file stat object of file or directory
|
||||
#### [`get(fileName)`](src/fs.js#L44)
|
||||
Returns a file stat object of file or directory
|
||||
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
|
||||
|
||||
#### [`list(path)`](src/fs.js#L39)
|
||||
Returns array of file and directory stat objects
|
||||
#### [`list(path)`](src/fs.js#L50)
|
||||
Returns array of file and directory stat objects
|
||||
__Used in:__ `LIST`, `NLST`, `STAT`
|
||||
|
||||
#### [`chdir(path)`](src/fs.js#L56)
|
||||
Returns new directory relative to current directory
|
||||
#### [`chdir(path)`](src/fs.js#L67)
|
||||
Returns new directory relative to current directory
|
||||
__Used in:__ `CWD`, `CDUP`
|
||||
|
||||
#### [`mkdir(path)`](src/fs.js#L96)
|
||||
Returns a path to a newly created directory
|
||||
#### [`mkdir(path)`](src/fs.js#L114)
|
||||
Returns a path to a newly created directory
|
||||
__Used in:__ `MKD`
|
||||
|
||||
#### [`write(fileName, {append, start})`](src/fs.js#L68)
|
||||
Returns a writable stream
|
||||
Options:
|
||||
`append` if true, append to existing file
|
||||
`start` if set, specifies the byte offset to write to
|
||||
#### [`write(fileName, {append, start})`](src/fs.js#L79)
|
||||
Returns a writable stream
|
||||
Options:
|
||||
`append` if true, append to existing file
|
||||
`start` if set, specifies the byte offset to write to
|
||||
__Used in:__ `STOR`, `APPE`
|
||||
|
||||
#### [`read(fileName, {start})`](src/fs.js#L75)
|
||||
Returns a readable stream
|
||||
Options:
|
||||
`start` if set, specifies the byte offset to read from
|
||||
#### [`read(fileName, {start})`](src/fs.js#L90)
|
||||
Returns a readable stream
|
||||
Options:
|
||||
`start` if set, specifies the byte offset to read from
|
||||
__Used in:__ `RETR`
|
||||
|
||||
#### [`delete(path)`](src/fs.js#L87)
|
||||
Delete a file or directory
|
||||
#### [`delete(path)`](src/fs.js#L105)
|
||||
Delete a file or directory
|
||||
__Used in:__ `DELE`
|
||||
|
||||
#### [`rename(from, to)`](src/fs.js#L102)
|
||||
Renames a file or directory
|
||||
#### [`rename(from, to)`](src/fs.js#L120)
|
||||
Renames a file or directory
|
||||
__Used in:__ `RNFR`, `RNTO`
|
||||
|
||||
#### [`chmod(path)`](src/fs.js#L108)
|
||||
Modifies a file or directory's permissions
|
||||
#### [`chmod(path)`](src/fs.js#L126)
|
||||
Modifies a file or directory's permissions
|
||||
__Used in:__ `SITE CHMOD`
|
||||
|
||||
#### [`getUniqueName()`](src/fs.js#L113)
|
||||
Returns a unique file name to write to
|
||||
#### [`getUniqueName()`](src/fs.js#L131)
|
||||
Returns a unique file name to write to
|
||||
__Used in:__ `STOU`
|
||||
|
||||
<!--[RM_CONTRIBUTING]-->
|
||||
@@ -319,6 +334,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
- [pkeuter](https://github.com/pkeuter)
|
||||
- [TimLuq](https://github.com/TimLuq)
|
||||
- [edin-mg](https://github.com/edin-m)
|
||||
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
|
||||
- [Johnnyrook777](https://github.com/Johnnyrook777)
|
||||
|
||||
<!--[RM_LICENSE]-->
|
||||
## License
|
||||
|
||||
40
bin/index.js
40
bin/index.js
@@ -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
|
||||
});
|
||||
|
||||
44
changelog/v2_to_v3_migation.md
Normal file
44
changelog/v2_to_v3_migation.md
Normal 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`
|
||||
|
||||
----
|
||||
@@ -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
19
ftp-srv.d.ts
vendored
@@ -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};
|
||||
|
||||
@@ -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
12662
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@@ -28,17 +28,12 @@
|
||||
"pre-release": "npm run verify",
|
||||
"commitmsg": "cz-customizable-ghooks",
|
||||
"dev": "cross-env NODE_ENV=development npm run verify:watch",
|
||||
"prepush": "npm-run-all verify test:unit:once --silent",
|
||||
"prepush": "npm run verify && npm run test:once --silent",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "npm run dev",
|
||||
"test": "npm run test:unit",
|
||||
"test:unit": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
|
||||
"test:unit:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
|
||||
"verify": "npm run verify:js --silent",
|
||||
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
|
||||
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
|
||||
"verify:js:watch": "chokidar 'src/**/*.js' 'test/**/*.js' 'config/**/*.js' -c 'npm run verify:js:fix' --initial --silent",
|
||||
"verify:watch": "npm run verify:js:watch --silent"
|
||||
"test": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
|
||||
"test:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
|
||||
"verify": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\""
|
||||
},
|
||||
"release": {
|
||||
"verifyConditions": "condition-circle"
|
||||
@@ -53,36 +48,33 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"ip": "^1.1.5",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.1",
|
||||
"signale": "^1.0.1",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.0.0"
|
||||
"moment": "^2.22.2",
|
||||
"uuid": "^3.3.2",
|
||||
"yargs": "^12.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@icetee/ftp": "^1.0.2",
|
||||
"chai": "^4.0.2",
|
||||
"chokidar-cli": "1.2.0",
|
||||
"condition-circle": "^1.6.0",
|
||||
"cross-env": "3.1.4",
|
||||
"@icetee/ftp": "^1.0.3",
|
||||
"chai": "^4.1.2",
|
||||
"condition-circle": "^2.0.1",
|
||||
"cross-env": "5.2.0",
|
||||
"cz-customizable": "5.2.0",
|
||||
"cz-customizable-ghooks": "1.5.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"eslint": "4.5.0",
|
||||
"eslint-config-google": "0.8.0",
|
||||
"eslint-friendly-formatter": "3.0.0",
|
||||
"eslint-plugin-mocha": "^4.11.0",
|
||||
"eslint-plugin-node": "5.1.1",
|
||||
"husky": "0.13.3",
|
||||
"eslint": "5.3.0",
|
||||
"eslint-config-google": "0.9.1",
|
||||
"eslint-friendly-formatter": "4.0.1",
|
||||
"eslint-plugin-mocha": "^5.1.0",
|
||||
"eslint-plugin-node": "7.0.1",
|
||||
"husky": "0.14.3",
|
||||
"istanbul": "0.4.5",
|
||||
"mocha": "3.5.0",
|
||||
"mocha-junit-reporter": "1.13.0",
|
||||
"mocha-multi-reporters": "1.1.5",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"rimraf": "2.6.1",
|
||||
"semantic-release": "^11.0.2",
|
||||
"sinon": "^2.3.5"
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-junit-reporter": "1.18.0",
|
||||
"mocha-multi-reporters": "1.1.7",
|
||||
"rimraf": "2.6.2",
|
||||
"semantic-release": "^15.9.8",
|
||||
"sinon": "^6.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.x",
|
||||
|
||||
@@ -3,25 +3,30 @@ const Promise = require('bluebird');
|
||||
|
||||
const REGISTRY = require('./registry');
|
||||
|
||||
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
|
||||
|
||||
class FtpCommands {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
this.previousCommand = {};
|
||||
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd));
|
||||
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
|
||||
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd));
|
||||
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd));
|
||||
}
|
||||
|
||||
parse(message) {
|
||||
const strippedMessage = message.replace(/"/g, '');
|
||||
const [directive, ...args] = strippedMessage.split(' ');
|
||||
let [directive, ...args] = strippedMessage.split(' ');
|
||||
directive = _.chain(directive).trim().toUpper().value();
|
||||
|
||||
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
|
||||
const params = args.reduce(({arg, flags}, param) => {
|
||||
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
|
||||
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
|
||||
else arg.push(param);
|
||||
return {arg, flags};
|
||||
}, {arg: [], flags: []});
|
||||
|
||||
const command = {
|
||||
directive: _.chain(directive).trim().toUpper().value(),
|
||||
directive,
|
||||
arg: params.arg.length ? params.arg.join(' ') : null,
|
||||
flags: params.flags,
|
||||
raw: message
|
||||
@@ -36,16 +41,19 @@ class FtpCommands {
|
||||
const logCommand = _.clone(command);
|
||||
if (logCommand.directive === 'PASS') logCommand.arg = '********';
|
||||
|
||||
const log = this.connection.log.child({directive: command.directive});
|
||||
log.trace({command: logCommand}, 'Handle command');
|
||||
|
||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
||||
return this.connection.reply(402, 'Command not allowed');
|
||||
}
|
||||
|
||||
if (_.includes(this.blacklist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command on blacklist');
|
||||
return this.connection.reply(502, 'Command blacklisted');
|
||||
}
|
||||
|
||||
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command not on whitelist');
|
||||
return this.connection.reply(502, 'Command not whitelisted');
|
||||
}
|
||||
|
||||
const commandRegister = REGISTRY[command.directive];
|
||||
@@ -58,7 +66,8 @@ class FtpCommands {
|
||||
return this.connection.reply(502, 'Handler not set on command');
|
||||
}
|
||||
|
||||
return Promise.try(() => commandRegister.handler.call(this, this.connection, command, this.previousCommand))
|
||||
const handler = commandRegister.handler.bind(this.connection);
|
||||
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
|
||||
.finally(() => {
|
||||
this.previousCommand = _.clone(command);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
module.exports = {
|
||||
directive: 'ABOR',
|
||||
handler: function (connection) {
|
||||
return connection.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
return connection.reply(426, {socket})
|
||||
.then(() => connection.connector.end())
|
||||
.then(() => connection.reply(226));
|
||||
handler: function () {
|
||||
return this.connector.waitForConnection()
|
||||
.then((socket) => {
|
||||
return this.reply(426, {socket})
|
||||
.then(() => this.reply(226));
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(225);
|
||||
});
|
||||
.catch(() => this.reply(225))
|
||||
.finally(() => this.connector.end());
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Abort an active file transfer'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'ALLO',
|
||||
handler: function (connection) {
|
||||
return connection.reply(202);
|
||||
handler: function () {
|
||||
return this.reply(202);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Allocate sufficient disk space to receive a file',
|
||||
|
||||
@@ -2,8 +2,8 @@ const stor = require('./stor').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'APPE',
|
||||
handler: function () {
|
||||
return stor.call(this, ...arguments);
|
||||
handler: function (args) {
|
||||
return stor.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Append to a file'
|
||||
|
||||
@@ -3,12 +3,12 @@ const tls = require('tls');
|
||||
|
||||
module.exports = {
|
||||
directive: 'AUTH',
|
||||
handler: function (connection, command) {
|
||||
handler: function ({command} = {}) {
|
||||
const method = _.upperCase(command.arg);
|
||||
|
||||
switch (method) {
|
||||
case 'TLS': return handleTLS.call(this, connection);
|
||||
default: return connection.reply(504);
|
||||
case 'TLS': return handleTLS.call(this);
|
||||
default: return this.reply(504);
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} <type>',
|
||||
@@ -19,24 +19,24 @@ module.exports = {
|
||||
}
|
||||
};
|
||||
|
||||
function handleTLS(connection) {
|
||||
if (!connection.server._tls) return connection.reply(502);
|
||||
if (connection.secure) return connection.reply(202);
|
||||
function handleTLS() {
|
||||
if (!this.server.options.tls) return this.reply(502);
|
||||
if (this.secure) return this.reply(202);
|
||||
|
||||
return connection.reply(234)
|
||||
return this.reply(234)
|
||||
.then(() => {
|
||||
const secureContext = tls.createSecureContext(connection.server._tls);
|
||||
const secureSocket = new tls.TLSSocket(connection.commandSocket, {
|
||||
const secureContext = tls.createSecureContext(this.server.options.tls);
|
||||
const secureSocket = new tls.TLSSocket(this.commandSocket, {
|
||||
isServer: true,
|
||||
secureContext
|
||||
});
|
||||
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
|
||||
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach((event) => {
|
||||
function forwardEvent() {
|
||||
connection.emit.apply(this, arguments);
|
||||
this.emit.apply(this, arguments);
|
||||
}
|
||||
secureSocket.on(event, forwardEvent.bind(connection.commandSocket, event));
|
||||
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
|
||||
});
|
||||
connection.commandSocket = secureSocket;
|
||||
connection.secure = true;
|
||||
this.commandSocket = secureSocket;
|
||||
this.secure = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ const cwd = require('./cwd').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: ['CDUP', 'XCUP'],
|
||||
handler: function (connection, command, ...args) {
|
||||
command.arg = '..';
|
||||
return cwd.call(this, connection, command, ...args);
|
||||
handler: function (args) {
|
||||
args.command.arg = '..';
|
||||
return cwd.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Change to Parent Directory'
|
||||
|
||||
@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['CWD', 'XCWD'],
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.chdir) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(connection.fs.chdir(command.arg))
|
||||
.then(cwd => {
|
||||
return Promise.try(() => this.fs.chdir(command.arg))
|
||||
.then((cwd) => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return connection.reply(250, path);
|
||||
return this.reply(250, path);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -2,17 +2,17 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'DELE',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.delete) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(connection.fs.delete(command.arg))
|
||||
return Promise.try(() => this.fs.delete(command.arg))
|
||||
.then(() => {
|
||||
return connection.reply(250);
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -8,14 +8,14 @@ const FAMILY = {
|
||||
|
||||
module.exports = {
|
||||
directive: 'EPRT',
|
||||
handler: function (connection, command) {
|
||||
handler: function ({command} = {}) {
|
||||
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
|
||||
const family = FAMILY[protocol];
|
||||
if (!family) return connection.reply(504, 'Unknown network protocol');
|
||||
if (!family) return this.reply(504, 'Unknown network protocol');
|
||||
|
||||
connection.connector = new ActiveConnector(connection);
|
||||
return connection.connector.setupConnection(ip, port, family)
|
||||
.then(() => connection.reply(200));
|
||||
this.connector = new ActiveConnector(this);
|
||||
return this.connector.setupConnection(ip, port, family)
|
||||
.then(() => this.reply(200));
|
||||
},
|
||||
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
|
||||
@@ -2,13 +2,13 @@ const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'EPSV',
|
||||
handler: function (connection) {
|
||||
connection.connector = new PassiveConnector(connection);
|
||||
return connection.connector.setupServer()
|
||||
.then(server => {
|
||||
handler: function () {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then((server) => {
|
||||
const {port} = server.address();
|
||||
|
||||
return connection.reply(229, `EPSV OK (|||${port}|)`);
|
||||
return this.reply(229, `EPSV OK (|||${port}|)`);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [<protocol>]',
|
||||
|
||||
@@ -2,7 +2,7 @@ const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'FEAT',
|
||||
handler: function (connection) {
|
||||
handler: function () {
|
||||
const registry = require('../registry');
|
||||
const features = Object.keys(registry)
|
||||
.reduce((feats, cmd) => {
|
||||
@@ -11,13 +11,13 @@ module.exports = {
|
||||
return feats;
|
||||
}, ['UTF8'])
|
||||
.sort()
|
||||
.map(feat => ({
|
||||
.map((feat) => ({
|
||||
message: ` ${feat}`,
|
||||
raw: true
|
||||
}));
|
||||
return features.length
|
||||
? connection.reply(211, 'Extensions supported', ...features, 'End')
|
||||
: connection.reply(211, 'No features');
|
||||
? this.reply(211, 'Extensions supported', ...features, 'End')
|
||||
: this.reply(211, 'No features');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Get the feature list implemented by the server',
|
||||
|
||||
@@ -2,18 +2,18 @@ const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'HELP',
|
||||
handler: function (connection, command) {
|
||||
handler: function ({command} = {}) {
|
||||
const registry = require('../registry');
|
||||
const directive = _.upperCase(command.arg);
|
||||
if (directive) {
|
||||
if (!registry.hasOwnProperty(directive)) return connection.reply(502, `Unknown command ${directive}.`);
|
||||
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
|
||||
|
||||
const {syntax, description} = registry[directive];
|
||||
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
|
||||
return connection.reply(214, ...reply);
|
||||
return this.reply(214, ...reply);
|
||||
} else {
|
||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
|
||||
return connection.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map((chunk) => chunk.join('\t'));
|
||||
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [<command>]',
|
||||
|
||||
@@ -6,49 +6,49 @@ const getFileStat = require('../../helpers/file-stat');
|
||||
// http://cr.yp.to/ftp/list/eplf.html
|
||||
module.exports = {
|
||||
directive: 'LIST',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return this.reply(550, 'File system not instantiated');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const simple = command.directive === 'NLST';
|
||||
|
||||
const path = command.arg || '.';
|
||||
return connection.connector.waitForConnection()
|
||||
.tap(() => connection.commandSocket.pause())
|
||||
.then(() => Promise.resolve(connection.fs.get(path)))
|
||||
.then(stat => stat.isDirectory() ? Promise.resolve(connection.fs.list(path)) : [stat])
|
||||
.then(files => {
|
||||
const getFileMessage = file => {
|
||||
return this.connector.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.try(() => this.fs.get(path)))
|
||||
.then((stat) => stat.isDirectory() ? Promise.try(() => this.fs.list(path)) : [stat])
|
||||
.then((files) => {
|
||||
const getFileMessage = (file) => {
|
||||
if (simple) return file.name;
|
||||
return getFileStat(file, _.get(connection, 'server.options.file_format', 'ls'));
|
||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
};
|
||||
|
||||
const fileList = files.map(file => {
|
||||
return Promise.try(() => files.map((file) => {
|
||||
const message = getFileMessage(file);
|
||||
return {
|
||||
raw: true,
|
||||
message,
|
||||
socket: connection.connector.socket
|
||||
socket: this.connector.socket
|
||||
};
|
||||
});
|
||||
return connection.reply(150)
|
||||
.then(() => {
|
||||
if (fileList.length) return connection.reply({}, ...fileList);
|
||||
});
|
||||
}));
|
||||
})
|
||||
.then(() => connection.reply(226))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(425, 'No connection established');
|
||||
.tap(() => this.reply(150))
|
||||
.then((fileList) => {
|
||||
if (fileList.length) return this.reply({}, ...fileList);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(451, err.message || 'No directory');
|
||||
.tap(() => this.reply(226))
|
||||
.catch(Promise.TimeoutError, (err) => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(451, err.message || 'No directory');
|
||||
})
|
||||
.finally(() => {
|
||||
connection.connector.end();
|
||||
connection.commandSocket.resume();
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
|
||||
@@ -3,18 +3,18 @@ const moment = require('moment');
|
||||
|
||||
module.exports = {
|
||||
directive: 'MDTM',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(connection.fs.get(command.arg))
|
||||
.then(fileStat => {
|
||||
return Promise.try(() => this.fs.get(command.arg))
|
||||
.then((fileStat) => {
|
||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
||||
return connection.reply(213, modificationTime);
|
||||
return this.reply(213, modificationTime);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['MKD', 'XMKD'],
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.mkdir) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(connection.fs.mkdir(command.arg))
|
||||
.then(dir => {
|
||||
return Promise.try(() => this.fs.mkdir(command.arg))
|
||||
.then((dir) => {
|
||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||
return connection.reply(257, path);
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'MODE',
|
||||
handler: function (connection, command) {
|
||||
return connection.reply(/^S$/i.test(command.arg) ? 200 : 504);
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(/^S$/i.test(command.arg) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} <mode>',
|
||||
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
|
||||
|
||||
@@ -2,8 +2,8 @@ const list = require('./list').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'NLST',
|
||||
handler: function () {
|
||||
return list.call(this, ...arguments);
|
||||
handler: function (args) {
|
||||
return list.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
description: 'Returns a list of file names in a specified directory'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'NOOP',
|
||||
handler: function (connection) {
|
||||
return connection.reply(200);
|
||||
handler: function () {
|
||||
return this.reply(200);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'No operation',
|
||||
|
||||
@@ -7,14 +7,14 @@ const OPTIONS = {
|
||||
|
||||
module.exports = {
|
||||
directive: 'OPTS',
|
||||
handler: function (connection, command) {
|
||||
if (!_.has(command, 'arg')) return connection.reply(501);
|
||||
handler: function ({command} = {}) {
|
||||
if (!_.has(command, 'arg')) return this.reply(501);
|
||||
|
||||
const [_option, ...args] = command.arg.split(' ');
|
||||
const option = _.toUpper(_option);
|
||||
|
||||
if (!OPTIONS.hasOwnProperty(option)) return connection.reply(500);
|
||||
return OPTIONS[option].call(this, ...args);
|
||||
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
|
||||
return OPTIONS[option].call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Select options for a feature'
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
module.exports = {
|
||||
directive: 'PASS',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.username) return this.reply(503);
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.username) return this.reply(503);
|
||||
if (this.authenticated) return this.reply(202);
|
||||
|
||||
// 332 : require account name (ACCT)
|
||||
|
||||
const password = command.arg;
|
||||
if (!password) return this.reply(501, 'Must provide password');
|
||||
return connection.login(connection.username, password)
|
||||
return this.login(this.username, password)
|
||||
.then(() => {
|
||||
return connection.reply(230);
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(530, err.message || 'Authentication failed');
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <password>',
|
||||
|
||||
@@ -2,17 +2,17 @@ const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASV',
|
||||
handler: function (connection) {
|
||||
connection.connector = new PassiveConnector(connection);
|
||||
return connection.connector.setupServer()
|
||||
.then(server => {
|
||||
const address = connection.server.url.hostname;
|
||||
handler: function () {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then((server) => {
|
||||
const address = this.server.options.pasv_url;
|
||||
const {port} = server.address();
|
||||
const host = address.replace(/\./g, ',');
|
||||
const portByte1 = port / 256 | 0;
|
||||
const portByte2 = port % 256;
|
||||
|
||||
return connection.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module.exports = {
|
||||
directive: 'PBSZ',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.secure) return connection.reply(202, 'Not suppored');
|
||||
connection.bufferSize = parseInt(command.arg, 10);
|
||||
return connection.reply(200, connection.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
||||
handler: function ({command} = {}) {
|
||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
||||
this.bufferSize = parseInt(command.arg, 10);
|
||||
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Protection Buffer Size',
|
||||
|
||||
@@ -3,18 +3,18 @@ const ActiveConnector = require('../../connector/active');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PORT',
|
||||
handler: function (connection, command) {
|
||||
connection.connector = new ActiveConnector(connection);
|
||||
handler: function ({command} = {}) {
|
||||
this.connector = new ActiveConnector(this);
|
||||
|
||||
const rawConnection = _.get(command, 'arg', '').split(',');
|
||||
if (rawConnection.length !== 6) return connection.reply(425);
|
||||
if (rawConnection.length !== 6) return this.reply(425);
|
||||
|
||||
const ip = rawConnection.slice(0, 4).join('.');
|
||||
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
|
||||
const portBytes = rawConnection.slice(4).map((p) => parseInt(p));
|
||||
const port = portBytes[0] * 256 + portBytes[1];
|
||||
|
||||
return connection.connector.setupConnection(ip, port)
|
||||
.then(() => connection.reply(200));
|
||||
return this.connector.setupConnection(ip, port)
|
||||
.then(() => this.reply(200));
|
||||
},
|
||||
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
|
||||
@@ -2,16 +2,16 @@ const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PROT',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.secure) return connection.reply(202, 'Not suppored');
|
||||
if (!connection.bufferSize && typeof connection.bufferSize !== 'number') return connection.reply(503);
|
||||
handler: function ({command} = {}) {
|
||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
||||
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
|
||||
|
||||
switch (_.toUpper(command.arg)) {
|
||||
case 'P': return connection.reply(200, 'OK');
|
||||
case 'P': return this.reply(200, 'OK');
|
||||
case 'C':
|
||||
case 'S':
|
||||
case 'E': return connection.reply(536, 'Not supported');
|
||||
default: return connection.reply(504);
|
||||
case 'E': return this.reply(536, 'Not supported');
|
||||
default: return this.reply(504);
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -3,18 +3,18 @@ const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['PWD', 'XPWD'],
|
||||
handler: function (connection) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.currentDirectory) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(connection.fs.currentDirectory())
|
||||
.then(cwd => {
|
||||
return Promise.try(() => this.fs.currentDirectory())
|
||||
.then((cwd) => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return connection.reply(257, path);
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'QUIT',
|
||||
handler: function (connection) {
|
||||
return connection.close(221, 'Client called QUIT');
|
||||
handler: function () {
|
||||
return this.close(221, 'Client called QUIT');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Disconnect',
|
||||
|
||||
@@ -2,14 +2,14 @@ const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'REST',
|
||||
handler: function (connection, command) {
|
||||
handler: function ({command} = {}) {
|
||||
const arg = _.get(command, 'arg');
|
||||
const byteCount = parseInt(arg, 10);
|
||||
|
||||
if (isNaN(byteCount) || byteCount < 0) return connection.reply(501, 'Byte count must be 0 or greater');
|
||||
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
|
||||
|
||||
connection.restByteCount = byteCount;
|
||||
return connection.reply(350, `Restarting next transfer at ${byteCount}`);
|
||||
this.restByteCount = byteCount;
|
||||
return this.reply(350, `Restarting next transfer at ${byteCount}`);
|
||||
},
|
||||
syntax: '{{cmd}} <byte-count>',
|
||||
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
|
||||
|
||||
@@ -2,54 +2,61 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RETR',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.read) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const filePath = command.arg;
|
||||
|
||||
return connection.connector.waitForConnection()
|
||||
.tap(() => connection.commandSocket.pause())
|
||||
.then(() => Promise.resolve(connection.fs.read(filePath, {start: connection.restByteCount})))
|
||||
.then(stream => {
|
||||
const destroyConnection = (conn, reject) => err => {
|
||||
if (conn) conn.destroy(err);
|
||||
return this.connector.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.try(() => this.fs.read(filePath, {start: this.restByteCount})))
|
||||
.then((fsResponse) => {
|
||||
let {stream, clientPath} = fsResponse;
|
||||
if (!stream && !clientPath) {
|
||||
stream = fsResponse;
|
||||
clientPath = filePath;
|
||||
}
|
||||
const serverPath = stream.path || filePath;
|
||||
|
||||
const destroyConnection = (connection, reject) => (err) => {
|
||||
if (connection) connection.destroy(err);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const eventsPromise = new Promise((resolve, reject) => {
|
||||
stream.on('data', data => {
|
||||
stream.on('data', (data) => {
|
||||
if (stream) stream.pause();
|
||||
if (connection.connector.socket) {
|
||||
connection.connector.socket.write(data, connection.transferType, () => stream && stream.resume());
|
||||
if (this.connector.socket) {
|
||||
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
|
||||
}
|
||||
});
|
||||
stream.once('end', () => resolve());
|
||||
stream.once('error', destroyConnection(connection.connector.socket, reject));
|
||||
stream.once('error', destroyConnection(this.connector.socket, reject));
|
||||
|
||||
connection.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
this.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
});
|
||||
|
||||
connection.restByteCount = 0;
|
||||
this.restByteCount = 0;
|
||||
|
||||
return connection.reply(150).then(() => stream.resume() && connection.connector.socket.resume())
|
||||
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
|
||||
.then(() => eventsPromise)
|
||||
.tap(() => connection.emit('RETR', null, filePath))
|
||||
.tap(() => this.emit('RETR', null, serverPath))
|
||||
.then(() => this.reply(226, clientPath))
|
||||
.finally(() => stream.destroy && stream.destroy());
|
||||
})
|
||||
.then(() => connection.reply(226))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(425, 'No connection established');
|
||||
.catch(Promise.TimeoutError, (err) => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
connection.emit('RETR', err);
|
||||
return connection.reply(551, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
this.emit('RETR', err);
|
||||
return this.reply(551, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
connection.connector.end();
|
||||
connection.commandSocket.resume();
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -2,8 +2,8 @@ const {handler: dele} = require('./dele');
|
||||
|
||||
module.exports = {
|
||||
directive: ['RMD', 'XRMD'],
|
||||
handler: function (...args) {
|
||||
return dele.call(this, ...args);
|
||||
handler: function (args) {
|
||||
return dele.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Remove a directory'
|
||||
|
||||
@@ -2,19 +2,19 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNFR',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const fileName = command.arg;
|
||||
return Promise.resolve(connection.fs.get(fileName))
|
||||
return Promise.try(() => this.fs.get(fileName))
|
||||
.then(() => {
|
||||
connection.renameFrom = fileName;
|
||||
return connection.reply(350);
|
||||
this.renameFrom = fileName;
|
||||
return this.reply(350);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <name>',
|
||||
|
||||
@@ -2,25 +2,25 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNTO',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.renameFrom) return connection.reply(503);
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.renameFrom) return this.reply(503);
|
||||
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.rename) return connection.reply(402, 'Not supported by file system');
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const from = connection.renameFrom;
|
||||
const from = this.renameFrom;
|
||||
const to = command.arg;
|
||||
|
||||
return Promise.resolve(connection.fs.rename(from, to))
|
||||
return Promise.try(() => this.fs.rename(from, to))
|
||||
.then(() => {
|
||||
return connection.reply(250);
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
delete connection.renameFrom;
|
||||
delete this.renameFrom;
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <name>',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ module.exports = {
|
||||
handler: function ({log, command} = {}) {
|
||||
const rawSubCommand = _.get(command, 'arg', '');
|
||||
const subCommand = this.commands.parse(rawSubCommand);
|
||||
const subLog = log.scope(subCommand.directive);
|
||||
const subLog = log.child({subverb: subCommand.directive});
|
||||
|
||||
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'SIZE',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(connection.fs.get(command.arg))
|
||||
.then(fileStat => {
|
||||
return connection.reply(213, {message: fileStat.size});
|
||||
return Promise.try(() => this.fs.get(command.arg))
|
||||
.then((fileStat) => {
|
||||
return this.reply(213, {message: fileStat.size});
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -4,39 +4,40 @@ const getFileStat = require('../../helpers/file-stat');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STAT',
|
||||
handler: function (connection, command) {
|
||||
handler: function (args = {}) {
|
||||
const {log, command} = args;
|
||||
const path = _.get(command, 'arg');
|
||||
if (path) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.get) return connection.reply(402, 'Not supported by file system');
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(connection.fs.get(path))
|
||||
.then(stat => {
|
||||
return Promise.try(() => this.fs.get(path))
|
||||
.then((stat) => {
|
||||
if (stat.isDirectory()) {
|
||||
if (!connection.fs.list) return connection.reply(402, 'Not supported by file system');
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(connection.fs.list(path))
|
||||
.then(stats => [213, stats]);
|
||||
return Promise.try(() => this.fs.list(path))
|
||||
.then((stats) => [213, stats]);
|
||||
}
|
||||
return [212, [stat]];
|
||||
})
|
||||
.then(([code, fileStats]) => {
|
||||
return Promise.map(fileStats, file => {
|
||||
const message = getFileStat(file, _.get(connection, 'server.options.file_format', 'ls'));
|
||||
return Promise.map(fileStats, (file) => {
|
||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
return {
|
||||
raw: true,
|
||||
message
|
||||
};
|
||||
})
|
||||
.then(messages => [code, messages]);
|
||||
.then((messages) => [code, messages]);
|
||||
})
|
||||
.then(([code, messages]) => connection.reply(code, 'Status begin', ...messages, 'Status end'))
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(450, err.message);
|
||||
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(450, err.message);
|
||||
});
|
||||
} else {
|
||||
return connection.reply(211, 'Status OK');
|
||||
return this.reply(211, 'Status OK');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
|
||||
@@ -2,62 +2,72 @@ const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOR',
|
||||
handler: function (connection, command) {
|
||||
if (!connection.fs) return connection.reply(550, 'File system not instantiated');
|
||||
if (!connection.fs.write) return connection.reply(402, 'Not supported by file system');
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const append = command.directive === 'APPE';
|
||||
const fileName = command.arg;
|
||||
|
||||
return connection.connector.waitForConnection()
|
||||
.tap(() => connection.commandSocket.pause())
|
||||
.then(() => Promise.resolve(connection.fs.write(fileName, {append, start: connection.restByteCount})))
|
||||
.then(stream => {
|
||||
const destroyConnection = (conn, reject) => err => {
|
||||
if (conn) conn.destroy(err);
|
||||
return this.connector.waitForConnection()
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
|
||||
.then((fsResponse) => {
|
||||
let {stream, clientPath} = fsResponse;
|
||||
if (!stream && !clientPath) {
|
||||
stream = fsResponse;
|
||||
clientPath = fileName;
|
||||
}
|
||||
const serverPath = stream.path || fileName;
|
||||
|
||||
const destroyConnection = (connection, reject) => (err) => {
|
||||
if (connection) {
|
||||
if (connection.writeable) connection.end();
|
||||
connection.destroy(err);
|
||||
}
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const streamPromise = new Promise((resolve, reject) => {
|
||||
stream.once('error', destroyConnection(connection.connector.socket, reject));
|
||||
stream.once('error', destroyConnection(this.connector.socket, reject));
|
||||
stream.once('finish', () => resolve());
|
||||
});
|
||||
|
||||
const socketPromise = new Promise((resolve, reject) => {
|
||||
connection.connector.socket.on('data', data => {
|
||||
if (connection.connector.socket) connection.connector.socket.pause();
|
||||
this.connector.socket.on('data', (data) => {
|
||||
if (this.connector.socket) this.connector.socket.pause();
|
||||
if (stream) {
|
||||
stream.write(data, connection.transferType, () => connection.connector.socket && connection.connector.socket.resume());
|
||||
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
|
||||
}
|
||||
});
|
||||
connection.connector.socket.once('end', () => {
|
||||
this.connector.socket.once('end', () => {
|
||||
if (stream.listenerCount('close')) stream.emit('close');
|
||||
else stream.end();
|
||||
resolve();
|
||||
});
|
||||
connection.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
this.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
});
|
||||
|
||||
connection.restByteCount = 0;
|
||||
this.restByteCount = 0;
|
||||
|
||||
return connection.reply(150).then(() => connection.connector.socket.resume())
|
||||
.then(() => Promise.join(streamPromise, socketPromise))
|
||||
.tap(() => connection.emit('STOR', null, fileName))
|
||||
return this.reply(150).then(() => this.connector.socket.resume())
|
||||
.then(() => Promise.all([streamPromise, socketPromise]))
|
||||
.tap(() => this.emit('STOR', null, serverPath))
|
||||
.then(() => this.reply(226, clientPath))
|
||||
.finally(() => stream.destroy && stream.destroy());
|
||||
})
|
||||
.then(() => connection.reply(226, fileName))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(425, 'No connection established');
|
||||
.catch(Promise.TimeoutError, (err) => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
connection.emit('STOR', err);
|
||||
return connection.reply(550, err.message);
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
this.emit('STOR', err);
|
||||
return this.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
connection.connector.end();
|
||||
connection.commandSocket.resume();
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} <path>',
|
||||
|
||||
@@ -3,19 +3,17 @@ const {handler: stor} = require('./stor');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOU',
|
||||
handler: function (connection, command, ...args) {
|
||||
handler: function (args) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const fileName = command.arg;
|
||||
return Promise.try(() => {
|
||||
return Promise.resolve(this.fs.get(fileName))
|
||||
.then(() => Promise.resolve(this.fs.getUniqueName()))
|
||||
.catch(() => Promise.resolve(fileName));
|
||||
})
|
||||
.then(name => {
|
||||
command.arg = name;
|
||||
return stor.call(this, connection, command, ...args);
|
||||
const fileName = args.command.arg;
|
||||
return Promise.try(() => this.fs.get(fileName))
|
||||
.then(() => Promise.try(() => this.fs.getUniqueName()))
|
||||
.catch(() => fileName)
|
||||
.then((name) => {
|
||||
args.command.arg = name;
|
||||
return stor.call(this, args);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'STRU',
|
||||
handler: function (connection, command) {
|
||||
return connection.reply(/^F$/i.test(command.arg) ? 200 : 504);
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(/^F$/i.test(command.arg) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} <structure>',
|
||||
description: 'Set file transfer structure',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
directive: 'SYST',
|
||||
handler: function (connection) {
|
||||
return connection.reply(215);
|
||||
handler: function () {
|
||||
return this.reply(215);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Return system type',
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module.exports = {
|
||||
directive: 'TYPE',
|
||||
handler: function (connection, command) {
|
||||
handler: function ({command} = {}) {
|
||||
if (/^A[0-9]?$/i.test(command.arg)) {
|
||||
connection.transferType = 'ascii';
|
||||
this.transferType = 'ascii';
|
||||
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
|
||||
connection.transferType = 'binary';
|
||||
this.transferType = 'binary';
|
||||
} else {
|
||||
return connection.reply(501);
|
||||
return this.reply(501);
|
||||
}
|
||||
return connection.reply(200, `Switch to "${connection.transferType}" transfer mode.`);
|
||||
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`);
|
||||
},
|
||||
syntax: '{{cmd}} <mode>',
|
||||
description: 'Set the transfer mode, binary (I) or ascii (A)',
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
module.exports = {
|
||||
directive: 'USER',
|
||||
handler: function (connection, command) {
|
||||
if (connection.username) return connection.reply(530, 'Username already set');
|
||||
if (connection.authenticated) return connection.reply(230);
|
||||
handler: function ({log, command} = {}) {
|
||||
if (this.username) return this.reply(530, 'Username already set');
|
||||
if (this.authenticated) return this.reply(230);
|
||||
|
||||
connection.username = command.arg;
|
||||
if (!connection.username) return connection.reply(501, 'Must provide username');
|
||||
this.username = command.arg;
|
||||
if (!this.username) return this.reply(501, 'Must provide username');
|
||||
|
||||
if (connection.server.options.anonymous === true && connection.username === 'anonymous' ||
|
||||
connection.username === connection.server.options.anonymous) {
|
||||
return connection.login(connection.username, '@anonymous')
|
||||
if (this.server.options.anonymous === true && this.username === 'anonymous' ||
|
||||
this.username === this.server.options.anonymous) {
|
||||
return this.login(this.username, '@anonymous')
|
||||
.then(() => {
|
||||
return connection.reply(230);
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
connection.emit('error', err);
|
||||
return connection.reply(530, err.message || 'Authentication failed');
|
||||
.catch((err) => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
}
|
||||
return connection.reply(331);
|
||||
return this.reply(331);
|
||||
},
|
||||
syntax: '{{cmd}} <username>',
|
||||
description: 'Authentication username',
|
||||
|
||||
@@ -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;
|
||||
}, {});
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ const errors = require('./errors');
|
||||
const DEFAULT_MESSAGE = require('./messages');
|
||||
|
||||
class FtpConnection extends EventEmitter {
|
||||
constructor(server, socket) {
|
||||
constructor(server, options) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.id = uuid.v4();
|
||||
this.commandSocket = socket;
|
||||
this.log = server.log.scope(`client: ${this.ip}`);
|
||||
// this.log = options.log.child({id: this.id, ip: this.ip});
|
||||
this.log = options.log.child({id: this.id, ip: this.ip});
|
||||
this.commands = new Commands(this);
|
||||
this.transferType = 'binary';
|
||||
this.encoding = 'utf8';
|
||||
@@ -26,8 +24,10 @@ class FtpConnection extends EventEmitter {
|
||||
|
||||
this.connector = new BaseConnector(this);
|
||||
|
||||
this.commandSocket.on('error', err => {
|
||||
this.log.scope('error event').error(err);
|
||||
this.commandSocket = options.socket;
|
||||
this.commandSocket.on('error', (err) => {
|
||||
this.log.error(err, 'Client error');
|
||||
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
|
||||
});
|
||||
this.commandSocket.on('data', this._handleData.bind(this));
|
||||
this.commandSocket.on('timeout', () => {});
|
||||
@@ -40,7 +40,8 @@ class FtpConnection extends EventEmitter {
|
||||
|
||||
_handleData(data) {
|
||||
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
|
||||
return Promise.mapSeries(messages, message => this.commands.handle(message));
|
||||
this.log.trace(messages);
|
||||
return Promise.mapSeries(messages, (message) => this.commands.handle(message));
|
||||
}
|
||||
|
||||
get ip() {
|
||||
@@ -67,7 +68,7 @@ class FtpConnection extends EventEmitter {
|
||||
|
||||
close(code = 421, message = 'Closing connection') {
|
||||
return Promise.resolve(code)
|
||||
.then(_code => _code && this.reply(_code, message))
|
||||
.then((_code) => _code && this.reply(_code, message))
|
||||
.then(() => this.commandSocket && this.commandSocket.end());
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ class FtpConnection extends EventEmitter {
|
||||
if (!letters.length) letters = [{}];
|
||||
return Promise.map(letters, (promise, index) => {
|
||||
return Promise.resolve(promise)
|
||||
.then(letter => {
|
||||
.then((letter) => {
|
||||
if (!letter) letter = {};
|
||||
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
||||
|
||||
@@ -103,7 +104,7 @@ class FtpConnection extends EventEmitter {
|
||||
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
|
||||
if (!letter.encoding) letter.encoding = this.encoding;
|
||||
return Promise.resolve(letter.message) // allow passing in a promise as a message
|
||||
.then(message => {
|
||||
.then((message) => {
|
||||
const seperator = !options.hasOwnProperty('eol') ?
|
||||
letters.length - 1 === index ? ' ' : '-' :
|
||||
options.eol ? ' ' : '-';
|
||||
@@ -115,14 +116,13 @@ class FtpConnection extends EventEmitter {
|
||||
});
|
||||
};
|
||||
|
||||
const processLetter = letter => {
|
||||
const log = this.log.scope('reply');
|
||||
const processLetter = (letter) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (letter.socket && letter.socket.writable) {
|
||||
log.debug(letter.message, {port: letter.socket.address().port});
|
||||
letter.socket.write(letter.message + '\r\n', letter.encoding, err => {
|
||||
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
|
||||
letter.socket.write(letter.message + '\r\n', letter.encoding, (err) => {
|
||||
if (err) {
|
||||
log.error(err);
|
||||
this.log.error(err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
@@ -132,10 +132,10 @@ class FtpConnection extends EventEmitter {
|
||||
};
|
||||
|
||||
return satisfyParameters()
|
||||
.then(satisfiedLetters => Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
||||
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
||||
return processLetter(letter, index);
|
||||
}))
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
this.log.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ class Active extends Connector {
|
||||
constructor(connection) {
|
||||
super(connection);
|
||||
this.type = 'active';
|
||||
this.log = connection.log.scope('active');
|
||||
}
|
||||
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
@@ -30,18 +29,12 @@ class Active extends Connector {
|
||||
.then(() => {
|
||||
this.dataSocket = new Socket();
|
||||
this.dataSocket.setEncoding(this.connection.transferType);
|
||||
this.dataSocket.on('error', err => this.connection.emit('error', err));
|
||||
this.dataSocket.on('close', () => {
|
||||
this.log.debug('socket closed');
|
||||
this.end();
|
||||
});
|
||||
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||
this.dataSocket.connect({host, port, family}, () => {
|
||||
this.dataSocket.pause();
|
||||
|
||||
this.log.debug('connection', {port, remoteAddress: this.dataSocket.remoteAddress});
|
||||
|
||||
if (this.connection.secure) {
|
||||
const secureContext = tls.createSecureContext(this.server._tls);
|
||||
const secureContext = tls.createSecureContext(this.server.options.tls);
|
||||
const secureSocket = new tls.TLSSocket(this.dataSocket, {
|
||||
isServer: true,
|
||||
secureContext
|
||||
|
||||
@@ -8,7 +8,10 @@ class Connector {
|
||||
this.dataSocket = null;
|
||||
this.dataServer = null;
|
||||
this.type = false;
|
||||
this.log = connection.log.scope('connector');
|
||||
}
|
||||
|
||||
get log() {
|
||||
return this.connection.log;
|
||||
}
|
||||
|
||||
get socket() {
|
||||
@@ -23,22 +26,28 @@ class Connector {
|
||||
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
||||
}
|
||||
|
||||
end() {
|
||||
const closeDataSocket = new Promise(resolve => {
|
||||
if (this.dataSocket) this.dataSocket.end();
|
||||
else resolve();
|
||||
});
|
||||
const closeDataServer = new Promise(resolve => {
|
||||
if (this.dataServer) this.dataServer.close(() => resolve());
|
||||
else resolve();
|
||||
});
|
||||
|
||||
return Promise.all([closeDataSocket, closeDataServer])
|
||||
.then(() => {
|
||||
closeSocket() {
|
||||
if (this.dataSocket) {
|
||||
const socket = this.dataSocket;
|
||||
this.dataSocket.end(() => socket.destroy());
|
||||
this.dataSocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
closeServer() {
|
||||
if (this.dataServer) {
|
||||
this.dataServer.close();
|
||||
this.dataServer = null;
|
||||
this.type = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
end() {
|
||||
this.closeSocket();
|
||||
this.closeServer();
|
||||
|
||||
this.type = false;
|
||||
this.connection.connector = new Connector(this);
|
||||
}
|
||||
}
|
||||
module.exports = Connector;
|
||||
|
||||
@@ -4,17 +4,15 @@ const ip = require('ip');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const Connector = require('./base');
|
||||
const findPort = require('../helpers/find-port');
|
||||
const errors = require('../errors');
|
||||
|
||||
class Passive extends Connector {
|
||||
constructor(connection) {
|
||||
super(connection);
|
||||
this.type = 'passive';
|
||||
this.log = connection.log.scope('passive');
|
||||
}
|
||||
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
waitForConnection({timeout = 5000, delay = 50} = {}) {
|
||||
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
|
||||
|
||||
const checkSocket = () => {
|
||||
@@ -29,59 +27,54 @@ class Passive extends Connector {
|
||||
}
|
||||
|
||||
setupServer() {
|
||||
const closeExistingServer = () => this.dataServer ?
|
||||
new Promise(resolve => this.dataServer.close(() => resolve())) :
|
||||
Promise.resolve();
|
||||
|
||||
return closeExistingServer()
|
||||
.then(() => this.getPort())
|
||||
.then(port => {
|
||||
const connectionHandler = socket => {
|
||||
this.closeServer();
|
||||
return this.server.getNextPasvPort()
|
||||
.then((port) => {
|
||||
const connectionHandler = (socket) => {
|
||||
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
|
||||
this.log.error('ip address mismatch', {
|
||||
this.log.error({
|
||||
pasv_connection: socket.remoteAddress,
|
||||
cmd_connection: this.connection.commandSocket.remoteAddress
|
||||
});
|
||||
}, 'Connecting addresses do not match');
|
||||
|
||||
socket.destroy();
|
||||
return this.connection.reply(550, 'IP address mismatch')
|
||||
return this.connection.reply(550, 'Remote addresses do not match')
|
||||
.finally(() => this.connection.close());
|
||||
}
|
||||
this.log.debug('connection', {port, remoteAddress: socket.remoteAddress});
|
||||
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
||||
|
||||
if (this.connection.secure) {
|
||||
const secureContext = tls.createSecureContext(this.server._tls);
|
||||
const secureSocket = new tls.TLSSocket(socket, {
|
||||
isServer: true,
|
||||
secureContext
|
||||
});
|
||||
this.dataSocket = secureSocket;
|
||||
} else {
|
||||
this.dataSocket = socket;
|
||||
}
|
||||
this.dataSocket.connected = true;
|
||||
this.dataSocket = socket;
|
||||
this.dataSocket.setEncoding(this.connection.transferType);
|
||||
this.dataSocket.on('error', err => this.connection.emit('error', err));
|
||||
this.dataSocket.on('close', () => {
|
||||
this.log.debug('socket closed');
|
||||
this.end();
|
||||
});
|
||||
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||
|
||||
if (!this.connection.secure) {
|
||||
this.dataSocket.connected = true;
|
||||
}
|
||||
};
|
||||
|
||||
this.dataSocket = null;
|
||||
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
|
||||
|
||||
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
|
||||
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
|
||||
this.dataServer.maxConnections = 1;
|
||||
this.dataServer.on('error', err => this.connection.emit('error', err));
|
||||
this.dataServer.on('close', () => {
|
||||
this.log.debug('server closed');
|
||||
this.dataServer = null;
|
||||
|
||||
this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
|
||||
this.dataServer.once('close', () => {
|
||||
this.log.trace('Passive server closed');
|
||||
this.end();
|
||||
});
|
||||
|
||||
if (this.connection.secure) {
|
||||
this.dataServer.on('secureConnection', (socket) => {
|
||||
socket.connected = true;
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataServer.listen(port, err => {
|
||||
this.dataServer.listen(port, this.server.url.hostname, (err) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
this.log.debug('listening', {port});
|
||||
this.log.debug({port}, 'Passive connection listening');
|
||||
resolve(this.dataServer);
|
||||
}
|
||||
});
|
||||
@@ -89,15 +82,5 @@ class Passive extends Connector {
|
||||
});
|
||||
}
|
||||
|
||||
getPort() {
|
||||
if (this.server.options.pasv_range) {
|
||||
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
|
||||
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
|
||||
[this.server.options.pasv_range];
|
||||
return findPort(min, max);
|
||||
}
|
||||
throw new errors.ConnectorError('Invalid pasv_range');
|
||||
}
|
||||
|
||||
}
|
||||
module.exports = Passive;
|
||||
|
||||
61
src/fs.js
61
src/fs.js
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
68
src/index.js
68
src/index.js
@@ -1,43 +1,46 @@
|
||||
const _ = require('lodash');
|
||||
const Promise = require('bluebird');
|
||||
const nodeUrl = require('url');
|
||||
const {Signale} = require('signale');
|
||||
const buyan = require('bunyan');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const fs = require('fs');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const Connection = require('./connection');
|
||||
const resolveHost = require('./helpers/resolve-host');
|
||||
const {getNextPortFactory} = require('./helpers/find-port');
|
||||
|
||||
class FtpServer extends EventEmitter {
|
||||
constructor(url, options = {}) {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
this.options = _.merge({
|
||||
log: new Signale({
|
||||
scope: 'ftp-srv'
|
||||
}),
|
||||
this.options = Object.assign({
|
||||
log: buyan.createLogger({name: 'ftp-srv'}),
|
||||
url: 'ftp://127.0.0.1:21',
|
||||
pasv_min: 1024,
|
||||
pasv_max: 65535,
|
||||
pasv_url: null,
|
||||
anonymous: false,
|
||||
pasv_range: 22,
|
||||
file_format: 'ls',
|
||||
blacklist: [],
|
||||
whitelist: [],
|
||||
greeting: null,
|
||||
tls: false
|
||||
}, options);
|
||||
|
||||
this._greeting = this.setupGreeting(this.options.greeting);
|
||||
this._features = this.setupFeaturesMessage();
|
||||
this._tls = this.setupTLS(this.options.tls);
|
||||
|
||||
delete this.options.greeting;
|
||||
delete this.options.tls;
|
||||
|
||||
this.connections = {};
|
||||
this.log = this.options.log;
|
||||
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
|
||||
this.url = nodeUrl.parse(this.options.url);
|
||||
this.getNextPasvPort = getNextPortFactory(
|
||||
_.get(this, 'options.pasv_min'),
|
||||
_.get(this, 'options.pasv_max'));
|
||||
|
||||
const serverConnectionHandler = socket => {
|
||||
let connection = new Connection(this, socket);
|
||||
const serverConnectionHandler = (socket) => {
|
||||
let connection = new Connection(this, {log: this.log, socket});
|
||||
this.connections[connection.id] = connection;
|
||||
|
||||
socket.on('close', () => this.disconnectClient(connection.id));
|
||||
@@ -47,10 +50,10 @@ class FtpServer extends EventEmitter {
|
||||
return connection.reply(220, ...greeting, features)
|
||||
.finally(() => socket.resume());
|
||||
};
|
||||
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
|
||||
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
|
||||
|
||||
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
|
||||
this.server.on('error', err => this.log.scope('error event').error(err));
|
||||
this.server.on('error', (err) => this.log.error(err, '[Event] error'));
|
||||
|
||||
const quit = _.debounce(this.quit.bind(this), 100);
|
||||
|
||||
@@ -60,16 +63,17 @@ class FtpServer extends EventEmitter {
|
||||
}
|
||||
|
||||
get isTLS() {
|
||||
return this.url.protocol === 'ftps:' && this._tls;
|
||||
return this.url.protocol === 'ftps:' && this.options.tls;
|
||||
}
|
||||
|
||||
listen() {
|
||||
return resolveHost(this.url.hostname)
|
||||
.then(hostname => {
|
||||
this.url.hostname = hostname;
|
||||
return resolveHost(this.options.pasv_url || this.url.hostname)
|
||||
.then((pasvUrl) => {
|
||||
this.options.pasv_url = pasvUrl;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.once('error', reject);
|
||||
this.server.listen(this.url.port, this.url.hostname, err => {
|
||||
this.server.listen(this.url.port, this.url.hostname, (err) => {
|
||||
this.server.removeListener('error', reject);
|
||||
if (err) return reject(err);
|
||||
this.log.info({
|
||||
@@ -90,15 +94,6 @@ class FtpServer extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
setupTLS(_tls) {
|
||||
if (!_tls) return false;
|
||||
return _.assign({}, _tls, {
|
||||
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
|
||||
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
|
||||
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
|
||||
});
|
||||
}
|
||||
|
||||
setupGreeting(greet) {
|
||||
if (!greet) return [];
|
||||
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
|
||||
@@ -117,15 +112,14 @@ class FtpServer extends EventEmitter {
|
||||
}
|
||||
|
||||
disconnectClient(id) {
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
const client = this.connections[id];
|
||||
if (!client) return resolve();
|
||||
delete this.connections[id];
|
||||
try {
|
||||
client.close(0);
|
||||
} catch (err) {
|
||||
this.log.error('Error disconnecting client', err);
|
||||
this.log.debug('User ID', {id});
|
||||
this.log.error(err, 'Error closing connection', {id});
|
||||
} finally {
|
||||
resolve('Disconnected');
|
||||
}
|
||||
@@ -138,12 +132,12 @@ class FtpServer extends EventEmitter {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.log.await('Closing server...');
|
||||
this.log.info('Server closing...');
|
||||
this.server.maxConnections = 0;
|
||||
return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
|
||||
.then(() => new Promise(resolve => {
|
||||
this.server.close(err => {
|
||||
if (err) this.log.error('Error closing server', err);
|
||||
return Promise.map(Object.keys(this.connections), (id) => Promise.try(this.disconnectClient.bind(this, id)))
|
||||
.then(() => new Promise((resolve) => {
|
||||
this.server.close((err) => {
|
||||
if (err) this.log.error(err, 'Error closing server');
|
||||
resolve('Closed');
|
||||
});
|
||||
}))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const {expect} = require('chai');
|
||||
const Promise = require('bluebird');
|
||||
const {Signale} = require('signale');
|
||||
const bunyan = require('bunyan');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const FtpCommands = require('../../src/commands');
|
||||
@@ -10,7 +10,7 @@ describe('FtpCommands', function () {
|
||||
let commands;
|
||||
let mockConnection = {
|
||||
authenticated: false,
|
||||
log: new Signale('commands'),
|
||||
log: bunyan.createLogger({name: 'FtpCommands'}),
|
||||
reply: () => Promise.resolve({}),
|
||||
server: {
|
||||
options: {
|
||||
@@ -20,7 +20,7 @@ describe('FtpCommands', function () {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
commands = new FtpCommands(mockConnection);
|
||||
|
||||
@@ -64,8 +64,8 @@ describe('FtpCommands', function () {
|
||||
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
|
||||
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal('arg1 arg2');
|
||||
expect(cmd.flags).to.deep.equal(['-l', '-A', '--zz88A']);
|
||||
expect(cmd.arg).to.equal('arg1 arg2 --zz88A');
|
||||
expect(cmd.flags).to.deep.equal(['-l', '-A']);
|
||||
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
|
||||
});
|
||||
|
||||
@@ -76,6 +76,13 @@ describe('FtpCommands', function () {
|
||||
expect(cmd.flags).to.deep.equal(['-l']);
|
||||
expect(cmd.raw).to.equal('list -l');
|
||||
});
|
||||
|
||||
it('does not check for option flags', () => {
|
||||
const cmd = commands.parse('retr -test');
|
||||
expect(cmd.directive).to.equal('RETR');
|
||||
expect(cmd.arg).to.equal('-test');
|
||||
expect(cmd.flags).to.deep.equal([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle', function () {
|
||||
@@ -92,7 +99,7 @@ describe('FtpCommands', function () {
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(502);
|
||||
expect(mockConnection.reply.args[0][1]).to.match(/blacklist/);
|
||||
expect(mockConnection.reply.args[0][1]).to.match(/blacklisted/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,7 +109,7 @@ describe('FtpCommands', function () {
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(502);
|
||||
expect(mockConnection.reply.args[0][1]).to.match(/whitelist/);
|
||||
expect(mockConnection.reply.args[0][1]).to.match(/whitelisted/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,20 +3,19 @@ const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'ABOR';
|
||||
describe(CMD, function () {
|
||||
describe.skip(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve(),
|
||||
connector: {
|
||||
waitForConnection: () => Promise.resolve(),
|
||||
end: () => Promise.resolve()
|
||||
},
|
||||
emit: () => Promise.resolve()
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.spy(mockClient.connector, 'waitForConnection');
|
||||
@@ -30,7 +29,7 @@ describe(CMD, function () {
|
||||
mockClient.connector.waitForConnection.restore();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').rejects();
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||
expect(mockClient.connector.end.callCount).to.equal(0);
|
||||
@@ -39,7 +38,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful | active connection', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||
expect(mockClient.connector.end.callCount).to.equal(1);
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -20,7 +20,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(202);
|
||||
});
|
||||
|
||||
@@ -8,13 +8,15 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve(),
|
||||
server: {
|
||||
_tls: {}
|
||||
options: {
|
||||
tls: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -23,7 +25,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('TLS // supported', () => {
|
||||
return cmdFn(mockClient, {arg: 'TLS', directive: CMD})
|
||||
return cmdFn({command: {arg: 'TLS', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(234);
|
||||
expect(mockClient.secure).to.equal(true);
|
||||
@@ -31,14 +33,14 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('SSL // not supported', () => {
|
||||
return cmdFn(mockClient, {arg: 'SSL', directive: CMD})
|
||||
return cmdFn({command: {arg: 'SSL', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
});
|
||||
|
||||
it('bad // bad', () => {
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
const Promise = require('bluebird');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'CDUP';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve(),
|
||||
fs: {
|
||||
chdir: () => Promise.resolve()
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.spy(mockClient.fs, 'chdir');
|
||||
@@ -24,7 +26,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('.. // successful', () => {
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('..');
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'CWD';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: {chdir: () => {}}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'chdir').resolves();
|
||||
@@ -23,10 +25,10 @@ describe(CMD, function () {
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(mockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -34,10 +36,10 @@ describe(CMD, function () {
|
||||
|
||||
it('fails on no fs chdir command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -45,7 +47,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('test // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'test', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
|
||||
@@ -56,7 +58,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.chdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
|
||||
@@ -67,7 +69,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.chdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad');
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'DELE';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: {delete: () => {}}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'delete').resolves();
|
||||
@@ -23,10 +25,10 @@ describe(CMD, function () {
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -37,7 +39,7 @@ describe(CMD, function () {
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -45,7 +47,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('test // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'test', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.delete.args[0][0]).to.equal('test');
|
||||
@@ -56,7 +58,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.delete.restore();
|
||||
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
expect(mockClient.fs.delete.args[0][0]).to.equal('bad');
|
||||
|
||||
@@ -10,10 +10,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
||||
@@ -23,21 +23,21 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// unsuccessful | no argument', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
});
|
||||
|
||||
it('// unsuccessful | invalid argument', () => {
|
||||
return cmdFn(mockClient, {arg: 'blah'})
|
||||
return cmdFn({command: {arg: 'blah'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
});
|
||||
|
||||
it('// successful IPv4', () => {
|
||||
return cmdFn(mockClient, {arg: '|1|192.168.0.100|35286|'})
|
||||
return cmdFn({command: {arg: '|1|192.168.0.100|35286|'}})
|
||||
.then(() => {
|
||||
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
@@ -48,7 +48,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful IPv6', () => {
|
||||
return cmdFn(mockClient, {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'})
|
||||
return cmdFn({command: {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'}})
|
||||
.then(() => {
|
||||
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
|
||||
@@ -10,10 +10,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
|
||||
@@ -25,7 +25,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful IPv4', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
const [code, message] = mockClient.reply.args[0];
|
||||
expect(code).to.equal(229);
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -20,28 +20,28 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(211);
|
||||
});
|
||||
});
|
||||
|
||||
it('help // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'help', directive: CMD})
|
||||
return cmdFn({command: {arg: 'help', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||
});
|
||||
});
|
||||
|
||||
it('allo // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'allo', directive: CMD})
|
||||
return cmdFn({command: {arg: 'allo', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||
});
|
||||
});
|
||||
|
||||
it('bad // unsuccessful', () => {
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(502);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const Promise = require('bluebird');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'LIST';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: {
|
||||
@@ -13,17 +15,17 @@ describe(CMD, function () {
|
||||
},
|
||||
connector: {
|
||||
waitForConnection: () => Promise.resolve({}),
|
||||
end: () => {}
|
||||
end: () => Promise.resolve({})
|
||||
},
|
||||
commandSocket: {
|
||||
resume: () => {},
|
||||
pause: () => {}
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||
@@ -87,10 +89,10 @@ describe(CMD, function () {
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -98,10 +100,10 @@ describe(CMD, function () {
|
||||
|
||||
it('fails on no fs list command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -109,7 +111,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('. // successful', () => {
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1].length).to.equal(3);
|
||||
@@ -141,7 +143,7 @@ describe(CMD, function () {
|
||||
isDirectory: () => false
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {directive: CMD, arg: 'testfile.txt'})
|
||||
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1].length).to.equal(2);
|
||||
@@ -156,7 +158,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.list.restore();
|
||||
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
|
||||
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(451);
|
||||
});
|
||||
@@ -165,7 +167,7 @@ describe(CMD, function () {
|
||||
it('. // unsuccessful (timeout)', () => {
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').returns(Promise.reject(new Promise.TimeoutError()));
|
||||
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||
});
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'MDTM';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: {get: () => {}}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'get').resolves({mtime: 'Mon, 10 Oct 2011 23:24:11 GMT'});
|
||||
@@ -23,10 +25,10 @@ describe(CMD, function () {
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -34,10 +36,10 @@ describe(CMD, function () {
|
||||
|
||||
it('fails on no fs get command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -45,7 +47,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('. // successful', () => {
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(213);
|
||||
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
|
||||
@@ -56,7 +58,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.get.restore();
|
||||
sandbox.stub(mockClient.fs, 'get').rejects(new Error());
|
||||
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'MKD';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: {mkdir: () => {}}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'mkdir').resolves();
|
||||
@@ -23,10 +25,10 @@ describe(CMD, function () {
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -34,10 +36,10 @@ describe(CMD, function () {
|
||||
|
||||
it('fails on no fs mkdir command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -45,7 +47,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('test // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'test', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
|
||||
@@ -56,7 +58,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.mkdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'mkdir').resolves('test');
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
|
||||
@@ -67,7 +69,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.mkdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
expect(mockClient.fs.mkdir.args[0][0]).to.equal('bad');
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -20,14 +20,14 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('S // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'S'})
|
||||
return cmdFn({command: {arg: 'S'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
it('Q // unsuccessful', () => {
|
||||
return cmdFn(mockClient, {arg: 'Q'})
|
||||
return cmdFn({command: {arg: 'Q'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const Promise = require('bluebird');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'NLST';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: {
|
||||
@@ -13,17 +15,17 @@ describe(CMD, function () {
|
||||
},
|
||||
connector: {
|
||||
waitForConnection: () => Promise.resolve({}),
|
||||
end: () => {}
|
||||
end: () => Promise.resolve({})
|
||||
},
|
||||
commandSocket: {
|
||||
resume: () => {},
|
||||
pause: () => {}
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||
@@ -85,7 +87,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('. // successful', () => {
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1].length).to.equal(3);
|
||||
@@ -117,7 +119,7 @@ describe(CMD, function () {
|
||||
isDirectory: () => false
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {directive: CMD, arg: 'testfile.txt'})
|
||||
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1].length).to.equal(2);
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -20,7 +20,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
});
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -20,28 +20,28 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// unsuccessful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||
});
|
||||
});
|
||||
|
||||
it('BAD // unsuccessful', () => {
|
||||
return cmdFn(mockClient, {arg: 'BAD', directive: CMD})
|
||||
return cmdFn({command: {arg: 'BAD', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(500);
|
||||
});
|
||||
});
|
||||
|
||||
it('UTF8 BAD // unsuccessful', () => {
|
||||
return cmdFn(mockClient, {arg: 'UTF8 BAD', directive: CMD})
|
||||
return cmdFn({command: {arg: 'UTF8 BAD', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||
});
|
||||
});
|
||||
|
||||
it('UTF8 OFF // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'UTF8 OFF', directive: CMD})
|
||||
return cmdFn({command: {arg: 'UTF8 OFF', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.encoding).to.equal('ascii');
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
@@ -49,7 +49,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('UTF8 ON // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'UTF8 ON', directive: CMD})
|
||||
return cmdFn({command: {arg: 'UTF8 ON', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.encoding).to.equal('utf8');
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'PASS';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
login: () => {},
|
||||
server: {options: {anonymous: false}},
|
||||
username: 'anonymous'
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient, 'login').resolves();
|
||||
@@ -23,7 +25,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('pass // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'pass', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'pass', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(230);
|
||||
expect(mockClient.login.args[0]).to.eql(['anonymous', 'pass']);
|
||||
@@ -33,7 +35,7 @@ describe(CMD, function () {
|
||||
it('// successful (already authenticated)', () => {
|
||||
mockClient.server.options.anonymous = true;
|
||||
mockClient.authenticated = true;
|
||||
return cmdFn(mockClient, {directive: CMD})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(202);
|
||||
expect(mockClient.login.callCount).to.equal(0);
|
||||
@@ -46,7 +48,7 @@ describe(CMD, function () {
|
||||
mockClient.login.restore();
|
||||
sandbox.stub(mockClient, 'login').rejects('bad');
|
||||
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(530);
|
||||
});
|
||||
@@ -56,7 +58,7 @@ describe(CMD, function () {
|
||||
mockClient.login.restore();
|
||||
sandbox.stub(mockClient, 'login').rejects({});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(530);
|
||||
});
|
||||
@@ -64,7 +66,7 @@ describe(CMD, function () {
|
||||
|
||||
it('bad // unsuccessful', () => {
|
||||
delete mockClient.username;
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(503);
|
||||
});
|
||||
|
||||
@@ -9,10 +9,10 @@ describe(CMD, function () {
|
||||
reply: () => Promise.resolve(),
|
||||
server: {}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// unsuccessful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(202);
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe(CMD, function () {
|
||||
mockClient.secure = true;
|
||||
mockClient.server._tls = {};
|
||||
|
||||
return cmdFn(mockClient, {arg: '0'})
|
||||
return cmdFn({command: {arg: '0'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
expect(mockClient.bufferSize).to.equal(0);
|
||||
@@ -42,7 +42,7 @@ describe(CMD, function () {
|
||||
mockClient.secure = true;
|
||||
mockClient.server._tls = {};
|
||||
|
||||
return cmdFn(mockClient, {arg: '10'})
|
||||
return cmdFn({command: {arg: '10'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
expect(mockClient.bufferSize).to.equal(10);
|
||||
|
||||
@@ -10,10 +10,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
||||
@@ -23,21 +23,21 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// unsuccessful | no argument', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||
});
|
||||
});
|
||||
|
||||
it('// unsuccessful | invalid argument', () => {
|
||||
return cmdFn(mockClient, {arg: '1,2,3,4,5'})
|
||||
return cmdFn({command: {arg: '1,2,3,4,5'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||
});
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient, {arg: '192,168,0,100,137,214'})
|
||||
return cmdFn({command: {arg: '192,168,0,100,137,214'}})
|
||||
.then(() => {
|
||||
const [ip, port] = ActiveConnector.prototype.setupConnection.args[0];
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
|
||||
@@ -9,10 +9,10 @@ describe(CMD, function () {
|
||||
reply: () => Promise.resolve(),
|
||||
server: {}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// unsuccessful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(202);
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe(CMD, function () {
|
||||
mockClient.server._tls = {};
|
||||
mockClient.secure = true;
|
||||
|
||||
return cmdFn(mockClient, {arg: 'P'})
|
||||
return cmdFn({command: {arg: 'P'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(503);
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe(CMD, function () {
|
||||
mockClient.bufferSize = 0;
|
||||
mockClient.secure = true;
|
||||
|
||||
return cmdFn(mockClient, {arg: 'p'})
|
||||
return cmdFn({command: {arg: 'p'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
});
|
||||
@@ -49,7 +49,7 @@ describe(CMD, function () {
|
||||
|
||||
it('// unsuccessful - unsupported', () => {
|
||||
mockClient.secure = true;
|
||||
return cmdFn(mockClient, {arg: 'C'})
|
||||
return cmdFn({command: {arg: 'C'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(536);
|
||||
});
|
||||
@@ -57,7 +57,7 @@ describe(CMD, function () {
|
||||
|
||||
it('// unsuccessful - unknown', () => {
|
||||
mockClient.secure = true;
|
||||
return cmdFn(mockClient, {arg: 'QQ'})
|
||||
return cmdFn({command: {arg: 'QQ'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'PWD';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: {currentDirectory: () => {}}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'currentDirectory').resolves();
|
||||
@@ -26,7 +28,7 @@ describe(CMD, function () {
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -37,7 +39,7 @@ describe(CMD, function () {
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn(badMockClient)
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -45,7 +47,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'test', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||
});
|
||||
@@ -55,7 +57,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.currentDirectory.restore();
|
||||
sandbox.stub(mockClient.fs, 'currentDirectory').resolves('/test');
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||
});
|
||||
@@ -65,7 +67,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.currentDirectory.restore();
|
||||
sandbox.stub(mockClient.fs, 'currentDirectory').rejects(new Error('Bad'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
|
||||
@@ -7,10 +7,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
close: () => {}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'close').resolves();
|
||||
});
|
||||
@@ -19,7 +19,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.close.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -20,28 +20,28 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// unsuccessful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||
});
|
||||
});
|
||||
|
||||
it('-1 // unsuccessful', () => {
|
||||
return cmdFn(mockClient, {arg: '-1', directive: CMD})
|
||||
return cmdFn({command: {arg: '-1', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||
});
|
||||
});
|
||||
|
||||
it('bad // unsuccessful', () => {
|
||||
return cmdFn(mockClient, {arg: 'bad', directive: CMD})
|
||||
return cmdFn({command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||
});
|
||||
});
|
||||
|
||||
it('1 // successful', () => {
|
||||
return cmdFn(mockClient, {arg: '1', directive: CMD})
|
||||
return cmdFn({command: {arg: '1', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.restByteCount).to.equal(1);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(350);
|
||||
@@ -49,7 +49,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('0 // successful', () => {
|
||||
return cmdFn(mockClient, {arg: '0', directive: CMD})
|
||||
return cmdFn({command: {arg: '0', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.restByteCount).to.equal(0);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(350);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Promise = require('bluebird');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const EventEmitter = require('events');
|
||||
@@ -6,6 +7,7 @@ const EventEmitter = require('events');
|
||||
const CMD = 'RETR';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
let emitter;
|
||||
const mockClient = {
|
||||
commandSocket: {
|
||||
@@ -17,13 +19,13 @@ describe(CMD, function () {
|
||||
waitForConnection: () => Promise.resolve({
|
||||
resume: () => {}
|
||||
}),
|
||||
end: () => {}
|
||||
end: () => Promise.resolve({})
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
read: () => {}
|
||||
@@ -41,7 +43,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | no file system', () => {
|
||||
delete mockClient.fs;
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -50,7 +52,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file system does not have functions', () => {
|
||||
mockClient.fs = {};
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -62,7 +64,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test.txt'})
|
||||
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||
});
|
||||
@@ -73,7 +75,7 @@ describe(CMD, function () {
|
||||
return Promise.reject(new Error('test'));
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test.txt'})
|
||||
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(551);
|
||||
});
|
||||
@@ -85,11 +87,11 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
let errorEmitted = false;
|
||||
emitter.once('RETR', err => {
|
||||
emitter.once('RETR', (err) => {
|
||||
errorEmitted = !!err;
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test.txt'})
|
||||
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||
.then(() => {
|
||||
expect(errorEmitted).to.equal(true);
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@ const sinon = require('sinon');
|
||||
const CMD = 'RNFR';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockLog = {error: () => {}};
|
||||
const mockClient = {reply: () => Promise.resolve()};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.renameFrom = 'test';
|
||||
mockClient.fs = {
|
||||
@@ -26,7 +27,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | no file system', () => {
|
||||
delete mockClient.fs;
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -35,7 +36,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file system does not have functions', () => {
|
||||
mockClient.fs = {};
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -45,14 +46,14 @@ describe(CMD, function () {
|
||||
mockClient.fs.get.restore();
|
||||
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
});
|
||||
|
||||
it('test // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.fs.get.args[0][0]).to.equal('test');
|
||||
expect(mockClient.reply.args[0][0]).to.equal(350);
|
||||
|
||||
@@ -5,11 +5,12 @@ const sinon = require('sinon');
|
||||
const CMD = 'RNTO';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockLog = {error: () => {}};
|
||||
const mockClient = {reply: () => Promise.resolve()};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.renameFrom = 'test';
|
||||
mockClient.fs = {
|
||||
@@ -27,7 +28,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | no renameFrom set', () => {
|
||||
delete mockClient.renameFrom;
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(503);
|
||||
});
|
||||
@@ -36,7 +37,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | no file system', () => {
|
||||
delete mockClient.fs;
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -45,7 +46,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file system does not have functions', () => {
|
||||
mockClient.fs = {};
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -55,14 +56,14 @@ describe(CMD, function () {
|
||||
mockClient.fs.rename.restore();
|
||||
sandbox.stub(mockClient.fs, 'rename').rejects(new Error('test'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'new'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'new'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
});
|
||||
|
||||
it('new // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'new'})
|
||||
return cmdFn({command: {arg: 'new'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.rename.args[0]).to.eql(['test', 'new']);
|
||||
|
||||
@@ -5,11 +5,12 @@ const sinon = require('sinon');
|
||||
const CMD = 'CHMOD';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockLog = {error: () => {}};
|
||||
const mockClient = {reply: () => Promise.resolve()};
|
||||
const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`);
|
||||
const cmdFn = require(`../../../../src/commands/registration/site/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
chmod: () => Promise.resolve()
|
||||
@@ -22,10 +23,10 @@ describe(CMD, function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// unsuccessful | no file system', done => {
|
||||
it('// unsuccessful | no file system', (done) => {
|
||||
delete mockClient.fs;
|
||||
|
||||
cmdFn(mockClient)
|
||||
cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
@@ -33,10 +34,10 @@ describe(CMD, function () {
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('// unsuccessful | file system does not have functions', done => {
|
||||
it('// unsuccessful | file system does not have functions', (done) => {
|
||||
mockClient.fs = {};
|
||||
|
||||
cmdFn(mockClient)
|
||||
cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
@@ -44,11 +45,11 @@ describe(CMD, function () {
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('777 test // unsuccessful | file chmod fails', done => {
|
||||
it('777 test // unsuccessful | file chmod fails', (done) => {
|
||||
mockClient.fs.chmod.restore();
|
||||
sandbox.stub(mockClient.fs, 'chmod').rejects(new Error('test'));
|
||||
|
||||
cmdFn(mockClient, {arg: '777 test'})
|
||||
cmdFn({log: mockLog, command: {arg: '777 test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(500);
|
||||
done();
|
||||
@@ -56,8 +57,8 @@ describe(CMD, function () {
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('777 test // successful', done => {
|
||||
cmdFn(mockClient, {arg: '777 test'})
|
||||
it('777 test // successful', (done) => {
|
||||
cmdFn({log: mockLog, command: {arg: '777 test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.fs.chmod.args[0]).to.eql(['test', 511]);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const Promise = require('bluebird');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const bunyan = require('bunyan');
|
||||
|
||||
const siteRegistry = require('../../../../src/commands/registration/site/registry');
|
||||
const FtpCommands = require('../../../../src/commands');
|
||||
@@ -8,14 +9,15 @@ const FtpCommands = require('../../../../src/commands');
|
||||
const CMD = 'SITE';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const log = bunyan.createLogger({name: 'site-test'});
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve(),
|
||||
commands: new FtpCommands()
|
||||
};
|
||||
const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
});
|
||||
@@ -24,14 +26,14 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// unsuccessful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn({log})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(502);
|
||||
});
|
||||
});
|
||||
|
||||
it('// unsuccessful', () => {
|
||||
return cmdFn(mockClient, {arg: 'BAD'})
|
||||
return cmdFn({log, command: {arg: 'BAD'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(502);
|
||||
});
|
||||
@@ -40,7 +42,7 @@ describe(CMD, function () {
|
||||
it('// successful', () => {
|
||||
sandbox.stub(siteRegistry.CHMOD, 'handler').resolves();
|
||||
|
||||
return cmdFn(mockClient, {arg: 'CHMOD test'})
|
||||
return cmdFn({log, command: {arg: 'CHMOD test'}})
|
||||
.then(() => {
|
||||
const {command} = siteRegistry.CHMOD.handler.args[0][0];
|
||||
expect(command.directive).to.equal('CHMOD');
|
||||
|
||||
@@ -5,11 +5,12 @@ const sinon = require('sinon');
|
||||
const CMD = 'SIZE';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockLog = {error: () => {}};
|
||||
const mockClient = {reply: () => Promise.resolve()};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
get: () => Promise.resolve({size: 1})
|
||||
@@ -24,7 +25,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | no file system', () => {
|
||||
delete mockClient.fs;
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -33,7 +34,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file system does not have functions', () => {
|
||||
mockClient.fs = {};
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -42,14 +43,14 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file get fails', () => {
|
||||
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(213);
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@ const sinon = require('sinon');
|
||||
const CMD = 'STAT';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockLog = {error: () => {}};
|
||||
const mockClient = {reply: () => Promise.resolve()};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
get: () => Promise.resolve({}),
|
||||
@@ -23,7 +24,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(211);
|
||||
});
|
||||
@@ -32,7 +33,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | no file system', () => {
|
||||
delete mockClient.fs;
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -41,7 +42,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file system does not have functions', () => {
|
||||
mockClient.fs = {};
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -50,7 +51,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file get fails', () => {
|
||||
sandbox.stub(mockClient.fs, 'get').rejects(new Error('test'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(450);
|
||||
});
|
||||
@@ -76,7 +77,7 @@ describe(CMD, function () {
|
||||
isDirectory: () => false
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(212);
|
||||
});
|
||||
@@ -121,7 +122,7 @@ describe(CMD, function () {
|
||||
isDirectory: () => true
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(213);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Promise = require('bluebird');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const EventEmitter = require('events');
|
||||
@@ -6,6 +7,7 @@ const EventEmitter = require('events');
|
||||
const CMD = 'STOR';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
let emitter;
|
||||
const mockClient = {
|
||||
commandSocket: {
|
||||
@@ -17,13 +19,13 @@ describe(CMD, function () {
|
||||
waitForConnection: () => Promise.resolve({
|
||||
resume: () => {}
|
||||
}),
|
||||
end: () => {}
|
||||
end: () => Promise.resolve({})
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
write: () => {}
|
||||
@@ -41,7 +43,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | no file system', () => {
|
||||
delete mockClient.fs;
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -50,7 +52,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file system does not have functions', () => {
|
||||
mockClient.fs = {};
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -61,7 +63,7 @@ describe(CMD, function () {
|
||||
return Promise.reject(new Promise.TimeoutError());
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test.txt'})
|
||||
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||
});
|
||||
@@ -72,7 +74,7 @@ describe(CMD, function () {
|
||||
return Promise.reject(new Error('test'));
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test.txt'})
|
||||
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -84,11 +86,11 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
let errorEmitted = false;
|
||||
emitter.once('STOR', err => {
|
||||
emitter.once('STOR', (err) => {
|
||||
errorEmitted = !!err;
|
||||
});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test.txt'})
|
||||
return cmdFn({log, command: {arg: 'test.txt'}})
|
||||
.then(() => {
|
||||
expect(errorEmitted).to.equal(true);
|
||||
});
|
||||
|
||||
@@ -10,10 +10,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.fs = {
|
||||
get: () => Promise.resolve(),
|
||||
@@ -33,7 +33,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | no file system', () => {
|
||||
delete mockClient.fs;
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
@@ -42,7 +42,7 @@ describe(CMD, function () {
|
||||
it('// unsuccessful | file system does not have functions', () => {
|
||||
mockClient.fs = {};
|
||||
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
@@ -52,7 +52,7 @@ describe(CMD, function () {
|
||||
mockClient.fs.get.restore();
|
||||
sandbox.stub(mockClient.fs, 'get').rejects({});
|
||||
|
||||
return cmdFn(mockClient, {arg: 'good'})
|
||||
return cmdFn({command: {arg: 'good'}})
|
||||
.then(() => {
|
||||
const call = stor.handler.call.args[0][1];
|
||||
expect(call).to.have.property('command');
|
||||
@@ -63,7 +63,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful | generates unique name', () => {
|
||||
return cmdFn(mockClient, {arg: 'bad'})
|
||||
return cmdFn({command: {arg: 'bad'}})
|
||||
.then(() => {
|
||||
const call = stor.handler.call.args[0][1];
|
||||
expect(call).to.have.property('command');
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -20,14 +20,14 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'F'})
|
||||
return cmdFn({command: {arg: 'F'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
it('// unsuccessful', () => {
|
||||
return cmdFn(mockClient, {arg: 'X'})
|
||||
return cmdFn({command: {arg: 'X'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
@@ -20,7 +20,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn(mockClient)
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(215);
|
||||
});
|
||||
|
||||
@@ -8,10 +8,10 @@ describe(CMD, function () {
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handle;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
mockClient.transferType = null;
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
@@ -21,7 +21,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('A // successful', () => {
|
||||
return cmdFn(mockClient, {arg: 'A'})
|
||||
return cmdFn({command: {arg: 'A'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
expect(mockClient.transferType).to.equal('ascii');
|
||||
|
||||
@@ -5,15 +5,18 @@ const sinon = require('sinon');
|
||||
const CMD = 'USER';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockLog = {
|
||||
error: () => {}
|
||||
};
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve(),
|
||||
server: {options: {}},
|
||||
login: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler;
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
|
||||
delete mockClient.username;
|
||||
mockClient.server.options = {};
|
||||
@@ -26,7 +29,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('test // successful | prompt for password', () => {
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(331);
|
||||
});
|
||||
@@ -35,7 +38,7 @@ describe(CMD, function () {
|
||||
it('test // successful | anonymous login', () => {
|
||||
mockClient.server.options = {anonymous: true};
|
||||
|
||||
return cmdFn(mockClient, {arg: 'anonymous'})
|
||||
return cmdFn({command: {arg: 'anonymous'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(230);
|
||||
expect(mockClient.login.callCount).to.equal(1);
|
||||
@@ -43,7 +46,7 @@ describe(CMD, function () {
|
||||
});
|
||||
|
||||
it('test // unsuccessful | no username provided', () => {
|
||||
return cmdFn(mockClient, { })
|
||||
return cmdFn({command: { }})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(501);
|
||||
expect(mockClient.login.callCount).to.equal(0);
|
||||
@@ -53,7 +56,7 @@ describe(CMD, function () {
|
||||
it('test // unsuccessful | already set username', () => {
|
||||
mockClient.username = 'test';
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(530);
|
||||
expect(mockClient.login.callCount).to.equal(0);
|
||||
@@ -63,7 +66,7 @@ describe(CMD, function () {
|
||||
it('test // successful | regular login if anonymous is true', () => {
|
||||
mockClient.server.options = {anonymous: true};
|
||||
|
||||
return cmdFn(mockClient, {arg: 'test'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'test'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(331);
|
||||
expect(mockClient.login.callCount).to.equal(0);
|
||||
@@ -73,7 +76,7 @@ describe(CMD, function () {
|
||||
it('test // successful | anonymous login with set username', () => {
|
||||
mockClient.server.options = {anonymous: 'sillyrabbit'};
|
||||
|
||||
return cmdFn(mockClient, {arg: 'sillyrabbit'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'sillyrabbit'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(230);
|
||||
expect(mockClient.login.callCount).to.equal(1);
|
||||
@@ -85,7 +88,7 @@ describe(CMD, function () {
|
||||
mockClient.login.restore();
|
||||
sandbox.stub(mockClient, 'login').rejects(new Error('test'));
|
||||
|
||||
return cmdFn(mockClient, {arg: 'anonymous'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'anonymous'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(530);
|
||||
expect(mockClient.login.callCount).to.equal(1);
|
||||
@@ -95,7 +98,7 @@ describe(CMD, function () {
|
||||
it('test // successful | does not login if already authenticated', () => {
|
||||
mockClient.authenticated = true;
|
||||
|
||||
return cmdFn(mockClient, {arg: 'sillyrabbit'})
|
||||
return cmdFn({log: mockLog, command: {arg: 'sillyrabbit'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(230);
|
||||
expect(mockClient.login.callCount).to.equal(0);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,88 +4,120 @@ const sinon = require('sinon');
|
||||
|
||||
const Promise = require('bluebird');
|
||||
const net = require('net');
|
||||
const bunyan = require('bunyan');
|
||||
|
||||
const PassiveConnector = require('../../src/connector/passive');
|
||||
const {getNextPortFactory} = require('../../src/helpers/find-port');
|
||||
|
||||
describe('Connector - Passive //', function () {
|
||||
let passive;
|
||||
let mockConnection = {
|
||||
reply: () => Promise.resolve({}),
|
||||
close: () => Promise.resolve({}),
|
||||
encoding: 'utf8',
|
||||
commandSocket: {},
|
||||
server: {options: {}}
|
||||
log: bunyan.createLogger({name: 'passive-test'}),
|
||||
commandSocket: {
|
||||
remoteAddress: '::ffff:127.0.0.1'
|
||||
},
|
||||
server: {
|
||||
url: '',
|
||||
getNextPasvPort: getNextPortFactory(1024)
|
||||
}
|
||||
};
|
||||
let sandbox;
|
||||
|
||||
function shouldNotResolve() {
|
||||
throw new Error('Should not resolve');
|
||||
}
|
||||
|
||||
before(() => {
|
||||
passive = new PassiveConnector(mockConnection);
|
||||
sandbox = sinon.sandbox.create().usingPromise(Promise);
|
||||
});
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.spy(mockConnection, 'reply');
|
||||
sandbox.spy(mockConnection, 'close');
|
||||
|
||||
mockConnection.commandSocket.remoteAddress = '::ffff:127.0.0.1';
|
||||
mockConnection.server.options.pasv_range = '8000';
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('cannot wait for connection with no server', function () {
|
||||
return passive.waitForConnection()
|
||||
.then(shouldNotResolve)
|
||||
.catch(err => {
|
||||
it('cannot wait for connection with no server', function (done) {
|
||||
let passive = new PassiveConnector(mockConnection);
|
||||
passive.waitForConnection()
|
||||
.catch((err) => {
|
||||
expect(err.name).to.equal('ConnectorError');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('no pasv range provided', function () {
|
||||
delete mockConnection.server.options.pasv_range;
|
||||
describe('setup', function () {
|
||||
before(function () {
|
||||
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory());
|
||||
});
|
||||
|
||||
return passive.setupServer()
|
||||
.then(shouldNotResolve)
|
||||
.catch(err => {
|
||||
expect(err.name).to.equal('ConnectorError');
|
||||
it('no pasv range provided', function (done) {
|
||||
let passive = new PassiveConnector(mockConnection);
|
||||
passive.setupServer()
|
||||
.catch((err) => {
|
||||
try {
|
||||
expect(err.name).to.equal('ConnectorError');
|
||||
done();
|
||||
} catch (ex) {
|
||||
done(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has invalid pasv range', function () {
|
||||
mockConnection.server.options.pasv_range = -1;
|
||||
describe('setup', function () {
|
||||
let connection;
|
||||
before(function () {
|
||||
sandbox.stub(mockConnection.server, 'getNextPasvPort').value(getNextPortFactory(-1, -1));
|
||||
|
||||
return passive.setupServer()
|
||||
.then(shouldNotResolve)
|
||||
.catch(err => {
|
||||
expect(err).to.be.instanceOf(RangeError);
|
||||
connection = new PassiveConnector(mockConnection);
|
||||
});
|
||||
|
||||
it('has invalid pasv range', function (done) {
|
||||
connection.setupServer()
|
||||
.catch((err) => {
|
||||
expect(err.name).to.equal('ConnectorError');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets up a server', function () {
|
||||
let passive = new PassiveConnector(mockConnection);
|
||||
return passive.setupServer()
|
||||
.then(() => {
|
||||
expect(passive.dataServer).to.exist;
|
||||
return passive.end();
|
||||
});
|
||||
});
|
||||
|
||||
it('destroys existing server, then sets up a server', function () {
|
||||
const closeFnSpy = sandbox.spy(passive.dataServer, 'close');
|
||||
describe('setup', function () {
|
||||
let passive;
|
||||
let closeFnSpy;
|
||||
beforeEach(function () {
|
||||
passive = new PassiveConnector(mockConnection);
|
||||
return passive.setupServer()
|
||||
.then(() => {
|
||||
closeFnSpy = sandbox.spy(passive.dataServer, 'close');
|
||||
});
|
||||
});
|
||||
afterEach(function () {
|
||||
return passive.end();
|
||||
});
|
||||
|
||||
return passive.setupServer()
|
||||
.then(() => {
|
||||
expect(closeFnSpy.callCount).to.equal(1);
|
||||
expect(passive.dataServer).to.exist;
|
||||
it('destroys existing server, then sets up a server', function () {
|
||||
return passive.setupServer()
|
||||
.then(() => {
|
||||
expect(closeFnSpy.callCount).to.equal(1);
|
||||
expect(passive.dataServer).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses connection with different remote address', function (done) {
|
||||
mockConnection.commandSocket.remoteAddress = 'bad';
|
||||
sandbox.stub(mockConnection.commandSocket, 'remoteAddress').value('bad');
|
||||
|
||||
let passive = new PassiveConnector(mockConnection);
|
||||
passive.setupServer()
|
||||
.then(() => {
|
||||
expect(passive.dataServer).to.exist;
|
||||
@@ -96,6 +128,8 @@ describe('Connector - Passive //', function () {
|
||||
setTimeout(() => {
|
||||
expect(passive.connection.reply.callCount).to.equal(1);
|
||||
expect(passive.connection.reply.args[0][0]).to.equal(550);
|
||||
|
||||
passive.end();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
@@ -104,6 +138,7 @@ describe('Connector - Passive //', function () {
|
||||
});
|
||||
|
||||
it('accepts connection', function () {
|
||||
let passive = new PassiveConnector(mockConnection);
|
||||
return passive.setupServer()
|
||||
.then(() => {
|
||||
expect(passive.dataServer).to.exist;
|
||||
|
||||
83
test/fs.spec.js
Normal file
83
test/fs.spec.js
Normal 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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ describe('helpers // file-stat', function () {
|
||||
let sandbox;
|
||||
|
||||
before(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox = sinon.createSandbox().usingPromise(Promise);
|
||||
});
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user