Compare commits

..

2 Commits

Author SHA1 Message Date
Tyler Stewart
79438158f3 chore: version specific fixes 2020-08-17 14:22:13 -06:00
Tyler Stewart
fb32b012c3 fix: disallow PORT connections to alternate hosts
Ensure the data socket that the server connects to from the PORT command is the same IP as the current command socket.

* fix: add error handling to additional connection commands
2020-08-17 14:18:50 -06:00
102 changed files with 5433 additions and 10279 deletions

View File

@@ -1,86 +1,108 @@
version: 2.1
version: 2
orbs:
node: circleci/node@4.1.0
create-cache-file: &create-cache-file
run:
name: Setup cache
command: echo "$NODE_VERSION" > _cache_node_version
commands:
setup_git_bot:
description: set up the bot git user to make changes
steps:
- run:
name: "Git: Botovance"
command: |
git config --global user.name "Bot Vance"
git config --global user.email bot@autovance.com
package-json-cache: &package-json-cache
key: npm-install-{{ checksum "_cache_node_version" }}-{{ checksum "package-lock.json" }}
executors:
node-lts:
parameters:
node-version:
type: string
default: lts
docker:
- image: cimg/node:<< parameters.node-version >>
base-build: &base-build
steps:
- checkout
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- run:
name: Install
command: npm install
- save_cache:
<<: *package-json-cache
paths:
- node_modules
- run:
name: Lint
command: npm run verify:js
- run:
name: Test
command: npm run test:unit:once
jobs:
lint:
executor: node-lts
steps:
- checkout
- node/install-packages
- run:
name: Lint
command: npm run verify
test_node_10:
docker:
- image: circleci/node:10
environment:
- NODE_VERSION: 10
<<: *base-build
release_dry_run:
executor: node-lts
steps:
- checkout
- node/install-packages
- setup_git_bot
- deploy:
name: Dry Release
command: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release --dry-run
test_node_8:
docker:
- image: circleci/node:8
environment:
- NODE_VERSION: 8
<<: *base-build
test_node_6:
docker:
- image: circleci/node:6
environment:
- NODE_VERSION: 6
<<: *base-build
release:
executor: node-lts
docker:
- image: circleci/node:8
environment:
- NODE_VERSION: 8
steps:
- checkout
- node/install-packages
- setup_git_bot
- deploy:
name: Release
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- run:
name: Update NPM
command: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release
npm install npm@5
npm install semantic-release@11
- deploy:
name: Semantic Release
command: |
npm run semantic-release || true
workflows:
version: 2
test:
test_and_tag:
jobs:
- lint
- node/test:
matrix:
parameters:
version:
- '10.23'
- '12.20'
- '14.15'
- 'current'
- release_dry_run:
- test_node_10:
filters:
branches:
only: master
- test_node_8:
filters:
branches:
only: master
- test_node_6:
filters:
branches:
only: master
requires:
- node/test
- lint
- hold_release:
type: approval
requires:
- release_dry_run
- release:
context: deploy-npm
requires:
- hold_release
- test_node_6
- test_node_8
- test_node_10
build_and_test:
jobs:
- test_node_10:
filters:
branches:
ignore: master
- test_node_8:
filters:
branches:
ignore: master
- test_node_6:
filters:
branches:
ignore: master

View File

