Compare commits

..

1 Commits

Author SHA1 Message Date
Tyler Stewart
21bb611c30 WIP: typescript refactor 2019-04-08 16:37:51 -06:00
143 changed files with 19259 additions and 16923 deletions

View File

@@ -1,112 +1,97 @@
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 >>
version: 2
jobs:
lint:
executor: node-lts
build:
docker:
- image: &node-image circleci/node:lts
steps:
- checkout
- node/install-packages
- 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
release_dry_run:
executor: node-lts
test:
docker:
- image: *node-image
steps:
- checkout
- node/install-packages
- setup_git_bot
- 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: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release --dry-run
npm run semantic-release -- --dry-run
release:
executor: node-lts
docker:
- image: *node-image
steps:
- checkout
- node/install-packages
- setup_git_bot
- attach_workspace:
at: .
- deploy:
name: Release
command: |
git branch -u "origin/${CIRCLE_BRANCH}"
npx semantic-release
npm run semantic-release
workflows:
version: 2
release_scheduled:
triggers:
# 6:03 UTC (mornings) 1 monday
- schedule:
cron: "3 6 * * 1"
filters:
branches:
only:
- main
publish:
jobs:
- lint
- node/test:
matrix:
parameters:
version:
- '12.22'
- '14.19'
- '16.14'
- 'current'
- release:
context: npm-deploy-av
- build
- lint:
requires:
- node/test
- lint
test:
jobs:
- lint
- node/test:
matrix:
parameters:
version:
- '12.22'
- '14.19'
- '16.14'
- 'current'
- build
- test:
requires:
- build
- release_dry_run:
filters:
branches:
only: main
only: master
requires:
- node/test
- test
- lint
- hold_release:
type: approval
requires:
- release_dry_run
- release:
context: npm-deploy-av
requires:
- hold_release

View File

@@ -1,19 +0,0 @@
---
### Acceptance Checklist
- [ ] **Story**: Code is focused on the linked stories and solves a problem
- One of:
- [ ] **For Bugs**: A unit test is added or an existing one modified
- [ ] **For Features**: New unit tests are added covering the new functions or modifications
- [ ] Code Documentation changes are included for public interfaces and important / complex additions
- [ ] External Documentation is included for API changes, or other external facing interfaces
### Review Checklist
- [ ] The code does not duplicate existing functionality that exists elsewhere
- [ ] The code has been linted and follows team practices and style guidelines
- [ ] The changes in the PR are relevant to the title
- changes not related should be moved to a different PR
- [ ] All errors or error handling is actionable, and informs the viewer on how to correct it

View File

@@ -1,5 +0,0 @@
# 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

416
README.md
View File

