Compare commits

..

64 Commits

Author SHA1 Message Date
Amr Ibrahim
a8cdcb0eb3 fix: replace .finally with .then (#322) 2022-06-24 10:55:10 -06:00
simultsop
4ba583420a refactor: reject with Error object (#319)
* refactor: reject with Error object

* fix: use error constructor

Co-authored-by: Matt Forster <hey@mattforster.ca>
2022-06-24 10:47:18 -06:00
bartbutenaers
51049f631a New event emitters (connect, server-error, closing, closed) (#314) 2022-06-24 09:03:51 -04:00
Matt Forster
50e8c455d6 docs(readme): update prime login example (#326) 2022-06-23 16:09:38 -04:00
Matt Forster
1af62a7c4f fix: update engine (#298)
This will prevent users of node 10  and below from installing and using >= v4.6
2022-05-06 10:18:08 -06:00
botovance
0b65a22296 chore(codeowners): update CODEOWNERS 2022-04-27 19:25:08 -06:00
Botovance
f59857e34a chore(codeowners): update CODEOWNERS 2022-04-19 14:07:30 -06:00
Amr Ibrahim
1ad45fc757 fix pasv_url resolverFunction docs (#297) 2022-04-18 15:50:49 -06:00
Matt Forster
bc8abb14da feat: npm updates, security zero, test corrections (#296) 2022-04-18 15:50:14 -06:00
dependabot[bot]
0b55b3f79d chore(deps): bump ssri from 6.0.1 to 6.0.2 (#293)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 14:44:51 -06:00
dependabot[bot]
4f4a6c25a5 chore(deps): bump moment from 2.22.2 to 2.29.2 (#288)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 14:44:41 -06:00
dependabot[bot]
45a4bf15bf chore(deps): bump trim-off-newlines from 1.0.1 to 1.0.3 (#289)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 14:44:32 -06:00
dependabot[bot]
a1be4416a7 chore(deps): bump node-fetch from 2.6.1 to 2.6.7 (#290)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 14:44:08 -06:00
Bas Broerse
32a0750e2c feat: allow mkd command to create directories recursively (#294)
Co-authored-by: Bas Broerse <b.broerse@sector-orange.com>
2022-04-14 14:43:33 -06:00
Matt Forster
4eb17015f1 chore: update CI context (#295) 2022-04-14 14:40:36 -06:00
dependabot[bot]
d2566e7745 chore(deps): bump npm-user-validate from 1.0.0 to 1.0.1 (#292)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 19:50:41 +00:00
dependabot[bot]
f8cd1e8f64 chore(deps): bump tar from 4.4.13 to 4.4.19 (#291)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 13:48:56 -06:00
dependabot[bot]
e0e676e7e9 chore(deps): bump pathval from 1.1.0 to 1.1.1 (#285)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 13:46:14 -06:00
dependabot[bot]
a7775a46ae chore(deps): bump minimist from 1.2.5 to 1.2.6 (#287)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 13:44:16 -06:00
Chen WeiJian
f9c81b162a Provide more friendly example code. (#282)
* Update README.md

* Update README.md
2021-12-06 09:32:59 -07:00
dependabot[bot]
e1f1aa09cd chore(deps): bump path-parse from 1.0.6 to 1.0.7 (#274)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-04 14:50:26 -06:00
Victor
b0174bb24e chore: make fileName available in getUniqueName (#273) 2021-11-04 14:49:50 -06:00
Matt Forster
5852851ded Update README.md 2021-08-09 13:51:31 -06:00
Matt Forster
1c5db00a5e chore: update readme 2021-08-09 13:50:38 -06:00
Victor
02227d653e feat: allow dynamic pasv_url depending on remote address (#269)
Co-authored-by: Matt Forster <hey@mattforster.ca>
2021-08-09 13:49:34 -06:00
dependabot[bot]
bf44cbba58 chore(deps): bump glob-parent from 5.1.1 to 5.1.2 (#262)
Bumps [glob-parent](https://github.com/gulpjs/glob-parent) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/gulpjs/glob-parent/releases)
- [Changelog](https://github.com/gulpjs/glob-parent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gulpjs/glob-parent/compare/v5.1.1...v5.1.2)

---
updated-dependencies:
- dependency-name: glob-parent
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 09:07:50 -06:00
dependabot[bot]
80ff71655e chore(deps): bump trim-newlines from 3.0.0 to 3.0.1 (#263)
Bumps [trim-newlines](https://github.com/sindresorhus/trim-newlines) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sindresorhus/trim-newlines/releases)
- [Commits](https://github.com/sindresorhus/trim-newlines/commits)

---
updated-dependencies:
- dependency-name: trim-newlines
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 09:07:39 -06:00
dependabot[bot]
eb8e3d837f chore(deps): bump normalize-url from 5.3.0 to 5.3.1 (#265)
Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/sindresorhus/normalize-url/releases)
- [Commits](https://github.com/sindresorhus/normalize-url/commits)

---
updated-dependencies:
- dependency-name: normalize-url
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 09:07:25 -06:00
dependabot[bot]
dc37c9c435 chore(deps): bump hosted-git-info from 2.7.1 to 2.8.9 (#256)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.7.1 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.7.1...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 09:06:58 -06:00
dependabot[bot]
6a94a20f64 chore(deps): bump lodash from 4.17.19 to 4.17.21 (#255)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 10:47:57 -06:00
dependabot[bot]
02355eda28 chore(deps): bump handlebars from 4.7.6 to 4.7.7 (#254)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-06 10:43:03 -06:00
Andres Watson
8b32be7acc Update README.md (#253)
* Update README.md

Based on https://github.com/autovance/ftp-srv/discussions/250 I propose to explain that is not a HOST, must be an IP Address.

* Update README.md

* Update README.md

Co-authored-by: Matt Forster <hey@mattforster.ca>

Co-authored-by: Matt Forster <hey@mattforster.ca>
2021-05-05 09:44:58 -06:00
dependabot[bot]
5daaa9883c chore(deps): bump y18n from 4.0.0 to 4.0.1 (#247)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-29 10:28:31 -06:00
Connor Skees
02798e094f chore: small readme typo (#242) 2021-01-18 08:51:57 -07:00
Tyler Stewart
720c93d088 fix(feat): lazy require registry (#238)
* chore: give mocha explicit test paths

Mocha doesn't seem to find nested paths with **

* fix(feat): lazy require registry

If requiring the registry from the module scope the result is an empty object. I assume this is caused by the circular nature of this dep
2021-01-04 16:51:02 -07:00
Ricardas Jonaitis
bffcc35299 fix: dns lookup fail with IP with zeros padding (#237) 2021-01-04 08:34:40 -07:00
Matt Forster
78de22f518 chore: fix semantic release dry run 2020-12-16 15:36:11 -07:00
Matt Forster
2b140ecb0d chore: condition-circle using 'branch' 2020-12-16 15:27:40 -07:00
Matt Forster
57ddfb5e08 chore: set master as release branch 2020-12-16 15:22:56 -07:00
Matt Forster
beef19af30 chore: set release branch 2020-12-16 15:15:55 -07:00
Matt Forster
e7c5f83311 chore: setup release branch 2020-12-16 15:07:06 -07:00
Matt Forster
1ab793c04e chore: add install to release steps
requires some dev deps
2020-12-16 15:00:53 -07:00
Matt Forster
24f7126acf chore: improve ci flows (#227)
current release flow doesn't work
improve supported version testing
2020-12-16 14:58:20 -07:00
Matt Forster
457b859450 fix(fs): check resolved path against root (#224)
* fix(fs): check resolved path against root

This should prevent paths from being resolved above the root.

Should affect all commands that utilize the FS functions.

Fixes #167

* test: use __dirname for relative certs

* fix: improve path resolution

* chore: remove unused package

* fix: normalize resolve path if absolute

Otherwise join will normalize

Co-authored-by: Tyler Stewart <tyler@autovance.com>
2020-12-16 10:19:28 -07:00
Matt Forster
722da60a82 chore: set deploy context 2020-12-15 12:04:35 -07:00
dependabot[bot]
9f95d60916 chore(deps): bump ini from 1.3.5 to 1.3.7 (#223)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-10 10:46:24 -07:00
techmunk
4cd88b129c fix: ensure commandSocket is set before retrieving ip address of connection (#222) 2020-12-08 11:05:50 -07:00
dependabot[bot]
db49063b0d chore(deps-dev): bump semantic-release from 15.14.0 to 17.2.3 (#220)
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 15.14.0 to 17.2.3.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v15.14.0...v17.2.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-11-25 09:03:08 -07:00
dependabot[bot]
b55557292e chore(deps): bump node-fetch from 2.6.0 to 2.6.1 (#218)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-11 08:28:39 -06:00
Devonte
c87ce2fef6 fix: log errors, improved messages (#156)
Spelling errors, improved error logs.
2020-09-08 08:38:14 -06:00
Yaser
05a68cfb08 feat: pipe stream on fs write (#206)
* pipe allloewd on write stream

* code readable

* fix: 'if' statment missed curly braces

* Update src/commands/registration/stor.js

Replacing write data with pipe on STOR command

Co-authored-by: Tyler Stewart <hello@tylerstewart.ca>

* fix: socket on data replaced with socket on pipe

* fix: extra space

Co-authored-by: Tyler Stewart <hello@tylerstewart.ca>
Co-authored-by: Tyler Stewart <tyler@autovance.com>
2020-08-25 15:44:37 -06:00
Matt Forster
31290fc964 chore: security.md (#215)
* chore: security.md

* Update SECURITY.md
2020-08-18 11:44:53 -06:00
Matt Forster
a598fab03c fix: packages updates - npm advisories (#214)
Correct NPM advisories by updating package-lock.json and package
dependencies.

Major upgrade for yargs, but did not affect our code directly.
2020-08-17 15:44:23 -06:00
Matt Forster
87e8ac6ca8 chore: fix maintenance version regex 2020-08-17 13:43:27 -06:00
Matt Forster
75e34988f4 chore: add release support for backport branches (#213) 2020-08-17 13:37:49 -06:00
Tyler Stewart
e449e75219 fix: disallow PORT connections to alternate hosts
Ensure the data socket that the server connects to from the PORT command is the same IP as the current command socket.

* fix: add error handling to additional connection commands
2020-08-17 13:15:06 -06:00
Tyler Stewart
296573ae01 fix: cli arg consistency 2020-08-13 08:38:01 -06:00
Tyler Stewart
be579f65d0 docs: clarify passive options and behaviour 2020-08-13 08:38:01 -06:00
dependabot[bot]
c440050945 chore(deps): bump npm from 6.13.4 to 6.14.6 (#203)
Bumps [npm](https://github.com/npm/cli) from 6.13.4 to 6.14.6.
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v6.13.4...v6.14.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tyler Stewart <tyler@autovance.com>
2020-07-21 10:05:34 -06:00
Tyler Stewart
ca576acf2e fix: correctly destroy socket on close (#208)
* chore: update owner references

* fix: correctly destroy socket on close

This fixes an issue where the client would never close, hanging the server and preventing shutdown

* fix: only set timeout if greater than 0

* fix: move dependency to top

* fix: notify of command that caused error

* fix: emit disconnect event on client close
2020-07-20 16:30:04 -06:00
dependabot[bot]
649d582f30 chore(deps): bump lodash from 4.17.15 to 4.17.19 (#205)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-17 13:59:28 -06:00
dependabot[bot]
e26caad783 chore(deps): bump https-proxy-agent from 2.2.2 to 2.2.4 (#198) 2020-05-01 09:04:06 -06:00
dependabot[bot]
2470db6482 chore(deps): bump handlebars from 4.1.2 to 4.5.3 (#186) 2020-05-01 09:01:50 -06:00
dependabot[bot]
d78fae0ce6 chore(deps): bump acorn from 6.1.0 to 6.4.1 (#195) 2020-05-01 08:59:54 -06:00
160 changed files with 18610 additions and 12061 deletions

112
.circleci/config.yml Normal file
View File

@@ -0,0 +1,112 @@
version: 2.1
orbs:
node: circleci/node@5.0.2
commands:
setup_git_bot:
description: set up the bot git user to make changes
steps:
- run:
name: "Git: Botovance"
command: |
git config --global user.name "Bot Vance"
git config --global user.email bot@autovance.com
executors:
node-lts:
parameters:
node-version:
type: string
default: lts
docker:
- image: cimg/node:<< parameters.node-version >>
jobs:
lint:
executor: node-lts
steps:
- checkout
- node/install-packages
- run:
name: Lint
command: npm run verify
release_dry_run:
executor: node-lts
steps:
- checkout
- node/install-packages
- setup_git_bot
- deploy:
name: Dry Release
command: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release --dry-run
release:
executor: node-lts
steps:
- checkout
- node/install-packages
- setup_git_bot
- deploy:
name: Release
command: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release
workflows:
version: 2
release_scheduled:
triggers:
# 6:03 UTC (mornings) 1 monday
- schedule:
cron: "3 6 * * 1"
filters:
branches:
only:
- main
jobs:
- lint
- node/test:
matrix:
parameters:
version:
- '12.22'
- '14.19'
- '16.14'
- 'current'
- release:
context: npm-deploy-av
requires:
- node/test
- lint
test:
jobs:
- lint
- node/test:
matrix:
parameters:
version:
- '12.22'
- '14.19'
- '16.14'
- 'current'
- release_dry_run:
filters:
branches:
only: main
requires:
- node/test
- lint
- hold_release:
type: approval
requires:
- release_dry_run
- release:
context: npm-deploy-av
requires:
- hold_release

4
.gitignore vendored
View File

@@ -1,2 +1,6 @@
test_tmp/
node_modules/
dist/
npm-debug.log

21
.vscode/launch.json vendored
View File

@@ -1,21 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\dist\\index.js",
"preLaunchTask": "build",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}

12
.vscode/tasks.json vendored
View File

@@ -1,12 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "npm run build"
}
]
}

5
CODEOWNERS Normal file
View File

@@ -0,0 +1,5 @@
# Lines starting with '#' are comments.
# Each line is a file pattern followed by one or more owners.
# Order is important. The last matching pattern has the most precedence.
* @quorumdms/team-gbt

446
README.md
View File

@@ -1,48 +1,416 @@
```ts
const loginMiddleware = () => (client) => {
let username;
let password;
<p align="center">
<a href="https://github.com/autovance/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
return {
USER(client, command) => {
username = command.arg;
},
PASS(client, command) => {
password = command.arg;
<p align="center">
Modern, extensible FTP Server
</p>
<p align="center">
<a href="https://www.npmjs.com/package/ftp-srv">
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://circleci.com/gh/autovance/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/autovance/ftp-srv/master.svg?style=for-the-badge" />
</a>
</p>
---
## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
## Features
- Extensible [file systems](#file-system) per connection
- 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, create an active ftp server.
const FtpSrv = require('ftp-srv');
const port=21;
const ftpServer = new FtpSrv({
url: "ftp://0.0.0.0:" + port,
anonymous: true
});
ftpServer.on('login', ({ connection, username, password }, resolve, reject) => {
if(username === 'anonymous' && password === 'anonymous'){
return resolve({ root:"/" });
}
};
}
return reject(new errors.GeneralError('Invalid username or password', 401));
});
const fileSystemMiddleware = () => (client) => {
return {
CWD(client, command) => {},
CDUP(client) => {},
}
}
const transferMiddleware = () => (client) => {
}
client.use(loginMiddleware());
ftpServer.listen().then(() => {
console.log('Ftp server is starting...')
});
```
5.1. MINIMUM IMPLEMENTATION
## API
In order to make FTP workable without needless error messages, the
following minimum implementation is required for all servers:
### `new FtpSrv({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
TYPE - ASCII Non-print
MODE - Stream
STRUCTURE - File, Record
COMMANDS - USER, QUIT, PORT,
TYPE, MODE, STRU,
for the default values
RETR, STOR,
NOOP.
_Note:_ The hostname must be the external IP address to accept external connections. `0.0.0.0` will listen on any available hosts for server and passive connections.
__Default:__ `"ftp://127.0.0.1:21"`
The default values for transfer parameters are:
#### `pasv_url`
`FTP-srv` provides an IP address to the client when a `PASV` command is received in the handshake for a passive connection. Reference [PASV verb](https://cr.yp.to/ftp/retr.html#pasv). This can be one of two options:
- A function which takes one parameter containing the remote IP address of the FTP client. This can be useful when the user wants to return a different IP address depending if the user is connecting from Internet or from an LAN address.
Example:
```js
const { networkInterfaces } = require('os');
const { Netmask } = require('netmask');
TYPE - ASCII Non-print
MODE - Stream
STRU - File
const nets = networkInterfaces();
function getNetworks() {
let networks = {};
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
if (net.family === 'IPv4' && !net.internal) {
networks[net.address + "/24"] = net.address
}
}
}
return networks;
}
const resolverFunction = (address) => {
// const networks = {
// '$GATEWAY_IP/32': `${public_ip}`,
// '10.0.0.0/8' : `${lan_ip}`
// }
const networks = getNetworks();
for (const network in networks) {
if (new Netmask(network).contains(address)) {
return networks[network];
}
}
return "127.0.0.1";
}
new FtpSrv({pasv_url: resolverFunction});
```
- A static IP address (ie. an external WAN **IP address** that the FTP server is bound to). In this case, only connections from localhost are handled differently returning `127.0.0.1` to the client.
If not provided, clients can only connect using an `Active` connection.
#### `pasv_min`
The starting port to accept passive connections.
__Default:__ `1024`
#### `pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
#### `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.
#### `timeout`
Sets the timeout (in ms) after that an idle connection is closed by the server
__Default:__ `0`
## CLI
`ftp-srv` also comes with a builtin CLI.
```bash
$ ftp-srv [url] [options]
```
```bash
$ ftp-srv ftp://0.0.0.0:9876 --root ~/Documents
```
#### `url`
Set the listening URL.
Defaults to `ftp://127.0.0.1:21`
#### `--pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`).
If not provided, clients can only connect using an `Active` connection.
#### `--pasv_min`
The starting port to accept passive connections.
__Default:__ `1024`
#### `--pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
#### `--root` / `-r`
Set the default root directory for users.
Defaults to the current directory.
#### `--credentials` / `-c`
Set the path to a json credentials file.
Format:
```js
[
{
"username": "...",
"password": "...",
"root": "..." // Root directory
},
...
]
```
#### `--username`
Set the username for the only user. Do not provide an argument to allow anonymous login.
#### `--password`
Set the password for the given `username`.
#### `--read-only`
Disable write actions such as upload, delete, etc.
## 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`.
### `client-error`
```js
ftpServer.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 occurred
`error` error object
### `disconnect`
```js
ftpServer.on('disconnect', ({connection, id, newConnectionCount}) => { ... });
```
Occurs when a client has disconnected.
`connection` [client class object](src/connection.js)
`id` string of the disconnected connection id
`id` number of the new connection count (exclusive the disconnected client connection)
### `closed`
```js
ftpServer.on('closed', ({}) => { ... });
```
Occurs when the FTP server has been closed.
### `closing`
```js
ftpServer.on('closing', ({}) => { ... });
```
Occurs when the FTP server has started closing.
### `login`
```js
ftpServer.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
### `server-error`
```js
ftpServer.on('server-error', ({error}) => { ... });
```
Occurs when an error arises in the FTP server.
`error` error object
### `RETR`
```js
connection.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
connection.on('STOR', (error, fileName) => { ... });
```
Occurs when a file is uploaded.
`error` if successful, will be `null`
`fileName` name of the file that was uploaded
### `RNTO`
```js
connection.on('RNTO', (error, fileName) => { ... });
```
Occurs when a file is renamed.
`error` if successful, will be `null`
`fileName` name of the file that was renamed
## Supported Commands
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
## 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#L40)
Returns a string of the current working directory
__Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L44)
Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L50)
Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L67)
Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L114)
Returns a path to a newly created directory
__Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L79)
Returns a writable stream
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L90)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L105)
Delete a file or directory
__Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L120)
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L126)
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`getUniqueName(fileName)`](src/fs.js#L131)
Returns a unique file name to write to. Client requested filename available if you want to base your function on it.
__Used in:__ `STOU`
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## 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)

16
SECURITY.md Normal file
View File

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

15
old/bin/index.js → bin/index.js Normal file → Executable file
View File

@@ -37,19 +37,22 @@ function setupYargs() {
boolean: true,
default: false
})
.option('pasv_url', {
.option('pasv-url', {
describe: 'URL to provide for passive connections',
type: 'string'
type: 'string',
alias: 'pasv_url'
})
.option('pasv_min', {
.option('pasv-min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024
default: 1024,
alias: 'pasv_min'
})
.option('pasv_max', {
.option('pasv-max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535
default: 65535,
alias: 'pasv_max'
})
.parse();
}

View File

@@ -38,7 +38,7 @@ export class FileSystem {
chmod(path: string, mode: string): Promise<any>;
getUniqueName(): string;
getUniqueName(fileName: string): string;
}
export class FtpConnection extends EventEmitter {

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,97 +0,0 @@
version: 2
jobs:
build:
docker:
- image: &node-image circleci/node:lts
steps:
- checkout
- restore_cache:
keys:
- &npm-cache-key npm-cache-{{ .Branch }}-{{ .Revision }}
- npm-cache-{{ .Branch }}
- npm-cache
- run:
name: Install
command: npm ci
- persist_to_workspace:
root: .
paths:
- node_modules
- save_cache:
key: *npm-cache-key
paths:
- ~/.npm/_cacache
lint:
docker:
- image: *node-image
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Lint
command: npm run verify
test:
docker:
- image: *node-image
steps:
- checkout
- attach_workspace:
at: .
- deploy:
name: Test
command: |
npm run test
release_dry_run:
docker:
- image: *node-image
steps:
- checkout
- attach_workspace:
at: .
- deploy:
name: Dry Release
command: |
npm run semantic-release -- --dry-run
release:
docker:
- image: *node-image
steps:
- checkout
- attach_workspace:
at: .
- deploy:
name: Release
command: |
npm run semantic-release
workflows:
version: 2
publish:
jobs:
- build
- lint:
requires:
- build
- test:
requires:
- build
- release_dry_run:
filters:
branches:
only: master
requires:
- test
- lint
- hold_release:
type: approval
requires:
- release_dry_run
- release:
requires:
- hold_release

6
old/.gitignore vendored
View File

@@ -1,6 +0,0 @@
test_tmp/
node_modules/
dist/
npm-debug.log

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Tyler Stewart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,357 +0,0 @@
<p align="center">
<a href="https://github.com/trs/ftp-srv">
<img alt="ftp-srv" src="logo.png" width="600px" />
</a>
</p>
<p align="center">
Modern, extensible FTP Server
</p>
<p align="center">
<a href="https://www.npmjs.com/package/ftp-srv">
<img alt="npm" src="https://img.shields.io/npm/dm/ftp-srv.svg?style=for-the-badge" />
</a>
<a href="https://circleci.com/gh/trs/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/trs/ftp-srv/master.svg?style=for-the-badge" />
</a>
</p>
---
- [Overview](#overview)
- [Features](#features)
- [Install](#install)
- [Usage](#usage)
- [API](#api)
- [CLI](#cli)
- [Events](#events)
- [Supported Commands](#supported-commands)
- [File System](#file-system)
- [Contributing](#contributing)
- [License](#license)
## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
## Features
- Extensible [file systems](#file-system) per connection
- 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({ options ... });
ftpServer.on('login', (data, resolve, reject) => { ... });
...
ftpServer.listen()
.then(() => { ... });
```
## API
### `new FtpSrv({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. `0.0.0.0` will listen on any available hosts for server and passive connections.
__Default:__ `"ftp://127.0.0.1:21"`
#### `pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`). This defaults to the provided `url` hostname.
__Default:__ `"127.0.0.1"`
#### `pasv_min`
Tne starting port to accept passive connections.
__Default:__ `1024`
#### `pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
#### `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.
#### `timeout`
Sets the timeout (in ms) after that an idle connection is closed by the server
__Default:__ `0`
## CLI
`ftp-srv` also comes with a builtin CLI.
```bash
$ ftp-srv [url] [options]
```
```bash
$ ftp-srv ftp://0.0.0.0:9876 --root ~/Documents
```
#### `url`
Set the listening URL.
Defaults to `ftp://127.0.0.1:21`
#### `--root` / `-r`
Set the default root directory for users.
Defaults to the current directory.
#### `--credentials` / `-c`
Set the path to a json credentials file.
Format:
```js
[
{
"username": "...",
"password": "...",
"root": "..." // Root directory
},
...
]
```
#### `--username`
Set the username for the only user. Do not provide an argument to allow anonymous login.
#### `--password`
Set the password for the given `username`.
## 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
ftpServer.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
ftpServer.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 occurred
`error` error object
### `RETR`
```js
connection.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
connection.on('STOR', (error, fileName) => { ... });
```
Occurs when a file is uploaded.
`error` if successful, will be `null`
`fileName` name of the file that was uploaded
### `RNTO`
```js
connection.on('RNTO', (error, fileName) => { ... });
```
Occurs when a file is renamed.
`error` if successful, will be `null`
`fileName` name of the file that was renamed
## Supported Commands
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
## 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#L40)
Returns a string of the current working directory
__Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L44)
Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L50)
Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L67)
Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L114)
Returns a path to a newly created directory
__Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L79)
Returns a writable stream
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L90)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L105)
Delete a file or directory
__Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L120)
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L126)
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`getUniqueName()`](src/fs.js#L131)
Returns a unique file name to write to
__Used in:__ `STOU`
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## Contributors
- [OzairP](https://github.com/OzairP)
- [TimLuq](https://github.com/TimLuq)
- [crabl](https://github.com/crabl)
- [hirviid](https://github.com/hirviid)
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
- [edin-m](https://github.com/edin-m)
- [voxsoftware](https://github.com/voxsoftware)
- [jorinvo](https://github.com/jorinvo)
- [Johnnyrook777](https://github.com/Johnnyrook777)
- [qchar](https://github.com/qchar)
- [mikejestes](https://github.com/mikejestes)
- [pkeuter](https://github.com/pkeuter)
- [qiansc](https://github.com/qiansc)
- [broofa](https://github.com/broofa)
- [lafin](https://github.com/lafin)
- [alancnet](https://github.com/alancnet)
- [zgwit](https://github.com/zgwit)
## 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)

10366
old/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +0,0 @@
{
"name": "ftp-srv",
"version": "0.0.0",
"description": "Modern, extensible FTP Server",
"keywords": [
"ftp",
"ftp-server",
"ftp-srv",
"ftp-svr",
"ftpd",
"ftpserver",
"server"
],
"license": "MIT",
"files": [
"src",
"bin",
"ftp-srv.d.ts"
],
"main": "ftp-srv.js",
"bin": "./bin/index.js",
"types": "./ftp-srv.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/trs/ftp-srv"
},
"scripts": {
"pre-release": "npm run verify",
"semantic-release": "semantic-release",
"test": "mocha test/**/*.spec.js test/*.spec.js --ui bdd",
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
},
"release": {
"verifyConditions": "condition-circle"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
},
"dependencies": {
"bluebird": "^3.5.1",
"bunyan": "^1.8.12",
"ip": "^1.1.5",
"lodash": "^4.17.15",
"moment": "^2.22.1",
"uuid": "^3.2.1",
"yargs": "^11.0.0"
},
"devDependencies": {
"@commitlint/cli": "^8.1.0",
"@commitlint/config-conventional": "^8.1.0",
"@icetee/ftp": "^1.0.2",
"chai": "^4.2.0",
"condition-circle": "^2.0.2",
"eslint": "^5.14.1",
"husky": "^1.3.1",
"lint-staged": "^8.1.4",
"mocha": "^5.2.0",
"rimraf": "^2.6.1",
"semantic-release": "^15.13.24",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=6.x"
}
}

17773
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,24 +13,81 @@
],
"license": "MIT",
"files": [
"dist"
"src",
"bin",
"ftp-srv.d.ts"
],
"main": "dist/index",
"main": "ftp-srv.js",
"bin": "./bin/index.js",
"types": "./ftp-srv.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/trs/ftp-srv"
"url": "https://github.com/autovance/ftp-srv"
},
"scripts": {
"build": "ncc build src/example.ts -o dist -s",
"dev": "ncc run src/example.ts"
"pre-release": "npm run verify",
"test": "mocha test/*/*/*.spec.js test/*/*.spec.js test/*.spec.js",
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
},
"devDependencies": {
"@types/node": "^13.13.2",
"@zeit/ncc": "^0.22.2",
"typescript": "^3.9.3"
"release": {
"verifyConditions": "condition-circle",
"branch": "main",
"branches": [
"main"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": [
"eslint --fix"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
},
"dependencies": {
"ix": "^3.0.2",
"rxjs": "^6.5.5"
"bluebird": "^3.5.1",
"bunyan": "^1.8.12",
"ip": "^1.1.5",
"lodash": "^4.17.15",
"moment": "^2.22.1",
"uuid": "^3.2.1",
"yargs": "^15.4.1"
},
"devDependencies": {
"@commitlint/cli": "^10.0.0",
"@commitlint/config-conventional": "^16.2.1",
"@icetee/ftp": "^1.0.2",
"chai": "^4.2.0",
"condition-circle": "^2.0.2",
"eslint": "^5.14.1",
"husky": "^1.3.1",
"lint-staged": "^12.3.7",
"mocha": "^9.2.2",
"rimraf": "^2.6.1",
"semantic-release": "^19.0.2",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=12"
}
}

View File

@@ -1,113 +0,0 @@
// import { Socket } from 'net';
// import { fromEvent, Subject } from 'rxjs';
// import { map, tap, takeUntil } from 'rxjs/operators';
// import { parseCommandString, handleCommand } from '~/command';
// import { RecordMap } from './types';
// import { MiddlewareDefinition, MiddlewareCommandHandler } from './middleware/types';
// import { CommandDirective, Command } from './command/types';
// import { formatReply } from './reply';
// import { CommandError } from './error';
// export interface Context {
// username?: string;
// password?: string;
// account?: string;
// authenticated: boolean;
// }
// function getDefaultContext() {
// const context: RecordMap<Context> = new Map();
// context.set('authenticated', false);
// return context;
// }
// function parseBuffer(buffer: Buffer): string {
// return buffer.toString('utf8');
// }
// export default class FTPClient {
// private middleware = new Set<MiddlewareDefinition>();
// private commandMiddleware: RecordMap<{[T in CommandDirective]?: Set<MiddlewareCommandHandler<T>>}> = new Map();
// private commandSubject: Subject<Command>;
// private replySubject: Subject<[number, ...string[]]>;
// public context = getDefaultContext();
// public constructor(private connection: Socket) {
// this.commandSubject = new Subject();
// fromEvent<Buffer>(this.connection, 'data')
// .pipe(
// takeUntil(fromEvent(this.connection, 'close')),
// map(parseBuffer),
// map(parseCommandString),
// tap((command) => console.log(`recv: ${command.raw.trim()}`))
// )
// .subscribe(this.commandSubject);
// this.replySubject = new Subject();
// this.replySubject
// .pipe(
// takeUntil(fromEvent(this.connection, 'close')),
// map(([code, ...lines]) => formatReply(code, lines)),
// tap((message) => console.log(`send: ${message.trim()}`))
// )
// .subscribe((message) => this.connection.write(message));
// }
// private initializeMiddleware() {
// type Def = ReturnType<MiddlewareDefinition>;
// type MiddlewareEntry = [ keyof Def, Def[keyof Def] ];
// for (const createMiddleware of this.middleware.values()) {
// const ware = createMiddleware(this);
// for (const [command, handle] of Object.entries(ware) as MiddlewareEntry[]) {
// const handles = this.commandMiddleware.get(command) ?? new Set<MiddlewareCommandHandler<typeof command>>();
// handles.add(handle as MiddlewareCommandHandler<typeof command>);
// this.commandMiddleware.set(command, handles);
// }
// }
// }
// public resetContext() {
// this.context = getDefaultContext();
// }
// public use(ware: MiddlewareDefinition) {
// this.middleware.add(ware);
// }
// public send(code: number, ...lines: string[]) {
// this.replySubject.next([code, ...lines]);
// }
// public resume() {
// this.initializeMiddleware();
// this.commandSubject.subscribe(async (command) => {
// try {
// const wares = this.commandMiddleware.get(command.directive) ?? new Set();
// await handleCommand(this, wares)(command);
// } catch (err) {
// if (err instanceof CommandError) {
// this.send(err.code, err.message);
// } else {
// this.connection.emit('error', err);
// }
// }
// });
// this.connection.resume();
// }
// public close() {
// this.connection.destroy();
// }
// }
// export const createFTPClient = (socket: Socket) => {
// return new FTPClient(socket);
// }

View File

@@ -1,37 +0,0 @@
import { CommandDefinition } from '~/command/types';
import { CommandError, SkipCommandError } from '~/error';
/*
230
202
530
500, 501, 503, 421
*/
export const ACCT: CommandDefinition<'ACCT'> = (client) => {
return {
setup(command) {
if (client.context.get('authenticated') === true) {
throw new SkipCommandError(202);
}
if (client.context.has('account')) {
throw new CommandError(503, 'Account already set');
}
if (!client.context.has('username')) {
throw new CommandError(503, 'Must send USER');
}
if (!client.context.has('password')) {
throw new CommandError(503, 'Must send PASS');
}
const account = command.arg;
if (!account) {
throw new CommandError(501, 'Must provide account');
}
return {account};
}
}
}

View File

@@ -1,34 +0,0 @@
import { CommandDefinition } from '~/command/types';
import { CommandError, SkipCommandError } from '~/error';
/*
230
202
530
500, 501, 503, 421
332
*/
export const PASS: CommandDefinition<'PASS'> = (client) => {
return {
setup(command) {
if (client.context.get('authenticated') === true) {
throw new SkipCommandError(202);
}
if (client.context.has('password')) {
throw new CommandError(503, 'Password already set');
}
if (!client.context.has('username')) {
throw new CommandError(503, 'Must send USER');
}
const password = command.arg;
if (!password) {
throw new CommandError(501, 'Must provide password');
}
return {password};
}
}
}

View File

@@ -1,10 +0,0 @@
import { CommandDefinition } from '~/command/types';
import { CommandError } from '~/error';
export const PASV: CommandDefinition<'PASV'> = () => {
return {
setup(command) {
}
}
}

View File

@@ -1,22 +0,0 @@
import { CommandDefinition } from '~/command/types';
import { CommandError } from '~/error';
export const PORT: CommandDefinition<'PORT'> = () => {
return {
setup(command) {
const connection = command.arg.split(',');
if (connection.length !== 6) {
throw new CommandError(425, 'Unable to open data connection');
}
const ip = connection.slice(0, 4).join('.');
const portBytes = connection.slice(4).map((p) => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1];
return {
ip,
port
};
}
}
}

View File

@@ -1,17 +0,0 @@
import { CommandDefinition } from '~/command/types';
/*
230
202
530
500, 501, 503, 421
*/
export const QUIT: CommandDefinition<'QUIT'> = (client) => {
return {
setup() {
// Wait for data connection, then close
// client.dataconnection.on('close', client.close()) ?
client.destroy();
}
}
}

View File

@@ -1,17 +0,0 @@
import { CommandDefinition } from '~/command/types';
import { getDefaultContext } from '~/connection/command';
/*
120
220
220
421
500, 502
*/
export const REIN: CommandDefinition<'REIN'> = (client) => {
return {
setup() {
client.context = getDefaultContext();
}
}
}

View File

@@ -1,13 +0,0 @@
import { CommandDefinition } from '~/command/types';
/*
215
500, 501, 502, 421
*/
export const SYST: CommandDefinition<'SYST'> = (client) => {
return {
setup() {
client.send(215, 'UNIX Type: L8');
}
}
}

View File

@@ -1,29 +0,0 @@
import { CommandDefinition } from '~/command/types';
import { CommandError, SkipCommandError } from '~/error';
/*
230
530
500, 501, 421
331, 332
*/
export const USER: CommandDefinition<'USER'> = (client) => {
return {
setup(command) {
if (client.context.get('authenticated') === true) {
throw new SkipCommandError(230);
}
if (client.context.has('username')) {
throw new CommandError(530, 'Username already set');
}
const username = command.arg;
if (!username) {
throw new CommandError(501, 'Must provide username');
}
return {username};
}
}
};

View File

@@ -1,17 +0,0 @@
import { CommandDefinition } from '../types';
import {USER} from './USER';
import {PASS} from './PASS';
import {ACCT} from './ACCT';
import {QUIT} from './QUIT';
import {PORT} from './PORT';
const registry = new Map<string, CommandDefinition<any>>();
registry.set('USER', USER);
registry.set('PASS', PASS);
registry.set('ACCT', ACCT);
registry.set('QUIT', QUIT);
registry.set('PORT', PORT);
export default registry;

View File

@@ -1,40 +0,0 @@
import { Command, CommandDirective } from './types';
import definitions from './definitions';
import { UnsupportedCommandError } from "~/error";
import { CommandConnection } from "~/connection/command";
const CMD_FLAG_REGEX = new RegExp(/^(?:-(\w{1}))|(?:--(\w{2,}))$/);
export function parseCommandString(commandString: string): Command {
// TODO replace this function with something better
const strippedMessage = commandString.replace(/"/g, '');
let [directive, ...args] = strippedMessage.replace(/\r?\n/g, '').split(' ');
directive = directive.trim().toLocaleUpperCase();
const parseCommandFlags = !['RETR', 'SIZE', 'STOR'].includes(directive);
const params = args.reduce(({arg, flags}: {arg: string[], flags: string[]}, param) => {
if (parseCommandFlags && CMD_FLAG_REGEX.test(param)) flags.push(param);
else arg.push(param);
return {arg, flags};
}, {arg: [], flags: []});
const command: Command = {
directive: directive as CommandDirective,
arg: params.arg.length ? params.arg.join(' ') : undefined,
flags: params.flags,
raw: commandString
};
return command;
}
export const getCommandContext = (client: CommandConnection, command: Command) => {
const createDefinition = definitions.get(command.directive);
if (!createDefinition) {
throw new UnsupportedCommandError(command.directive);
}
const definition = createDefinition(client);
const context = 'setup' in definition ? definition.setup(command) : undefined;
return context;
}

View File

@@ -1,54 +0,0 @@
import { CommandConnection } from '~/connection/command';
import { OrPromise } from '../types';
export interface CommandContext {
USER: {username: string};
PASS: {password: string};
ACCT: {account: string};
CWD: void;
CDUP: void;
SMNT: void;
REIN: void;
QUIT: void;
PORT: {ip: string, port: number};
PASV: void;
MODE: void;
TYPE: void;
STRU: void;
ALLO: void;
REST: void;
STOR: void;
STOU: void;
RETR: void;
LIST: void;
NLST: void;
APPE: void;
RNFR: void;
RNTO: void;
DELE: void;
RMD: void;
MKD: void;
PWD: void;
ABOR: void;
SYST: void;
STAT: void;
HELP: void;
SITE: void;
NOOP: void;
}
export type CommandDirective = keyof CommandContext;
export type Command = {
directive: CommandDirective;
arg: string | undefined,
flags: string[],
raw: string;
}
export type CommandDefinition<T extends CommandDirective> = (client: CommandConnection) => {
/** Checks that command is valid, creates context for handle */
setup?: (command: Command) => OrPromise<CommandContext[T]>;
/** Performs actions required for command, can be extended with plugins */
handle?: (context: CommandContext[T]) => OrPromise<void>;
}

View File

@@ -45,30 +45,30 @@ class FtpCommands {
log.trace({command: logCommand}, 'Handle command');
if (!REGISTRY.hasOwnProperty(command.directive)) {
return this.connection.reply(502, 'Command not allowed');
return this.connection.reply(502, `Command not allowed: ${command.directive}`);
}
if (_.includes(this.blacklist, command.directive)) {
return this.connection.reply(502, 'Command blacklisted');
return this.connection.reply(502, `Command blacklisted: ${command.directive}`);
}
if (this.whitelist.length > 0 && !_.includes(this.whitelist, command.directive)) {
return this.connection.reply(502, 'Command not whitelisted');
return this.connection.reply(502, `Command not whitelisted: ${command.directive}`);
}
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');
return this.connection.reply(530, `Command requires authentication: ${command.directive}`);
}
if (!commandRegister.handler) {
return this.connection.reply(502, 'Handler not set on command');
return this.connection.reply(502, `Handler not set on command: ${command.directive}`);
}
const handler = commandRegister.handler.bind(this.connection);
return Promise.resolve(handler({log, command, previous_command: this.previousCommand}))
.finally(() => {
.then(() => {
this.previousCommand = _.clone(command);
});
}

View File

@@ -7,7 +7,7 @@ module.exports = {
.then(() => this.reply(226));
})
.catch(() => this.reply(225))
.finally(() => this.connector.end());
.then(() => this.connector.end());
},
syntax: '{{cmd}}',
description: 'Abort an active file transfer'

View File

@@ -8,14 +8,18 @@ const FAMILY = {
module.exports = {
directive: 'EPRT',
handler: function ({command} = {}) {
handler: function ({log, 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));
.then(() => this.reply(200))
.catch((err) => {
log.error(err);
return this.reply(err.code || 425, err.message);
});
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'

View File

@@ -2,13 +2,17 @@ const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'EPSV',
handler: function () {
handler: function ({log}) {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
const {port} = server.address();
return this.reply(229, `EPSV OK (|||${port}|)`);
})
.catch((err) => {
log.error(err);
return this.reply(err.code || 425, err.message);
});
},
syntax: '{{cmd}} [<protocol>]',

View File

@@ -47,7 +47,7 @@ module.exports = {
log.error(err);
return this.reply(451, err.message || 'No directory');
})
.finally(() => {
.then(() => {
this.connector.end();
this.commandSocket.resume();
});

View File

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

View File

@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const PassiveConnector = require('../../connector/passive');
const {isLocalIP} = require('../../helpers/is-local');
@@ -11,12 +12,17 @@ module.exports = {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
let address = this.server.options.pasv_url;
// Allow connecting from local
if (isLocalIP(this.ip)) {
address = this.ip;
}
const {port} = server.address();
let pasvAddress = this.server.options.pasv_url;
if (typeof pasvAddress === "function") {
return Promise.try(() => pasvAddress(this.ip))
.then((address) => ({address, port}));
}
// Allow connecting from local
if (isLocalIP(this.ip)) pasvAddress = this.ip;
return {address: pasvAddress, port};
})
.then(({address, port}) => {
const host = address.replace(/\./g, ',');
const portByte1 = port / 256 | 0;
const portByte2 = port % 256;
@@ -25,7 +31,7 @@ module.exports = {
})
.catch((err) => {
log.error(err);
return this.reply(425);
return this.reply(err.code || 425, err.message);
});
},
syntax: '{{cmd}}',

View File

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

View File

@@ -9,7 +9,7 @@ module.exports = {
const rawConnection = _.get(command, 'arg', '').split(',');
if (rawConnection.length !== 6) return this.reply(425);
const ip = rawConnection.slice(0, 4).join('.');
const ip = rawConnection.slice(0, 4).map((b) => parseInt(b)).join('.');
const portBytes = rawConnection.slice(4).map((p) => parseInt(p));
const port = portBytes[0] * 256 + portBytes[1];
@@ -17,7 +17,7 @@ module.exports = {
.then(() => this.reply(200))
.catch((err) => {
log.error(err);
return this.reply(425);
return this.reply(err.code || 425, err.message);
});
},
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',

View File

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

View File

@@ -43,7 +43,7 @@ module.exports = {
.then(() => eventsPromise)
.tap(() => this.emit('RETR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
.then(() => stream.destroy && stream.destroy());
})
.catch(Promise.TimeoutError, (err) => {
log.error(err);
@@ -54,7 +54,7 @@ module.exports = {
this.emit('RETR', err);
return this.reply(551, err.message);
})
.finally(() => {
.then(() => {
this.connector.end();
this.commandSocket.resume();
});

View File

@@ -21,7 +21,7 @@ module.exports = {
this.emit('RNTO', err);
return this.reply(550, err.message);
})
.finally(() => {
.then(() => {
delete this.renameFrom;
});
},

View File

@@ -37,12 +37,7 @@ module.exports = {
});
const socketPromise = new Promise((resolve, reject) => {
this.connector.socket.on('data', (data) => {
if (this.connector.socket) this.connector.socket.pause();
if (stream && stream.writable) {
stream.write(data, () => this.connector.socket && this.connector.socket.resume());
}
});
this.connector.socket.pipe(stream, {end: false});
this.connector.socket.once('end', () => {
if (stream.listenerCount('close')) stream.emit('close');
else stream.end();
@@ -53,11 +48,11 @@ module.exports = {
this.restByteCount = 0;
return this.reply(150).then(() => this.connector.socket.resume())
return this.reply(150).then(() => this.connector.socket && this.connector.socket.resume())
.then(() => Promise.all([streamPromise, socketPromise]))
.tap(() => this.emit('STOR', null, serverPath))
.then(() => this.reply(226, clientPath))
.finally(() => stream.destroy && stream.destroy());
.then(() => stream.destroy && stream.destroy());
})
.catch(Promise.TimeoutError, (err) => {
log.error(err);
@@ -68,7 +63,7 @@ module.exports = {
this.emit('STOR', err);
return this.reply(550, err.message);
})
.finally(() => {
.then(() => {
this.connector.end();
this.commandSocket.resume();
});

View File

@@ -9,7 +9,7 @@ module.exports = {
const fileName = args.command.arg;
return Promise.try(() => this.fs.get(fileName))
.then(() => Promise.try(() => this.fs.getUniqueName()))
.then(() => Promise.try(() => this.fs.getUniqueName(fileName)))
.catch(() => fileName)
.then((name) => {
args.command.arg = name;

View File

@@ -14,6 +14,7 @@ class FtpConnection extends EventEmitter {
super();
this.server = server;
this.id = uuid.v4();
this.commandSocket = options.socket;
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
@@ -24,7 +25,6 @@ class FtpConnection extends EventEmitter {
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});
@@ -32,7 +32,7 @@ class FtpConnection extends EventEmitter {
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', () => {
this.log.trace('Client timeout');
this.close().catch((e) => this.log.trace(e, 'Client close error'));
this.close();
});
this.commandSocket.on('close', () => {
if (this.connector) this.connector.end();
@@ -72,7 +72,7 @@ class FtpConnection extends EventEmitter {
close(code = 421, message = 'Closing connection') {
return Promise.resolve(code)
.then((_code) => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.end());
.then(() => this.commandSocket && this.commandSocket.destroy());
}
login(username, password) {
@@ -129,14 +129,17 @@ class FtpConnection extends EventEmitter {
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);
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
if (error) {
this.log.error('[Process Letter] Socket Write Error', { error: error.message });
return reject(error);
}
resolve();
});
} else reject(new errors.SocketError('Socket not writable'));
} else {
this.log.trace({message: letter.message}, 'Could not write message');
reject(new errors.SocketError('Socket not writable'));
}
});
};
@@ -144,8 +147,8 @@ class FtpConnection extends EventEmitter {
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch((err) => {
this.log.error(err);
.catch((error) => {
this.log.error('Satisfy Parameters Error', { error: error.message });
});
}
}

View File

@@ -1,101 +0,0 @@
import { Socket } from "net";
import { fromEvent, Subject, from } from 'rxjs';
import { takeUntil, map, tap, filter, switchMap } from 'rxjs/operators';
import { parseCommandString, getCommandContext } from '~/command';
import { Command } from "~/command/types";
import { formatReply } from "~/reply";
import { MiddlewareDefinition } from "~/middleware/types";
import { CommandError } from "~/error";
import { RecordMap } from "~/types";
interface Context {
username?: string;
password?: string;
account?: string;
authenticated: boolean;
}
type ContextMap = RecordMap<Context>;
export interface CommandConnection extends Socket {
context: ContextMap;
use: (ware: MiddlewareDefinition) => void;
send: (code: number, ...lines: string[]) => void;
}
export function getDefaultContext() {
const context: RecordMap<Context> = new Map();
context.set('authenticated', false);
return context;
}
export const createCommandConnection = (socket: Socket) => {
const connection = socket as CommandConnection;
connection.context = getDefaultContext();
// Observables
const commandSubject = new Subject<Command>();
fromEvent<Buffer>(connection, 'data')
.pipe(
takeUntil(fromEvent(connection, 'close')),
map(parseBuffer),
map(parseCommandString),
tap((command) => console.log(`recv: ${command.raw.trim()}`))
)
.subscribe(commandSubject);
const replySubject = new Subject();
replySubject
.pipe(
takeUntil(fromEvent(connection, 'close')),
map(([code, ...lines]) => formatReply(code, lines)),
tap((message) => console.log(`send: ${message.trim()}`))
)
.subscribe((message) => connection.write(message));
// Methods
connection.use = (createMiddleware) => {
const middleware = createMiddleware(connection);
commandSubject.pipe(
filter((command) => command.directive in middleware),
switchMap(async (command) => {
const context = getCommandContext(connection, command);
await middleware[command.directive](context);
})
)
.subscribe({
error(err) {
if (err instanceof CommandError) {
this.send(err.code, err.message);
} else {
this.connection.emit('error', err);
}
}
});
};
connection.send = (code: number, ...lines: string[]) => {
replySubject.next([code, ...lines]);
};
return connection;
};
const parseBuffer = (buffer: Buffer) => buffer.toString('utf8');
export const createCommandObservable = (connection: CommandConnection) =>
fromEvent<Buffer>(connection, 'data').pipe(
takeUntil(fromEvent(connection, 'close')),
map(parseBuffer),
map(parseCommandString)
);
export const resumeCommandConnection = (connection: CommandConnection) => {
connection.resume();
};

View File

@@ -1,60 +0,0 @@
import { fromEvent } from 'rxjs';
import { map, timeout, takeUntil, take } from 'rxjs/operators';
import { Socket, createConnection } from 'net';
import {createServer, ServerOptions} from '~/server';
export interface DataConnection extends Socket {
}
export const createDataConnection = () => map<Socket, DataConnection>((socket) => {
const connection = socket as DataConnection;
return connection;
});
// Active
export interface ActiveDataConnectionConfig {
ip: string;
port: number;
timeout: number;
}
export const createActiveDataConnection = (config: ActiveDataConnectionConfig) => {
const connection = createConnection({
port: config.port,
host: config.ip,
family: 4,
allowHalfOpen: false
});
return fromEvent(connection, 'connect', {once: true})
.pipe(
timeout(config.timeout),
createDataConnection()
)
.toPromise();
}
// Passive
export interface PassiveDataConnectionConfig extends ServerOptions {
timeout: number;
}
export const createPassiveDataConnection = (config: PassiveDataConnectionConfig) => {
const server = createServer(config);
server.maxConnections = 1;
const connection = fromEvent(server, 'connection').pipe(
takeUntil(fromEvent(server, 'close')),
timeout(config.timeout),
createDataConnection()
);
server.listen(config.port, config.hostname);
return connection.toPromise();
}

View File

@@ -1,7 +1,9 @@
const {Socket} = require('net');
const tls = require('tls');
const ip = require('ip');
const Promise = require('bluebird');
const Connector = require('./base');
const {SocketError} = require('../errors');
class Active extends Connector {
constructor(connection) {
@@ -27,6 +29,10 @@ class Active extends Connector {
return closeExistingServer()
.then(() => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, host)) {
throw new SocketError('The given address is not yours', 500);
}
this.dataSocket = new Socket();
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.connect({host, port, family}, () => {

View File

@@ -29,7 +29,7 @@ class Connector {
closeSocket() {
if (this.dataSocket) {
const socket = this.dataSocket;
this.dataSocket.end(() => socket.destroy());
this.dataSocket.end(() => socket && socket.destroy());
this.dataSocket = null;
}
}

View File

@@ -44,7 +44,7 @@ class Passive extends Connector {
socket.destroy();
return this.connection.reply(550, 'Remote addresses do not match')
.finally(() => this.connection.close());
.then(() => this.connection.close());
}
clearTimeout(idleServerTimeout);
@@ -86,6 +86,10 @@ class Passive extends Connector {
}
});
});
})
.catch((error) => {
this.log.trace(error.message);
throw error;
});
}

View File

@@ -1,17 +0,0 @@
export class CommandError extends Error {
constructor(public code: number, message: string) {
super(message);
}
}
export class UnsupportedCommandError extends CommandError {
constructor(public directive: string) {
super(502, `Command not implemented: ${directive}`);
}
}
export class SkipCommandError extends CommandError {
constructor(code: number, message?: string) {
super(code, message);
}
}

View File

@@ -1,16 +0,0 @@
import { createFTPServer } from './';
import { createGreetingMiddleware } from './middleware/greeting';
import { createLoginMiddleware } from './middleware/login';
createFTPServer({
port: 2121,
hostname: 'localhost'
})
.use(createGreetingMiddleware({message: 'Hello World!'}))
.use(createLoginMiddleware(
(username, password) => {
return username === 'sudo' && password === 'password'
},
{requirePassword: true, requireAccount: false}
))
.listen();

View File

@@ -6,10 +6,13 @@ const {createReadStream, createWriteStream, constants} = require('fs');
const fsAsync = require('./helpers/fs-async');
const errors = require('./errors');
const UNIX_SEP_REGEX = /\//g;
const WIN_SEP_REGEX = /\\/g;
class FileSystem {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = nodePath.normalize(cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep);
this.cwd = nodePath.normalize((cwd || '/').replace(WIN_SEP_REGEX, '/'));
this._root = nodePath.resolve(root || process.cwd());
}
@@ -18,19 +21,21 @@ class FileSystem {
}
_resolvePath(path = '.') {
const clientPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
} else {
return nodePath.join(this.cwd, path);
}
})();
// Unix separators normalize nicer on both unix and win platforms
const resolvedPath = path.replace(WIN_SEP_REGEX, '/');
const fsPath = (() => {
const resolvedPath = nodePath.join(this.root, clientPath);
return nodePath.resolve(nodePath.normalize(nodePath.join(resolvedPath)));
})();
// Join cwd with new path
const joinedPath = nodePath.isAbsolute(resolvedPath)
? nodePath.normalize(resolvedPath)
: nodePath.join('/', this.cwd, resolvedPath);
// Create local filesystem path using the platform separator
const fsPath = nodePath.resolve(nodePath.join(this.root, joinedPath)
.replace(UNIX_SEP_REGEX, nodePath.sep)
.replace(WIN_SEP_REGEX, nodePath.sep));
// Create FTP client path using unix separator
const clientPath = joinedPath.replace(WIN_SEP_REGEX, '/');
return {
clientPath,
@@ -114,7 +119,7 @@ class FileSystem {
mkdir(path) {
const {fsPath} = this._resolvePath(path);
return fsAsync.mkdir(fsPath)
return fsAsync.mkdir(fsPath, { recursive: true })
.then(() => fsPath);
}

View File

@@ -44,22 +44,30 @@ class FtpServer extends EventEmitter {
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout);
const serverConnectionHandler = (socket) => {
socket.setTimeout(this.options.timeout);
this.options.timeout > 0 && socket.setTimeout(this.options.timeout);
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
socket.on('close', () => this.disconnectClient(connection.id));
socket.once('close', () => {
this.emit('disconnect', {connection, id: connection.id, newConnectionCount: Object.keys(this.connections).length});
})
this.emit('connect', {connection, id: connection.id, newConnectionCount: Object.keys(this.connections).length});
const greeting = this._greeting || [];
const features = this._features || 'Ready';
return connection.reply(220, ...greeting, features)
.finally(() => socket.resume());
.then(() => socket.resume());
};
const serverOptions = Object.assign({}, this.isTLS ? this.options.tls : {}, {pauseOnConnect: true});
this.server = (this.isTLS ? tls : net).createServer(serverOptions, serverConnectionHandler);
this.server.on('error', (err) => this.log.error(err, '[Event] error'));
this.server.on('error', (err) => {
this.log.error(err, '[Event] error');
this.emit('server-error', {error: err});
});
const quit = _.debounce(this.quit.bind(this), 100);
process.on('SIGTERM', quit);
@@ -116,37 +124,49 @@ class FtpServer extends EventEmitter {
}
disconnectClient(id) {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
const client = this.connections[id];
if (!client) return resolve();
this.emit('disconnect', {connection: client, id});
delete this.connections[id];
setTimeout(() => {
reject(new Error('Timed out disconnecting the client'))
}, this.options.timeout || 1000)
try {
client.close(0);
} catch (err) {
this.log.error(err, 'Error closing connection', {id});
} finally {
resolve('Disconnected');
}
resolve('Disconnected');
});
}
quit() {
return this.close()
.finally(() => process.exit(0));
.then(() => 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)))
this.emit('closing');
this.log.info('Closing connections:', Object.keys(this.connections).length);
return Promise.all(Object.keys(this.connections).map((id) => this.disconnectClient(id)))
.then(() => new Promise((resolve) => {
this.server.close((err) => {
this.log.info('Server closing...');
if (err) this.log.error(err, 'Error closing server');
resolve('Closed');
});
}))
.then(() => this.removeAllListeners());
.then(() => {
this.log.debug('Removing event listeners...')
this.emit('closed', {});
this.removeAllListeners();
return;
});
}
}

View File

@@ -1,42 +0,0 @@
import {Server} from 'net';
import {promisify} from 'util';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil, map } from 'rxjs/operators';
import { createCommandConnection, resumeCommandConnection, CommandConnection } from '~/connection/command';
import { MiddlewareDefinition } from './middleware/types';
import { ServerOptions, createServer } from './server';
export default class FTPServer {
private server: Server;
private connectionSubject: Subject<CommandConnection>;
public constructor(private config: ServerOptions) {
this.server = createServer(this.config);
this.connectionSubject = new Subject()
fromEvent(this.server, 'connection').pipe(
takeUntil(fromEvent(this.server, 'close')),
map(createCommandConnection)
)
.subscribe(this.connectionSubject);
}
public close = () => promisify(this.server.close)();
public listen() {
this.server.listen(this.config.port, this.config.hostname);
this.connectionSubject.subscribe(resumeCommandConnection);
}
public use(ware: MiddlewareDefinition) {
this.connectionSubject.subscribe((client) => client.use(ware));
return this;
}
}
export function createFTPServer(...args: ConstructorParameters<typeof FTPServer>) {
return new FTPServer(...args);
}

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