@@ -1,2 +1,9 @@
# START_CONFIT_GENERATED_CONTENT
# Common folders to ignore
node_modules/*
bower_components/*
# Config folder (optional - you might want to lint this...)
config/*
# END_CONFIT_GENERATED_CONTENT

5
.gitignore vendored
View File

@@ -1,6 +1,7 @@
test_tmp/
node_modules/
dist/
reports/
npm-debug.log
.nyc_output/
test_tmp/

View File

@@ -16,10 +16,5 @@
- Any new fixes are features should include new or updated [tests](/test).
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
- Submit your pull requests to the `master` branch, these will normally be merged into a separate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.
## Setup
- Clone the repository `git clone`
- Install dependencies `npm install`
- Submit your pull requests to the `master` branch, these will normally be merged into a seperate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Tyler Stewart
Copyright (c) 2018 Tyler Stewart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

126
README.md
View File

@@ -1,5 +1,5 @@
<p align="center">
<a href="https://github.com/autovance/ftp-srv">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
@@ -14,8 +14,8 @@
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://circleci.com/gh/autovance/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/autovance/ftp-srv/master.svg?style=for-the-badge" />
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
</a>
</p>
@@ -51,7 +51,7 @@
// Quick start
const FtpSrv = require('ftp-srv');
const ftpServer = new FtpSrv({ options ... });
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876', { options ... });
ftpServer.on('login', (data, resolve, reject) => { ... });
...
@@ -62,7 +62,7 @@ ftpServer.listen()
## API
### `new FtpSrv({options})`
### `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:
@@ -72,45 +72,45 @@ Supported protocols:
_Note:_ The hostname must be the external IP address to accept external connections. `0.0.0.0` will listen on any available hosts for server and passive connections.
__Default:__ `"ftp://127.0.0.1:21"`
#### `pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`).
If not provided, clients can only connect using an `Active` connection.
#### options
#### `pasv_min`
Tne starting port to accept passive connections.
__Default:__ `1024`
##### `pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
#### `pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
__Default:__ `"127.0.0.1"`
#### `greeting`
##### `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`
##### `greeting`
A human readable array of lines or string to send when a client connects.
__Default:__ `null`
#### `tls`
##### `tls`
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
__Default:__ `false`
#### `anonymous`
##### `anonymous`
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
Can also set as a string which allows users to authenticate using the username provided.
The `login` event is then sent with the provided username and `@anonymous` as the password.
__Default:__ `false`
#### `blacklist`
##### `blacklist`
Array of commands that are not allowed.
Response code `502` is sent to clients sending one of these commands.
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
__Default:__ `[]`
#### `whitelist`
##### `whitelist`
Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command.
__Default:__ `[]`
#### `file_format`
##### `file_format`
Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"`
__Allowable values:__
@@ -119,13 +119,9 @@ __Allowable values:__
- `function () {}` A custom function returning a format or promise for one.
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
#### `log`
##### `log`
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
#### `timeout`
Sets the timeout (in ms) after that an idle connection is closed by the server
__Default:__ `0`
## CLI
`ftp-srv` also comes with a builtin CLI.
@@ -139,29 +135,19 @@ $ ftp-srv ftp://0.0.0.0:9876 --root ~/Documents
```
#### `url`
Set the listening URL.
Defaults to `ftp://127.0.0.1:21`
#### `--pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`).
If not provided, clients can only connect using an `Active` connection.
#### `--pasv_min`
The starting port to accept passive connections.
__Default:__ `1024`
#### `--pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
#### `--root` / `-r`
Set the default root directory for users.
Defaults to the current directory.
#### `--credentials` / `-c`
Set the path to a json credentials file.
Format:
@@ -178,13 +164,12 @@ Format:
```
#### `--username`
Set the username for the only user. Do not provide an argument to allow anonymous login.
#### `--password`
Set the password for the given `username`.
#### `--read-only`
Disable write actions such as upload, delete, etc.
Set the password for the given `username`.
## Events
@@ -248,16 +233,6 @@ Occurs when a file is uploaded.
`error` if successful, will be `null`
`fileName` name of the file that was uploaded
### `RNTO`
```js
connection.on('RNTO', (error, fileName) => { ... });
```
Occurs when a file is renamed.
`error` if successful, will be `null`
`fileName` name of the file that was renamed
## Supported Commands
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
@@ -285,83 +260,80 @@ class MyFileSystem extends FileSystem {
Custom file systems can implement the following variables depending on the developers needs:
### Methods
#### [`currentDirectory()`](src/fs.js#L40)
#### [`currentDirectory()`](src/fs.js#L29)
Returns a string of the current working directory
__Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L44)
#### [`get(fileName)`](src/fs.js#L33)
Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L50)
#### [`list(path)`](src/fs.js#L39)
Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L67)
#### [`chdir(path)`](src/fs.js#L56)
Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L114)
#### [`mkdir(path)`](src/fs.js#L96)
Returns a path to a newly created directory
__Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L79)
#### [`write(fileName, {append, start})`](src/fs.js#L68)
Returns a writable stream
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L90)
#### [`read(fileName, {start})`](src/fs.js#L75)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L105)
#### [`delete(path)`](src/fs.js#L87)
Delete a file or directory
__Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L120)
#### [`rename(from, to)`](src/fs.js#L102)
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L126)
#### [`chmod(path)`](src/fs.js#L108)
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`getUniqueName()`](src/fs.js#L131)
#### [`getUniqueName()`](src/fs.js#L113)
Returns a unique file name to write to
__Used in:__ `STOU`
<!--[RM_CONTRIBUTING]-->
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
<!--[]-->
## Contributors
- [OzairP](https://github.com/OzairP)
- [TimLuq](https://github.com/TimLuq)
- [crabl](https://github.com/crabl)
- [hirviid](https://github.com/hirviid)
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
- [edin-m](https://github.com/edin-m)
- [voxsoftware](https://github.com/voxsoftware)
- [jorinvo](https://github.com/jorinvo)
- [Johnnyrook777](https://github.com/Johnnyrook777)
- [qchar](https://github.com/qchar)
- [mikejestes](https://github.com/mikejestes)
- [jorinvo](https://github.com/jorinvo)
- [voxsoftware](https://github.com/voxsoftware)
- [pkeuter](https://github.com/pkeuter)
- [qiansc](https://github.com/qiansc)
- [broofa](https://github.com/broofa)
- [lafin](https://github.com/lafin)
- [alancnet](https://github.com/alancnet)
- [zgwit](https://github.com/zgwit)
- [TimLuq](https://github.com/TimLuq)
- [edin-mg](https://github.com/edin-m)
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
<!--[RM_LICENSE]-->
## License
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
<!--[]-->
## References
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)

View File

@@ -1,16 +0,0 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 4.x | :white_check_mark: |
| 3.x | :white_check_mark: |
| < 3.0 | :x: |
__Critical vulnerabilities will be ported as far back as possible.__
## Reporting a Vulnerability
Report a security vulnerability directly to the maintainers by sending an email to security@autovance.com
or by reporting a vulnerability to the [NPM and Github security teams](https://docs.npmjs.com/reporting-a-vulnerability-in-an-npm-package).

View File

@@ -19,8 +19,7 @@ function setupYargs() {
})
.option('username', {
describe: 'Blank for anonymous',
type: 'string',
default: ''
type: 'string'
})
.option('password', {
describe: 'Password for given username',
@@ -37,23 +36,6 @@ function setupYargs() {
boolean: true,
default: false
})
.option('pasv-url', {
describe: 'URL to provide for passive connections',
type: 'string',
alias: 'pasv_url'
})
.option('pasv-min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024,
alias: 'pasv_min'
})
.option('pasv-max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535,
alias: 'pasv_max'
})
.parse();
}
@@ -64,18 +46,15 @@ function setupState(_args) {
if (_args._ && _args._.length > 0) {
_state.url = _args._[0];
}
_state.pasv_url = _args.pasv_url;
_state.pasv_min = _args.pasv_min;
_state.pasv_max = _args.pasv_max;
_state.anonymous = _args.username === '';
}
function setupRoot() {
const dirPath = _args.root;
if (dirPath) {
_state.root = dirPath;
} else {
_state.root = process.cwd();
} else {
_state.root = dirPath;
}
}
@@ -116,25 +95,17 @@ function setupState(_args) {
}
function startFtpServer(_state) {
// Remove null/undefined options so they get set to defaults, below
for (const key in _state) {
if (_state[key] === undefined) delete _state[key];
}
function checkLogin(data, resolve, reject) {
const user = _state.credentials[data.username];
if (_state.anonymous || user && user.password === data.password) {
return resolve({root: user && user.root || _state.root});
const user = _state.credentials[data.username]
if (_state.anonymous || (user && user.password === data.password)) {
return resolve({root: (user && user.root) || _state.root});
}
return reject(new errors.GeneralError('Invalid username or password', 401));
}
const ftpServer = new FtpSrv({
url: _state.url,
pasv_url: _state.pasv_url,
pasv_min: _state.pasv_min,
pasv_max: _state.pasv_max,
const ftpServer = new FtpSrv(_state.url, {
anonymous: _state.anonymous,
blacklist: _state.blacklist
});

View File

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

View File

@@ -0,0 +1,38 @@
'use strict';
module.exports = {
types: [
{value: 'feat', name: 'feat: A new feature'},
{value: 'fix', name: 'fix: A bug fix'},
{value: 'docs', name: 'docs: Documentation only changes'},
{value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'},
{value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
{value: 'perf', name: 'perf: A code change that improves performance'},
{value: 'test', name: 'test: Adding missing tests'},
{value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'},
{value: 'revert', name: 'revert: Revert to a commit'},
{value: 'WIP', name: 'WIP: Work in progress'}
],
scopes: [],
// it needs to match the value for field type. Eg.: 'fix'
/*
scopeOverrides: {
fix: [
{name: 'merge'},
{name: 'style'},
{name: 'e2eTest'},
{name: 'unitTest'}
]
},
*/
allowCustomScopes: true,
allowBreakingChanges: ['feat', 'fix'],
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
appendBranchNameToCommitMessage: false
};

