Compare commits
180 Commits
v2.0.1
...
update-doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90b4b78ddc | ||
|
|
5e20556a0e | ||
|
|
74760189ed | ||
|
|
c60606971a | ||
|
|
bd41b31821 | ||
|
|
ce1c526c41 | ||
|
|
d822101a07 | ||
|
|
47c8eedd3b | ||
|
|
6c08cc2aed | ||
|
|
e2a5c78b0a | ||
|
|
2cadac3f7e | ||
|
|
2255be9acd | ||
|
|
d22c911a36 | ||
|
|
5dabbc251b | ||
|
|
ef89577627 | ||
|
|
8fbe750086 | ||
|
|
3b33508f44 | ||
|
|
23368b04b9 | ||
|
|
876a061e92 | ||
|
|
65b1fd27a0 | ||
|
|
286c1063fa | ||
|
|
e87c36d7ff | ||
|
|
de0aafad2f | ||
|
|
4f80e11745 | ||
|
|
6bbd905379 | ||
|
|
de50f55457 | ||
|
|
32cdedd163 | ||
|
|
6c2c1a87dc | ||
|
|
9e83143690 | ||
|
|
0238529edf | ||
|
|
d0c204eb81 | ||
|
|
cdebe9a464 | ||
|
|
eeb8f9ab4d | ||
|
|
60d06c21c8 | ||
|
|
8609b1d02e | ||
|
|
80b05215ff | ||
|
|
37f0a15549 | ||
|
|
1ba67034b1 | ||
|
|
0a331c5998 | ||
|
|
a7103ded7e | ||
|
|
d787d4cab6 | ||
|
|
154cd5a5d7 | ||
|
|
5fc59b50b1 | ||
|
|
043c97c80f | ||
|
|
772fe5ca06 | ||
|
|
e272802525 | ||
|
|
7589322abc | ||
|
|
fae5564041 | ||
|
|
e9b4a6385d | ||
|
|
71621aae4f | ||
|
|
0eaa0f8743 | ||
|
|
8828a4ea09 | ||
|
|
b33659320f | ||
|
|
6a6b949d3b | ||
|
|
283be85db3 | ||
|
|
e555ce9230 | ||
|
|
e6575808f1 | ||
|
|
a5e58a106e | ||
|
|
ed086e576a | ||
|
|
31f0f3b0dc | ||
|
|
d763820c86 | ||
|
|
f3183314cc | ||
|
|
dde7b36c46 | ||
|
|
00af9e7e61 | ||
|
|
99a885cd44 | ||
|
|
443051d753 | ||
|
|
27ecc4d835 | ||
|
|
c8526be1f4 | ||
|
|
e0b11ff480 | ||
|
|
58b9d8db9d | ||
|
|
fa121ba0fd | ||
|
|
2e02dc20ad | ||
|
|
8aeb6976d2 | ||
|
|
84a68ae03c | ||
|
|
9dfc80b99d | ||
|
|
090e3d8105 | ||
|
|
c3b0dbf5b0 | ||
|
|
69a5133936 | ||
|
|
5394908a6b | ||
|
|
3e7bd5bcf9 | ||
|
|
175b422c5f | ||
|
|
b2a9851204 | ||
|
|
977dd1579a | ||
|
|
176b2b7ca8 | ||
|
|
63777c0d74 | ||
|
|
9be8ffa60d | ||
|
|
b8cd6022e1 | ||
|
|
0618a3c675 | ||
|
|
3c533a5fbc | ||
|
|
3d0a58ca15 | ||
|
|
4b4c809af8 | ||
|
|
a234534de0 | ||
|
|
635fb35341 | ||
|
|
51a6448ac2 | ||
|
|
4d8a69615c | ||
|
|
ab5a2e9641 | ||
|
|
a7f25accd2 | ||
|
|
c49a361c36 | ||
|
|
d3d65aa5cf | ||
|
|
e53848f881 | ||
|
|
b5cf75b09f | ||
|
|
dd0a790519 | ||
|
|
e25ee55865 | ||
|
|
0433bb48cd | ||
|
|
350c2b3e81 | ||
|
|
887bf1fa58 | ||
|
|
70a62f1da1 | ||
|
|
3a1afdb694 | ||
|
|
10127b32e5 | ||
|
|
e1aad3e021 | ||
|
|
a254e6c5f3 | ||
|
|
c46d6086ea | ||
|
|
88f02cd498 | ||
|
|
2cc5d54d7f | ||
|
|
f127d0e7b6 | ||
|
|
13048a96bd | ||
|
|
f6355e66c3 | ||
|
|
6e79e958cc | ||
|
|
db7d88f411 | ||
|
|
323ee62110 | ||
|
|
1e446a7801 | ||
|
|
977fbd4190 | ||
|
|
d5d1b98b04 | ||
|
|
df0a4d640c | ||
|
|
73274191fe | ||
|
|
37c3da3a62 | ||
|
|
9bece5f946 | ||
|
|
83947142df | ||
|
|
c54045e0b9 | ||
|
|
cf71243729 | ||
|
|
7fb43a5790 | ||
|
|
e99059125e | ||
|
|
954e9a1252 | ||
|
|
2b9e163958 | ||
|
|
c6a49d2191 | ||
|
|
14e5f87cc3 | ||
|
|
580b8d6eae | ||
|
|
a75d63df92 | ||
|
|
301ae110e8 | ||
|
|
4d69b48466 | ||
|
|
ec010697bb | ||
|
|
cf3d543f1a | ||
|
|
69bec2b01c | ||
|
|
2eac41d127 | ||
|
|
eb32f93fc6 | ||
|
|
095423606e | ||
|
|
61cf1bda39 | ||
|
|
75f847ed5d | ||
|
|
ad4b32fc13 | ||
|
|
be3c57bed0 | ||
|
|
dc7dd1075c | ||
|
|
543e6cc1cc | ||
|
|
5c1f8f7a65 | ||
|
|
557995a1a9 | ||
|
|
45eca5afe0 | ||
|
|
695e594d97 | ||
|
|
97b55fc92c | ||
|
|
577066850b | ||
|
|
0ec989cf1e | ||
|
|
568833e216 | ||
|
|
6b0c06e588 | ||
|
|
acd485a571 | ||
|
|
2b2ca45673 | ||
|
|
a62b6f9559 | ||
|
|
84d54cbc2b | ||
|
|
ef6134d91b | ||
|
|
043d9369cc | ||
|
|
6b81748fd7 | ||
|
|
0f4f5cdbd7 | ||
|
|
0293752635 | ||
|
|
aa278105f9 | ||
|
|
bbe0bf2942 | ||
|
|
846df72e24 | ||
|
|
8227c512dd | ||
|
|
b7e17af99e | ||
|
|
9276f7f448 | ||
|
|
99a0ebd536 | ||
|
|
0c5f8562d5 | ||
|
|
5154743a3a | ||
|
|
6654f2c25c |
140
.circleci/config.yml
Normal file
140
.circleci/config.yml
Normal file
@@ -0,0 +1,140 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build_node_8:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: npm-install-node-8-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Install
|
||||
command: npm install
|
||||
- save_cache:
|
||||
key: npm-install-node-8-{{ checksum "package.json" }}
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
build_node_6:
|
||||
docker:
|
||||
- image: circleci/node:6
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: npm-install-node-6-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Install
|
||||
command: npm install
|
||||
- save_cache:
|
||||
key: npm-install-node-6-{{ checksum "package.json" }}
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
lint:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: npm-install-node-8-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Lint
|
||||
command: npm run verify:js
|
||||
|
||||
test_node_8:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: npm-install-node-8-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Test Node 8
|
||||
command: npm run test:coverage
|
||||
when: always
|
||||
- store_test_results:
|
||||
path: reports
|
||||
- store_artifacts:
|
||||
path: reports/coverage
|
||||
prefix: coverage
|
||||
|
||||
test_node_6:
|
||||
docker:
|
||||
- image: circleci/node:6
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: npm-install-node-6-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Test Node 6
|
||||
command: npm run test:coverage
|
||||
when: always
|
||||
- store_test_results:
|
||||
path: reports
|
||||
- store_artifacts:
|
||||
path: reports/coverage
|
||||
prefix: coverage
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: npm-install-node-6-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Update NPM
|
||||
command: |
|
||||
npm install npm@5
|
||||
npm install semantic-release@11
|
||||
- deploy:
|
||||
name: Semantic Release
|
||||
command: |
|
||||
npm run semantic-release || true
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
test_and_tag:
|
||||
jobs:
|
||||
- build_node_8:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- build_node_6:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- lint:
|
||||
requires:
|
||||
- build_node_8
|
||||
- test_node_6:
|
||||
requires:
|
||||
- build_node_6
|
||||
- test_node_8:
|
||||
requires:
|
||||
- build_node_8
|
||||
- release:
|
||||
requires:
|
||||
- lint
|
||||
- test_node_6
|
||||
- test_node_8
|
||||
|
||||
build_and_test:
|
||||
jobs:
|
||||
- build_node_8:
|
||||
filters:
|
||||
branches:
|
||||
ignore: master
|
||||
- build_node_6:
|
||||
filters:
|
||||
branches:
|
||||
ignore: master
|
||||
- lint:
|
||||
requires:
|
||||
- build_node_8
|
||||
- test_node_6:
|
||||
requires:
|
||||
- build_node_6
|
||||
- test_node_8:
|
||||
requires:
|
||||
- build_node_8
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
package-lock.json binary
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,5 +2,6 @@ node_modules/
|
||||
|
||||
dist/
|
||||
reports/
|
||||
.env
|
||||
npm-debug.log
|
||||
.nyc_output/
|
||||
test_tmp/
|
||||
24
.nycrc
Normal file
24
.nycrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"check-coverage": true,
|
||||
"per-file": false,
|
||||
"lines": 90,
|
||||
"statements": 90,
|
||||
"functions": 85,
|
||||
"branches": 75,
|
||||
"include": [
|
||||
"src/**/*.js"
|
||||
],
|
||||
"exclude": [
|
||||
"test/**/*.spec.js"
|
||||
],
|
||||
"reporter": [
|
||||
"lcovonly",
|
||||
"html",
|
||||
"text",
|
||||
"cobertura",
|
||||
"json"
|
||||
],
|
||||
"cache": true,
|
||||
"all": true,
|
||||
"report-dir": "./reports/coverage/"
|
||||
}
|
||||
12
.travis.yml
12
.travis.yml
@@ -1,12 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
|
||||
install: npm install
|
||||
|
||||
script:
|
||||
- npm run verify:js
|
||||
- npm run test:coverage
|
||||
|
||||
after_success:
|
||||
- if [ $TRAVIS_BRANCH = 'master' ]; then npm run semantic-release; fi
|
||||
@@ -136,6 +136,9 @@ Command | Description
|
||||
Command | Description
|
||||
:------ | :----------
|
||||
<pre>npm run verify</pre> | Verify code style and syntax<ul><li>Verifies source *and test code* aginst customisable rules (unlike Webpack loaders)</li></ul>
|
||||
<pre>npm run verify:js</pre> | Verify Javascript code style and syntax
|
||||
<pre>npm run verify:js:fix</pre> | Verify Javascript code style and syntax and fix any errors that can be fixed automatically
|
||||
<pre>npm run verify:js:watch</pre> | Verify Javascript code style and syntax and watch files for changes
|
||||
<pre>npm run verify:watch</pre> | Runs verify task whenever JS or CSS code is changed
|
||||
|
||||
|
||||
|
||||
22
LICENSE
22
LICENSE
@@ -1,9 +1,21 @@
|
||||
ftp-srv Copyright (c) 2017 Tyler Stewart
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Copyright (c) 2017 Tyler Stewart
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
188
README.md
188
README.md
@@ -1,157 +1,85 @@
|
||||
# ftp-srv [](https://badge.fury.io/js/ftp-srv) [](https://travis-ci.org/stewarttylerr/ftp-srv) [](https://github.com/semantic-release/semantic-release) [](http://commitizen.github.io/cz-cli/)
|
||||
<p align="center">
|
||||
<a href="https://github.com/trs/ftp-srv">
|
||||
<img alt="ftp-srv" src="logo.png" width="600px" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
<!--[RM_DESCRIPTION]-->
|
||||
> Modern, extensible FTP Server
|
||||
<p align="center">
|
||||
Modern, extensible FTP Server
|
||||
</p>
|
||||
|
||||
<!--[]-->
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/ftp-srv">
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
|
||||
</a>
|
||||
|
||||
- [Features](#features)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
<a href="https://circleci.com/gh/trs/ftp-srv">
|
||||
<img alt="npm" src="https://img.shields.io/circleci/project/github/trs/ftp-srv.svg?style=for-the-badge" />
|
||||
</a>
|
||||
|
||||
<a href="https://coveralls.io/github/trs/ftp-srv?branch=master">
|
||||
<img alt="npm" src="https://img.shields.io/coveralls/github/trs/ftp-srv.svg?style=for-the-badge" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Synopsis
|
||||
|
||||
`ftp-srv` is an extensible FTP server solution that enables custom file systems per connection allowing the use of virtual file systems. By default, it acts like a regular FTP server. Just include it in your project and start listening.
|
||||
|
||||
## Features
|
||||
- Supports passive and active connections
|
||||
- Extensible [file system](#file-system)
|
||||
|
||||
- Passive and Active transfer support
|
||||
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
|
||||
- Extensible [file systems](#file-system) per connection
|
||||
- Promise based API
|
||||
|
||||
## Install
|
||||
|
||||
`npm install ftp-srv --save`
|
||||
```
|
||||
$ npm install ftp-srv
|
||||
```
|
||||
|
||||
`yarn add ftp-srv`
|
||||
|
||||
## Usage
|
||||
- [Options](#options)
|
||||
- [Events](#events)
|
||||
- [File System](#file-system)
|
||||
## Quick Start
|
||||
|
||||
```js
|
||||
const FtpSvr = require('ftp-srv');
|
||||
const ftpServer = new FtpSvr({ [options] ... });
|
||||
const FtpSrv = require('ftp-srv');
|
||||
|
||||
ftpServer.on('...', (data, resolve, reject) => { ... })
|
||||
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876');
|
||||
|
||||
ftpServer.on('login', ({connection, username, password}, resolve, reject) => {
|
||||
// fetch credentials from database, file, or hard coded
|
||||
database.users.fetch({username, password})
|
||||
.then(() => {
|
||||
connection.on('STOR', (err, file) => console.log(`Uploaded file: ${file}`));
|
||||
|
||||
resolve({
|
||||
root: '/'
|
||||
});
|
||||
})
|
||||
.catch(() => reject);
|
||||
});
|
||||
|
||||
ftpServer.listen()
|
||||
.then(() => { ... });
|
||||
.then(() => {
|
||||
console.log('Waiting for connections!');
|
||||
});
|
||||
```
|
||||
|
||||
### Options
|
||||
__url__ : `ftp://127.0.0.1:21`
|
||||
> Host and port to listen on and make passive connections to.
|
||||
Set the hostname to "0.0.0.0" to fetch the external IP automatically: `ftp://0.0.0.0:21`
|
||||
## API
|
||||
|
||||
__pasv_range__ : `22`
|
||||
> Minimum port or range to use for passive connections.
|
||||
Provide either a starting integer (`1000`) or a range (`1000-2000`).
|
||||
Checkout the [Documentation](/docs).
|
||||
|
||||
__anonymous__ : `false`
|
||||
> Set whether a valid username or password combination is required.
|
||||
If true, will not require the `PASS` command to be sent for login.
|
||||
|
||||
__blacklist__ : `[]`
|
||||
> Commands listed will not be allowed.
|
||||
`['RMD', 'RNFR', 'RNTO']`
|
||||
|
||||
__whitelist__ : `[]`
|
||||
> If set, no other commands are allowed except for those explicitly listed.
|
||||
`['USER', 'PASS', 'PWD']`
|
||||
|
||||
__file_format__ : `ls`
|
||||
> Format to use for [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) responses (such as with the `LIST` command).
|
||||
Possible values:
|
||||
`ls` : [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
|
||||
`ep` : [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
|
||||
`function` : pass in your own format function, returning a string:
|
||||
`function (fileStats) { ... }`
|
||||
|
||||
__log__ : `bunyan`
|
||||
> A [bunyan logger](https://github.com/trentm/node-bunyan) instance.
|
||||
|
||||
### Events
|
||||
All events emit the same structure: `({data object}, resolve, reject)`
|
||||
|
||||
__login__ : `{connection, username, password}`
|
||||
> Occurs after `PASV` (or `USER` if `options.anonymous`)
|
||||
```
|
||||
resolve({
|
||||
fs, // [optional] custom file system class
|
||||
cwd, // [optional] initial working directory (if not using custom file system),
|
||||
blacklist, // [optional] commands to forbid for this connection only
|
||||
whitelist // [optional] if set, only these commands are allowed for this connection only
|
||||
})
|
||||
```
|
||||
|
||||
### File System
|
||||
The file system can be overridden to use your own custom class. This an allow for interacting with files without actually writing them.
|
||||
|
||||
*Anytime a [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object is used, it must have added `name` property with the file's name.*
|
||||
|
||||
#### Functions
|
||||
`currentDirectory()`
|
||||
> Returns a string of the current working directory
|
||||
|
||||
> Used in: `PWD`
|
||||
|
||||
`get(fileName)`
|
||||
> Returns a file stat object of file or directory
|
||||
|
||||
> Used in: `STAT`, `SIZE`, `RNFR`, `MDTM`
|
||||
|
||||
`list(path)`
|
||||
> Returns array of file and directory stat objects
|
||||
|
||||
> Used in `LIST`, `STAT`
|
||||
|
||||
`chdir(path)`
|
||||
> Returns new directory relative to cwd
|
||||
|
||||
> Used in `CWD`, `CDUP`
|
||||
|
||||
`mkdir(path)`
|
||||
> Return a path to a newly created directory
|
||||
|
||||
> Used in `MKD`
|
||||
|
||||
`write(fileName, options)`
|
||||
> Returns a writable stream
|
||||
Options:
|
||||
`append` if true, append to existing file
|
||||
|
||||
> Used in `STOR`, `APPE`
|
||||
|
||||
`read(fileName)`
|
||||
> Returns a readable stream
|
||||
|
||||
> Used in `RETR`
|
||||
|
||||
`delete(path)`
|
||||
> Delete a file or directory
|
||||
|
||||
> Used in `DELE`
|
||||
|
||||
`rename(from, to)`
|
||||
> Rename a file or directory
|
||||
|
||||
> Used in `RNFR`, `RNTO`
|
||||
|
||||
`chmod(path)`
|
||||
> Modify a file or directory's permissions
|
||||
|
||||
> Used in `SITE CHMOD`
|
||||
|
||||
<!--[RM_CONTRIBUTING]-->
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
|
||||
<!--[]-->
|
||||
|
||||
<!--[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)
|
||||
|
||||
@@ -15,12 +15,7 @@ module.exports = {
|
||||
{value: 'WIP', name: 'WIP: Work in progress'}
|
||||
],
|
||||
|
||||
scopes: [
|
||||
{name: 'accounts'},
|
||||
{name: 'admin'},
|
||||
{name: 'exampleScope'},
|
||||
{name: 'changeMe'}
|
||||
],
|
||||
scopes: [],
|
||||
|
||||
// it needs to match the value for field type. Eg.: 'fix'
|
||||
/*
|
||||
@@ -39,5 +34,5 @@ module.exports = {
|
||||
allowBreakingChanges: ['feat', 'fix'],
|
||||
|
||||
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
|
||||
appendBranchNameToCommitMessage: true
|
||||
appendBranchNameToCommitMessage: false
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Use JS to support loading of threshold data from external file
|
||||
var coverageConfig = {
|
||||
instrumentation: {
|
||||
root: 'src/'
|
||||
},
|
||||
check: require('./thresholds.json'),
|
||||
reporting: {
|
||||
print: 'both',
|
||||
dir: 'reports/coverage/',
|
||||
reports: [
|
||||
'cobertura',
|
||||
'html',
|
||||
'lcovonly',
|
||||
'html',
|
||||
'json'
|
||||
],
|
||||
'report-config': {
|
||||
cobertura: {
|
||||
file: 'cobertura/coverage.xml'
|
||||
},
|
||||
json: {
|
||||
file: 'json/coverage.json'
|
||||
},
|
||||
lcovonly: {
|
||||
file: 'lcov/lcov.info'
|
||||
},
|
||||
text: {
|
||||
file: null
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = coverageConfig;
|
||||
@@ -1,4 +1,5 @@
|
||||
test/**/*.spec.js
|
||||
--reporter list
|
||||
--no-timeouts
|
||||
--reporter mocha-multi-reporters
|
||||
--reporter-options configFile=config/testUnit/reporters.json
|
||||
--ui bdd
|
||||
--bail
|
||||
6
config/testUnit/reporters.json
Normal file
6
config/testUnit/reporters.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"reporterEnabled": "mocha-pretty-bunyan-nyan",
|
||||
"mochaJunitReporterReporterOptions": {
|
||||
"mochaFile": "reports/junit.xml"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"global": {
|
||||
"statements": 70,
|
||||
"branches": 60,
|
||||
"functions": 80,
|
||||
"lines": 80
|
||||
},
|
||||
"each": {
|
||||
"statements": 0,
|
||||
"branches": 0,
|
||||
"functions": 0,
|
||||
"lines": 0
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,162 @@
|
||||
# START_CONFIT_GENERATED_CONTENT
|
||||
confit:
|
||||
extends: &confit-extends
|
||||
- plugin:node/recommended
|
||||
|
||||
plugins: &confit-plugins
|
||||
- node
|
||||
|
||||
env: &confit-env
|
||||
commonjs: true # For Webpack, CommonJS
|
||||
node: true
|
||||
mocha: true
|
||||
es6: true
|
||||
|
||||
globals: &confit-globals {}
|
||||
parser: &confit-parser espree
|
||||
|
||||
parserOptions: &confit-parserOptions
|
||||
ecmaVersion: 6
|
||||
sourceType: module
|
||||
ecmaFeatures:
|
||||
globalReturn: false
|
||||
impliedStrict: true
|
||||
jsx: false
|
||||
|
||||
# END_CONFIT_GENERATED_CONTENT
|
||||
|
||||
# Customise this section to meet your needs...
|
||||
|
||||
extends: *confit-extends
|
||||
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-extends" from the line above
|
||||
# <<: *confit-extends
|
||||
|
||||
plugins: *confit-plugins
|
||||
# Uncomment this next line if you need to add more items to the array, and remove the "*confit-plugins" from the line above
|
||||
# <<: *confit-extends
|
||||
|
||||
env:
|
||||
<<: *confit-env
|
||||
|
||||
globals:
|
||||
<<: *confit-globals
|
||||
|
||||
parser: *confit-parser
|
||||
|
||||
parserOptions:
|
||||
<<: *confit-parserOptions
|
||||
|
||||
rules:
|
||||
max-len:
|
||||
- warn
|
||||
- 200 # Line Length
|
||||
node/no-unpublished-require:
|
||||
- 2
|
||||
- allowModules:
|
||||
- chai
|
||||
- dotenv
|
||||
- ftp
|
||||
- sinon
|
||||
- sinon-as-promised
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
15
confit.yml
15
confit.yml
@@ -1,6 +1,6 @@
|
||||
generator-confit:
|
||||
app:
|
||||
_version: f02196cc5cb7941ca46ec46d23bd6aef0dfcaca0
|
||||
_version: 462ecd915fd9db1aef6a37c2b5ce8b58b80c18ba
|
||||
buildProfile: Latest
|
||||
copyrightOwner: Tyler Stewart
|
||||
license: MIT
|
||||
@@ -8,7 +8,7 @@ generator-confit:
|
||||
publicRepository: true
|
||||
repositoryType: GitHub
|
||||
paths:
|
||||
_version: 7f33e41600b34cd6867478d8f2b3d6b2bbd42508
|
||||
_version: 780b129e0c7e5cab7e29c4f185bcf78524593a33
|
||||
config:
|
||||
configDir: config/
|
||||
input:
|
||||
@@ -18,22 +18,23 @@ generator-confit:
|
||||
prodDir: dist/
|
||||
reportDir: reports/
|
||||
buildJS:
|
||||
_version: df428a706d926204228c5d9ebdbd7b49908926d9
|
||||
_version: ead8ce4280b07d696aff499a5fca1a933727582f
|
||||
framework: []
|
||||
frameworkScripts: []
|
||||
outputFormat: ES6
|
||||
sourceFormat: ES6
|
||||
entryPoint:
|
||||
_version: de20402bf85c703080ef6daf21e35325a3b9d604
|
||||
_version: 39082c3df887fbc08744dfd088c25465e7a2e3a4
|
||||
entryPoints:
|
||||
main:
|
||||
- src/index.js
|
||||
- ftp-srv.js
|
||||
testUnit:
|
||||
_version: 4472a6d59b434226f463992d3c1914c77a6a115d
|
||||
_version: 30eee42a88ee42cce4f1ae48fe0cbe81647d189a
|
||||
testDependencies: []
|
||||
testFramework: mocha
|
||||
verify:
|
||||
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
|
||||
jsCodingStandard: eslint
|
||||
jsCodingStandard: none
|
||||
documentation:
|
||||
_version: b1658da3278b16d1982212f5e8bc05348af20e0b
|
||||
generateDocs: false
|
||||
|
||||
2
docs/README.md
Normal file
2
docs/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## Documentation
|
||||
|
||||
122
ftp-srv.d.ts
vendored
Normal file
122
ftp-srv.d.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
import * as tls from 'tls'
|
||||
import { Stats } from 'fs'
|
||||
|
||||
export class FileSystem {
|
||||
|
||||
readonly connection: FtpConnection;
|
||||
readonly root: string;
|
||||
readonly cwd: string;
|
||||
|
||||
constructor(connection: FtpConnection, {root, cwd}?: {
|
||||
root: any;
|
||||
cwd: any;
|
||||
});
|
||||
|
||||
currentDirectory(): string;
|
||||
|
||||
get(fileName: string): Promise<any>;
|
||||
|
||||
list(path?: string): Promise<any>;
|
||||
|
||||
chdir(path?: string): Promise<string>;
|
||||
|
||||
write(fileName: string, {append, start}?: {
|
||||
append?: boolean;
|
||||
start?: any;
|
||||
}): any;
|
||||
|
||||
read(fileName: string, {start}?: {
|
||||
start?: any;
|
||||
}): Promise<any>;
|
||||
|
||||
delete(path: string): Promise<any>;
|
||||
|
||||
mkdir(path: string): Promise<any>;
|
||||
|
||||
rename(from: string, to: string): Promise<any>;
|
||||
|
||||
chmod(path: string, mode: string): Promise<any>;
|
||||
|
||||
getUniqueName(): string;
|
||||
}
|
||||
|
||||
export class FtpConnection {
|
||||
server: FtpServer;
|
||||
id: string;
|
||||
log: any;
|
||||
transferType: string;
|
||||
encoding: string;
|
||||
bufferSize: boolean;
|
||||
readonly ip: string;
|
||||
restByteCount: number | undefined;
|
||||
secure: boolean
|
||||
|
||||
close (code: number, message: number): Promise<any>
|
||||
login (username: string, password: string): Promise<any>
|
||||
reply (options: number | Object, ...letters: Array<any>): Promise<any>
|
||||
|
||||
}
|
||||
|
||||
export interface FtpServerOptions {
|
||||
pasv_range?: number | string,
|
||||
greeting?: string | string[],
|
||||
tls?: tls.SecureContext | false,
|
||||
anonymous?: boolean,
|
||||
blacklist?: Array<string>,
|
||||
whitelist?: Array<string>,
|
||||
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
|
||||
log?: any
|
||||
}
|
||||
|
||||
export class FtpServer {
|
||||
constructor(url: string, options?: FtpServerOptions);
|
||||
|
||||
readonly isTLS: boolean;
|
||||
|
||||
listen(): any;
|
||||
|
||||
emitPromise(action: any, ...data: any[]): Promise<any>;
|
||||
|
||||
emit(action: any, ...data: any[]): void;
|
||||
|
||||
setupTLS(_tls: boolean): boolean | {
|
||||
cert: string;
|
||||
key: string;
|
||||
ca: string
|
||||
};
|
||||
|
||||
setupGreeting(greet: string): string[];
|
||||
|
||||
setupFeaturesMessage(): string;
|
||||
|
||||
disconnectClient(id: string): Promise<any>;
|
||||
|
||||
close(): any;
|
||||
|
||||
on(event: "login", listener: (
|
||||
data: {
|
||||
connection: FtpConnection,
|
||||
username: string,
|
||||
password: string
|
||||
},
|
||||
resolve: (config: {
|
||||
fs?: FileSystem,
|
||||
root?: string,
|
||||
cwd?: string,
|
||||
blacklist?: Array<string>,
|
||||
whitelist?: Array<string>
|
||||
}) => void,
|
||||
reject: (err?: Error) => void
|
||||
) => void)
|
||||
|
||||
on(event: "client-error", listener: (
|
||||
data: {
|
||||
connection: FtpConnection,
|
||||
context: string,
|
||||
error: Error,
|
||||
}
|
||||
) => void)
|
||||
}
|
||||
|
||||
export {FtpServer as FtpSrv};
|
||||
export default FtpServer;
|
||||
6
ftp-srv.js
Normal file
6
ftp-srv.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const FtpSrv = require('./src');
|
||||
const FileSystem = require('./src/fs');
|
||||
|
||||
module.exports = FtpSrv;
|
||||
module.exports.FtpSrv = FtpSrv;
|
||||
module.exports.FileSystem = FileSystem;
|
||||
23
logo/generate.js
Normal file
23
logo/generate.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const logoPath = `file://${process.cwd()}/logo/logo.html`;
|
||||
|
||||
puppeteer.launch()
|
||||
.then(browser => {
|
||||
return browser.newPage()
|
||||
.then(page => {
|
||||
return page.goto(logoPath)
|
||||
.then(() => page);
|
||||
})
|
||||
.then(page => {
|
||||
return page.setViewport({
|
||||
width: 600,
|
||||
height: 250,
|
||||
deviceScaleFactor: 2
|
||||
})
|
||||
.then(() => page.screenshot({
|
||||
path: 'logo.png',
|
||||
omitBackground: true
|
||||
}));
|
||||
})
|
||||
.then(() => browser.close());
|
||||
});
|
||||
68
logo/logo.html
Normal file
68
logo/logo.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://fonts.googleapis.com/css?family=Overpass+Mono:700" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
}
|
||||
h1 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0px;
|
||||
width: 75vw;
|
||||
font-size: 68px;
|
||||
font-family: 'Overpass Mono', monospace;
|
||||
font-weight: bold;
|
||||
line-height: 0.8em;
|
||||
letter-spacing: -3px;
|
||||
color: #fff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-stroke: 1px #0063B1;
|
||||
text-shadow:
|
||||
3px 3px 0 #0063B1,
|
||||
-1px -1px 0 #0063B1,
|
||||
1px -1px 0 #0063B1,
|
||||
-1px 1px 0 #0063B1,
|
||||
1px 1px 0 #0063B1;
|
||||
}
|
||||
h1 > span {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
h1 > hr {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: 22px;
|
||||
border: 1px solid #0063B1;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h1>
|
||||
<span>ftp</span>
|
||||
<hr />
|
||||
<span>srv</span>
|
||||
</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
7495
package-lock.json
generated
Normal file
7495
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ftp-srv",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.0-development",
|
||||
"description": "Modern, extensible FTP Server",
|
||||
"keywords": [
|
||||
"ftp",
|
||||
@@ -8,13 +8,19 @@
|
||||
"ftp-srv",
|
||||
"ftp-svr",
|
||||
"ftpd",
|
||||
"ftpserver",
|
||||
"server"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
"ftp-srv.d.ts"
|
||||
],
|
||||
"main": "ftp-srv.js",
|
||||
"types": "./ftp-srv.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/stewarttylerr/ftp-srv"
|
||||
"url": "https://github.com/trs/ftp-srv"
|
||||
},
|
||||
"scripts": {
|
||||
"pre-release": "npm-run-all verify test:coverage build ",
|
||||
@@ -23,14 +29,14 @@
|
||||
"commitmsg": "cz-customizable-ghooks",
|
||||
"dev": "cross-env NODE_ENV=development npm run verify:watch",
|
||||
"prepush": "npm-run-all verify test:coverage --silent",
|
||||
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "npm run dev",
|
||||
"test": "npm run test:unit",
|
||||
"test:check-coverage": "cross-env NODE_ENV=test istanbul check-coverage reports/coverage/coverage.json --config config/testUnit/istanbul.js",
|
||||
"test:check-coverage": "nyc check-coverage",
|
||||
"test:coverage": "npm-run-all test:unit:once test:check-coverage --silent",
|
||||
"test:unit": "chokidar 'src/**/*.js' 'test/**/*.js' -c 'npm run test:unit:once' --initial --silent",
|
||||
"test:unit:once": "cross-env NODE_ENV=test istanbul cover --config config/testUnit/istanbul.js _mocha -- --opts config/testUnit/mocha.opts",
|
||||
"upload-coverage": "cat reports/coverage/lcov/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
|
||||
"test:unit": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts -w",
|
||||
"test:unit:once": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts",
|
||||
"upload-coverage": "cat reports/coverage/lcov.info | coveralls",
|
||||
"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",
|
||||
@@ -46,36 +52,44 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.5",
|
||||
"date-fns": "^1.28.0",
|
||||
"bluebird": "^3.5.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"lodash": "^4.17.4",
|
||||
"minimist-string": "^1.0.2",
|
||||
"uuid": "^3.0.1",
|
||||
"when": "^3.7.8"
|
||||
"moment": "^2.19.1",
|
||||
"uuid": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"@icetee/ftp": "^0.3.15",
|
||||
"chai": "^4.0.2",
|
||||
"chokidar-cli": "1.2.0",
|
||||
"coveralls": "2.11.15",
|
||||
"condition-circle": "^1.6.0",
|
||||
"coveralls": "2.13.1",
|
||||
"cross-env": "3.1.4",
|
||||
"cz-customizable": "4.0.0",
|
||||
"cz-customizable": "5.2.0",
|
||||
"cz-customizable-ghooks": "1.5.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"eslint": "3.14.1",
|
||||
"eslint-config-google": "0.7.1",
|
||||
"eslint-plugin-node": "3.0.5",
|
||||
"ftp": "^0.3.10",
|
||||
"husky": "0.13.1",
|
||||
"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": "3.2.0",
|
||||
"npm-run-all": "4.0.1",
|
||||
"rimraf": "2.5.4",
|
||||
"semantic-release": "^6.3.2",
|
||||
"sinon": "^1.17.7",
|
||||
"sinon-as-promised": "^4.0.2"
|
||||
"mocha": "3.5.0",
|
||||
"mocha-junit-reporter": "1.13.0",
|
||||
"mocha-multi-reporters": "1.1.5",
|
||||
"mocha-pretty-bunyan-nyan": "^1.0.4",
|
||||
"npm-run-all": "4.0.2",
|
||||
"nyc": "11.1.0",
|
||||
"rimraf": "2.6.1",
|
||||
"semantic-release": "^11.0.2",
|
||||
"sinon": "^2.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.x",
|
||||
"npm": ">=3.9.5"
|
||||
},
|
||||
"release": {
|
||||
"verifyConditions": "condition-circle"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const REGISTRY = require('./registry');
|
||||
|
||||
class FtpCommands {
|
||||
constructor(connection) {
|
||||
console.log(REGISTRY)
|
||||
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));
|
||||
}
|
||||
|
||||
parse(message) {
|
||||
const strippedMessage = message.replace(/"/g, '');
|
||||
const [directive, ...args] = strippedMessage.split(' ');
|
||||
const params = args.reduce(({arg, flags}, 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: _.chain(directive).trim().toUpper().value(),
|
||||
arg: params.arg.length ? params.arg.join(' ') : null,
|
||||
flags: params.flags,
|
||||
raw: message
|
||||
};
|
||||
return command;
|
||||
}
|
||||
|
||||
handle(command) {
|
||||
const log = this.connection.log.child({command});
|
||||
log.trace('Handle command');
|
||||
if (typeof command === 'string') command = this.parse(command);
|
||||
|
||||
// Obfuscate password from logs
|
||||
const logCommand = _.clone(command);
|
||||
if (logCommand.directive === 'PASS') logCommand.arg = '********';
|
||||
|
||||
const log = this.connection.log.child({directive: command.directive});
|
||||
log.trace({command: logCommand}, 'Handle command');
|
||||
|
||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
||||
return this.connection.reply(402, 'Command not allowed');
|
||||
@@ -31,7 +54,7 @@ class FtpCommands {
|
||||
const commandRegister = REGISTRY[command.directive];
|
||||
const commandFlags = _.get(commandRegister, 'flags', {});
|
||||
if (!commandFlags.no_auth && !this.connection.authenticated) {
|
||||
return this.connection.reply(530);
|
||||
return this.connection.reply(530, 'Command requires authentication');
|
||||
}
|
||||
|
||||
if (!commandRegister.handler) {
|
||||
@@ -39,7 +62,7 @@ class FtpCommands {
|
||||
}
|
||||
|
||||
const handler = commandRegister.handler.bind(this.connection);
|
||||
return when.try(handler, { log, command, previous_command: this.previousCommand })
|
||||
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
|
||||
.finally(() => {
|
||||
this.previousCommand = _.clone(command);
|
||||
});
|
||||
|
||||
14
src/commands/registration/abor.js
Normal file
14
src/commands/registration/abor.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
directive: 'ABOR',
|
||||
handler: function () {
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
return this.reply(426, {socket})
|
||||
.then(() => this.connector.end())
|
||||
.then(() => this.reply(226));
|
||||
})
|
||||
.catch(() => this.reply(225));
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Abort an active file transfer'
|
||||
};
|
||||
@@ -5,6 +5,6 @@ module.exports = {
|
||||
handler: function (args) {
|
||||
return stor.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Append to a file'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
const _ = require('lodash');
|
||||
const tls = require('tls');
|
||||
|
||||
module.exports = {
|
||||
directive: 'AUTH',
|
||||
handler: function ({command} = {}) {
|
||||
const method = _.upperCase(command._[1]);
|
||||
const method = _.upperCase(command.arg);
|
||||
|
||||
switch (method) {
|
||||
case 'TLS': return handleTLS.call(this);
|
||||
case 'SSL': return handleSSL.call(this);
|
||||
default: return this.reply(504);
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [type]',
|
||||
syntax: '{{cmd}} <type>',
|
||||
description: 'Set authentication mechanism',
|
||||
flags: {
|
||||
no_auth: true
|
||||
no_auth: true,
|
||||
feat: 'AUTH TLS'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleTLS() {
|
||||
return this.reply(504);
|
||||
}
|
||||
if (!this.server._tls) return this.reply(502);
|
||||
if (this.secure) return this.reply(202);
|
||||
|
||||
function handleSSL() {
|
||||
return this.reply(504);
|
||||
return this.reply(234)
|
||||
.then(() => {
|
||||
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 => {
|
||||
function forwardEvent() {
|
||||
this.emit.apply(this, arguments);
|
||||
}
|
||||
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
|
||||
});
|
||||
this.commandSocket = secureSocket;
|
||||
this.secure = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ const cwd = require('./cwd').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: ['CDUP', 'XCUP'],
|
||||
handler: function(args) {
|
||||
args.command._ = [args.command._[0], '..'];
|
||||
handler: function (args) {
|
||||
args.command.arg = '..';
|
||||
return cwd.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Change to Parent Directory'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.chdir(command._[1]))
|
||||
return Promise.resolve(this.fs.chdir(command.arg))
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(250, path);
|
||||
@@ -17,6 +17,6 @@ module.exports = {
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}[path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Change working directory'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'DELE',
|
||||
@@ -6,15 +6,15 @@ 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 when(this.fs.delete(command._[1]))
|
||||
return Promise.resolve(this.fs.delete(command.arg))
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Delete file'
|
||||
}
|
||||
};
|
||||
|
||||
22
src/commands/registration/eprt.js
Normal file
22
src/commands/registration/eprt.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const _ = require('lodash');
|
||||
const ActiveConnector = require('../../connector/active');
|
||||
|
||||
const FAMILY = {
|
||||
1: 4,
|
||||
2: 6
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
directive: 'EPRT',
|
||||
handler: function ({command} = {}) {
|
||||
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
|
||||
const family = FAMILY[protocol];
|
||||
if (!family) return this.reply(504, 'Unknown network protocol');
|
||||
|
||||
this.connector = new ActiveConnector(this);
|
||||
return this.connector.setupConnection(ip, port, family)
|
||||
.then(() => this.reply(200));
|
||||
},
|
||||
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
};
|
||||
16
src/commands/registration/epsv.js
Normal file
16
src/commands/registration/epsv.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'EPSV',
|
||||
handler: function () {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then(server => {
|
||||
const {port} = server.address();
|
||||
|
||||
return this.reply(229, `EPSV OK (|||${port}|)`);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [<protocol>]',
|
||||
description: 'Initiate passive mode'
|
||||
};
|
||||
@@ -9,13 +9,19 @@ module.exports = {
|
||||
const feat = _.get(registry[cmd], 'flags.feat', null);
|
||||
if (feat) return _.concat(feats, feat);
|
||||
return feats;
|
||||
}, [])
|
||||
.map(feat => ` ${feat}`);
|
||||
return this.reply(211, 'Extensions supported', ...features, 'END');
|
||||
}, ['UTF8'])
|
||||
.sort()
|
||||
.map(feat => ({
|
||||
message: ` ${feat}`,
|
||||
raw: true
|
||||
}));
|
||||
return features.length
|
||||
? this.reply(211, 'Extensions supported', ...features, 'End')
|
||||
: this.reply(211, 'No features');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Get the feature list implemented by the server',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ module.exports = {
|
||||
directive: 'HELP',
|
||||
handler: function ({command} = {}) {
|
||||
const registry = require('../registry');
|
||||
const directive = _.upperCase(command._[1]);
|
||||
const directive = _.upperCase(command.arg);
|
||||
if (directive) {
|
||||
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
|
||||
|
||||
@@ -16,9 +16,9 @@ module.exports = {
|
||||
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [command(optional)]',
|
||||
syntax: '{{cmd}} [<command>]',
|
||||
description: 'Returns usage documentation on a command if specified, else a general help document is returned',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const getFileStat = require('../../helpers/file-stat');
|
||||
|
||||
// http://cr.yp.to/ftp/list.html
|
||||
// http://cr.yp.to/ftp/list/eplf.html
|
||||
module.exports = {
|
||||
directive: 'LIST',
|
||||
handler: function ({log, command, previous_command} = {}) {
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const simple = command.directive === 'NLST';
|
||||
|
||||
let dataSocket;
|
||||
const directory = command._[1] || '.';
|
||||
const path = command.arg || '.';
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.list(directory)))
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.resolve(this.fs.get(path)))
|
||||
.then(stat => stat.isDirectory() ? Promise.resolve(this.fs.list(path)) : [stat])
|
||||
.then(files => {
|
||||
const getFileMessage = (file) => {
|
||||
const getFileMessage = file => {
|
||||
if (simple) return file.name;
|
||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
};
|
||||
@@ -31,30 +29,28 @@ module.exports = {
|
||||
return {
|
||||
raw: true,
|
||||
message,
|
||||
socket: dataSocket
|
||||
socket: this.connector.socket
|
||||
};
|
||||
})
|
||||
});
|
||||
return this.reply(150)
|
||||
.then(() => {
|
||||
if (fileList.length) return this.reply(...fileList);
|
||||
if (fileList.length) return this.reply({}, ...fileList);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return this.reply(226, 'Transfer OK');
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
.then(() => this.reply(226))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(err.code || 451, err.message || 'No directory');
|
||||
return this.reply(451, err.message || 'No directory');
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const when = require('when');
|
||||
const format = require('date-fns/format');
|
||||
const Promise = require('bluebird');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = {
|
||||
directive: 'MDTM',
|
||||
@@ -7,19 +7,19 @@ 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 when(this.fs.get(command._[1]))
|
||||
return Promise.resolve(this.fs.get(command.arg))
|
||||
.then(fileStat => {
|
||||
const modificationTime = format(fileStat.mtime, 'YYYYMMDDHHmmss.SSS');
|
||||
return this.reply(213, modificationTime)
|
||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
||||
return this.reply(213, modificationTime);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Return the last-modified time of a specified file',
|
||||
flags: {
|
||||
feat: 'MDTM'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
@@ -7,16 +7,16 @@ 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 when(this.fs.mkdir(command._[1]))
|
||||
return Promise.resolve(this.fs.mkdir(command.arg))
|
||||
.then(dir => {
|
||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}[path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Make directory'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'MODE',
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(command._[1] === 'S' ? 200 : 504);
|
||||
return this.reply(/^S$/i.test(command.arg) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} [mode]',
|
||||
syntax: '{{cmd}} <mode>',
|
||||
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,6 @@ module.exports = {
|
||||
handler: function (args) {
|
||||
return list.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
description: 'Returns a list of file names in a specified directory'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,4 +8,4 @@ module.exports = {
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const OPTIONS = {
|
||||
UTF8: utf8,
|
||||
'UTF-8': utf8
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
directive: 'OPTS',
|
||||
handler: function () {
|
||||
return this.reply(501);
|
||||
handler: function ({command} = {}) {
|
||||
if (!_.has(command, 'arg')) return this.reply(501);
|
||||
|
||||
const [_option, ...args] = command.arg.split(' ');
|
||||
const option = _.toUpper(_option);
|
||||
|
||||
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
|
||||
return OPTIONS[option].call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Select options for a feature'
|
||||
};
|
||||
|
||||
function utf8([setting] = []) {
|
||||
const getEncoding = () => {
|
||||
switch (_.toUpper(setting)) {
|
||||
case 'ON': return 'utf8';
|
||||
case 'OFF': return 'ascii';
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
const encoding = getEncoding();
|
||||
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)}`);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASS',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.username) return this.reply(503);
|
||||
if (this.username && this.authenticated &&
|
||||
_.get(this, 'server.options.anonymous') === true) return this.reply(230);
|
||||
if (this.authenticated) return this.reply(202);
|
||||
|
||||
// 332 : require account name (ACCT)
|
||||
|
||||
const password = command._[1];
|
||||
const password = command.arg;
|
||||
if (!password) return this.reply(501, 'Must provide password');
|
||||
return this.login(this.username, password)
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
@@ -19,9 +17,9 @@ module.exports = {
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [password]',
|
||||
syntax: '{{cmd}} <password>',
|
||||
description: 'Authentication password',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASV',
|
||||
handler: function ({command} = {}) {
|
||||
handler: function () {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then(server => {
|
||||
@@ -17,4 +17,4 @@ module.exports = {
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Initiate passive mode'
|
||||
}
|
||||
};
|
||||
|
||||
14
src/commands/registration/pbsz.js
Normal file
14
src/commands/registration/pbsz.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
directive: 'PBSZ',
|
||||
handler: function ({command} = {}) {
|
||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
||||
this.bufferSize = parseInt(command.arg, 10);
|
||||
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Protection Buffer Size',
|
||||
flags: {
|
||||
no_auth: true,
|
||||
feat: 'PBSZ'
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
const _ = require('lodash');
|
||||
const ActiveConnector = require('../../connector/active');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PORT',
|
||||
handler: function ({command} = {}) {
|
||||
this.connector = new ActiveConnector(this);
|
||||
const rawConnection = command._[1].split(',');
|
||||
|
||||
const rawConnection = _.get(command, 'arg', '').split(',');
|
||||
if (rawConnection.length !== 6) return this.reply(425);
|
||||
|
||||
const ip = rawConnection.slice(0, 4).join('.');
|
||||
@@ -12,10 +14,8 @@ module.exports = {
|
||||
const port = portBytes[0] * 256 + portBytes[1];
|
||||
|
||||
return this.connector.setupConnection(ip, port)
|
||||
.then(socket => {
|
||||
return this.reply(200);
|
||||
})
|
||||
.then(() => this.reply(200));
|
||||
},
|
||||
syntax: '{{cmd}} [x,x,x,x,y,y]',
|
||||
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
}
|
||||
};
|
||||
|
||||
23
src/commands/registration/prot.js
Normal file
23
src/commands/registration/prot.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PROT',
|
||||
handler: function ({command} = {}) {
|
||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
||||
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
|
||||
|
||||
switch (_.toUpper(command.arg)) {
|
||||
case 'P': return this.reply(200, 'OK');
|
||||
case 'C':
|
||||
case 'S':
|
||||
case 'E': return this.reply(536, 'Not supported');
|
||||
default: return this.reply(504);
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Data Channel Protection Level',
|
||||
flags: {
|
||||
no_auth: true,
|
||||
feat: 'PROT'
|
||||
}
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['PWD', 'XPWD'],
|
||||
handler: function ({log, command} = {}) {
|
||||
handler: function ({log} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.currentDirectory())
|
||||
return Promise.resolve(this.fs.currentDirectory())
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
@@ -15,8 +15,8 @@ module.exports = {
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
})
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Print current working directory'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'QUIT',
|
||||
handler: function () {
|
||||
return this.close(221);
|
||||
return this.close(221, 'Client called QUIT');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Disconnect',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
16
src/commands/registration/rest.js
Normal file
16
src/commands/registration/rest.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'REST',
|
||||
handler: function ({command} = {}) {
|
||||
const arg = _.get(command, 'arg');
|
||||
const byteCount = parseInt(arg, 10);
|
||||
|
||||
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
|
||||
|
||||
this.restByteCount = byteCount;
|
||||
return this.reply(350, `Resarting next transfer at ${byteCount}`);
|
||||
},
|
||||
syntax: '{{cmd}} <byte-count>',
|
||||
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RETR',
|
||||
@@ -6,36 +6,48 @@ module.exports = {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.read(command._[1])))
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.resolve(this.fs.read(command.arg, {start: this.restByteCount})))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
dataSocket.on('error', err => stream.emit('error', err));
|
||||
const destroyConnection = (connection, reject) => err => {
|
||||
if (connection) connection.destroy(err);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
stream.on('data', data => dataSocket.write(data, this.encoding));
|
||||
stream.on('end', () => resolve(this.reply(226)));
|
||||
stream.on('error', err => reject(err));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
const eventsPromise = new Promise((resolve, reject) => {
|
||||
stream.on('data', data => {
|
||||
if (stream) stream.pause();
|
||||
if (this.connector.socket) {
|
||||
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
|
||||
}
|
||||
});
|
||||
stream.once('end', () => resolve());
|
||||
stream.once('error', destroyConnection(this.connector.socket, reject));
|
||||
|
||||
this.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
});
|
||||
|
||||
this.restByteCount = 0;
|
||||
|
||||
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
|
||||
.then(() => eventsPromise)
|
||||
.finally(() => stream.destroy && stream.destroy());
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
.then(() => this.reply(226))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(551);
|
||||
return this.reply(551, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
})
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Retrieve a copy of the file'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const dele = require('./dele').handler;
|
||||
const {handler: dele} = require('./dele');
|
||||
|
||||
module.exports = {
|
||||
directive: ['RMD', 'XRMD'],
|
||||
handler: function (args) {
|
||||
return dele.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Remove a directory'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNFR',
|
||||
@@ -6,17 +6,17 @@ 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');
|
||||
|
||||
const fileName = command._[1];
|
||||
return when(this.fs.get(fileName))
|
||||
const fileName = command.arg;
|
||||
return Promise.resolve(this.fs.get(fileName))
|
||||
.then(() => {
|
||||
this.renameFrom = fileName;
|
||||
return this.reply(350);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [name]',
|
||||
syntax: '{{cmd}} <name>',
|
||||
description: 'Rename from'
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNTO',
|
||||
@@ -9,20 +9,20 @@ module.exports = {
|
||||
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const from = this.renameFrom;
|
||||
const to = command._[1];
|
||||
const to = command.arg;
|
||||
|
||||
return when(this.fs.rename(from, to))
|
||||
return Promise.resolve(this.fs.rename(from, to))
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
return this.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.renameFrom;
|
||||
})
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [name]',
|
||||
syntax: '{{cmd}} <name>',
|
||||
description: 'Rename to'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
const Promise = require('bluebird');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const [, mode, fileName] = command._;
|
||||
return this.fs.chmod(fileName, parseInt(mode, 8))
|
||||
const [mode, ...fileNameParts] = command.arg.split(' ');
|
||||
const fileName = fileNameParts.join(' ');
|
||||
return Promise.resolve(this.fs.chmod(fileName, parseInt(mode, 8)))
|
||||
.then(() => {
|
||||
return this.reply(200);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(500);
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
const Promise = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
|
||||
const registry = require('./registry');
|
||||
|
||||
module.exports = {
|
||||
directive: 'SITE',
|
||||
handler: function ({log, command} = {}) {
|
||||
const registry = require('./registry');
|
||||
let [, subverb, ...subparameters] = command._;
|
||||
subverb = _.upperCase(subverb);
|
||||
const subLog = log.child({subverb});
|
||||
const rawSubCommand = _.get(command, 'arg', '');
|
||||
const subCommand = this.commands.parse(rawSubCommand);
|
||||
const subLog = log.child({subverb: subCommand.directive});
|
||||
|
||||
if (!registry.hasOwnProperty(subverb)) return this.reply(502);
|
||||
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
|
||||
|
||||
const subCommand = {
|
||||
_: [subverb, ...subparameters],
|
||||
directive: subverb
|
||||
}
|
||||
const handler = registry[subverb].handler.bind(this);
|
||||
return when.try(handler, { log: subLog, command: subCommand });
|
||||
const handler = registry[subCommand.directive].handler.bind(this);
|
||||
return Promise.resolve(handler({log: subLog, command: subCommand}));
|
||||
},
|
||||
syntax: '{{cmd}} [subVerb] [subParams]',
|
||||
syntax: '{{cmd}} <subVerb> [...<subParams>]',
|
||||
description: 'Sends site specific commands to remote server'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'SIZE',
|
||||
@@ -6,18 +6,18 @@ 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 when(this.fs.get(command._[1]))
|
||||
return Promise.resolve(this.fs.get(command.arg))
|
||||
.then(fileStat => {
|
||||
return this.reply(213, {message: fileStat.size});
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Return the size of a file',
|
||||
flags: {
|
||||
feat: 'SIZE'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const getFileStat = require('../../helpers/file-stat');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STAT',
|
||||
handler: function (args = {}) {
|
||||
const {log, command} = args;
|
||||
const path = command._[1];
|
||||
const path = _.get(command, 'arg');
|
||||
if (path) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when(this.fs.get(path))
|
||||
return Promise.resolve(this.fs.get(path))
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) {
|
||||
return when(this.fs.list(path))
|
||||
.then(files => {
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
return {
|
||||
raw: true,
|
||||
message
|
||||
};
|
||||
})
|
||||
return this.reply(213, 'Status begin', ...fileList, 'Status end');
|
||||
})
|
||||
} else {
|
||||
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')))
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return Promise.resolve(this.fs.list(path))
|
||||
.then(stats => [213, stats]);
|
||||
}
|
||||
return [212, [stat]];
|
||||
})
|
||||
.then(([code, fileStats]) => {
|
||||
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(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(450);
|
||||
})
|
||||
return this.reply(450, err.message);
|
||||
});
|
||||
} else {
|
||||
return this.reply(211, 'Status OK');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
syntax: '{{cmd}} [<path>]',
|
||||
description: 'Returns the current status'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOR',
|
||||
@@ -7,37 +7,57 @@ module.exports = {
|
||||
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const append = command.directive === 'APPE';
|
||||
const fileName = command.arg;
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when(this.fs.write(command._[1], {append})))
|
||||
.tap(() => this.commandSocket.pause())
|
||||
.then(() => Promise.resolve(this.fs.write(fileName, {append, start: this.restByteCount})))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
stream.on('error', err => dataSocket.emit('error', err));
|
||||
const destroyConnection = (connection, reject) => err => {
|
||||
if (connection) connection.destroy(err);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
dataSocket.on('end', () => stream.end(() => resolve(this.reply(226))));
|
||||
dataSocket.on('error', err => reject(err));
|
||||
dataSocket.on('data', data => stream.write(data, this.encoding));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
const streamPromise = new Promise((resolve, reject) => {
|
||||
stream.once('error', destroyConnection(this.connector.socket, reject));
|
||||
stream.once('finish', () => resolve());
|
||||
});
|
||||
|
||||
const socketPromise = new Promise((resolve, reject) => {
|
||||
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();
|
||||
resolve();
|
||||
});
|
||||
this.connector.socket.once('error', destroyConnection(stream, reject));
|
||||
});
|
||||
|
||||
this.restByteCount = 0;
|
||||
|
||||
return this.reply(150).then(() => this.connector.socket.resume())
|
||||
.then(() => Promise.join(streamPromise, socketPromise))
|
||||
.finally(() => stream.destroy && stream.destroy());
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
.then(() => this.reply(226, fileName))
|
||||
.catch(Promise.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(553);
|
||||
return this.reply(550, err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
syntax: '{{cmd}} <path>',
|
||||
description: 'Store data as a file at the server site'
|
||||
}
|
||||
};
|
||||
|
||||
23
src/commands/registration/stou.js
Normal file
23
src/commands/registration/stou.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const Promise = require('bluebird');
|
||||
const {handler: stor} = require('./stor');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOU',
|
||||
handler: function (args) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const fileName = args.command.arg;
|
||||
return 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);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Store file uniquely'
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
module.exports = {
|
||||
directive: 'STRU',
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(command._[1] === 'F' ? 200 : 504);
|
||||
return this.reply(/^F$/i.test(command.arg) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} [structure]',
|
||||
syntax: '{{cmd}} <structure>',
|
||||
description: 'Set file transfer structure',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,4 +8,4 @@ module.exports = {
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'TYPE',
|
||||
handler: function ({command} = {}) {
|
||||
const encoding = _.upperCase(command._[1]);
|
||||
switch (encoding) {
|
||||
case 'A':
|
||||
this.encoding = 'utf-8';
|
||||
case 'I':
|
||||
case 'L':
|
||||
this.encoding = 'binary';
|
||||
return this.reply(200);
|
||||
default:
|
||||
return this.reply(501);
|
||||
if (/^A[0-9]?$/i.test(command.arg)) {
|
||||
this.transferType = 'ascii';
|
||||
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
|
||||
this.transferType = 'binary';
|
||||
} else {
|
||||
return this.reply(501);
|
||||
}
|
||||
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`);
|
||||
},
|
||||
syntax: '{{cmd}} [mode]',
|
||||
description: 'Set the transfer mode, binary (I) or utf-8 (A)'
|
||||
}
|
||||
syntax: '{{cmd}} <mode>',
|
||||
description: 'Set the transfer mode, binary (I) or ascii (A)',
|
||||
flags: {
|
||||
feat: 'TYPE A,I,L'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
module.exports = {
|
||||
directive: 'USER',
|
||||
handler: function ({log, command} = {}) {
|
||||
console.log('HANDLE USER')
|
||||
if (this.username) return this.reply(530, 'Username already set');
|
||||
this.username = command._[1];
|
||||
if (this.server.options.anonymous === true) {
|
||||
if (this.authenticated) return this.reply(230);
|
||||
|
||||
this.username = command.arg;
|
||||
if (!this.username) return this.reply(501, 'Must provide username');
|
||||
|
||||
if (this.server.options.anonymous === true && this.username === 'anonymous' ||
|
||||
this.username === this.server.options.anonymous) {
|
||||
return this.login(this.username, '@anonymous')
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err || 'Authentication failed');
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
}
|
||||
return this.reply(331);
|
||||
},
|
||||
syntax: '{{cmd}} [username]',
|
||||
syntax: '{{cmd}} <username>',
|
||||
description: 'Authentication username',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint no-return-assign: 0 */
|
||||
const commands = [
|
||||
require('./registration/abor'),
|
||||
require('./registration/allo'),
|
||||
require('./registration/appe'),
|
||||
require('./registration/auth'),
|
||||
@@ -18,6 +20,8 @@ const commands = [
|
||||
require('./registration/pasv'),
|
||||
require('./registration/port'),
|
||||
require('./registration/pwd'),
|
||||
require('./registration/quit'),
|
||||
require('./registration/rest'),
|
||||
require('./registration/retr'),
|
||||
require('./registration/rmd'),
|
||||
require('./registration/rnfr'),
|
||||
@@ -26,10 +30,15 @@ const commands = [
|
||||
require('./registration/size'),
|
||||
require('./registration/stat'),
|
||||
require('./registration/stor'),
|
||||
require('./registration/stou'),
|
||||
require('./registration/stru'),
|
||||
require('./registration/syst'),
|
||||
require('./registration/type'),
|
||||
require('./registration/user')
|
||||
require('./registration/user'),
|
||||
require('./registration/pbsz'),
|
||||
require('./registration/prot'),
|
||||
require('./registration/eprt'),
|
||||
require('./registration/epsv')
|
||||
];
|
||||
|
||||
const registry = commands.reduce((result, cmd) => {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const uuid = require('uuid');
|
||||
const when = require('when');
|
||||
const sequence = require('when/sequence');
|
||||
const parseSentence = require('minimist-string');
|
||||
const net = require('net');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const BaseConnector = require('./connector/base');
|
||||
const FileSystem = require('./fs');
|
||||
@@ -14,59 +11,78 @@ const DEFAULT_MESSAGE = require('./messages');
|
||||
class FtpConnection {
|
||||
constructor(server, options) {
|
||||
this.server = server;
|
||||
this.commandSocket = options.socket;
|
||||
this.id = uuid.v4();
|
||||
this.log = options.log.child({ftp_session_id: this.commandSocket.ftp_session_id});
|
||||
this.log = options.log.child({id: this.id, ip: this.ip});
|
||||
this.commands = new Commands(this);
|
||||
this.encoding = 'utf-8';
|
||||
this.transferType = 'binary';
|
||||
this.encoding = 'utf8';
|
||||
this.bufferSize = false;
|
||||
this._restByteCount = 0;
|
||||
this._secure = false;
|
||||
|
||||
this.connector = new BaseConnector(this);
|
||||
|
||||
this.commandSocket = options.socket;
|
||||
this.commandSocket.on('error', err => {
|
||||
console.log('error', err)
|
||||
});
|
||||
this.commandSocket.on('data', data => {
|
||||
const messages = _.compact(data.toString('utf-8').split('\r\n'));
|
||||
const handleMessage = (message) => {
|
||||
const command = parseSentence(message);
|
||||
command.directive = _.upperCase(command._[0]);
|
||||
return this.commands.handle(command);
|
||||
};
|
||||
|
||||
return sequence(messages.map(message => handleMessage.bind(this, message)));
|
||||
});
|
||||
this.commandSocket.on('timeout', () => {
|
||||
console.log('timeout')
|
||||
this.log.error(err, 'Client error');
|
||||
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
|
||||
});
|
||||
this.commandSocket.on('data', this._handleData.bind(this));
|
||||
this.commandSocket.on('timeout', () => {});
|
||||
this.commandSocket.on('close', () => {
|
||||
if (this.connector) this.connector.end();
|
||||
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
_handleData(data) {
|
||||
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
|
||||
this.log.trace(messages);
|
||||
return Promise.mapSeries(messages, message => this.commands.handle(message));
|
||||
}
|
||||
|
||||
get ip() {
|
||||
try {
|
||||
return this.dataSocket ? this.dataSocket.remoteAddress : this.commandSocket.remoteAddress;
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get restByteCount() {
|
||||
return this._restByteCount > 0 ? this._restByteCount : undefined;
|
||||
}
|
||||
set restByteCount(rbc) {
|
||||
this._restByteCount = rbc;
|
||||
}
|
||||
|
||||
get secure() {
|
||||
return this.server.isTLS || this._secure;
|
||||
}
|
||||
set secure(sec) {
|
||||
this._secure = sec;
|
||||
}
|
||||
|
||||
close(code = 421, message = 'Closing connection') {
|
||||
return when(() => {
|
||||
if (code) return this.reply(code, message);
|
||||
})
|
||||
.then(() => {
|
||||
if (this.commandSocket) this.commandSocket.end();
|
||||
});
|
||||
return Promise.resolve(code)
|
||||
.then(_code => _code && this.reply(_code, message))
|
||||
.then(() => this.commandSocket && this.commandSocket.end());
|
||||
}
|
||||
|
||||
login(username, password) {
|
||||
return when.try(() => {
|
||||
return Promise.try(() => {
|
||||
const loginListeners = this.server.listeners('login');
|
||||
if (!loginListeners || !loginListeners.length) {
|
||||
if (!this.server.options.anoymous) throw new errors.GeneralError('No "login" listener setup', 500);
|
||||
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
|
||||
} else {
|
||||
return this.server.emit('login', {connection: this, username, password});
|
||||
return this.server.emitPromise('login', {connection: this, username, password});
|
||||
}
|
||||
})
|
||||
.then(({fs, cwd, blacklist = [], whitelist = []} = {}) => {
|
||||
.then(({root, cwd, fs, blacklist = [], whitelist = []} = {}) => {
|
||||
this.authenticated = true;
|
||||
this.commands.blacklist = _.concat(this.commands.blacklist, blacklist);
|
||||
this.commands.whitelist = _.concat(this.commands.whitelist, whitelist);
|
||||
this.fs = fs || new FileSystem(this, {cwd});
|
||||
this.fs = fs || new FileSystem(this, {root, cwd});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,8 +91,8 @@ class FtpConnection {
|
||||
if (typeof options === 'number') options = {code: options}; // allow passing in code as first param
|
||||
if (!Array.isArray(letters)) letters = [letters];
|
||||
if (!letters.length) letters = [{}];
|
||||
return when.map(letters, (promise, index) => {
|
||||
return when(promise)
|
||||
return Promise.map(letters, (promise, index) => {
|
||||
return Promise.resolve(promise)
|
||||
.then(letter => {
|
||||
if (!letter) letter = {};
|
||||
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
||||
@@ -84,25 +100,24 @@ class FtpConnection {
|
||||
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
|
||||
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
|
||||
if (!letter.encoding) letter.encoding = this.encoding;
|
||||
return when(letter.message) // allow passing in a promise as a message
|
||||
return Promise.resolve(letter.message) // allow passing in a promise as a 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, index) => {
|
||||
return when.promise((resolve, reject) => {
|
||||
const seperator = !options.hasOwnProperty('eol') ?
|
||||
(letters.length - 1 === index ? ' ' : '-') :
|
||||
(options.eol ? ' ' : '-');
|
||||
const packet = !letter.raw ? _.compact([letter.code || options.code, letter.message]).join(seperator) : letter.message;
|
||||
};
|
||||
|
||||
const processLetter = letter => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (letter.socket && letter.socket.writable) {
|
||||
this.log.trace({port: letter.socket.address().port, packet}, 'Reply');
|
||||
letter.socket.write(packet + '\r\n', letter.encoding, err => {
|
||||
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
|
||||
letter.socket.write(letter.message + '\r\n', letter.encoding, err => {
|
||||
if (err) {
|
||||
this.log.error(err);
|
||||
return reject(err);
|
||||
@@ -111,10 +126,12 @@ class FtpConnection {
|
||||
});
|
||||
} else reject(new errors.SocketError('Socket not writable'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return satisfyParameters()
|
||||
.then(letters => sequence(letters.map((letter, index) => processLetter.bind(this, letter, index))))
|
||||
.then(satisfiedLetters => Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
||||
return processLetter(letter, index);
|
||||
}))
|
||||
.catch(err => {
|
||||
this.log.error(err);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const net = require('net');
|
||||
const when = require('when');
|
||||
const {Socket} = require('net');
|
||||
const tls = require('tls');
|
||||
const Promise = require('bluebird');
|
||||
const Connector = require('./base');
|
||||
|
||||
class Active extends Connector {
|
||||
@@ -8,26 +9,38 @@ class Active extends Connector {
|
||||
this.type = 'active';
|
||||
}
|
||||
|
||||
waitForConnection() {
|
||||
return when.iterate(
|
||||
() => {},
|
||||
() => this.dataSocket && this.dataSocket.connected,
|
||||
() => when().delay(250)
|
||||
).timeout(5000)
|
||||
.then(() => this.dataSocket);
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
const checkSocket = () => {
|
||||
if (this.dataSocket && this.dataSocket.connected) {
|
||||
return Promise.resolve(this.dataSocket);
|
||||
}
|
||||
return Promise.resolve().delay(delay)
|
||||
.then(() => checkSocket());
|
||||
};
|
||||
|
||||
return checkSocket().timeout(timeout);
|
||||
}
|
||||
|
||||
setupConnection(host, port) {
|
||||
const closeExistingServer = () => this.dataSocket ?
|
||||
when(this.dataSocket.destroy()) :
|
||||
when.resolve()
|
||||
setupConnection(host, port, family = 4) {
|
||||
const closeExistingServer = () => Promise.resolve(
|
||||
this.dataSocket ? this.dataSocket.destroy() : undefined);
|
||||
|
||||
return closeExistingServer()
|
||||
.then(() => {
|
||||
this.dataSocket = new net.Socket();
|
||||
this.dataSocket.setEncoding(this.encoding);
|
||||
this.dataSocket.connect({ host, port }, () => {
|
||||
this.dataSocket = new Socket();
|
||||
this.dataSocket.setEncoding(this.connection.transferType);
|
||||
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||
this.dataSocket.connect({host, port, family}, () => {
|
||||
this.dataSocket.pause();
|
||||
|
||||
if (this.connection.secure) {
|
||||
const secureContext = tls.createSecureContext(this.server._tls);
|
||||
const secureSocket = new tls.TLSSocket(this.dataSocket, {
|
||||
isServer: true,
|
||||
secureContext
|
||||
});
|
||||
this.dataSocket = secureSocket;
|
||||
}
|
||||
this.dataSocket.connected = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const errors = require('../errors');
|
||||
|
||||
class Connector {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
this.server = connection.server;
|
||||
this.log = connection.log;
|
||||
|
||||
this.dataSocket = null;
|
||||
this.dataServer = null;
|
||||
this.type = false;
|
||||
}
|
||||
|
||||
get log() {
|
||||
return this.connection.log;
|
||||
}
|
||||
|
||||
get socket() {
|
||||
return this.dataSocket;
|
||||
}
|
||||
|
||||
get server() {
|
||||
return this.connection.server;
|
||||
}
|
||||
|
||||
waitForConnection() {
|
||||
return when.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
||||
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
||||
}
|
||||
|
||||
end() {
|
||||
if (this.dataSocket) this.dataSocket.end();
|
||||
if (this.dataServer) this.dataServer.close();
|
||||
this.dataSocket = null;
|
||||
this.dataServer = null;
|
||||
const closeDataSocket = new Promise(resolve => {
|
||||
if (this.dataSocket) this.dataSocket.end();
|
||||
else resolve();
|
||||
});
|
||||
const closeDataServer = new Promise(resolve => {
|
||||
if (this.dataServer) this.dataServer.close(() => resolve());
|
||||
else resolve();
|
||||
});
|
||||
|
||||
this.connection.connector = new Connector(this.connection);
|
||||
return Promise.all([closeDataSocket, closeDataServer])
|
||||
.then(() => {
|
||||
this.dataSocket = null;
|
||||
this.dataServer = null;
|
||||
this.type = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
module.exports = Connector;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const net = require('net');
|
||||
const when = require('when');
|
||||
const tls = require('tls');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const Connector = require('./base');
|
||||
const findPort = require('../helpers/find-port');
|
||||
const errors = require('../errors');
|
||||
@@ -10,30 +12,29 @@ class Passive extends Connector {
|
||||
this.type = 'passive';
|
||||
}
|
||||
|
||||
waitForConnection() {
|
||||
if (!this.dataServer) {
|
||||
return when.reject(new errors.ConnectorError('Passive server not setup'));
|
||||
}
|
||||
return when.iterate(
|
||||
() => {},
|
||||
() => this.dataServer && this.dataServer.listening && this.dataSocket,
|
||||
() => when().delay(250)
|
||||
).timeout(5000)
|
||||
.then(() => this.dataSocket);
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
|
||||
|
||||
const checkSocket = () => {
|
||||
if (this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected) {
|
||||
return Promise.resolve(this.dataSocket);
|
||||
}
|
||||
return Promise.resolve().delay(delay)
|
||||
.then(() => checkSocket());
|
||||
};
|
||||
|
||||
return checkSocket().timeout(timeout);
|
||||
}
|
||||
|
||||
setupServer() {
|
||||
const closeExistingServer = () => this.dataServer ?
|
||||
when.promise(resolve => this.dataServer.close(() => resolve())) :
|
||||
when.resolve()
|
||||
new Promise(resolve => this.dataServer.close(() => resolve())) :
|
||||
Promise.resolve();
|
||||
|
||||
return closeExistingServer()
|
||||
.then(() => this.getPort())
|
||||
.then(port => {
|
||||
this.dataSocket = null;
|
||||
this.dataServer = net.createServer({pauseOnConnect: true});
|
||||
this.dataServer.maxConnections = 1;
|
||||
this.dataServer.on('connection', socket => {
|
||||
const connectionHandler = socket => {
|
||||
if (this.connection.commandSocket.remoteAddress !== socket.remoteAddress) {
|
||||
this.log.error({
|
||||
pasv_connection: socket.remoteAddress,
|
||||
@@ -44,26 +45,41 @@ class Passive extends Connector {
|
||||
return this.connection.reply(550, 'Remote addresses do not match')
|
||||
.finally(() => this.connection.close());
|
||||
}
|
||||
this.log.info({port}, 'Passive connection fulfilled.');
|
||||
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
||||
|
||||
this.dataSocket = socket;
|
||||
this.dataSocket.setEncoding(this.connection.encoding);
|
||||
this.dataSocket.on('data', data => {
|
||||
|
||||
});
|
||||
if (this.connection.secure) {
|
||||
const secureContext = tls.createSecureContext(this.server._tls);
|
||||
const secureSocket = new tls.TLSSocket(socket, {
|
||||
isServer: true,
|
||||
secureContext
|
||||
});
|
||||
this.dataSocket = secureSocket;
|
||||
} else {
|
||||
this.dataSocket = socket;
|
||||
}
|
||||
this.dataSocket.connected = true;
|
||||
this.dataSocket.setEncoding(this.connection.transferType);
|
||||
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
||||
this.dataSocket.on('close', () => {
|
||||
this.log.trace('Passive connection closed');
|
||||
this.end();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.dataSocket = null;
|
||||
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
|
||||
this.dataServer.maxConnections = 1;
|
||||
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
|
||||
this.dataServer.on('close', () => {
|
||||
this.log.trace('Passive server closed');
|
||||
this.dataServer = null;
|
||||
});
|
||||
|
||||
return when.promise((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataServer.listen(port, err => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
this.log.info({port}, 'Passive connection listening');
|
||||
this.log.debug({port}, 'Passive connection listening');
|
||||
resolve(this.dataServer);
|
||||
}
|
||||
});
|
||||
@@ -77,8 +93,9 @@ class Passive extends Connector {
|
||||
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
|
||||
[this.server.options.pasv_range];
|
||||
return findPort(min, max);
|
||||
} else return undefined;
|
||||
};
|
||||
}
|
||||
throw new errors.ConnectorError('Invalid pasv_range');
|
||||
}
|
||||
|
||||
}
|
||||
module.exports = Passive;
|
||||
|
||||
97
src/fs.js
97
src/fs.js
@@ -1,17 +1,27 @@
|
||||
const _ = require('lodash');
|
||||
const nodePath = require('path');
|
||||
const when = require('when');
|
||||
const whenNode = require('when/node');
|
||||
const syncFs = require('fs');
|
||||
const fs = whenNode.liftAll(syncFs);
|
||||
const uuid = require('uuid');
|
||||
const Promise = require('bluebird');
|
||||
const fs = Promise.promisifyAll(require('fs'));
|
||||
const errors = require('./errors');
|
||||
|
||||
class FileSystem {
|
||||
constructor(connection, {
|
||||
cwd = '/'
|
||||
} = {}) {
|
||||
constructor(connection, {root, cwd} = {}) {
|
||||
this.connection = connection;
|
||||
this.cwd = cwd;
|
||||
this.cwd = cwd || nodePath.sep;
|
||||
this.root = root || process.cwd();
|
||||
}
|
||||
|
||||
_resolvePath(path = '') {
|
||||
const isFromRoot = _.startsWith(path, '/') || _.startsWith(path, nodePath.sep);
|
||||
const cwd = isFromRoot ? nodePath.sep : this.cwd || nodePath.sep;
|
||||
const serverPath = nodePath.join(nodePath.sep, cwd, path);
|
||||
const fsPath = nodePath.join(this.root, serverPath);
|
||||
|
||||
return {
|
||||
serverPath,
|
||||
fsPath
|
||||
};
|
||||
}
|
||||
|
||||
currentDirectory() {
|
||||
@@ -19,20 +29,20 @@ class FileSystem {
|
||||
}
|
||||
|
||||
get(fileName) {
|
||||
const path = nodePath.resolve(this.cwd, fileName);
|
||||
return fs.stat(path)
|
||||
const {fsPath} = this._resolvePath(fileName);
|
||||
return fs.statAsync(fsPath)
|
||||
.then(stat => _.set(stat, 'name', fileName));
|
||||
}
|
||||
|
||||
list(path = '.') {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.readdir(path)
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.readdirAsync(fsPath)
|
||||
.then(fileNames => {
|
||||
return when.map(fileNames, fileName => {
|
||||
const filePath = nodePath.join(path, fileName);
|
||||
return fs.access(filePath, syncFs.constants.F_OK)
|
||||
return Promise.map(fileNames, fileName => {
|
||||
const filePath = nodePath.join(fsPath, fileName);
|
||||
return fs.accessAsync(filePath, fs.constants.F_OK)
|
||||
.then(() => {
|
||||
return fs.stat(filePath)
|
||||
return fs.statAsync(filePath)
|
||||
.then(stat => _.set(stat, 'name', fileName));
|
||||
})
|
||||
.catch(() => null);
|
||||
@@ -42,60 +52,65 @@ class FileSystem {
|
||||
}
|
||||
|
||||
chdir(path = '.') {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.stat(path)
|
||||
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 = path;
|
||||
return this.cwd;
|
||||
this.cwd = serverPath;
|
||||
return this.currentDirectory();
|
||||
});
|
||||
}
|
||||
|
||||
write(fileName, {append = false} = {}) {
|
||||
const path = nodePath.resolve(this.cwd, fileName);
|
||||
const stream = syncFs.createWriteStream(path, {flags: !append ? 'w+' : 'a+'});
|
||||
stream.on('error', () => fs.unlink(path));
|
||||
write(fileName, {append = false, start = undefined} = {}) {
|
||||
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;
|
||||
}
|
||||
|
||||
read(fileName) {
|
||||
const path = nodePath.resolve(this.cwd, fileName);
|
||||
return fs.stat(path)
|
||||
read(fileName, {start = undefined} = {}) {
|
||||
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 = syncFs.createReadStream(path, {flags: 'r'});
|
||||
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
|
||||
return stream;
|
||||
});
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.stat(path)
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.statAsync(fsPath)
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) return fs.rmdir(path);
|
||||
else return fs.unlink(path);
|
||||
})
|
||||
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
|
||||
else return fs.unlinkAsync(fsPath);
|
||||
});
|
||||
}
|
||||
|
||||
mkdir(path) {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.mkdir(path)
|
||||
.then(() => path);
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.mkdirAsync(fsPath)
|
||||
.then(() => fsPath);
|
||||
}
|
||||
|
||||
rename(from, to) {
|
||||
const fromPath = nodePath.resolve(this.cwd, from);
|
||||
const toPath = nodePath.resolve(this.cwd, to);
|
||||
return fs.rename(fromPath, toPath);
|
||||
const {fsPath: fromPath} = this._resolvePath(from);
|
||||
const {fsPath: toPath} = this._resolvePath(to);
|
||||
return fs.renameAsync(fromPath, toPath);
|
||||
}
|
||||
|
||||
chmod(path, mode) {
|
||||
path = nodePath.resolve(this.cwd, path);
|
||||
return fs.chmod(path, mode);
|
||||
const {fsPath} = this._resolvePath(path);
|
||||
return fs.chmodAsync(fsPath, mode);
|
||||
}
|
||||
|
||||
getUniqueName() {
|
||||
return uuid.v4().replace(/\W/g, '');
|
||||
}
|
||||
}
|
||||
module.exports = FileSystem;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = function (path) {
|
||||
return path
|
||||
.replace(/"/g, '""');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
const _ = require('lodash');
|
||||
const format = require('date-fns/format');
|
||||
const moment = require('moment');
|
||||
const errors = require('../errors');
|
||||
|
||||
const FORMATS = {
|
||||
ls,
|
||||
ep
|
||||
};
|
||||
|
||||
module.exports = function (fileStat, format = 'ls') {
|
||||
if (typeof format === 'function') return format(fileStat);
|
||||
|
||||
const formats = {
|
||||
'ls': ls,
|
||||
'ep': ep
|
||||
};
|
||||
if (!formats.hasOwnProperty(format)) {
|
||||
if (!FORMATS.hasOwnProperty(format)) {
|
||||
throw new errors.FileSystemError('Bad file stat formatter');
|
||||
}
|
||||
return formats[format](fileStat);
|
||||
}
|
||||
return FORMATS[format](fileStat);
|
||||
};
|
||||
|
||||
function ls(fileStat) {
|
||||
const now = moment.utc();
|
||||
const mtime = moment.utc(new Date(fileStat.mtime));
|
||||
const dateFormat = now.diff(mtime, 'months') < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
|
||||
|
||||
return [
|
||||
fileStat.mode !== null
|
||||
? [
|
||||
fileStat.isDirectory() ? 'd' : '-',
|
||||
400 & fileStat.mode ? 'r' : '-',
|
||||
200 & fileStat.mode ? 'w' : '-',
|
||||
100 & fileStat.mode ? 'x' : '-',
|
||||
40 & fileStat.mode ? 'r' : '-',
|
||||
20 & fileStat.mode ? 'w' : '-',
|
||||
10 & fileStat.mode ? 'x' : '-',
|
||||
4 & fileStat.mode ? 'r' : '-',
|
||||
2 & fileStat.mode ? 'w' : '-',
|
||||
1 & fileStat.mode ? 'x' : '-'
|
||||
].join('')
|
||||
: fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
|
||||
fileStat.mode ? [
|
||||
fileStat.isDirectory() ? 'd' : '-',
|
||||
fileStat.mode & 256 ? 'r' : '-',
|
||||
fileStat.mode & 128 ? 'w' : '-',
|
||||
fileStat.mode & 64 ? 'x' : '-',
|
||||
fileStat.mode & 32 ? 'r' : '-',
|
||||
fileStat.mode & 16 ? 'w' : '-',
|
||||
fileStat.mode & 8 ? 'x' : '-',
|
||||
fileStat.mode & 4 ? 'r' : '-',
|
||||
fileStat.mode & 2 ? 'w' : '-',
|
||||
fileStat.mode & 1 ? 'x' : '-'
|
||||
].join('') : fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
|
||||
'1',
|
||||
fileStat.uid,
|
||||
fileStat.gid,
|
||||
fileStat.uid || 1,
|
||||
fileStat.gid || 1,
|
||||
_.padStart(fileStat.size, 12),
|
||||
_.padStart(format(fileStat.mtime, 'MMM DD HH:mm'), 12),
|
||||
_.padStart(mtime.format(dateFormat), 12),
|
||||
fileStat.name
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function ep(fileStat) {
|
||||
const facts = [
|
||||
const facts = _.compact([
|
||||
fileStat.dev && fileStat.ino ? `i${fileStat.dev.toString(16)}.${fileStat.ino.toString(16)}` : null,
|
||||
fileStat.size ? `s${fileStat.size}` : null,
|
||||
fileStat.mtime ? `m${format(fileStat.mtime, 'X')}` : null,
|
||||
fileStat.mode ? `up${fileStat.mode.toString(8).substr(fileStat.mode.toString(8).length - 3)}` : null,
|
||||
fileStat.isDirectory() ? 'r' : '/'
|
||||
].join(',');
|
||||
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
|
||||
fileStat.mode ? `up${(fileStat.mode & 4095).toString(8)}` : null,
|
||||
fileStat.isDirectory() ? '/' : 'r'
|
||||
]).join(',');
|
||||
return `+${facts}\t${fileStat.name}`;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
const net = require('net');
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const errors = require('../errors');
|
||||
|
||||
module.exports = function (min = 22, max = undefined) {
|
||||
return when.promise((resolve, reject) => {
|
||||
let port = min;
|
||||
module.exports = function (min = 1, max = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let checkPort = min;
|
||||
let portCheckServer = net.createServer();
|
||||
portCheckServer.maxConnections = 0;
|
||||
portCheckServer.on('error', () => {
|
||||
if (!max || port < max) {
|
||||
port = port + 1;
|
||||
portCheckServer.listen(port);
|
||||
if (checkPort < 65535 && (!max || checkPort < max)) {
|
||||
checkPort = checkPort + 1;
|
||||
portCheckServer.listen(checkPort);
|
||||
} else {
|
||||
reject(new errors.GeneralError('Unable to find open port', 500));
|
||||
}
|
||||
})
|
||||
});
|
||||
portCheckServer.on('listening', () => {
|
||||
const {port} = portCheckServer.address();
|
||||
portCheckServer.close(() => {
|
||||
@@ -22,6 +22,6 @@ module.exports = function (min = 22, max = undefined) {
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
portCheckServer.listen(port);
|
||||
portCheckServer.listen(checkPort);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
const http = require('http');
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const errors = require('../errors');
|
||||
|
||||
const IP_WEBSITE = 'http://api.ipify.org/';
|
||||
|
||||
module.exports = function (hostname) {
|
||||
return when.promise((resolve, reject) => {
|
||||
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('utf-8');
|
||||
response.setEncoding('utf8');
|
||||
response.on('data', chunk => {
|
||||
ip += chunk;
|
||||
});
|
||||
@@ -21,5 +21,5 @@ module.exports = function (hostname) {
|
||||
});
|
||||
});
|
||||
} else resolve(hostname);
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
116
src/index.js
116
src/index.js
@@ -1,8 +1,10 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const nodeUrl = require('url');
|
||||
const buyan = require('bunyan');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const fs = require('fs');
|
||||
|
||||
const Connection = require('./connection');
|
||||
const resolveHost = require('./helpers/resolve-host');
|
||||
@@ -15,92 +17,132 @@ class FtpServer {
|
||||
pasv_range: 22,
|
||||
file_format: 'ls',
|
||||
blacklist: [],
|
||||
whitelist: []
|
||||
whitelist: [],
|
||||
greeting: null,
|
||||
tls: false
|
||||
}, options);
|
||||
this._greeting = this.setupGreeting(this.options.greeting);
|
||||
this._features = this.setupFeaturesMessage();
|
||||
this._tls = this.setupTLS(this.options.tls);
|
||||
|
||||
delete this.options.greeting;
|
||||
delete this.options.tls;
|
||||
|
||||
this.connections = {};
|
||||
this.log = this.options.log;
|
||||
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
|
||||
this.server = net.createServer({pauseOnConnect: true}, socket => {
|
||||
|
||||
const serverConnectionHandler = socket => {
|
||||
let connection = new Connection(this, {log: this.log, socket});
|
||||
this.connections[connection.id] = connection;
|
||||
|
||||
socket.on('close', () => this.disconnectClient(connection.id));
|
||||
|
||||
const greeting = this.getGreetingMessage();
|
||||
const features = this.getFeaturesMessage();
|
||||
return connection.reply(220, greeting, features)
|
||||
const greeting = this._greeting || [];
|
||||
const features = this._features || 'Ready';
|
||||
return connection.reply(220, ...greeting, features)
|
||||
.finally(() => socket.resume());
|
||||
});
|
||||
this.server.on('error', err => {
|
||||
this.log.error(err);
|
||||
});
|
||||
};
|
||||
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
|
||||
|
||||
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
|
||||
this.server.on('error', err => this.log.error(err, '[Event] error'));
|
||||
this.on = this.server.on.bind(this.server);
|
||||
this.once = this.server.once.bind(this.server);
|
||||
this.listeners = this.server.listeners.bind(this.server);
|
||||
|
||||
process.on('SIGTERM', () => this.quit());
|
||||
process.on('SIGINT', () => this.quit());
|
||||
process.on('SIGQUIT', () => this.quit());
|
||||
}
|
||||
|
||||
get isTLS() {
|
||||
return this.url.protocol === 'ftps:' && this._tls;
|
||||
}
|
||||
|
||||
listen() {
|
||||
return resolveHost(this.url.hostname)
|
||||
.then(hostname => {
|
||||
this.url.hostname = hostname;
|
||||
return when.promise((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.listen(this.url.port, err => {
|
||||
if (err) return reject(err);
|
||||
this.log.info({port: this.url.port}, 'Listening');
|
||||
resolve();
|
||||
this.log.info({
|
||||
protocol: this.url.protocol.replace(/\W/g, ''),
|
||||
ip: this.url.hostname,
|
||||
port: this.url.port
|
||||
}, 'Listening');
|
||||
resolve('Listening');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
emitPromise(action, ...data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = _.concat(data, [resolve, reject]);
|
||||
this.server.emit(action, ...params);
|
||||
});
|
||||
}
|
||||
|
||||
emit(action, ...data) {
|
||||
const defer = when.defer();
|
||||
const params = _.concat(data, [defer.resolve, defer.reject]);
|
||||
this.server.emit(action, ...params);
|
||||
return defer.promise;
|
||||
this.server.emit(action, ...data);
|
||||
}
|
||||
|
||||
getGreetingMessage() {
|
||||
return null;
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
getFeaturesMessage() {
|
||||
setupGreeting(greet) {
|
||||
if (!greet) return [];
|
||||
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
|
||||
return greeting;
|
||||
}
|
||||
|
||||
setupFeaturesMessage() {
|
||||
let features = [];
|
||||
if (this.options.anonymous) features.push('a');
|
||||
|
||||
if (features.length) {
|
||||
features.unshift('Features:');
|
||||
features.push('.')
|
||||
features.push('.');
|
||||
}
|
||||
return features.length ? features.join(' ') : 'Ready';
|
||||
}
|
||||
|
||||
setGreeting(gretting) {
|
||||
if (typeof greeting === 'string') {
|
||||
this.options.greeting = greeting;
|
||||
} else {
|
||||
gretting.then(greeting => {
|
||||
this.options.gretting = greeting;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
disconnectClient(id) {
|
||||
return when.promise((resolve, reject) => {
|
||||
return new Promise(resolve => {
|
||||
const client = this.connections[id];
|
||||
if (!client) return resolve();
|
||||
delete this.connections[id];
|
||||
return client.close(0);
|
||||
try {
|
||||
client.close(0);
|
||||
} catch (err) {
|
||||
this.log.error(err, 'Error closing connection', {id});
|
||||
} finally {
|
||||
resolve('Disconnected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
quit() {
|
||||
return this.close()
|
||||
.finally(() => process.exit(0));
|
||||
}
|
||||
|
||||
close() {
|
||||
this.log.info('Server closing...');
|
||||
this.server.maxConnections = 0;
|
||||
return when.map(Object.keys(this.connections), id => this.disconnectClient(id))
|
||||
.then(() => when.promise((resolve, reject) => {
|
||||
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) return reject(err);
|
||||
resolve();
|
||||
if (err) this.log.error(err, 'Error closing server');
|
||||
resolve('Closed');
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
12
test/cert/server.crt
Normal file
12
test/cert/server.crt
Normal file
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBtTCCAR4CCQDsFyLCxvy4qzANBgkqhkiG9w0BAQUFADAfMQswCQYDVQQGEwJD
|
||||
QTEQMA4GA1UECBMHQWxiZXJ0YTAeFw0xNzA1MDgyMzQzMjNaFw0xODA1MDgyMzQz
|
||||
MjNaMB8xCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdBbGJlcnRhMIGfMA0GCSqGSIb3
|
||||
DQEBAQUAA4GNADCBiQKBgQD2uOwjV7Nb7ubBsK/tHZQ5hUpaQ9QD9Jo8qj7DBOia
|
||||
C4xbwpF6w+ZDf5OnkR7Hl3QlcofbkXKkLWmG0Mm5wvFA6kYW6D8vMT5Di7+eksf/
|
||||
agkklBnRtdoBb3lsbMbo3/EXijwHbCKbm+sTTe7dwGK/w6p782K/kgHVWk+L58O7
|
||||
rQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAOkX32keFpo0kKQKpeZgxVYjvn4/Voy6
|
||||
6oLsj7jJYq3oZts1dX6kHVpLEbF9sWKB2iz7nqz7pSN1ATq0IL/5rcxvNwiL4Idv
|
||||
F8CCBvsBui+0gwX755NJK/L57a5i8yQ5HC65NujGAA4I5+2x8HlefMVuBpEYjzQ2
|
||||
6lW2OJJ8xtP/
|
||||
-----END CERTIFICATE-----
|
||||
10
test/cert/server.csr
Normal file
10
test/cert/server.csr
Normal file
@@ -0,0 +1,10 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBXjCByAIBADAfMQswCQYDVQQGEwJDQTEQMA4GA1UECBMHQWxiZXJ0YTCBnzAN
|
||||
BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA9rjsI1ezW+7mwbCv7R2UOYVKWkPUA/Sa
|
||||
PKo+wwTomguMW8KResPmQ3+Tp5Eex5d0JXKH25FypC1phtDJucLxQOpGFug/LzE+
|
||||
Q4u/npLH/2oJJJQZ0bXaAW95bGzG6N/xF4o8B2wim5vrE03u3cBiv8Oqe/Niv5IB
|
||||
1VpPi+fDu60CAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBACzo+Wecs3CTbItrugdL
|
||||
pP4crsRs+HJljWA0e+WEGKhcd1FjrcLBr4WqzHFQJWHOTz2vM5PiKXPZk9crLxWa
|
||||
Y8kMhU6eQPnCM6+7Gffm32+VS1ipNlhzHyYsjYpgC3ROElqo0J5M5sas4lbaamr+
|
||||
FnlyRjrPSUcFdcbPL6ozND3e
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
15
test/cert/server.key
Normal file
15
test/cert/server.key
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQD2uOwjV7Nb7ubBsK/tHZQ5hUpaQ9QD9Jo8qj7DBOiaC4xbwpF6
|
||||
w+ZDf5OnkR7Hl3QlcofbkXKkLWmG0Mm5wvFA6kYW6D8vMT5Di7+eksf/agkklBnR
|
||||
tdoBb3lsbMbo3/EXijwHbCKbm+sTTe7dwGK/w6p782K/kgHVWk+L58O7rQIDAQAB
|
||||
AoGANRPVYUkVwfpkVFkBj/5kC/fb5g1fiDZQFCr/846Tx8giOv9hssqAOBczGcKD
|
||||
n6a6iu/XwGnLAvzuDd3O+BKzObrKV36u9HfvCxohKaKvhPg3lBlJ5fFq/UNBoLv8
|
||||
eHz0GUGGoCxwJBAV43ojV1GdyRZ7vdmYw2hzltsHIp7UDqECQQD7yltCCJm3+gcw
|
||||
p+Sde0+M9CubkTETPwpd3XPu6Bs5f2nxNVj6RAInPEBr5//5UY7Q9BBEHrDPsq0j
|
||||
/+gsSlWZAkEA+tjf9qRXk9JHoN3PD0xLNEgUZAsQwDic29jxb3xrGkuUjCebKRuC
|
||||
FU2sAfNgDp+MyG1iyAoZcySzH2Dp3+v7NQJAaBwBo8oelT2in3GsS5ljCSskpMxh
|
||||
+E1Gog0hFJWQPDP8wCmIwuI/6a02Def9pT8dyDRCTYhLH3YHtSzo+Pc7cQJBAJ4G
|
||||
XiD8qwc+o00eLsEOaRoIhn/30JenknmVE5QOJ1KrZmtc0Ax3fd15zvBzp4HO1Vu2
|
||||
PVKTujClYApWfT9JZDkCQQC3Ne79bb5WSsGNbg4eT+FWde4hkdpheBsWraEDN1Pp
|
||||
NanupXMPNP0EduAQ1O+oPRiZ5pG38MQYcPZHTtlULoiO
|
||||
-----END RSA PRIVATE KEY-----
|
||||
18
test/cert/server.key.org
Normal file
18
test/cert/server.key.org
Normal file
@@ -0,0 +1,18 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,A253DC8068106194
|
||||
|
||||
jrYyhujaDsASrOfb2kn5Tvb0mRyIRsz7gVdjwlUdF2+lPdA/w6Os/NQBo1BIUAJp
|
||||
lfS5KDTwiE3QPgrBXUNgpy71Yr+MSzmsYWdonGlXGtchohQKXpxtL3qOpczX3ERR
|
||||
0AZninOKOYLw9+pe/tLNZI78DHxN1X0qTXS56RFydlW3XZbnl2Ux9CGuaCVq6vwh
|
||||
yzr9H+XTqeh95wTfdXkRdFRSTyUuJ72cvMsBFRDhz60epDmUDo1XDf844BpXXfcU
|
||||
kQoXHEtNkWZqzsc4ClOopp3Bgtd7eYoOLQluyovHgXzjtsur0xeMkHn9uTfkJ+IM
|
||||
cYMS71ZKbMePS7XBt3YPLBvVXNcyYhWUP7VdGYxXTPqd1AVWciDB9S5EvfMxnnZz
|
||||
O4M4ejxV8S7fF/cGju+sRzXx9oHPfo091Q3XKV1hrsUcF+ULrA8A8rHr64bDJ3wp
|
||||
luhekzwb+5yFfZDj9XUuGMD6pSWYoWB8Jmk8cxVsdZPtGXbTQHFL9/+UZ65wSGpj
|
||||
CjTLuFyVhY8pliynZH80vsNeRycdfmx93XoLqfS4xwEmI5v/MGUF24eTpF1/VIa5
|
||||
oKDrVuERdXAn4JBKeaMratrl6p1BhkPe7VNnMUFw3U+C4x+QHISxbboUJiTcCe1C
|
||||
pT6+YYkxQJ88rKunSEXkQYt6LeYSDg8Dw5y5Oq68DmW2Rp1m4ptTbqk3+uh83vzV
|
||||
Ff0JnfNfT80GHD3T5hMgizZal2vV8DeH1WAwPzpNaeV6wTy6MSgRLgQ89cCQ0TXV
|
||||
0GYnCZCaoXA2ldvbB3vW3fweOTr8Mp7aSl4s8K0R8sT3eief9/SyWA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,30 +0,0 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
|
||||
const CMD = 'ALLO';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful', done => {
|
||||
CMDFN()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(202)
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
})
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
|
||||
const CMD = 'AUTH';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('TLS // not supported', done => {
|
||||
CMDFN({command: {_: [CMD, 'TLS'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504)
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('SSL // not supported', done => {
|
||||
CMDFN({command: {_: [CMD, 'SSL'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504)
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('bad // bad', done => {
|
||||
CMDFN({command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504)
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
|
||||
const CMD = 'HELP';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful', done => {
|
||||
CMDFN({command: {_: [CMD], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(211);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('help // successful', done => {
|
||||
CMDFN({command: {_: [CMD, 'help'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('help // successful', done => {
|
||||
CMDFN({command: {_: [CMD, 'allo'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('bad // unsuccessful', done => {
|
||||
CMDFN({command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(502);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
118
test/commands/index.spec.js
Normal file
118
test/commands/index.spec.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const {expect} = require('chai');
|
||||
const Promise = require('bluebird');
|
||||
const bunyan = require('bunyan');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const FtpCommands = require('../../src/commands');
|
||||
|
||||
describe('FtpCommands', function () {
|
||||
let sandbox;
|
||||
let commands;
|
||||
let mockConnection = {
|
||||
authenticated: false,
|
||||
log: bunyan.createLogger({name: 'FtpCommands'}),
|
||||
reply: () => Promise.resolve({}),
|
||||
server: {
|
||||
options: {
|
||||
blacklist: ['allo']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
commands = new FtpCommands(mockConnection);
|
||||
|
||||
sandbox.spy(mockConnection, 'reply');
|
||||
sandbox.spy(commands, 'handle');
|
||||
sandbox.spy(commands, 'parse');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('parse', function () {
|
||||
it('no args: test', () => {
|
||||
const cmd = commands.parse('test');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal(null);
|
||||
expect(cmd.raw).to.equal('test');
|
||||
});
|
||||
|
||||
it('one arg: test arg', () => {
|
||||
const cmd = commands.parse('test arg');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal('arg');
|
||||
expect(cmd.raw).to.equal('test arg');
|
||||
});
|
||||
|
||||
it('two args: test arg1 arg2', () => {
|
||||
const cmd = commands.parse('test arg1 arg2');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal('arg1 arg2');
|
||||
expect(cmd.raw).to.equal('test arg1 arg2');
|
||||
});
|
||||
|
||||
it('two args with quotes: test "hello world"', () => {
|
||||
const cmd = commands.parse('test "hello world"');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal('hello world');
|
||||
expect(cmd.raw).to.equal('test "hello world"');
|
||||
});
|
||||
|
||||
it('two args, with flags: test -l arg1 -A arg2 --zz88A', () => {
|
||||
const cmd = commands.parse('test -l arg1 -A arg2 --zz88A');
|
||||
expect(cmd.directive).to.equal('TEST');
|
||||
expect(cmd.arg).to.equal('arg1 arg2');
|
||||
expect(cmd.flags).to.deep.equal(['-l', '-A', '--zz88A']);
|
||||
expect(cmd.raw).to.equal('test -l arg1 -A arg2 --zz88A');
|
||||
});
|
||||
|
||||
it('one arg, with flags: list -l', () => {
|
||||
const cmd = commands.parse('list -l');
|
||||
expect(cmd.directive).to.equal('LIST');
|
||||
expect(cmd.arg).to.equal(null);
|
||||
expect(cmd.flags).to.deep.equal(['-l']);
|
||||
expect(cmd.raw).to.equal('list -l');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle', function () {
|
||||
it('fails with unsupported command', () => {
|
||||
return commands.handle('bad')
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with blacklisted command', () => {
|
||||
return commands.handle('allo')
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(502);
|
||||
expect(mockConnection.reply.args[0][1]).to.match(/blacklisted/);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with non whitelisted command', () => {
|
||||
commands.whitelist.push('USER');
|
||||
return commands.handle('auth')
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(502);
|
||||
expect(mockConnection.reply.args[0][1]).to.match(/whitelisted/);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to being unauthenticated', () => {
|
||||
return commands.handle('stor')
|
||||
.then(() => {
|
||||
expect(mockConnection.reply.callCount).to.equal(1);
|
||||
expect(mockConnection.reply.args[0][0]).to.equal(530);
|
||||
expect(mockConnection.reply.args[0][1]).to.match(/authentication/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised')(when.Promise);
|
||||
|
||||
const CMD = 'LIST';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { list: () => {} },
|
||||
connector: {
|
||||
waitForConnection: () => {},
|
||||
end: () => {}
|
||||
},
|
||||
commandSocket: {
|
||||
resume: () => {},
|
||||
pause: () => {}
|
||||
}
|
||||
};
|
||||
const mockSocket = {};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').resolves(mockSocket);
|
||||
sandbox.stub(mockClient.fs, 'list').resolves([{
|
||||
name: 'test1',
|
||||
dev: 2114,
|
||||
ino: 48064969,
|
||||
mode: 33188,
|
||||
nlink: 1,
|
||||
uid: 85,
|
||||
gid: 100,
|
||||
rdev: 0,
|
||||
size: 527,
|
||||
blksize: 4096,
|
||||
blocks: 8,
|
||||
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
isDirectory: () => false
|
||||
}]);
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('fails on no fs list command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('. // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('raw');
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('message');
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('socket');
|
||||
expect(mockClient.reply.args[2][0]).to.equal(226);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('. // unsuccessful', done => {
|
||||
mockClient.fs.list.restore();
|
||||
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
|
||||
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(451);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('. // unsuccessful (timeout)', done => {
|
||||
mockClient.connector.waitForConnection.restore();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').rejects(new when.TimeoutError());
|
||||
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
|
||||
const CMD = 'MODE';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('S // successful', done => {
|
||||
CMDFN({command: {_: [CMD, 'S']}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200)
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('Q // unsuccessful', done => {
|
||||
CMDFN({command: {_: [CMD, 'Q']}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504)
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised')(when.Promise);
|
||||
|
||||
const CMD = 'NLST';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { list: () => {} },
|
||||
connector: {
|
||||
waitForConnection: () => {},
|
||||
end: () => {}
|
||||
},
|
||||
commandSocket: {
|
||||
resume: () => {},
|
||||
pause: () => {}
|
||||
}
|
||||
};
|
||||
const mockSocket = {};
|
||||
const CMDFN = require(`../../src/commands/registration/list`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').resolves(mockSocket);
|
||||
sandbox.stub(mockClient.fs, 'list').resolves([{
|
||||
name: 'test1',
|
||||
dev: 2114,
|
||||
ino: 48064969,
|
||||
mode: 33188,
|
||||
nlink: 1,
|
||||
uid: 85,
|
||||
gid: 100,
|
||||
rdev: 0,
|
||||
size: 527,
|
||||
blksize: 4096,
|
||||
blocks: 8,
|
||||
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
isDirectory: () => false
|
||||
}]);
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('. // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('raw');
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('message');
|
||||
expect(mockClient.reply.args[1][0]).to.have.property('socket');
|
||||
expect(mockClient.reply.args[2][0]).to.equal(226);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
|
||||
const CMD = 'NOOP';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful', done => {
|
||||
CMDFN()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200)
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
})
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon')
|
||||
|
||||
const CMD = 'OPTS';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve()
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful', done => {
|
||||
CMDFN()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(501)
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
})
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised');
|
||||
|
||||
const CMD = 'PWD';
|
||||
describe(CMD, done => {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { currentDirectory: () => {} }
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves()
|
||||
sandbox.stub(mockClient.fs, 'currentDirectory').resolves();
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('fails on no fs currentDirectory command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('// successful', done => {
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('// successful', done => {
|
||||
mockClient.fs.currentDirectory.restore();
|
||||
sandbox.stub(mockClient.fs, 'currentDirectory').resolves('/test')
|
||||
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('// unsuccessful', done => {
|
||||
mockClient.fs.currentDirectory.restore();
|
||||
sandbox.stub(mockClient.fs, 'currentDirectory').rejects(new Error('Bad'))
|
||||
|
||||
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
49
test/commands/registration/abor.spec.js
Normal file
49
test/commands/registration/abor.spec.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const Promise = require('bluebird');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'ABOR';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve(),
|
||||
connector: {
|
||||
waitForConnection: () => Promise.resolve(),
|
||||
end: () => Promise.resolve()
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.spy(mockClient.connector, 'waitForConnection');
|
||||
sandbox.spy(mockClient.connector, 'end');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful | no active connection', () => {
|
||||
mockClient.connector.waitForConnection.restore();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').rejects();
|
||||
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||
expect(mockClient.connector.end.callCount).to.equal(0);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(225);
|
||||
});
|
||||
});
|
||||
|
||||
it('// successful | active connection', () => {
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||
expect(mockClient.connector.end.callCount).to.equal(1);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(426);
|
||||
expect(mockClient.reply.args[1][0]).to.equal(226);
|
||||
});
|
||||
});
|
||||
});
|
||||
28
test/commands/registration/allo.spec.js
Normal file
28
test/commands/registration/allo.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const Promise = require('bluebird');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'ALLO';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(202);
|
||||
});
|
||||
});
|
||||
});
|
||||
46
test/commands/registration/auth.spec.js
Normal file
46
test/commands/registration/auth.spec.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const Promise = require('bluebird');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'AUTH';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve(),
|
||||
server: {
|
||||
_tls: {}
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('TLS // supported', () => {
|
||||
return cmdFn({command: {arg: 'TLS', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(234);
|
||||
expect(mockClient.secure).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('SSL // not supported', () => {
|
||||
return cmdFn({command: {arg: 'SSL', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
});
|
||||
|
||||
it('bad // bad', () => {
|
||||
return cmdFn({command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,19 @@
|
||||
const when = require('when');
|
||||
const Promise = require('bluebird');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'CDUP';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => when.resolve(),
|
||||
reply: () => Promise.resolve(),
|
||||
fs: {
|
||||
chdir: () => when.resolve()
|
||||
chdir: () => Promise.resolve()
|
||||
}
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -25,13 +25,11 @@ describe(CMD, done => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('.. // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
it('.. // successful', () => {
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('..');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +1,21 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised');
|
||||
|
||||
const CMD = 'CWD';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { chdir: () => {} }
|
||||
fs: {chdir: () => {}}
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves()
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'chdir').resolves();
|
||||
});
|
||||
afterEach(() => {
|
||||
@@ -25,63 +23,56 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on no fs chdir command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
it('fails on no fs chdir command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('test // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
it('test // successful', () => {
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('test // successful', done => {
|
||||
it('test // successful', () => {
|
||||
mockClient.fs.chdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'chdir').resolves('/test');
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('test');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('bad // unsuccessful', done => {
|
||||
it('bad // unsuccessful', () => {
|
||||
mockClient.fs.chdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'))
|
||||
sandbox.stub(mockClient.fs, 'chdir').rejects(new Error('Bad'));
|
||||
|
||||
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
expect(mockClient.fs.chdir.args[0][0]).to.equal('bad');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,16 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised');
|
||||
|
||||
const CMD = 'DELE';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { delete: () => {} }
|
||||
fs: {delete: () => {}}
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -25,51 +23,45 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on no fs delete command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
it('fails on no fs delete command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('test // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
it('test // successful', () => {
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(250);
|
||||
expect(mockClient.fs.delete.args[0][0]).to.equal('test');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('bad // unsuccessful', done => {
|
||||
it('bad // unsuccessful', () => {
|
||||
mockClient.fs.delete.restore();
|
||||
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'))
|
||||
sandbox.stub(mockClient.fs, 'delete').rejects(new Error('Bad'));
|
||||
|
||||
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
expect(mockClient.fs.delete.args[0][0]).to.equal('bad');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
60
test/commands/registration/eprt.spec.js
Normal file
60
test/commands/registration/eprt.spec.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const Promise = require('bluebird');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const ActiveConnector = require('../../../src/connector/active');
|
||||
|
||||
const CMD = 'EPRT';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.stub(ActiveConnector.prototype, 'setupConnection').resolves();
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// unsuccessful | no argument', () => {
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
});
|
||||
|
||||
it('// unsuccessful | invalid argument', () => {
|
||||
return cmdFn({command: {arg: 'blah'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(504);
|
||||
});
|
||||
});
|
||||
|
||||
it('// successful IPv4', () => {
|
||||
return cmdFn({command: {arg: '|1|192.168.0.100|35286|'}})
|
||||
.then(() => {
|
||||
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
expect(ip).to.equal('192.168.0.100');
|
||||
expect(port).to.equal('35286');
|
||||
expect(family).to.equal(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('// successful IPv6', () => {
|
||||
return cmdFn({command: {arg: '|2|8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23|35286|'}})
|
||||
.then(() => {
|
||||
const [ip, port, family] = ActiveConnector.prototype.setupConnection.args[0];
|
||||
expect(mockClient.reply.args[0][0]).to.equal(200);
|
||||
expect(ip).to.equal('8536:933f:e7f3:3e91:6dc1:e8c6:8482:7b23');
|
||||
expect(port).to.equal('35286');
|
||||
expect(family).to.equal(6);
|
||||
});
|
||||
});
|
||||
});
|
||||
35
test/commands/registration/epsv.spec.js
Normal file
35
test/commands/registration/epsv.spec.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const Promise = require('bluebird');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const PassiveConnector = require('../../../src/connector/passive');
|
||||
|
||||
const CMD = 'EPSV';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(PassiveConnector.prototype, 'setupServer').resolves({
|
||||
address: () => ({port: 12345})
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful IPv4', () => {
|
||||
return cmdFn()
|
||||
.then(() => {
|
||||
const [code, message] = mockClient.reply.args[0];
|
||||
expect(code).to.equal(229);
|
||||
expect(message).to.equal('EPSV OK (|||12345|)');
|
||||
});
|
||||
});
|
||||
});
|
||||
49
test/commands/registration/help.spec.js
Normal file
49
test/commands/registration/help.spec.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const Promise = require('bluebird');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'HELP';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => Promise.resolve()
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful', () => {
|
||||
return cmdFn({command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(211);
|
||||
});
|
||||
});
|
||||
|
||||
it('help // successful', () => {
|
||||
return cmdFn({command: {arg: 'help', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||
});
|
||||
});
|
||||
|
||||
it('allo // successful', () => {
|
||||
return cmdFn({command: {arg: 'allo', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(214);
|
||||
});
|
||||
});
|
||||
|
||||
it('bad // unsuccessful', () => {
|
||||
return cmdFn({command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(502);
|
||||
});
|
||||
});
|
||||
});
|
||||
175
test/commands/registration/list.spec.js
Normal file
175
test/commands/registration/list.spec.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const Promise = require('bluebird');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'LIST';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: {
|
||||
list: () => {},
|
||||
get: () => {}
|
||||
},
|
||||
connector: {
|
||||
waitForConnection: () => Promise.resolve({}),
|
||||
end: () => {}
|
||||
},
|
||||
commandSocket: {
|
||||
resume: () => {},
|
||||
pause: () => {}
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.stub(mockClient, 'reply').resolves();
|
||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||
name: 'testdir',
|
||||
dev: 2114,
|
||||
ino: 48064969,
|
||||
mode: 33188,
|
||||
nlink: 1,
|
||||
uid: 85,
|
||||
gid: 100,
|
||||
rdev: 0,
|
||||
size: 527,
|
||||
blksize: 4096,
|
||||
blocks: 8,
|
||||
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
isDirectory: () => true
|
||||
});
|
||||
sandbox.stub(mockClient.fs, 'list').resolves([{
|
||||
name: 'test1',
|
||||
dev: 2114,
|
||||
ino: 48064969,
|
||||
mode: 33188,
|
||||
nlink: 1,
|
||||
uid: 85,
|
||||
gid: 100,
|
||||
rdev: 0,
|
||||
size: 527,
|
||||
blksize: 4096,
|
||||
blocks: 8,
|
||||
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
isDirectory: () => false
|
||||
}, {
|
||||
name: 'test2',
|
||||
dev: 2114,
|
||||
ino: 48064969,
|
||||
mode: 33188,
|
||||
nlink: 1,
|
||||
uid: 85,
|
||||
gid: 100,
|
||||
rdev: 0,
|
||||
size: 527,
|
||||
blksize: 4096,
|
||||
blocks: 8,
|
||||
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
isDirectory: () => true
|
||||
}]);
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on no fs list command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('. // successful', () => {
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1].length).to.equal(3);
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('raw');
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('message');
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('socket');
|
||||
expect(mockClient.reply.args[2][0]).to.equal(226);
|
||||
});
|
||||
});
|
||||
|
||||
it('testfile.txt // successful', () => {
|
||||
mockClient.fs.get.restore();
|
||||
sandbox.stub(mockClient.fs, 'get').resolves({
|
||||
name: 'testfile.txt',
|
||||
dev: 2114,
|
||||
ino: 48064969,
|
||||
mode: 33188,
|
||||
nlink: 1,
|
||||
uid: 85,
|
||||
gid: 100,
|
||||
rdev: 0,
|
||||
size: 527,
|
||||
blksize: 4096,
|
||||
blocks: 8,
|
||||
atime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
mtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
ctime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
birthtime: 'Mon, 10 Oct 2011 23:24:11 GMT',
|
||||
isDirectory: () => false
|
||||
});
|
||||
|
||||
return cmdFn({log, command: {directive: CMD, arg: 'testfile.txt'}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(150);
|
||||
expect(mockClient.reply.args[1].length).to.equal(2);
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('raw');
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('message');
|
||||
expect(mockClient.reply.args[1][1]).to.have.property('socket');
|
||||
expect(mockClient.reply.args[2][0]).to.equal(226);
|
||||
});
|
||||
});
|
||||
|
||||
it('. // unsuccessful', () => {
|
||||
mockClient.fs.list.restore();
|
||||
sandbox.stub(mockClient.fs, 'list').rejects(new Error());
|
||||
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(451);
|
||||
});
|
||||
});
|
||||
|
||||
it('. // unsuccessful (timeout)', () => {
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').returns(Promise.reject(new Promise.TimeoutError()));
|
||||
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(425);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,16 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised')(when.Promise);
|
||||
|
||||
const CMD = 'MDTM';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { get: () => {} }
|
||||
fs: {get: () => {}}
|
||||
};
|
||||
const mockSocket = {};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -26,50 +23,44 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on no fs get command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
it('fails on no fs get command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('. // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
it('. // successful', () => {
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(213);
|
||||
//expect(mockClient.reply.args[0][1]).to.equal('20111010172411.000');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('. // unsuccessful', done => {
|
||||
it('. // unsuccessful', () => {
|
||||
mockClient.fs.get.restore();
|
||||
sandbox.stub(mockClient.fs, 'get').rejects(new Error());
|
||||
|
||||
CMDFN({log, command: {_: [CMD], directive: CMD}})
|
||||
return cmdFn({log, command: {directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,16 @@
|
||||
const when = require('when');
|
||||
const bunyan = require('bunyan');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
require('sinon-as-promised');
|
||||
|
||||
const CMD = 'MKD';
|
||||
describe(CMD, done => {
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
let log = bunyan.createLogger({name: CMD});
|
||||
const mockClient = {
|
||||
reply: () => {},
|
||||
fs: { mkdir: () => {} }
|
||||
fs: {mkdir: () => {}}
|
||||
};
|
||||
const CMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
const cmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
@@ -25,63 +23,56 @@ describe(CMD, done => {
|
||||
});
|
||||
|
||||
describe('// check', function () {
|
||||
it('fails on no fs', done => {
|
||||
const badMockClient = { reply: () => {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
it('fails on no fs', () => {
|
||||
const badMockClient = {reply: () => {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(550);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on no fs mkdir command', done => {
|
||||
const badMockClient = { reply: () => {}, fs: {} };
|
||||
const BADCMDFN = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
it('fails on no fs mkdir command', () => {
|
||||
const badMockClient = {reply: () => {}, fs: {}};
|
||||
const badCmdFn = require(`../../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(badMockClient);
|
||||
sandbox.stub(badMockClient, 'reply').resolves();
|
||||
BADCMDFN()
|
||||
|
||||
return badCmdFn()
|
||||
.then(() => {
|
||||
expect(badMockClient.reply.args[0][0]).to.equal(402);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('test // successful', done => {
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
it('test // successful', () => {
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('test // successful', done => {
|
||||
it('test // successful', () => {
|
||||
mockClient.fs.mkdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'mkdir').resolves('test');
|
||||
CMDFN({log, command: {_: [CMD, 'test'], directive: CMD}})
|
||||
|
||||
return cmdFn({log, command: {arg: 'test', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(257);
|
||||
expect(mockClient.fs.mkdir.args[0][0]).to.equal('test');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('bad // unsuccessful', done => {
|
||||
it('bad // unsuccessful', () => {
|
||||
mockClient.fs.mkdir.restore();
|
||||
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'))
|
||||
sandbox.stub(mockClient.fs, 'mkdir').rejects(new Error('Bad'));
|
||||
|
||||
CMDFN({log, command: {_: [CMD, 'bad'], directive: CMD}})
|
||||
return cmdFn({log, command: {arg: 'bad', directive: CMD}})
|
||||
.then(() => {
|
||||
expect(mockClient.reply.args[0][0]).to.equal(550);
|
||||
expect(mockClient.fs.mkdir.args[0][0]).to.equal('bad');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user