Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(commands): implement RFC 7151 (File Transfer Protocol HOST Command for Virtual Hosts) #169

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,16 +181,36 @@ Set the password for the given `username`.

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`.

### `virtualhost`
```js
ftpServer.on('virtualhost', ({connection, host}, resolve, reject) => { ... })
```

Occurs when client sets hostname using the `HOST` command before authentication. If you attach a listener to this event, you can make the server [RFC 7151](https://tools.ietf
.org/html/rfc7151) compliant. Here you can setup an environment for different virtualhosts. Note that the client may change the host multiple times before a successful authentication, the last successful attempt will be saved in that case.

`connection` [client class object](src/connection.js)
`host` string of hostname from `HOST` command
`resolve` takes an object of arguments:
- `motd`
- Array of lines from the welcome message of the virtualhost.
- The last line will always be _Host accepted_ regardless of this argument.
- `anonymous`
- Set to a boolean to enable/disable anonymous login on this host

`reject` takes an error object

### `login`
```js
ftpServer.on('login', ({connection, username, password}, resolve, reject) => { ... });
ftpServer.on('login', ({connection, username, password, host}, 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
`host` string of hostname from `HOST` command, if issued before login
`resolve` takes an object of arguments:
- `fs`
- Set a custom file system class for this connection to use.
Expand Down
17 changes: 15 additions & 2 deletions ftp-srv.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class FtpConnection extends EventEmitter {
secure: boolean

close (code: number, message: number): Promise<any>
login (username: string, password: string): Promise<any>
login (username: string, password: string, host: string): Promise<any>
reply (options: number | Object, ...letters: Array<any>): Promise<any>

}
Expand Down Expand Up @@ -98,11 +98,24 @@ export class FtpServer extends EventEmitter {

close(): any;

on(event: "virtualhost", listener: (
data: {
connection: FtpConnection,
host: string
},
resolve: (config: {
motd?: Array<string>,
anonymous?: boolean
}) => void,
reject: (err?: Error) => void
) => void): this

on(event: "login", listener: (
data: {
connection: FtpConnection,
username: string,
password: string
password: string,
host: string
},
resolve: (config: {
fs?: FileSystem,
Expand Down
31 changes: 31 additions & 0 deletions src/commands/registration/host.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = {
directive: 'HOST',
handler: function ({log, command} = {}) {
if (this.authenticated) return this.reply(503, 'Already logged in');

const host = command.arg;
if (!host) return this.reply(501, 'Must provide hostname');

const virtualhostListeners = this.server.listeners('virtualhost');
if (!virtualhostListeners || virtualhostListeners.length == 0) {
return this.reply(501, 'This server does not handle virtualhost changes');
} else {
return this.server.emitPromise('virtualhost', {connection: this, host}).then(
({motd = [], anonymous}) => {
this.host = host
if (anonymous !== undefined) this._vh_anonymous = anonymous
this.reply(220, 'Host accepted', ...motd)
},
(err) => {
log.error(err)
return this.reply(err.code || 504, err.message || (!err.code && 'Host rejected'))
}
);
}
},
syntax: '{{cmd}} <hostname>',
description: 'Virtual host',
flags: {
no_auth: true
}
};
2 changes: 1 addition & 1 deletion src/commands/registration/pass.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {

const password = command.arg;
if (!password) return this.reply(501, 'Must provide password');
return this.login(this.username, password)
return this.login(this.username, password, this.host)
.then(() => {
return this.reply(230);
})
Expand Down
8 changes: 5 additions & 3 deletions src/commands/registration/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ module.exports = {
this.username = command.arg;
if (!this.username) return this.reply(501, 'Must provide username');

if (this.server.options.anonymous === true && this.username === 'anonymous' ||
this.username === this.server.options.anonymous) {
return this.login(this.username, '@anonymous')
if (this._vh_anonymous === undefined
? (this.server.options.anonymous === true && this.username === 'anonymous' || this.username === this.server.options.anonymous)
: (this._vh_anonymous === true && this.username === 'anonymous' || this.username === this._vh_anonymous)
) {
return this.login(this.username, '@anonymous', this.host)
.then(() => {
return this.reply(230);
})
Expand Down
1 change: 1 addition & 0 deletions src/commands/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const commands = [
require('./registration/dele'),
require('./registration/feat'),
require('./registration/help'),
require('./registration/host'),
require('./registration/list'),
require('./registration/mdtm'),
require('./registration/mkd'),
Expand Down
4 changes: 2 additions & 2 deletions src/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ class FtpConnection extends EventEmitter {
.then(() => this.commandSocket && this.commandSocket.end());
}

login(username, password) {
login(username, password, host) {
return Promise.try(() => {
const loginListeners = this.server.listeners('login');
if (!loginListeners || !loginListeners.length) {
if (!this.server.options.anonymous) throw new errors.GeneralError('No "login" listener setup', 500);
} else {
return this.server.emitPromise('login', {connection: this, username, password});
return this.server.emitPromise('login', {connection: this, username, password, host});
}
})
.then(({root, cwd, fs, blacklist = [], whitelist = []} = {}) => {
Expand Down