From 360ac0f12832949d83dc4a8b4875f1e6ee2cf649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domonkos=20Lezs=C3=A1k?= Date: Wed, 24 Jul 2019 22:03:01 +0200 Subject: [PATCH 1/2] feat: implement RFC 7151 (FTP HOST Command for Virtual Hosts) Issue for feature request: https://github.com/trs/ftp-srv/issues/114 --- README.md | 20 +++++++++++++++++++- ftp-srv.d.ts | 16 ++++++++++++++-- src/commands/registration/host.js | 30 ++++++++++++++++++++++++++++++ src/commands/registration/pass.js | 2 +- src/commands/registration/user.js | 2 +- src/commands/registry.js | 1 + src/connection.js | 4 ++-- 7 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 src/commands/registration/host.js diff --git a/README.md b/README.md index d5942d7b..d2f2207b 100644 --- a/README.md +++ b/README.md @@ -181,9 +181,26 @@ 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. + +`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. @@ -191,6 +208,7 @@ Occurs when a client is attempting to login. Here you can resolve the login requ `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. diff --git a/ftp-srv.d.ts b/ftp-srv.d.ts index 6b47c4c5..caef08c8 100644 --- a/ftp-srv.d.ts +++ b/ftp-srv.d.ts @@ -53,7 +53,7 @@ export class FtpConnection extends EventEmitter { secure: boolean close (code: number, message: number): Promise - login (username: string, password: string): Promise + login (username: string, password: string, host: string): Promise reply (options: number | Object, ...letters: Array): Promise } @@ -98,11 +98,23 @@ export class FtpServer extends EventEmitter { close(): any; + on(event: "virtualhost", listener: ( + data: { + connection: FtpConnection, + host: string + }, + resolve: (config: { + motd?: Array + }) => 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, diff --git a/src/commands/registration/host.js b/src/commands/registration/host.js new file mode 100644 index 00000000..f8ab7a6e --- /dev/null +++ b/src/commands/registration/host.js @@ -0,0 +1,30 @@ +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 = []}) => { + this.host = host + this.reply(220, 'Host accepted', ...motd) + }, + (err) => { + log.error(err) + return this.reply(err.code || 504, err.message || (!err.code && 'Host rejected')) + } + ); + } + }, + syntax: '{{cmd}} ', + description: 'Virtual host', + flags: { + no_auth: true + } +}; diff --git a/src/commands/registration/pass.js b/src/commands/registration/pass.js index 09743112..bf8156ba 100644 --- a/src/commands/registration/pass.js +++ b/src/commands/registration/pass.js @@ -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); }) diff --git a/src/commands/registration/user.js b/src/commands/registration/user.js index 35142b51..46682095 100644 --- a/src/commands/registration/user.js +++ b/src/commands/registration/user.js @@ -9,7 +9,7 @@ module.exports = { if (this.server.options.anonymous === true && this.username === 'anonymous' || this.username === this.server.options.anonymous) { - return this.login(this.username, '@anonymous') + return this.login(this.username, '@anonymous', this.host) .then(() => { return this.reply(230); }) diff --git a/src/commands/registry.js b/src/commands/registry.js index f5c1f6d3..66646512 100644 --- a/src/commands/registry.js +++ b/src/commands/registry.js @@ -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'), diff --git a/src/connection.js b/src/connection.js index bb14a50e..b36ebb83 100644 --- a/src/connection.js +++ b/src/connection.js @@ -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 = []} = {}) => { From 289c6a2ec837345267727ad71bc8607806952e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domonkos=20Lezs=C3=A1k?= Date: Wed, 24 Jul 2019 22:35:19 +0200 Subject: [PATCH 2/2] feat(virtualhost): override anonymous login per vhost This commits lets you pass an `anonymous` argument at `virtualhost` event accept --- README.md | 2 ++ ftp-srv.d.ts | 3 ++- src/commands/registration/host.js | 3 ++- src/commands/registration/user.js | 6 ++++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d2f2207b..47539ccd 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,8 @@ Occurs when client sets hostname using the `HOST` command before authentication. - `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 diff --git a/ftp-srv.d.ts b/ftp-srv.d.ts index caef08c8..0d6a21c2 100644 --- a/ftp-srv.d.ts +++ b/ftp-srv.d.ts @@ -104,7 +104,8 @@ export class FtpServer extends EventEmitter { host: string }, resolve: (config: { - motd?: Array + motd?: Array, + anonymous?: boolean }) => void, reject: (err?: Error) => void ) => void): this diff --git a/src/commands/registration/host.js b/src/commands/registration/host.js index f8ab7a6e..655415cb 100644 --- a/src/commands/registration/host.js +++ b/src/commands/registration/host.js @@ -11,8 +11,9 @@ module.exports = { return this.reply(501, 'This server does not handle virtualhost changes'); } else { return this.server.emitPromise('virtualhost', {connection: this, host}).then( - ({motd = []}) => { + ({motd = [], anonymous}) => { this.host = host + if (anonymous !== undefined) this._vh_anonymous = anonymous this.reply(220, 'Host accepted', ...motd) }, (err) => { diff --git a/src/commands/registration/user.js b/src/commands/registration/user.js index 46682095..09f6e85b 100644 --- a/src/commands/registration/user.js +++ b/src/commands/registration/user.js @@ -7,8 +7,10 @@ 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) { + 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);