Skip to content

Commit

Permalink
Merge pull request #9 from mailsvb/handler
Browse files Browse the repository at this point in the history
additional handler and documentation updates
  • Loading branch information
mailsvb authored Nov 8, 2021
2 parents 8266763 + 70c0300 commit e37593f
Show file tree
Hide file tree
Showing 13 changed files with 2,996 additions and 2,145 deletions.
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

0 comments on commit e37593f

Please sign in to comment.