View File

@@ -0,0 +1,5 @@
test/**/*.spec.js
--reporter mocha-multi-reporters
--reporter-options configFile=config/testUnit/reporters.json
--ui bdd
--bail

View File

@@ -0,0 +1,6 @@
{
"reporterEnabled": "spec",
"mochaJunitReporterReporterOptions": {
"mochaFile": "reports/junit.xml"
}
}

162
config/verify/.eslintrc Normal file
View File

@@ -0,0 +1,162 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"mocha",
"node"
],
"rules": {
"mocha/no-exclusive-tests": 2,
"no-warning-comments": [
1,
{
"terms": ["todo", "fixme", "xxx"],
"location": "start"
},
],
"object-curly-spacing": [
2,
"never"
],
"array-bracket-spacing": [
2,
"never"
],
"brace-style": [
2,
"1tbs"
],
"consistent-return": 0,
"indent": [
"error",
2,
{
"SwitchCase": 1,
"MemberExpression": "off"
}
],
"no-multiple-empty-lines": [
2,
{
"max": 2
}
],
"no-use-before-define": [
2,
"nofunc"
],
"one-var": [
2,
"never"
],
"quote-props": [
2,
"as-needed"
],
"quotes": [
2,
"single"
],
"keyword-spacing": 2,
"space-before-function-paren": [
2,
{
"anonymous": "always",
"named": "never"
}
],
"space-in-parens": [
2,
"never"
],
"strict": [
2,
"global"
],
"curly": [
2,
"multi-line"
],
"eol-last": 2,
"key-spacing": [
2,
{
"beforeColon": false,
"afterColon": true
}
],
"no-eval": 2,
"no-with": 2,
"space-infix-ops": 2,
"dot-notation": [
2,
{
"allowKeywords": true
}
],
"eqeqeq": 2,
"no-alert": 2,
"no-caller": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-proto": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-unused-expressions": 2,
"yoda": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-undef-init": 2,
"no-console": 1,
"camelcase": [
0,
{
"properties": "never"
}
],
"comma-spacing": 2,
"comma-dangle": 1,
"new-cap": 2,
"new-parens": 2,
"arrow-parens": [2, "as-needed"],
"no-array-constructor": 2,
"array-callback-return": 1,
"no-extra-parens": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-underscore-dangle": 0,
"no-fallthrough": 0,
"semi": 2,
"semi-spacing": [
2,
{
"before": false,
"after": true
}
]
},
"parserOptions": {
"emcaVersion": 6,
"sourceType": "module",
"impliedStrict": true
}
}

19
ftp-srv.d.ts vendored
View File

