Skip to content

Commit 3e4b648

Browse files
authored
Merge pull request #3 from mailsvb/handlers
add downloadHandler
2 parents 2693f4b + 7f64ff0 commit 3e4b648

2 files changed

Lines changed: 144 additions & 29 deletions

File tree

lib/jsftpd.js

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const FTPdefaults = {
3535
allowAnonymousFolderCreate: false,
3636
allowAnonymousLogin: false,
3737
minDataPort: 1024,
38-
uploadHandler: null
38+
uploadHandler: null,
39+
downloadHandler: null
3940
}
4041

4142
const UserDefaults = {
@@ -572,37 +573,47 @@ class ftpd {
572573
file = path.join(basefolder, relativePath, relativeFile)
573574
}
574575
if (fs.existsSync(file) === true && fs.statSync(file).isFile() === true && main._beginsWith(basefolder, file) === true) {
575-
dataObj.method = function (obj) {
576+
dataObj.method = async function (obj) {
576577
if (obj.dataSocket && obj.cmdSocket && obj.file && obj.relativeFile) {
577-
const streamOpts = {
578-
flags: 'r',
579-
start: retrOffset,
580-
encoding: asciiOn ? 'ascii' : null,
581-
autoClose: true,
582-
emitClose: true
583-
}
584-
retrOffset = 0
585578
asciiOn && obj.dataSocket.setEncoding('ascii')
586-
const stream = fs.createReadStream(obj.file, streamOpts)
587-
stream.on('error', main.ErrorHandler)
588-
stream.on('open', () => {
589-
obj.dataSocket.on('close', () => {
590-
if (!obj.dataSocket.destroyed) {
591-
stream.destroy()
592-
main._writeToSocket(obj.cmdSocket, '426', ' ', `Connection closed. Aborted transfer of "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
593-
}
594-
})
595-
stream.pipe(obj.dataSocket)
596-
})
597-
stream.on('end', () => {
579+
if (obj.handler) {
580+
const data = await obj.handler(username, relativePath, obj.fileName, retrOffset)
581+
retrOffset = 0
582+
obj.dataSocket.write(data)
598583
obj.dataSocket.end()
599584
main._writeToSocket(obj.cmdSocket, '226', ' ', `Successfully transferred "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
600-
})
585+
} else {
586+
const streamOpts = {
587+
flags: 'r',
588+
start: retrOffset,
589+
encoding: asciiOn ? 'ascii' : null,
590+
autoClose: true,
591+
emitClose: true
592+
}
593+
retrOffset = 0
594+
const stream = fs.createReadStream(obj.file, streamOpts)
595+
stream.on('error', main.ErrorHandler)
596+
stream.on('open', () => {
597+
obj.dataSocket.on('close', () => {
598+
if (!obj.dataSocket.destroyed) {
599+
stream.destroy()
600+
main._writeToSocket(obj.cmdSocket, '426', ' ', `Connection closed. Aborted transfer of "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
601+
}
602+
})
603+
stream.pipe(obj.dataSocket)
604+
})
605+
stream.on('end', () => {
606+
obj.dataSocket.end()
607+
main._writeToSocket(obj.cmdSocket, '226', ' ', `Successfully transferred "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
608+
})
609+
}
601610
}
602611
}
603612
dataObj.cmdSocket = socket
604613
dataObj.file = file
614+
dataObj.fileName = path.basename(file)
605615
dataObj.relativeFile = relativeFile
616+
dataObj.handler = main._opt.cnf.downloadHandler
606617
openDataChannel(dataObj)
607618
} else {
608619
main._writeToSocket(socket, '550', ' ', 'File not found', connectionInfo, SocketStateAfterWrite.Open)
@@ -654,8 +665,8 @@ class ftpd {
654665
if (obj.handler) {
655666
const data = []
656667
obj.dataSocket.on('data', (d) => data.push(d))
657-
obj.dataSocket.on('close', () => {
658-
obj.handler(relativePath, obj.fileName, Buffer.concat(data))
668+
obj.dataSocket.on('close', async () => {
669+
await obj.handler(username, relativePath, obj.fileName, Buffer.concat(data), obj.retrOffset)
659670
main._writeToSocket(obj.cmdSocket, '226', ' ', `Successfully transferred "${obj.relativeFile}"`, connectionInfo, SocketStateAfterWrite.Open)
660671
})
661672
} else if (obj.stream) {
@@ -668,6 +679,8 @@ class ftpd {
668679
}
669680
}
670681
if (dataObj.handler) {
682+
dataObj.retrOffset = retrOffset
683+
retrOffset = 0
671684
openDataChannel(dataObj)
672685
} else {
673686
const streamOpts = {

test/jsftpd.test.js

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ const formatPort = (addr, port) => {
1313
}
1414

1515
afterEach(() => {
16-
server.stop()
17-
server.cleanup()
18-
server = null
16+
if (server) {
17+
server.stop()
18+
server.cleanup()
19+
server = null
20+
}
1921
});
2022

2123
test('create ftpd instance without options created with default values', () => {
@@ -46,6 +48,32 @@ test('ftp server can be started on non default ports', () => {
4648
expect(server._tls.address().port).toBe(50990)
4749
});
4850

51+
test('ftp server fails when basefolder does not exist', () => {
52+
try {
53+
server = new ftpd({cnf: {basefolder: '/NOTEXISTING'}})
54+
} catch(err) {
55+
expect(err.message).toMatch('Basefolder must exist')
56+
}
57+
});
58+
59+
test('error message when not logged in', async () => {
60+
server = new ftpd({cnf: {port: 50021}})
61+
expect(server).toBeInstanceOf(ftpd);
62+
server.start()
63+
64+
const promiseSocket = new PromiseSocket(new net.Socket())
65+
const socket = promiseSocket.stream
66+
let content
67+
await socket.connect(50021, 'localhost')
68+
content = await promiseSocket.read();
69+
expect(content.toString().trim()).toBe('220 Welcome')
70+
await promiseSocket.write('REST 0')
71+
content = await promiseSocket.read();
72+
expect(content.toString().trim()).toBe('530 Not logged in')
73+
74+
await promiseSocket.end()
75+
});
76+
4977
test('login as anonymous not allowed by default', async () => {
5078
server = new ftpd({cnf: {port: 50021}})
5179
expect(server).toBeInstanceOf(ftpd);
@@ -1051,10 +1079,12 @@ test('test STOR message with handler', async () => {
10511079
allowLoginWithoutPassword: true,
10521080
}
10531081
]
1054-
const handler = (path, filename, data) => {
1082+
const handler = async (username, path, filename, data, offset) => {
1083+
expect(username).toMatch('john')
10551084
expect(filename).toMatch('mytestfile')
10561085
expect(path).toMatch('/')
10571086
expect(data.toString()).toMatch('SOMETESTCONTENT')
1087+
expect(offset).toBe(0)
10581088
}
10591089
server = new ftpd({cnf: {uploadHandler: handler, port: 50021, user: users}})
10601090
expect(server).toBeInstanceOf(ftpd);
@@ -1162,6 +1192,78 @@ test('test RETR message', async () => {
11621192
await promiseSocket.end()
11631193
});
11641194

1195+
test('test RETR message with handler', async () => {
1196+
const users = [
1197+
{
1198+
username: 'john',
1199+
allowLoginWithoutPassword: true,
1200+
}
1201+
]
1202+
const handler = async (username, path, filename, offset) => {
1203+
expect(username).toMatch('john')
1204+
expect(filename).toMatch('mytestfile')
1205+
expect(path).toMatch('/')
1206+
expect(offset).toBe(0)
1207+
return 'SOMETESTCONTENT'
1208+
}
1209+
server = new ftpd({cnf: {downloadHandler: handler, port: 50021, user: users}})
1210+
expect(server).toBeInstanceOf(ftpd);
1211+
server.start()
1212+
1213+
let content
1214+
1215+
let promiseSocket = new PromiseSocket(new net.Socket())
1216+
let socket = promiseSocket.stream
1217+
await socket.connect(50021, 'localhost')
1218+
content = await promiseSocket.read();
1219+
expect(content.toString().trim()).toBe('220 Welcome')
1220+
1221+
await promiseSocket.write('USER john')
1222+
content = await promiseSocket.read();
1223+
expect(content.toString().trim()).toBe('232 User logged in')
1224+
1225+
await promiseSocket.write('EPSV')
1226+
content = await promiseSocket.read();
1227+
expect(content.toString().trim()).toBe('229 Entering extended passive mode (|||1024|)')
1228+
1229+
let promiseDataSocket = new PromiseSocket(new net.Socket())
1230+
let dataSocket = promiseDataSocket.stream
1231+
await dataSocket.connect(1024, 'localhost')
1232+
1233+
await promiseSocket.write('STOR mytestfile')
1234+
content = await promiseSocket.read();
1235+
expect(content.toString().trim()).toBe('150 Opening data channel')
1236+
1237+
await promiseDataSocket.write('SOMETESTCONTENT');
1238+
dataSocket.end()
1239+
await promiseDataSocket.end()
1240+
1241+
content = await promiseSocket.read();
1242+
expect(content.toString().trim()).toBe('226 Successfully transferred "mytestfile"')
1243+
1244+
await promiseSocket.write('EPSV')
1245+
content = await promiseSocket.read();
1246+
expect(content.toString().trim()).toBe('229 Entering extended passive mode (|||1024|)')
1247+
1248+
promiseDataSocket = new PromiseSocket(new net.Socket())
1249+
dataSocket = promiseDataSocket.stream
1250+
await dataSocket.connect(1024, 'localhost')
1251+
1252+
await promiseSocket.write('RETR /someotherfile')
1253+
content = await promiseSocket.read();
1254+
expect(content.toString().trim()).toBe('550 File not found')
1255+
1256+
await promiseSocket.write('RETR mytestfile')
1257+
// content = await promiseSocket.read();
1258+
// expect(content.toString().trim()).toBe('150 Opening data channel')
1259+
1260+
content = await promiseDataSocket.read();
1261+
expect(content.toString().trim()).toMatch('SOMETESTCONTENT')
1262+
await promiseDataSocket.end()
1263+
1264+
await promiseSocket.end()
1265+
});
1266+
11651267
test('test MFMT message', async () => {
11661268
const users = [
11671269
{

0 commit comments

Comments
 (0)