Compare commits

...

12 Commits

Author SHA1 Message Date
Tyler Stewart
21bb611c30 WIP: typescript refactor 2019-04-08 16:37:51 -06:00
Tyler Stewart
0d9131c370 chore: simplify circle config 2019-03-08 11:14:42 -07:00
Tyler Stewart
eafaf6f642 chore: drop testing of node v6 2019-03-08 11:00:56 -07:00
Tyler Stewart
ea99e6ebbd chore: fix circle ci node_modules persistance 2019-03-08 10:47:55 -07:00
Tyler Stewart
342911eb36 fix: dont specify host when port searching (#158)
fix: handle errors on pasv/port commands
2019-03-08 17:40:48 +00:00
ls
0094614ee3 feat: socket timeout (#154)
* feat: managed new param socketTimeout expressed in ms

* feat: changed README.md

* feat: changed README.md

* feat: minor change

* feat: pull request changes

* feat: timeout value is set on 'options' object
2019-03-04 16:45:27 +00:00
Jean Gregory
0773967ba5 fix: find available port using hostname (#155)
The passive url is passed to getNextPortFactory so in passive mode, the
opening of a listening port fail when the passive url is not the ip of
an interface on the ftp server host machine.
2019-03-02 22:56:09 +00:00
Tyler Stewart
e5820688c4 feat: remove pasv url resolution (#153)
* fix: correctly try additional ports on EADDRINUSE

* feat: remove passive url host resoltion

See: https://github.com/trs/ftp-srv/issues/139

* feat: require pasv url to be specified

* fix(pasv): check for set pasv url

* chore: update scripts

* fix(cli): remove undefined values

* chore: add cli to eslint verify

* chore: run eslint

* chore: update scripts

* feat: add maximum number of retries to port selection

* chore: simplify eslint config

* chore: simplify dev dependencies

* chore: generate contributors

* chore: update readme contributors

* chore: add verify step to release

BREAKING CHANGE: remove ipify.org automatic resolution of ip
2019-02-21 18:50:16 +00:00
Tyler Stewart
a3fa1a2f71 Merge pull request #152 from trs/revert-143-remove-pasv-url-resolution
fix: revert "feat: remove host resolution"
2019-02-21 18:43:28 +00:00
Tyler Stewart
e540822d5b Revert "feat: remove host resolution (#143)"
This reverts commit 36f331d15d.
2019-02-21 11:42:32 -07:00
Tyler Stewart
36f331d15d feat: remove host resolution (#143)
* fix: correctly try additional ports on EADDRINUSE

* feat: remove passive url host resoltion

See: https://github.com/trs/ftp-srv/issues/139

* feat: require pasv url to be specified

* fix(pasv): check for set pasv url

* chore: update scripts

* fix(cli): remove undefined values

* chore: add cli to eslint verify

* chore: run eslint

* chore: update scripts

* feat: add maximum number of retries to port selection

* chore: simplify eslint config

* chore: simplify dev dependencies

* chore: generate contributors

* chore: update readme contributors
2019-02-21 18:39:07 +00:00
Devonte
03ff982959 fix: correct writable checks (#147)
fix: correct writable checks
2019-02-21 18:30:58 +00:00
145 changed files with 18434 additions and 7360 deletions

View File

@@ -1,103 +1,97 @@
version: 2
create-cache-file: &create-cache-file
run:
name: Setup cache
command: echo "$NODE_VERSION" > _cache_node_version
package-json-cache: &package-json-cache
key: npm-install-{{ checksum "_cache_node_version" }}-{{ checksum "package-lock.json" }}
base-build: &base-build
steps:
- checkout
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- run:
name: Install
command: npm install
- save_cache:
<<: *package-json-cache
paths:
- node_modules
- run:
name: Lint
command: npm run verify:js
- run:
name: Test
command: npm run test:once
jobs:
test_node_10:
build:
docker:
- image: circleci/node:10
environment:
- NODE_VERSION: 10
<<: *base-build
- 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
test_node_8:
lint:
docker:
- image: circleci/node:8
environment:
- NODE_VERSION: 8
<<: *base-build
- image: *node-image
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Lint
command: npm run verify
test_node_6:
test:
docker:
- image: circleci/node:6
environment:
- NODE_VERSION: 6
<<: *base-build
- 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: circleci/node:8
environment:
- NODE_VERSION: 8
- image: *node-image
steps:
- checkout
- <<: *create-cache-file
- restore_cache:
<<: *package-json-cache
- attach_workspace:
at: .
- deploy:
name: Semantic Release
name: Release
command: |
npm run semantic-release
workflows:
version: 2
test_and_tag:
publish:
jobs:
- test_node_10:
filters:
branches:
only: master
- test_node_8:
filters:
branches:
only: master
- test_node_6:
- 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:
- test_node_6
- test_node_8
- test_node_10
build_and_test:
jobs:
- test_node_10:
filters:
branches:
ignore: master
- test_node_8:
filters:
branches:
ignore: master
- test_node_6:
filters:
branches:
ignore: master
- hold_release

View File

@@ -1,9 +0,0 @@
# START_CONFIT_GENERATED_CONTENT
# Common folders to ignore
node_modules/*
bower_components/*
# Config folder (optional - you might want to lint this...)
config/*
# END_CONFIT_GENERATED_CONTENT

5
.gitignore vendored
View File

@@ -1,7 +1,6 @@
test_tmp/
node_modules/
dist/
reports/
npm-debug.log
.nyc_output/
test_tmp/

352
README.md
View File

@@ -1,352 +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.
_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.
## 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`
<!--[RM_CONTRIBUTING]-->
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
<!--[]-->
## Contributors
- [OzairP](https://github.com/OzairP)
- [qchar](https://github.com/qchar)
- [jorinvo](https://github.com/jorinvo)
- [voxsoftware](https://github.com/voxsoftware)
- [pkeuter](https://github.com/pkeuter)
- [TimLuq](https://github.com/TimLuq)
- [edin-mg](https://github.com/edin-m)
- [DiegoRBaquero](https://github.com/DiegoRBaquero)
- [Johnnyrook777](https://github.com/Johnnyrook777)
<!--[RM_LICENSE]-->
## License
This software is licensed under the MIT Licence. See [LICENSE](LICENSE).
<!--[]-->
## References
- [https://cr.yp.to/ftp.html](https://cr.yp.to/ftp.html)

View File

@@ -1,38 +0,0 @@
'use strict';
module.exports = {
types: [
{value: 'feat', name: 'feat: A new feature'},
{value: 'fix', name: 'fix: A bug fix'},
{value: 'docs', name: 'docs: Documentation only changes'},
{value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'},
{value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
{value: 'perf', name: 'perf: A code change that improves performance'},
{value: 'test', name: 'test: Adding missing tests'},
{value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'},
{value: 'revert', name: 'revert: Revert to a commit'},
{value: 'WIP', name: 'WIP: Work in progress'}
],
scopes: [],
// it needs to match the value for field type. Eg.: 'fix'
/*
scopeOverrides: {
fix: [
{name: 'merge'},
{name: 'style'},
{name: 'e2eTest'},
{name: 'unitTest'}
]
},
*/
allowCustomScopes: true,
allowBreakingChanges: ['feat', 'fix'],
// Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged
appendBranchNameToCommitMessage: false
};

View File

@@ -1,5 +0,0 @@
test/**/*.spec.js
--reporter mocha-multi-reporters
--reporter-options configFile=config/testUnit/reporters.json
--ui bdd
--bail

View File

@@ -1,6 +0,0 @@
{
"reporterEnabled": "spec",
"mochaJunitReporterReporterOptions": {
"mochaFile": "reports/junit.xml"
}
}

View File

@@ -1,162 +0,0 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"mocha",
"node"
],
"rules": {
"mocha/no-exclusive-tests": 2,
"no-warning-comments": [
1,
{
"terms": ["todo", "fixme", "xxx"],
"location": "start"
},
],
"object-curly-spacing": [
2,
"never"
],
"array-bracket-spacing": [
2,
"never"
],
"brace-style": [
2,
"1tbs"
],
"consistent-return": 0,
"indent": [
"error",
2,
{
"SwitchCase": 1,
"MemberExpression": "off"
}
],
"no-multiple-empty-lines": [
2,
{
"max": 2
}
],
"no-use-before-define": [
2,
"nofunc"
],
"one-var": [
2,
"never"
],
"quote-props": [
2,
"as-needed"
],
"quotes": [
2,
"single"
],
"keyword-spacing": 2,
"space-before-function-paren": [
2,
{
"anonymous": "always",
"named": "never"
}
],
"space-in-parens": [
2,
"never"
],
"strict": [
2,
"global"
],
"curly": [
2,
"multi-line"
],
"eol-last": 2,
"key-spacing": [
2,
{
"beforeColon": false,
"afterColon": true
}
],
"no-eval": 2,
"no-with": 2,
"space-infix-ops": 2,
"dot-notation": [
2,
{
"allowKeywords": true
}
],
"eqeqeq": 2,
"no-alert": 2,
"no-caller": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-proto": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-unused-expressions": 2,
"yoda": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-undef-init": 2,
"no-console": 1,
"camelcase": [
0,
{
"properties": "never"
}
],
"comma-spacing": 2,
"comma-dangle": 1,
"new-cap": 2,
"new-parens": 2,
"arrow-parens": [2, "always"],
"no-array-constructor": 2,
"array-callback-return": 1,
"no-extra-parens": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-underscore-dangle": 0,
"no-fallthrough": 0,
"semi": 2,
"semi-spacing": [
2,
{
"before": false,
"after": true
}
]
},
"parserOptions": {
"emcaVersion": 6,
"sourceType": "module",
"impliedStrict": true
}
}

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();

2
old/.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules/*
bower_components/*

View File

@@ -16,5 +16,10 @@
- Any new fixes are features should include new or updated [tests](/test).
- Commits follow the [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit), please review and commit accordingly
- Submit your pull requests to the `master` branch, these will normally be merged into a seperate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.
- Submit your pull requests to the `master` branch, these will normally be merged into a separate branch for any finally changes before being merged into `master`.
- Submit any bugs or requests to the issues page in Github.
## Setup
- Clone the repository `git clone`
- Install dependencies `npm install`

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Tyler Stewart
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

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

@@ -113,11 +113,15 @@ function setupState(_args) {
}
function startFtpServer(_state) {
// Remove null/undefined options so they get set to defaults, below
for (const key in _state) {
if (_state[key] === undefined) delete _state[key];
}
function checkLogin(data, resolve, reject) {
const user = _state.credentials[data.username]
if (_state.anonymous || (user && user.password === data.password)) {
return resolve({root: (user && user.root) || _state.root});
const user = _state.credentials[data.username];
if (_state.anonymous || user && user.password === data.password) {
return resolve({root: user && user.root || _state.root});
}
return reject(new errors.GeneralError('Invalid username or password', 401));

View File

@@ -4,9 +4,9 @@ import { EventEmitter } from 'events';
export class FileSystem {
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
readonly connection: FtpConnection;
readonly root: string;
readonly cwd: string;
constructor(connection: FtpConnection, {root, cwd}?: {
root: any;
@@ -42,34 +42,35 @@ export class FileSystem {
}
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
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>
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?: Array<string>,
whitelist?: Array<string>,
file_format?: (stat: Stats) => string | Promise<string> | "ls" | "ep",
log?: any,
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 {
@@ -84,9 +85,9 @@ export class FtpServer extends EventEmitter {
// emit is exported from super class
setupTLS(_tls: boolean): boolean | {
cert: string;
key: string;
ca: string
cert: string;
key: string;
ca: string;
};
setupGreeting(greet: string): string[];
@@ -97,29 +98,29 @@ export class FtpServer extends EventEmitter {
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>
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;
reject: (err?: Error) => void
) => void): this;
on(event: "client-error", listener: (
data: {
connection: FtpConnection,
context: string,
error: Error,
}
) => void): this;
on(event: "client-error", listener: (
data: {
connection: FtpConnection;
context: string;
error: Error;
}
) => void): this;
}
export {FtpServer as FtpSrv};

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,27 @@
const {get} = require('https');
get('https://api.github.com/repos/trs/ftp-srv/contributors', {
headers: {
'User-Agent': 'Chrome'
}
}, (res) => {
let response = '';
res.on('data', (data) => {
response += data;
});
res.on('end', () => {
const contributors = JSON.parse(response)
.filter((contributor) => contributor.type === 'User');
for (const contributor of contributors) {
const url = contributor.html_url;
const username = contributor.login;
const markdown = `- [${username}](${url})\n`;
process.stdout.write(markdown);
}
});
}).on('error', (err) => {
process.stderr.write(err);
});

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

@@ -2,7 +2,11 @@ const PassiveConnector = require('../../connector/passive');
module.exports = {
directive: 'PASV',
handler: function () {
handler: function ({log} = {}) {
if (!this.server.options.pasv_url) {
return this.reply(502);
}
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then((server) => {
@@ -13,6 +17,10 @@ module.exports = {
const portByte2 = port % 256;
return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
})
.catch((err) => {
log.error(err);
return this.reply(425);
});
},
syntax: '{{cmd}}',

View File

@@ -3,7 +3,7 @@ const ActiveConnector = require('../../connector/active');
module.exports = {
directive: 'PORT',
handler: function ({command} = {}) {
handler: function ({log, command} = {}) {
this.connector = new ActiveConnector(this);
const rawConnection = _.get(command, 'arg', '').split(',');
@@ -14,7 +14,11 @@ module.exports = {
const port = portBytes[0] * 256 + portBytes[1];
return this.connector.setupConnection(ip, port)
.then(() => this.reply(200));
.then(() => this.reply(200))
.catch((err) => {
log.error(err);
return this.reply(425);
});
},
syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
description: 'Specifies an address and port to which the server should connect'

View File

@@ -22,7 +22,7 @@ module.exports = {
const destroyConnection = (connection, reject) => (err) => {
if (connection) {
if (connection.writeable) connection.end();
if (connection.writable) connection.end();
connection.destroy(err);
}
reject(err);
@@ -36,7 +36,7 @@ module.exports = {
const socketPromise = new Promise((resolve, reject) => {
this.connector.socket.on('data', (data) => {
if (this.connector.socket) this.connector.socket.pause();
if (stream) {
if (stream && stream.writable) {
stream.write(data, this.transferType, () => this.connector.socket && this.connector.socket.resume());
}
});

View File

@@ -30,8 +30,8 @@ class FtpConnection extends EventEmitter {
this.server.emit('client-error', {connection: this, context: 'commandSocket', error: err});
});
this.commandSocket.on('data', this._handleData.bind(this));
this.commandSocket.on('timeout', (err) => {
this.log.trace(err, 'Client timeout');
this.commandSocket.on('timeout', () => {
this.log.trace('Client timeout');
this.close().catch((e) => this.log.trace(e, 'Client close error'));
});
this.commandSocket.on('close', () => {

View File

@@ -0,0 +1,59 @@
const net = require('net');
const errors = require('../errors');
const MAX_PORT = 65535;
const MAX_PORT_CHECK_ATTEMPT = 5;
function* portNumberGenerator(min, max = MAX_PORT) {
let current = min;
while (true) {
if (current > MAX_PORT || current > max) {
current = min;
}
yield current++;
}
}
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) => {
let attemptCount = 0;
const tryGetPort = () => {
attemptCount++;
if (attemptCount > maxAttempts) {
reject(new errors.ConnectorError('Unable to find valid port'));
return;
}
const {value: port} = nextPortNumber.next();
portCheckServer.removeAllListeners();
portCheckServer.once('error', (err) => {
if (['EADDRINUSE'].includes(err.code)) {
tryGetPort();
} else {
reject(err);
}
});
portCheckServer.once('listening', () => {
portCheckServer.close(() => resolve(port));
});
try {
portCheckServer.listen(port, host);
} catch (err) {
reject(err);
}
};
tryGetPort();
});
}
module.exports = {
getNextPortFactory,
portNumberGenerator
};

View File

@@ -7,7 +7,6 @@ const tls = require('tls');
const EventEmitter = require('events');
const Connection = require('./connection');
const resolveHost = require('./helpers/resolve-host');
const {getNextPortFactory} = require('./helpers/find-port');
class FtpServer extends EventEmitter {
@@ -24,7 +23,8 @@ class FtpServer extends EventEmitter {
blacklist: [],
whitelist: [],
greeting: null,
tls: false
tls: false,
timeout: 0
}, options);
this._greeting = this.setupGreeting(this.options.greeting);
@@ -36,10 +36,15 @@ class FtpServer extends EventEmitter {
this.log = this.options.log;
this.url = nodeUrl.parse(this.options.url);
this.getNextPasvPort = getNextPortFactory(
_.get(this, 'url.hostname'),
_.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 serverConnectionHandler = (socket) => {
socket.setTimeout(this.options.timeout);
let connection = new Connection(this, {log: this.log, socket});
this.connections[connection.id] = connection;
@@ -67,22 +72,21 @@ class FtpServer extends EventEmitter {
}
listen() {
return resolveHost(this.options.pasv_url || this.url.hostname)
.then((pasvUrl) => {
this.options.pasv_url = pasvUrl;
if (!this.options.pasv_url) {
this.log.warn('Passive URL not set. Passive connections not available.');
}
return new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject);
if (err) return reject(err);
this.log.info({
protocol: this.url.protocol.replace(/\W/g, ''),
ip: this.url.hostname,
port: this.url.port
}, 'Listening');
resolve('Listening');
});
return new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.url.port, this.url.hostname, (err) => {
this.server.removeListener('error', reject);
if (err) return reject(err);
this.log.info({
protocol: this.url.protocol.replace(/\W/g, ''),
ip: this.url.hostname,
port: this.url.port
}, 'Listening');
resolve('Listening');
});
});
}

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
};

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