UNPKG

4.28 kBJavaScriptView Raw
1'use strict';
2
3const { timingSafeEqual } = require('crypto');
4const { constants, readFileSync } = require('fs');
5
6const { Server, sftp: { OPEN_MODE, STATUS_CODE } } = require('ssh2');
7
8const allowedUser = Buffer.from('foo');
9const allowedPassword = Buffer.from('bar');
10
11function checkValue(input, allowed) {
12 const autoReject = (input.length !== allowed.length);
13 if (autoReject) {
14 // Prevent leaking length information by always making a comparison with the
15 // same input when lengths don't match what we expect ...
16 allowed = input;
17 }
18 const isMatch = timingSafeEqual(input, allowed);
19 return (!autoReject && isMatch);
20}
21
22new Server({
23 hostKeys: [readFileSync('host.key')]
24}, (client) => {
25 console.log('Client connected!');
26
27 client.on('authentication', (ctx) => {
28 let allowed = true;
29 if (!checkValue(Buffer.from(ctx.username), allowedUser))
30 allowed = false;
31
32 switch (ctx.method) {
33 case 'password':
34 if (!checkValue(Buffer.from(ctx.password), allowedPassword))
35 return ctx.reject();
36 break;
37 default:
38 return ctx.reject();
39 }
40
41 if (allowed)
42 ctx.accept();
43 else
44 ctx.reject();
45 }).on('ready', () => {
46 console.log('Client authenticated!');
47
48 client.on('session', (accept, reject) => {
49 const session = accept();
50 session.on('sftp', (accept, reject) => {
51 console.log('Client SFTP session');
52
53 const openFiles = new Map();
54 let handleCount = 0;
55 const sftp = accept();
56 sftp.on('OPEN', (reqid, filename, flags, attrs) => {
57 // Only allow opening /tmp/foo.txt for writing
58 if (filename !== '/tmp/foo.txt' || !(flags & OPEN_MODE.READ))
59 return sftp.status(reqid, STATUS_CODE.FAILURE);
60
61 // Create a fake handle to return to the client, this could easily
62 // be a real file descriptor number for example if actually opening
63 // the file on the disk
64 const handle = Buffer.alloc(4);
65 openFiles.set(handleCount, { read: false });
66 handle.writeUInt32BE(handleCount++, 0, true);
67
68 console.log('Opening file for read');
69 sftp.handle(reqid, handle);
70 }).on('READ', (reqid, handle, offset, length) => {
71 let fnum;
72 if (handle.length !== 4
73 || !openFiles.has(fnum = handle.readUInt32BE(0, true))) {
74 return sftp.status(reqid, STATUS_CODE.FAILURE);
75 }
76
77 // Fake the read
78 const state = openFiles.get(fnum);
79 if (state.read) {
80 sftp.status(reqid, STATUS_CODE.EOF);
81 } else {
82 state.read = true;
83
84 console.log(
85 'Read from file at offset %d, length %d', offset, length
86 );
87 sftp.data(reqid, 'bar');
88 }
89 }).on('CLOSE', (reqid, handle) => {
90 let fnum;
91 if (handle.length !== 4
92 || !openFiles.has(fnum = handle.readUInt32BE(0))) {
93 return sftp.status(reqid, STATUS_CODE.FAILURE);
94 }
95
96 openFiles.delete(fnum);
97
98 console.log('Closing file');
99 sftp.status(reqid, STATUS_CODE.OK);
100 }).on('REALPATH', function(reqid, path) {
101 const name = [{
102 filename: '/tmp/foo.txt',
103 longname: '-rwxrwxrwx 1 foo foo 3 Dec 8 2009 foo.txt',
104 attrs: {}
105 }];
106 sftp.name(reqid, name);
107 }).on('STAT', onSTAT)
108 .on('LSTAT', onSTAT);
109
110 function onSTAT(reqid, path) {
111 if (path !== '/tmp/foo.txt')
112 return sftp.status(reqid, STATUS_CODE.FAILURE);
113
114 let mode = constants.S_IFREG; // Regular file
115 mode |= constants.S_IRWXU; // Read, write, execute for user
116 mode |= constants.S_IRWXG; // Read, write, execute for group
117 mode |= constants.S_IRWXO; // Read, write, execute for other
118 sftp.attrs(reqid, {
119 mode: mode,
120 uid: 0,
121 gid: 0,
122 size: 3,
123 atime: Date.now(),
124 mtime: Date.now(),
125 });
126 }
127 });
128 });
129 }).on('close', () => {
130 console.log('Client disconnected');
131 });
132}).listen(0, '127.0.0.1', function() {
133 console.log(`Listening on port ${this.address().port}`);
134});