Compare commits
173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa1e543c24 | ||
|
|
57f7fa23cc | ||
|
|
e0dbbfce2d | ||
|
|
fc8981021c | ||
|
|
cf9852465a | ||
|
|
1f3f26706e | ||
|
|
54cb2a2fe4 | ||
|
|
5fd6414e60 | ||
|
|
ef7750def0 | ||
|
|
427275a0b8 | ||
|
|
c428787ade | ||
|
|
8566df451e | ||
|
|
35789e430a | ||
|
|
a468d4ffd0 | ||
|
|
40b08893ac | ||
|
|
8a2454ceea | ||
|
|
0c7cc4fe6e | ||
|
|
6ea6baceb0 | ||
|
|
b07e0189ee | ||
|
|
ec30a5a4f3 | ||
|
|
6020409979 | ||
|
|
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 |
61
.circleci/config.yml
Normal file
61
.circleci/config.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Building...
|
||||
command: npm install
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: node_modules_{{ checksum package.json }}
|
||||
lint:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: node_modules_{{ checksum package.json }}
|
||||
- run:
|
||||
name: Linting...
|
||||
command: npm run lint
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: node_modules_{{ checksum package.json }}
|
||||
- run:
|
||||
name: Testing...
|
||||
command: npm run test
|
||||
publish:
|
||||
branches:
|
||||
only: master
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: node_modules_{{ checksum package.json }}
|
||||
- run:
|
||||
name: Publishing...
|
||||
command: npx semantic-release
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
main:
|
||||
jobs:
|
||||
- build
|
||||
- lint:
|
||||
requires:
|
||||
- build
|
||||
- test:
|
||||
requires:
|
||||
- lint
|
||||
- publish:
|
||||
requires:
|
||||
- test
|
||||
2
.config/.eslintignore
Normal file
2
.config/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/*
|
||||
config/*
|
||||
15
.config/.eslintrc.json
Normal file
15
.config/.eslintrc.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
# Editor Config - generated by Confit. This file will NOT be re-overwritten by Confit
|
||||
# Feel free to customise it further.
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# START_CONFIT_GENERATED_CONTENT
|
||||
# Common folders to ignore
|
||||
node_modules/*
|
||||
bower_components/*
|
||||
|
||||
# Config folder (optional - you might want to lint this...)
|
||||
config/*
|
||||
|
||||
# END_CONFIT_GENERATED_CONTENT
|
||||
1
.github/.gitattributes
vendored
Normal file
1
.github/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
yarn.lock binary
|
||||
2
.github/.gitignore
vendored
Normal file
2
.github/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.vscode
|
||||
3
CONTRIBUTING.md → .github/CONTRIBUTING.md
vendored
3
CONTRIBUTING.md → .github/CONTRIBUTING.md
vendored
@@ -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
|
||||
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
node_modules/
|
||||
|
||||
dist/
|
||||
reports/
|
||||
.env
|
||||
npm-debug.log
|
||||
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
|
||||
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) 2018 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.
|
||||
|
||||
197
README.md
197
README.md
@@ -1,184 +1,35 @@
|
||||
# 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>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [API](#api)
|
||||
- [Events](#events)
|
||||
- [File System](#file-system)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
<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>
|
||||
|
||||
## Overview
|
||||
> `ftp-srv` is designed to be easy, exensible, and modern.
|
||||
> Configuration is very minimal for a basic FTP server,
|
||||
but can easily grow to fit a larger scale project.
|
||||
<a href="https://circleci.com/gh/trs/ftp-srv">
|
||||
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv.svg?style=for-the-badge" />
|
||||
</a>
|
||||
|
||||
## Features
|
||||
- Supports passive and active connections
|
||||
- Allows extensible [file systems](#file-system) on a per connection basis
|
||||
<a href="https://coveralls.io/github/trs/ftp-srv?branch=master">
|
||||
<img alt="coveralls" src="https://img.shields.io/coveralls/github/trs/ftp-srv.svg?style=for-the-badge" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Install
|
||||
`npm install ftp-srv --save`
|
||||
---
|
||||
|
||||
## Usage
|
||||
> Looking for v2? Check the [v2](#v2) branch.
|
||||
|
||||
```js
|
||||
// Quick start
|
||||
# Installation
|
||||
|
||||
const FtpSvr = require('ftp-srv');
|
||||
const ftpServer = new FtpSvr(url, [{ options ... }]);
|
||||
|
||||
ftpServer.on('...', (data, resolve, reject) => { ... })
|
||||
|
||||
ftpServer.listen()
|
||||
.then(() => { ... });
|
||||
```
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
#### new FtpSrv(url, [options])
|
||||
|
||||
- __url__ :: `ftp://127.0.0.1:21`
|
||||
- A full href url, indicating the protocol, and external IP with port to listen for connections.
|
||||
- Supported protocols:
|
||||
- `ftp`
|
||||
- To accept external connections, the hostname must be the box's external IP address. This can be fetched automatically by setting the hostname to `0.0.0.0`.
|
||||
- __options__ :: `{}`
|
||||
- __pasv_range__ :: `22`
|
||||
- Starting port or min - max range to accept passive connections
|
||||
- Ports will be queried for an unused port in the range to use for the connection.
|
||||
- If none are found, the connection cannot be established
|
||||
- If an integer is supplied: will indicate the minimum allowable port
|
||||
- If a range is defined (`3000-3100`): only ports within that range will be used
|
||||
- __anonymous__ :: `false`
|
||||
- If true, will authenticate connections after passing the `USER` command. Passwords will not be required.
|
||||
- __blacklist__ :: `[]`
|
||||
- Array of commands to be blacklisted globally
|
||||
- `['RMD', 'RNFR', 'RNTO']`
|
||||
- A connection sending one of these commands will be replied with code `502`
|
||||
- __whitelist__ :: `[]`
|
||||
- If set, only commands within this array are allowed
|
||||
- A connection sending any other command will be replied to with code `502`
|
||||
- __file_format__ :: `ls`
|
||||
- Set the format to use for file stat queries, such as `LIST`
|
||||
- Possible values include:
|
||||
- `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 a function as the parameter to use your own
|
||||
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
|
||||
- __log__ :: `bunyan.createLogger()`
|
||||
- A [bunyan logger](https://github.com/trentm/node-bunyan) instance
|
||||
- By default, one is created, but a custom instance can be passed in as well
|
||||
|
||||
## Events
|
||||
|
||||
#### "login" ({connection, username, password}, resolve, reject)
|
||||
> Occurs after `PASS` command is set, or after `USER` if `anonymous` is `true`
|
||||
|
||||
- __connection__
|
||||
- Instance of the FTP client
|
||||
- __username__
|
||||
- Username provided in the `USER` command
|
||||
- __password__
|
||||
- Password provided in the `PASS` command
|
||||
- Only provided if `anonymous` is set to `false`
|
||||
- __resolve ({fs, root, cwd, blacklist, whitelist})__
|
||||
- __fs__ _[optional]_
|
||||
- Optional file system class for connection to use
|
||||
- See [File System](#file-system) for implementation details
|
||||
- __root__ _[optional]_
|
||||
- If `fs` not provided, will set the root directory for the connection
|
||||
- The user cannot traverse lower than this directory
|
||||
- __cwd__ _[optional]_
|
||||
- If `fs` not provided, will set the starting directory for the connection
|
||||
- __blacklist__ _[optional]_
|
||||
- Commands that are forbidden for this connection only
|
||||
- __whitelist__ _[optional]_
|
||||
- If set, this connection will only be able to use the provided commands
|
||||
- __reject (error)__
|
||||
- __error__
|
||||
- Error object
|
||||
|
||||
## File System
|
||||
> The default file system can be overriden to use your own implementation. This can allow for virtual file systems and more.
|
||||
> Each connection can be given it's own file system depending on the user.
|
||||
|
||||
#### 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`
|
||||
|
||||
`getUniqueName()`
|
||||
Return a unique file name to write to
|
||||
|
||||
> Used in `STOU`
|
||||
|
||||
<!--[RM_CONTRIBUTING]-->
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
|
||||
<!--[]-->
|
||||
|
||||
<!--[RM_LICENSE]-->
|
||||
## License
|
||||
|
||||
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
|
||||
|
||||
<!--[]-->
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
types: [
|
||||
{value: 'feat', name: 'feat: A new feature'},
|
||||
{value: 'fix', name: 'fix: A bug fix'},
|
||||
{value: 'docs', name: 'docs: Documentation only changes'},
|
||||
{value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'},
|
||||
{value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
|
||||
{value: 'perf', name: 'perf: A code change that improves performance'},
|
||||
{value: 'test', name: 'test: Adding missing tests'},
|
||||
{value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'},
|
||||
{value: 'revert', name: 'revert: Revert to a commit'},
|
||||
{value: 'WIP', name: 'WIP: Work in progress'}
|
||||
],
|
||||
|
||||
scopes: [
|
||||
{name: 'accounts'},
|
||||
{name: 'admin'},
|
||||
{name: 'exampleScope'},
|
||||
{name: 'changeMe'}
|
||||
],
|
||||
|
||||
// it needs to match the value for field type. Eg.: 'fix'
|
||||
/*
|
||||
scopeOverrides: {
|
||||
fix: [
|
||||
|
||||
{name: 'merge'},
|
||||
{name: 'style'},
|
||||
{name: 'e2eTest'},
|
||||
{name: 'unitTest'}
|
||||
]
|
||||
},
|
||||
*/
|
||||
|
||||
allowCustomScopes: true,
|
||||
allowBreakingChanges: ['feat', 'fix'],
|
||||
|
||||
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
|
||||
appendBranchNameToCommitMessage: true
|
||||
};
|
||||
@@ -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 +0,0 @@
|
||||
test/**/*.spec.js
|
||||
--reporter list
|
||||
--no-timeouts
|
||||
--ui bdd
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"global": {
|
||||
"statements": 70,
|
||||
"branches": 60,
|
||||
"functions": 80,
|
||||
"lines": 80
|
||||
},
|
||||
"each": {
|
||||
"statements": 0,
|
||||
"branches": 0,
|
||||
"functions": 0,
|
||||
"lines": 0
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
# 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
|
||||
48
confit.yml
48
confit.yml
@@ -1,48 +0,0 @@
|
||||
generator-confit:
|
||||
app:
|
||||
_version: f02196cc5cb7941ca46ec46d23bd6aef0dfcaca0
|
||||
buildProfile: Latest
|
||||
copyrightOwner: Tyler Stewart
|
||||
license: MIT
|
||||
projectType: node
|
||||
publicRepository: true
|
||||
repositoryType: GitHub
|
||||
paths:
|
||||
_version: 7f33e41600b34cd6867478d8f2b3d6b2bbd42508
|
||||
config:
|
||||
configDir: config/
|
||||
input:
|
||||
srcDir: src/
|
||||
unitTestDir: test/
|
||||
output:
|
||||
prodDir: dist/
|
||||
reportDir: reports/
|
||||
buildJS:
|
||||
_version: df428a706d926204228c5d9ebdbd7b49908926d9
|
||||
framework: []
|
||||
frameworkScripts: []
|
||||
outputFormat: ES6
|
||||
sourceFormat: ES6
|
||||
entryPoint:
|
||||
_version: de20402bf85c703080ef6daf21e35325a3b9d604
|
||||
entryPoints:
|
||||
main:
|
||||
- src/index.js
|
||||
testUnit:
|
||||
_version: 4472a6d59b434226f463992d3c1914c77a6a115d
|
||||
testDependencies: []
|
||||
verify:
|
||||
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
|
||||
jsCodingStandard: eslint
|
||||
documentation:
|
||||
_version: b1658da3278b16d1982212f5e8bc05348af20e0b
|
||||
generateDocs: false
|
||||
release:
|
||||
_version: 47f220593935b502abf17cb34a396f692e453c49
|
||||
checkCodeCoverage: true
|
||||
commitMessageFormat: Conventional
|
||||
useSemantic: true
|
||||
sampleApp:
|
||||
_version: 00c0a2c6fc0ed17fcccce2d548d35896121e58ba
|
||||
createSampleApp: false
|
||||
zzfinish: {}
|
||||
11
examples/basic.js
Normal file
11
examples/basic.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/* eslint no-console: 0 */
|
||||
const FtpSrv = require('../src');
|
||||
|
||||
const server = new FtpSrv();
|
||||
server.listen(8880)
|
||||
.then(() => {
|
||||
console.log('listening');
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('err', err)
|
||||
})
|
||||
19
logo/generate.js
Normal file
19
logo/generate.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async function () {
|
||||
const logoPath = `file://${process.cwd()}/logo/logo.html`;
|
||||
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto(logoPath);
|
||||
await page.setViewport({
|
||||
width: 600,
|
||||
height: 250,
|
||||
deviceScaleFactor: 2
|
||||
});
|
||||
await page.screenshot({
|
||||
path: 'logo.png',
|
||||
omitBackground: true
|
||||
});
|
||||
await 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>
|
||||
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,73 +8,29 @@
|
||||
"ftp-srv",
|
||||
"ftp-svr",
|
||||
"ftpd",
|
||||
"ftpserver",
|
||||
"server"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"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 ",
|
||||
"build": "cross-env NODE_ENV=production npm run clean:prod",
|
||||
"clean:prod": "rimraf dist/",
|
||||
"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",
|
||||
"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: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",
|
||||
"verify": "npm run verify:js --silent",
|
||||
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
|
||||
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
|
||||
"verify:js:watch": "chokidar 'src/**/*.js' 'test/**/*.js' 'config/**/*.js' -c 'npm run verify:js:fix' --initial --silent",
|
||||
"verify:watch": "npm run verify:js:watch --silent"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "node_modules/cz-customizable"
|
||||
},
|
||||
"cz-customizable": {
|
||||
"config": "config/release/commitMessageConfig.js"
|
||||
}
|
||||
"test": "jest ./src/**/*.test.js --verbose",
|
||||
"lint": "eslint -c .config/.eslintrc.json \"src/**/*.js\" \"logo/**/*.js\" \"examples/**/*.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.9",
|
||||
"lodash": "^4.17.4",
|
||||
"minimist-string": "^1.0.2",
|
||||
"moment": "^2.18.1",
|
||||
"uuid": "^3.0.1",
|
||||
"when": "^3.7.8"
|
||||
"bee-queue": "^1.2.2",
|
||||
"signale": "^1.1.0",
|
||||
"z": "^1.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"chokidar-cli": "1.2.0",
|
||||
"coveralls": "2.11.15",
|
||||
"cross-env": "3.1.4",
|
||||
"cz-customizable": "4.0.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",
|
||||
"istanbul": "0.4.5",
|
||||
"mocha": "3.2.0",
|
||||
"npm-run-all": "4.0.1",
|
||||
"rimraf": "2.5.4",
|
||||
"semantic-release": "^6.3.2",
|
||||
"sinon": "^2.1.0"
|
||||
"eslint": "^4.18.2",
|
||||
"jest": "^22.4.2",
|
||||
"semantic-release": "^15.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.x",
|
||||
"npm": ">=3.9.5"
|
||||
"node": ">=8.x"
|
||||
}
|
||||
}
|
||||
|
||||
84
src-old/Client.js
Normal file
84
src-old/Client.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const net = require('net');
|
||||
|
||||
const Queue = require('./Queue');
|
||||
const {getCommandHandler} = require('./commands');
|
||||
|
||||
class Client extends net.Socket {
|
||||
constructor(id, socket) {
|
||||
super();
|
||||
socket && Object.assign(this, socket);
|
||||
this.id = id;
|
||||
this.commandQueue = new Queue({
|
||||
[Queue.QUEUE_TYPES.IN]: () => {},
|
||||
[Queue.QUEUE_TYPES.OUT]: () => {}
|
||||
});
|
||||
this.dataQueue = new Queue();
|
||||
this.resetSession();
|
||||
|
||||
super.on('data', data => this._onData(data));
|
||||
}
|
||||
|
||||
resetSession() {
|
||||
this.session = {
|
||||
encoding: 'utf8',
|
||||
transferType: 'binary'
|
||||
};
|
||||
}
|
||||
|
||||
setSession(key, value) {
|
||||
this.session[key] = value;
|
||||
}
|
||||
|
||||
getSession(key) {
|
||||
return this.session[key];
|
||||
}
|
||||
|
||||
send(message) {
|
||||
// this.sendQueue.enqueue(message);
|
||||
}
|
||||
|
||||
get closed() {
|
||||
return this.closing || super.destroyed;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (super.destroyed) return;
|
||||
this.closing = true;
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
_onData(data) {
|
||||
if (this.closed) return;
|
||||
|
||||
const commands = data
|
||||
.toString(this.getSession('encoding'))
|
||||
.split('\r\n')
|
||||
.map(command => command.trim())
|
||||
.filter(command => !!command);
|
||||
|
||||
this.commandQueue.enqueue(Queue.QUEUE_TYPES.IN, ...commands);
|
||||
}
|
||||
|
||||
// async _processCommand(command) {
|
||||
|
||||
// this.emit('command', {command});
|
||||
|
||||
// const commandHandler = getCommandHandler(this, command);
|
||||
// if (typeof commandHandler === 'string') {
|
||||
// return this.send(commandHandler);
|
||||
// }
|
||||
|
||||
// await commandHandler(this, command);
|
||||
// }
|
||||
|
||||
// async _processSend(message) {
|
||||
// await new Promise((resolve, reject) => {
|
||||
// super.write(`${message}\r\n`, err => {
|
||||
// if (err) reject(err);
|
||||
// else resolve();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
module.exports = Client;
|
||||
33
src-old/ConnectionManager.js
Normal file
33
src-old/ConnectionManager.js
Normal file
@@ -0,0 +1,33 @@
|
||||
class ConnectionManager {
|
||||
constructor() {
|
||||
this._connections = {};
|
||||
}
|
||||
|
||||
add(id, client) {
|
||||
this._connections[id] = client;
|
||||
return true;
|
||||
}
|
||||
|
||||
remove(id) {
|
||||
if (!this._connections.hasOwnProperty(id)) return false;
|
||||
delete this._connections[id];
|
||||
return true;
|
||||
}
|
||||
|
||||
invoke(method, ...args) {
|
||||
const invokeResults = Object.values(this._connections).map(connection => {
|
||||
if (typeof connection[method] !== 'function') return undefined;
|
||||
return connection[method](...args);
|
||||
});
|
||||
return Promise.all(invokeResults);
|
||||
}
|
||||
|
||||
iterate() {
|
||||
console.log('iterate', iterate)
|
||||
const connections = Object.entires(this._connections);
|
||||
console.log('connections', connections)
|
||||
return connections
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionManager;
|
||||
40
src-old/Queue.js
Normal file
40
src-old/Queue.js
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
const QUEUE_TYPES = {
|
||||
IN: Symbol('in'),
|
||||
OUT: Symbol('out')
|
||||
}
|
||||
|
||||
class Queue {
|
||||
constructor(handlers = {}) {
|
||||
this.items = {};
|
||||
this.handlers = {};
|
||||
for (const type of Object.values(QUEUE_TYPES)) {
|
||||
this.items[type] = [];
|
||||
this.handlers[type] = handlers[type];
|
||||
}
|
||||
}
|
||||
|
||||
enqueue(type, ...items) {
|
||||
if (!this.items[type]) return;
|
||||
|
||||
items = items.map(item => {
|
||||
if (!Array.isArray(item)) return [item];
|
||||
return item;
|
||||
});
|
||||
|
||||
this.items[type].push(...items);
|
||||
}
|
||||
|
||||
tryDequeue(type) {
|
||||
if (!this.items[type]) return;
|
||||
if (!this.items[type].length) return;
|
||||
if (!this.handlers[type]) return;
|
||||
|
||||
const item = this.items[type].shift();
|
||||
const method = this.handlers[type];
|
||||
return method(...item);
|
||||
}
|
||||
}
|
||||
|
||||
Queue.QUEUE_TYPES = QUEUE_TYPES;
|
||||
module.exports = Queue;
|
||||
0
src-old/Queue.test.js
Normal file
0
src-old/Queue.test.js
Normal file
75
src-old/Server.js
Normal file
75
src-old/Server.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
const {fork} = require('child_process');
|
||||
const Queue = require('bee-queue');
|
||||
|
||||
const Client = require('./Client');
|
||||
const ConnectionManager = require('./ConnectionManager');
|
||||
const {idGenerator} = require('./utils/idGenerator');
|
||||
const message = require('./const/message');
|
||||
|
||||
class Server extends net.Server {
|
||||
constructor() {
|
||||
super({pauseOnConnect: true});
|
||||
|
||||
this.connectionManager = new ConnectionManager();
|
||||
this.clientIDGenerator = idGenerator(1);
|
||||
this.receiveQueue = new Queue('receive');
|
||||
this.sendQueue = new Queue('send');
|
||||
|
||||
this.on('connection', socket => this._onConnection(socket));
|
||||
}
|
||||
|
||||
async send(client, data) {
|
||||
const job = await this.sendQueue.createJob({
|
||||
id: client.id,
|
||||
data
|
||||
})
|
||||
.timeout(30000)
|
||||
.save();
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.connectionManager.invoke('close');
|
||||
await new Promise(resolve => super.close(() => resolve()));
|
||||
return this;
|
||||
}
|
||||
|
||||
async listen(port) {
|
||||
// const processor = path.resolve(__dirname, './commands/processor.js');
|
||||
// this.commandProcess = fork(processor, {
|
||||
// stdio: 'pipe'
|
||||
// });
|
||||
// this.commandProcess.on('message', (message) => {
|
||||
// console.log('got', message)
|
||||
// });
|
||||
// this.commandProcess.on('error', (err) => {
|
||||
// console.log('error', err)
|
||||
// });
|
||||
// this.commandProcess.once('exit', (code) => {
|
||||
// console.log('exit', code)
|
||||
// });
|
||||
// this.commandProcess.once('close', (code) => {
|
||||
// console.log('close', code)
|
||||
// });
|
||||
this.commandProcess.send('server', this);
|
||||
|
||||
await new Promise(resolve => super.listen(port, () => resolve()));
|
||||
return this;
|
||||
}
|
||||
|
||||
_onConnection(socket) {
|
||||
const id = this.clientIDGenerator.next().value;
|
||||
const client = new Client(id, socket);
|
||||
client.once('close', () => this.connectionManager.remove(client.id));
|
||||
|
||||
this.connectionManager.add(id, client);
|
||||
this.emit('client', client);
|
||||
|
||||
// client.send(message.GREETING)
|
||||
// .then(() => client.resume())
|
||||
// .catch(() => client.close());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
41
src-old/Server.test.js
Normal file
41
src-old/Server.test.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const net = require('net');
|
||||
|
||||
const Server = require('./Server');
|
||||
const {getUsablePort} = require('./utils/getUsablePort');
|
||||
|
||||
let PORT;
|
||||
|
||||
beforeEach(async () => {
|
||||
PORT = await getUsablePort(8000);
|
||||
});
|
||||
|
||||
test('expects server to start listening', done => {
|
||||
const server = new Server();
|
||||
server.once('listening', () => server.close());
|
||||
server.once('close', () => done());
|
||||
server.listen(PORT);
|
||||
});
|
||||
|
||||
test('expects server to accept a client', done => {
|
||||
const server = new Server();
|
||||
server.once('client', client => {
|
||||
expect(client.id).toBeGreaterThan(0);
|
||||
server.close();
|
||||
});
|
||||
server.once('close', () => done());
|
||||
server.listen(PORT);
|
||||
|
||||
net.createConnection(PORT);
|
||||
});
|
||||
|
||||
test('expects server to send greeting on client connection', done => {
|
||||
const server = new Server();
|
||||
server.once('client', client => {
|
||||
expect(client.id).toBeGreaterThan(0);
|
||||
});
|
||||
server.once('close', () => done());
|
||||
server.listen(PORT);
|
||||
|
||||
const connection = net.createConnection(PORT);
|
||||
connection.once('data', () => server.close());
|
||||
});
|
||||
39
src-old/commands/index.js
Normal file
39
src-old/commands/index.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const registry = require('./registry');
|
||||
const message = require('../const/message');
|
||||
|
||||
function parseCommand(rawCommand) {
|
||||
const strippedRawCommand = rawCommand.replace(/"/g, '');
|
||||
const [directive, ...args] = strippedRawCommand.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: String(directive).trim().toLocaleUpperCase(),
|
||||
arg: params.arg.length ? params.arg.join(' ') : null,
|
||||
flags: params.flags,
|
||||
// raw: rawCommand
|
||||
};
|
||||
return command;
|
||||
}
|
||||
|
||||
async function getCommandHandler(client, command) {
|
||||
command = parseCommand(command);
|
||||
|
||||
if (!registry.hasOwnProperty(command.directive)) return message.UNSUPPORTED_COMMAND;
|
||||
|
||||
const commandRegister = registry[command.directive];
|
||||
const commandFlags = commandRegister.flags ? commandRegister.flags : {};
|
||||
if (!commandFlags.no_auth && !client.authenticated) {
|
||||
return message.COMMAND_REQUIRES_AUTHENTICATION;
|
||||
}
|
||||
|
||||
return commandRegister.handle;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCommandHandler,
|
||||
parseCommand
|
||||
};
|
||||
23
src-old/commands/processor.js
Executable file
23
src-old/commands/processor.js
Executable file
@@ -0,0 +1,23 @@
|
||||
process.once('message', (initMsg, server) => {
|
||||
if (initMsg !== 'server') {
|
||||
return process.exit(-1);
|
||||
}
|
||||
|
||||
process.on('message', (msg, ...args) => {
|
||||
|
||||
});
|
||||
|
||||
processQueues(server);
|
||||
});
|
||||
|
||||
async function processQueues(server) {
|
||||
process.send('processQueues');
|
||||
const iterable = server.connectionManager.iterate();
|
||||
process.send('interable');
|
||||
for (const [id, client] of iterable) {
|
||||
process.send('process', id);
|
||||
}
|
||||
|
||||
process.send('/processQueues');
|
||||
return processQueues(server);
|
||||
}
|
||||
4
src-old/commands/registry/index.js
Normal file
4
src-old/commands/registry/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
USER: require('./user'),
|
||||
PASS: require('./pass')
|
||||
};
|
||||
20
src-old/commands/registry/pass.js
Normal file
20
src-old/commands/registry/pass.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const message = require('../../const/message');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASS',
|
||||
handler: async function (client, command) {
|
||||
if (!client.getSession('username')) return client.send(message.BAD_COMMAND_SEQUENCE);
|
||||
if (client.authenticated) return client.send(message.SUPERFLUOUS_COMMAND);
|
||||
if (!command.arg) return client.send(message.SYNTAX_ERROR_ARGS);
|
||||
// TODO: 332 : require account name (ACCT)
|
||||
|
||||
// TODO: do login
|
||||
|
||||
await client.send(message.AUTHENTICATED);
|
||||
},
|
||||
args: ['<password>'],
|
||||
description: 'Authenticate client session',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
21
src-old/commands/registry/user.js
Normal file
21
src-old/commands/registry/user.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const message = require('../../const/message');
|
||||
|
||||
module.exports = {
|
||||
directive: 'USER',
|
||||
handler: async function (client, command) {
|
||||
if (client.getSession('username')) return client.send(message.USERNAME_SET_ALREADY);
|
||||
if (client.authenticated) return client.send(message.USER_AUTHENTICATED);
|
||||
if (!client.arg) return client.send(message.SYNTAX_ERROR_ARGS);
|
||||
|
||||
this.setSession('username', command.arg);
|
||||
|
||||
// TODO: allow anonymous logins
|
||||
|
||||
await this.reply(message.AWAITING_PASSWORD);
|
||||
},
|
||||
args: ['<username>'],
|
||||
description: 'Set client session username',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
12
src-old/const/message.js
Normal file
12
src-old/const/message.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
SUPERFLUOUS_COMMAND: '202 Superfluous command',
|
||||
GREETING: '220 Greetings',
|
||||
AUTHENTICATED: '230 User authenticated successfully',
|
||||
AWAITING_PASSWORD: '331 Username okay, awaiting password',
|
||||
SYNTAX_ERROR_ARGS: '501 Syntax error in arguments',
|
||||
UNSUPPORTED_COMMAND: '502 Command not supported',
|
||||
BAD_COMMAND_SEQUENCE: '503 Bad sequence of commands',
|
||||
USERNAME_SET_ALREADY: '530 Username already set',
|
||||
COMMAND_REQUIRES_AUTHENTICATION: '530 Username already set',
|
||||
AUTHENTICATED_FAILED: '530 Authentication failed',
|
||||
};
|
||||
4
src-old/index.js
Normal file
4
src-old/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const Server = require('./Server');
|
||||
|
||||
module.exports = Server;
|
||||
module.exports.FtpSrv = Server;
|
||||
39
src-old/utils/getUsablePort.js
Normal file
39
src-old/utils/getUsablePort.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const net = require('net');
|
||||
|
||||
const PORT_MAX = 65535;
|
||||
|
||||
function getUsablePort(portStart = 21, portStop = PORT_MAX) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.maxConnections = 0;
|
||||
|
||||
const cleanUpServer = () => {
|
||||
server.removeAllListeners();
|
||||
server.unref();
|
||||
};
|
||||
|
||||
let currentPort = portStart;
|
||||
server.on('error', err => {
|
||||
if (currentPort < PORT_MAX && currentPort < portStop) {
|
||||
server.listen(++currentPort);
|
||||
} else {
|
||||
server.close(() => {
|
||||
cleanUpServer();
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
server.on('listening', () => {
|
||||
const {port} = server.address();
|
||||
server.close(() => {
|
||||
cleanUpServer();
|
||||
resolve(port);
|
||||
})
|
||||
});
|
||||
server.listen(currentPort);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUsablePort
|
||||
};
|
||||
6
src-old/utils/getUsablePort.test.js
Normal file
6
src-old/utils/getUsablePort.test.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const {getUsablePort} = require('./getUsablePort');
|
||||
|
||||
test('expects an available port to be found', async () => {
|
||||
const port = await getUsablePort();
|
||||
expect(port).toBeGreaterThan(0);
|
||||
});
|
||||
6
src-old/utils/idGenerator.js
Normal file
6
src-old/utils/idGenerator.js
Normal file
@@ -0,0 +1,6 @@
|
||||
function* idGenerator(start) {
|
||||
let i = start;
|
||||
while (true) yield i++;
|
||||
}
|
||||
|
||||
module.exports = {idGenerator};
|
||||
8
src-old/utils/idGenerator.test.js
Normal file
8
src-old/utils/idGenerator.test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const {idGenerator} = require('./idGenerator');
|
||||
|
||||
test('expects ids to be generated', () => {
|
||||
const id = idGenerator(1);
|
||||
expect(id.next().value).toBe(1);
|
||||
expect(id.next().value).toBe(2);
|
||||
expect(id.next().value).toBe(3);
|
||||
});
|
||||
16
src/client/index.js
Normal file
16
src/client/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const net = require('net');
|
||||
|
||||
class Client extends net.Socket {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
send() {
|
||||
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
}
|
||||
}
|
||||
module.exports = Client;
|
||||
@@ -1,47 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
|
||||
const REGISTRY = require('./registry');
|
||||
|
||||
class FtpCommands {
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
this.previousCommand = {};
|
||||
this.blacklist = _.get(this.connection, 'server.options.blacklist', []).map(cmd => _.upperCase(cmd));
|
||||
this.whitelist = _.get(this.connection, 'server.options.whitelist', []).map(cmd => _.upperCase(cmd));
|
||||
}
|
||||
|
||||
handle(command) {
|
||||
const log = this.connection.log.child({command});
|
||||
log.trace('Handle command');
|
||||
|
||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
||||
return this.connection.reply(402, 'Command not allowed');
|
||||
}
|
||||
|
||||
if (_.includes(this.blacklist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command blacklisted');
|
||||
}
|
||||
|
||||
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
|
||||
return this.connection.reply(502, 'Command not whitelisted');
|
||||
}
|
||||
|
||||
const commandRegister = REGISTRY[command.directive];
|
||||
const commandFlags = _.get(commandRegister, 'flags', {});
|
||||
if (!commandFlags.no_auth && !this.connection.authenticated) {
|
||||
return this.connection.reply(530, 'Command requires authentication');
|
||||
}
|
||||
|
||||
if (!commandRegister.handler) {
|
||||
return this.connection.reply(502, 'Handler not set on command');
|
||||
}
|
||||
|
||||
const handler = commandRegister.handler.bind(this.connection);
|
||||
return when.try(handler, { log, command, previous_command: this.previousCommand })
|
||||
.finally(() => {
|
||||
this.previousCommand = _.clone(command);
|
||||
});
|
||||
}
|
||||
}
|
||||
module.exports = FtpCommands;
|
||||
@@ -1,14 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'ABOR',
|
||||
handler: function () {
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
return this.reply(426, {socket})
|
||||
.then(() => this.connector.end());
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => this.reply(226));
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Abort an active file transfer'
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'ALLO',
|
||||
handler: function () {
|
||||
return this.reply(202);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Allocate sufficient disk space to receive a file',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
const stor = require('./stor').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'APPE',
|
||||
handler: function (args) {
|
||||
return stor.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Append to a file'
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'AUTH',
|
||||
handler: function ({command} = {}) {
|
||||
const method = _.upperCase(command._[1]);
|
||||
|
||||
switch (method) {
|
||||
case 'TLS': return handleTLS.call(this);
|
||||
case 'SSL': return handleSSL.call(this);
|
||||
default: return this.reply(504);
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [type]',
|
||||
description: 'Set authentication mechanism',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
|
||||
function handleTLS() {
|
||||
return this.reply(504);
|
||||
}
|
||||
|
||||
function handleSSL() {
|
||||
return this.reply(504);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
const cwd = require('./cwd').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: ['CDUP', 'XCUP'],
|
||||
handler: function (args) {
|
||||
args.command._ = [args.command._[0], '..'];
|
||||
return cwd.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Change to Parent Directory'
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['CWD', 'XCWD'],
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chdir) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.chdir.bind(this.fs), command._[1])
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(250, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}[path]',
|
||||
description: 'Change working directory'
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'DELE',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.delete) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.delete.bind(this.fs), command._[1])
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Delete file'
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'FEAT',
|
||||
handler: function () {
|
||||
const registry = require('../registry');
|
||||
const features = Object.keys(registry)
|
||||
.reduce((feats, cmd) => {
|
||||
const feat = _.get(registry[cmd], 'flags.feat', null);
|
||||
if (feat) return _.concat(feats, feat);
|
||||
return feats;
|
||||
}, [])
|
||||
.map(feat => ` ${feat}`);
|
||||
return this.reply(211, 'Extensions supported', ...features, 'END');
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Get the feature list implemented by the server',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'HELP',
|
||||
handler: function ({command} = {}) {
|
||||
const registry = require('../registry');
|
||||
const directive = _.upperCase(command._[1]);
|
||||
if (directive) {
|
||||
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
|
||||
|
||||
const {syntax, description} = registry[directive];
|
||||
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
|
||||
return this.reply(214, ...reply);
|
||||
} else {
|
||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
|
||||
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [command(optional)]',
|
||||
description: 'Returns usage documentation on a command if specified, else a general help document is returned',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const getFileStat = require('../../helpers/file-stat');
|
||||
|
||||
// http://cr.yp.to/ftp/list.html
|
||||
// http://cr.yp.to/ftp/list/eplf.html
|
||||
module.exports = {
|
||||
directive: 'LIST',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const simple = command.directive === 'NLST';
|
||||
|
||||
let dataSocket;
|
||||
const directory = command._[1] || '.';
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when.try(this.fs.list.bind(this.fs), directory))
|
||||
.then(files => {
|
||||
const getFileMessage = file => {
|
||||
if (simple) return file.name;
|
||||
return getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
};
|
||||
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileMessage(file);
|
||||
return {
|
||||
raw: true,
|
||||
message,
|
||||
socket: dataSocket
|
||||
};
|
||||
});
|
||||
return this.reply(150)
|
||||
.then(() => {
|
||||
if (fileList.length) return this.reply({}, ...fileList);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return this.reply(226, 'Transfer OK');
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(err.code || 451, err.message || 'No directory');
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
const when = require('when');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = {
|
||||
directive: 'MDTM',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.get.bind(this.fs), command._[1])
|
||||
.then(fileStat => {
|
||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
||||
return this.reply(213, modificationTime);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Return the last-modified time of a specified file',
|
||||
flags: {
|
||||
feat: 'MDTM'
|
||||
}
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['MKD', 'XMKD'],
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.mkdir.bind(this.fs), command._[1])
|
||||
.then(dir => {
|
||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}[path]',
|
||||
description: 'Make directory'
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'MODE',
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(/^S$/i.test(command._[1]) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} [mode]',
|
||||
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
const list = require('./list').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'NLST',
|
||||
handler: function (args) {
|
||||
return list.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
description: 'Returns a list of file names in a specified directory'
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'NOOP',
|
||||
handler: function () {
|
||||
return this.reply(200);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'No operation',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'OPTS',
|
||||
handler: function () {
|
||||
return this.reply(501);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Select options for a feature'
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASS',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.username) return this.reply(503);
|
||||
if (this.username && this.authenticated &&
|
||||
_.get(this, 'server.options.anonymous') === true) return this.reply(230);
|
||||
|
||||
// 332 : require account name (ACCT)
|
||||
|
||||
const password = command._[1];
|
||||
return this.login(this.username, password)
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err.message || 'Authentication failed');
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [password]',
|
||||
description: 'Authentication password',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
const PassiveConnector = require('../../connector/passive');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PASV',
|
||||
handler: function () {
|
||||
this.connector = new PassiveConnector(this);
|
||||
return this.connector.setupServer()
|
||||
.then(server => {
|
||||
const address = this.server.url.hostname;
|
||||
const {port} = server.address();
|
||||
const host = address.replace(/\./g, ',');
|
||||
const portByte1 = port / 256 | 0;
|
||||
const portByte2 = port % 256;
|
||||
|
||||
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Initiate passive mode'
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const ActiveConnector = require('../../connector/active');
|
||||
|
||||
module.exports = {
|
||||
directive: 'PORT',
|
||||
handler: function ({command} = {}) {
|
||||
this.connector = new ActiveConnector(this);
|
||||
const rawConnection = _.get(command, '_[1]', '').split(',');
|
||||
if (rawConnection.length !== 6) return this.reply(425);
|
||||
|
||||
const ip = rawConnection.slice(0, 4).join('.');
|
||||
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
|
||||
const port = portBytes[0] * 256 + portBytes[1];
|
||||
|
||||
return this.connector.setupConnection(ip, port)
|
||||
.then(() => this.reply(200));
|
||||
},
|
||||
syntax: '{{cmd}} x,x,x,x,y,y',
|
||||
description: 'Specifies an address and port to which the server should connect'
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
const when = require('when');
|
||||
const escapePath = require('../../helpers/escape-path');
|
||||
|
||||
module.exports = {
|
||||
directive: ['PWD', 'XPWD'],
|
||||
handler: function ({log} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.currentDirectory) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.currentDirectory.bind(this.fs))
|
||||
.then(cwd => {
|
||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
||||
return this.reply(257, path);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550, err.message);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Print current working directory'
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'QUIT',
|
||||
handler: function () {
|
||||
return this.close(221);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Disconnect',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RETR',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.read) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when.try(this.fs.read.bind(this.fs), command._[1]))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
dataSocket.on('error', err => stream.emit('error', err));
|
||||
|
||||
stream.on('data', data => dataSocket.write(data, this.encoding));
|
||||
stream.on('end', () => resolve(this.reply(226)));
|
||||
stream.on('error', err => reject(err));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
});
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(551);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Retrieve a copy of the file'
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
const dele = require('./dele').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: ['RMD', 'XRMD'],
|
||||
handler: function (args) {
|
||||
return dele.call(this, args);
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Remove a directory'
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNFR',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const fileName = command._[1];
|
||||
return when.try(this.fs.get.bind(this.fs), fileName)
|
||||
.then(() => {
|
||||
this.renameFrom = fileName;
|
||||
return this.reply(350);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [name]',
|
||||
description: 'Rename from'
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'RNTO',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.renameFrom) return this.reply(503);
|
||||
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const from = this.renameFrom;
|
||||
const to = command._[1];
|
||||
|
||||
return when.try(this.fs.rename.bind(this.fs), from, to)
|
||||
.then(() => {
|
||||
return this.reply(250);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.renameFrom;
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [name]',
|
||||
description: 'Rename to'
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const [, mode, fileName] = command._;
|
||||
return when.try(this.fs.chmod.bind(this.fs), fileName, parseInt(mode, 8))
|
||||
.then(() => {
|
||||
return this.reply(200);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(500);
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'SITE',
|
||||
handler: function ({log, command} = {}) {
|
||||
const registry = require('./registry');
|
||||
let [, subverb, ...subparameters] = command._;
|
||||
subverb = _.upperCase(subverb);
|
||||
const subLog = log.child({subverb});
|
||||
|
||||
if (!registry.hasOwnProperty(subverb)) return this.reply(502);
|
||||
|
||||
const subCommand = {
|
||||
_: [subverb, ...subparameters],
|
||||
directive: subverb
|
||||
};
|
||||
const handler = registry[subverb].handler.bind(this);
|
||||
return when.try(handler, { log: subLog, command: subCommand });
|
||||
},
|
||||
syntax: '{{cmd}} [subVerb] [subParams]',
|
||||
description: 'Sends site specific commands to remote server'
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
CHMOD: {
|
||||
handler: require('./chmod')
|
||||
}
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'SIZE',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.get.bind(this.fs), command._[1])
|
||||
.then(fileStat => {
|
||||
return this.reply(213, {message: fileStat.size});
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(550);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Return the size of a file',
|
||||
flags: {
|
||||
feat: 'SIZE'
|
||||
}
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const getFileStat = require('../../helpers/file-stat');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STAT',
|
||||
handler: function (args = {}) {
|
||||
const {log, command} = args;
|
||||
const path = _.get(command, '_[1]');
|
||||
if (path) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.get.bind(this.fs), path)
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) {
|
||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
return when.try(this.fs.list.bind(this.fs), path)
|
||||
.then(files => {
|
||||
const fileList = files.map(file => {
|
||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
||||
return {
|
||||
raw: true,
|
||||
message
|
||||
};
|
||||
});
|
||||
return this.reply(213, 'Status begin', ...fileList, 'Status end');
|
||||
});
|
||||
} else {
|
||||
return this.reply(212, getFileStat(stat, _.get(this, 'server.options.file_format', 'ls')));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(450);
|
||||
});
|
||||
} else {
|
||||
return this.reply(211, 'Status OK');
|
||||
}
|
||||
},
|
||||
syntax: '{{cmd}} [path(optional)]',
|
||||
description: 'Returns the current status'
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOR',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const append = command.directive === 'APPE';
|
||||
const fileName = command._[1];
|
||||
|
||||
let dataSocket;
|
||||
return this.connector.waitForConnection()
|
||||
.then(socket => {
|
||||
this.commandSocket.pause();
|
||||
dataSocket = socket;
|
||||
})
|
||||
.then(() => when.try(this.fs.write.bind(this.fs), fileName, {append}))
|
||||
.then(stream => {
|
||||
return when.promise((resolve, reject) => {
|
||||
stream.on('error', err => dataSocket.emit('error', err));
|
||||
|
||||
dataSocket.on('end', () => stream.end(() => resolve(this.reply(226, fileName))));
|
||||
dataSocket.on('error', err => reject(err));
|
||||
dataSocket.on('data', data => stream.write(data, this.encoding));
|
||||
this.reply(150).then(() => dataSocket.resume());
|
||||
});
|
||||
})
|
||||
.catch(when.TimeoutError, err => {
|
||||
log.error(err);
|
||||
return this.reply(425, 'No connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(553);
|
||||
})
|
||||
.finally(() => {
|
||||
this.connector.end();
|
||||
this.commandSocket.resume();
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}} [path]',
|
||||
description: 'Store data as a file at the server site'
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
const when = require('when');
|
||||
|
||||
const stor = require('./stor').handler;
|
||||
|
||||
module.exports = {
|
||||
directive: 'STOU',
|
||||
handler: function (args) {
|
||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
||||
|
||||
const fileName = args.command._[1];
|
||||
return when.try(() => {
|
||||
return when.try(this.fs.get.bind(this.fs), fileName)
|
||||
.then(() => when.try(this.fs.getUniqueName.bind(this.fs)))
|
||||
.catch(() => when.resolve(fileName));
|
||||
})
|
||||
.then(name => {
|
||||
args.command._[1] = name;
|
||||
return stor.call(this, args);
|
||||
});
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Store file uniquely'
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'STRU',
|
||||
handler: function ({command} = {}) {
|
||||
return this.reply(/^F$/i.test(command._[1]) ? 200 : 504);
|
||||
},
|
||||
syntax: '{{cmd}} [structure]',
|
||||
description: 'Set file transfer structure',
|
||||
flags: {
|
||||
obsolete: true
|
||||
}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'SYST',
|
||||
handler: function () {
|
||||
return this.reply(215);
|
||||
},
|
||||
syntax: '{{cmd}}',
|
||||
description: 'Return system type',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const ENCODING_TYPES = {
|
||||
A: 'utf-8',
|
||||
I: 'binary',
|
||||
L: 'binary'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
directive: 'TYPE',
|
||||
handler: function ({command} = {}) {
|
||||
const encoding = _.upperCase(command._[1]);
|
||||
if (!ENCODING_TYPES.hasOwnProperty(encoding)) return this.reply(501);
|
||||
|
||||
this.encoding = ENCODING_TYPES[encoding];
|
||||
return this.reply(200);
|
||||
},
|
||||
syntax: '{{cmd}} [mode]',
|
||||
description: 'Set the transfer mode, binary (I) or utf-8 (A)'
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
module.exports = {
|
||||
directive: 'USER',
|
||||
handler: function ({log, command} = {}) {
|
||||
if (this.username) return this.reply(530, 'Username already set');
|
||||
|
||||
this.username = command._[1];
|
||||
if (!this.username) return this.reply(501, 'Must send username requirement');
|
||||
|
||||
if (this.server.options.anonymous === true) {
|
||||
return this.login(this.username, '@anonymous')
|
||||
.then(() => {
|
||||
return this.reply(230);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
return this.reply(530, err || 'Authentication failed');
|
||||
});
|
||||
}
|
||||
return this.reply(331);
|
||||
},
|
||||
syntax: '{{cmd}} [username]',
|
||||
description: 'Authentication username',
|
||||
flags: {
|
||||
no_auth: true
|
||||
}
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
/* eslint no-return-assign: 0 */
|
||||
const commands = [
|
||||
require('./registration/abor'),
|
||||
require('./registration/allo'),
|
||||
require('./registration/appe'),
|
||||
require('./registration/auth'),
|
||||
require('./registration/cdup'),
|
||||
require('./registration/cwd'),
|
||||
require('./registration/dele'),
|
||||
require('./registration/feat'),
|
||||
require('./registration/help'),
|
||||
require('./registration/list'),
|
||||
require('./registration/mdtm'),
|
||||
require('./registration/mkd'),
|
||||
require('./registration/mode'),
|
||||
require('./registration/nlst'),
|
||||
require('./registration/noop'),
|
||||
require('./registration/opts'),
|
||||
require('./registration/pass'),
|
||||
require('./registration/pasv'),
|
||||
require('./registration/port'),
|
||||
require('./registration/pwd'),
|
||||
require('./registration/retr'),
|
||||
require('./registration/rmd'),
|
||||
require('./registration/rnfr'),
|
||||
require('./registration/rnto'),
|
||||
require('./registration/site'),
|
||||
require('./registration/size'),
|
||||
require('./registration/stat'),
|
||||
require('./registration/stor'),
|
||||
require('./registration/stou'),
|
||||
require('./registration/stru'),
|
||||
require('./registration/syst'),
|
||||
require('./registration/type'),
|
||||
require('./registration/user')
|
||||
];
|
||||
|
||||
const registry = commands.reduce((result, cmd) => {
|
||||
const aliases = Array.isArray(cmd.directive) ? cmd.directive : [cmd.directive];
|
||||
aliases.forEach(alias => result[alias] = cmd);
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
module.exports = registry;
|
||||
@@ -1,120 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const uuid = require('uuid');
|
||||
const when = require('when');
|
||||
const sequence = require('when/sequence');
|
||||
const parseCommandString = require('minimist-string');
|
||||
|
||||
const BaseConnector = require('./connector/base');
|
||||
const FileSystem = require('./fs');
|
||||
const Commands = require('./commands');
|
||||
const errors = require('./errors');
|
||||
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({id: this.id});
|
||||
this.commands = new Commands(this);
|
||||
this.encoding = 'utf-8';
|
||||
|
||||
this.connector = new BaseConnector(this);
|
||||
|
||||
this.commandSocket.on('error', err => {
|
||||
this.server.server.emit('error', {connection: this, error: err});
|
||||
});
|
||||
this.commandSocket.on('data', data => {
|
||||
const messages = _.compact(data.toString('utf-8').split('\r\n'));
|
||||
const handleMessage = message => {
|
||||
const command = parseCommandString(message);
|
||||
command.directive = _.upperCase(command._[0]);
|
||||
return this.commands.handle(command);
|
||||
};
|
||||
|
||||
return sequence(messages.map(message => handleMessage.bind(this, message)));
|
||||
});
|
||||
this.commandSocket.on('timeout', () => {});
|
||||
this.commandSocket.on('close', () => {
|
||||
if (this.connector) this.connector.end();
|
||||
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
close(code = 421, message = 'Closing connection') {
|
||||
return when(() => {
|
||||
if (code) return this.reply(code, message);
|
||||
})
|
||||
.then(() => {
|
||||
if (this.commandSocket) this.commandSocket.end();
|
||||
});
|
||||
}
|
||||
|
||||
login(username, password) {
|
||||
return when.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);
|
||||
} else {
|
||||
return this.server.emit('login', {connection: this, username, password});
|
||||
}
|
||||
})
|
||||
.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, {root, cwd});
|
||||
});
|
||||
}
|
||||
|
||||
reply(options = {}, ...letters) {
|
||||
const satisfyParameters = () => {
|
||||
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 => {
|
||||
return when(promise)
|
||||
.then(letter => {
|
||||
if (!letter) letter = {};
|
||||
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
||||
|
||||
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
|
||||
if (!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
|
||||
.then(message => {
|
||||
letter.message = message;
|
||||
return letter;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const processLetter = (letter, index) => {
|
||||
return when.promise((resolve, reject) => {
|
||||
const seperator = !options.hasOwnProperty('eol') ?
|
||||
letters.length - 1 === index ? ' ' : '-' :
|
||||
options.eol ? ' ' : '-';
|
||||
const packet = !letter.raw ? _.compact([letter.code || options.code, letter.message]).join(seperator) : letter.message;
|
||||
|
||||
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 => {
|
||||
if (err) {
|
||||
this.log.error(err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
} else reject(new errors.SocketError('Socket not writable'));
|
||||
});
|
||||
};
|
||||
|
||||
return satisfyParameters()
|
||||
.then(satisfiedLetters => sequence(satisfiedLetters.map((letter, index) => processLetter.bind(this, letter, index))))
|
||||
.catch(err => {
|
||||
this.log.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
module.exports = FtpConnection;
|
||||
@@ -1,36 +0,0 @@
|
||||
const {Socket} = require('net');
|
||||
const when = require('when');
|
||||
const Connector = require('./base');
|
||||
|
||||
class Active extends Connector {
|
||||
constructor(connection) {
|
||||
super(connection);
|
||||
this.type = 'active';
|
||||
}
|
||||
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
return when.iterate(
|
||||
() => {},
|
||||
() => this.dataSocket && this.dataSocket.connected,
|
||||
() => when().delay(delay)
|
||||
).timeout(timeout)
|
||||
.then(() => this.dataSocket);
|
||||
}
|
||||
|
||||
setupConnection(host, port) {
|
||||
const closeExistingServer = () => this.dataSocket ?
|
||||
when(this.dataSocket.destroy()) :
|
||||
when.resolve();
|
||||
|
||||
return closeExistingServer()
|
||||
.then(() => {
|
||||
this.dataSocket = new Socket();
|
||||
this.dataSocket.setEncoding(this.encoding);
|
||||
this.dataSocket.connect({ host, port }, () => {
|
||||
this.dataSocket.pause();
|
||||
this.dataSocket.connected = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
module.exports = Active;
|
||||
@@ -1,27 +0,0 @@
|
||||
const when = require('when');
|
||||
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;
|
||||
}
|
||||
|
||||
waitForConnection() {
|
||||
return when.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;
|
||||
this.type = false;
|
||||
}
|
||||
}
|
||||
module.exports = Connector;
|
||||
@@ -1,84 +0,0 @@
|
||||
const net = require('net');
|
||||
const when = require('when');
|
||||
const Connector = require('./base');
|
||||
const findPort = require('../helpers/find-port');
|
||||
const errors = require('../errors');
|
||||
|
||||
class Passive extends Connector {
|
||||
constructor(connection) {
|
||||
super(connection);
|
||||
this.type = 'passive';
|
||||
}
|
||||
|
||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
||||
if (!this.dataServer) {
|
||||
return when.reject(new errors.ConnectorError('Passive server not setup'));
|
||||
}
|
||||
return when.iterate(
|
||||
() => {},
|
||||
() => this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected,
|
||||
() => when().delay(delay)
|
||||
).timeout(timeout)
|
||||
.then(() => this.dataSocket);
|
||||
}
|
||||
|
||||
setupServer() {
|
||||
const closeExistingServer = () => this.dataServer ?
|
||||
when.promise(resolve => this.dataServer.close(() => resolve())) :
|
||||
when.resolve();
|
||||
|
||||
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 => {
|
||||
if (this.connection.commandSocket.remoteAddress !== socket.remoteAddress) {
|
||||
this.log.error({
|
||||
pasv_connection: socket.remoteAddress,
|
||||
cmd_connection: this.connection.commandSocket.remoteAddress
|
||||
}, 'Connecting addresses do not match');
|
||||
|
||||
socket.destroy();
|
||||
return this.connection.reply(550, 'Remote addresses do not match')
|
||||
.finally(() => this.connection.close());
|
||||
}
|
||||
this.log.debug({port}, 'Passive connection fulfilled.');
|
||||
|
||||
this.dataSocket = socket;
|
||||
this.dataSocket.connected = true;
|
||||
this.dataSocket.setEncoding(this.connection.encoding);
|
||||
this.dataSocket.on('close', () => {
|
||||
this.log.debug('Passive connection closed');
|
||||
this.end();
|
||||
});
|
||||
});
|
||||
this.dataServer.on('close', () => {
|
||||
this.log.debug('Passive server closed');
|
||||
this.dataServer = null;
|
||||
});
|
||||
|
||||
return when.promise((resolve, reject) => {
|
||||
this.dataServer.listen(port, err => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
this.log.info({port}, 'Passive connection listening');
|
||||
resolve(this.dataServer);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getPort() {
|
||||
if (this.server.options.pasv_range) {
|
||||
const [min, max] = typeof this.server.options.pasv_range === 'string' ?
|
||||
this.server.options.pasv_range.split('-').map(v => v ? parseInt(v) : v) :
|
||||
[this.server.options.pasv_range];
|
||||
return findPort(min, max);
|
||||
} else return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
module.exports = Passive;
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
class GeneralError extends Error {
|
||||
constructor(message, code = 400) {
|
||||
super();
|
||||
this.code = code;
|
||||
this.name = 'GeneralError';
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
class SocketError extends Error {
|
||||
constructor(message, code = 500) {
|
||||
super();
|
||||
this.code = code;
|
||||
this.name = 'SocketError';
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
class FileSystemError extends Error {
|
||||
constructor(message, code = 400) {
|
||||
super();
|
||||
this.code = code;
|
||||
this.name = 'FileSystemError';
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectorError extends Error {
|
||||
constructor(message, code = 400) {
|
||||
super();
|
||||
this.code = code;
|
||||
this.name = 'ConnectorError';
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
SocketError,
|
||||
FileSystemError,
|
||||
ConnectorError,
|
||||
GeneralError
|
||||
};
|
||||
117
src/fs.js
117
src/fs.js
@@ -1,117 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const nodePath = require('path');
|
||||
const uuid = require('uuid');
|
||||
const when = require('when');
|
||||
const whenNode = require('when/node');
|
||||
const syncFs = require('fs');
|
||||
const fs = whenNode.liftAll(syncFs);
|
||||
const errors = require('./errors');
|
||||
|
||||
class FileSystem {
|
||||
constructor(connection, {
|
||||
root = '/',
|
||||
cwd = '/'
|
||||
} = {}) {
|
||||
this.connection = connection;
|
||||
this.cwd = nodePath.resolve(cwd);
|
||||
this.root = nodePath.resolve(root);
|
||||
}
|
||||
|
||||
_resolvePath(path) {
|
||||
const pathParts = {
|
||||
root: this.root,
|
||||
base: nodePath.resolve(this.cwd, path)
|
||||
};
|
||||
path = nodePath.format(pathParts);
|
||||
return path;
|
||||
}
|
||||
|
||||
currentDirectory() {
|
||||
return this.cwd;
|
||||
}
|
||||
|
||||
get(fileName) {
|
||||
const path = this._resolvePath(fileName);
|
||||
return fs.stat(path)
|
||||
.then(stat => _.set(stat, 'name', fileName));
|
||||
}
|
||||
|
||||
list(path = '.') {
|
||||
path = this._resolvePath(path);
|
||||
return fs.readdir(path)
|
||||
.then(fileNames => {
|
||||
return when.map(fileNames, fileName => {
|
||||
const filePath = nodePath.join(path, fileName);
|
||||
return fs.access(filePath, syncFs.constants.F_OK)
|
||||
.then(() => {
|
||||
return fs.stat(filePath)
|
||||
.then(stat => _.set(stat, 'name', fileName));
|
||||
})
|
||||
.catch(() => null);
|
||||
});
|
||||
})
|
||||
.then(_.compact);
|
||||
}
|
||||
|
||||
chdir(path = '.') {
|
||||
path = this._resolvePath(path);
|
||||
return fs.stat(path)
|
||||
.tap(stat => {
|
||||
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
|
||||
})
|
||||
.then(() => {
|
||||
this.cwd = path.substring(this.root.length) || '/';
|
||||
return this.currentDirectory();
|
||||
});
|
||||
}
|
||||
|
||||
write(fileName, {append = false} = {}) {
|
||||
const path = this._resolvePath(fileName);
|
||||
const stream = syncFs.createWriteStream(path, {flags: !append ? 'w+' : 'a+'});
|
||||
stream.on('error', () => fs.unlink(path));
|
||||
return stream;
|
||||
}
|
||||
|
||||
read(fileName) {
|
||||
const path = this._resolvePath(fileName);
|
||||
return fs.stat(path)
|
||||
.tap(stat => {
|
||||
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
|
||||
})
|
||||
.then(() => {
|
||||
const stream = syncFs.createReadStream(path, {flags: 'r'});
|
||||
return stream;
|
||||
});
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
path = this._resolvePath(path);
|
||||
return fs.stat(path)
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) return fs.rmdir(path);
|
||||
else return fs.unlink(path);
|
||||
});
|
||||
}
|
||||
|
||||
mkdir(path) {
|
||||
path = this._resolvePath(path);
|
||||
return fs.mkdir(path)
|
||||
.then(() => path);
|
||||
}
|
||||
|
||||
rename(from, to) {
|
||||
const fromPath = this._resolvePath(from);
|
||||
const toPath = this._resolvePath(to);
|
||||
return fs.rename(fromPath, toPath);
|
||||
}
|
||||
|
||||
chmod(path, mode) {
|
||||
path = this._resolvePath(path);
|
||||
return fs.chmod(path, mode);
|
||||
}
|
||||
|
||||
getUniqueName() {
|
||||
return uuid.v4().replace(/\W/g, '');
|
||||
}
|
||||
}
|
||||
module.exports = FileSystem;
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = function (path) {
|
||||
return path
|
||||
.replace(/"/g, '""');
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const errors = require('../errors');
|
||||
|
||||
module.exports = function (fileStat, format = 'ls') {
|
||||
if (typeof format === 'function') return format(fileStat);
|
||||
|
||||
const formats = {
|
||||
ls: ls,
|
||||
ep: ep
|
||||
};
|
||||
if (!formats.hasOwnProperty(format)) {
|
||||
throw new errors.FileSystemError('Bad file stat formatter');
|
||||
}
|
||||
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',
|
||||
'1',
|
||||
fileStat.uid,
|
||||
fileStat.gid,
|
||||
_.padStart(fileStat.size, 12),
|
||||
_.padStart(mtime.format(dateFormat), 12),
|
||||
fileStat.name
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function ep(fileStat) {
|
||||
const facts = [
|
||||
fileStat.dev && fileStat.ino ? `i${fileStat.dev.toString(16)}.${fileStat.ino.toString(16)}` : null,
|
||||
fileStat.size ? `s${fileStat.size}` : null,
|
||||
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
|
||||
fileStat.mode ? `up${fileStat.mode.toString(8).substr(fileStat.mode.toString(8).length - 3)}` : null,
|
||||
fileStat.isDirectory() ? 'r' : '/'
|
||||
].join(',');
|
||||
return `+${facts}\t${fileStat.name}`;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
const net = require('net');
|
||||
const when = require('when');
|
||||
const errors = require('../errors');
|
||||
|
||||
module.exports = function (min = 22, max = undefined) {
|
||||
return when.promise((resolve, reject) => {
|
||||
let port = min;
|
||||
let portCheckServer = net.createServer();
|
||||
portCheckServer.maxConnections = 0;
|
||||
portCheckServer.on('error', () => {
|
||||
if (!max || port < max) {
|
||||
port = port + 1;
|
||||
portCheckServer.listen(port);
|
||||
} else {
|
||||
reject(new errors.GeneralError('Unable to find open port', 500));
|
||||
}
|
||||
});
|
||||
portCheckServer.on('listening', () => {
|
||||
const {port} = portCheckServer.address();
|
||||
portCheckServer.close(() => {
|
||||
portCheckServer = null;
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
portCheckServer.listen(port);
|
||||
});
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
const http = require('http');
|
||||
const when = require('when');
|
||||
const errors = require('../errors');
|
||||
|
||||
const IP_WEBSITE = 'http://api.ipify.org/';
|
||||
|
||||
module.exports = function (hostname) {
|
||||
return when.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.on('data', chunk => {
|
||||
ip += chunk;
|
||||
});
|
||||
response.on('end', () => {
|
||||
resolve(ip);
|
||||
});
|
||||
});
|
||||
} else resolve(hostname);
|
||||
});
|
||||
};
|
||||
110
src/index.js
110
src/index.js
@@ -1,110 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const when = require('when');
|
||||
const nodeUrl = require('url');
|
||||
const buyan = require('bunyan');
|
||||
const net = require('net');
|
||||
|
||||
const Connection = require('./connection');
|
||||
const resolveHost = require('./helpers/resolve-host');
|
||||
|
||||
class FtpServer {
|
||||
constructor(url, options = {}) {
|
||||
this.options = _.merge({
|
||||
log: buyan.createLogger({name: 'ftp-srv'}),
|
||||
anonymous: false,
|
||||
pasv_range: 22,
|
||||
file_format: 'ls',
|
||||
blacklist: [],
|
||||
whitelist: []
|
||||
}, options);
|
||||
|
||||
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 => {
|
||||
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)
|
||||
.finally(() => socket.resume());
|
||||
});
|
||||
this.server.on('error', err => {
|
||||
this.log.error(err);
|
||||
});
|
||||
this.on = this.server.on.bind(this.server);
|
||||
this.listeners = this.server.listeners.bind(this.server);
|
||||
}
|
||||
|
||||
listen() {
|
||||
return resolveHost(this.url.hostname)
|
||||
.then(hostname => {
|
||||
this.url.hostname = hostname;
|
||||
return when.promise((resolve, reject) => {
|
||||
this.server.listen(this.url.port, err => {
|
||||
if (err) return reject(err);
|
||||
this.log.info({port: this.url.port}, 'Listening');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
emit(action, ...data) {
|
||||
const defer = when.defer();
|
||||
const params = _.concat(data, [defer.resolve, defer.reject]);
|
||||
this.server.emit(action, ...params);
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
getGreetingMessage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getFeaturesMessage() {
|
||||
let features = [];
|
||||
if (this.options.anonymous) features.push('a');
|
||||
|
||||
if (features.length) {
|
||||
features.unshift('Features:');
|
||||
features.push('.');
|
||||
}
|
||||
return features.length ? features.join(' ') : 'Ready';
|
||||
}
|
||||
|
||||
setGreeting(greeting) {
|
||||
if (typeof greeting === 'string') {
|
||||
this.options.greeting = greeting;
|
||||
} else {
|
||||
greeting.then(greet => {
|
||||
this.options.greeting = greet;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disconnectClient(id) {
|
||||
return when.promise(resolve => {
|
||||
const client = this.connections[id];
|
||||
if (!client) return resolve();
|
||||
delete this.connections[id];
|
||||
client.close(0);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.server.maxConnections = 0;
|
||||
return when.map(Object.keys(this.connections), id => this.disconnectClient(id))
|
||||
.then(() => when.promise((resolve, reject) => {
|
||||
this.server.close(err => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
module.exports = FtpServer;
|
||||
@@ -1,56 +0,0 @@
|
||||
module.exports = {
|
||||
// 100 - 199 :: Remarks
|
||||
100: 'The requested action is being initiated',
|
||||
110: 'Restart marker reply',
|
||||
120: 'Service ready in %s minutes',
|
||||
125: 'Data connection already open; transfer starting',
|
||||
150: 'File status okay; about to open data connection',
|
||||
// 200 - 399 :: Acceptance
|
||||
/// 200 - 299 :: Positive Completion Replies
|
||||
/// These type of replies indicate that the requested action was taken and that the server is awaiting another command.
|
||||
200: 'The requested action has been successfully completed',
|
||||
202: 'Superfluous command',
|
||||
211: 'System status, or system help reply',
|
||||
212: 'Directory status',
|
||||
213: 'File status',
|
||||
214: 'Help message', // On how to use the server or the meaning of a particular non-standard command. This reply is useful only to the human user.
|
||||
215: 'UNIX Type: L8', // NAME system type. Where NAME is an official system name from the list in the Assigned Numbers document.
|
||||
220: 'Service ready for new user',
|
||||
221: 'Service closing control connection', // Logged out if appropriate.
|
||||
225: 'Data connection open; no transfer in progress',
|
||||
226: 'Closing data connection', // Requested file action successful (for example, file transfer or file abort).
|
||||
227: 'Entering Passive Mode', // (h1,h2,h3,h4,p1,p2).
|
||||
230: 'User logged in, proceed',
|
||||
234: 'Honored',
|
||||
250: 'Requested file action okay, completed',
|
||||
257: '\'%s\' created',
|
||||
/// 300 - 399 :: Positive Intermediate Replies
|
||||
/// These types of replies indicate that the requested action was taken and that the server is awaiting further information to complete the request.
|
||||
331: 'Username okay, awaiting password',
|
||||
332: 'Need account for login',
|
||||
350: 'Requested file action pending further information',
|
||||
// 400 - 599 :: Rejection
|
||||
/// 400 - 499 :: Transient Negative Completion Replies
|
||||
/// These types of replies indicate that the command was not accepted; the requested action was not taken.
|
||||
/// However, the error is temporary and the action may be requested again.
|
||||
421: 'Service not available, closing control connection', // This may be a reply to any command if the service knows it must shut down.
|
||||
425: 'Unable to open data connection',
|
||||
426: 'Connection closed; transfer aborted',
|
||||
450: 'Requested file action not taken', // File unavailable (e.g., file busy).
|
||||
451: 'Requested action aborted. Local error in processing',
|
||||
452: 'Requested action not taken. Insufficient storage',
|
||||
/// 500 - 599 :: Permanent Negative Completion Replies
|
||||
/// These types of replies indicate that the command was not accepted; the requested action was not taken.
|
||||
/// The FTP client is "discouraged" from repeating the same exact request.
|
||||
500: 'Syntax error', // Can close connection
|
||||
501: 'Syntax error in parameters or arguments',
|
||||
502: 'Command not supported',
|
||||
503: 'Bad sequence of commands',
|
||||
504: 'Command parameter not supported',
|
||||
530: 'Not logged in', // Permission Denied, Can close connection
|
||||
532: 'Need account for storing files',
|
||||
550: 'Requested action not taken. File unavailable', // (e.g., file not found, no access).
|
||||
551: 'Requested action aborted. Page type unknown',
|
||||
552: 'Requested file action aborted. Exceeded storage allocation', // (for current directory or dataset).
|
||||
553: 'Requested action not taken. File name not allowed'
|
||||
};
|
||||
90
src/server/index.js
Normal file
90
src/server/index.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const net = require('net');
|
||||
const Queue = require('bee-queue');
|
||||
const {Signale} = require('signale');
|
||||
const {matches} = require('z');
|
||||
|
||||
const KeyValueStore = require('../utils/keyValueStore');
|
||||
const {setAsyncTimeout} = require('../utils/setAsyncTimeout')
|
||||
const {setupWorkers} = require('../workers');
|
||||
|
||||
const LISTEN_RETRY_MAX = 2;
|
||||
const LISTEN_RETRY_DELAY = 1500;
|
||||
|
||||
class Server extends net.Server {
|
||||
constructor({
|
||||
host = '0.0.0.0',
|
||||
port = 21,
|
||||
log = {}
|
||||
} = {}) {
|
||||
super({
|
||||
pauseOnConnect: true
|
||||
});
|
||||
|
||||
this.log = new Signale(Object.assign({
|
||||
scope: 'ftp-srv',
|
||||
}, log));
|
||||
this.debugLog = this.log.scope('debug');
|
||||
this.debugLog.config({
|
||||
displayTimestamp: true
|
||||
})
|
||||
|
||||
this.receiveQueue = new Queue('receive');
|
||||
this.sendQueue = new Queue('send');
|
||||
this.workers = new KeyValueStore();
|
||||
this.options = new KeyValueStore({
|
||||
host,
|
||||
port
|
||||
});
|
||||
}
|
||||
|
||||
async listen() {
|
||||
const workers = await setupWorkers();
|
||||
this.workers.sets(workers);
|
||||
|
||||
const port = this.options.get('port');
|
||||
const host = this.options.get('host');
|
||||
|
||||
const tryListen = (retryCount = 1) =>
|
||||
new Promise((resolve, reject) => {
|
||||
super.once('error', reject);
|
||||
super.once('listening', resolve);
|
||||
super.listen(port, host);
|
||||
})
|
||||
.catch(err => matches(err)(
|
||||
(e = {code: 'EADDRINUSE'}) => {
|
||||
if (retryCount > LISTEN_RETRY_MAX) throw e;
|
||||
|
||||
this.log.error({
|
||||
message: `Port (${port}) in use, retrying...`,
|
||||
suffix: `${retryCount} / ${LISTEN_RETRY_MAX}`
|
||||
});
|
||||
return setAsyncTimeout(() => tryListen(++retryCount), LISTEN_RETRY_DELAY);
|
||||
},
|
||||
(e) => {
|
||||
throw e;
|
||||
}
|
||||
))
|
||||
.catch(async e => {
|
||||
await this.close();
|
||||
throw e;
|
||||
});
|
||||
|
||||
await tryListen();
|
||||
return this;
|
||||
}
|
||||
|
||||
async close() {
|
||||
const tryClose = () => new Promise((resolve) => {
|
||||
super.close(err => {
|
||||
if (err) {
|
||||
this.debugLog.error(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await tryClose();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
23
src/server/index.test.js
Normal file
23
src/server/index.test.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
const Server = require('./');
|
||||
|
||||
describe('Server', function () {
|
||||
let server;
|
||||
|
||||
beforeAll(function () {
|
||||
server = new Server({
|
||||
port: 8880
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async function () {
|
||||
const value = await server.close();
|
||||
console.log(value)
|
||||
});
|
||||
|
||||
describe('.listen', function () {
|
||||
it('# listens', async function () {
|
||||
await server.listen();
|
||||
});
|
||||
});
|
||||
});
|
||||
27
src/utils/keyValueStore.js
Normal file
27
src/utils/keyValueStore.js
Normal file
@@ -0,0 +1,27 @@
|
||||
class KeyValueStore {
|
||||
constructor(initial = {}) {
|
||||
this.reset();
|
||||
this.sets(initial);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.values = {};
|
||||
}
|
||||
|
||||
get(key) {
|
||||
if (!this.values || !this.values[key]) return undefined;
|
||||
return this.values[key];
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (!this.values) this.reset();
|
||||
this.values[key] = value;
|
||||
}
|
||||
|
||||
sets(values) {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
this.set(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = KeyValueStore;
|
||||
12
src/utils/setAsyncTimeout.js
Normal file
12
src/utils/setAsyncTimeout.js
Normal file
@@ -0,0 +1,12 @@
|
||||
function setAsyncTimeout(method, timeout, ...args) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(async () => {
|
||||
const result = await method(...args);
|
||||
resolve(result)
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setAsyncTimeout
|
||||
};
|
||||
0
src/workers/command/index.js
Normal file
0
src/workers/command/index.js
Normal file
0
src/workers/data/index.js
Normal file
0
src/workers/data/index.js
Normal file
19
src/workers/index.js
Normal file
19
src/workers/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const path = require('path');
|
||||
const {fork} = require('child_process');
|
||||
|
||||
async function setupWorkers() {
|
||||
const commandWorkerPath = path.resolve(__dirname, './command/index.js');
|
||||
const dataWorkerPath = path.resolve(__dirname, './data/index.js');
|
||||
|
||||
const commandWorker = fork(commandWorkerPath, [], {});
|
||||
const dataWorker = fork(dataWorkerPath, [], {});
|
||||
|
||||
return {
|
||||
command: commandWorker,
|
||||
data: dataWorker
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupWorkers
|
||||
};
|
||||
0
src/workers/worker.js
Normal file
0
src/workers/worker.js
Normal file
@@ -1,53 +0,0 @@
|
||||
const when = require('when');
|
||||
const {expect} = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const CMD = 'ABOR';
|
||||
describe(CMD, function () {
|
||||
let sandbox;
|
||||
const mockClient = {
|
||||
reply: () => when.resolve(),
|
||||
connector: {
|
||||
waitForConnection: () => when.resolve(),
|
||||
end: () => when.resolve()
|
||||
}
|
||||
};
|
||||
const cmdFn = require(`../../src/commands/registration/${CMD.toLowerCase()}`).handler.bind(mockClient);
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
sandbox.spy(mockClient, 'reply');
|
||||
sandbox.spy(mockClient.connector, 'waitForConnection');
|
||||
sandbox.spy(mockClient.connector, 'end');
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('// successful | no active connection', done => {
|
||||
mockClient.connector.waitForConnection.restore();
|
||||
sandbox.stub(mockClient.connector, 'waitForConnection').rejects();
|
||||
|
||||
cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||
expect(mockClient.connector.end.callCount).to.equal(0);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(226);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('// successful | active connection', done => {
|
||||
cmdFn()
|
||||
.then(() => {
|
||||
expect(mockClient.connector.waitForConnection.callCount).to.equal(1);
|
||||
expect(mockClient.connector.end.callCount).to.equal(1);
|
||||
expect(mockClient.reply.args[0][0]).to.equal(426);
|
||||
expect(mockClient.reply.args[1][0]).to.equal(226);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user