@@ -59,22 +59,18 @@ export class FtpConnection extends EventEmitter {
}
export interface FtpServerOptions {
url?: string,
pasv_min?: number,
pasv_max?: number,
pasv_url?: string,
pasv_range?: number | string,
greeting?: string | string[],
tls?: tls.SecureContextOptions | false,
tls?: tls.SecureContext | false,
anonymous?: boolean,
blacklist?: Array<string>,
whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log?: any,
timeout?: number
log?: any
}
export class FtpServer extends EventEmitter {
constructor(options?: FtpServerOptions);
constructor(url: string, options?: FtpServerOptions);
readonly isTLS: boolean;
@@ -112,13 +108,6 @@ export class FtpServer extends EventEmitter {
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void): this;
on(event: "disconnect", listener: (
data: {
connection: FtpConnection,
id: string
}
) => void): this;
on(event: "client-error", listener: (

View File

@@ -1,27 +0,0 @@
const {get} = require('https');
get('https://api.github.com/repos/trs/ftp-srv/contributors', {
headers: {
'User-Agent': 'Chrome'
}
}, (res) => {
let response = '';
res.on('data', (data) => {
response += data;
});
res.on('end', () => {
const contributors = JSON.parse(response)
.filter((contributor) => contributor.type === 'User');
for (const contributor of contributors) {
const url = contributor.html_url;
const username = contributor.login;
const markdown = `- [${username}](${url})\n`;
process.stdout.write(markdown);
}
});
}).on('error', (err) => {
process.stderr.write(err);
});

13592
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ftp-srv",
"version": "0.0.0",
"version": "2.19.6",
"description": "Modern, extensible FTP Server",
"keywords": [
"ftp",
@@ -22,71 +22,68 @@
"types": "./ftp-srv.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/autovance/ftp-srv"
"url": "https://github.com/trs/ftp-srv"
},
"scripts": {
"pre-release": "npm run verify",
"test": "mocha test/**/*.spec.js test/*.spec.js --ui bdd",
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
"commitmsg": "cz-customizable-ghooks",
"dev": "cross-env NODE_ENV=development npm run verify:watch",
"prepush": "npm-run-all verify test:unit:once --silent",
"semantic-release": "semantic-release",
"start": "npm run dev",
"test": "npm run test:unit",
"test:unit": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts -w",
"test:unit:once": "cross-env NODE_ENV=test mocha --opts config/testUnit/mocha.opts",
"verify": "npm run verify:js --silent",
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
"verify:watch": "npm run verify:js:watch --silent"
},
"release": {
"verifyConditions": "condition-circle",
"branch": "master",
"branches": ["master"]
"verifyConditions": "condition-circle"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
"cz-customizable": {
"config": "config/release/commitMessageConfig.js"
}
},
"dependencies": {
"bluebird": "^3.5.1",
"bunyan": "^1.8.12",
"ip": "^1.1.5",
"lodash": "^4.17.15",
"lodash": "^4.17.10",
"moment": "^2.22.1",
"uuid": "^3.2.1",
"yargs": "^15.4.1"
"yargs": "^11.0.0"
},
"devDependencies": {
"@commitlint/cli": "^10.0.0",
"@commitlint/config-conventional": "^8.1.0",
"@icetee/ftp": "^1.0.2",
"chai": "^4.2.0",
"condition-circle": "^2.0.2",
"eslint": "^5.14.1",
"husky": "^1.3.1",
"lint-staged": "^8.2.1",
"mocha": "^8.1.1",
"rimraf": "^2.6.1",
"semantic-release": "^17.2.3",
"chai": "^4.0.2",
"condition-circle": "^1.6.0",
"cross-env": "3.1.4",
"cz-customizable": "5.2.0",
"cz-customizable-ghooks": "1.5.0",
"dotenv": "^4.0.0",
"eslint": "4.5.0",
"eslint-config-google": "0.8.0",
"eslint-friendly-formatter": "3.0.0",
"eslint-plugin-mocha": "^4.11.0",
"eslint-plugin-node": "5.1.1",
"husky": "0.13.3",
"istanbul": "0.4.5",
"mocha": "^5.2.0",
"mocha-junit-reporter": "1.13.0",
"mocha-multi-reporters": "1.1.5",
"npm-run-all": "^4.1.3",
"rimraf": "2.6.1",
"semantic-release": "^11.0.2",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=6.x"
"node": ">=6.x",
"npm": ">=5.x"
}
}

View File

@@ -3,30 +3,25 @@ const Promise = require('bluebird');
const REGISTRY = require('./registry');
const CMD_FLAG_REGEX = new RegExp(/^-(\w{1})$/);
class FtpCommands {
constructor(connection) {
this.connection = connection;
this.previousCommand = {};
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map((cmd) => _.upperCase(cmd));
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map((cmd) => _.upperCase(cmd));
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd));
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
}
parse(message) {
const strippedMessage = message.replace(/"/g, '');
let [directive, ...args] = strippedMessage.split(' ');
directive = _.chain(directive).trim().toUpper().value();
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
const [directive, ...args] = strippedMessage.split(' ');
const params = args.reduce(({arg, flags}, param) => {
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const command = {
directive,
directive: _.chain(directive).trim().toUpper().value(),
arg: params.arg.length ? params.arg.join(' ') : null,
flags: params.flags,
raw: message
@@ -45,25 +40,25 @@ class FtpCommands {
log.trace({command: logCommand}, 'Handle command');
if (!REGISTRY.hasOwnProperty(command.directive)) {
return this.connection.reply(502, `Command not allowed: ${command.directive}`);
return this.connection.reply(402, 'Command not allowed');
}
if (_.includes(this.blacklist, command.directive)) {
return this.connection.reply(502, `Command blacklisted: ${command.directive}`);
return this.connection.reply(502, 'Command blacklisted');
}
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
return this.connection.reply(502, `Command not whitelisted: ${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: ${command.directive}`);
return this.connection.reply(530, 'Command requires authentication');
}
if (!commandRegister.handler) {
return this.connection.reply(502, `Handler not set on command: ${command.directive}`);
return this.connection.reply(502, 'Handler not set on command');
}
const handler = commandRegister.handler.bind(this.connection);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
const _ = require('lodash');
const registry = require('../registry');
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);
@@ -11,7 +11,7 @@ module.exports = {
return feats;
}, ['UTF8'])
.sort()
.map((feat) => ({
.map(feat => ({
message: ` ${feat}`,
raw: true
}));

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ module.exports = {
const [_option, ...args] = command.arg.split(' ');
const option = _.toUpper(_option);
if (!OPTIONS.hasOwnProperty(option)) return this.reply(501, 'Unknown option command');
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
return OPTIONS[option].call(this, args);
},
syntax: '{{cmd}}',
@@ -33,6 +33,7 @@ function utf8([setting] = []) {
if (!encoding) return this.reply(501, 'Unknown setting for option');
this.encoding = encoding;
if (this.transferType !== 'binary') this.transferType = this.encoding;
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`);
}

View File

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

View File

@@ -1,21 +1,12 @@
const PassiveConnector = require('../../connector/passive');
const {isLocalIP} = require('../../helpers/is-local');
module.exports = {
directive: 'PASV',
handler: function ({log} = {}) {
if (!this.server.options.pasv_url) {
return this.reply(502);
}
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
let address = this.server.options.pasv_url;
// Allow connecting from local
if (isLocalIP(this.ip)) {
address = this.ip;
}
.then(server => {
const address = this.server.options.pasv_url;
const {port} = server.address();
const host = address.replace(/\./g, ',');
const portByte1 = port / 256 | 0;

View File

@@ -1,7 +1,7 @@
module.exports = {
directive: 'PBSZ',
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not supported');
if (!this.secure) return this.reply(202, 'Not suppored');
this.bufferSize = parseInt(command.arg, 10);
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
},

View File

@@ -3,14 +3,14 @@ const ActiveConnector = require('../../connector/active');
module.exports = {
directive: 'PORT',
handler: function ({log, command} = {}) {
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 portBytes = rawConnection.slice(4).map(p => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1];
return this.connector.setupConnection(ip, port)

View File

@@ -3,7 +3,7 @@ const _ = require('lodash');
module.exports = {
directive: 'PROT',
handler: function ({command} = {}) {
if (!this.secure) return this.reply(202, 'Not supported');
if (!this.secure) return this.reply(202, 'Not suppored');
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
switch (_.toUpper(command.arg)) {

View File

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

View File

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

View File

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

View File

@@ -11,14 +11,12 @@ module.exports = {
const from = this.renameFrom;
const to = command.arg;
return Promise.try(() => this.fs.rename(from, to))
return Promise.resolve(this.fs.rename(from, to))
.then(() => {
return this.reply(250);
})
.tap(() => this.emit('RNTO', null, to))
.catch((err) => {
.catch(err => {
log.error(err);
this.emit('RNTO', err);
return this.reply(550, err.message);
})
.finally(() => {

View File

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

View File

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

View File

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

View File

@@ -11,24 +11,11 @@ module.exports = {
return this.connector.waitForConnection()
.tap(() => this.commandSocket.pause())
.then(() => Promise.try(() => this.fs.write(fileName, {append, start: this.restByteCount})))
.then((fsResponse) => {
let {stream, clientPath} = fsResponse;
if (!stream && !clientPath) {
stream = fsResponse;
clientPath = fileName;
}
const serverPath = stream.path || fileName;
const destroyConnection = (connection, reject) => (err) => {
try {
if (connection) {
if (connection.writable) connection.end();
connection.destroy(err);
}
} finally {
reject(err);
}
.then(() => Promise.resolve(this.fs.write(fileName, {append, start: this.restByteCount})))
.then(stream => {
const destroyConnection = (connection, reject) => err => {
if (connection) connection.destroy(err);
reject(err);
};
const streamPromise = new Promise((resolve, reject) => {
@@ -37,7 +24,12 @@ module.exports = {
});
const socketPromise = new Promise((resolve, reject) => {
this.connector.socket.pipe(stream, {end: false});
this.connector.socket.on('data', data => {
if (this.connector.socket) this.connector.socket.pause();
if (stream) {
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
}
});
this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close');
else stream.end();
@@ -48,17 +40,17 @@ module.exports = {
this.restByteCount = 0;
return this.reply(150).then(() => this.connector.socket && this.connector.socket.resume())
.then(() => Promise.all([streamPromise, socketPromise]))
.tap(() => this.emit('STOR', null, serverPath))
.then(() => this.reply(226, clientPath))
return this.reply(150).then(() => this.connector.socket.resume())
.then(() => Promise.join(streamPromise, socketPromise))
.tap(() => this.emit('STOR', null, fileName))
.finally(() => stream.destroy && stream.destroy());
})
.catch(Promise.TimeoutError, (err) => {
.then(() => this.reply(226, fileName))
.catch(Promise.TimeoutError, err => {
log.error(err);
return this.reply(425, 'No connection established');
})
.catch((err) => {
.catch(err => {
log.error(err);
this.emit('STOR', err);
return this.reply(550, err.message);

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,6 @@ class FtpConnection extends EventEmitter {
super();
this.server = server;
this.id = uuid.v4();
this.commandSocket = options.socket;
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
@@ -25,15 +24,13 @@ class FtpConnection extends EventEmitter {
this.connector = new BaseConnector(this);
this.commandSocket.on('error', (err) => {
this.commandSocket = options.socket;
this.commandSocket.on('error', err => {
this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
});
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', () => {
this.log.trace('Client timeout');
this.close();
});
this.commandSocket.on('timeout', () => {});
this.commandSocket.on('close', () => {
if (this.connector) this.connector.end();
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
@@ -44,7 +41,7 @@ class FtpConnection extends EventEmitter {
_handleData(data) {
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
this.log.trace(messages);
return Promise.mapSeries(messages, (message) => this.commands.handle(message));
return Promise.mapSeries(messages, message => this.commands.handle(message));
}
get ip() {
@@ -71,8 +68,8 @@ class FtpConnection extends EventEmitter {
close(code = 421, message = 'Closing connection') {
return Promise.resolve(code)
.then((_code) => _code && this.reply(_code, message))
.finally(() => this.commandSocket && this.commandSocket.destroy());
.then(_code => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
}
login(username, password) {
@@ -99,56 +96,47 @@ class FtpConnection extends EventEmitter {
if (!letters.length) letters = [{}];
return Promise.map(letters, (promise, index) => {
return Promise.resolve(promise)
.then((letter) => {
.then(letter => {
if (!letter) letter = {};
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
if (!options.useEmptyMessage) {
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding;
}
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding;
return Promise.resolve(letter.message) // allow passing in a promise as a message
.then((message) => {
if (!options.useEmptyMessage) {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
letter.message = message;
} else {
letter.message = '';
}
.then(message => {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
letter.message = message;
return letter;
});
});
});
};
const processLetter = (letter) => {
const processLetter = letter => {
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
if (error) {
this.log.error('[Process Letter] Socket Write Error', { error: error.message });
return reject(error);
letter.socket.write(letter.message + '\r\n', letter.encoding, err => {
if (err) {
this.log.error(err);
return reject(err);
}
resolve();
});
} else {
this.log.trace({message: letter.message}, 'Could not write message');
reject(new errors.SocketError('Socket not writable'));
}
} else reject(new errors.SocketError('Socket not writable'));
});
};
return satisfyParameters()
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
.then(satisfiedLetters => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch((error) => {
this.log.error('Satisfy Parameters Error', { error: error.message });
.catch(err => {
this.log.error(err);
});
}
}

View File

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

View File

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

View File

@@ -4,17 +4,16 @@ const ip = require('ip');
const Promise = require('bluebird');
const Connector = require('./base');
const findPort = require('../helpers/find-port');
const errors = require('../errors');
const CONNECT_TIMEOUT = 30 * 1000;
class Passive extends Connector {
constructor(connection) {
super(connection);
this.type = 'passive';
}
waitForConnection({timeout = 5000, delay = 50} = {}) {
waitForConnection({timeout = 5000, delay = 250} = {}) {
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
const checkSocket = () => {
@@ -29,13 +28,14 @@ class Passive extends Connector {
}
setupServer() {
this.closeServer();
return this.server.getNextPasvPort()
.then((port) => {
this.dataSocket = null;
let idleServerTimeout;
const closeExistingServer = () => this.dataServer ?
new Promise(resolve => this.dataServer.close(() => resolve())) :
Promise.resolve();
const connectionHandler = (socket) => {
return closeExistingServer()
.then(() => this.getPort())
.then(port => {
const connectionHandler = socket => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
this.log.error({
pasv_connection: socket.remoteAddress,
@@ -46,52 +46,51 @@ class Passive extends Connector {
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
}
clearTimeout(idleServerTimeout);
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
this.dataSocket = socket;
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.once('close', () => this.closeServer());
if (!this.connection.secure) {
this.dataSocket.connected = true;
}
this.dataSocket.connected = true;
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.on('close', () => {
this.log.trace('Passive connection closed');
this.end();
});
};
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
this.dataSocket = null;
const serverOptions = Object.assign({}, this.connection.secure ? this.server._tls : {}, {pauseOnConnect: true});
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
this.dataServer.maxConnections = 1;
this.dataServer.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.once('close', () => {
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
this.dataServer.on('close', () => {
this.log.trace('Passive server closed');
this.end();
this.dataServer = null;
});
if (this.connection.secure) {
this.dataServer.on('secureConnection', (socket) => {
socket.connected = true;
});
}
return new Promise((resolve, reject) => {
this.dataServer.listen(port, this.server.url.hostname, (err) => {
this.dataServer.listen(port, this.server.url.hostname, err => {
if (err) reject(err);
else {
idleServerTimeout = setTimeout(() => this.closeServer(), CONNECT_TIMEOUT);
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
}
});
});
})
.catch((error) => {
this.log.trace(error.message);
throw error;
});
}
getPort() {
if (this.server.options.pasv_range) {
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
[this.server.options.pasv_range];
return findPort(min, max);
}
throw new errors.ConnectorError('Invalid pasv_range');
}
}
module.exports = Passive;

View File

@@ -2,17 +2,13 @@ const _ = require('lodash');
const nodePath = require('path');
const uuid = require('uuid');
const Promise = require('bluebird');
const {createReadStream, createWriteStream, constants} = require('fs');
const fsAsync = require('./helpers/fs-async');
const fs = Promise.promisifyAll(require('fs'));
const errors = require('./errors');
const UNIX_SEP_REGEX = /\//g;
const WIN_SEP_REGEX = /\\/g;
class FileSystem {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = nodePath.normalize((cwd || '/').replace(WIN_SEP_REGEX, '/'));
this.cwd = cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep;
this._root = nodePath.resolve(root || process.cwd());
}
@@ -21,24 +17,22 @@ class FileSystem {
}
_resolvePath(path = '.') {
// Unix separators normalize nicer on both unix and win platforms
const resolvedPath = path.replace(WIN_SEP_REGEX, '/');
const serverPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
} else {
return nodePath.join(this.cwd, path);
}
})();
// Join cwd with new path
const joinedPath = nodePath.isAbsolute(resolvedPath)
? nodePath.normalize(resolvedPath)
: nodePath.join('/', this.cwd, resolvedPath);
// Create local filesystem path using the platform separator
const fsPath = nodePath.resolve(nodePath.join(this.root, joinedPath)
.replace(UNIX_SEP_REGEX, nodePath.sep)
.replace(WIN_SEP_REGEX, nodePath.sep));
// Create FTP client path using unix separator
const clientPath = joinedPath.replace(WIN_SEP_REGEX, '/');
const fsPath = (() => {
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${serverPath}`);
return nodePath.join(resolvedPath);
})();
return {
clientPath,
serverPath,
fsPath
};
}
@@ -49,20 +43,20 @@ class FileSystem {
get(fileName) {
const {fsPath} = this._resolvePath(fileName);
return fsAsync.stat(fsPath)
.then((stat) => _.set(stat, 'name', fileName));
return fs.statAsync(fsPath)
.then(stat => _.set(stat, 'name', fileName));
}
list(path = '.') {
const {fsPath} = this._resolvePath(path);
return fsAsync.readdir(fsPath)
.then((fileNames) => {
return Promise.map(fileNames, (fileName) => {
return fs.readdirAsync(fsPath)
.then(fileNames => {
return Promise.map(fileNames, fileName => {
const filePath = nodePath.join(fsPath, fileName);
return fsAsync.access(filePath, constants.F_OK)
return fs.accessAsync(filePath, fs.constants.F_OK)
.then(() => {
return fsAsync.stat(filePath)
.then((stat) => _.set(stat, 'name', fileName));
return fs.statAsync(filePath)
.then(stat => _.set(stat, 'name', fileName));
})
.catch(() => null);
});
@@ -71,67 +65,61 @@ class FileSystem {
}
chdir(path = '.') {
const {fsPath, clientPath} = this._resolvePath(path);
return fsAsync.stat(fsPath)
.tap((stat) => {
const {fsPath, serverPath} = this._resolvePath(path);
return fs.statAsync(fsPath)
.tap(stat => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
.then(() => {
this.cwd = clientPath;
this.cwd = serverPath;
return this.currentDirectory();
});
}
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath, clientPath} = this._resolvePath(fileName);
const stream = createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fsAsync.unlink(fsPath));
const {fsPath} = this._resolvePath(fileName);
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlinkAsync(fsPath));
stream.once('close', () => stream.end());
return {
stream,
clientPath
};
return stream;
}
read(fileName, {start = undefined} = {}) {
const {fsPath, clientPath} = this._resolvePath(fileName);
return fsAsync.stat(fsPath)
.tap((stat) => {
const {fsPath} = this._resolvePath(fileName);
return fs.statAsync(fsPath)
.tap(stat => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = createReadStream(fsPath, {flags: 'r', start});
return {
stream,
clientPath
};
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
return stream;
});
}
delete(path) {
const {fsPath} = this._resolvePath(path);
return fsAsync.stat(fsPath)
.then((stat) => {
if (stat.isDirectory()) return fsAsync.rmdir(fsPath);
else return fsAsync.unlink(fsPath);
return fs.statAsync(fsPath)
.then(stat => {
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
else return fs.unlinkAsync(fsPath);
});
}
mkdir(path) {
const {fsPath} = this._resolvePath(path);
return fsAsync.mkdir(fsPath)
return fs.mkdirAsync(fsPath)
.then(() => fsPath);
}
rename(from, to) {
const {fsPath: fromPath} = this._resolvePath(from);
const {fsPath: toPath} = this._resolvePath(to);
return fsAsync.rename(fromPath, toPath);
return fs.renameAsync(fromPath, toPath);
}
chmod(path, mode) {
const {fsPath} = this._resolvePath(path);
return fsAsync.chmod(fsPath, mode);
return fs.chmodAsync(fsPath, mode);
}
getUniqueName() {

View File

@@ -1,61 +1,27 @@
const net = require('net');
const Promise = require('bluebird');
const errors = require('../errors');
const MAX_PORT = 65535;
const MAX_PORT_CHECK_ATTEMPT = 5;
function* portNumberGenerator(min, max = MAX_PORT) {
let current = min;
while (true) {
if (current > MAX_PORT || current > max) {
current = min;
}
yield current++;
}
}
function getNextPortFactory(host, portMin, portMax, maxAttempts = MAX_PORT_CHECK_ATTEMPT) {
const nextPortNumber = portNumberGenerator(portMin, portMax);
return () => new Promise((resolve, reject) => {
const portCheckServer = net.createServer();
module.exports = function (min = 1, max = undefined) {
return new Promise((resolve, reject) => {
let checkPort = min;
let portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
let attemptCount = 0;
const tryGetPort = () => {
attemptCount++;
if (attemptCount > maxAttempts) {
reject(new errors.ConnectorError('Unable to find valid port'));
return;
portCheckServer.on('error', () => {
if (checkPort < 65535 && (!max || checkPort < max)) {
checkPort = checkPort + 1;
portCheckServer.listen(checkPort);
} else {
reject(new errors.GeneralError('Unable to find open port', 500));
}
const {value: port} = nextPortNumber.next();
portCheckServer.removeAllListeners();
portCheckServer.once('error', (err) => {
if (['EADDRINUSE'].includes(err.code)) {
tryGetPort();
} else {
reject(err);
}
});
portCheckServer.on('listening', () => {
const {port} = portCheckServer.address();
portCheckServer.close(() => {
portCheckServer = null;
resolve(port);
});
portCheckServer.once('listening', () => {
portCheckServer.removeAllListeners();
portCheckServer.close(() => resolve(port));
});
try {
portCheckServer.listen(port, host);
} catch (err) {
reject(err);
}
};
tryGetPort();
});
portCheckServer.listen(checkPort);
});
}
module.exports = {
getNextPortFactory,
portNumberGenerator
};

View File

@@ -1,18 +0,0 @@
const fs = require('fs');
const {promisify} = require('bluebird');
const methods = [
'stat',
'readdir',
'access',
'unlink',
'rmdir',
'mkdir',
'rename',
'chmod'
];
module.exports = methods.reduce((obj, method) => {
obj[method] = promisify(fs[method]);
return obj;
}, {});

View File

@@ -1,3 +0,0 @@
module.exports.isLocalIP = function(ip) {
return ip === '127.0.0.1' || ip == '::1';
}

View File

@@ -0,0 +1,25 @@
const http = require('http');
const Promise = require('bluebird');
const errors = require('../errors');
const IP_WEBSITE = 'http://api.ipify.org/';
module.exports = function (hostname) {
return new Promise((resolve, reject) => {
if (!hostname || hostname === '0.0.0.0') {
let ip = '';
http.get(IP_WEBSITE, response => {
if (response.statusCode !== 200) {
return reject(new errors.GeneralError('Unable to resolve hostname', response.statusCode));
}
response.setEncoding('utf8');
response.on('data', chunk => {
ip += chunk;
});
response.on('end', () => {
resolve(ip);
});
});
} else resolve(hostname);
});
};

View File

@@ -4,62 +4,52 @@ const nodeUrl = require('url');
const buyan = require('bunyan');
const net = require('net');
const tls = require('tls');
const fs = require('fs');
const EventEmitter = require('events');
const Connection = require('./connection');
const {getNextPortFactory} = require('./helpers/find-port');
const resolveHost = require('./helpers/resolve-host');
class FtpServer extends EventEmitter {
constructor(options = {}) {
constructor(url, options = {}) {
super();
this.options = Object.assign({
this.options = _.merge({
log: buyan.createLogger({name: 'ftp-srv'}),
url: 'ftp://127.0.0.1:21',
pasv_min: 1024,
pasv_max: 65535,
pasv_url: null,
anonymous: false,
pasv_range: 22,
pasv_url: null,
file_format: 'ls',
blacklist: [],
whitelist: [],
greeting: null,
tls: false,
timeout: 0
tls: false
}, options);
this._greeting = this.setupGreeting(this.options.greeting);
this._features = this.setupFeaturesMessage();
this._tls = this.setupTLS(this.options.tls);
delete this.options.greeting;
delete this.options.tls;
this.connections = {};
this.log = this.options.log;
this.url = nodeUrl.parse(this.options.url);
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'url.hostname'),
_.get(this, 'options.pasv_min'),
_.get(this, 'options.pasv_max'));
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
const timeout = Number(this.options.timeout);
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout);
const serverConnectionHandler = (socket) => {
this.options.timeout > 0 && socket.setTimeout(this.options.timeout);
const serverConnectionHandler = socket => {
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
socket.on('close', () => this.disconnectClient(connection.id));
socket.once('close', () => this.emit('disconnect', {connection, id: connection.id}));
const greeting = this._greeting || [];
const features = this._features || 'Ready';
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
.finally(() => socket.resume());
};
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
const serverOptions = Object.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'));
this.server.on('error', err => this.log.error(err, '[Event] error'));
const quit = _.debounce(this.quit.bind(this), 100);
@@ -69,25 +59,26 @@ class FtpServer extends EventEmitter {
}
get isTLS() {
return this.url.protocol === 'ftps:' && this.options.tls;
return this.url.protocol === 'ftps:' && this._tls;
}
listen() {
if (!this.options.pasv_url) {
this.log.warn('Passive URL not set. Passive connections not available.');
}
return resolveHost(this.options.pasv_url || this.url.hostname)
.then(pasvUrl => {
this.options.pasv_url = pasvUrl;
return new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject);
if (err) return reject(err);
this.log.info({
protocol: this.url.protocol.replace(/\W/g, ''),
ip: this.url.hostname,
port: this.url.port
}, 'Listening');
resolve('Listening');
return new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.url.port, this.url.hostname, err => {
this.server.removeListener('error', reject);
if (err) return reject(err);
this.log.info({
protocol: this.url.protocol.replace(/\W/g, ''),
ip: this.url.hostname,
port: this.url.port
}, 'Listening');
resolve('Listening');
});
});
});
}
@@ -99,6 +90,15 @@ class FtpServer extends EventEmitter {
});
}
setupTLS(_tls) {
if (!_tls) return false;
return _.assign({}, _tls, {
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
});
}
setupGreeting(greet) {
if (!greet) return [];
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
@@ -117,7 +117,7 @@ class FtpServer extends EventEmitter {
}
disconnectClient(id) {
return new Promise((resolve) => {
return new Promise(resolve => {
const client = this.connections[id];
if (!client) return resolve();
delete this.connections[id];
@@ -139,9 +139,9 @@ class FtpServer extends EventEmitter {
close() {
this.log.info('Server closing...');
this.server.maxConnections = 0;
return Promise.map(Object.keys(this.connections), (id) => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => new Promise((resolve) => {
this.server.close((err) => {
return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
.then(() => new Promise(resolve => {
this.server.close(err => {
if (err) this.log.error(err, 'Error closing server');
resolve('Closed');
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ describe(CMD, function () {
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create();
sandbox.spy(mockClient, 'reply');
});
@@ -29,7 +29,7 @@ describe(CMD, function () {
it('BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
expect(mockClient.reply.args[0][0]).to.equal(500);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,16 @@
const Promise = require('bluebird');
const {expect} = require('chai');
const sinon = require('sinon');
const EventEmitter = require('events');
const CMD = 'RNTO';
describe(CMD, function () {
let sandbox;
const mockLog = {error: () => {}};
let emitter;
const mockClient = {reply: () => Promise.resolve()};
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
beforeEach(() => {
sandbox = sinon.sandbox.create().usingPromise(Promise);
sandbox = sinon.sandbox.create();
mockClient.renameFrom = 'test';
mockClient.fs = {
@@ -20,9 +18,6 @@ describe(CMD, function () {
rename: () => Promise.resolve()
};
emitter = new EventEmitter();
mockClient.emit = emitter.emit.bind(emitter);
sandbox.spy(mockClient, 'reply');
sandbox.spy(mockClient.fs, 'rename');
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,15 @@
/* eslint no-unused-expressions: 0 */
const {expect} = require('chai');
const sinon = require('sinon');
const Promise = require('bluebird');
const net = require('net');
const tls = require('tls');
const ActiveConnector = require('../../src/connector/active');
const {getNextPortFactory} = require('../../src/helpers/find-port');
const findPort = require('../../src/helpers/find-port');
describe('Connector - Active //', function () {
const host = '127.0.0.1';
let getNextPort = getNextPortFactory(host, 1024);
let PORT;
let active;
let mockConnection = {
@@ -25,11 +24,11 @@ describe('Connector - Active //', function () {
active = new ActiveConnector(mockConnection);
sandbox = sinon.sandbox.create().usingPromise(Promise);
getNextPort()
.then((port) => {
findPort()
.then(port => {
PORT = port;
server = net.createServer()
.on('connection', (socket) => socket.destroy())
.on('connection', socket => socket.destroy())
.listen(PORT, () => done());
});
});
@@ -38,11 +37,10 @@ describe('Connector - Active //', function () {
sandbox.restore();
server.close();
active.end();
});
it('sets up a connection', function () {
return active.setupConnection(host, PORT)
return active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(active.dataSocket).to.exist;
});
@@ -60,11 +58,11 @@ describe('Connector - Active //', function () {
});
it('destroys existing connection, then sets up a connection', function () {
return active.setupConnection(host, PORT)
return active.setupConnection('127.0.0.1', PORT)
.then(() => {
const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
return active.setupConnection(host, PORT)
return active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(destroyFnSpy.callCount).to.equal(1);
expect(active.dataSocket).to.exist;
@@ -73,12 +71,12 @@ describe('Connector - Active //', function () {
});
it('waits for connection', function () {
return active.setupConnection(host, PORT)
return active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(active.dataSocket).to.exist;
return active.waitForConnection();
})
.then((dataSocket) => {
.then(dataSocket => {
expect(dataSocket.connected).to.equal(true);
expect(dataSocket instanceof net.Socket).to.equal(true);
expect(dataSocket instanceof tls.TLSSocket).to.equal(false);
@@ -87,18 +85,14 @@ describe('Connector - Active //', function () {
it('upgrades to a secure connection', function () {
mockConnection.secure = true;
mockConnection.server = {
options: {
tls: {}
}
};
mockConnection.server = {_tls: {}};
return active.setupConnection(host, PORT)
return active.setupConnection('127.0.0.1', PORT)
.then(() => {
expect(active.dataSocket).to.exist;
return active.waitForConnection();
})
.then((dataSocket) => {
.then(dataSocket => {
expect(dataSocket.connected).to.equal(true);
expect(dataSocket instanceof net.Socket).to.equal(true);
expect(dataSocket instanceof tls.TLSSocket).to.equal(true);

View File

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

View File

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

View File

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

View File

@@ -1,55 +1,35 @@
/* eslint no-unused-expressions: 0 */
const {expect} = require('chai');
const net = require('net');
const {Server} = require('net');
const sinon = require('sinon');
const {getNextPortFactory, portNumberGenerator} = require('../../src/helpers/find-port');
const findPort = require('../../src/helpers/find-port');
describe('getNextPortFactory', function () {
describe('portNumberGenerator', () => {
it('loops through given set of numbers', () => {
const nextNumber = portNumberGenerator(1, 5);
expect(nextNumber.next().value).to.equal(1);
expect(nextNumber.next().value).to.equal(2);
expect(nextNumber.next().value).to.equal(3);
expect(nextNumber.next().value).to.equal(4);
expect(nextNumber.next().value).to.equal(5);
expect(nextNumber.next().value).to.equal(1);
expect(nextNumber.next().value).to.equal(2);
describe('helpers // find-port', function () {
let sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.spy(Server.prototype, 'listen');
});
afterEach(() => {
sandbox.restore();
});
it('finds a port', () => {
return findPort(1)
.then(port => {
expect(Server.prototype.listen.callCount).to.be.above(0);
expect(port).to.be.above(0);
});
});
describe('keeps trying new ports', () => {
let getNextPort;
let serverAlreadyRunning;
beforeEach((done) => {
getNextPort = getNextPortFactory('::', 8821);
serverAlreadyRunning = net.createServer();
serverAlreadyRunning.listen(8821, () => done());
});
afterEach((done) => {
serverAlreadyRunning.close(() => done());
});
it('test', () => {
return getNextPort()
.then((port) => {
expect(port).to.equal(8822);
});
});
});
it('finds ports concurrently', () => {
const portStart = 10000;
const getCount = 100;
const getNextPort = getNextPortFactory('::', portStart);
const portFinders = new Array(getCount).fill().map(() => getNextPort());
return Promise.all(portFinders)
.then((ports) => {
expect(ports.length).to.equal(getCount);
expect(ports).to.eql(new Array(getCount).fill().map((v, i) => i + portStart));
it('does not find a port', () => {
return findPort(1, 2)
.then(() => expect(1).to.equal(2)) // should not happen
.catch(err => {
expect(err).to.exist;
});
});
});

View File

@@ -0,0 +1,51 @@
const {expect} = require('chai');
const sinon = require('sinon');
const resolveHost = require('../../src/helpers/resolve-host');
describe('helpers //resolve-host', function () {
this.timeout(4000);
let sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
});
afterEach(() => sandbox.restore());
it('fetches ip address', () => {
const hostname = '0.0.0.0';
return resolveHost(hostname)
.then(resolvedHostname => {
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
});
});
it('fetches ip address', () => {
const hostname = null;
return resolveHost(hostname)
.then(resolvedHostname => {
expect(resolvedHostname).to.match(/^\d+\.\d+\.\d+\.\d+$/);
});
});
it('does nothing', () => {
const hostname = '127.0.0.1';
return resolveHost(hostname)
.then(resolvedHostname => {
expect(resolvedHostname).to.equal(hostname);
});
});
it('fails on getting hostname', () => {
sandbox.stub(require('http'), 'get').callsFake(function (url, cb) {
cb({
statusCode: 420
});
});
return resolveHost(null)
.then(() => expect(1).to.equal(2))
.catch(err => {
expect(err.code).to.equal(420);
});
});
});

Some files were not shown because too many files have changed in this diff Show More