Skip to content

Commit

Permalink
Merge pull request #14 from trs/explicit-tls-support
Browse files Browse the repository at this point in the history
feat: add explicit TLS support with AUTH
  • Loading branch information
trs authored May 16, 2017
2 parents 8394714 + 2cc5d54 commit 88f02cd
Showing 46 changed files with 507 additions and 156 deletions.
56 changes: 35 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# ftp-srv [![npm version](https://badge.fury.io/js/ftp-srv.svg)](https://badge.fury.io/js/ftp-srv) [![Build Status](https://travis-ci.org/stewarttylerr/ftp-srv.svg?branch=master)](https://travis-ci.org/stewarttylerr/ftp-srv) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
![ftp-srv](logo.png)

[![npm version](https://badge.fury.io/js/ftp-srv.svg)](https://badge.fury.io/js/ftp-srv) [![Build Status](https://travis-ci.org/trs/ftp-srv.svg?branch=master)](https://travis-ci.org/trs/ftp-srv) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)

<!--[RM_DESCRIPTION]-->
> Modern, extensible FTP Server
<!--[]-->

---

- [Overview](#overview)
- [Features](#features)
- [Install](#install)
@@ -22,7 +26,7 @@
## Features
- Extensible [file systems](#file-system) per connection
- Passive and active transfers
- Implicit TLS connections
- [Explicit](https://en.wikipedia.org/wiki/FTPS#Explicit) & [Implicit](https://en.wikipedia.org/wiki/FTPS#Implicit) TLS connections

## Install
`npm install ftp-srv --save`
@@ -45,45 +49,48 @@ ftpServer.listen()
## API

### `new FtpSrv(url, [{options}])`
#### `url`
#### 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
- `ftps` Implicit FTP over TLS

_Note:_ The hostname must be the external IP address to accept external connections. Setting the hostname to `0.0.0.0` will automatically set the external IP.
__Default:__ `"ftp://127.0.0.1:21"`

#### `options`
#### options

- ##### `pasv_range`
##### `pasv_range`
A starting port (eg `8000`) or a range (eg `"8000-9000"`) to accept passive connections.
This range is then queried for an available port to use when required.
__Default:__ `22`

- ##### `greeting`
##### `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`.
__Default:__ `{}`
##### `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 call the event login after `USER`, not requiring a password from the user.
##### `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`
##### `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`
##### `whitelist`
Array of commands that are only allowed.
Response code `502` is sent to clients sending any other command.
__Default:__ `[]`

- ##### `file_format`
##### `file_format`
Sets the format to use for file stat queries such as `LIST`.
__Default:__ `"ls"`
__Allowable values:__
@@ -92,7 +99,7 @@ __Allowable values:__
- `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`
##### `log`
A [bunyan logger](https://github.com/trentm/node-bunyan) instance. Created by default.

## Events
@@ -104,7 +111,11 @@ The `FtpSvr` class extends the [node net.Server](https://nodejs.org/api/net.html
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.
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.
@@ -127,11 +138,15 @@ Occurs when a client is attempting to login. Here you can resolve the login requ
on('client-error', {connection, context, error}) => { ... }
```

Occurs when an error occurs in the client connection.
Occurs when an error arises in the client connection.

`connection` [client class object](src/connection.js)
`context` string of where the error occured
`error` error object

## Supported Commands

See [commands](src/commands) for a list of all implemented FTP commands.
See the [command registry](src/commands/registration) for a list of all implemented FTP commands.

## File System
The default file system can be overwritten to use your own implementation.
@@ -150,8 +165,7 @@ Returns a file stat object of file or directory
__Used in:__ `STAT`, `SIZE`, `RNFR`, `MDTM`

#### [`list(path)`](src/fs.js#L39)
Returns array of file and directory stat objects

Returns array of file and directory stat objects
__Used in:__ `LIST`, `STAT`

#### [`chdir(path)`](src/fs.js#L56)
43 changes: 43 additions & 0 deletions logo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!doctype html>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
<style>
body {
padding: 0px;
}
h1 {
display: flex;
margin: 0;
padding: 0;
font-size: 73px;
font-family: 'Source Code Pro', monospace;
font-weight: bold;
line-height: 0.9em;
letter-spacing: -3px;
color: #444;
text-shadow:
1px 0px 1px #ccc, 0px 1px 1px #eee,
2px 1px 1px #ccc, 1px 2px 1px #eee,
3px 2px 1px #ccc, 2px 3px 1px #eee,
4px 3px 1px #ccc, 3px 4px 1px #eee,
5px 4px 1px #ccc, 4px 5px 1px #eee,
6px 5px 1px #ccc, 5px 6px 1px #eee,
7px 6px 1px #ccc;
-webkit-font-smoothing: antialiased;
}
h1 > i {
font-size: 42px !important;
color: #03A9F4;
align-self: center;
padding-top: 15px;
}
</style>
</head>

<body>
<h1><i class="material-icons">markunread_mailbox</i>ftp-srv</h1>
</body>
</html>
Binary file added logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
"main": "src/index.js",
"repository": {
"type": "git",
"url": "https://github.com/stewarttylerr/ftp-srv"
"url": "https://github.com/trs/ftp-srv"
},
"scripts": {
"pre-release": "npm-run-all verify test:coverage build ",
2 changes: 1 addition & 1 deletion src/commands/registration/appe.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,6 @@ module.exports = {
handler: function (args) {
return stor.call(this, args);
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Append to a file'
};
28 changes: 21 additions & 7 deletions src/commands/registration/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const _ = require('lodash');
const tls = require('tls');

module.exports = {
directive: 'AUTH',
@@ -7,21 +8,34 @@ module.exports = {

switch (method) {
case 'TLS': return handleTLS.call(this);
case 'SSL': return handleSSL.call(this);
default: return this.reply(504);
}
},
syntax: '{{cmd}} [type]',
syntax: '{{cmd}} <type>',
description: 'Set authentication mechanism',
flags: {
no_auth: true
no_auth: true,
feat: 'AUTH TLS'
}
};

function handleTLS() {
return this.reply(504);
}
if (!this.server._tls) return this.reply(504);

function handleSSL() {
return this.reply(504);
return this.reply(234)
.then(() => {
const secureContext = tls.createSecureContext(this.server._tls);
const secureSocket = new tls.TLSSocket(this.commandSocket, {
isServer: true,
secureContext
});
['data', 'timeout', 'end', 'close', 'drain', 'error'].forEach(event => {
function forwardEvent() {
this.emit.apply(this, arguments);
}
secureSocket.on(event, forwardEvent.bind(this.commandSocket, event));
});
this.commandSocket = secureSocket;
this.secure = true;
});
}
2 changes: 1 addition & 1 deletion src/commands/registration/cwd.js
Original file line number Diff line number Diff line change
@@ -17,6 +17,6 @@ module.exports = {
return this.reply(550, err.message);
});
},
syntax: '{{cmd}}[path]',
syntax: '{{cmd}} <path>',
description: 'Change working directory'
};
2 changes: 1 addition & 1 deletion src/commands/registration/dele.js
Original file line number Diff line number Diff line change
@@ -15,6 +15,6 @@ module.exports = {
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Delete file'
};
22 changes: 22 additions & 0 deletions src/commands/registration/eprt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const _ = require('lodash');
const ActiveConnector = require('../../connector/active');

const FAMILY = {
1: 4,
2: 6
};

module.exports = {
directive: 'EPRT',
handler: function ({command} = {}) {
this.connector = new ActiveConnector(this);
const [protocol, ip, port] = _.compact(command.arg.split('|'));
const family = FAMILY[protocol];
if (!family) return this.reply(502, 'Unknown network protocol');

return this.connector.setupConnection(ip, port, family)
.then(() => this.reply(200));
},
syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect'
};
16 changes: 16 additions & 0 deletions src/commands/registration/epsv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const PassiveConnector = require('../../connector/passive');

module.exports = {
directive: 'EPSV',
handler: function () {
this.connector = new PassiveConnector(this);
return this.connector.setupServer()
.then(server => {
const {port} = server.address();

return this.reply(229, `EPSV OK (|||${port}|)`);
});
},
syntax: '{{cmd}} [<protocol>]',
description: 'Initiate passive mode'
};
4 changes: 3 additions & 1 deletion src/commands/registration/feat.js
Original file line number Diff line number Diff line change
@@ -11,7 +11,9 @@ module.exports = {
return feats;
}, [])
.map(feat => ` ${feat}`);
return this.reply(211, 'Extensions supported', ...features, 'END');
return features.length
? this.reply(211, 'Extensions supported', ...features, 'End')
: this.reply(211, 'No features');
},
syntax: '{{cmd}}',
description: 'Get the feature list implemented by the server',
2 changes: 1 addition & 1 deletion src/commands/registration/help.js
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ module.exports = {
return this.reply(211, 'Supported commands:', ...supportedCommands, 'Use "HELP [command]" for syntax help.');
}
},
syntax: '{{cmd}} [command(optional)]',
syntax: '{{cmd}} [<command>]',
description: 'Returns usage documentation on a command if specified, else a general help document is returned',
flags: {
no_auth: true
2 changes: 1 addition & 1 deletion src/commands/registration/list.js
Original file line number Diff line number Diff line change
@@ -55,6 +55,6 @@ module.exports = {
this.commandSocket.resume();
});
},
syntax: '{{cmd}} [path(optional)]',
syntax: '{{cmd}} [<path>]',
description: 'Returns information of a file or directory if specified, else information of the current working directory is returned'
};
2 changes: 1 addition & 1 deletion src/commands/registration/mdtm.js
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ module.exports = {
return this.reply(550, err.message);
});
},
syntax: '{{cmd}} [path]',
syntax: '{{cmd}} <path>',
description: 'Return the last-modified time of a specified file',
flags: {
feat: 'MDTM'
2 changes: 1 addition & 1 deletion src/commands/registration/mkd.js
Original file line number Diff line number Diff line change
@@ -17,6 +17,6 @@ module.exports = {
return this.reply(550, err.message);
});
},
syntax: '{{cmd}}[path]',
syntax: '{{cmd}} <path>',
description: 'Make directory'
};
2 changes: 1 addition & 1 deletion src/commands/registration/mode.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ module.exports = {
handler: function ({command} = {}) {
return this.reply(/^S$/i.test(command.arg) ? 200 : 504);
},
syntax: '{{cmd}} [mode]',
syntax: '{{cmd}} <mode>',
description: 'Sets the transfer mode (Stream, Block, or Compressed)',
flags: {
obsolete: true
2 changes: 1 addition & 1 deletion src/commands/registration/nlst.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,6 @@ module.exports = {
handler: function (args) {
return list.call(this, args);
},
syntax: '{{cmd}} [path(optional)]',
syntax: '{{cmd}} [<path>]',
description: 'Returns a list of file names in a specified directory'
};
8 changes: 3 additions & 5 deletions src/commands/registration/pass.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
const _ = require('lodash');

module.exports = {
directive: 'PASS',
handler: function ({log, command} = {}) {
if (!this.username) return this.reply(503);
if (this.username && this.authenticated &&
_.get(this, 'server.options.anonymous') === true) return this.reply(230);
if (this.authenticated) return this.reply(202);

// 332 : require account name (ACCT)

const password = command.arg;
if (!password) return this.reply(501, 'Must provide password');
return this.login(this.username, password)
.then(() => {
return this.reply(230);
@@ -19,7 +17,7 @@ module.exports = {
return this.reply(530, err.message || 'Authentication failed');
});
},
syntax: '{{cmd}} [password]',
syntax: '{{cmd}} <password>',
description: 'Authentication password',
flags: {
no_auth: true
Loading

0 comments on commit 88f02cd

Please sign in to comment.