ftp-srv-esm
Version:
Modern, extensible FTP server (daemon) for Node.js with ESM support. Based on ftp-srv.
159 lines (140 loc) • 4.33 kB
JavaScript
#!/usr/bin/env node
import yargs from 'yargs';
import nodePath from 'path';
import FtpSrv from '../src/index.js';
import * as errors from '../src/errors.js';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
function setupYargs() {
return yargs
.option('credentials', {
alias: 'c',
describe: 'Load user & pass from json file',
normalize: true
})
.option('username', {
describe: 'Blank for anonymous',
type: 'string',
default: ''
})
.option('password', {
describe: 'Password for given username',
type: 'string'
})
.option('root', {
alias: 'r',
describe: 'Default root directory for users',
type: 'string',
normalize: true
})
.option('read-only', {
describe: 'Disable write actions such as upload, delete, etc',
boolean: true,
default: false
})
.option('pasv-hostname', {
describe: 'Hostname to provide for passive connections',
type: 'string',
alias: 'pasv_hostname'
})
.option('pasv-url', { // Deprecated alias for pasv_hostname
describe: 'URL to provide for passive connections',
type: 'string',
alias: 'pasv_hostname'
})
.option('pasv-min', {
describe: 'Starting point to use when creating passive connections',
type: 'number',
default: 1024,
alias: 'pasv_min'
})
.option('pasv-max', {
describe: 'Ending port to use when creating passive connections',
type: 'number',
default: 65535,
alias: 'pasv_max'
})
.parse();
}
function setupState(_args) {
const _state = {};
function setupOptions() {
if (Array.isArray(_args._) && _args._.length > 0) {
_state.url = _args._[0];
}
_state.pasv_hostname = _args.pasv_hostname;
_state.pasv_min = _args.pasv_min;
_state.pasv_max = _args.pasv_max;
_state.anonymous = _args.username === '';
}
function setupRoot() {
const dirPath = _args.root;
if (dirPath) {
_state.root = dirPath;
} else {
_state.root = process.cwd();
}
}
function setupCredentials() {
_state.credentials = {};
const setCredentials = (username, password, root = null) => {
_state.credentials[username] = {
password,
root
};
};
if (_args.credentials) {
const credentialsFile = nodePath.resolve(_args.credentials);
let credentials;
try {
// Synchronously require the credentials file (must be CommonJS or .json)
credentials = require(credentialsFile);
if (credentials.default) credentials = credentials.default;
} catch (err) {
console.error(`Failed to load credentials file: ${credentialsFile}`);
throw err;
}
for (const cred of credentials) {
setCredentials(cred.username, cred.password, cred.root);
}
} else if (_args.username) {
setCredentials(_args.username, _args.password);
}
}
function setupCommandBlacklist() {
if (_args.readOnly) {
_state.blacklist = ['ALLO', 'APPE', 'DELE', 'MKD', 'RMD', 'RNRF', 'RNTO', 'STOR', 'STRU'];
}
}
setupOptions();
setupRoot();
setupCredentials();
setupCommandBlacklist();
return _state;
}
function startFtpServer(_state) {
// Remove null/undefined options so they get set to defaults, below
for (const key in _state) {
if (_state[key] === undefined) delete _state[key];
}
function checkLogin(data, resolve, reject) {
const user = _state.credentials[data.username];
if (_state.anonymous || user && user.password === data.password) {
return resolve({root: user && user.root || _state.root});
}
return reject(new errors.GeneralError('Invalid username or password', 401));
}
const ftpServer = new FtpSrv({
url: _state.url,
pasv_hostname: _state.pasv_hostname,
pasv_min: _state.pasv_min,
pasv_max: _state.pasv_max,
anonymous: _state.anonymous,
blacklist: _state.blacklist
});
ftpServer.on('login', checkLogin);
ftpServer.listen();
}
const args = setupYargs();
const state = setupState(args);
startFtpServer(state);