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
Changes from 1 commit
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
Next Next commit
feat: implement RFC 7151 (FTP HOST Command for Virtual Hosts)
Issue for feature request: #114
  • Loading branch information
lezsakdomi committed Jul 24, 2019
commit 360ac0f12832949d83dc4a8b4875f1e6ee2cf649
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -181,16 +181,34 @@ 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.

`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.
16 changes: 14 additions & 2 deletions ftp-srv.d.ts
Original file line number Diff line number Diff line change
@@ -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>

}
@@ -98,11 +98,23 @@ export class FtpServer extends EventEmitter {

close(): any;

on(event: "virtualhost", listener: (
data: {
connection: FtpConnection,
host: string
},
resolve: (config: {
motd?: Array<string>
}) => 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,
30 changes: 30 additions & 0 deletions src/commands/registration/host.js
Original file line number Diff line number Diff line change
@@ -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}} <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
@@ -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);
})
2 changes: 1 addition & 1 deletion src/commands/registration/user.js
Original file line number Diff line number Diff line change
@@ -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);
})
1 change: 1 addition & 0 deletions src/commands/registry.js
Original file line number Diff line number Diff line change
@@ -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'),
4 changes: 2 additions & 2 deletions src/connection.js
Original file line number Diff line number Diff line change
@@ -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 = []} = {}) => {