1 | 'use strict';
|
2 |
|
3 | const { timingSafeEqual } = require('crypto');
|
4 | const { constants, readFileSync } = require('fs');
|
5 |
|
6 | const { Server, sftp: { OPEN_MODE, STATUS_CODE } } = require('ssh2');
|
7 |
|
8 | const allowedUser = Buffer.from('foo');
|
9 | const allowedPassword = Buffer.from('bar');
|
10 |
|
11 | function checkValue(input, allowed) {
|
12 | const autoReject = (input.length !== allowed.length);
|
13 | if (autoReject) {
|
14 |
|
15 |
|
16 | allowed = input;
|
17 | }
|
18 | const isMatch = timingSafeEqual(input, allowed);
|
19 | return (!autoReject && isMatch);
|
20 | }
|
21 |
|
22 | new 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 |
|
58 | if (filename !== '/tmp/foo.txt' || !(flags & OPEN_MODE.READ))
|
59 | return sftp.status(reqid, STATUS_CODE.FAILURE);
|
60 |
|
61 |
|
62 |
|
63 |
|
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 |
|
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;
|
115 | mode |= constants.S_IRWXU;
|
116 | mode |= constants.S_IRWXG;
|
117 | mode |= constants.S_IRWXO;
|
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 | });
|