Compare commits
60 Commits
observable
...
v4.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af62a7c4f | ||
|
|
0b65a22296 | ||
|
|
f59857e34a | ||
|
|
1ad45fc757 | ||
|
|
bc8abb14da | ||
|
|
0b55b3f79d | ||
|
|
4f4a6c25a5 | ||
|
|
45a4bf15bf | ||
|
|
a1be4416a7 | ||
|
|
32a0750e2c | ||
|
|
4eb17015f1 | ||
|
|
d2566e7745 | ||
|
|
f8cd1e8f64 | ||
|
|
e0e676e7e9 | ||
|
|
a7775a46ae | ||
|
|
f9c81b162a | ||
|
|
e1f1aa09cd | ||
|
|
b0174bb24e | ||
|
|
5852851ded | ||
|
|
1c5db00a5e | ||
|
|
02227d653e | ||
|
|
bf44cbba58 | ||
|
|
80ff71655e | ||
|
|
eb8e3d837f | ||
|
|
dc37c9c435 | ||
|
|
6a94a20f64 | ||
|
|
02355eda28 | ||
|
|
8b32be7acc | ||
|
|
5daaa9883c | ||
|
|
02798e094f | ||
|
|
720c93d088 | ||
|
|
bffcc35299 | ||
|
|
78de22f518 | ||
|
|
2b140ecb0d | ||
|
|
57ddfb5e08 | ||
|
|
beef19af30 | ||
|
|
e7c5f83311 | ||
|
|
1ab793c04e | ||
|
|
24f7126acf | ||
|
|
457b859450 | ||
|
|
722da60a82 | ||
|
|
9f95d60916 | ||
|
|
4cd88b129c | ||
|
|
db49063b0d | ||
|
|
b55557292e | ||
|
|
c87ce2fef6 | ||
|
|
05a68cfb08 | ||
|
|
31290fc964 | ||
|
|
a598fab03c | ||
|
|
87e8ac6ca8 | ||
|
|
75e34988f4 | ||
|
|
e449e75219 | ||
|
|
296573ae01 | ||
|
|
be579f65d0 | ||
|
|
c440050945 | ||
|
|
ca576acf2e | ||
|
|
649d582f30 | ||
|
|
e26caad783 | ||
|
|
2470db6482 | ||
|
|
d78fae0ce6 |
112
.circleci/config.yml
Normal file
112
.circleci/config.yml
Normal 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
|
||||||
0
old/.gitattributes → .gitattributes
vendored
0
old/.gitattributes → .gitattributes
vendored
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
|
test_tmp/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
|
npm-debug.log
|
||||||
|
|||||||
21
.vscode/launch.json
vendored
21
.vscode/launch.json
vendored
@@ -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
12
.vscode/tasks.json
vendored
@@ -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
5
CODEOWNERS
Normal 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
|
||||||
412
README.md
412
README.md
@@ -1,48 +1,382 @@
|
|||||||
```ts
|
<p align="center">
|
||||||
const loginMiddleware = () => (client) => {
|
<a href="https://github.com/autovance/ftp-srv">
|
||||||
let username;
|
<img alt="ftp-srv" src="logo.png" width="600px" />
|
||||||
let password;
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
return {
|
|
||||||
USER(client, command) => {
|
<p align="center">
|
||||||
username = command.arg;
|
Modern, extensible FTP Server
|
||||||
},
|
</p>
|
||||||
PASS(client, command) => {
|
|
||||||
password = command.arg;
|
<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', (data, resolve, reject) => {
|
||||||
|
if(data.username === 'anonymous' && data.password === 'anonymous'){
|
||||||
|
return resolve({ root:"/" });
|
||||||
}
|
}
|
||||||
};
|
return reject(new errors.GeneralError('Invalid username or password', 401));
|
||||||
}
|
});
|
||||||
|
|
||||||
const fileSystemMiddleware = () => (client) => {
|
ftpServer.listen().then(() => {
|
||||||
return {
|
console.log('Ftp server is starting...')
|
||||||
CWD(client, command) => {},
|
});
|
||||||
CDUP(client) => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const transferMiddleware = () => (client) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
client.use(loginMiddleware());
|
|
||||||
```
|
```
|
||||||
|
|
||||||
5.1. MINIMUM IMPLEMENTATION
|
## API
|
||||||
|
|
||||||
In order to make FTP workable without needless error messages, the
|
### `new FtpSrv({options})`
|
||||||
following minimum implementation is required for all servers:
|
#### 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
|
_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.
|
||||||
MODE - Stream
|
__Default:__ `"ftp://127.0.0.1:21"`
|
||||||
STRUCTURE - File, Record
|
|
||||||
COMMANDS - USER, QUIT, PORT,
|
|
||||||
TYPE, MODE, STRU,
|
|
||||||
for the default values
|
|
||||||
RETR, STOR,
|
|
||||||
NOOP.
|
|
||||||
|
|
||||||
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
|
const nets = networkInterfaces();
|
||||||
MODE - Stream
|
function getNetworks() {
|
||||||
STRU - File
|
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`.
|
||||||
|
|
||||||
|
### `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(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
16
SECURITY.md
Normal 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
15
old/bin/index.js → bin/index.js
Normal file → Executable file
@@ -37,19 +37,22 @@ function setupYargs() {
|
|||||||
boolean: true,
|
boolean: true,
|
||||||
default: false
|
default: false
|
||||||
})
|
})
|
||||||
.option('pasv_url', {
|
.option('pasv-url', {
|
||||||
describe: 'URL to provide for passive connections',
|
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',
|
describe: 'Starting point to use when creating passive connections',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: 1024
|
default: 1024,
|
||||||
|
alias: 'pasv_min'
|
||||||
})
|
})
|
||||||
.option('pasv_max', {
|
.option('pasv-max', {
|
||||||
describe: 'Ending port to use when creating passive connections',
|
describe: 'Ending port to use when creating passive connections',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: 65535
|
default: 65535,
|
||||||
|
alias: 'pasv_max'
|
||||||
})
|
})
|
||||||
.parse();
|
.parse();
|
||||||
}
|
}
|
||||||
2
old/ftp-srv.d.ts → ftp-srv.d.ts
vendored
2
old/ftp-srv.d.ts → ftp-srv.d.ts
vendored
@@ -38,7 +38,7 @@ export class FileSystem {
|
|||||||
|
|
||||||
chmod(path: string, mode: string): Promise<any>;
|
chmod(path: string, mode: string): Promise<any>;
|
||||||
|
|
||||||
getUniqueName(): string;
|
getUniqueName(fileName: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FtpConnection extends EventEmitter {
|
export class FtpConnection extends EventEmitter {
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@@ -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
6
old/.gitignore
vendored
@@ -1,6 +0,0 @@
|
|||||||
test_tmp/
|
|
||||||
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
dist/
|
|
||||||
npm-debug.log
|
|
||||||
21
old/LICENSE
21
old/LICENSE
@@ -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.
|
|
||||||
357
old/README.md
357
old/README.md
@@ -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
10366
old/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
17773
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
79
package.json
79
package.json
@@ -13,24 +13,81 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/trs/ftp-srv"
|
"url": "https://github.com/autovance/ftp-srv"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ncc build src/example.ts -o dist -s",
|
"pre-release": "npm run verify",
|
||||||
"dev": "ncc run src/example.ts"
|
"test": "mocha test/*/*/*.spec.js test/*/*.spec.js test/*.spec.js",
|
||||||
|
"verify": "eslint src/**/*.js test/**/*.js bin/**/*.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"release": {
|
||||||
"@types/node": "^13.13.2",
|
"verifyConditions": "condition-circle",
|
||||||
"@zeit/ncc": "^0.22.2",
|
"branch": "main",
|
||||||
"typescript": "^3.9.3"
|
"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": {
|
"dependencies": {
|
||||||
"ix": "^3.0.2",
|
"bluebird": "^3.5.1",
|
||||||
"rxjs": "^6.5.5"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/client.ts
113
src/client.ts
@@ -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);
|
|
||||||
// }
|
|
||||||
@@ -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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { CommandDefinition } from '~/command/types';
|
|
||||||
import { CommandError } from '~/error';
|
|
||||||
|
|
||||||
export const PASV: CommandDefinition<'PASV'> = () => {
|
|
||||||
return {
|
|
||||||
setup(command) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -45,25 +45,25 @@ class FtpCommands {
|
|||||||
log.trace({command: logCommand}, 'Handle command');
|
log.trace({command: logCommand}, 'Handle command');
|
||||||
|
|
||||||
if (!REGISTRY.hasOwnProperty(command.directive)) {
|
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)) {
|
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)) {
|
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 commandRegister = REGISTRY[command.directive];
|
||||||
const commandFlags = _.get(commandRegister, 'flags', {});
|
const commandFlags = _.get(commandRegister, 'flags', {});
|
||||||
if (!commandFlags.no_auth && !this.connection.authenticated) {
|
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) {
|
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);
|
const handler = commandRegister.handler.bind(this.connection);
|
||||||
@@ -8,14 +8,18 @@ const FAMILY = {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'EPRT',
|
directive: 'EPRT',
|
||||||
handler: function ({command} = {}) {
|
handler: function ({log, command} = {}) {
|
||||||
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
|
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
|
||||||
const family = FAMILY[protocol];
|
const family = FAMILY[protocol];
|
||||||
if (!family) return this.reply(504, 'Unknown network protocol');
|
if (!family) return this.reply(504, 'Unknown network protocol');
|
||||||
|
|
||||||
this.connector = new ActiveConnector(this);
|
this.connector = new ActiveConnector(this);
|
||||||
return this.connector.setupConnection(ip, port, family)
|
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>|',
|
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
|
||||||
description: 'Specifies an address and port to which the server should connect'
|
description: 'Specifies an address and port to which the server should connect'
|
||||||
@@ -2,13 +2,17 @@ const PassiveConnector = require('../../connector/passive');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'EPSV',
|
directive: 'EPSV',
|
||||||
handler: function () {
|
handler: function ({log}) {
|
||||||
this.connector = new PassiveConnector(this);
|
this.connector = new PassiveConnector(this);
|
||||||
return this.connector.setupServer()
|
return this.connector.setupServer()
|
||||||
.then((server) => {
|
.then((server) => {
|
||||||
const {port} = server.address();
|
const {port} = server.address();
|
||||||
|
|
||||||
return this.reply(229, `EPSV OK (|||${port}|)`);
|
return this.reply(229, `EPSV OK (|||${port}|)`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log.error(err);
|
||||||
|
return this.reply(err.code || 425, err.message);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}} [<protocol>]',
|
syntax: '{{cmd}} [<protocol>]',
|
||||||
@@ -7,7 +7,7 @@ module.exports = {
|
|||||||
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
if (!this.fs) return this.reply(550, 'File system not instantiated');
|
||||||
if (!this.fs.mkdir) return this.reply(402, 'Not supported by file system');
|
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) => {
|
.then((dir) => {
|
||||||
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
const path = dir ? `"${escapePath(dir)}"` : undefined;
|
||||||
return this.reply(257, path);
|
return this.reply(257, path);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const Promise = require('bluebird');
|
||||||
const PassiveConnector = require('../../connector/passive');
|
const PassiveConnector = require('../../connector/passive');
|
||||||
const {isLocalIP} = require('../../helpers/is-local');
|
const {isLocalIP} = require('../../helpers/is-local');
|
||||||
|
|
||||||
@@ -11,12 +12,17 @@ module.exports = {
|
|||||||
this.connector = new PassiveConnector(this);
|
this.connector = new PassiveConnector(this);
|
||||||
return this.connector.setupServer()
|
return this.connector.setupServer()
|
||||||
.then((server) => {
|
.then((server) => {
|
||||||
let address = this.server.options.pasv_url;
|
|
||||||
// Allow connecting from local
|
|
||||||
if (isLocalIP(this.ip)) {
|
|
||||||
address = this.ip;
|
|
||||||
}
|
|
||||||
const {port} = server.address();
|
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 host = address.replace(/\./g, ',');
|
||||||
const portByte1 = port / 256 | 0;
|
const portByte1 = port / 256 | 0;
|
||||||
const portByte2 = port % 256;
|
const portByte2 = port % 256;
|
||||||
@@ -25,7 +31,7 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(425);
|
return this.reply(err.code || 425, err.message);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}}',
|
syntax: '{{cmd}}',
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PBSZ',
|
directive: 'PBSZ',
|
||||||
handler: function ({command} = {}) {
|
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);
|
this.bufferSize = parseInt(command.arg, 10);
|
||||||
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
return this.reply(200, this.bufferSize === 0 ? 'OK' : 'Buffer too large: PBSZ=0');
|
||||||
},
|
},
|
||||||
@@ -9,7 +9,7 @@ module.exports = {
|
|||||||
const rawConnection = _.get(command, 'arg', '').split(',');
|
const rawConnection = _.get(command, 'arg', '').split(',');
|
||||||
if (rawConnection.length !== 6) return this.reply(425);
|
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 portBytes = rawConnection.slice(4).map((p) => parseInt(p));
|
||||||
const port = portBytes[0] * 256 + portBytes[1];
|
const port = portBytes[0] * 256 + portBytes[1];
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ module.exports = {
|
|||||||
.then(() => this.reply(200))
|
.then(() => this.reply(200))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
return this.reply(425);
|
return this.reply(err.code || 425, err.message);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
|
||||||
@@ -3,7 +3,7 @@ const _ = require('lodash');
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
directive: 'PROT',
|
directive: 'PROT',
|
||||||
handler: function ({command} = {}) {
|
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);
|
if (!this.bufferSize && typeof this.bufferSize !== 'number') return this.reply(503);
|
||||||
|
|
||||||
switch (_.toUpper(command.arg)) {
|
switch (_.toUpper(command.arg)) {
|
||||||
@@ -37,12 +37,7 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const socketPromise = new Promise((resolve, reject) => {
|
const socketPromise = new Promise((resolve, reject) => {
|
||||||
this.connector.socket.on('data', (data) => {
|
this.connector.socket.pipe(stream, {end: false});
|
||||||
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.once('end', () => {
|
this.connector.socket.once('end', () => {
|
||||||
if (stream.listenerCount('close')) stream.emit('close');
|
if (stream.listenerCount('close')) stream.emit('close');
|
||||||
else stream.end();
|
else stream.end();
|
||||||
@@ -53,7 +48,7 @@ module.exports = {
|
|||||||
|
|
||||||
this.restByteCount = 0;
|
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]))
|
.then(() => Promise.all([streamPromise, socketPromise]))
|
||||||
.tap(() => this.emit('STOR', null, serverPath))
|
.tap(() => this.emit('STOR', null, serverPath))
|
||||||
.then(() => this.reply(226, clientPath))
|
.then(() => this.reply(226, clientPath))
|
||||||
@@ -9,7 +9,7 @@ module.exports = {
|
|||||||
|
|
||||||
const fileName = args.command.arg;
|
const fileName = args.command.arg;
|
||||||
return Promise.try(() => this.fs.get(fileName))
|
return Promise.try(() => this.fs.get(fileName))
|
||||||
.then(() => Promise.try(() => this.fs.getUniqueName()))
|
.then(() => Promise.try(() => this.fs.getUniqueName(fileName)))
|
||||||
.catch(() => fileName)
|
.catch(() => fileName)
|
||||||
.then((name) => {
|
.then((name) => {
|
||||||
args.command.arg = name;
|
args.command.arg = name;
|
||||||
@@ -14,6 +14,7 @@ class FtpConnection extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.id = uuid.v4();
|
this.id = uuid.v4();
|
||||||
|
this.commandSocket = options.socket;
|
||||||
this.log = options.log.child({id: this.id, ip: this.ip});
|
this.log = options.log.child({id: this.id, ip: this.ip});
|
||||||
this.commands = new Commands(this);
|
this.commands = new Commands(this);
|
||||||
this.transferType = 'binary';
|
this.transferType = 'binary';
|
||||||
@@ -24,7 +25,6 @@ class FtpConnection extends EventEmitter {
|
|||||||
|
|
||||||
this.connector = new BaseConnector(this);
|
this.connector = new BaseConnector(this);
|
||||||
|
|
||||||
this.commandSocket = options.socket;
|
|
||||||
this.commandSocket.on('error', (err) => {
|
this.commandSocket.on('error', (err) => {
|
||||||
this.log.error(err, 'Client error');
|
this.log.error(err, 'Client error');
|
||||||
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
|
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('data', this._handleData.bind(this));
|
||||||
this.commandSocket.on('timeout', () => {
|
this.commandSocket.on('timeout', () => {
|
||||||
this.log.trace('Client timeout');
|
this.log.trace('Client timeout');
|
||||||
this.close().catch((e) => this.log.trace(e, 'Client close error'));
|
this.close();
|
||||||
});
|
});
|
||||||
this.commandSocket.on('close', () => {
|
this.commandSocket.on('close', () => {
|
||||||
if (this.connector) this.connector.end();
|
if (this.connector) this.connector.end();
|
||||||
@@ -72,7 +72,7 @@ class FtpConnection extends EventEmitter {
|
|||||||
close(code = 421, message = 'Closing connection') {
|
close(code = 421, message = 'Closing connection') {
|
||||||
return Promise.resolve(code)
|
return Promise.resolve(code)
|
||||||
.then((_code) => _code && this.reply(_code, message))
|
.then((_code) => _code && this.reply(_code, message))
|
||||||
.then(() => this.commandSocket && this.commandSocket.end());
|
.finally(() => this.commandSocket && this.commandSocket.destroy());
|
||||||
}
|
}
|
||||||
|
|
||||||
login(username, password) {
|
login(username, password) {
|
||||||
@@ -129,14 +129,17 @@ class FtpConnection extends EventEmitter {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (letter.socket && letter.socket.writable) {
|
if (letter.socket && letter.socket.writable) {
|
||||||
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
|
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) => {
|
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
|
||||||
if (err) {
|
if (error) {
|
||||||
this.log.error(err);
|
this.log.error('[Process Letter] Socket Write Error', { error: error.message });
|
||||||
return reject(err);
|
return reject(error);
|
||||||
}
|
}
|
||||||
resolve();
|
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) => {
|
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
|
||||||
return processLetter(letter, index);
|
return processLetter(letter, index);
|
||||||
}))
|
}))
|
||||||
.catch((err) => {
|
.catch((error) => {
|
||||||
this.log.error(err);
|
this.log.error('Satisfy Parameters Error', { error: error.message });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
};
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
const {Socket} = require('net');
|
const {Socket} = require('net');
|
||||||
const tls = require('tls');
|
const tls = require('tls');
|
||||||
|
const ip = require('ip');
|
||||||
const Promise = require('bluebird');
|
const Promise = require('bluebird');
|
||||||
const Connector = require('./base');
|
const Connector = require('./base');
|
||||||
|
const {SocketError} = require('../errors');
|
||||||
|
|
||||||
class Active extends Connector {
|
class Active extends Connector {
|
||||||
constructor(connection) {
|
constructor(connection) {
|
||||||
@@ -27,6 +29,10 @@ class Active extends Connector {
|
|||||||
|
|
||||||
return closeExistingServer()
|
return closeExistingServer()
|
||||||
.then(() => {
|
.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 = new Socket();
|
||||||
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
|
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.connect({host, port, family}, () => {
|
||||||
@@ -29,7 +29,7 @@ class Connector {
|
|||||||
closeSocket() {
|
closeSocket() {
|
||||||
if (this.dataSocket) {
|
if (this.dataSocket) {
|
||||||
const socket = this.dataSocket;
|
const socket = this.dataSocket;
|
||||||
this.dataSocket.end(() => socket.destroy());
|
this.dataSocket.end(() => socket && socket.destroy());
|
||||||
this.dataSocket = null;
|
this.dataSocket = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,10 @@ class Passive extends Connector {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.log.trace(error.message);
|
||||||
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
17
src/error.ts
17
src/error.ts
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
@@ -6,10 +6,13 @@ const {createReadStream, createWriteStream, constants} = require('fs');
|
|||||||
const fsAsync = require('./helpers/fs-async');
|
const fsAsync = require('./helpers/fs-async');
|
||||||
const errors = require('./errors');
|
const errors = require('./errors');
|
||||||
|
|
||||||
|
const UNIX_SEP_REGEX = /\//g;
|
||||||
|
const WIN_SEP_REGEX = /\\/g;
|
||||||
|
|
||||||
class FileSystem {
|
class FileSystem {
|
||||||
constructor(connection, {root, cwd} = {}) {
|
constructor(connection, {root, cwd} = {}) {
|
||||||
this.connection = connection;
|
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());
|
this._root = nodePath.resolve(root || process.cwd());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,19 +21,21 @@ class FileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_resolvePath(path = '.') {
|
_resolvePath(path = '.') {
|
||||||
const clientPath = (() => {
|
// Unix separators normalize nicer on both unix and win platforms
|
||||||
path = nodePath.normalize(path);
|
const resolvedPath = path.replace(WIN_SEP_REGEX, '/');
|
||||||
if (nodePath.isAbsolute(path)) {
|
|
||||||
return nodePath.join(path);
|
|
||||||
} else {
|
|
||||||
return nodePath.join(this.cwd, path);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
const fsPath = (() => {
|
// Join cwd with new path
|
||||||
const resolvedPath = nodePath.join(this.root, clientPath);
|
const joinedPath = nodePath.isAbsolute(resolvedPath)
|
||||||
return nodePath.resolve(nodePath.normalize(nodePath.join(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 {
|
return {
|
||||||
clientPath,
|
clientPath,
|
||||||
@@ -114,7 +119,7 @@ class FileSystem {
|
|||||||
|
|
||||||
mkdir(path) {
|
mkdir(path) {
|
||||||
const {fsPath} = this._resolvePath(path);
|
const {fsPath} = this._resolvePath(path);
|
||||||
return fsAsync.mkdir(fsPath)
|
return fsAsync.mkdir(fsPath, { recursive: true })
|
||||||
.then(() => fsPath);
|
.then(() => fsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +44,12 @@ class FtpServer extends EventEmitter {
|
|||||||
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout);
|
this.options.timeout = isNaN(timeout) ? 0 : Number(timeout);
|
||||||
|
|
||||||
const serverConnectionHandler = (socket) => {
|
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});
|
let connection = new Connection(this, {log: this.log, socket});
|
||||||
this.connections[connection.id] = connection;
|
this.connections[connection.id] = connection;
|
||||||
|
|
||||||
socket.on('close', () => this.disconnectClient(connection.id));
|
socket.on('close', () => this.disconnectClient(connection.id));
|
||||||
|
socket.once('close', () => this.emit('disconnect', {connection, id: connection.id}));
|
||||||
|
|
||||||
const greeting = this._greeting || [];
|
const greeting = this._greeting || [];
|
||||||
const features = this._features || 'Ready';
|
const features = this._features || 'Ready';
|
||||||
@@ -116,18 +117,22 @@ class FtpServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnectClient(id) {
|
disconnectClient(id) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
const client = this.connections[id];
|
const client = this.connections[id];
|
||||||
if (!client) return resolve();
|
if (!client) return resolve();
|
||||||
this.emit('disconnect', {connection: client, id});
|
|
||||||
delete this.connections[id];
|
delete this.connections[id];
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
reject('Timed out disconnecting client')
|
||||||
|
}, this.options.timeout || 1000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
client.close(0);
|
client.close(0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.log.error(err, 'Error closing connection', {id});
|
this.log.error(err, 'Error closing connection', {id});
|
||||||
} finally {
|
|
||||||
resolve('Disconnected');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve('Disconnected');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,16 +142,22 @@ class FtpServer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.log.info('Server closing...');
|
|
||||||
this.server.maxConnections = 0;
|
this.server.maxConnections = 0;
|
||||||
return Promise.map(Object.keys(this.connections), (id) => Promise.try(this.disconnectClient.bind(this, id)))
|
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) => {
|
.then(() => new Promise((resolve) => {
|
||||||
this.server.close((err) => {
|
this.server.close((err) => {
|
||||||
|
this.log.info('Server closing...');
|
||||||
if (err) this.log.error(err, 'Error closing server');
|
if (err) this.log.error(err, 'Error closing server');
|
||||||
resolve('Closed');
|
resolve('Closed');
|
||||||
});
|
});
|
||||||
}))
|
}))
|
||||||
.then(() => this.removeAllListeners());
|
.then(() => {
|
||||||
|
this.log.debug('Removing event listeners...')
|
||||||
|
this.removeAllListeners();
|
||||||
|
return;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
42
src/index.ts
42
src/index.ts
@@ -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
Reference in New Issue
Block a user