@@ -1,416 +0,0 @@
<p align="center">
<a href="https://github.com/autovance/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/autovance/workflows/ftp-srv/tree/master">
<img alt="circleci" src="https://img.shields.io/circleci/project/github/autovance/ftp-srv/master.svg?style=for-the-badge" />
</a>
</p>
---
## Overview
`ftp-srv` is a modern and extensible FTP server designed to be simple yet configurable.
## Features
- Extensible [file systems](#file-system) per connection
- Passive and active transfers
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections
- Promise based API
## Install
`npm install ftp-srv --save`
## Usage
```js
// Quick start, create an active ftp server.
const FtpSrv = require('ftp-srv');
const port=21;
const ftpServer = new FtpSrv({
url: "ftp://0.0.0.0:" + port,
anonymous: true
});
ftpServer.on('login', ({ connection, username, password }, resolve, reject) => {
if(username === 'anonymous' && password === 'anonymous'){
return resolve({ root:"/" });
}
return reject(new errors.GeneralError('Invalid username or password', 401));
});
ftpServer.listen().then(() => {
console.log('Ftp server is starting...')
});
```
## 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`
`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');
const nets = networkInterfaces();
function getNetworks() {
let networks = {};
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
if (net.family === 'IPv4' && !net.internal) {
networks[net.address + "/24"] = net.address
}
}
}
return networks;
}
const resolverFunction = (address) => {
// const networks = {
// '$GATEWAY_IP/32': `${public_ip}`,
// '10.0.0.0/8' : `${lan_ip}`
// }
const networks = getNetworks();
for (const network in networks) {
if (new Netmask(network).contains(address)) {
return networks[network];
}
}
return "127.0.0.1";
}
new FtpSrv({pasv_url: resolverFunction});
```
- A static IP address (ie. an external WAN **IP address** that the FTP server is bound to). In this case, only connections from localhost are handled differently returning `127.0.0.1` to the client.
If not provided, clients can only connect using an `Active` connection.
#### `pasv_min`
The starting port to accept passive connections.
__Default:__ `1024`
#### `pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
#### `greeting`
A human readable array of lines or string to send when a client connects.
__Default:__ `null`
#### `tls`
Node [TLS secure context object](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) used for implicit (`ftps` protocol) or explicit (`AUTH TLS`) connections.
__Default:__ `false`
#### `anonymous`
If true, will allow clients to authenticate using the username `anonymous`, not requiring a password from the user.
Can also set as a string which allows users to authenticate using the username provided.
The `login` event is then sent with the provided username and `@anonymous` as the password.
__Default:__ `false`
#### `blacklist`
Array of commands that are not allowed.
Response code `502` is sent to clients sending one of these commands.
__Example:__ `['RMD', 'RNFR', 'RNTO']` will not allow users to delete directories or rename any files.
__Default:__ `[]`
#### `whitelist`
Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command.
__Default:__ `[]`
#### `file_format`
Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"`
__Allowable values:__
- `ls` [bin/ls format](https://cr.yp.to/ftp/list/binls.html)
- `ep` [Easily Parsed LIST format](https://cr.yp.to/ftp/list/eplf.html)
- `function () {}` A custom function returning a format or promise for one.
- Only one argument is passed in: a node [file stat](https://nodejs.org/api/fs.html#fs_class_fs_stats) object with additional file `name` parameter
#### `log`
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.
#### `timeout`
Sets the timeout (in ms) after that an idle connection is closed by the server
__Default:__ `0`
## CLI
`ftp-srv` also comes with a builtin CLI.
```bash
$ ftp-srv [url] [options]
```
```bash
$ ftp-srv ftp://0.0.0.0:9876 --root ~/Documents
```
#### `url`
Set the listening URL.
Defaults to `ftp://127.0.0.1:21`
#### `--pasv_url`
The hostname to provide a client when attempting a passive connection (`PASV`).
If not provided, clients can only connect using an `Active` connection.
#### `--pasv_min`
The starting port to accept passive connections.
__Default:__ `1024`
#### `--pasv_max`
The ending port to accept passive connections.
The range is then queried for an available port to use when required.
__Default:__ `65535`
#### `--root` / `-r`
Set the default root directory for users.
Defaults to the current directory.
#### `--credentials` / `-c`
Set the path to a json credentials file.
Format:
```js
[
{
"username": "...",
"password": "...",
"root": "..." // Root directory
},
...
]
```
#### `--username`
Set the username for the only user. Do not provide an argument to allow anonymous login.
#### `--password`
Set the password for the given `username`.
#### `--read-only`
Disable write actions such as upload, delete, etc.
## Events
The `FtpSrv` class extends the [node net.Server](https://nodejs.org/api/net.html#net_class_net_server). Some custom events can be resolved or rejected, such as `login`.
### `client-error`
```js
ftpServer.on('client-error', ({connection, context, error}) => { ... });
```
Occurs when an error arises in the client connection.
`connection` [client class object](src/connection.js)
`context` string of where the error occurred
`error` error object
### `disconnect`
```js
ftpServer.on('disconnect', ({connection, id, newConnectionCount}) => { ... });
```
Occurs when a client has disconnected.
`connection` [client class object](src/connection.js)
`id` string of the disconnected connection id
`id` number of the new connection count (exclusive the disconnected client connection)
### `closed`
```js
ftpServer.on('closed', ({}) => { ... });
```
Occurs when the FTP server has been closed.
### `closing`
```js
ftpServer.on('closing', ({}) => { ... });
```
Occurs when the FTP server has started closing.
### `login`
```js
ftpServer.on('login', ({connection, username, password}, resolve, reject) => { ... });
```
Occurs when a client is attempting to login. Here you can resolve the login request by username and password.
`connection` [client class object](src/connection.js)
`username` string of username from `USER` command
`password` string of password from `PASS` command
`resolve` takes an object of arguments:
- `fs`
- Set a custom file system class for this connection to use.
- See [File System](#file-system) for implementation details.
- `root`
- If `fs` is not provided, this will set the root directory for the connection.
- The user cannot traverse lower than this directory.
- `cwd`
- If `fs` is not provided, will set the starting directory for the connection
- This is relative to the `root` directory.
- `blacklist`
- Commands that are forbidden for only this connection
- `whitelist`
- If set, this connection will only be able to use the provided commands
`reject` takes an error object
### `server-error`
```js
ftpServer.on('server-error', ({error}) => { ... });
```
Occurs when an error arises in the FTP server.
`error` error object
### `RETR`
```js
connection.on('RETR', (error, filePath) => { ... });
```
Occurs when a file is downloaded.
`error` if successful, will be `null`
`filePath` location to which file was downloaded
### `STOR`
```js
connection.on('STOR', (error, fileName) => { ... });
```
Occurs when a file is uploaded.
`error` if successful, will be `null`
`fileName` name of the file that was uploaded
### `RNTO`
```js
connection.on('RNTO', (error, fileName) => { ... });
```
Occurs when a file is renamed.
`error` if successful, will be `null`
`fileName` name of the file that was renamed
## Supported Commands
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.
## File System
The default [file system](src/fs.js) can be overwritten to use your own implementation.
This can allow for virtual file systems, and more.
Each connection can set it's own file system based on the user.
The default file system is exported and can be extended as needed:
```js
const {FtpSrv, FileSystem} = require('ftp-srv');
class MyFileSystem extends FileSystem {
constructor() {
super(...arguments);
}
get(fileName) {
...
}
}
```
Custom file systems can implement the following variables depending on the developers needs:
### Methods
#### [`currentDirectory()`](src/fs.js#L40)
Returns a string of the current working directory
__Used in:__ `PWD`
#### [`get(fileName)`](src/fs.js#L44)
Returns a file stat object of file or directory
__Used in:__ `LIST`, `NLST`, `STAT`, `SIZE`, `RNFR`, `MDTM`
#### [`list(path)`](src/fs.js#L50)
Returns array of file and directory stat objects
__Used in:__ `LIST`, `NLST`, `STAT`
#### [`chdir(path)`](src/fs.js#L67)
Returns new directory relative to current directory
__Used in:__ `CWD`, `CDUP`
#### [`mkdir(path)`](src/fs.js#L114)
Returns a path to a newly created directory
__Used in:__ `MKD`
#### [`write(fileName, {append, start})`](src/fs.js#L79)
Returns a writable stream
Options:
`append` if true, append to existing file
`start` if set, specifies the byte offset to write to
__Used in:__ `STOR`, `APPE`
#### [`read(fileName, {start})`](src/fs.js#L90)
Returns a readable stream
Options:
`start` if set, specifies the byte offset to read from
__Used in:__ `RETR`
#### [`delete(path)`](src/fs.js#L105)
Delete a file or directory
__Used in:__ `DELE`
#### [`rename(from, to)`](src/fs.js#L120)
Renames a file or directory
__Used in:__ `RNFR`, `RNTO`
#### [`chmod(path)`](src/fs.js#L126)
Modifies a file or directory's permissions
__Used in:__ `SITE CHMOD`
#### [`getUniqueName(fileName)`](src/fs.js#L131)
Returns a unique file name to write to. Client requested filename available if you want to base your function on it.
__Used in:__ `STOU`
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## License
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
## References
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)

View File

@@ -1,16 +0,0 @@
# 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
example/index.ts Normal file
View File

@@ -0,0 +1,15 @@
import FTPServer from '../src/server';
const server = new FTPServer();
server.registerPlugin({
command: 'PASS',
handler: async ({connection, reply}) => {
const username = connection.getContext('username');
const password = connection.getContext('password');
// AUTHENTICATE
reply.set([230]);
}
});
server.listen();

166
ftp-srv.d.ts vendored
View File

@@ -1,166 +0,0 @@
import * as tls from 'tls'
import { Stats } from 'fs'
import { EventEmitter } from 'events';
export class FileSystem {
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
constructor(connection: FtpConnection, {root, cwd}?: {
root: any;
cwd: any;
});
currentDirectory(): string;
get(fileName: string): Promise<any>;
list(path?: string): Promise<any>;
chdir(path?: string): Promise<string>;
write(fileName: string, {append, start}?: {
append?: boolean;
start?: any;
}): any;
read(fileName: string, {start}?: {
start?: any;
}): Promise<any>;
delete(path: string): Promise<any>;
mkdir(path: string): Promise<any>;
rename(from: string, to: string): Promise<any>;
chmod(path: string, mode: string): Promise<any>;
getUniqueName(fileName: string): string;
}
export class GeneralError extends Error {
/**
* @param message The error message.
* @param code Default value is `400`.
*/
constructor(message: string, code?: number);
}
export class SocketError extends Error {
/**
* @param message The error message.
* @param code Default value is `500`.
*/
constructor(message: string, code?: number);
}
export class FileSystemError extends Error {
/**
* @param message The error message.
* @param code Default value is `400`.
*/
constructor(message: string, code?: number);
}
export class ConnectorError extends Error {
/**
* @param message The error message.
* @param code Default value is `400`.
*/
constructor(message: string, code?: number);
}
export class FtpConnection extends EventEmitter {
server: FtpServer;
id: string;
log: any;
transferType: string;
encoding: string;
bufferSize: boolean;
readonly ip: string;
restByteCount: number | undefined;
secure: boolean
close (code: number, message: number): Promise<any>
login (username: string, password: string): Promise<any>
reply (options: number | Object, ...letters: Array<any>): Promise<any>
}
export interface FtpServerOptions {
url?: string,
pasv_min?: number,
pasv_max?: number,
pasv_url?: string,
greeting?: string | string[],
tls?: tls.SecureContextOptions | false,
anonymous?: boolean,
blacklist?: Array<string>,
whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log?: any,
timeout?: number
}
export class FtpServer extends EventEmitter {
constructor(options?: FtpServerOptions);
readonly isTLS: boolean;
listen(): any;
emitPromise(action: any, ...data: any[]): Promise<any>;
// emit is exported from super class
setupTLS(_tls: boolean): boolean | {
cert: string;
key: string;
ca: string
};
setupGreeting(greet: string): string[];
setupFeaturesMessage(): string;
disconnectClient(id: string): Promise<any>;
close(): any;
on(event: "login", listener: (
data: {
connection: FtpConnection,
username: string,
password: string
},
resolve: (config: {
fs?: FileSystem,
root?: string,
cwd?: string,
blacklist?: Array<string>,
whitelist?: Array<string>
}) => void,
reject: (err?: Error) => void
) => void): this;
on(event: "disconnect", listener: (
data: {
connection: FtpConnection,
id: string
}
) => void): this;
on(event: "client-error", listener: (
data: {
connection: FtpConnection,
context: string,
error: Error,
}
) => void): this;
}
export {FtpServer as FtpSrv};
export default FtpServer;

358
old/README.md Normal file
View File

@@ -0,0 +1,358 @@
<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.
_Note:_ If set to `0.0.0.0`, this will automatically resolve to the external IP of the box.
__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)

View File

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

127
old/ftp-srv.d.ts vendored Normal file
View File

@@ -0,0 +1,127 @@
import * as tls from 'tls'
import { Stats } from 'fs'
import { EventEmitter } from 'events';
export class FileSystem {
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
constructor(connection: FtpConnection, {root, cwd}?: {
root: any;
cwd: any;
});
currentDirectory(): string;
get(fileName: string): Promise<any>;
list(path?: string): Promise<any>;
chdir(path?: string): Promise<string>;
write(fileName: string, {append, start}?: {
append?: boolean;
start?: any;
}): any;
read(fileName: string, {start}?: {
start?: any;
}): Promise<any>;
delete(path: string): Promise<any>;
mkdir(path: string): Promise<any>;
rename(from: string, to: string): Promise<any>;
chmod(path: string, mode: string): Promise<any>;
getUniqueName(): string;
}
export class FtpConnection extends EventEmitter {
server: FtpServer;
id: string;
log: any;
transferType: string;
encoding: string;
bufferSize: boolean;
readonly ip: string;
restByteCount: number | undefined;
secure: boolean
close (code: number, message: number): Promise<any>
login (username: string, password: string): Promise<any>
reply (options: number | Record<string, any>, ...letters: any[]): Promise<any>
}
export interface FtpServerOptions {
url?: string;
pasv_min?: number;
pasv_max?: number;
pasv_url?: string;
greeting?: string | string[];
tls?: tls.SecureContextOptions | false;
anonymous?: boolean;
blacklist?: string[];
whitelist?: string[];
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep";
log?: any;
timeout?: number;
}
export class FtpServer extends EventEmitter {
constructor(options?: FtpServerOptions);
readonly isTLS: boolean;
listen(): any;
emitPromise(action: any, ...data: any[]): Promise<any>;
// emit is exported from super class
setupTLS(_tls: boolean): boolean | {
cert: string;
key: string;
ca: string;
};
setupGreeting(greet: string): string[];
setupFeaturesMessage(): string;
disconnectClient(id: string): Promise<any>;
close(): any;
on(event: "login", listener: (
data: {
connection: FtpConnection;
username: string;
password: string;
},
resolve: (config: {
fs?: FileSystem;
root?: string;
cwd?: string;
blacklist?: string[];
whitelist?: string[];
}) => void,
reject: (err?: Error) => void
) => void): this;
on(event: "client-error", listener: (
data: {
connection: FtpConnection;
context: string;
error: Error;
}
) => void): this;
}
export {FtpServer as FtpSrv};
export default FtpServer;

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

9976
old/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

91
old/package.json Normal file
View File

@@ -0,0 +1,91 @@
{
"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 **/*.spec.js --ui bdd --bail",
"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.10",
"moment": "^2.22.1",
"uuid": "^3.2.1",
"yargs": "^11.0.0"
},
"devDependencies": {
"@commitlint/cli": "^7.5.2",
"@commitlint/config-conventional": "^7.5.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.10.6",
"sinon": "^2.3.5"
},
"engines": {
"node": ">=6.x"
}
}

View File

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

View File

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

View File

@@ -8,18 +8,14 @@ const FAMILY = {
module.exports = {
directive: 'EPRT',
handler: function ({log, command} = {}) {
handler: function ({command} = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol];
if (!family) return this.reply(504, 'Unknown network protocol');
this.connector = new ActiveConnector(this);
return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200))
.catch((err) => {
log.error(err);
return this.reply(err.code || 425, err.message);
});
.then(() => this.reply(200));
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'

View File

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

View File

@@ -36,7 +36,6 @@ module.exports = {
.tap(() => this.reply(150))
.then((fileList) => {
if (fileList.length) return this.reply({}, ...fileList);
return this.reply({socket: this.connector.socket, useEmptyMessage: true});
})
.tap(() => this.reply(226))
.catch(Promise.TimeoutError, (err) => {
@@ -47,7 +46,7 @@ module.exports = {
log.error(err);
return this.reply(451, err.message || 'No directory');
})
.then(() => {
.finally(() => {
this.connector.end();
this.commandSocket.resume();
});

View File

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

View File

@@ -13,7 +13,7 @@ module.exports = {
const [_option, ...args] = command.arg.split(' ');
const option = _.toUpper(_option);
if (!OPTIONS.hasOwnProperty(option)) return this.reply(501, 'Unknown option command');
if (!OPTIONS.hasOwnProperty(option)) return this.reply(500);
return OPTIONS[option].call(this, args);
},
syntax: '{{cmd}}',
@@ -33,6 +33,7 @@ function utf8([setting] = []) {
if (!encoding) return this.reply(501, 'Unknown setting for option');
this.encoding = encoding;
if (this.transferType !== 'binary') this.transferType = this.encoding;
return this.reply(200, `UTF8 encoding ${_.toLower(setting)}`);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,6 @@ class FtpConnection extends EventEmitter {
super();
this.server = server;
this.id = uuid.v4();
this.commandSocket = options.socket;
this.log = options.log.child({id: this.id, ip: this.ip});
this.commands = new Commands(this);
this.transferType = 'binary';
@@ -25,6 +24,7 @@ class FtpConnection extends EventEmitter {
this.connector = new BaseConnector(this);
this.commandSocket = options.socket;
this.commandSocket.on('error', (err) => {
this.log.error(err, 'Client error');
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
@@ -32,7 +32,7 @@ class FtpConnection extends EventEmitter {
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', () => {
this.log.trace('Client timeout');
this.close();
this.close().catch((e) => this.log.trace(e, 'Client close error'));
});
this.commandSocket.on('close', () => {
if (this.connector) this.connector.end();
@@ -72,7 +72,7 @@ class FtpConnection extends EventEmitter {
close(code = 421, message = 'Closing connection') {
return Promise.resolve(code)
.then((_code) => _code && this.reply(_code, message))
.then(() => this.commandSocket && this.commandSocket.destroy());
.then(() => this.commandSocket && this.commandSocket.end());
}
login(username, password) {
@@ -104,21 +104,15 @@ class FtpConnection extends EventEmitter {
else if (typeof letter === 'string') letter = {message: letter}; // allow passing in message as first param
if (!letter.socket) letter.socket = options.socket ? options.socket : this.commandSocket;
if (!options.useEmptyMessage) {
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding;
}
if (!letter.message) letter.message = DEFAULT_MESSAGE[options.code] || 'No information';
if (!letter.encoding) letter.encoding = this.encoding;
return Promise.resolve(letter.message) // allow passing in a promise as a message
.then((message) => {
if (!options.useEmptyMessage) {
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
letter.message = message;
} else {
letter.message = '';
}
const seperator = !options.hasOwnProperty('eol') ?
letters.length - 1 === index ? ' ' : '-' :
options.eol ? ' ' : '-';
message = !letter.raw ? _.compact([letter.code || options.code, message]).join(seperator) : message;
letter.message = message;
return letter;
});
});
@@ -129,17 +123,14 @@ class FtpConnection extends EventEmitter {
return new Promise((resolve, reject) => {
if (letter.socket && letter.socket.writable) {
this.log.trace({port: letter.socket.address().port, encoding: letter.encoding, message: letter.message}, 'Reply');
letter.socket.write(letter.message + '\r\n', letter.encoding, (error) => {
if (error) {
this.log.error('[Process Letter] Socket Write Error', { error: error.message });
return reject(error);
letter.socket.write(letter.message + '\r\n', letter.encoding, (err) => {
if (err) {
this.log.error(err);
return reject(err);
}
resolve();
});
} else {
this.log.trace({message: letter.message}, 'Could not write message');
reject(new errors.SocketError('Socket not writable'));
}
} else reject(new errors.SocketError('Socket not writable'));
});
};
@@ -147,8 +138,8 @@ class FtpConnection extends EventEmitter {
.then((satisfiedLetters) => Promise.mapSeries(satisfiedLetters, (letter, index) => {
return processLetter(letter, index);
}))
.catch((error) => {
this.log.error('Satisfy Parameters Error', { error: error.message });
.catch((err) => {
this.log.error(err);
});
}
}

View File

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

View File

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

View File

@@ -6,8 +6,6 @@ const Promise = require('bluebird');
const Connector = require('./base');
const errors = require('../errors');
const CONNECT_TIMEOUT = 30 * 1000;
class Passive extends Connector {
constructor(connection) {
super(connection);
@@ -32,9 +30,6 @@ class Passive extends Connector {
this.closeServer();
return this.server.getNextPasvPort()
.then((port) => {
this.dataSocket = null;
let idleServerTimeout;
const connectionHandler = (socket) => {
if (!ip.isEqual(this.connection.commandSocket.remoteAddress, socket.remoteAddress)) {
this.log.error({
@@ -44,21 +39,21 @@ class Passive extends Connector {
socket.destroy();
return this.connection.reply(550, 'Remote addresses do not match')
.then(() => this.connection.close());
.finally(() => this.connection.close());
}
clearTimeout(idleServerTimeout);
this.log.trace({port, remoteAddress: socket.remoteAddress}, 'Passive connection fulfilled.');
this.dataSocket = socket;
this.dataSocket.setEncoding(this.connection.transferType);
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.once('close', () => this.closeServer());
if (!this.connection.secure) {
this.dataSocket.connected = true;
}
};
this.dataSocket = null;
const serverOptions = Object.assign({}, this.connection.secure ? this.server.options.tls : {}, {pauseOnConnect: true});
this.dataServer = (this.connection.secure ? tls : net).createServer(serverOptions, connectionHandler);
this.dataServer.maxConnections = 1;
@@ -79,17 +74,11 @@ class Passive extends Connector {
this.dataServer.listen(port, this.server.url.hostname, (err) => {
if (err) reject(err);
else {
idleServerTimeout = setTimeout(() => this.closeServer(), CONNECT_TIMEOUT);
this.log.debug({port}, 'Passive connection listening');
resolve(this.dataServer);
}
});
});
})
.catch((error) => {
this.log.trace(error.message);
throw error;
});
}

View File

@@ -2,17 +2,13 @@ const _ = require('lodash');
const nodePath = require('path');
const uuid = require('uuid');
const Promise = require('bluebird');
const {createReadStream, createWriteStream, constants} = require('fs');
const fsAsync = require('./helpers/fs-async');
const fs = Promise.promisifyAll(require('fs'));
const errors = require('./errors');
const UNIX_SEP_REGEX = /\//g;
const WIN_SEP_REGEX = /\\/g;
class FileSystem {
constructor(connection, {root, cwd} = {}) {
this.connection = connection;
this.cwd = nodePath.normalize((cwd || '/').replace(WIN_SEP_REGEX, '/'));
this.cwd = cwd ? nodePath.join(nodePath.sep, cwd) : nodePath.sep;
this._root = nodePath.resolve(root || process.cwd());
}
@@ -21,21 +17,19 @@ class FileSystem {
}
_resolvePath(path = '.') {
// Unix separators normalize nicer on both unix and win platforms
const resolvedPath = path.replace(WIN_SEP_REGEX, '/');
const clientPath = (() => {
path = nodePath.normalize(path);
if (nodePath.isAbsolute(path)) {
return nodePath.join(path);
} else {
return nodePath.join(this.cwd, path);
}
})();
// Join cwd with new path
const joinedPath = nodePath.isAbsolute(resolvedPath)
? nodePath.normalize(resolvedPath)
: nodePath.join('/', this.cwd, resolvedPath);
// Create local filesystem path using the platform separator
const fsPath = nodePath.resolve(nodePath.join(this.root, joinedPath)
.replace(UNIX_SEP_REGEX, nodePath.sep)
.replace(WIN_SEP_REGEX, nodePath.sep));
// Create FTP client path using unix separator
const clientPath = joinedPath.replace(WIN_SEP_REGEX, '/');
const fsPath = (() => {
const resolvedPath = nodePath.resolve(this.root, `.${nodePath.sep}${clientPath}`);
return nodePath.join(resolvedPath);
})();
return {
clientPath,
@@ -49,19 +43,19 @@ class FileSystem {
get(fileName) {
const {fsPath} = this._resolvePath(fileName);
return fsAsync.stat(fsPath)
return fs.statAsync(fsPath)
.then((stat) => _.set(stat, 'name', fileName));
}
list(path = '.') {
const {fsPath} = this._resolvePath(path);
return fsAsync.readdir(fsPath)
return fs.readdirAsync(fsPath)
.then((fileNames) => {
return Promise.map(fileNames, (fileName) => {
const filePath = nodePath.join(fsPath, fileName);
return fsAsync.access(filePath, constants.F_OK)
return fs.accessAsync(filePath, fs.constants.F_OK)
.then(() => {
return fsAsync.stat(filePath)
return fs.statAsync(filePath)
.then((stat) => _.set(stat, 'name', fileName));
})
.catch(() => null);
@@ -72,7 +66,7 @@ class FileSystem {
chdir(path = '.') {
const {fsPath, clientPath} = this._resolvePath(path);
return fsAsync.stat(fsPath)
return fs.statAsync(fsPath)
.tap((stat) => {
if (!stat.isDirectory()) throw new errors.FileSystemError('Not a valid directory');
})
@@ -84,8 +78,8 @@ class FileSystem {
write(fileName, {append = false, start = undefined} = {}) {
const {fsPath, clientPath} = this._resolvePath(fileName);
const stream = createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fsAsync.unlink(fsPath));
const stream = fs.createWriteStream(fsPath, {flags: !append ? 'w+' : 'a+', start});
stream.once('error', () => fs.unlinkAsync(fsPath));
stream.once('close', () => stream.end());
return {
stream,
@@ -95,12 +89,12 @@ class FileSystem {
read(fileName, {start = undefined} = {}) {
const {fsPath, clientPath} = this._resolvePath(fileName);
return fsAsync.stat(fsPath)
return fs.statAsync(fsPath)
.tap((stat) => {
if (stat.isDirectory()) throw new errors.FileSystemError('Cannot read a directory');
})
.then(() => {
const stream = createReadStream(fsPath, {flags: 'r', start});
const stream = fs.createReadStream(fsPath, {flags: 'r', start});
return {
stream,
clientPath
@@ -110,28 +104,28 @@ class FileSystem {
delete(path) {
const {fsPath} = this._resolvePath(path);
return fsAsync.stat(fsPath)
return fs.statAsync(fsPath)
.then((stat) => {
if (stat.isDirectory()) return fsAsync.rmdir(fsPath);
else return fsAsync.unlink(fsPath);
if (stat.isDirectory()) return fs.rmdirAsync(fsPath);
else return fs.unlinkAsync(fsPath);
});
}
mkdir(path) {
const {fsPath} = this._resolvePath(path);
return fsAsync.mkdir(fsPath, { recursive: true })
return fs.mkdirAsync(fsPath)
.then(() => fsPath);
}
rename(from, to) {
const {fsPath: fromPath} = this._resolvePath(from);
const {fsPath: toPath} = this._resolvePath(to);
return fsAsync.rename(fromPath, toPath);
return fs.renameAsync(fromPath, toPath);
}
chmod(path, mode) {
const {fsPath} = this._resolvePath(path);
return fsAsync.chmod(fsPath, mode);
return fs.chmodAsync(fsPath, mode);
}
getUniqueName() {

View File

@@ -16,11 +16,10 @@ function* portNumberGenerator(min, max = MAX_PORT) {
function getNextPortFactory(host, portMin, portMax, maxAttempts = MAX_PORT_CHECK_ATTEMPT) {
const nextPortNumber = portNumberGenerator(portMin, portMax);
const portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
return () => new Promise((resolve, reject) => {
const portCheckServer = net.createServer();
portCheckServer.maxConnections = 0;
let attemptCount = 0;
const tryGetPort = () => {
attemptCount++;
@@ -40,7 +39,6 @@ function getNextPortFactory(host, portMin, portMax, maxAttempts = MAX_PORT_CHECK
}
});
portCheckServer.once('listening', () => {
portCheckServer.removeAllListeners();
portCheckServer.close(() => resolve(port));
});

View File

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

56
old/src/messages.js Normal file
View File

@@ -0,0 +1,56 @@
module.exports = {
| 100
| 110
| 120
| 125
| 150
| 200
| 202
| 211
| 212
| 213
| 214
| 215
| 220
| 221
| 225
| 226
| 227
| 230
| 234
| 250
| 257
| 331
| 332
| 350
| 421
| 425
| 426
| 450
| 451
| 452
| 500
| 501
| 502
| 503
| 504
| 530
| 532
| 550
| 551
| 552
| 553
};

View File

@@ -90,7 +90,7 @@ describe('FtpCommands', function () {
return commands.handle('bad')
.then(() => {
expect(mockConnection.reply.callCount).to.equal(1);
expect(mockConnection.reply.args[0][0]).to.equal(502);
expect(mockConnection.reply.args[0][0]).to.equal(402);
});
});

View File

@@ -23,7 +23,7 @@ describe(CMD, function () {
});
it('// unsuccessful | no argument', () => {
return cmdFn({})
return cmdFn()
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504);
});

View File

@@ -25,7 +25,7 @@ describe(CMD, function () {
});
it('// successful IPv4', () => {
return cmdFn({})
return cmdFn()
.then(() => {
const [code, message] = mockClient.reply.args[0];
expect(code).to.equal(229);

View File

@@ -29,7 +29,7 @@ describe(CMD, function () {
it('BAD // unsuccessful', () => {
return cmdFn({command: {arg: 'BAD', directive: CMD}})
.then(() => {
expect(mockClient.reply.args[0][0]).to.equal(501);
expect(mockClient.reply.args[0][0]).to.equal(500);
});
});

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