@twec/sftp-server
Version:
SFTP Server.
230 lines (203 loc) • 6.73 kB
JavaScript
/* eslint-disable no-param-reassign */
// Needed for assignment of statresponder parameters
// "use strict";
const fs = require('fs');
const debug = require('debug');
const SFTPServer = require('node-sftp-server');
const logger = {
debug: debug('sftp-server:debug'),
info: debug('sftp-server:info'),
error: debug('sftp-server:error'),
};
// Use the directory that the server lives in as the root directory
const serverRootDirectory = __dirname;
const tempDirectory = 'tmp';
// Build the full path on the server
function getFullPath(path) {
return `${serverRootDirectory}/${path}`;
}
function tempDir() {
return getFullPath(tempDirectory);
}
function exists(path) {
try {
fs.lstatSync(path);
return true;
} catch (err) {
return false;
}
}
function deleteFile(path) {
try {
if (exists(path)) fs.unlinkSync(path);
} catch (err) {}
}
function deleteAllFilesInDirectory(path) {
try {
if (!exists(path)) return;
const files = fs.readdirSync(path);
files.forEach(file => {
deleteFile(`${path}/${file}`);
});
} catch (err) {}
}
function createTempDir() {
try {
if (!exists(tempDir())) fs.mkdirSync(tempDir());
return true;
} catch (err) {
return false;
}
}
function createSFTPServer({ hostKey, port, username, password }) {
if (!createTempDir()) {
logger.info(`sftp-server unable to create temp directory: ${tempDir()}`);
return;
}
const srv = new SFTPServer({
privateKeyFile: hostKey,
temporaryFileDirectory: tempDir(),
// debug: true
});
srv.listen(port);
logger.info(`sftp-server listening on port: ${port}`);
srv.on('connect', function(auth, info) {
logger.info(
`Authentication attempted, client info is: ${JSON.stringify(
info,
)}, auth method is: ${auth.method}`,
);
// Reject the connection if invalid username or password
if (
auth.method !== 'password' ||
auth.username !== username ||
auth.password !== password
) {
logger.info('Connection rejected because of username and/or password');
return auth.reject(['password'], false);
}
// Try accepting the connection
return auth.accept(function(session) {
logger.info('Accepted the connection.');
// Server directory listing
session.on('readdir', function(path, responder) {
const fullPath = getFullPath(path);
logger.info(`readdir: ${fullPath}`);
let results = [];
const dirs = function() {
try {
if (exists(fullPath)) results = fs.readdirSync(fullPath);
} catch (err) {}
return results;
}.apply(this);
let i = 0;
responder.on('dir', function() {
if (dirs[i]) {
responder.file(dirs[i]);
i += 1;
return i;
}
return responder.end();
});
return responder.on('end', function() {
return logger.info('readdir: do cleanup here.');
});
});
// Read a file on the server
session.on('readfile', function(path, writestream) {
const fullPath = getFullPath(path);
logger.info(`readfile: ${fullPath}`);
const readstream = fs.createReadStream(fullPath);
return readstream.pipe(writestream);
});
// Write a file to the server
session.on('writefile', function(path, readstream) {
const fullPath = getFullPath(path);
logger.info(`writefile: ${fullPath}`);
const writestream = fs.createWriteStream(fullPath);
readstream.on('end', function() {
logger.info('writefile request has ended.');
});
return readstream.pipe(writestream);
});
// Delete a file on the server
session.on('delete', function(path, callback) {
try {
const fullPath = getFullPath(path);
logger.info(`delete file: ${fullPath}`);
if (exists(fullPath)) fs.unlinkSync(fullPath);
callback.ok();
} catch (err) {
callback.fail();
}
});
// Rename a file on the server
session.on('rename', function(oldPath, newPath, callback) {
try {
const oldFullPath = getFullPath(oldPath);
const newFullPath = getFullPath(newPath);
logger.info(`rename file: ${oldFullPath} to ${newFullPath}`);
if (exists(oldFullPath)) fs.renameSync(oldFullPath, newFullPath);
callback.ok();
} catch (err) {
callback.fail();
}
});
// Create a directory on the server
session.on('mkdir', function(path, callback) {
try {
const fullPath = getFullPath(path);
logger.info(`mkdir: ${fullPath}`);
if (!exists(fullPath)) fs.mkdirSync(fullPath);
callback.ok();
} catch (err) {
callback.fail();
}
});
// Remove a directory on the server
session.on('rmdir', function(path, callback) {
try {
const fullPath = getFullPath(path);
logger.info(`rmdir: ${fullPath}`);
if (exists(fullPath)) {
deleteAllFilesInDirectory(fullPath);
fs.rmdirSync(fullPath);
}
callback.ok();
} catch (err) {
callback.fail();
}
});
// Get file or directory information
session.on('stat', function(path, statkind, statresponder) {
const fullPath = getFullPath(path);
if (!exists(fullPath)) {
statresponder.nofile(); // Tells the statter that no file exists.
return;
}
try {
const stats = fs.lstatSync(fullPath);
if (stats.isFile()) {
statresponder.is_file();
} else {
statresponder.is_directory();
}
statresponder.permissions = stats.mode; // Octal permissions, like what you'd send to a chmod command
statresponder.uid = stats.uid; // User ID that owns the file.
statresponder.gid = stats.gid; // Group ID that owns the file.
statresponder.size = stats.size; // File size in bytes.
statresponder.atime = stats.atime; // Created at (unix style timestamp in seconds-from-epoch).
statresponder.mtime = stats.mtime; // Modified at (unix style timestamp in seconds-from-epoch).
statresponder.file(); // Tells the statter to actually send the values above down the wire.
} catch (err) {}
});
});
});
srv.on('error', function() {
return logger.info('Encountered an error');
});
srv.on('end', function() {
return logger.info('User disconnected');
});
}
module.exports.createSFTPServer = createSFTPServer;