Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa1e543c24 | ||
|
|
57f7fa23cc | ||
|
|
e0dbbfce2d | ||
|
|
fc8981021c | ||
|
|
cf9852465a | ||
|
|
1f3f26706e | ||
|
|
54cb2a2fe4 | ||
|
|
5fd6414e60 | ||
|
|
ef7750def0 | ||
|
|
427275a0b8 | ||
|
|
c428787ade | ||
|
|
8566df451e | ||
|
|
35789e430a |
@@ -1,140 +1,61 @@
|
|||||||
version: 2
|
version: 2
|
||||||
jobs:
|
jobs:
|
||||||
build_node_8:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:8
|
- image: circleci/node:8
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
|
||||||
key: npm-install-node-8-{{ checksum "package.json" }}
|
|
||||||
- run:
|
- run:
|
||||||
name: Install
|
name: Building...
|
||||||
command: npm install
|
command: npm install
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: npm-install-node-8-{{ checksum "package.json" }}
|
|
||||||
paths:
|
paths:
|
||||||
- node_modules
|
- node_modules
|
||||||
|
key: node_modules_{{ checksum package.json }}
|
||||||
build_node_6:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:6
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
key: npm-install-node-6-{{ checksum "package.json" }}
|
|
||||||
- run:
|
|
||||||
name: Install
|
|
||||||
command: npm install
|
|
||||||
- save_cache:
|
|
||||||
key: npm-install-node-6-{{ checksum "package.json" }}
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:8
|
- image: circleci/node:8
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: npm-install-node-8-{{ checksum "package.json" }}
|
key: node_modules_{{ checksum package.json }}
|
||||||
- run:
|
- run:
|
||||||
name: Lint
|
name: Linting...
|
||||||
command: npm run verify:js
|
command: npm run lint
|
||||||
|
test:
|
||||||
test_node_8:
|
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:8
|
- image: circleci/node:8
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: npm-install-node-8-{{ checksum "package.json" }}
|
key: node_modules_{{ checksum package.json }}
|
||||||
- run:
|
- run:
|
||||||
name: Test Node 8
|
name: Testing...
|
||||||
command: npm run test:coverage
|
command: npm run test
|
||||||
when: always
|
publish:
|
||||||
- store_test_results:
|
branches:
|
||||||
path: reports
|
only: master
|
||||||
- store_artifacts:
|
|
||||||
path: reports/coverage
|
|
||||||
prefix: coverage
|
|
||||||
|
|
||||||
test_node_6:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:6
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
key: npm-install-node-6-{{ checksum "package.json" }}
|
|
||||||
- run:
|
|
||||||
name: Test Node 6
|
|
||||||
command: npm run test:coverage
|
|
||||||
when: always
|
|
||||||
- store_test_results:
|
|
||||||
path: reports
|
|
||||||
- store_artifacts:
|
|
||||||
path: reports/coverage
|
|
||||||
prefix: coverage
|
|
||||||
|
|
||||||
release:
|
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:8
|
- image: circleci/node:8
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: npm-install-node-6-{{ checksum "package.json" }}
|
key: node_modules_{{ checksum package.json }}
|
||||||
- run:
|
- run:
|
||||||
name: Update NPM
|
name: Publishing...
|
||||||
command: |
|
command: npx semantic-release
|
||||||
npm install npm@5
|
|
||||||
npm install semantic-release@11
|
|
||||||
- deploy:
|
|
||||||
name: Semantic Release
|
|
||||||
command: |
|
|
||||||
npm run semantic-release || true
|
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
test_and_tag:
|
main:
|
||||||
jobs:
|
jobs:
|
||||||
- build_node_8:
|
- build
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: master
|
|
||||||
- build_node_6:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: master
|
|
||||||
- lint:
|
- lint:
|
||||||
requires:
|
requires:
|
||||||
- build_node_8
|
- build
|
||||||
- test_node_6:
|
- test:
|
||||||
requires:
|
|
||||||
- build_node_6
|
|
||||||
- test_node_8:
|
|
||||||
requires:
|
|
||||||
- build_node_8
|
|
||||||
- release:
|
|
||||||
requires:
|
requires:
|
||||||
- lint
|
- lint
|
||||||
- test_node_6
|
- publish:
|
||||||
- test_node_8
|
|
||||||
|
|
||||||
build_and_test:
|
|
||||||
jobs:
|
|
||||||
- build_node_8:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
ignore: master
|
|
||||||
- build_node_6:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
ignore: master
|
|
||||||
- lint:
|
|
||||||
requires:
|
requires:
|
||||||
- build_node_8
|
- test
|
||||||
- test_node_6:
|
|
||||||
requires:
|
|
||||||
- build_node_6
|
|
||||||
- test_node_8:
|
|
||||||
requires:
|
|
||||||
- build_node_8
|
|
||||||
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
|
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
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
|||||||
package-lock.json binary
|
|
||||||
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
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
|
|
||||||
dist/
|
|
||||||
reports/
|
|
||||||
npm-debug.log
|
|
||||||
.nyc_output/
|
|
||||||
test_tmp/
|
|
||||||
24
.nycrc
24
.nycrc
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"check-coverage": true,
|
|
||||||
"per-file": false,
|
|
||||||
"lines": 90,
|
|
||||||
"statements": 90,
|
|
||||||
"functions": 85,
|
|
||||||
"branches": 75,
|
|
||||||
"include": [
|
|
||||||
"src/**/*.js"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"test/**/*.spec.js"
|
|
||||||
],
|
|
||||||
"reporter": [
|
|
||||||
"lcovonly",
|
|
||||||
"html",
|
|
||||||
"text",
|
|
||||||
"cobertura",
|
|
||||||
"json"
|
|
||||||
],
|
|
||||||
"cache": true,
|
|
||||||
"all": true,
|
|
||||||
"report-dir": "./reports/coverage/"
|
|
||||||
}
|
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017 Tyler Stewart
|
Copyright (c) 2018 Tyler Stewart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
254
README.md
254
README.md
@@ -15,263 +15,21 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="https://circleci.com/gh/trs/ftp-srv">
|
<a href="https://circleci.com/gh/trs/ftp-srv">
|
||||||
<img alt="npm" src="https://img.shields.io/circleci/project/github/trs/ftp-srv.svg?style=for-the-badge" />
|
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv.svg?style=for-the-badge" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="https://coveralls.io/github/trs/ftp-srv?branch=master">
|
<a href="https://coveralls.io/github/trs/ftp-srv?branch=master">
|
||||||
<img alt="npm" src="https://img.shields.io/coveralls/github/trs/ftp-srv.svg?style=for-the-badge" />
|
<img alt="coveralls" src="https://img.shields.io/coveralls/github/trs/ftp-srv.svg?style=for-the-badge" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [Overview](#overview)
|
> Looking for v2? Check the [v2](#v2) branch.
|
||||||
- [Features](#features)
|
|
||||||
- [Install](#install)
|
|
||||||
- [Usage](#usage)
|
|
||||||
- [API](#api)
|
|
||||||
- [Events](#events)
|
|
||||||
- [Supported Commands](#supported-commands)
|
|
||||||
- [File System](#file-system)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Overview
|
# Installation
|
||||||
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
|
|
||||||
|
|
||||||
## Features
|
```
|
||||||
- Extensible [file systems](#file-system) per connection
|
$ yarn install
|
||||||
- Passive and active transfers
|
|
||||||
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
|
|
||||||
- Promise based API
|
|
||||||
|
|
||||||
## Install
|
|
||||||
`npm install ftp-srv --save`
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Quick start
|
|
||||||
|
|
||||||
const FtpSrv = require('ftp-srv');
|
|
||||||
const ftpServer = new FtpSrv('ftp://0.0.0.0:9876', { options ... });
|
|
||||||
|
|
||||||
ftpServer.on('login', (data, resolve, reject) => { ... });
|
|
||||||
...
|
|
||||||
|
|
||||||
ftpServer.listen()
|
|
||||||
.then(() => { ... });
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### `new FtpSrv(url, [{options}])`
|
|
||||||
#### url
|
|
||||||
[URL string](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) indicating the protocol, hostname, and port to listen on for connections.
|
|
||||||
Supported protocols:
|
|
||||||
- `ftp` Plain FTP
|
|
||||||
- `ftps` Implicit FTP over TLS
|
|
||||||
|
|
||||||
_Note:_ The hostname must be the external IP address to accept external connections. Setting the hostname to `0.0.0.0` will automatically set the external IP.
|
|
||||||
__Default:__ `"ftp://127.0.0.1:21"`
|
|
||||||
|
|
||||||
#### options
|
|
||||||
|
|
||||||
##### `pasv_range`
|
|
||||||
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
|
|
||||||
This range is then queried for an available port to use when required.
|
|
||||||
__Default:__ `22`
|
|
||||||
|
|
||||||
##### `greeting`
|
|
||||||
A human readable array of lines or string to send when a client connects.
|
|
||||||
__Default:__ `null`
|
|
||||||
|
|
||||||
##### `tls`
|
|
||||||
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
|
|
||||||
__Default:__ `false`
|
|
||||||
|
|
||||||
##### `anonymous`
|
|
||||||
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
|
|
||||||
Can also set as a string which allows users to authenticate using the username provided.
|
|
||||||
The `login` event is then sent with the provided username and `@anonymous` as the password.
|
|
||||||
__Default:__ `false`
|
|
||||||
|
|
||||||
##### `blacklist`
|
|
||||||
Array of commands that are not allowed.
|
|
||||||
Response code `502` is sent to clients sending one of these commands.
|
|
||||||
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
|
|
||||||
__Default:__ `[]`
|
|
||||||
|
|
||||||
##### `whitelist`
|
|
||||||
Array of commands that are only allowed.
|
|
||||||
Response code `502` is sent to clients sending any other command.
|
|
||||||
__Default:__ `[]`
|
|
||||||
|
|
||||||
##### `file_format`
|
|
||||||
Sets the format to use for file stat queries such as `LIST`.
|
|
||||||
__Default:__ `"ls"`
|
|
||||||
__Allowable values:__
|
|
||||||
- `ls` [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
|
|
||||||
- `ep` [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
|
|
||||||
- `function () {}` A custom function returning a format or promise for one.
|
|
||||||
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
|
|
||||||
|
|
||||||
##### `log`
|
|
||||||
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
The `FtpSrv` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
|
|
||||||
|
|
||||||
### `login`
|
|
||||||
```js
|
|
||||||
on('login', ({connection, username, password}, resolve, reject) => { ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
|
|
||||||
|
|
||||||
`connection` [client class object](src/connection.js)
|
|
||||||
`username` string of username from `USER` command
|
|
||||||
`password` string of password from `PASS` command
|
|
||||||
`resolve` takes an object of arguments:
|
|
||||||
- `fs`
|
|
||||||
- Set a custom file system class for this connection to use.
|
|
||||||
- See [File System](#file-system) for implementation details.
|
|
||||||
- `root`
|
|
||||||
- If `fs` is not provided, this will set the root directory for the connection.
|
|
||||||
- The user cannot traverse lower than this directory.
|
|
||||||
- `cwd`
|
|
||||||
- If `fs` is not provided, will set the starting directory for the connection
|
|
||||||
- This is relative to the `root` directory.
|
|
||||||
- `blacklist`
|
|
||||||
- Commands that are forbidden for only this connection
|
|
||||||
- `whitelist`
|
|
||||||
- If set, this connection will only be able to use the provided commands
|
|
||||||
|
|
||||||
`reject` takes an error object
|
|
||||||
|
|
||||||
### `client-error`
|
|
||||||
```js
|
|
||||||
on('client-error', ({connection, context, error}) => { ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
Occurs when an error arises in the client connection.
|
|
||||||
|
|
||||||
`connection` [client class object](src/connection.js)
|
|
||||||
`context` string of where the error occured
|
|
||||||
`error` error object
|
|
||||||
|
|
||||||
### `RETR`
|
|
||||||
```js
|
|
||||||
on('RETR', (error, filePath) => { ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
Occurs when a file is downloaded.
|
|
||||||
|
|
||||||
`error` if successful, will be `null`
|
|
||||||
`filePath` location to which file was downloaded
|
|
||||||
|
|
||||||
### `STOR`
|
|
||||||
```js
|
|
||||||
on('STOR', (error, fileName) => { ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
Occurs when a file is uploaded.
|
|
||||||
|
|
||||||
`error` if successful, will be `null`
|
|
||||||
`fileName` name of the file that was downloaded
|
|
||||||
|
|
||||||
## Supported Commands
|
|
||||||
|
|
||||||
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
|
|
||||||
|
|
||||||
## File System
|
|
||||||
The default [file system](src/fs.js) can be overwritten to use your own implementation.
|
|
||||||
This can allow for virtual file systems, and more.
|
|
||||||
Each connection can set it's own file system based on the user.
|
|
||||||
|
|
||||||
The default file system is exported and can be extended as needed:
|
|
||||||
```js
|
|
||||||
const {FtpSrv, FileSystem} = require('ftp-srv');
|
|
||||||
|
|
||||||
class MyFileSystem extends FileSystem {
|
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(fileName) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Custom file systems can implement the following variables depending on the developers needs:
|
|
||||||
|
|
||||||
### Methods
|
|
||||||
#### [`currentDirectory()`](src/fs.js#L29)
|
|
||||||
Returns a string of the current working directory
|
|
||||||
__Used in:__ `PWD`
|
|
||||||
|
|
||||||
#### [`get(fileName)`](src/fs.js#L33)
|
|
||||||
Returns a file stat object of file or directory
|
|
||||||
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
|
|
||||||
|
|
||||||
#### [`list(path)`](src/fs.js#L39)
|
|
||||||
Returns array of file and directory stat objects
|
|
||||||
__Used in:__ `LIST`, `NLST`, `STAT`
|
|
||||||
|
|
||||||
#### [`chdir(path)`](src/fs.js#L56)
|
|
||||||
Returns new directory relative to current directory
|
|
||||||
__Used in:__ `CWD`, `CDUP`
|
|
||||||
|
|
||||||
#### [`mkdir(path)`](src/fs.js#L96)
|
|
||||||
Returns a path to a newly created directory
|
|
||||||
__Used in:__ `MKD`
|
|
||||||
|
|
||||||
#### [`write(fileName, {append, start})`](src/fs.js#L68)
|
|
||||||
Returns a writable stream
|
|
||||||
Options:
|
|
||||||
`append` if true, append to existing file
|
|
||||||
`start` if set, specifies the byte offset to write to
|
|
||||||
__Used in:__ `STOR`, `APPE`
|
|
||||||
|
|
||||||
#### [`read(fileName, {start})`](src/fs.js#L75)
|
|
||||||
Returns a readable stream
|
|
||||||
Options:
|
|
||||||
`start` if set, specifies the byte offset to read from
|
|
||||||
__Used in:__ `RETR`
|
|
||||||
|
|
||||||
#### [`delete(path)`](src/fs.js#L87)
|
|
||||||
Delete a file or directory
|
|
||||||
__Used in:__ `DELE`
|
|
||||||
|
|
||||||
#### [`rename(from, to)`](src/fs.js#L102)
|
|
||||||
Renames a file or directory
|
|
||||||
__Used in:__ `RNFR`, `RNTO`
|
|
||||||
|
|
||||||
#### [`chmod(path)`](src/fs.js#L108)
|
|
||||||
Modifies a file or directory's permissions
|
|
||||||
__Used in:__ `SITE CHMOD`
|
|
||||||
|
|
||||||
#### [`getUniqueName()`](src/fs.js#L113)
|
|
||||||
Returns a unique file name to write to
|
|
||||||
__Used in:__ `STOU`
|
|
||||||
|
|
||||||
<!--[RM_CONTRIBUTING]-->
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
<!--[RM_LICENSE]-->
|
|
||||||
## License
|
|
||||||
|
|
||||||
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
|
|
||||||
|
|
||||||
<!--[]-->
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)
|
|
||||||
|
|||||||
@@ -1,38 +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: [],
|
|
||||||
|
|
||||||
// it needs to match the value for field type. Eg.: 'fix'
|
|
||||||
/*
|
|
||||||
scopeOverrides: {
|
|
||||||
fix: [
|
|
||||||
|
|
||||||
{name: 'merge'},
|
|
||||||
{name: 'style'},
|
|
||||||
{name: 'e2eTest'},
|
|
||||||
{name: 'unitTest'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
allowCustomScopes: true,
|
|
||||||
allowBreakingChanges: ['feat', 'fix'],
|
|
||||||
|
|
||||||
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
|
|
||||||
appendBranchNameToCommitMessage: false
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
test/**/*.spec.js
|
|
||||||
--reporter mocha-multi-reporters
|
|
||||||
--reporter-options configFile=config/testUnit/reporters.json
|
|
||||||
--ui bdd
|
|
||||||
--bail
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"reporterEnabled": "mocha-pretty-bunyan-nyan",
|
|
||||||
"mochaJunitReporterReporterOptions": {
|
|
||||||
"mochaFile": "reports/junit.xml"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"mocha": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"mocha",
|
|
||||||
"node"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"mocha/no-exclusive-tests": 2,
|
|
||||||
"no-warning-comments": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"terms": ["todo", "fixme", "xxx"],
|
|
||||||
"location": "start"
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"object-curly-spacing": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"array-bracket-spacing": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"brace-style": [
|
|
||||||
2,
|
|
||||||
"1tbs"
|
|
||||||
],
|
|
||||||
"consistent-return": 0,
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"SwitchCase": 1,
|
|
||||||
"MemberExpression": "off"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-multiple-empty-lines": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"max": 2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-use-before-define": [
|
|
||||||
2,
|
|
||||||
"nofunc"
|
|
||||||
],
|
|
||||||
"one-var": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"quote-props": [
|
|
||||||
2,
|
|
||||||
"as-needed"
|
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
2,
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"keyword-spacing": 2,
|
|
||||||
"space-before-function-paren": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"anonymous": "always",
|
|
||||||
"named": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"space-in-parens": [
|
|
||||||
2,
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"strict": [
|
|
||||||
2,
|
|
||||||
"global"
|
|
||||||
],
|
|
||||||
"curly": [
|
|
||||||
2,
|
|
||||||
"multi-line"
|
|
||||||
],
|
|
||||||
"eol-last": 2,
|
|
||||||
"key-spacing": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"beforeColon": false,
|
|
||||||
"afterColon": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-eval": 2,
|
|
||||||
"no-with": 2,
|
|
||||||
"space-infix-ops": 2,
|
|
||||||
"dot-notation": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"allowKeywords": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"eqeqeq": 2,
|
|
||||||
"no-alert": 2,
|
|
||||||
"no-caller": 2,
|
|
||||||
"no-extend-native": 2,
|
|
||||||
"no-extra-bind": 2,
|
|
||||||
"no-implied-eval": 2,
|
|
||||||
"no-iterator": 2,
|
|
||||||
"no-label-var": 2,
|
|
||||||
"no-labels": 2,
|
|
||||||
"no-lone-blocks": 2,
|
|
||||||
"no-loop-func": 2,
|
|
||||||
"no-multi-spaces": 2,
|
|
||||||
"no-multi-str": 2,
|
|
||||||
"no-native-reassign": 2,
|
|
||||||
"no-new": 2,
|
|
||||||
"no-new-func": 2,
|
|
||||||
"no-new-wrappers": 2,
|
|
||||||
"no-octal-escape": 2,
|
|
||||||
"no-proto": 2,
|
|
||||||
"no-return-assign": 2,
|
|
||||||
"no-script-url": 2,
|
|
||||||
"no-sequences": 2,
|
|
||||||
"no-unused-expressions": 2,
|
|
||||||
"yoda": 2,
|
|
||||||
"no-shadow": 2,
|
|
||||||
"no-shadow-restricted-names": 2,
|
|
||||||
"no-undef-init": 2,
|
|
||||||
"no-console": 1,
|
|
||||||
"camelcase": [
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
"properties": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"comma-spacing": 2,
|
|
||||||
"comma-dangle": 1,
|
|
||||||
"new-cap": 2,
|
|
||||||
"new-parens": 2,
|
|
||||||
"arrow-parens": [2, "as-needed"],
|
|
||||||
"no-array-constructor": 2,
|
|
||||||
"array-callback-return": 1,
|
|
||||||
"no-extra-parens": 2,
|
|
||||||
"no-new-object": 2,
|
|
||||||
"no-spaced-func": 2,
|
|
||||||
"no-trailing-spaces": 2,
|
|
||||||
"no-underscore-dangle": 0,
|
|
||||||
"no-fallthrough": 0,
|
|
||||||
"semi": 2,
|
|
||||||
"semi-spacing": [
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"before": false,
|
|
||||||
"after": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"emcaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"impliedStrict": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
confit.yml
49
confit.yml
@@ -1,49 +0,0 @@
|
|||||||
generator-confit:
|
|
||||||
app:
|
|
||||||
_version: 462ecd915fd9db1aef6a37c2b5ce8b58b80c18ba
|
|
||||||
buildProfile: Latest
|
|
||||||
copyrightOwner: Tyler Stewart
|
|
||||||
license: MIT
|
|
||||||
projectType: node
|
|
||||||
publicRepository: true
|
|
||||||
repositoryType: GitHub
|
|
||||||
paths:
|
|
||||||
_version: 780b129e0c7e5cab7e29c4f185bcf78524593a33
|
|
||||||
config:
|
|
||||||
configDir: config/
|
|
||||||
input:
|
|
||||||
srcDir: src/
|
|
||||||
unitTestDir: test/
|
|
||||||
output:
|
|
||||||
prodDir: dist/
|
|
||||||
reportDir: reports/
|
|
||||||
buildJS:
|
|
||||||
_version: ead8ce4280b07d696aff499a5fca1a933727582f
|
|
||||||
framework: []
|
|
||||||
frameworkScripts: []
|
|
||||||
outputFormat: ES6
|
|
||||||
sourceFormat: ES6
|
|
||||||
entryPoint:
|
|
||||||
_version: 39082c3df887fbc08744dfd088c25465e7a2e3a4
|
|
||||||
entryPoints:
|
|
||||||
main:
|
|
||||||
- src/index.js
|
|
||||||
testUnit:
|
|
||||||
_version: 30eee42a88ee42cce4f1ae48fe0cbe81647d189a
|
|
||||||
testDependencies: []
|
|
||||||
testFramework: mocha
|
|
||||||
verify:
|
|
||||||
_version: 30ae86c5022840a01fc08833e238a82c683fa1c7
|
|
||||||
jsCodingStandard: none
|
|
||||||
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)
|
||||||
|
})
|
||||||
123
ftp-srv.d.ts
vendored
123
ftp-srv.d.ts
vendored
@@ -1,123 +0,0 @@
|
|||||||
import * as tls from 'tls'
|
|
||||||
import { Stats } from 'fs'
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
|
|
||||||
export class FileSystem {
|
|
||||||
|
|
||||||
readonly connection: FtpConnection;
|
|
||||||
readonly root: string;
|
|
||||||
readonly cwd: string;
|
|
||||||
|
|
||||||
constructor(connection: FtpConnection, {root, cwd}?: {
|
|
||||||
root: any;
|
|
||||||
cwd: any;
|
|
||||||
});
|
|
||||||
|
|
||||||
currentDirectory(): string;
|
|
||||||
|
|
||||||
get(fileName: string): Promise<any>;
|
|
||||||
|
|
||||||
list(path?: string): Promise<any>;
|
|
||||||
|
|
||||||
chdir(path?: string): Promise<string>;
|
|
||||||
|
|
||||||
write(fileName: string, {append, start}?: {
|
|
||||||
append?: boolean;
|
|
||||||
start?: any;
|
|
||||||
}): any;
|
|
||||||
|
|
||||||
read(fileName: string, {start}?: {
|
|
||||||
start?: any;
|
|
||||||
}): Promise<any>;
|
|
||||||
|
|
||||||
delete(path: string): Promise<any>;
|
|
||||||
|
|
||||||
mkdir(path: string): Promise<any>;
|
|
||||||
|
|
||||||
rename(from: string, to: string): Promise<any>;
|
|
||||||
|
|
||||||
chmod(path: string, mode: string): Promise<any>;
|
|
||||||
|
|
||||||
getUniqueName(): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FtpConnection {
|
|
||||||
server: FtpServer;
|
|
||||||
id: string;
|
|
||||||
log: any;
|
|
||||||
transferType: string;
|
|
||||||
encoding: string;
|
|
||||||
bufferSize: boolean;
|
|
||||||
readonly ip: string;
|
|
||||||
restByteCount: number | undefined;
|
|
||||||
secure: boolean
|
|
||||||
|
|
||||||
close (code: number, message: number): Promise<any>
|
|
||||||
login (username: string, password: string): Promise<any>
|
|
||||||
reply (options: number | Object, ...letters: Array<any>): Promise<any>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FtpServerOptions {
|
|
||||||
pasv_range?: number | string,
|
|
||||||
greeting?: string | string[],
|
|
||||||
tls?: tls.SecureContext | false,
|
|
||||||
anonymous?: boolean,
|
|
||||||
blacklist?: Array<string>,
|
|
||||||
whitelist?: Array<string>,
|
|
||||||
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
|
|
||||||
log?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FtpServer {
|
|
||||||
constructor(url: string, options?: FtpServerOptions);
|
|
||||||
|
|
||||||
readonly isTLS: boolean;
|
|
||||||
|
|
||||||
listen(): any;
|
|
||||||
|
|
||||||
emitPromise(action: any, ...data: any[]): Promise<any>;
|
|
||||||
|
|
||||||
emit(action: any, ...data: any[]): void;
|
|
||||||
|
|
||||||
setupTLS(_tls: boolean): boolean | {
|
|
||||||
cert: string;
|
|
||||||
key: string;
|
|
||||||
ca: string
|
|
||||||
};
|
|
||||||
|
|
||||||
setupGreeting(greet: string): string[];
|
|
||||||
|
|
||||||
setupFeaturesMessage(): string;
|
|
||||||
|
|
||||||
disconnectClient(id: string): Promise<any>;
|
|
||||||
|
|
||||||
close(): any;
|
|
||||||
|
|
||||||
on(event: "login", listener: (
|
|
||||||
data: {
|
|
||||||
connection: FtpConnection,
|
|
||||||
username: string,
|
|
||||||
password: string
|
|
||||||
},
|
|
||||||
resolve: (config: {
|
|
||||||
fs?: FileSystem,
|
|
||||||
root?: string,
|
|
||||||
cwd?: string,
|
|
||||||
blacklist?: Array<string>,
|
|
||||||
whitelist?: Array<string>
|
|
||||||
}) => void,
|
|
||||||
reject: (err?: Error) => void
|
|
||||||
) => void): EventEmitter;
|
|
||||||
|
|
||||||
on(event: "client-error", listener: (
|
|
||||||
data: {
|
|
||||||
connection: FtpConnection,
|
|
||||||
context: string,
|
|
||||||
error: Error,
|
|
||||||
}
|
|
||||||
) => void): EventEmitter;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {FtpServer as FtpSrv};
|
|
||||||
export default FtpServer;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const FtpSrv = require('./src');
|
|
||||||
const FileSystem = require('./src/fs');
|
|
||||||
|
|
||||||
module.exports = FtpSrv;
|
|
||||||
module.exports.FtpSrv = FtpSrv;
|
|
||||||
module.exports.FileSystem = FileSystem;
|
|
||||||
@@ -1,23 +1,19 @@
|
|||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer');
|
||||||
const logoPath = `file://${process.cwd()}/logo/logo.html`;
|
|
||||||
|
|
||||||
puppeteer.launch()
|
(async function () {
|
||||||
.then(browser => {
|
const logoPath = `file://${process.cwd()}/logo/logo.html`;
|
||||||
return browser.newPage()
|
|
||||||
.then(page => {
|
const browser = await puppeteer.launch();
|
||||||
return page.goto(logoPath)
|
const page = await browser.newPage();
|
||||||
.then(() => page);
|
await page.goto(logoPath);
|
||||||
})
|
await page.setViewport({
|
||||||
.then(page => {
|
width: 600,
|
||||||
return page.setViewport({
|
height: 250,
|
||||||
width: 600,
|
deviceScaleFactor: 2
|
||||||
height: 250,
|
});
|
||||||
deviceScaleFactor: 2
|
await page.screenshot({
|
||||||
})
|
path: 'logo.png',
|
||||||
.then(() => page.screenshot({
|
omitBackground: true
|
||||||
path: 'logo.png',
|
});
|
||||||
omitBackground: true
|
await browser.close();
|
||||||
}));
|
})();
|
||||||
})
|
|
||||||
.then(() => browser.close());
|
|
||||||
});
|
|
||||||
|
|||||||
7495
package-lock.json
generated
7495
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
77
package.json
77
package.json
@@ -12,84 +12,25 @@
|
|||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
|
||||||
"src",
|
|
||||||
"ftp-srv.d.ts"
|
|
||||||
],
|
|
||||||
"main": "ftp-srv.js",
|
|
||||||
"types": "./ftp-srv.d.ts",
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/trs/ftp-srv"
|
"url": "https://github.com/trs/ftp-srv"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pre-release": "npm-run-all verify test:coverage build ",
|
"test": "jest ./src/**/*.test.js --verbose",
|
||||||
"build": "cross-env NODE_ENV=production npm run clean:prod",
|
"lint": "eslint -c .config/.eslintrc.json \"src/**/*.js\" \"logo/**/*.js\" \"examples/**/*.js\""
|
||||||
"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",
|
|
||||||
"start": "npm run dev",
|
|
||||||
"test": "npm run test:unit",
|
|
||||||
"test:check-coverage": "nyc check-coverage",
|
|
||||||
"test:coverage": "npm-run-all test:unit:once test:check-coverage --silent",
|
|
||||||
"test:unit": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts -w",
|
|
||||||
"test:unit:once": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts",
|
|
||||||
"upload-coverage": "cat reports/coverage/lcov.info | coveralls",
|
|
||||||
"verify": "npm run verify:js --silent",
|
|
||||||
"verify:js": "eslint -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js success",
|
|
||||||
"verify:js:fix": "eslint --fix -c config/verify/.eslintrc \"src/**/*.js\" \"test/**/*.js\" \"config/**/*.js\" && echo ✅ verify:js:fix success",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"release": {
|
|
||||||
"verifyConditions": "condition-circle"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"commitizen": {
|
|
||||||
"path": "node_modules/cz-customizable"
|
|
||||||
},
|
|
||||||
"cz-customizable": {
|
|
||||||
"config": "config/release/commitMessageConfig.js"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bluebird": "^3.5.1",
|
"bee-queue": "^1.2.2",
|
||||||
"bunyan": "^1.8.12",
|
"signale": "^1.1.0",
|
||||||
"lodash": "^4.17.4",
|
"z": "^1.0.8"
|
||||||
"moment": "^2.19.1",
|
|
||||||
"uuid": "^3.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@icetee/ftp": "^0.3.15",
|
"eslint": "^4.18.2",
|
||||||
"chai": "^4.0.2",
|
"jest": "^22.4.2",
|
||||||
"chokidar-cli": "1.2.0",
|
"semantic-release": "^15.0.2"
|
||||||
"condition-circle": "^1.6.0",
|
|
||||||
"coveralls": "2.13.1",
|
|
||||||
"cross-env": "3.1.4",
|
|
||||||
"cz-customizable": "5.2.0",
|
|
||||||
"cz-customizable-ghooks": "1.5.0",
|
|
||||||
"dotenv": "^4.0.0",
|
|
||||||
"eslint": "4.5.0",
|
|
||||||
"eslint-config-google": "0.8.0",
|
|
||||||
"eslint-friendly-formatter": "3.0.0",
|
|
||||||
"eslint-plugin-mocha": "^4.11.0",
|
|
||||||
"eslint-plugin-node": "5.1.1",
|
|
||||||
"husky": "0.13.3",
|
|
||||||
"istanbul": "0.4.5",
|
|
||||||
"mocha": "3.5.0",
|
|
||||||
"mocha-junit-reporter": "1.13.0",
|
|
||||||
"mocha-multi-reporters": "1.1.5",
|
|
||||||
"mocha-pretty-bunyan-nyan": "^1.0.4",
|
|
||||||
"npm-run-all": "4.0.2",
|
|
||||||
"nyc": "11.1.0",
|
|
||||||
"rimraf": "2.6.1",
|
|
||||||
"semantic-release": "^11.0.2",
|
|
||||||
"sinon": "^2.3.5"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.x",
|
"node": ">=8.x"
|
||||||
"npm": ">=3.9.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,71 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(message) {
|
|
||||||
const strippedMessage = message.replace(/"/g, '');
|
|
||||||
const [directive, ...args] = strippedMessage.split(' ');
|
|
||||||
const params = args.reduce(({arg, flags}, param) => {
|
|
||||||
if (/^-{1,2}[a-zA-Z0-9_]+/.test(param)) flags.push(param);
|
|
||||||
else arg.push(param);
|
|
||||||
return {arg, flags};
|
|
||||||
}, {arg: [], flags: []});
|
|
||||||
|
|
||||||
const command = {
|
|
||||||
directive: _.chain(directive).trim().toUpper().value(),
|
|
||||||
arg: params.arg.length ? params.arg.join(' ') : null,
|
|
||||||
flags: params.flags,
|
|
||||||
raw: message
|
|
||||||
};
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
handle(command) {
|
|
||||||
if (typeof command === 'string') command = this.parse(command);
|
|
||||||
|
|
||||||
// Obfuscate password from logs
|
|
||||||
const logCommand = _.clone(command);
|
|
||||||
if (logCommand.directive === 'PASS') logCommand.arg = '********';
|
|
||||||
|
|
||||||
const log = this.connection.log.child({directive: command.directive});
|
|
||||||
log.trace({command: logCommand}, 'Handle command');
|
|
||||||
|
|
||||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
|
||||||
return this.connection.reply(402, 'Command not allowed');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Promise.resolve(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())
|
|
||||||
.then(() => this.reply(226));
|
|
||||||
})
|
|
||||||
.catch(() => this.reply(225));
|
|
||||||
},
|
|
||||||
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,42 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const tls = require('tls');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'AUTH',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
const method = _.upperCase(command.arg);
|
|
||||||
|
|
||||||
switch (method) {
|
|
||||||
case 'TLS': return handleTLS.call(this);
|
|
||||||
default: return this.reply(504);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <type>',
|
|
||||||
description: 'Set authentication mechanism',
|
|
||||||
flags: {
|
|
||||||
no_auth: true,
|
|
||||||
feat: 'AUTH TLS'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleTLS() {
|
|
||||||
if (!this.server._tls) return this.reply(502);
|
|
||||||
if (this.secure) return this.reply(202);
|
|
||||||
|
|
||||||
return this.reply(234)
|
|
||||||
.then(() => {
|
|
||||||
const secureContext = tls.createSecureContext(this.server._tls);
|
|
||||||
const secureSocket = new tls.TLSSocket(this.commandSocket, {
|
|
||||||
isServer: true,
|
|
||||||
secureContext
|
|
||||||
});
|
|
||||||
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
|
|
||||||
function forwardEvent() {
|
|
||||||
this.emit.apply(this, arguments);
|
|
||||||
}
|
|
||||||
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
|
|
||||||
});
|
|
||||||
this.commandSocket = secureSocket;
|
|
||||||
this.secure = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const cwd = require('./cwd').handler;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: ['CDUP', 'XCUP'],
|
|
||||||
handler: function (args) {
|
|
||||||
args.command.arg = '..';
|
|
||||||
return cwd.call(this, args);
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}}',
|
|
||||||
description: 'Change to Parent Directory'
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
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 Promise.resolve(this.fs.chdir(command.arg))
|
|
||||||
.then(cwd => {
|
|
||||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
|
||||||
return this.reply(250, path);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <path>',
|
|
||||||
description: 'Change working directory'
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
|
|
||||||
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 Promise.resolve(this.fs.delete(command.arg))
|
|
||||||
.then(() => {
|
|
||||||
return this.reply(250);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <path>',
|
|
||||||
description: 'Delete file'
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const ActiveConnector = require('../../connector/active');
|
|
||||||
|
|
||||||
const FAMILY = {
|
|
||||||
1: 4,
|
|
||||||
2: 6
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'EPRT',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
|
|
||||||
const family = FAMILY[protocol];
|
|
||||||
if (!family) return this.reply(504, 'Unknown network protocol');
|
|
||||||
|
|
||||||
this.connector = new ActiveConnector(this);
|
|
||||||
return this.connector.setupConnection(ip, port, family)
|
|
||||||
.then(() => this.reply(200));
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
|
|
||||||
description: 'Specifies an address and port to which the server should connect'
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const PassiveConnector = require('../../connector/passive');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'EPSV',
|
|
||||||
handler: function () {
|
|
||||||
this.connector = new PassiveConnector(this);
|
|
||||||
return this.connector.setupServer()
|
|
||||||
.then(server => {
|
|
||||||
const {port} = server.address();
|
|
||||||
|
|
||||||
return this.reply(229, `EPSV OK (|||${port}|)`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} [<protocol>]',
|
|
||||||
description: 'Initiate passive mode'
|
|
||||||
};
|
|
||||||
@@ -1,27 +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;
|
|
||||||
}, ['UTF8'])
|
|
||||||
.sort()
|
|
||||||
.map(feat => ({
|
|
||||||
message: ` ${feat}`,
|
|
||||||
raw: true
|
|
||||||
}));
|
|
||||||
return features.length
|
|
||||||
? this.reply(211, 'Extensions supported', ...features, 'End')
|
|
||||||
: this.reply(211, 'No features');
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}}',
|
|
||||||
description: 'Get the feature list implemented by the server',
|
|
||||||
flags: {
|
|
||||||
no_auth: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'HELP',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
const registry = require('../registry');
|
|
||||||
const directive = _.upperCase(command.arg);
|
|
||||||
if (directive) {
|
|
||||||
if (!registry.hasOwnProperty(directive)) return this.reply(502, `Unknown command ${directive}.`);
|
|
||||||
|
|
||||||
const {syntax, description} = registry[directive];
|
|
||||||
const reply = _.concat([syntax.replace('{{cmd}}', directive), description]);
|
|
||||||
return this.reply(214, ...reply);
|
|
||||||
} else {
|
|
||||||
const supportedCommands = _.chunk(Object.keys(registry), 5).map(chunk => chunk.join('\t'));
|
|
||||||
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} [<command>]',
|
|
||||||
description: 'Returns usage documentation on a command if specified, else a general help document is returned',
|
|
||||||
flags: {
|
|
||||||
no_auth: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const getFileStat = require('../../helpers/file-stat');
|
|
||||||
|
|
||||||
// http://cr.yp.to/ftp/list.html
|
|
||||||
// http://cr.yp.to/ftp/list/eplf.html
|
|
||||||
module.exports = {
|
|
||||||
directive: 'LIST',
|
|
||||||
handler: function ({log, command} = {}) {
|
|
||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
|
||||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
|
||||||
|
|
||||||
const simple = command.directive === 'NLST';
|
|
||||||
|
|
||||||
const path = command.arg || '.';
|
|
||||||
return this.connector.waitForConnection()
|
|
||||||
.tap(() => this.commandSocket.pause())
|
|
||||||
.then(() => Promise.resolve(this.fs.get(path)))
|
|
||||||
.then(stat => stat.isDirectory() ? Promise.resolve(this.fs.list(path)) : [stat])
|
|
||||||
.then(files => {
|
|
||||||
const getFileMessage = file => {
|
|
||||||
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: this.connector.socket
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return this.reply(150)
|
|
||||||
.then(() => {
|
|
||||||
if (fileList.length) return this.reply({}, ...fileList);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => this.reply(226))
|
|
||||||
.catch(Promise.TimeoutError, err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(425, 'No connection established');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(451, err.message || 'No directory');
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.connector.end();
|
|
||||||
this.commandSocket.resume();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} [<path>]',
|
|
||||||
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
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 Promise.resolve(this.fs.get(command.arg))
|
|
||||||
.then(fileStat => {
|
|
||||||
const modificationTime = moment.utc(fileStat.mtime).format('YYYYMMDDHHmmss.SSS');
|
|
||||||
return this.reply(213, modificationTime);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <path>',
|
|
||||||
description: 'Return the last-modified time of a specified file',
|
|
||||||
flags: {
|
|
||||||
feat: 'MDTM'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
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 Promise.resolve(this.fs.mkdir(command.arg))
|
|
||||||
.then(dir => {
|
|
||||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
|
||||||
return this.reply(257, path);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <path>',
|
|
||||||
description: 'Make directory'
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
directive: 'MODE',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
return this.reply(/^S$/i.test(command.arg) ? 200 : 504);
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <mode>',
|
|
||||||
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
|
|
||||||
flags: {
|
|
||||||
obsolete: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const list = require('./list').handler;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'NLST',
|
|
||||||
handler: function (args) {
|
|
||||||
return list.call(this, args);
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} [<path>]',
|
|
||||||
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,39 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const OPTIONS = {
|
|
||||||
UTF8: utf8,
|
|
||||||
'UTF-8': utf8
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'OPTS',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
if (!_.has(command, 'arg')) return this.reply(501);
|
|
||||||
|
|
||||||
const [_option, ...args] = command.arg.split(' ');
|
|
||||||
const option = _.toUpper(_option);
|
|
||||||
|
|
||||||
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
|
|
||||||
return OPTIONS[option].call(this, args);
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}}',
|
|
||||||
description: 'Select options for a feature'
|
|
||||||
};
|
|
||||||
|
|
||||||
function utf8([setting] = []) {
|
|
||||||
const getEncoding = () => {
|
|
||||||
switch (_.toUpper(setting)) {
|
|
||||||
case 'ON': return 'utf8';
|
|
||||||
case 'OFF': return 'ascii';
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const encoding = getEncoding();
|
|
||||||
if (!encoding) return this.reply(501, 'Unknown setting for option');
|
|
||||||
|
|
||||||
this.encoding = encoding;
|
|
||||||
if (this.transferType !== 'binary') this.transferType = this.encoding;
|
|
||||||
|
|
||||||
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
directive: 'PASS',
|
|
||||||
handler: function ({log, command} = {}) {
|
|
||||||
if (!this.username) return this.reply(503);
|
|
||||||
if (this.authenticated) return this.reply(202);
|
|
||||||
|
|
||||||
// 332 : require account name (ACCT)
|
|
||||||
|
|
||||||
const password = command.arg;
|
|
||||||
if (!password) return this.reply(501, 'Must provide password');
|
|
||||||
return 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,14 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
directive: 'PBSZ',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
|
||||||
this.bufferSize = parseInt(command.arg, 10);
|
|
||||||
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}}',
|
|
||||||
description: 'Protection Buffer Size',
|
|
||||||
flags: {
|
|
||||||
no_auth: true,
|
|
||||||
feat: 'PBSZ'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,21 +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, 'arg', '').split(',');
|
|
||||||
if (rawConnection.length !== 6) return this.reply(425);
|
|
||||||
|
|
||||||
const ip = rawConnection.slice(0, 4).join('.');
|
|
||||||
const portBytes = rawConnection.slice(4).map(p => parseInt(p));
|
|
||||||
const port = portBytes[0] * 256 + portBytes[1];
|
|
||||||
|
|
||||||
return this.connector.setupConnection(ip, port)
|
|
||||||
.then(() => this.reply(200));
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
|
||||||
description: 'Specifies an address and port to which the server should connect'
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'PROT',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
if (!this.secure) return this.reply(202, 'Not suppored');
|
|
||||||
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
|
|
||||||
|
|
||||||
switch (_.toUpper(command.arg)) {
|
|
||||||
case 'P': return this.reply(200, 'OK');
|
|
||||||
case 'C':
|
|
||||||
case 'S':
|
|
||||||
case 'E': return this.reply(536, 'Not supported');
|
|
||||||
default: return this.reply(504);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}}',
|
|
||||||
description: 'Data Channel Protection Level',
|
|
||||||
flags: {
|
|
||||||
no_auth: true,
|
|
||||||
feat: 'PROT'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
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 Promise.resolve(this.fs.currentDirectory())
|
|
||||||
.then(cwd => {
|
|
||||||
const path = cwd ? `"${escapePath(cwd)}"` : undefined;
|
|
||||||
return this.reply(257, path);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}}',
|
|
||||||
description: 'Print current working directory'
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
directive: 'QUIT',
|
|
||||||
handler: function () {
|
|
||||||
return this.close(221, 'Client called QUIT');
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}}',
|
|
||||||
description: 'Disconnect',
|
|
||||||
flags: {
|
|
||||||
no_auth: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'REST',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
const arg = _.get(command, 'arg');
|
|
||||||
const byteCount = parseInt(arg, 10);
|
|
||||||
|
|
||||||
if (isNaN(byteCount) || byteCount < 0) return this.reply(501, 'Byte count must be 0 or greater');
|
|
||||||
|
|
||||||
this.restByteCount = byteCount;
|
|
||||||
return this.reply(350, `Restarting next transfer at ${byteCount}`);
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <byte-count>',
|
|
||||||
description: 'Restart transfer from the specified point. Resets after any STORE or RETRIEVE'
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
const filePath = command.arg;
|
|
||||||
|
|
||||||
return this.connector.waitForConnection()
|
|
||||||
.tap(() => this.commandSocket.pause())
|
|
||||||
.then(() => Promise.resolve(this.fs.read(filePath, {start: this.restByteCount})))
|
|
||||||
.then(stream => {
|
|
||||||
const destroyConnection = (connection, reject) => err => {
|
|
||||||
if (connection) connection.destroy(err);
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventsPromise = new Promise((resolve, reject) => {
|
|
||||||
stream.on('data', data => {
|
|
||||||
if (stream) stream.pause();
|
|
||||||
if (this.connector.socket) {
|
|
||||||
this.connector.socket.write(data, this.transferType, () => stream && stream.resume());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stream.once('end', () => resolve());
|
|
||||||
stream.once('error', destroyConnection(this.connector.socket, reject));
|
|
||||||
|
|
||||||
this.connector.socket.once('error', destroyConnection(stream, reject));
|
|
||||||
});
|
|
||||||
|
|
||||||
this.restByteCount = 0;
|
|
||||||
|
|
||||||
return this.reply(150).then(() => stream.resume() && this.connector.socket.resume())
|
|
||||||
.then(() => eventsPromise)
|
|
||||||
.tap(() => this.emit('RETR', null, filePath))
|
|
||||||
.finally(() => stream.destroy && stream.destroy());
|
|
||||||
})
|
|
||||||
.then(() => this.reply(226))
|
|
||||||
.catch(Promise.TimeoutError, err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(425, 'No connection established');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
this.emit('RETR', err);
|
|
||||||
return this.reply(551, err.message);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.connector.end();
|
|
||||||
this.commandSocket.resume();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <path>',
|
|
||||||
description: 'Retrieve a copy of the file'
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const {handler: dele} = require('./dele');
|
|
||||||
|
|
||||||
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 Promise = require('bluebird');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'RNFR',
|
|
||||||
handler: function ({log, command} = {}) {
|
|
||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
|
||||||
|
|
||||||
const fileName = command.arg;
|
|
||||||
return Promise.resolve(this.fs.get(fileName))
|
|
||||||
.then(() => {
|
|
||||||
this.renameFrom = fileName;
|
|
||||||
return this.reply(350);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <name>',
|
|
||||||
description: 'Rename from'
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'RNTO',
|
|
||||||
handler: function ({log, command} = {}) {
|
|
||||||
if (!this.renameFrom) return this.reply(503);
|
|
||||||
|
|
||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
|
||||||
if (!this.fs.rename) return this.reply(402, 'Not supported by file system');
|
|
||||||
|
|
||||||
const from = this.renameFrom;
|
|
||||||
const to = command.arg;
|
|
||||||
|
|
||||||
return Promise.resolve(this.fs.rename(from, to))
|
|
||||||
.then(() => {
|
|
||||||
return this.reply(250);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
delete this.renameFrom;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <name>',
|
|
||||||
description: 'Rename to'
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
|
|
||||||
module.exports = function ({log, command} = {}) {
|
|
||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
|
||||||
if (!this.fs.chmod) return this.reply(402, 'Not supported by file system');
|
|
||||||
|
|
||||||
const [mode, ...fileNameParts] = command.arg.split(' ');
|
|
||||||
const fileName = fileNameParts.join(' ');
|
|
||||||
return Promise.resolve(this.fs.chmod(fileName, parseInt(mode, 8)))
|
|
||||||
.then(() => {
|
|
||||||
return this.reply(200);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(500);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const registry = require('./registry');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'SITE',
|
|
||||||
handler: function ({log, command} = {}) {
|
|
||||||
const rawSubCommand = _.get(command, 'arg', '');
|
|
||||||
const subCommand = this.commands.parse(rawSubCommand);
|
|
||||||
const subLog = log.child({subverb: subCommand.directive});
|
|
||||||
|
|
||||||
if (!registry.hasOwnProperty(subCommand.directive)) return this.reply(502);
|
|
||||||
|
|
||||||
const handler = registry[subCommand.directive].handler.bind(this);
|
|
||||||
return Promise.resolve(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 Promise = require('bluebird');
|
|
||||||
|
|
||||||
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 Promise.resolve(this.fs.get(command.arg))
|
|
||||||
.then(fileStat => {
|
|
||||||
return this.reply(213, {message: fileStat.size});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <path>',
|
|
||||||
description: 'Return the size of a file',
|
|
||||||
flags: {
|
|
||||||
feat: 'SIZE'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const getFileStat = require('../../helpers/file-stat');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'STAT',
|
|
||||||
handler: function (args = {}) {
|
|
||||||
const {log, command} = args;
|
|
||||||
const path = _.get(command, 'arg');
|
|
||||||
if (path) {
|
|
||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
|
||||||
if (!this.fs.get) return this.reply(402, 'Not supported by file system');
|
|
||||||
|
|
||||||
return Promise.resolve(this.fs.get(path))
|
|
||||||
.then(stat => {
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
if (!this.fs.list) return this.reply(402, 'Not supported by file system');
|
|
||||||
|
|
||||||
return Promise.resolve(this.fs.list(path))
|
|
||||||
.then(stats => [213, stats]);
|
|
||||||
}
|
|
||||||
return [212, [stat]];
|
|
||||||
})
|
|
||||||
.then(([code, fileStats]) => {
|
|
||||||
return Promise.map(fileStats, file => {
|
|
||||||
const message = getFileStat(file, _.get(this, 'server.options.file_format', 'ls'));
|
|
||||||
return {
|
|
||||||
raw: true,
|
|
||||||
message
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.then(messages => [code, messages]);
|
|
||||||
})
|
|
||||||
.then(([code, messages]) => this.reply(code, 'Status begin', ...messages, 'Status end'))
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(450, err.message);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.reply(211, 'Status OK');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} [<path>]',
|
|
||||||
description: 'Returns the current status'
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'STOR',
|
|
||||||
handler: function ({log, command} = {}) {
|
|
||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
|
||||||
if (!this.fs.write) return this.reply(402, 'Not supported by file system');
|
|
||||||
|
|
||||||
const append = command.directive === 'APPE';
|
|
||||||
const fileName = command.arg;
|
|
||||||
|
|
||||||
return this.connector.waitForConnection()
|
|
||||||
.tap(() => this.commandSocket.pause())
|
|
||||||
.then(() => Promise.resolve(this.fs.write(fileName, {append, start: this.restByteCount})))
|
|
||||||
.then(stream => {
|
|
||||||
const destroyConnection = (connection, reject) => err => {
|
|
||||||
if (connection) connection.destroy(err);
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
|
|
||||||
const streamPromise = new Promise((resolve, reject) => {
|
|
||||||
stream.once('error', destroyConnection(this.connector.socket, reject));
|
|
||||||
stream.once('finish', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const socketPromise = new Promise((resolve, reject) => {
|
|
||||||
this.connector.socket.on('data', data => {
|
|
||||||
if (this.connector.socket) this.connector.socket.pause();
|
|
||||||
if (stream) {
|
|
||||||
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.connector.socket.once('end', () => {
|
|
||||||
if (stream.listenerCount('close')) stream.emit('close');
|
|
||||||
else stream.end();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
this.connector.socket.once('error', destroyConnection(stream, reject));
|
|
||||||
});
|
|
||||||
|
|
||||||
this.restByteCount = 0;
|
|
||||||
|
|
||||||
return this.reply(150).then(() => this.connector.socket.resume())
|
|
||||||
.then(() => Promise.join(streamPromise, socketPromise))
|
|
||||||
.tap(() => this.emit('STOR', null, fileName))
|
|
||||||
.finally(() => stream.destroy && stream.destroy());
|
|
||||||
})
|
|
||||||
.then(() => this.reply(226, fileName))
|
|
||||||
.catch(Promise.TimeoutError, err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(425, 'No connection established');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
this.emit('STOR', err);
|
|
||||||
return this.reply(550, err.message);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.connector.end();
|
|
||||||
this.commandSocket.resume();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <path>',
|
|
||||||
description: 'Store data as a file at the server site'
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const {handler: stor} = require('./stor');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
directive: 'STOU',
|
|
||||||
handler: function (args) {
|
|
||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
|
||||||
if (!this.fs.get || !this.fs.getUniqueName) return this.reply(402, 'Not supported by file system');
|
|
||||||
|
|
||||||
const fileName = args.command.arg;
|
|
||||||
return Promise.try(() => {
|
|
||||||
return Promise.resolve(this.fs.get(fileName))
|
|
||||||
.then(() => Promise.resolve(this.fs.getUniqueName()))
|
|
||||||
.catch(() => Promise.resolve(fileName));
|
|
||||||
})
|
|
||||||
.then(name => {
|
|
||||||
args.command.arg = name;
|
|
||||||
return stor.call(this, args);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}}',
|
|
||||||
description: 'Store file uniquely'
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
directive: 'STRU',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
return this.reply(/^F$/i.test(command.arg) ? 200 : 504);
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <structure>',
|
|
||||||
description: 'Set file transfer structure',
|
|
||||||
flags: {
|
|
||||||
obsolete: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
directive: 'TYPE',
|
|
||||||
handler: function ({command} = {}) {
|
|
||||||
if (/^A[0-9]?$/i.test(command.arg)) {
|
|
||||||
this.transferType = 'ascii';
|
|
||||||
} else if (/^L[0-9]?$/i.test(command.arg) || /^I$/i.test(command.arg)) {
|
|
||||||
this.transferType = 'binary';
|
|
||||||
} else {
|
|
||||||
return this.reply(501);
|
|
||||||
}
|
|
||||||
return this.reply(200, `Switch to "${this.transferType}" transfer mode.`);
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <mode>',
|
|
||||||
description: 'Set the transfer mode, binary (I) or ascii (A)',
|
|
||||||
flags: {
|
|
||||||
feat: 'TYPE A,I,L'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
directive: 'USER',
|
|
||||||
handler: function ({log, command} = {}) {
|
|
||||||
if (this.username) return this.reply(530, 'Username already set');
|
|
||||||
if (this.authenticated) return this.reply(230);
|
|
||||||
|
|
||||||
this.username = command.arg;
|
|
||||||
if (!this.username) return this.reply(501, 'Must provide username');
|
|
||||||
|
|
||||||
if (this.server.options.anonymous === true && this.username === 'anonymous' ||
|
|
||||||
this.username === this.server.options.anonymous) {
|
|
||||||
return this.login(this.username, '@anonymous')
|
|
||||||
.then(() => {
|
|
||||||
return this.reply(230);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
log.error(err);
|
|
||||||
return this.reply(530, err.message || 'Authentication failed');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.reply(331);
|
|
||||||
},
|
|
||||||
syntax: '{{cmd}} <username>',
|
|
||||||
description: 'Authentication username',
|
|
||||||
flags: {
|
|
||||||
no_auth: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,50 +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/quit'),
|
|
||||||
require('./registration/rest'),
|
|
||||||
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'),
|
|
||||||
require('./registration/pbsz'),
|
|
||||||
require('./registration/prot'),
|
|
||||||
require('./registration/eprt'),
|
|
||||||
require('./registration/epsv')
|
|
||||||
];
|
|
||||||
|
|
||||||
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,143 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const uuid = require('uuid');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const EventEmitter = require('events');
|
|
||||||
|
|
||||||
const BaseConnector = require('./connector/base');
|
|
||||||
const FileSystem = require('./fs');
|
|
||||||
const Commands = require('./commands');
|
|
||||||
const errors = require('./errors');
|
|
||||||
const DEFAULT_MESSAGE = require('./messages');
|
|
||||||
|
|
||||||
class FtpConnection extends EventEmitter {
|
|
||||||
constructor(server, options) {
|
|
||||||
super();
|
|
||||||
this.server = server;
|
|
||||||
this.id = uuid.v4();
|
|
||||||
this.log = options.log.child({id: this.id, ip: this.ip});
|
|
||||||
this.commands = new Commands(this);
|
|
||||||
this.transferType = 'binary';
|
|
||||||
this.encoding = 'utf8';
|
|
||||||
this.bufferSize = false;
|
|
||||||
this._restByteCount = 0;
|
|
||||||
this._secure = false;
|
|
||||||
|
|
||||||
this.connector = new BaseConnector(this);
|
|
||||||
|
|
||||||
this.commandSocket = options.socket;
|
|
||||||
this.commandSocket.on('error', err => {
|
|
||||||
this.log.error(err, 'Client error');
|
|
||||||
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
|
|
||||||
});
|
|
||||||
this.commandSocket.on('data', this._handleData.bind(this));
|
|
||||||
this.commandSocket.on('timeout', () => {});
|
|
||||||
this.commandSocket.on('close', () => {
|
|
||||||
if (this.connector) this.connector.end();
|
|
||||||
if (this.commandSocket && !this.commandSocket.destroyed) this.commandSocket.destroy();
|
|
||||||
this.removeAllListeners();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleData(data) {
|
|
||||||
const messages = _.compact(data.toString(this.encoding).split('\r\n'));
|
|
||||||
this.log.trace(messages);
|
|
||||||
return Promise.mapSeries(messages, message => this.commands.handle(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
get ip() {
|
|
||||||
try {
|
|
||||||
return this.commandSocket ? this.commandSocket.remoteAddress : undefined;
|
|
||||||
} catch (ex) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get restByteCount() {
|
|
||||||
return this._restByteCount > 0 ? this._restByteCount : undefined;
|
|
||||||
}
|
|
||||||
set restByteCount(rbc) {
|
|
||||||
this._restByteCount = rbc;
|
|
||||||
}
|
|
||||||
|
|
||||||
get secure() {
|
|
||||||
return this.server.isTLS || this._secure;
|
|
||||||
}
|
|
||||||
set secure(sec) {
|
|
||||||
this._secure = sec;
|
|
||||||
}
|
|
||||||
|
|
||||||
close(code = 421, message = 'Closing connection') {
|
|
||||||
return Promise.resolve(code)
|
|
||||||
.then(_code => _code && this.reply(_code, message))
|
|
||||||
.then(() => this.commandSocket && this.commandSocket.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
login(username, password) {
|
|
||||||
return Promise.try(() => {
|
|
||||||
const loginListeners = this.server.listeners('login');
|
|
||||||
if (!loginListeners || !loginListeners.length) {
|
|
||||||
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
|
|
||||||
} else {
|
|
||||||
return this.server.emitPromise('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 Promise.map(letters, (promise, index) => {
|
|
||||||
return Promise.resolve(promise)
|
|
||||||
.then(letter => {
|
|
||||||
if (!letter) letter = {};
|
|
||||||
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
|
|
||||||
|
|
||||||
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 Promise.resolve(letter.message) // allow passing in a promise as a message
|
|
||||||
.then(message => {
|
|
||||||
const seperator = !options.hasOwnProperty('eol') ?
|
|
||||||
letters.length - 1 === index ? ' ' : '-' :
|
|
||||||
options.eol ? ' ' : '-';
|
|
||||||
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
|
|
||||||
letter.message = message;
|
|
||||||
return letter;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const processLetter = letter => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (letter.socket && letter.socket.writable) {
|
|
||||||
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
|
|
||||||
letter.socket.write(letter.message + '\r\n', letter.encoding, err => {
|
|
||||||
if (err) {
|
|
||||||
this.log.error(err);
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
} else reject(new errors.SocketError('Socket not writable'));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return satisfyParameters()
|
|
||||||
.then(satisfiedLetters => Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
|
||||||
return processLetter(letter, index);
|
|
||||||
}))
|
|
||||||
.catch(err => {
|
|
||||||
this.log.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = FtpConnection;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const {Socket} = require('net');
|
|
||||||
const tls = require('tls');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const Connector = require('./base');
|
|
||||||
|
|
||||||
class Active extends Connector {
|
|
||||||
constructor(connection) {
|
|
||||||
super(connection);
|
|
||||||
this.type = 'active';
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
|
||||||
const checkSocket = () => {
|
|
||||||
if (this.dataSocket && this.dataSocket.connected) {
|
|
||||||
return Promise.resolve(this.dataSocket);
|
|
||||||
}
|
|
||||||
return Promise.resolve().delay(delay)
|
|
||||||
.then(() => checkSocket());
|
|
||||||
};
|
|
||||||
|
|
||||||
return checkSocket().timeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupConnection(host, port, family = 4) {
|
|
||||||
const closeExistingServer = () => Promise.resolve(
|
|
||||||
this.dataSocket ? this.dataSocket.destroy() : undefined);
|
|
||||||
|
|
||||||
return closeExistingServer()
|
|
||||||
.then(() => {
|
|
||||||
this.dataSocket = new Socket();
|
|
||||||
this.dataSocket.setEncoding(this.connection.transferType);
|
|
||||||
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
|
||||||
this.dataSocket.connect({host, port, family}, () => {
|
|
||||||
this.dataSocket.pause();
|
|
||||||
|
|
||||||
if (this.connection.secure) {
|
|
||||||
const secureContext = tls.createSecureContext(this.server._tls);
|
|
||||||
const secureSocket = new tls.TLSSocket(this.dataSocket, {
|
|
||||||
isServer: true,
|
|
||||||
secureContext
|
|
||||||
});
|
|
||||||
this.dataSocket = secureSocket;
|
|
||||||
}
|
|
||||||
this.dataSocket.connected = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = Active;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
const Promise = require('bluebird');
|
|
||||||
const errors = require('../errors');
|
|
||||||
|
|
||||||
class Connector {
|
|
||||||
constructor(connection) {
|
|
||||||
this.connection = connection;
|
|
||||||
|
|
||||||
this.dataSocket = null;
|
|
||||||
this.dataServer = null;
|
|
||||||
this.type = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get log() {
|
|
||||||
return this.connection.log;
|
|
||||||
}
|
|
||||||
|
|
||||||
get socket() {
|
|
||||||
return this.dataSocket;
|
|
||||||
}
|
|
||||||
|
|
||||||
get server() {
|
|
||||||
return this.connection.server;
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForConnection() {
|
|
||||||
return Promise.reject(new errors.ConnectorError('No connector setup, send PASV or PORT'));
|
|
||||||
}
|
|
||||||
|
|
||||||
end() {
|
|
||||||
const closeDataSocket = new Promise(resolve => {
|
|
||||||
if (this.dataSocket) this.dataSocket.end();
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
const closeDataServer = new Promise(resolve => {
|
|
||||||
if (this.dataServer) this.dataServer.close(() => resolve());
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all([closeDataSocket, closeDataServer])
|
|
||||||
.then(() => {
|
|
||||||
this.dataSocket = null;
|
|
||||||
this.dataServer = null;
|
|
||||||
this.type = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = Connector;
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
const net = require('net');
|
|
||||||
const tls = require('tls');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
|
|
||||||
const Connector = require('./base');
|
|
||||||
const findPort = require('../helpers/find-port');
|
|
||||||
const errors = require('../errors');
|
|
||||||
|
|
||||||
class Passive extends Connector {
|
|
||||||
constructor(connection) {
|
|
||||||
super(connection);
|
|
||||||
this.type = 'passive';
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForConnection({timeout = 5000, delay = 250} = {}) {
|
|
||||||
if (!this.dataServer) return Promise.reject(new errors.ConnectorError('Passive server not setup'));
|
|
||||||
|
|
||||||
const checkSocket = () => {
|
|
||||||
if (this.dataServer && this.dataServer.listening && this.dataSocket && this.dataSocket.connected) {
|
|
||||||
return Promise.resolve(this.dataSocket);
|
|
||||||
}
|
|
||||||
return Promise.resolve().delay(delay)
|
|
||||||
.then(() => checkSocket());
|
|
||||||
};
|
|
||||||
|
|
||||||
return checkSocket().timeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupServer() {
|
|
||||||
const closeExistingServer = () => this.dataServer ?
|
|
||||||
new Promise(resolve => this.dataServer.close(() => resolve())) :
|
|
||||||
Promise.resolve();
|
|
||||||
|
|
||||||
return closeExistingServer()
|
|
||||||
.then(() => this.getPort())
|
|
||||||
.then(port => {
|
|
||||||
const connectionHandler = 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.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
|
|
||||||
|
|
||||||
if (this.connection.secure) {
|
|
||||||
const secureContext = tls.createSecureContext(this.server._tls);
|
|
||||||
const secureSocket = new tls.TLSSocket(socket, {
|
|
||||||
isServer: true,
|
|
||||||
secureContext
|
|
||||||
});
|
|
||||||
this.dataSocket = secureSocket;
|
|
||||||
} else {
|
|
||||||
this.dataSocket = socket;
|
|
||||||
}
|
|
||||||
this.dataSocket.connected = true;
|
|
||||||
this.dataSocket.setEncoding(this.connection.transferType);
|
|
||||||
this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
|
||||||
this.dataSocket.on('close', () => {
|
|
||||||
this.log.trace('Passive connection closed');
|
|
||||||
this.end();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.dataSocket = null;
|
|
||||||
this.dataServer = net.createServer({pauseOnConnect: true}, connectionHandler);
|
|
||||||
this.dataServer.maxConnections = 1;
|
|
||||||
this.dataServer.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataServer', error: err}));
|
|
||||||
this.dataServer.on('close', () => {
|
|
||||||
this.log.trace('Passive server closed');
|
|
||||||
this.dataServer = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.dataServer.listen(port, err => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else {
|
|
||||||
this.log.debug({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);
|
|
||||||
}
|
|
||||||
throw new errors.ConnectorError('Invalid pasv_range');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
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
|
|
||||||
};
|
|
||||||
116
src/fs.js
116
src/fs.js
@@ -1,116 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const nodePath = require('path');
|
|
||||||
const uuid = require('uuid');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const fs = Promise.promisifyAll(require('fs'));
|
|
||||||
const errors = require('./errors');
|
|
||||||
|
|
||||||
class FileSystem {
|
|
||||||
constructor(connection, {root, cwd} = {}) {
|
|
||||||
this.connection = connection;
|
|
||||||
this.cwd = cwd || nodePath.sep;
|
|
||||||
this.root = root || process.cwd();
|
|
||||||
}
|
|
||||||
|
|
||||||
_resolvePath(path = '') {
|
|
||||||
const isFromRoot = _.startsWith(path, '/') || _.startsWith(path, nodePath.sep);
|
|
||||||
const cwd = isFromRoot ? nodePath.sep : this.cwd || nodePath.sep;
|
|
||||||
const serverPath = nodePath.join(nodePath.sep, cwd, path);
|
|
||||||
const fsPath = nodePath.join(this.root, serverPath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
serverPath,
|
|
||||||
fsPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDirectory() {
|
|
||||||
return this.cwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(fileName) {
|
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
|
||||||
return fs.statAsync(fsPath)
|
|
||||||
.then(stat => _.set(stat, 'name', fileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
list(path = '.') {
|
|
||||||
const {fsPath} = this._resolvePath(path);
|
|
||||||
return fs.readdirAsync(fsPath)
|
|
||||||
.then(fileNames => {
|
|
||||||
return Promise.map(fileNames, fileName => {
|
|
||||||
const filePath = nodePath.join(fsPath, fileName);
|
|
||||||
return fs.accessAsync(filePath, fs.constants.F_OK)
|
|
||||||
.then(() => {
|
|
||||||
return fs.statAsync(filePath)
|
|
||||||
.then(stat => _.set(stat, 'name', fileName));
|
|
||||||
})
|
|
||||||
.catch(() => null);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(_.compact);
|
|
||||||
}
|
|
||||||
|
|
||||||
chdir(path = '.') {
|
|
||||||
const {fsPath, serverPath} = this._resolvePath(path);
|
|
||||||
return fs.statAsync(fsPath)
|
|
||||||
.tap(stat => {
|
|
||||||
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.cwd = serverPath;
|
|
||||||
return this.currentDirectory();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
write(fileName, {append = false, start = undefined} = {}) {
|
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
|
||||||
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
|
|
||||||
stream.once('error', () => fs.unlinkAsync(fsPath));
|
|
||||||
stream.once('close', () => stream.end());
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
read(fileName, {start = undefined} = {}) {
|
|
||||||
const {fsPath} = this._resolvePath(fileName);
|
|
||||||
return fs.statAsync(fsPath)
|
|
||||||
.tap(stat => {
|
|
||||||
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
|
|
||||||
return stream;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(path) {
|
|
||||||
const {fsPath} = this._resolvePath(path);
|
|
||||||
return fs.statAsync(fsPath)
|
|
||||||
.then(stat => {
|
|
||||||
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
|
|
||||||
else return fs.unlinkAsync(fsPath);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir(path) {
|
|
||||||
const {fsPath} = this._resolvePath(path);
|
|
||||||
return fs.mkdirAsync(fsPath)
|
|
||||||
.then(() => fsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
rename(from, to) {
|
|
||||||
const {fsPath: fromPath} = this._resolvePath(from);
|
|
||||||
const {fsPath: toPath} = this._resolvePath(to);
|
|
||||||
return fs.renameAsync(fromPath, toPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
chmod(path, mode) {
|
|
||||||
const {fsPath} = this._resolvePath(path);
|
|
||||||
return fs.chmodAsync(fsPath, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUniqueName() {
|
|
||||||
return uuid.v4().replace(/\W/g, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = FileSystem;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
module.exports = function (path) {
|
|
||||||
return path
|
|
||||||
.replace(/"/g, '""');
|
|
||||||
};
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const moment = require('moment');
|
|
||||||
const errors = require('../errors');
|
|
||||||
|
|
||||||
const FORMATS = {
|
|
||||||
ls,
|
|
||||||
ep
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = function (fileStat, format = 'ls') {
|
|
||||||
if (typeof format === 'function') return format(fileStat);
|
|
||||||
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 timeDiff = now.diff(mtime, 'months');
|
|
||||||
const dateFormat = timeDiff < 6 ? 'MMM DD HH:mm' : 'MMM DD YYYY';
|
|
||||||
|
|
||||||
return [
|
|
||||||
fileStat.mode ? [
|
|
||||||
fileStat.isDirectory() ? 'd' : '-',
|
|
||||||
fileStat.mode & 256 ? 'r' : '-',
|
|
||||||
fileStat.mode & 128 ? 'w' : '-',
|
|
||||||
fileStat.mode & 64 ? 'x' : '-',
|
|
||||||
fileStat.mode & 32 ? 'r' : '-',
|
|
||||||
fileStat.mode & 16 ? 'w' : '-',
|
|
||||||
fileStat.mode & 8 ? 'x' : '-',
|
|
||||||
fileStat.mode & 4 ? 'r' : '-',
|
|
||||||
fileStat.mode & 2 ? 'w' : '-',
|
|
||||||
fileStat.mode & 1 ? 'x' : '-'
|
|
||||||
].join('') : fileStat.isDirectory() ? 'drwxr-xr-x' : '-rwxr-xr-x',
|
|
||||||
'1',
|
|
||||||
fileStat.uid || 1,
|
|
||||||
fileStat.gid || 1,
|
|
||||||
_.padStart(fileStat.size, 12),
|
|
||||||
_.padStart(mtime.format(dateFormat), 12),
|
|
||||||
fileStat.name
|
|
||||||
].join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function ep(fileStat) {
|
|
||||||
const facts = _.compact([
|
|
||||||
fileStat.dev && fileStat.ino ? `i${fileStat.dev.toString(16)}.${fileStat.ino.toString(16)}` : null,
|
|
||||||
fileStat.size ? `s${fileStat.size}` : null,
|
|
||||||
fileStat.mtime ? `m${moment.utc(new Date(fileStat.mtime)).format('X')}` : null,
|
|
||||||
fileStat.mode ? `up${(fileStat.mode & 4095).toString(8)}` : null,
|
|
||||||
fileStat.isDirectory() ? '/' : 'r'
|
|
||||||
]).join(',');
|
|
||||||
return `+${facts}\t${fileStat.name}`;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const net = require('net');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const errors = require('../errors');
|
|
||||||
|
|
||||||
module.exports = function (min = 1, max = undefined) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let checkPort = min;
|
|
||||||
let portCheckServer = net.createServer();
|
|
||||||
portCheckServer.maxConnections = 0;
|
|
||||||
portCheckServer.on('error', () => {
|
|
||||||
if (checkPort < 65535 && (!max || checkPort < max)) {
|
|
||||||
checkPort = checkPort + 1;
|
|
||||||
portCheckServer.listen(checkPort);
|
|
||||||
} else {
|
|
||||||
reject(new errors.GeneralError('Unable to find open port', 500));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
portCheckServer.on('listening', () => {
|
|
||||||
const {port} = portCheckServer.address();
|
|
||||||
portCheckServer.close(() => {
|
|
||||||
portCheckServer = null;
|
|
||||||
resolve(port);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
portCheckServer.listen(checkPort);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const http = require('http');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const errors = require('../errors');
|
|
||||||
|
|
||||||
const IP_WEBSITE = 'http://api.ipify.org/';
|
|
||||||
|
|
||||||
module.exports = function (hostname) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!hostname || hostname === '0.0.0.0') {
|
|
||||||
let ip = '';
|
|
||||||
http.get(IP_WEBSITE, response => {
|
|
||||||
if (response.statusCode !== 200) {
|
|
||||||
return reject(new errors.GeneralError('Unable to resolve hostname', response.statusCode));
|
|
||||||
}
|
|
||||||
response.setEncoding('utf8');
|
|
||||||
response.on('data', chunk => {
|
|
||||||
ip += chunk;
|
|
||||||
});
|
|
||||||
response.on('end', () => {
|
|
||||||
resolve(ip);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else resolve(hostname);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
147
src/index.js
147
src/index.js
@@ -1,147 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const Promise = require('bluebird');
|
|
||||||
const nodeUrl = require('url');
|
|
||||||
const buyan = require('bunyan');
|
|
||||||
const net = require('net');
|
|
||||||
const tls = require('tls');
|
|
||||||
const fs = require('fs');
|
|
||||||
const EventEmitter = require('events');
|
|
||||||
|
|
||||||
const Connection = require('./connection');
|
|
||||||
const resolveHost = require('./helpers/resolve-host');
|
|
||||||
|
|
||||||
class FtpServer extends EventEmitter {
|
|
||||||
constructor(url, options = {}) {
|
|
||||||
super();
|
|
||||||
this.options = _.merge({
|
|
||||||
log: buyan.createLogger({name: 'ftp-srv'}),
|
|
||||||
anonymous: false,
|
|
||||||
pasv_range: 22,
|
|
||||||
file_format: 'ls',
|
|
||||||
blacklist: [],
|
|
||||||
whitelist: [],
|
|
||||||
greeting: null,
|
|
||||||
tls: false
|
|
||||||
}, options);
|
|
||||||
this._greeting = this.setupGreeting(this.options.greeting);
|
|
||||||
this._features = this.setupFeaturesMessage();
|
|
||||||
this._tls = this.setupTLS(this.options.tls);
|
|
||||||
|
|
||||||
delete this.options.greeting;
|
|
||||||
delete this.options.tls;
|
|
||||||
|
|
||||||
this.connections = {};
|
|
||||||
this.log = this.options.log;
|
|
||||||
this.url = nodeUrl.parse(url || 'ftp://127.0.0.1:21');
|
|
||||||
|
|
||||||
const serverConnectionHandler = socket => {
|
|
||||||
let connection = new Connection(this, {log: this.log, socket});
|
|
||||||
this.connections[connection.id] = connection;
|
|
||||||
|
|
||||||
socket.on('close', () => this.disconnectClient(connection.id));
|
|
||||||
|
|
||||||
const greeting = this._greeting || [];
|
|
||||||
const features = this._features || 'Ready';
|
|
||||||
return connection.reply(220, ...greeting, features)
|
|
||||||
.finally(() => socket.resume());
|
|
||||||
};
|
|
||||||
const serverOptions = _.assign(this.isTLS ? this._tls : {}, {pauseOnConnect: true});
|
|
||||||
|
|
||||||
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
|
|
||||||
this.server.on('error', err => this.log.error(err, '[Event] error'));
|
|
||||||
|
|
||||||
process.on('SIGTERM', () => this.quit());
|
|
||||||
process.on('SIGINT', () => this.quit());
|
|
||||||
process.on('SIGQUIT', () => this.quit());
|
|
||||||
}
|
|
||||||
|
|
||||||
get isTLS() {
|
|
||||||
return this.url.protocol === 'ftps:' && this._tls;
|
|
||||||
}
|
|
||||||
|
|
||||||
listen() {
|
|
||||||
return resolveHost(this.url.hostname)
|
|
||||||
.then(hostname => {
|
|
||||||
this.url.hostname = hostname;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.server.listen(this.url.port, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
this.log.info({
|
|
||||||
protocol: this.url.protocol.replace(/\W/g, ''),
|
|
||||||
ip: this.url.hostname,
|
|
||||||
port: this.url.port
|
|
||||||
}, 'Listening');
|
|
||||||
resolve('Listening');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
emitPromise(action, ...data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const params = _.concat(data, [resolve, reject]);
|
|
||||||
this.emit.call(this, action, ...params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupTLS(_tls) {
|
|
||||||
if (!_tls) return false;
|
|
||||||
return _.assign({}, _tls, {
|
|
||||||
cert: _tls.cert ? fs.readFileSync(_tls.cert) : undefined,
|
|
||||||
key: _tls.key ? fs.readFileSync(_tls.key) : undefined,
|
|
||||||
ca: _tls.ca ? Array.isArray(_tls.ca) ? _tls.ca.map(_ca => fs.readFileSync(_ca)) : [fs.readFileSync(_tls.ca)] : undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupGreeting(greet) {
|
|
||||||
if (!greet) return [];
|
|
||||||
const greeting = Array.isArray(greet) ? greet : greet.split('\n');
|
|
||||||
return greeting;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupFeaturesMessage() {
|
|
||||||
let features = [];
|
|
||||||
if (this.options.anonymous) features.push('a');
|
|
||||||
|
|
||||||
if (features.length) {
|
|
||||||
features.unshift('Features:');
|
|
||||||
features.push('.');
|
|
||||||
}
|
|
||||||
return features.length ? features.join(' ') : 'Ready';
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectClient(id) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const client = this.connections[id];
|
|
||||||
if (!client) return resolve();
|
|
||||||
delete this.connections[id];
|
|
||||||
try {
|
|
||||||
client.close(0);
|
|
||||||
} catch (err) {
|
|
||||||
this.log.error(err, 'Error closing connection', {id});
|
|
||||||
} finally {
|
|
||||||
resolve('Disconnected');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
quit() {
|
|
||||||
return this.close()
|
|
||||||
.finally(() => process.exit(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.log.info('Server closing...');
|
|
||||||
this.server.maxConnections = 0;
|
|
||||||
return Promise.map(Object.keys(this.connections), id => Promise.try(this.disconnectClient.bind(this, id)))
|
|
||||||
.then(() => new Promise(resolve => {
|
|
||||||
this.server.close(err => {
|
|
||||||
if (err) this.log.error(err, 'Error closing server');
|
|
||||||
resolve('Closed');
|
|
||||||
});
|
|
||||||
}))
|
|
||||||
.then(() => this.removeAllListeners());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
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;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user