Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83947142df | ||
|
|
c54045e0b9 | ||
|
|
cf71243729 | ||
|
|
7fb43a5790 | ||
|
|
e99059125e | ||
|
|
954e9a1252 | ||
|
|
2b9e163958 | ||
|
|
c6a49d2191 | ||
|
|
14e5f87cc3 | ||
|
|
580b8d6eae | ||
|
|
a75d63df92 | ||
|
|
301ae110e8 | ||
|
|
4d69b48466 | ||
|
|
ec010697bb | ||
|
|
cf3d543f1a | ||
|
|
69bec2b01c | ||
|
|
2eac41d127 | ||
|
|
eb32f93fc6 | ||
|
|
095423606e | ||
|
|
61cf1bda39 | ||
|
|
75f847ed5d | ||
|
|
ad4b32fc13 | ||
|
|
be3c57bed0 | ||
|
|
dc7dd1075c | ||
|
|
543e6cc1cc | ||
|
|
5c1f8f7a65 | ||
|
|
557995a1a9 | ||
|
|
45eca5afe0 | ||
|
|
695e594d97 | ||
|
|
97b55fc92c | ||
|
|
577066850b | ||
|
|
0ec989cf1e | ||
|
|
568833e216 | ||
|
|
6b0c06e588 | ||
|
|
acd485a571 | ||
|
|
2b2ca45673 | ||
|
|
a62b6f9559 | ||
|
|
84d54cbc2b | ||
|
|
ef6134d91b | ||
|
|
043d9369cc | ||
|
|
6b81748fd7 | ||
|
|
0f4f5cdbd7 | ||
|
|
0293752635 | ||
|
|
aa278105f9 | ||
|
|
bbe0bf2942 | ||
|
|
846df72e24 | ||
|
|
8227c512dd | ||
|
|
b7e17af99e | ||
|
|
9276f7f448 | ||
|
|
99a0ebd536 | ||
|
|
0c5f8562d5 | ||
|
|
5154743a3a | ||
|
|
6654f2c25c | ||
|
|
83540d268a | ||
|
|
795c3d7c65 | ||
|
|
f6d1a3828a | ||
|
|
e5b10c5858 | ||
|
|
4a6ab71731 | ||
|
|
ccc053ac8d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,5 +2,4 @@ node_modules/
|
||||
|
||||
dist/
|
||||
reports/
|
||||
.env
|
||||
npm-debug.log
|
||||
|
||||
@@ -2,6 +2,10 @@ language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
|
||||
env:
|
||||
FTP_URL: ftp://127.0.0.1:8880
|
||||
PASV_RANGE: 8881
|
||||
|
||||
install: npm install
|
||||
|
||||
script:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--[CN_HEADING]-->
|
||||
# Contributing
|
||||
|
||||
Welcome! This document explains how you can contribute to making **ftp-svr** even better.
|
||||
Welcome! This document explains how you can contribute to making **ftp-srv** even better.
|
||||
|
||||
|
||||
<!--[]-->
|
||||
@@ -26,7 +26,7 @@ npm install
|
||||
|
||||
Code is organised into modules which contain one-or-more components. This a great way to ensure maintainable code by encapsulation of behavior logic. A component is basically a self contained app usually in a single file or a folder with each concern as a file: style, template, specs, e2e, and component class. Here's how it looks:
|
||||
```
|
||||
ftp-svr/
|
||||
ftp-srv/
|
||||
├──config/ * configuration files live here (e.g. eslint, verify, testUnit)
|
||||
│
|
||||
├──src/ * source code files should be here
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
ftp-svr Copyright (c) 2017 Tyler Stewart
|
||||
ftp-srv Copyright (c) 2017 Tyler Stewart
|
||||
|
||||
MIT License
|
||||
|
||||
|
||||
218
README.md
218
README.md
@@ -1,139 +1,191 @@
|
||||
# ftp-svr [](https://github.com/semantic-release/semantic-release) [](http://commitizen.github.io/cz-cli/)
|
||||
|
||||
# ftp-srv [](https://badge.fury.io/js/ftp-srv) [](https://travis-ci.org/stewarttylerr/ftp-srv) [](https://github.com/semantic-release/semantic-release) [](http://commitizen.github.io/cz-cli/)
|
||||
|
||||
<!--[RM_DESCRIPTION]-->
|
||||
> Modern, extensible FTP Server
|
||||
|
||||
<!--[]-->
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [API](#api)
|
||||
- [Events](#events)
|
||||
- [Supported Commands](#supported-commands)
|
||||
- [File System](#file-system)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Overview
|
||||
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
|
||||
|
||||
## Features
|
||||
- Supports passive and active connections
|
||||
- Extensible [file system](#file-system)
|
||||
- Extensible [file systems](#file-system) per connection
|
||||
- Passive and active transfers
|
||||
- Implicit TLS connections
|
||||
|
||||
## Install
|
||||
|
||||
`npm install ftp-svr --save`
|
||||
|
||||
`yarn add ftp-svr`
|
||||
`npm install ftp-srv --save`
|
||||
|
||||
## Usage
|
||||
- [Options](#options)
|
||||
- [Events](#events)
|
||||
- [File System](#file-system)
|
||||
|
||||
```js
|
||||
const FtpSvr = require('ftp-svr');
|
||||
const ftpServer = new FtpSvr({ [options] ... });
|
||||
// Quick start
|
||||
|
||||
ftpServer.on('...', (data, resolve, reject) => { ... })
|
||||
const FtpSvr = require('ftp-srv');
|
||||
const ftpServer = new FtpSvr('ftp://0.0.0.0:9876', { options ... });
|
||||
|
||||
ftpServer.on('login', (data, resolve, reject) => { ... });
|
||||
...
|
||||
|
||||
ftpServer.listen()
|
||||
.then(() => { ... });
|
||||
```
|
||||
|
||||
### Options
|
||||
__url__ : `ftp://127.0.0.1:21`
|
||||
> Host and port to listen on and make passive connections to.
|
||||
Set the hostname to "0.0.0.0" to fetch the external IP automatically: `ftp://0.0.0.0:21`
|
||||
## API
|
||||
|
||||
__pasv_range__ : `22`
|
||||
> Minimum port or range to use for passive connections.
|
||||
Provide either a starting integer (`1000`) or a range (`1000-2000`).
|
||||
### `new FtpSrv(url, [{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.
|
||||
__Default:__ `"ftp://127.0.0.1:21"`
|
||||
|
||||
__anonymous__ : `false`
|
||||
> Set whether a valid username or password combination is required.
|
||||
If true, will not require the `PASS` command to be sent for login.
|
||||
#### `options`
|
||||
|
||||
__disabled_commands__ : `[]`
|
||||
> String array of commands to forbid.
|
||||
`['RMD', 'RNFR', 'RNTO']`
|
||||
- ##### `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`
|
||||
|
||||
__file_format__ : `ls`
|
||||
> Format to use for [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) responses (such as with the `LIST` command).
|
||||
Possible 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` : pass in your own format function, returning a string:
|
||||
`function (fileStats) { ... }`
|
||||
- ##### `greeting`
|
||||
A human readable array of lines or string to send when a client connects.
|
||||
__Default:__ `null`
|
||||
|
||||
__log__ : `bunyan`
|
||||
> A [bunyan logger](https://github.com/trentm/node-bunyan) instance.
|
||||
- ##### `tls`
|
||||
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit `ftps`.
|
||||
__Default:__ `{}`
|
||||
|
||||
### Events
|
||||
All events emit the same structure: `({data object}, resolve, reject)`
|
||||
- ##### `anonymous`
|
||||
If true, will call the event login after `USER`, not requiring a password from the user.
|
||||
__Default:__ `false`
|
||||
|
||||
__login__ : `{connection, username, password}`
|
||||
> Occurs after `PASV` (or `USER` if `options.anonymous`)
|
||||
- ##### `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.
|
||||
__Default:__ `[]`
|
||||
|
||||
- ##### `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 [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
|
||||
|
||||
## Events
|
||||
|
||||
The `FtpSvr` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
|
||||
|
||||
### `login`
|
||||
```js
|
||||
on('login', {connection, username, password}, resolve, reject) => { ... }
|
||||
```
|
||||
resolve({
|
||||
fs, // [optional] custom file system class
|
||||
cwd // [optional] initial working directory (if not using custom file system)
|
||||
})
|
||||
```
|
||||
|
||||
### File System
|
||||
The file system can be overridden to use your own custom class. This an allow for interacting with files without actually writing them.
|
||||
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
|
||||
`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.
|
||||
- `root`
|
||||
- If `fs` is not provided, this will set the root directory for the connection.
|
||||
- The user cannot traverse lower than this directory.
|
||||
- `cwd`
|
||||
- If `fs` is not provided, will set the starting directory for the connection
|
||||
- This is relative to the `root` directory.
|
||||
- `blacklist`
|
||||
- Commands that are forbidden for only this connection
|
||||
- `whitelist`
|
||||
- If set, this connection will only be able to use the provided commands
|
||||
|
||||
*Anytime a [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object is used, it must have added `name` property with the file's name.*
|
||||
`reject` takes an error object
|
||||
|
||||
#### Functions
|
||||
`currentDirectory()`
|
||||
> Returns a string of the current working directory
|
||||
### `client-error`
|
||||
```js
|
||||
on('client-error', {connection, context, error}) => { ... }
|
||||
```
|
||||
|
||||
> Used in: `PWD`
|
||||
Occurs when an error occurs in the client connection.
|
||||
|
||||
`get(fileName)`
|
||||
> Returns a file stat object of file or directory
|
||||
## Supported Commands
|
||||
|
||||
> Used in: `STAT`, `SIZE`, `RNFR`, `MDTM`
|
||||
See [commands](src/commands) for a list of all implemented FTP commands.
|
||||
|
||||
`list(path)`
|
||||
> Returns array of file and directory stat objects
|
||||
## File System
|
||||
The default file system 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.
|
||||
|
||||
> Used in `LIST`, `STAT`
|
||||
Custom file systems can implement the following variables depending on the developers needs.
|
||||
|
||||
`chdir(path)`
|
||||
> Returns new directory relative to cwd
|
||||
### Methods
|
||||
#### [`currentDirectory()`](src/fs.js#L29)
|
||||
Returns a string of the current working directory
|
||||
__Used in:__ `PWD`
|
||||
|
||||
> Used in `CWD`, `CDUP`
|
||||
#### [`get(fileName)`](src/fs.js#L33)
|
||||
Returns a file stat object of file or directory
|
||||
__Used in:__ `STAT`, `SIZE`, `RNFR`, `MDTM`
|
||||
|
||||
`mkdir(path)`
|
||||
> Return a path to a newly created directory
|
||||
#### [`list(path)`](src/fs.js#L39)
|
||||
Returns array of file and directory stat objects
|
||||
|
||||
> Used in `MKD`
|
||||
__Used in:__ `LIST`, `STAT`
|
||||
|
||||
`write(fileName, options)`
|
||||
> Returns a writable stream
|
||||
Options:
|
||||
`append` if true, append to existing file
|
||||
#### [`chdir(path)`](src/fs.js#L56)
|
||||
Returns new directory relative to current directory
|
||||
__Used in:__ `CWD`, `CDUP`
|
||||
|
||||
> Used in `STOR`, `APPE`
|
||||
#### [`mkdir(path)`](src/fs.js#L96)
|
||||
Returns a path to a newly created directory
|
||||
__Used in:__ `MKD`
|
||||
|
||||
`read(fileName)`
|
||||
> Returns a readable stream
|
||||
#### [`write(fileName, {append = false})`](src/fs.js#L68)
|
||||
Returns a writable stream
|
||||
Options: `append` if true, append to existing file
|
||||
__Used in:__ `STOR`, `APPE`
|
||||
|
||||
> Used in `RETR`
|
||||
#### [`read(fileName)`](src/fs.js#L75)
|
||||
Returns a readable stream
|
||||
__Used in:__ `RETR`
|
||||
|
||||
`delete(path)`
|
||||
> Delete a file or directory
|
||||
#### [`delete(path)`](src/fs.js#L87)
|
||||
Delete a file or directory
|
||||
__Used in:__ `DELE`
|
||||
|
||||
> Used in `DELE`
|
||||
#### [`rename(from, to)`](src/fs.js#L102)
|
||||
Rename a file or directory
|
||||
__Used in:__ `RNFR`, `RNTO`
|
||||
|
||||
`rename(from, to)`
|
||||
> Rename a file or directory
|
||||
#### [`chmod(path)`](src/fs.js#L108)
|
||||
Modify a file or directory's permissions
|
||||
__Used in:__ `SITE CHMOD`
|
||||
|
||||
> Used in `RNFR`, `RNTO`
|
||||
|
||||
`chmod(path)`
|
||||
> Modify a file or directory's permissions
|
||||
|
||||
> Used in `SITE CHMOD`
|
||||
#### [`getUniqueName()`](src/fs.js#L113)
|
||||
Returns a unique file name to write to
|
||||
__Used in:__ `STOU`
|
||||
|
||||
<!--[RM_CONTRIBUTING]-->
|
||||
## Contributing
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Use JS to support loading of threshold data from external file
|
||||
var coverageConfig = {
|
||||
instrumentation: {
|
||||
root: 'src/'
|
||||
root: 'src/',
|
||||
excludes: ['errors.js']
|
||||
},
|
||||
check: require('./thresholds.json'),
|
||||
reporting: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
test/**/*.spec.js
|
||||
--reporter list
|
||||
--no-timeouts
|
||||
--reporter mocha-pretty-bunyan-nyan
|
||||
--ui bdd
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"global": {
|
||||
"statements": 70,
|
||||
"branches": 60,
|
||||
"functions": 80,
|
||||
"lines": 80
|
||||
"statements": 90,
|
||||
"branches": 80,
|
||||
"functions": 90,
|
||||
"lines": 90
|
||||
},
|
||||
"each": {
|
||||
"statements": 0,
|
||||
"branches": 0,
|
||||
"functions": 0,
|
||||
"lines": 0
|
||||
"statements": 70,
|
||||
"branches": 40,
|
||||
"functions": 60,
|
||||
"lines": 70
|
||||
}
|
||||
}
|
||||
|
||||
14
package.json
14
package.json
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "ftp-svr",
|
||||
"name": "ftp-srv",
|
||||
"version": "0.0.0",
|
||||
"description": "Modern, extensible FTP Server",
|
||||
"keywords": [
|
||||
"ftp",
|
||||
"ftp-server",
|
||||
"ftp-srv",
|
||||
"ftp-svr",
|
||||
"ftpd",
|
||||
"server"
|
||||
@@ -13,7 +14,7 @@
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/stewarttylerr/ftp-svr"
|
||||
"url": "https://github.com/stewarttylerr/ftp-srv"
|
||||
},
|
||||
"scripts": {
|
||||
"pre-release": "npm-run-all verify test:coverage build ",
|
||||
@@ -45,10 +46,9 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.5",
|
||||
"date-fns": "^1.28.0",
|
||||
"bunyan": "^1.8.9",
|
||||
"lodash": "^4.17.4",
|
||||
"minimist-string": "^1.0.2",
|
||||
"moment": "^2.18.1",
|
||||
"uuid": "^3.0.1",
|
||||
"when": "^3.7.8"
|
||||
},
|
||||
@@ -67,11 +67,11 @@
|
||||
"husky": "0.13.1",
|
||||
"istanbul": "0.4.5",
|
||||
"mocha": "3.2.0",
|
||||
"mocha-pretty-bunyan-nyan": "^1.0.4",
|
||||
"npm-run-all": "4.0.1",
|
||||
"rimraf": "2.5.4",
|
||||
"semantic-release": "^6.3.2",
|
||||
"sinon": "^1.17.7",
|
||||
"sinon-as-promised": "^4.0.2"
|
||||
"sinon": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.x",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.reply(202);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
const method = _.upperCase(command._[1]);
|
||||
|
||||
switch (method) {
|
||||
case 'TLS': return handleTLS.call(this);
|
||||
case 'SSL': return handleSSL.call(this);
|
||||
default: return this.reply(504);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTLS() {
|
||||
return this.reply(504);
|
||||
}
|
||||
|
||||
function handleSSL() {
|
||||
return this.reply(504);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
const cwd = require('./cwd');
|
||||
|
||||
module.exports = function(args) {
|
||||
args.command._ = [args.command._[0], '..'];
|
||||
return cwd.call(this, args);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../helpers/escape-path');
|
||||
|
||||
module.exports = 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 when(this.fs.chdir(command._[1]))
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(250, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = 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 when(this.fs.delete(command._[1]))
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function () {
|
||||
const registry = require('./registry');
|
||||
const features = Object.keys(registry)
|
||||
.filter(cmd => registry[cmd].hasOwnProperty('feat'))
|
||||
.reduce((feats, cmd) => _.concat(feats, registry[cmd].feat), [])
|
||||
.map(feat => ` ${feat}`);
|
||||
return this.reply(211, 'Extensions supported', ...features, 'END');
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
const registry = require('./registry');
|
||||
const directive = _.upperCase(command._[1]);
|
||||
if (directive) {
|
||||
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
|
||||
|
||||
const {syntax, help, obsolete} = registry[directive];
|
||||
const reply = _.concat([syntax, help, obsolete ? 'Obsolete' : null]);
|
||||
return this.reply(214, ...reply);
|
||||
} else {
|
||||
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.');
|
||||
}
|
||||
};
|
||||
@@ -1,29 +1,52 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
|
||||
const REGISTRY = require('./registry');
|
||||
|
||||
class FtpCommands {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
this.registry = require('./registry');
|
||||
this.previousCommand = {};
|
||||
this.disabledCommands = _.get(this.connection, 'server.options.disabled_commands', []).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 [directive, ...args] = message.replace(/"/g, '').split(' ');
|
||||
const command = {
|
||||
directive: _.chain(directive).trim().toUpper().value(),
|
||||
arg: _.compact(args).join(' ') || null,
|
||||
raw: message
|
||||
};
|
||||
return command;
|
||||
}
|
||||
|
||||
handle(command) {
|
||||
const log = this.connection.log.child({command});
|
||||
log.trace('Handle command');
|
||||
if (typeof command === 'string') command = this.parse(command);
|
||||
|
||||
if (!this.registry.hasOwnProperty(command.directive)) {
|
||||
// Obfuscate password from logs
|
||||
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.disabledCommands, command.directive)) {
|
||||
return this.connection.reply(502, 'Command forbidden');
|
||||
if (_.includes(this.blacklist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command blacklisted');
|
||||
}
|
||||
|
||||
const commandRegister = this.registry[command.directive];
|
||||
if (!commandRegister.no_auth && !this.connection.authenticated) {
|
||||
return this.connection.reply(530);
|
||||
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command not whitelisted');
|
||||
}
|
||||
|
||||
const commandRegister = REGISTRY[command.directive];
|
||||
const commandFlags = _.get(commandRegister, 'flags', {});
|
||||
if (!commandFlags.no_auth && !this.connection.authenticated) {
|
||||
return this.connection.reply(530, 'Command requires authentication');
|
||||
}
|
||||
|
||||
if (!commandRegister.handler) {
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const getFileStat = require('../helpers/file-stat');
|
||||
|
||||
// http://cr.yp.to/ftp/list.html
|
||||
// http://cr.yp.to/ftp/list/eplf.html
|
||||
module.exports = function ({log, command, previous_command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const simple = command.directive === 'NLST';
|
||||
|
||||
let dataSocket;
|
||||
const directory = command._[1] || '.';
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.list(directory)))
|
||||
.then(files => {
|
||||
const getFileMessage = (file) => {
|
||||
if (simple) return file.name;
|
||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
};
|
||||
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileMessage(file);
|
||||
return {
|
||||
raw: true,
|
||||
message,
|
||||
socket: dataSocket
|
||||
};
|
||||
})
|
||||
return this.reply(150)
|
||||
.then(() => this.reply(...fileList));
|
||||
})
|
||||
.then(() => {
|
||||
return this.reply(226, 'Transfer OK');
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(err.code || 451, err.message || 'No directory');
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
const format = require('date-fns/format');
|
||||
|
||||
module.exports = 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 when(this.fs.get(command._[1]))
|
||||
.then(fileStat => {
|
||||
const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS');
|
||||
return this.reply(213, modificationTime)
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../helpers/escape-path');
|
||||
|
||||
module.exports = 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 when(this.fs.mkdir(command._[1]))
|
||||
.then(dir => {
|
||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function ({command} = {}) {
|
||||
return this.reply(command._[1] === 'S' ? 200 : 504);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.reply(200);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.reply(501);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.username) return this.reply(503);
|
||||
if (this.username && this.authenticated &&
|
||||
_.get(this, 'server.options.anonymous') === true) return this.reply(230);
|
||||
|
||||
// 332 : require account name (ACCT)
|
||||
|
||||
const password = command._[1];
|
||||
return this.login(this.username, password)
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
const PassiveConnector = require('../connector/passive');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then(server => {
|
||||
const address = this.server.url.hostname;
|
||||
const {port} = server.address();
|
||||
const host = address.replace(/\./g, ',');
|
||||
const portByte1 = port / 256 | 0;
|
||||
const portByte2 = port % 256;
|
||||
|
||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
const ActiveConnector = require('../connector/active');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
this.connector = new ActiveConnector(this);
|
||||
const rawConnection = command._[1].split(',');
|
||||
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 port = portBytes[0] * 256 + portBytes[1];
|
||||
|
||||
return this.connector.setupConnection(ip, port)
|
||||
.then(socket => {
|
||||
return this.reply(200);
|
||||
})
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../helpers/escape-path');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
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 when(this.fs.currentDirectory())
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.close(221);
|
||||
}
|
||||
14
src/commands/registration/abor.js
Normal file
14
src/commands/registration/abor.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
directive: 'ABOR',
|
||||
handler: function () {
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
return this.reply(426, {socket})
|
||||
.then(() => this.connector.end());
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => this.reply(226));
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Abort an active file transfer'
|
||||
};
|
||||
11
src/commands/registration/allo.js
Normal file
11
src/commands/registration/allo.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'ALLO',
|
||||
handler: function () {
|
||||
return this.reply(202);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Allocate sufficient disk space to receive a file',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
};
|
||||
10
src/commands/registration/appe.js
Normal file
10
src/commands/registration/appe.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const stor = require('./stor').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'APPE',
|
||||
handler: function (args) {
|
||||
return stor.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Append to a file'
|
||||
};
|
||||
27
src/commands/registration/auth.js
Normal file
27
src/commands/registration/auth.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'AUTH',
|
||||
handler: function ({command} = {}) {
|
||||
const method = _.upperCase(command.arg);
|
||||
|
||||
switch (method) {
|
||||
case 'TLS': return handleTLS.call(this);
|
||||
case 'SSL': return handleSSL.call(this);
|
||||
default: return this.reply(504);
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [type]',
|
||||
description: 'Set authentication mechanism',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
|
||||
function handleTLS() {
|
||||
return this.reply(504);
|
||||
}
|
||||
|
||||
function handleSSL() {
|
||||
return this.reply(504);
|
||||
}
|
||||
11
src/commands/registration/cdup.js
Normal file
11
src/commands/registration/cdup.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const cwd = require('./cwd').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: ['CDUP', 'XCUP'],
|
||||
handler: function (args) {
|
||||
args.command.arg = '..';
|
||||
return cwd.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Change to Parent Directory'
|
||||
};
|
||||
22
src/commands/registration/cwd.js
Normal file
22
src/commands/registration/cwd.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['CWD', 'XCWD'],
|
||||
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 when.try(this.fs.chdir.bind(this.fs), command.arg)
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(250, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}[path]',
|
||||
description: 'Change working directory'
|
||||
};
|
||||
20
src/commands/registration/dele.js
Normal file
20
src/commands/registration/dele.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'DELE',
|
||||
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 when.try(this.fs.delete.bind(this.fs), command.arg)
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Delete file'
|
||||
};
|
||||
21
src/commands/registration/feat.js
Normal file
21
src/commands/registration/feat.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'FEAT',
|
||||
handler: function () {
|
||||
const registry = require('../registry');
|
||||
const features = Object.keys(registry)
|
||||
.reduce((feats, cmd) => {
|
||||
const feat = _.get(registry[cmd], 'flags.feat', null);
|
||||
if (feat) return _.concat(feats, feat);
|
||||
return feats;
|
||||
}, [])
|
||||
.map(feat => ` ${feat}`);
|
||||
return this.reply(211, 'Extensions supported', ...features, 'END');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Get the feature list implemented by the server',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
24
src/commands/registration/help.js
Normal file
24
src/commands/registration/help.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'HELP',
|
||||
handler: function ({command} = {}) {
|
||||
const registry = require('../registry');
|
||||
const directive = _.upperCase(command.arg);
|
||||
if (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 this.reply(214, ...reply);
|
||||
} else {
|
||||
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(optional)]',
|
||||
description: 'Returns usage documentation on a command if specified, else a general help document is returned',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
60
src/commands/registration/list.js
Normal file
60
src/commands/registration/list.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const getFileStat = require('../../helpers/file-stat');
|
||||
|
||||
// http://cr.yp.to/ftp/list.html
|
||||
// http://cr.yp.to/ftp/list/eplf.html
|
||||
module.exports = {
|
||||
directive: 'LIST',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const simple = command.directive === 'NLST';
|
||||
|
||||
let dataSocket;
|
||||
const directory = command.arg || '.';
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when.try(this.fs.list.bind(this.fs), directory))
|
||||
.then(files => {
|
||||
const getFileMessage = file => {
|
||||
if (simple) return file.name;
|
||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
};
|
||||
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileMessage(file);
|
||||
return {
|
||||
raw: true,
|
||||
message,
|
||||
socket: dataSocket
|
||||
};
|
||||
});
|
||||
return this.reply(150)
|
||||
.then(() => {
|
||||
if (fileList.length) return this.reply({}, ...fileList);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return this.reply(226, 'Transfer OK');
|
||||
})
|
||||
.catch(when.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(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
|
||||
};
|
||||
25
src/commands/registration/mdtm.js
Normal file
25
src/commands/registration/mdtm.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const when = require('when');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = {
|
||||
directive: 'MDTM',
|
||||
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 when.try(this.fs.get.bind(this.fs), command.arg)
|
||||
.then(fileStat => {
|
||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
||||
return this.reply(213, modificationTime);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Return the last-modified time of a specified file',
|
||||
flags: {
|
||||
feat: 'MDTM'
|
||||
}
|
||||
};
|
||||
22
src/commands/registration/mkd.js
Normal file
22
src/commands/registration/mkd.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['MKD', 'XMKD'],
|
||||
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 when.try(this.fs.mkdir.bind(this.fs), command.arg)
|
||||
.then(dir => {
|
||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}[path]',
|
||||
description: 'Make directory'
|
||||
};
|
||||
11
src/commands/registration/mode.js
Normal file
11
src/commands/registration/mode.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'MODE',
|
||||
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)',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
};
|
||||
10
src/commands/registration/nlst.js
Normal file
10
src/commands/registration/nlst.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const list = require('./list').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'NLST',
|
||||
handler: function (args) {
|
||||
return list.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
description: 'Returns a list of file names in a specified directory'
|
||||
};
|
||||
11
src/commands/registration/noop.js
Normal file
11
src/commands/registration/noop.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'NOOP',
|
||||
handler: function () {
|
||||
return this.reply(200);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'No operation',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
8
src/commands/registration/opts.js
Normal file
8
src/commands/registration/opts.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
directive: 'OPTS',
|
||||
handler: function () {
|
||||
return this.reply(501);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Select options for a feature'
|
||||
};
|
||||
27
src/commands/registration/pass.js
Normal file
27
src/commands/registration/pass.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASS',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.username) return this.reply(503);
|
||||
if (this.username && this.authenticated &&
|
||||
_.get(this, 'server.options.anonymous') === true) return this.reply(230);
|
||||
|
||||
// 332 : require account name (ACCT)
|
||||
|
||||
const password = command.arg;
|
||||
return this.login(this.username, password)
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [password]',
|
||||
description: 'Authentication password',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
20
src/commands/registration/pasv.js
Normal file
20
src/commands/registration/pasv.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASV',
|
||||
handler: function () {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then(server => {
|
||||
const address = this.server.url.hostname;
|
||||
const {port} = server.address();
|
||||
const host = address.replace(/\./g, ',');
|
||||
const portByte1 = port / 256 | 0;
|
||||
const portByte2 = port % 256;
|
||||
|
||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Initiate passive mode'
|
||||
};
|
||||
20
src/commands/registration/port.js
Normal file
20
src/commands/registration/port.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const _ = require('lodash');
|
||||
const ActiveConnector = require('../../connector/active');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PORT',
|
||||
handler: function ({command} = {}) {
|
||||
this.connector = new ActiveConnector(this);
|
||||
const rawConnection = _.get(command, 'arg', '').split(',');
|
||||
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 port = portBytes[0] * 256 + portBytes[1];
|
||||
|
||||
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'
|
||||
};
|
||||
22
src/commands/registration/pwd.js
Normal file
22
src/commands/registration/pwd.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['PWD', 'XPWD'],
|
||||
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 when.try(this.fs.currentDirectory.bind(this.fs))
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Print current working directory'
|
||||
};
|
||||
11
src/commands/registration/quit.js
Normal file
11
src/commands/registration/quit.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'QUIT',
|
||||
handler: function () {
|
||||
return this.close(221);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Disconnect',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
41
src/commands/registration/retr.js
Normal file
41
src/commands/registration/retr.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RETR',
|
||||
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');
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when.try(this.fs.read.bind(this.fs), command.arg))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
dataSocket.on('error', err => stream.emit('error', err));
|
||||
|
||||
stream.on('data', data => dataSocket.write(data, this.encoding));
|
||||
stream.on('end', () => resolve(this.reply(226)));
|
||||
stream.on('error', err => reject(err));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
});
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(551, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Retrieve a copy of the file'
|
||||
};
|
||||
10
src/commands/registration/rmd.js
Normal file
10
src/commands/registration/rmd.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const dele = require('./dele').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: ['RMD', 'XRMD'],
|
||||
handler: function (args) {
|
||||
return dele.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Remove a directory'
|
||||
};
|
||||
22
src/commands/registration/rnfr.js
Normal file
22
src/commands/registration/rnfr.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNFR',
|
||||
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 when.try(this.fs.get.bind(this.fs), fileName)
|
||||
.then(() => {
|
||||
this.renameFrom = fileName;
|
||||
return this.reply(350);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [name]',
|
||||
description: 'Rename from'
|
||||
};
|
||||
28
src/commands/registration/rnto.js
Normal file
28
src/commands/registration/rnto.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNTO',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.renameFrom) return this.reply(503);
|
||||
|
||||
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 = this.renameFrom;
|
||||
const to = command.arg;
|
||||
|
||||
return when.try(this.fs.rename.bind(this.fs), from, to)
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.renameFrom;
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [name]',
|
||||
description: 'Rename to'
|
||||
};
|
||||
@@ -1,14 +1,17 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const [, mode, fileName] = command._;
|
||||
return this.fs.chmod(fileName, parseInt(mode, 8))
|
||||
const [mode, ...fileNameParts] = command.arg.split(' ');
|
||||
const fileName = fileNameParts.join(' ');
|
||||
return when.try(this.fs.chmod.bind(this.fs), fileName, parseInt(mode, 8))
|
||||
.then(() => {
|
||||
return this.reply(200);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(500);
|
||||
})
|
||||
});
|
||||
};
|
||||
17
src/commands/registration/site/index.js
Normal file
17
src/commands/registration/site/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'SITE',
|
||||
handler: function ({log, command} = {}) {
|
||||
const registry = require('./registry');
|
||||
const subCommand = this.commands.parse(command.arg);
|
||||
const subLog = log.child({subverb: subCommand.directive});
|
||||
|
||||
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
|
||||
|
||||
const handler = registry[subCommand.directive].handler.bind(this);
|
||||
return when.try(handler, { log: subLog, command: subCommand });
|
||||
},
|
||||
syntax: '{{cmd}} [subVerb] [subParams]',
|
||||
description: 'Sends site specific commands to remote server'
|
||||
};
|
||||
23
src/commands/registration/size.js
Normal file
23
src/commands/registration/size.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'SIZE',
|
||||
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 when.try(this.fs.get.bind(this.fs), command.arg)
|
||||
.then(fileStat => {
|
||||
return this.reply(213, {message: fileStat.size});
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Return the size of a file',
|
||||
flags: {
|
||||
feat: 'SIZE'
|
||||
}
|
||||
};
|
||||
44
src/commands/registration/stat.js
Normal file
44
src/commands/registration/stat.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const getFileStat = require('../../helpers/file-stat');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STAT',
|
||||
handler: function (args = {}) {
|
||||
const {log, command} = args;
|
||||
const path = _.get(command, 'arg');
|
||||
if (path) {
|
||||
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 when.try(this.fs.get.bind(this.fs), path)
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) {
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.list.bind(this.fs), path)
|
||||
.then(files => {
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
return {
|
||||
raw: true,
|
||||
message
|
||||
};
|
||||
});
|
||||
return this.reply(213, 'Status begin', ...fileList, 'Status end');
|
||||
});
|
||||
} else {
|
||||
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(450, err.message);
|
||||
});
|
||||
} else {
|
||||
return this.reply(211, 'Status OK');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
description: 'Returns the current status'
|
||||
};
|
||||
44
src/commands/registration/stor.js
Normal file
44
src/commands/registration/stor.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOR',
|
||||
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;
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append}))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
stream.on('error', err => dataSocket.emit('error', err));
|
||||
|
||||
dataSocket.on('end', () => stream.end(() => resolve(this.reply(226, fileName))));
|
||||
dataSocket.on('error', err => reject(err));
|
||||
dataSocket.on('data', data => stream.write(data, this.encoding));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
});
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Store data as a file at the server site'
|
||||
};
|
||||
24
src/commands/registration/stou.js
Normal file
24
src/commands/registration/stou.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const when = require('when');
|
||||
|
||||
const stor = require('./stor').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOU',
|
||||
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 = args.command.arg;
|
||||
return when.try(() => {
|
||||
return when.try(this.fs.get.bind(this.fs), fileName)
|
||||
.then(() => when.try(this.fs.getUniqueName.bind(this.fs)))
|
||||
.catch(() => when.resolve(fileName));
|
||||
})
|
||||
.then(name => {
|
||||
args.command.arg = name;
|
||||
return stor.call(this, args);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Store file uniquely'
|
||||
};
|
||||
11
src/commands/registration/stru.js
Normal file
11
src/commands/registration/stru.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'STRU',
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(/^F$/i.test(command.arg) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} [structure]',
|
||||
description: 'Set file transfer structure',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
};
|
||||
11
src/commands/registration/syst.js
Normal file
11
src/commands/registration/syst.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'SYST',
|
||||
handler: function () {
|
||||
return this.reply(215);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Return system type',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
20
src/commands/registration/type.js
Normal file
20
src/commands/registration/type.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const ENCODING_TYPES = {
|
||||
A: 'utf-8',
|
||||
I: 'binary',
|
||||
L: 'binary'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
directive: 'TYPE',
|
||||
handler: function ({command} = {}) {
|
||||
const encoding = _.upperCase(command.arg);
|
||||
if (!ENCODING_TYPES.hasOwnProperty(encoding)) return this.reply(501);
|
||||
|
||||
this.encoding = ENCODING_TYPES[encoding];
|
||||
return this.reply(200);
|
||||
},
|
||||
syntax: '{{cmd}} [mode]',
|
||||
description: 'Set the transfer mode, binary (I) or utf-8 (A)'
|
||||
};
|
||||
27
src/commands/registration/user.js
Normal file
27
src/commands/registration/user.js
Normal file
@@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
directive: 'USER',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (this.username) return this.reply(530, 'Username already set');
|
||||
|
||||
this.username = command.arg;
|
||||
if (!this.username) return this.reply(501, 'Must send username requirement');
|
||||
|
||||
|
||||
if (this.server.options.anonymous === true) {
|
||||
return this.login(this.username, '@anonymous')
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
}
|
||||
return this.reply(331);
|
||||
},
|
||||
syntax: '{{cmd}} [username]',
|
||||
description: 'Authentication username',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
@@ -1,200 +1,45 @@
|
||||
module.exports = {
|
||||
AUTH: {
|
||||
handler: require('./auth'),
|
||||
syntax: 'AUTH [type]',
|
||||
help: 'Not supported',
|
||||
no_auth: true
|
||||
},
|
||||
USER: {
|
||||
handler: require('./user'),
|
||||
syntax: 'USER [username]',
|
||||
help: 'Authentication username',
|
||||
no_auth: true
|
||||
},
|
||||
PASS: {
|
||||
handler: require('./pass'),
|
||||
syntax: 'PASS [password]',
|
||||
help: 'Authentication password',
|
||||
no_auth: true
|
||||
},
|
||||
SYST: {
|
||||
handler: require('./syst'),
|
||||
syntax: 'SYST',
|
||||
help: 'Return system type',
|
||||
no_auth: true
|
||||
},
|
||||
FEAT: {
|
||||
handler: require('./feat'),
|
||||
syntax: 'FEAT',
|
||||
help: 'Get the feature list implemented by the server',
|
||||
no_auth: true
|
||||
},
|
||||
PWD: {
|
||||
handler: require('./pwd'),
|
||||
syntax: 'PWD',
|
||||
help: 'Print current working directory'
|
||||
},
|
||||
XPWD: {
|
||||
handler: require('./pwd'),
|
||||
syntax: 'XPWD',
|
||||
help: 'Print current working directory'
|
||||
},
|
||||
TYPE: {
|
||||
handler: require('./type'),
|
||||
syntax: 'TYPE',
|
||||
help: 'Set the transfer mode'
|
||||
},
|
||||
PASV: {
|
||||
handler: require('./pasv'),
|
||||
syntax: 'PASV',
|
||||
help: 'Initiate passive mode'
|
||||
},
|
||||
PORT: {
|
||||
handler: require('./port'),
|
||||
syntax: 'PORT [x,x,x,x,y,y]',
|
||||
help: 'Specifies an address and port to which the server should connect'
|
||||
},
|
||||
LIST: {
|
||||
handler: require('./list'),
|
||||
syntax: 'LIST [path(optional)]',
|
||||
help: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
|
||||
},
|
||||
NLST: {
|
||||
handler: require('./list'),
|
||||
syntax: 'NLST [path(optional)]',
|
||||
help: 'Returns a list of file names in a specified directory'
|
||||
},
|
||||
CWD: {
|
||||
handler: require('./cwd'),
|
||||
syntax: 'CWD [path]',
|
||||
help: 'Change working directory'
|
||||
},
|
||||
XCWD: {
|
||||
handler: require('./cwd'),
|
||||
syntax: 'XCWD [path]',
|
||||
help: 'Change working directory'
|
||||
},
|
||||
CDUP: {
|
||||
handler: require('./cdup'),
|
||||
syntax: 'CDUP',
|
||||
help: 'Change to Parent Directory'
|
||||
},
|
||||
XCUP: {
|
||||
handler: require('./cdup'),
|
||||
syntax: 'XCUP',
|
||||
help: 'Change to Parent Directory'
|
||||
},
|
||||
STOR: {
|
||||
handler: require('./stor'),
|
||||
syntax: 'STOR [path]',
|
||||
help: 'Accept the data and to store the data as a file at the server site'
|
||||
},
|
||||
APPE: {
|
||||
handler: require('./stor'),
|
||||
syntax: 'APPE [path]',
|
||||
help: 'Append to file'
|
||||
},
|
||||
RETR: {
|
||||
handler: require('./retr'),
|
||||
syntax: 'RETR [path]',
|
||||
help: 'Retrieve a copy of the file'
|
||||
},
|
||||
DELE: {
|
||||
handler: require('./dele'),
|
||||
syntax: 'DELE [path]',
|
||||
help: 'Delete file'
|
||||
},
|
||||
RMD: {
|
||||
handler: require('./dele'),
|
||||
syntax: 'RMD [path]',
|
||||
help: 'Remove a directory'
|
||||
},
|
||||
XRMD: {
|
||||
handler: require('./dele'),
|
||||
syntax: 'XRMD [path]',
|
||||
help: 'Remove a directory'
|
||||
},
|
||||
HELP: {
|
||||
handler: require('./help'),
|
||||
syntax: 'HELP [command(optional)]',
|
||||
help: 'Returns usage documentation on a command if specified, else a general help document is returned'
|
||||
},
|
||||
MDTM: {
|
||||
handler: require('./mdtm'),
|
||||
syntax: 'MDTM [path]',
|
||||
help: 'Return the last-modified time of a specified file',
|
||||
feat: 'MDTM'
|
||||
},
|
||||
MKD: {
|
||||
handler: require('./mkd'),
|
||||
syntax: 'MKD [path]',
|
||||
help: 'Make directory'
|
||||
},
|
||||
XMKD: {
|
||||
handler: require('./mkd'),
|
||||
syntax: 'XMKD [path]',
|
||||
help: 'Make directory'
|
||||
},
|
||||
NOOP: {
|
||||
handler: require('./noop'),
|
||||
syntax: 'NOOP',
|
||||
help: 'No operation',
|
||||
no_auth: true
|
||||
},
|
||||
QUIT: {
|
||||
handler: require('./quit'),
|
||||
syntax: 'QUIT',
|
||||
help: 'Disconnect',
|
||||
no_auth: true
|
||||
},
|
||||
RNFR: {
|
||||
handler: require('./rnfr'),
|
||||
syntax: 'RNFR [name]',
|
||||
help: 'Rename from'
|
||||
},
|
||||
RNTO: {
|
||||
handler: require('./rnto'),
|
||||
syntax: 'RNTO [name]',
|
||||
help: 'Rename to'
|
||||
},
|
||||
SIZE: {
|
||||
handler: require('./size'),
|
||||
syntax: 'SIZE [path]',
|
||||
help: 'Return the size of a file',
|
||||
feat: 'SIZE'
|
||||
},
|
||||
STAT: {
|
||||
handler: require('./stat'),
|
||||
syntax: 'SIZE [path(optional)]',
|
||||
help: 'Returns the current status'
|
||||
},
|
||||
SITE: {
|
||||
handler: require('./site'),
|
||||
syntax: 'SITE [subVerb] [subParams]',
|
||||
help: 'Sends site specific commands to remote server'
|
||||
},
|
||||
OPTS: {
|
||||
handler: require('./opts'),
|
||||
syntax: 'OPTS',
|
||||
help: 'Select options for a feature'
|
||||
},
|
||||
/* eslint no-return-assign: 0 */
|
||||
const commands = [
|
||||
require('./registration/abor'),
|
||||
require('./registration/allo'),
|
||||
require('./registration/appe'),
|
||||
require('./registration/auth'),
|
||||
require('./registration/cdup'),
|
||||
require('./registration/cwd'),
|
||||
require('./registration/dele'),
|
||||
require('./registration/feat'),
|
||||
require('./registration/help'),
|
||||
require('./registration/list'),
|
||||
require('./registration/mdtm'),
|
||||
require('./registration/mkd'),
|
||||
require('./registration/mode'),
|
||||
require('./registration/nlst'),
|
||||
require('./registration/noop'),
|
||||
require('./registration/opts'),
|
||||
require('./registration/pass'),
|
||||
require('./registration/pasv'),
|
||||
require('./registration/port'),
|
||||
require('./registration/pwd'),
|
||||
require('./registration/quit'),
|
||||
require('./registration/retr'),
|
||||
require('./registration/rmd'),
|
||||
require('./registration/rnfr'),
|
||||
require('./registration/rnto'),
|
||||
require('./registration/site'),
|
||||
require('./registration/size'),
|
||||
require('./registration/stat'),
|
||||
require('./registration/stor'),
|
||||
require('./registration/stou'),
|
||||
require('./registration/stru'),
|
||||
require('./registration/syst'),
|
||||
require('./registration/type'),
|
||||
require('./registration/user')
|
||||
];
|
||||
|
||||
STRU: {
|
||||
handler: require('./stru'),
|
||||
syntax: 'STRU [structure]',
|
||||
help: 'Set file transfer structure',
|
||||
obsolete: true
|
||||
},
|
||||
ALLO: {
|
||||
handler: require('./allo'),
|
||||
syntax: 'ALLO',
|
||||
help: 'Allocate sufficient disk space to receive a file',
|
||||
obsolete: true
|
||||
},
|
||||
MODE: {
|
||||
handler: require('./mode'),
|
||||
syntax: 'MODE [mode]',
|
||||
help: 'Sets the transfer mode (Stream, Block, or Compressed)',
|
||||
obsolete: true
|
||||
}
|
||||
};
|
||||
const registry = commands.reduce((result, cmd) => {
|
||||
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
|
||||
aliases.forEach(alias => result[alias] = cmd);
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
module.exports = registry;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = 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');
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.read(command._[1])))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
dataSocket.on('error', err => stream.emit('error', err));
|
||||
|
||||
stream.on('data', data => dataSocket.write(data, this.encoding));
|
||||
stream.on('end', () => resolve(this.reply(226)));
|
||||
stream.on('error', err => reject(err));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
});
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(551);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = 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._[1];
|
||||
return when(this.fs.get(fileName))
|
||||
.then(() => {
|
||||
this.renameFrom = fileName;
|
||||
return this.reply(350);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.renameFrom) return this.reply(503);
|
||||
|
||||
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 = this.renameFrom;
|
||||
const to = command._[1];
|
||||
|
||||
return when(this.fs.rename(from, to))
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.renameFrom;
|
||||
});
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const registry = require('./registry');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
let [, subverb, ...subparameters] = command._;
|
||||
subverb = _.upperCase(subverb);
|
||||
const subLog = log.child({subverb});
|
||||
|
||||
if (!registry.hasOwnProperty(subverb)) return this.reply(502);
|
||||
|
||||
const subCommand = {
|
||||
_: [subverb, ...subparameters],
|
||||
directive: subverb
|
||||
}
|
||||
const handler = registry[subverb].handler.bind(this);
|
||||
return when.try(handler, { log: subLog, command: subCommand });
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = 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 when(this.fs.get(command._[1]))
|
||||
.then(fileStat => {
|
||||
return this.reply(213, {message: fileStat.size});
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const getFileStat = require('../helpers/file-stat');
|
||||
|
||||
module.exports = function (args = {}) {
|
||||
const {log, command} = args;
|
||||
const path = command._[1];
|
||||
if (path) {
|
||||
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 when(this.fs.get(path))
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) {
|
||||
return when(this.fs.list(path))
|
||||
.then(files => {
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
return {
|
||||
raw: true,
|
||||
message
|
||||
};
|
||||
})
|
||||
return this.reply(213, 'Status begin', ...fileList, 'Status end');
|
||||
})
|
||||
} else {
|
||||
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')))
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(450);
|
||||
})
|
||||
} else {
|
||||
return this.reply(211, 'Status OK');
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = 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';
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.write(command._[1], {append})))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
stream.on('error', err => dataSocket.emit('error', err));
|
||||
|
||||
dataSocket.on('end', () => resolve(this.reply(226)));
|
||||
dataSocket.on('error', err => reject(err));
|
||||
dataSocket.on('data', data => stream.write(data, this.encoding));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
});
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(553);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function ({command} = {}) {
|
||||
return this.reply(command._[1] === 'F' ? 200 : 504);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = function () {
|
||||
return this.reply(215);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function ({command} = {}) {
|
||||
const encoding = _.upperCase(command._[1]);
|
||||
switch (encoding) {
|
||||
case 'A':
|
||||
this.encoding = 'utf-8';
|
||||
case 'I':
|
||||
case 'L':
|
||||
this.encoding = 'binary';
|
||||
return this.reply(200);
|
||||
default:
|
||||
return this.reply(501);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (this.username) return this.reply(530, 'Username already set');
|
||||
this.username = command._[1];
|
||||
if (this.server.options.anonymous === true) {
|
||||
return this.login(this.username, '@anonymous')
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err || 'Authentication failed');
|
||||
});
|
||||
}
|
||||
return this.reply(331);
|
||||
};
|
||||
@@ -2,8 +2,6 @@ const _ = require('lodash');
|
||||
const uuid = require('uuid');
|
||||
const when = require('when');
|
||||
const sequence = require('when/sequence');
|
||||
const parseSentence = require('minimist-string');
|
||||
const net = require('net');
|
||||
|
||||
const BaseConnector = require('./connector/base');
|
||||
const FileSystem = require('./fs');
|
||||
@@ -14,43 +12,45 @@ const DEFAULT_MESSAGE = require('./messages');
|
||||
class FtpConnection {
|
||||
constructor(server, options) {
|
||||
this.server = server;
|
||||
this.commandSocket = options.socket;
|
||||
this.id = uuid.v4();
|
||||
this.log = options.log.child({ftp_session_id: this.commandSocket.ftp_session_id});
|
||||
this.log = options.log.child({id: this.id, ip: this.ip});
|
||||
this.commands = new Commands(this);
|
||||
this.encoding = 'utf-8';
|
||||
|
||||
this.connector = new BaseConnector(this);
|
||||
|
||||
this.commandSocket = options.socket;
|
||||
this.commandSocket.on('error', err => {
|
||||
console.log('error', err)
|
||||
});
|
||||
this.commandSocket.on('data', data => {
|
||||
const messages = _.compact(data.toString('utf-8').split('\r\n'));
|
||||
const handleMessage = (message) => {
|
||||
const command = parseSentence(message);
|
||||
command.directive = _.upperCase(command._[0]);
|
||||
return this.commands.handle(command);
|
||||
};
|
||||
|
||||
return sequence(messages.map(message => handleMessage.bind(this, message)));
|
||||
});
|
||||
this.commandSocket.on('timeout', () => {
|
||||
console.log('timeout')
|
||||
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', () => {});
|
||||
this.commandSocket.on('close', () => {
|
||||
if (this.connector) this.connector.end();
|
||||
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
_handleData(data) {
|
||||
const messages = _.compact(data.toString('utf-8').split('\r\n'));
|
||||
this.log.trace(messages, 'Messages');
|
||||
return sequence(messages.map(message => this.commands.handle.bind(this.commands, message)));
|
||||
}
|
||||
|
||||
get ip() {
|
||||
try {
|
||||
return this.dataSocket ? this.dataSocket.remoteAddress : this.commandSocket.remoteAddress;
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
close(code = 421, message = 'Closing connection') {
|
||||
return when(() => {
|
||||
if (code) return this.reply(code, message);
|
||||
})
|
||||
.then(() => {
|
||||
if (this.commandSocket) this.commandSocket.end();
|
||||
});
|
||||
return when
|
||||
.resolve(code)
|
||||
.then(_code => _code && this.reply(_code, message))
|
||||
.then(() => this.commandSocket && this.commandSocket.end());
|
||||
}
|
||||
|
||||
login(username, password) {
|
||||
@@ -59,12 +59,14 @@ class FtpConnection {
|
||||
if (!loginListeners || !loginListeners.length) {
|
||||
if (!this.server.options.anoymous) throw new errors.GeneralError('No "login" listener setup', 500);
|
||||
} else {
|
||||
return this.server.emit('login', {connection: this, username, password});
|
||||
return this.server.emitPromise('login', {connection: this, username, password});
|
||||
}
|
||||
})
|
||||
.then(({fs, cwd} = {}) => {
|
||||
.then(({root, cwd, fs, blacklist = [], whitelist = []} = {}) => {
|
||||
this.authenticated = true;
|
||||
this.fs = fs || new FileSystem(this, {cwd});
|
||||
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist);
|
||||
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist);
|
||||
this.fs = fs || new FileSystem(this, {root, cwd});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ class FtpConnection {
|
||||
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
|
||||
if (!Array.isArray(letters)) letters = [letters];
|
||||
if (!letters.length) letters = [{}];
|
||||
return when.map(letters, (promise, index) => {
|
||||
return when.map(letters, promise => {
|
||||
return when(promise)
|
||||
.then(letter => {
|
||||
if (!letter) letter = {};
|
||||
@@ -86,16 +88,16 @@ class FtpConnection {
|
||||
.then(message => {
|
||||
letter.message = message;
|
||||
return letter;
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const processLetter = (letter, index) => {
|
||||
return when.promise((resolve, reject) => {
|
||||
const seperator = !options.hasOwnProperty('eol') ?
|
||||
(letters.length - 1 === index ? ' ' : '-') :
|
||||
(options.eol ? ' ' : '-');
|
||||
letters.length - 1 === index ? ' ' : '-' :
|
||||
options.eol ? ' ' : '-';
|
||||
const packet = !letter.raw ? _.compact([letter.code || options.code, letter.message]).join(seperator) : letter.message;
|
||||
|
||||
if (letter.socket && letter.socket.writable) {
|
||||
@@ -109,10 +111,10 @@ class FtpConnection {
|
||||
});
|
||||
} else reject(new errors.SocketError('Socket not writable'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return satisfyParameters()
|
||||
.then(letters => sequence(letters.map((letter, index) => processLetter.bind(this, letter, index))))
|
||||
.then(satisfiedLetters => sequence(satisfiedLetters.map((letter, index) => processLetter.bind(this, letter, index))))
|
||||
.catch(err => {
|
||||
this.log.error(err);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const net = require('net');
|
||||
const {Socket} = require('net');
|
||||
const when = require('when');
|
||||
const Connector = require('./base');
|
||||
|
||||
@@ -8,28 +8,29 @@ class Active extends Connector {
|
||||
this.type = 'active';
|
||||
}
|
||||
|
||||
waitForConnection() {
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
return when.iterate(
|
||||
() => {},
|
||||
() => this.dataSocket && this.dataSocket.connected,
|
||||
() => when().delay(250)
|
||||
).timeout(5000)
|
||||
() => when().delay(delay)
|
||||
).timeout(timeout)
|
||||
.then(() => this.dataSocket);
|
||||
}
|
||||
|
||||
setupConnection(host, port) {
|
||||
const closeExistingServer = () => this.dataSocket ?
|
||||
when(this.dataSocket.destroy()) :
|
||||
when.resolve()
|
||||
when.resolve();
|
||||
|
||||
return closeExistingServer()
|
||||
.then(() => {
|
||||
this.dataSocket = new net.Socket();
|
||||
this.dataSocket = new Socket();
|
||||
this.dataSocket.setEncoding(this.encoding);
|
||||
this.dataSocket.connect({ host, port }, () => {
|
||||
this.dataSocket.pause();
|
||||
this.dataSocket.connected = true;
|
||||
});
|
||||
this.dataSocket.on('error', err => this.server.emit('client-error', {connection: this, context: 'dataSocket', error: err}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,20 @@ const errors = require('../errors');
|
||||
class Connector {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
this.server = connection.server;
|
||||
this.log = connection.log;
|
||||
|
||||
this.dataSocket = null;
|
||||
this.dataServer = null;
|
||||
this.type = false;
|
||||
}
|
||||
|
||||
get log() {
|
||||
return this.connection.log;
|
||||
}
|
||||
|
||||
get server() {
|
||||
return this.connection.server;
|
||||
}
|
||||
|
||||
waitForConnection() {
|
||||
return when.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
||||
}
|
||||
@@ -21,8 +27,7 @@ class Connector {
|
||||
if (this.dataServer) this.dataServer.close();
|
||||
this.dataSocket = null;
|
||||
this.dataServer = null;
|
||||
|
||||
this.connection.connector = new Connector(this.connection);
|
||||
this.type = false;
|
||||
}
|
||||
}
|
||||
module.exports = Connector;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const net = require('net');
|
||||
const when = require('when');
|
||||
|
||||
const Connector = require('./base');
|
||||
const findPort = require('../helpers/find-port');
|
||||
const errors = require('../errors');
|
||||
@@ -10,30 +11,28 @@ class Passive extends Connector {
|
||||
this.type = 'passive';
|
||||
}
|
||||
|
||||
waitForConnection() {
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
if (!this.dataServer) {
|
||||
return when.reject(new errors.ConnectorError('Passive server not setup'));
|
||||
}
|
||||
return when.iterate(
|
||||
() => {},
|
||||
() => this.dataServer && this.dataServer.listening && this.dataSocket,
|
||||
() => when().delay(250)
|
||||
).timeout(5000)
|
||||
() => this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected,
|
||||
() => when().delay(delay)
|
||||
).timeout(timeout)
|
||||
.then(() => this.dataSocket);
|
||||
}
|
||||
|
||||
setupServer() {
|
||||
const closeExistingServer = () => this.dataServer ?
|
||||
when.promise(resolve => this.dataServer.close(() => resolve())) :
|
||||
when.resolve()
|
||||
when.resolve();
|
||||
|
||||
return closeExistingServer()
|
||||
.then(() => this.getPort())
|
||||
.then(port => {
|
||||
this.dataSocket = null;
|
||||
this.dataServer = net.createServer({pauseOnConnect: true});
|
||||
this.dataServer.maxConnections = 1;
|
||||
this.dataServer.on('connection', socket => {
|
||||
this.dataServer = net.createServer({ pauseOnConnect: true }, socket => {
|
||||
if (this.connection.commandSocket.remoteAddress !== socket.remoteAddress) {
|
||||
this.log.error({
|
||||
pasv_connection: socket.remoteAddress,
|
||||
@@ -44,18 +43,21 @@ class Passive extends Connector {
|
||||
return this.connection.reply(550, 'Remote addresses do not match')
|
||||
.finally(() => this.connection.close());
|
||||
}
|
||||
this.log.info({port}, 'Passive connection fulfilled.');
|
||||
this.log.debug({port}, 'Passive connection fulfilled.');
|
||||
|
||||
this.dataSocket = socket;
|
||||
this.dataSocket.connected = true;
|
||||
this.dataSocket.setEncoding(this.connection.encoding);
|
||||
this.dataSocket.on('data', data => {
|
||||
|
||||
});
|
||||
this.dataSocket.on('error', err => this.server.emit('client-error', {connection: this, context: 'dataSocket', error: err}));
|
||||
this.dataSocket.on('close', () => {
|
||||
this.log.debug('Passive connection closed');
|
||||
this.end();
|
||||
});
|
||||
});
|
||||
this.dataServer.maxConnections = 1;
|
||||
this.dataServer.on('error', err => this.server.emit('client-error', {connection: this, context: 'dataServer', error: err}));
|
||||
this.dataServer.on('close', () => {
|
||||
this.log.debug('Passive server closed');
|
||||
this.dataServer = null;
|
||||
});
|
||||
|
||||
@@ -78,7 +80,7 @@ class Passive extends Connector {
|
||||
[this.server.options.pasv_range];
|
||||
return findPort(min, max);
|
||||
} else return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
module.exports = Passive;
|
||||
|
||||
78
src/fs.js
78
src/fs.js
@@ -1,5 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const nodePath = require('path');
|
||||
const uuid = require('uuid');
|
||||
const when = require('when');
|
||||
const whenNode = require('when/node');
|
||||
const syncFs = require('fs');
|
||||
@@ -7,11 +8,22 @@ const fs = whenNode.liftAll(syncFs);
|
||||
const errors = require('./errors');
|
||||
|
||||
class FileSystem {
|
||||
constructor(connection, {
|
||||
cwd = '/'
|
||||
} = {}) {
|
||||
constructor(connection, { root, cwd } = {}) {
|
||||
this.connection = connection;
|
||||
this.cwd = cwd;
|
||||
this.cwd = cwd || nodePath.sep;
|
||||
this.root = 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);
|
||||
|
||||
return {
|
||||
serverPath,
|
||||
fsPath
|
||||
};
|
||||
}
|
||||
|
||||
currentDirectory() {
|
||||
@@ -19,17 +31,17 @@ class FileSystem {
|
||||
}
|
||||
|
||||
get(fileName) {
|
||||
const path = nodePath.resolve(this.cwd, fileName);
|
||||
return fs.stat(path)
|
||||
const {fsPath} = this._resolvePath(fileName);
|
||||
return fs.stat(fsPath)
|
||||
.then(stat => _.set(stat, 'name', fileName));
|
||||
}
|
||||
|
||||
list(path = '.') {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.readdir(path)
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.readdir(fsPath)
|
||||
.then(fileNames => {
|
||||
return when.map(fileNames, fileName => {
|
||||
const filePath = nodePath.join(path, fileName);
|
||||
const filePath = nodePath.join(fsPath, fileName);
|
||||
return fs.access(filePath, syncFs.constants.F_OK)
|
||||
.then(() => {
|
||||
return fs.stat(filePath)
|
||||
@@ -42,60 +54,64 @@ class FileSystem {
|
||||
}
|
||||
|
||||
chdir(path = '.') {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.stat(path)
|
||||
const {fsPath, serverPath} = this._resolvePath(path);
|
||||
return fs.stat(fsPath)
|
||||
.tap(stat => {
|
||||
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
|
||||
})
|
||||
.then(() => {
|
||||
this.cwd = path;
|
||||
return this.cwd;
|
||||
this.cwd = serverPath;
|
||||
return this.currentDirectory();
|
||||
});
|
||||
}
|
||||
|
||||
write(fileName, {append = false} = {}) {
|
||||
const path = nodePath.resolve(this.cwd, fileName);
|
||||
const stream = syncFs.createWriteStream(path, {flags: !append ? 'w+' : 'a+'});
|
||||
stream.on('error', () => fs.unlink(path));
|
||||
const {fsPath} = this._resolvePath(fileName);
|
||||
const stream = syncFs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+'});
|
||||
stream.on('error', () => fs.unlink(fsPath));
|
||||
return stream;
|
||||
}
|
||||
|
||||
read(fileName) {
|
||||
const path = nodePath.resolve(this.cwd, fileName);
|
||||
return fs.stat(path)
|
||||
const {fsPath} = this._resolvePath(fileName);
|
||||
return fs.stat(fsPath)
|
||||
.tap(stat => {
|
||||
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
|
||||
})
|
||||
.then(() => {
|
||||
const stream = syncFs.createReadStream(path, {flags: 'r'});
|
||||
const stream = syncFs.createReadStream(fsPath, {flags: 'r'});
|
||||
return stream;
|
||||
});
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.stat(path)
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.stat(fsPath)
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) return fs.rmdir(path);
|
||||
else return fs.unlink(path);
|
||||
})
|
||||
if (stat.isDirectory()) return fs.rmdir(fsPath);
|
||||
else return fs.unlink(fsPath);
|
||||
});
|
||||
}
|
||||
|
||||
mkdir(path) {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.mkdir(path)
|
||||
.then(() => path);
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.mkdir(fsPath)
|
||||
.then(() => fsPath);
|
||||
}
|
||||
|
||||
rename(from, to) {
|
||||
const fromPath = nodePath.resolve(this.cwd, from);
|
||||
const toPath = nodePath.resolve(this.cwd, to);
|
||||
const {fsPath: fromPath} = this._resolvePath(from);
|
||||
const {fsPath: toPath} = this._resolvePath(to);
|
||||
return fs.rename(fromPath, toPath);
|
||||
}
|
||||
|
||||
chmod(path, mode) {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.chmod(path, mode);
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.chmod(fsPath, mode);
|
||||
}
|
||||
|
||||
getUniqueName() {
|
||||
return uuid.v4().replace(/\W/g, '');
|
||||
}
|
||||
}
|
||||
module.exports = FileSystem;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = function (path) {
|
||||
return path
|
||||
.replace(/"/g, '""');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
const _ = require('lodash');
|
||||
const format = require('date-fns/format');
|
||||
const moment = require('moment');
|
||||
const errors = require('../errors');
|
||||
|
||||
const FORMATS = {
|
||||
ls,
|
||||
ep
|
||||
};
|
||||
|
||||
module.exports = function (fileStat, format = 'ls') {
|
||||
if (typeof format === 'function') return format(fileStat);
|
||||
|
||||
const formats = {
|
||||
'ls': ls,
|
||||
'ep': ep
|
||||
};
|
||||
if (!formats.hasOwnProperty(format)) {
|
||||
if (!FORMATS.hasOwnProperty(format)) {
|
||||
throw new errors.FileSystemError('Bad file stat formatter');
|
||||
}
|
||||
return formats[format](fileStat);
|
||||
}
|
||||
return FORMATS[format](fileStat);
|
||||
};
|
||||
|
||||
function ls(fileStat) {
|
||||
const now = moment.utc();
|
||||
const mtime = moment.utc(new Date(fileStat.mtime));
|
||||
const dateFormat = now.diff(mtime, 'months') < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
|
||||
|
||||
return [
|
||||
fileStat.mode !== null
|
||||
? [
|
||||
fileStat.isDirectory() ? 'd' : '-',
|
||||
400 & fileStat.mode ? 'r' : '-',
|
||||
200 & fileStat.mode ? 'w' : '-',
|
||||
100 & fileStat.mode ? 'x' : '-',
|
||||
40 & fileStat.mode ? 'r' : '-',
|
||||
20 & fileStat.mode ? 'w' : '-',
|
||||
10 & fileStat.mode ? 'x' : '-',
|
||||
4 & fileStat.mode ? 'r' : '-',
|
||||
2 & fileStat.mode ? 'w' : '-',
|
||||
1 & fileStat.mode ? 'x' : '-'
|
||||
].join('')
|
||||
: fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
|
||||
fileStat.mode ? [
|
||||
fileStat.isDirectory() ? 'd' : '-',
|
||||
fileStat.mode & 256 ? 'r' : '-',
|
||||
fileStat.mode & 128 ? 'w' : '-',
|
||||
fileStat.mode & 64 ? 'x' : '-',
|
||||
fileStat.mode & 32 ? 'r' : '-',
|
||||
fileStat.mode & 16 ? 'w' : '-',
|
||||
fileStat.mode & 8 ? 'x' : '-',
|
||||
fileStat.mode & 4 ? 'r' : '-',
|
||||
fileStat.mode & 2 ? 'w' : '-',
|
||||
fileStat.mode & 1 ? 'x' : '-'
|
||||
].join('') : fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
|
||||
'1',
|
||||
fileStat.uid,
|
||||
fileStat.gid,
|
||||
fileStat.uid || 1,
|
||||
fileStat.gid || 1,
|
||||
_.padStart(fileStat.size, 12),
|
||||
_.padStart(format(fileStat.mtime, 'MMM DD HH:mm'), 12),
|
||||
_.padStart(mtime.format(dateFormat), 12),
|
||||
fileStat.name
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function ep(fileStat) {
|
||||
const facts = [
|
||||
const facts = _.compact([
|
||||
fileStat.dev && fileStat.ino ? `i${fileStat.dev.toString(16)}.${fileStat.ino.toString(16)}` : null,
|
||||
fileStat.size ? `s${fileStat.size}` : null,
|
||||
fileStat.mtime ? `m${format(fileStat.mtime, 'X')}` : null,
|
||||
fileStat.mode ? `up${fileStat.mode.toString(8).substr(fileStat.mode.toString(8).length - 3)}` : null,
|
||||
fileStat.isDirectory() ? 'r' : '/'
|
||||
].join(',');
|
||||
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
|
||||
fileStat.mode ? `up${(fileStat.mode & 4095).toString(8)}` : null,
|
||||
fileStat.isDirectory() ? '/' : 'r'
|
||||
]).join(',');
|
||||
return `+${facts}\t${fileStat.name}`;
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@ const net = require('net');
|
||||
const when = require('when');
|
||||
const errors = require('../errors');
|
||||
|
||||
module.exports = function (min = 22, max = undefined) {
|
||||
module.exports = function (min = 1, max = undefined) {
|
||||
return when.promise((resolve, reject) => {
|
||||
let port = min;
|
||||
let checkPort = min;
|
||||
let portCheckServer = net.createServer();
|
||||
portCheckServer.maxConnections = 0;
|
||||
portCheckServer.on('error', () => {
|
||||
if (!max || port < max) {
|
||||
port = port + 1;
|
||||
portCheckServer.listen(port);
|
||||
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(() => {
|
||||
@@ -22,6 +22,6 @@ module.exports = function (min = 22, max = undefined) {
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
portCheckServer.listen(port);
|
||||
portCheckServer.listen(checkPort);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,5 +21,5 @@ module.exports = function (hostname) {
|
||||
});
|
||||
});
|
||||
} else resolve(hostname);
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
94
src/index.js
94
src/index.js
@@ -3,6 +3,8 @@ const when = require('when');
|
||||
const nodeUrl = require('url');
|
||||
const buyan = require('bunyan');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const fs = require('fs');
|
||||
|
||||
const Connection = require('./connection');
|
||||
const resolveHost = require('./helpers/resolve-host');
|
||||
@@ -10,32 +12,53 @@ const resolveHost = require('./helpers/resolve-host');
|
||||
class FtpServer {
|
||||
constructor(url, options = {}) {
|
||||
this.options = _.merge({
|
||||
log: buyan.createLogger({name: 'ftp-svr'}),
|
||||
log: buyan.createLogger({name: 'ftp-srv'}),
|
||||
anonymous: false,
|
||||
pasv_range: 22,
|
||||
file_format: 'ls',
|
||||
disabled_commands: []
|
||||
blacklist: [],
|
||||
whitelist: [],
|
||||
greeting: null,
|
||||
tls: {}
|
||||
}, options);
|
||||
this._greeting = this.setupGreeting(this.options.greeting);
|
||||
this._features = this.setupFeaturesMessage();
|
||||
this._tls = this.setupTLS(this.options.tls);
|
||||
|
||||
this.connections = {};
|
||||
this.log = this.options.log;
|
||||
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
|
||||
this.server = net.createServer({pauseOnConnect: true}, 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));
|
||||
|
||||
const greeting = this.getGreetingMessage();
|
||||
const features = this.getFeaturesMessage();
|
||||
return connection.reply(220, greeting, features)
|
||||
const greeting = this._greeting || [];
|
||||
const features = this._features || 'Ready';
|
||||
return connection.reply(220, ...greeting, features)
|
||||
.finally(() => socket.resume());
|
||||
});
|
||||
this.server.on('error', err => {
|
||||
this.log.error(err);
|
||||
});
|
||||
};
|
||||
const serverOptions = _.assign(this.isTLS ? this._tls : {}, { pauseOnConnect: true });
|
||||
|
||||
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
|
||||
this.server.on('error', err => this.log.error(err, '[Event] error'));
|
||||
if (this.isTLS) {
|
||||
this.server.on('tlsClientError', err => this.log.error(err, '[Event] tlsClientError'));
|
||||
}
|
||||
this.on = this.server.on.bind(this.server);
|
||||
this.once = this.server.once.bind(this.server);
|
||||
this.listeners = this.server.listeners.bind(this.server);
|
||||
|
||||
process.on('SIGTERM', () => this.close());
|
||||
process.on('SIGINT', () => this.close());
|
||||
process.on('SIGBREAK', () => this.close());
|
||||
process.on('SIGHUP', () => this.close());
|
||||
}
|
||||
|
||||
get isTLS() {
|
||||
return this.url.protocol === 'ftps:';
|
||||
}
|
||||
|
||||
listen() {
|
||||
@@ -45,60 +68,71 @@ class FtpServer {
|
||||
return when.promise((resolve, reject) => {
|
||||
this.server.listen(this.url.port, err => {
|
||||
if (err) return reject(err);
|
||||
this.log.info({port: this.url.port}, 'Listening');
|
||||
this.log.info({ip: this.url.hostname, port: this.url.port}, `Listening${this.isTLS ? ' (TLS)' : ''}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
emit(action, ...data) {
|
||||
emitPromise(action, ...data) {
|
||||
const defer = when.defer();
|
||||
const params = _.concat(data, [defer.resolve, defer.reject]);
|
||||
this.server.emit(action, ...params);
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
getGreetingMessage() {
|
||||
return null;
|
||||
emit(action, ...data) {
|
||||
this.server.emit(action, ...data);
|
||||
}
|
||||
|
||||
getFeaturesMessage() {
|
||||
setupTLS(_tls) {
|
||||
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');
|
||||
return greeting;
|
||||
}
|
||||
|
||||
setupFeaturesMessage() {
|
||||
let features = [];
|
||||
if (this.options.anonymous) features.push('a');
|
||||
|
||||
if (features.length) {
|
||||
features.unshift('Features:');
|
||||
features.push('.')
|
||||
features.push('.');
|
||||
}
|
||||
return features.length ? features.join(' ') : 'Ready';
|
||||
}
|
||||
|
||||
setGreeting(gretting) {
|
||||
if (typeof greeting === 'string') {
|
||||
this.options.greeting = greeting;
|
||||
} else {
|
||||
gretting.then(greeting => {
|
||||
this.options.gretting = greeting;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
disconnectClient(id) {
|
||||
return when.promise((resolve, reject) => {
|
||||
return when.promise(resolve => {
|
||||
const client = this.connections[id];
|
||||
if (!client) return resolve();
|
||||
delete this.connections[id];
|
||||
return client.close(0);
|
||||
try {
|
||||
client.close(0);
|
||||
} catch (err) {
|
||||
this.log.error(err, 'Error closing connection', {id});
|
||||
} finally {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.log.info('Server closing...');
|
||||
this.server.maxConnections = 0;
|
||||
return when.map(Object.keys(this.connections), id => this.disconnectClient(id))
|
||||
.then(() => when.promise((resolve, reject) => {
|
||||
.then(() => when.promise(resolve => {
|
||||
this.server.close(err => {
|
||||
if (err) return reject(err);
|
||||
if (err) this.log.error(err, 'Error closing server');
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
|
||||
12
test/cert/server.crt
Normal file
12
test/cert/server.crt
Normal file
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBtTCCAR4CCQDsFyLCxvy4qzANBgkqhkiG9w0BAQUFADAfMQswCQYDVQQGEwJD
|
||||
QTEQMA4GA1UECBMHQWxiZXJ0YTAeFw0xNzA1MDgyMzQzMjNaFw0xODA1MDgyMzQz
|
||||
MjNaMB8xCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdBbGJlcnRhMIGfMA0GCSqGSIb3
|
||||
DQEBAQUAA4GNADCBiQKBgQD2uOwjV7Nb7ubBsK/tHZQ5hUpaQ9QD9Jo8qj7DBOia
|
||||
C4xbwpF6w+ZDf5OnkR7Hl3QlcofbkXKkLWmG0Mm5wvFA6kYW6D8vMT5Di7+eksf/
|
||||
agkklBnRtdoBb3lsbMbo3/EXijwHbCKbm+sTTe7dwGK/w6p782K/kgHVWk+L58O7
|
||||
rQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAOkX32keFpo0kKQKpeZgxVYjvn4/Voy6
|
||||
6oLsj7jJYq3oZts1dX6kHVpLEbF9sWKB2iz7nqz7pSN1ATq0IL/5rcxvNwiL4Idv
|
||||
F8CCBvsBui+0gwX755NJK/L57a5i8yQ5HC65NujGAA4I5+2x8HlefMVuBpEYjzQ2
|
||||
6lW2OJJ8xtP/
|
||||
-----END CERTIFICATE-----
|
||||
10
test/cert/server.csr
Normal file
10
test/cert/server.csr
Normal file
@@ -0,0 +1,10 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBXjCByAIBADAfMQswCQYDVQQGEwJDQTEQMA4GA1UECBMHQWxiZXJ0YTCBnzAN
|
||||
BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA9rjsI1ezW+7mwbCv7R2UOYVKWkPUA/Sa
|
||||
PKo+wwTomguMW8KResPmQ3+Tp5Eex5d0JXKH25FypC1phtDJucLxQOpGFug/LzE+
|
||||
Q4u/npLH/2oJJJQZ0bXaAW95bGzG6N/xF4o8B2wim5vrE03u3cBiv8Oqe/Niv5IB
|
||||
1VpPi+fDu60CAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBACzo+Wecs3CTbItrugdL
|
||||
pP4crsRs+HJljWA0e+WEGKhcd1FjrcLBr4WqzHFQJWHOTz2vM5PiKXPZk9crLxWa
|
||||
Y8kMhU6eQPnCM6+7Gffm32+VS1ipNlhzHyYsjYpgC3ROElqo0J5M5sas4lbaamr+
|
||||
FnlyRjrPSUcFdcbPL6ozND3e
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
15
test/cert/server.key
Normal file
15
test/cert/server.key
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQD2uOwjV7Nb7ubBsK/tHZQ5hUpaQ9QD9Jo8qj7DBOiaC4xbwpF6
|
||||
w+ZDf5OnkR7Hl3QlcofbkXKkLWmG0Mm5wvFA6kYW6D8vMT5Di7+eksf/agkklBnR
|
||||
tdoBb3lsbMbo3/EXijwHbCKbm+sTTe7dwGK/w6p782K/kgHVWk+L58O7rQIDAQAB
|
||||
AoGANRPVYUkVwfpkVFkBj/5kC/fb5g1fiDZQFCr/846Tx8giOv9hssqAOBczGcKD
|
||||
n6a6iu/XwGnLAvzuDd3O+BKzObrKV36u9HfvCxohKaKvhPg3lBlJ5fFq/UNBoLv8
|
||||
eHz0GUGGoCxwJBAV43ojV1GdyRZ7vdmYw2hzltsHIp7UDqECQQD7yltCCJm3+gcw
|
||||
p+Sde0+M9CubkTETPwpd3XPu6Bs5f2nxNVj6RAInPEBr5//5UY7Q9BBEHrDPsq0j
|
||||
/+gsSlWZAkEA+tjf9qRXk9JHoN3PD0xLNEgUZAsQwDic29jxb3xrGkuUjCebKRuC
|
||||
FU2sAfNgDp+MyG1iyAoZcySzH2Dp3+v7NQJAaBwBo8oelT2in3GsS5ljCSskpMxh
|
||||
+E1Gog0hFJWQPDP8wCmIwuI/6a02Def9pT8dyDRCTYhLH3YHtSzo+Pc7cQJBAJ4G
|
||||
XiD8qwc+o00eLsEOaRoIhn/30JenknmVE5QOJ1KrZmtc0Ax3fd15zvBzp4HO1Vu2
|
||||
PVKTujClYApWfT9JZDkCQQC3Ne79bb5WSsGNbg4eT+FWde4hkdpheBsWraEDN1Pp
|
||||
NanupXMPNP0EduAQ1O+oPRiZ5pG38MQYcPZHTtlULoiO
|
||||
-----END RSA PRIVATE KEY-----
|
||||
18
test/cert/server.key.org
Normal file
18
test/cert/server.key.org
Normal file
@@ -0,0 +1,18 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,A253DC8068106194
|
||||
|
||||
jrYyhujaDsASrOfb2kn5Tvb0mRyIRsz7gVdjwlUdF2+lPdA/w6Os/NQBo1BIUAJp
|
||||
lfS5KDTwiE3QPgrBXUNgpy71Yr+MSzmsYWdonGlXGtchohQKXpxtL3qOpczX3ERR
|
||||
0AZninOKOYLw9+pe/tLNZI78DHxN1X0qTXS56RFydlW3XZbnl2Ux9CGuaCVq6vwh
|
||||
yzr9H+XTqeh95wTfdXkRdFRSTyUuJ72cvMsBFRDhz60epDmUDo1XDf844BpXXfcU
|
||||
kQoXHEtNkWZqzsc4ClOopp3Bgtd7eYoOLQluyovHgXzjtsur0xeMkHn9uTfkJ+IM
|
||||
cYMS71ZKbMePS7XBt3YPLBvVXNcyYhWUP7VdGYxXTPqd1AVWciDB9S5EvfMxnnZz
|
||||
O4M4ejxV8S7fF/cGju+sRzXx9oHPfo091Q3XKV1hrsUcF+ULrA8A8rHr64bDJ3wp
|
||||
luhekzwb+5yFfZDj9XUuGMD6pSWYoWB8Jmk8cxVsdZPtGXbTQHFL9/+UZ65wSGpj
|
||||
CjTLuFyVhY8pliynZH80vsNeRycdfmx93XoLqfS4xwEmI5v/MGUF24eTpF1/VIa5
|
||||
oKDrVuERdXAn4JBKeaMratrl6p1BhkPe7VNnMUFw3U+C4x+QHISxbboUJiTcCe1C
|
||||
pT6+YYkxQJ88rKunSEXkQYt6LeYSDg8Dw5y5Oq68DmW2Rp1m4ptTbqk3+uh83vzV
|
||||
Ff0JnfNfT80GHD3T5hMgizZal2vV8DeH1WAwPzpNaeV6wTy6MSgRLgQ89cCQ0TXV
|
||||
0GYnCZCaoXA2ldvbB3vW3fweOTr8Mp7aSl4s8K0R8sT3eief9/SyWA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
95
test/commands/index.spec.js
Normal file
95
test/commands/index.spec.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const {expect} = require('chai');
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const FtpCommands = require('../../src/commands');
|
||||
|
||||
describe('FtpCommands', function () {
|
||||
let sandbox;
|
||||
let commands;
|
||||
let mockConnection = {
|
||||
authenticated: false,
|
||||
log: bunyan.createLogger({name: 'FtpCommands'}),
|
||||
reply: () => when.resolve({}),
|
||||
server: {
|
||||
options: {
|
||||
blacklist: ['allo']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
commands = new FtpCommands(mockConnection);
|
||||
|
||||
sandbox.spy(mockConnection, 'reply');
|
||||
sandbox.spy(commands, 'handle');
|
||||
sandbox.spy(commands, 'parse');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('parse', function () {
|
||||
it('no args: test', () => {
|
||||
const cmd = commands.parse('test');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal(null);
|
||||
expect(cmd.raw).to.equal('test');
|
||||
});
|
||||
|
||||
it('one arg: test arg', () => {
|
||||
const cmd = commands.parse('test arg');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal('arg');
|
||||
expect(cmd.raw).to.equal('test arg');
|
||||
});
|
||||
|
||||
it('two args: test arg1 arg2', () => {
|
||||
const cmd = commands.parse('test arg1 arg2');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal('arg1 arg2');
|
||||
expect(cmd.raw).to.equal('test arg1 arg2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle', function () {
|
||||
it('fails with unsupported command', () => {
|
||||
return commands.handle('bad')
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with blacklisted command', () => {
|
||||
return commands.handle('allo')
|
||||
.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(/blacklisted/);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with non whitelisted command', () => {
|
||||
commands.whitelist.push('USER');
|
||||
return commands.handle('auth')
|
||||
.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(/whitelisted/);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to being unauthenticated', () => {
|
||||
return commands.handle('stor')
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(530);
|
||||
expect(mockConnection.reply.args[0][1]).to.match(/authentication/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
53
test/commands/registration/abor.spec.js
Normal file
53
test/commands/registration/abor.spec.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'ABOR';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve(),
|
||||
connector: {
|
||||
waitForConnection: () => when.resolve(),
|
||||
end: () => when.resolve()
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.spy(mockClient.connector, 'waitForConnection');
|
||||
sandbox.spy(mockClient.connector, 'end');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful | no active connection', done => {
|
||||
mockClient.connector.waitForConnection.restore();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').rejects();
|
||||
|
||||
cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||
expect(mockClient.connector.end.callCount).to.equal(0);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(226);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('// successful | active connection', done => {
|
||||
cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||
expect(mockClient.connector.end.callCount).to.equal(1);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(426);
|
||||
expect(mockClient.reply.args[1][0]).to.equal(226);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'ALLO';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -20,11 +20,11 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('// successful', done => {
|
||||
CMDFN()
|
||||
cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(202)
|
||||
expect(mockClient.reply.args[0][0]).to.equal(202);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'AUTH';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -20,27 +20,27 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('TLS // not supported', done => {
|
||||
CMDFN({command: {_: [CMD, 'TLS'], directive: CMD}})
|
||||
cmdFn({command: { arg: 'TLS', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504)
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('SSL // not supported', done => {
|
||||
CMDFN({command: {_: [CMD, 'SSL'], directive: CMD}})
|
||||
cmdFn({command: { arg: 'SSL', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504)
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('bad // bad', done => {
|
||||
CMDFN({command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
cmdFn({command: { arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504)
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
@@ -4,7 +4,7 @@ const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'CDUP';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
@@ -13,7 +13,7 @@ describe(CMD, done => {
|
||||
chdir: () => when.resolve()
|
||||
}
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -26,7 +26,7 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('.. // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
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,23 +1,21 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised');
|
||||
|
||||
const CMD = 'CWD';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { chdir: () => {} }
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves()
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'chdir').resolves();
|
||||
});
|
||||
afterEach(() => {
|
||||
@@ -27,9 +25,9 @@ describe(CMD, done => {
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient);
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
@@ -39,9 +37,9 @@ describe(CMD, done => {
|
||||
|
||||
it('fails on no fs chdir command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient);
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
@@ -51,7 +49,7 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('test // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
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');
|
||||
@@ -63,7 +61,7 @@ describe(CMD, done => {
|
||||
it('test // successful', done => {
|
||||
mockClient.fs.chdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
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');
|
||||
@@ -74,9 +72,9 @@ describe(CMD, done => {
|
||||
|
||||
it('bad // unsuccessful', done => {
|
||||
mockClient.fs.chdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'))
|
||||
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
|
||||
|
||||
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
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,18 +1,16 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised');
|
||||
|
||||
const CMD = 'DELE';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { delete: () => {} }
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -27,9 +25,9 @@ describe(CMD, done => {
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient);
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
@@ -39,9 +37,9 @@ describe(CMD, done => {
|
||||
|
||||
it('fails on no fs delete command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient);
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
@@ -51,7 +49,7 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('test // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
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');
|
||||
@@ -62,9 +60,9 @@ describe(CMD, done => {
|
||||
|
||||
it('bad // unsuccessful', done => {
|
||||
mockClient.fs.delete.restore();
|
||||
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'))
|
||||
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
|
||||
|
||||
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
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');
|
||||
@@ -1,14 +1,14 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'HELP';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -20,7 +20,7 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('// successful', done => {
|
||||
CMDFN({command: {_: [CMD], directive: CMD}})
|
||||
cmdFn({command: { directive: CMD }})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(211);
|
||||
done();
|
||||
@@ -29,7 +29,7 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('help // successful', done => {
|
||||
CMDFN({command: {_: [CMD, 'help'], directive: CMD}})
|
||||
cmdFn({command: { arg: 'help', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||
done();
|
||||
@@ -38,7 +38,7 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('help // successful', done => {
|
||||
CMDFN({command: {_: [CMD, 'allo'], directive: CMD}})
|
||||
cmdFn({command: { arg: 'allo', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||
done();
|
||||
@@ -47,7 +47,7 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('bad // unsuccessful', done => {
|
||||
CMDFN({command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
cmdFn({command: { arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(502);
|
||||
done();
|
||||
@@ -2,17 +2,16 @@ const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised')(when.Promise);
|
||||
|
||||
const CMD = 'LIST';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { list: () => {} },
|
||||
connector: {
|
||||
waitForConnection: () => {},
|
||||
waitForConnection: () => when({}),
|
||||
end: () => {}
|
||||
},
|
||||
commandSocket: {
|
||||
@@ -20,14 +19,12 @@ describe(CMD, done => {
|
||||
pause: () => {}
|
||||
}
|
||||
};
|
||||
const mockSocket = {};
|
||||
const CMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').resolves(mockSocket);
|
||||
sandbox.stub(mockClient.fs, 'list').resolves([{
|
||||
name: 'test1',
|
||||
dev: 2114,
|
||||
@@ -54,9 +51,9 @@ describe(CMD, done => {
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient);
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
@@ -66,9 +63,9 @@ describe(CMD, done => {
|
||||
|
||||
it('fails on no fs list command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/${CMD.toLowerCase()}`).bind(badMockClient);
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
@@ -78,12 +75,12 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('. // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('raw');
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('message');
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('socket');
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('raw');
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('message');
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('socket');
|
||||
expect(mockClient.reply.args[2][0]).to.equal(226);
|
||||
done();
|
||||
})
|
||||
@@ -94,7 +91,7 @@ describe(CMD, done => {
|
||||
mockClient.fs.list.restore();
|
||||
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
|
||||
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(451);
|
||||
done();
|
||||
@@ -103,10 +100,9 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
it('. // unsuccessful (timeout)', done => {
|
||||
mockClient.connector.waitForConnection.restore();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').rejects(new when.TimeoutError());
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').returns(when.reject(new when.TimeoutError()));
|
||||
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||
done();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user