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

additional handler and documentation updates #9

Merged
merged 5 commits into from
Nov 8, 2021
Merged
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
10 changes: 5 additions & 5 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Log events will provide debug information of a certain level. All events can be
### log

**Name: `log`**\
**Attribute: `string`
**Attribute: `string`**

Contains the plain FTP messages exchanged between client and server. Executing function needs to take one arugment of type `string`

Expand All @@ -28,7 +28,7 @@ server.on('log', (msg) => console.log(msg))
### debug

**Name: `debug`**\
**Attribute: `string`
**Attribute: `string`**

Contains detailed debug information. Executing function needs to take one arugment of type `string`

Expand All @@ -43,7 +43,7 @@ Other events for specific occasions.
### listen

**Name: `listen`**\
**Attribute: `object`
**Attribute: `object`**

This event will fire when the FTP server is listening on defined ports. Executing function needs to take one arugment of type `object`. The `object` contains the following information:

Expand All @@ -58,7 +58,7 @@ server.on('listen', (data) => console.log(`${data.protocol} on ${data.address}:$
### login

**Name: `login`**\
**Attribute: `object`
**Attribute: `object`**

This event will fire when a user logs into the server. Executing function needs to take one arugment of type `object`. The `object` contains the following information:

Expand All @@ -73,7 +73,7 @@ server.on('login', (data) => console.log(`${data.username} logged in from ${data
### logoff

**Name: `logoff`**\
**Attribute: `object`
**Attribute: `object`**

This event will fire when a user logs off from the server. Executing function needs to take one arugment of type `object`. The `object` contains the following information:

Expand Down
95 changes: 95 additions & 0 deletions docs/handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Handler

jsftpd can use handler functions instead of reading/writing to the file system for several FTP commands.

```{code-block} javascript
const handler = {
upload: async function (...) {},
download: async function (...) {},
list: async function (...) {},
rename: async function (...) {}
}
const server = new ftpd({hdl: handler})
```

The following FTP commands are covered by the handlers

* **STOR**: Uploading files to the FTP server
* **RETR**: Downloading files from the FTP server
* **LIST**: Listing directory contents on the FTP server
* **MLSD**: Listing directory contents on the FTP server
* **RNFR**: Renaming files on the FTP server
* **RNTO**: Renaming files on the FTP server

### upload

**Name: `upload`**\
**Returns: `boolean`**

The upload function takes 5 arguments when being called from jsftpd. It must return `true` on success or `false` on any error handling the file upload.

**username: `string` the user who has uploaded the file**\
**path: `string` relative path on the FTP server where the file is stored**\
**fileName: `string` the name of the file that is stored**\
**data: `Buffer` the file content that is stored**\
**offset: `number` the offset of the data received**

```{code-block} javascript
async upload (username, path, fileName, data, offset) {
...
}
```

### download

**Name: `upload`**\
**Returns: `Buffer`**

The download function takes 4 arguments when being called from jsftpd. It must return the file content as a `Buffer`.

**username: `string` the user who has uploaded the file**\
**path: `string` relative path on the FTP server where the file is stored**\
**fileName: `string` the name of the file that is stored**\
**data: `Buffer` the file content that is stored**\
**offset: `number` the offset of the data received**

```{code-block} javascript
async upload (username, path, fileName, data, offset) {
...
}
```

### list

**Name: `list`**\
**Returns: `string`**

The list function takes 3 arguments when being called from jsftpd. It must return the content of the specified directory as a `string`.

**username: `string` the current user**\
**path: `string` relative path on the FTP server**\
**format: `string` the format of the list reply (MLSD | LIST)**

```{code-block} javascript
async upload (username, path, format) {
...
}
```

### rename

**Name: `rename`**\
**Returns: `boolean`**

The rename function takes 4 arguments when being called from jsftpd. It must return `true` on success or `false` on any error handling the file rename.

**username: `string` the current user**\
**path: `string` relative path on the FTP server**\
**fromName: `string` the current file that needs to be renamed**
**newName: `string` the new name of the file**

```{code-block} javascript
async upload (username, path, fromName, newName) {
...
}
```
91 changes: 57 additions & 34 deletions lib/jsftpd.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const FTPdefaults = {
const HandlerDefaults = {
upload: async function () {},
download: async function () {},
list: async function () {}
list: async function () {},
rename: async function () {}
}

const UserDefaults = {
Expand Down Expand Up @@ -478,13 +479,13 @@ class ftpd {
*/
const LIST = function (cmd, arg) {
dataObj.method = async function (obj) {
if (obj.dataSocket && obj.cmdSocket && obj.absolutePath) {
if (obj.dataSocket && obj.cmdSocket && obj.absolutePath && obj.relativePath) {
if (asciiOn) {
obj.dataSocket.setEncoding('ascii')
}
let listData = ''
if (main._useHdl === true) {
const data = await main._opt.hdl.list(username, obj.absolutePath)
const data = await main._opt.hdl.list(username, obj.relativePath, obj.MLSD)
data && (listData = data)
} else {
const read = fs.readdirSync(obj.absolutePath)
Expand Down Expand Up @@ -515,13 +516,14 @@ class ftpd {
listData = '\r\n'
}
main.DebugHandler(`${connectionInfo} LIST response on data channel\r\n${listData}`)
obj.dataSocket.end(listData)
obj.dataSocket.end(Buffer.from(listData))
main._writeToSocket(obj.cmdSocket, '226', ' ', `Successfully transferred "${relativePath}"`, connectionInfo, SocketStateAfterWrite.Open)
}
}
dataObj.MLSD = (cmd === 'MLSD')
dataObj.cmdSocket = socket
dataObj.absolutePath = absolutePath
dataObj.relativePath = relativePath
openDataChannel(dataObj)
}

Expand Down Expand Up @@ -550,15 +552,19 @@ class ftpd {
pasv = false
actv = false
main._getDataPort((port) => {
ftpData.listen(port, () => {
main.DebugHandler(`${connectionInfo} listening on ${ftpData.address().port} for data connection`)
dataObj = {}
pasv = true
const p1 = (ftpData.address().port) / 256 | 0
const p2 = (ftpData.address().port) % 256
const pasvData = util.format('Entering passive mode (%s,%d,%d)', localAddr.split('.').join(','), p1, p2)
main._writeToSocket(socket, '227', ' ', `${pasvData}`, connectionInfo, SocketStateAfterWrite.Open)
})
if (port > 0) {
ftpData.listen(port, () => {
main.DebugHandler(`${connectionInfo} listening on ${ftpData.address().port} for data connection`)
dataObj = {}
pasv = true
const p1 = (ftpData.address().port) / 256 | 0
const p2 = (ftpData.address().port) % 256
const pasvData = util.format('Entering passive mode (%s,%d,%d)', localAddr.split('.').join(','), p1, p2)
main._writeToSocket(socket, '227', ' ', `${pasvData}`, connectionInfo, SocketStateAfterWrite.Open)
})
} else {
main._writeToSocket(socket, '501', ' ', 'Passive command failed', connectionInfo, SocketStateAfterWrite.Open)
}
})
}

Expand Down Expand Up @@ -587,13 +593,17 @@ class ftpd {
pasv = false
actv = false
main._getDataPort((port) => {
ftpData.listen(port, () => {
main.DebugHandler(`${connectionInfo} listening on ${ftpData.address().port} for data connection`)
dataObj = {}
pasv = true
const pasvData = util.format('Entering extended passive mode (|||%d|)', ftpData.address().port)
main._writeToSocket(socket, '229', ' ', `${pasvData}`, connectionInfo, SocketStateAfterWrite.Open)
})
if (port > 0) {
ftpData.listen(port, () => {
main.DebugHandler(`${connectionInfo} listening on ${ftpData.address().port} for data connection`)
dataObj = {}
pasv = true
const pasvData = util.format('Entering extended passive mode (|||%d|)', ftpData.address().port)
main._writeToSocket(socket, '229', ' ', `${pasvData}`, connectionInfo, SocketStateAfterWrite.Open)
})
} else {
main._writeToSocket(socket, '501', ' ', 'Extended passive command failed', connectionInfo, SocketStateAfterWrite.Open)
}
})
}

Expand Down Expand Up @@ -621,9 +631,13 @@ class ftpd {
if (main._useHdl) {
const data = await main._opt.hdl.download(username, relativePath, obj.fileName, retrOffset)
retrOffset = 0
obj.dataSocket.write(data || '')
obj.dataSocket.end()
main._writeToSocket(obj.cmdSocket, '226', ' ', `Successfully transferred "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
if (Buffer.isBuffer(data)) {
obj.dataSocket.end(data)
main._writeToSocket(obj.cmdSocket, '226', ' ', `Successfully transferred "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
} else {
obj.dataSocket.end()
main._writeToSocket(obj.cmdSocket, '550', ' ', `Transfer failed "${relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
}
} else {
const streamOpts = {
flags: 'r',
Expand Down Expand Up @@ -704,8 +718,12 @@ class ftpd {
const data = []
obj.dataSocket.on('data', (d) => data.push(d))
obj.dataSocket.on('close', async () => {
await main._opt.hdl.upload(username, relativePath, obj.fileName, Buffer.concat(data), obj.retrOffset)
main._writeToSocket(obj.cmdSocket, '226', ' ', `Successfully transferred "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
const success = await main._opt.hdl.upload(username, relativePath, obj.fileName, Buffer.concat(data), obj.retrOffset)
if (success === true) {
main._writeToSocket(obj.cmdSocket, '226', ' ', `Successfully transferred "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
} else {
main._writeToSocket(obj.cmdSocket, '550', ' ', `Transfer failed "${relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
}
})
} else if (obj.stream) {
obj.dataSocket.on('close', () => {
Expand Down Expand Up @@ -778,11 +796,9 @@ class ftpd {
} else {
file = path.join(basefolder, relativePath, relativeFile)
}
if (main._useHdl === false && fs.existsSync(file) === true && fs.statSync(file).isFile() === true && main._beginsWith(basefolder, file) === true) {
if (((fs.existsSync(file) === true && fs.statSync(file).isFile() === true) || main._useHdl === true) && main._beginsWith(basefolder, file) === true) {
renameFrom = file
main._writeToSocket(socket, '350', ' ', 'File exists', connectionInfo, SocketStateAfterWrite.Open)
} else if (main._useHdl === true) {
main._writeToSocket(socket, '350', ' ', 'File exists', connectionInfo, SocketStateAfterWrite.Open)
} else {
main._writeToSocket(socket, '550', ' ', 'File does not exist', connectionInfo, SocketStateAfterWrite.Open)
}
Expand All @@ -791,7 +807,7 @@ class ftpd {
/*
* RNTO
*/
const RNTO = function (cmd, arg) {
const RNTO = async function (cmd, arg) {
const relativeFile = arg
let file
if (relativeFile.charAt(0) === '/') {
Expand All @@ -804,7 +820,13 @@ class ftpd {
renameFrom = ''
main._writeToSocket(socket, '250', ' ', 'File renamed successfully', connectionInfo, SocketStateAfterWrite.Open)
} else if (main._useHdl === true) {
main._writeToSocket(socket, '250', ' ', 'File renamed successfully', connectionInfo, SocketStateAfterWrite.Open)
const success = await main._opt.hdl.rename(username, relativePath, path.basename(renameFrom), relativeFile)
renameFrom = ''
if (success === true) {
main._writeToSocket(socket, '250', ' ', 'File renamed successfully', connectionInfo, SocketStateAfterWrite.Open)
} else {
main._writeToSocket(socket, '550', ' ', 'File rename failed', connectionInfo, SocketStateAfterWrite.Open)
}
} else {
main._writeToSocket(socket, '550', ' ', 'File already exists', connectionInfo, SocketStateAfterWrite.Open)
}
Expand Down Expand Up @@ -967,7 +989,7 @@ class ftpd {
})
}
dataChannel.on('error', main.ErrorHandler)
dataChannel.maxConnections = main._opt.cnf.maxConnections
dataChannel.maxConnections = 1
return dataChannel
}

Expand Down Expand Up @@ -1018,11 +1040,12 @@ class ftpd {
}

_getDataPort (resolve) {
if (this._opt.cnf.minDataPort > 0 && this._opt.cnf.minDataPort < 65535) {
const config = this._opt.cnf
if (config.minDataPort > 0 && config.minDataPort < 65535) {
const testPort = function (port) {
const server = net.createServer()
server.once('error', function (_err) {
if (port > (this._opt.cnf.minDataPort + this._opt.cnf.maxConnections)) {
if (port >= (config.minDataPort + config.maxConnections)) {
resolve(0)
} else {
testPort(port + 1)
Expand All @@ -1036,7 +1059,7 @@ class ftpd {
})
server.listen(port)
}
testPort(this._opt.cnf.minDataPort)
testPort(config.minDataPort)
} else {
resolve(0)
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"scripts": {
"lint:js": "eslint lib/ --ext .js",
"install-dev": "npm install --save-dev && husky install",
"test": "jest"
"test": "jest --runInBand"
},
"license": "MIT",
"engines": {
Expand Down
Loading