jsharmony-cms
Version:
1,137 lines (980 loc) • 51.1 kB
JavaScript
/*
Copyright 2021 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var _ = require('lodash');
var async = require('async');
var fs = require('fs');
var os = require('os');
var ftppath = require('path').posix;
var fspath = require('path');
var ssh2 = require('ssh2-classic');
var moment = require('jsharmony/lib/moment');
var Helper = require('jsharmony/Helper');
var HelperFS = require('jsharmony/HelperFS');
function jsHarmonyCMSSFTPServer(cms){
this.cms = cms;
this.jsh = cms.jsh;
}
jsHarmonyCMSSFTPServer.WRITES_BEFORE_FLUSH = 50;
jsHarmonyCMSSFTPServer.MAX_HANDLE_QUEUE_LENGTH = 100;
jsHarmonyCMSSFTPServer.prototype.Auth = function(req, user, password, clientIp, auth_cb){
var _this = this;
var jsh = _this.jsh;
var sqlparams = {};
sqlparams[jsh.map.user_email] = user;
req.jshsite.auth.on_login(req, jsh, sqlparams, function (err, rslt) {
if ((rslt != null) && (rslt.length == 1) && (rslt[0].length == 1)) {
var user_info = rslt[0][0];
if ((user_info[jsh.map.user_status]||'').toUpperCase() != 'ACTIVE') {
if(jsh.Config.debug_params.auth_debug) jsh.Log('Login: User account not ACTIVE', { source: 'authentication' });
return auth_cb(false);
}
else {
req.jshsite.auth.validatePassword(req, jsh, user_info, password, function(error, token) { //onAuthenticate
if (error) {
if(jsh.Config.debug_params.auth_debug) jsh.Log('Login: Error Authenticating ' + error.toString(), { source: 'authentication' });
return auth_cb(false);
}
else if (token) {
var user_id = user_info[jsh.map.user_id];
var pe_ll_tstmp = new Date();
var sqlparams = {};
sqlparams[jsh.map.user_last_ip] = clientIp;
sqlparams[jsh.map.user_id] = user_id;
sqlparams[jsh.map.user_last_tstmp] = pe_ll_tstmp;
if(jsh.Config.debug_params.auth_debug) jsh.Log('Login: Success', { source: 'authentication' });
req.jshsite.auth.on_loginsuccess(req, jsh, sqlparams, function (err, rslt) {
if ((rslt != null) && (rslt.length == 1) && (rslt[0] != null) && (rslt[0][jsh.map.rowcount] == 1)) {
sqlparams = {};
sqlparams[jsh.map.user_email] = user;
req.jshsite.auth.on_auth(req, jsh, { username: user }, sqlparams, function (err, rslt) {
if (err) { jsh.Log.error(err); return auth_cb(false); }
if ((rslt != null) && (rslt.length == 1) && (rslt[0].length == 2) && (rslt[0][0].length == 1)) {
var user_roles = [];
if (rslt[0][1].length > 0) user_roles = _.map(rslt[0][1], jsh.map.user_role);
var user_info = rslt[0][0][0];
if ((user_info[jsh.map.user_status]||'').toUpperCase() != 'ACTIVE'){
if(jsh.Config.debug_params.auth_debug) jsh.Log('Auth: User status not ACTIVE', { source: 'authentication' });
return auth_cb(false);
}
else {
req.isAuthenticated = true;
req.user_id = user_info[jsh.map.user_id];
req.user_name = req.jshsite.auth.getuser_name(user_info, jsh);
req._DBContext = req.jshsite.auth.getContextUser(user_info, jsh);
req._roles = {};
_.each(user_roles, function (role) { req._roles[role] = role; });
if(jsh.Config.debug_params.auth_debug) jsh.Log('Auth: Success', { source: 'authentication' });
var onSuccess = function(){ return auth_cb(true); };
if ('onAuthComplete' in req.jshsite.auth) req.jshsite.auth.onAuthComplete(req, user_info, jsh, onSuccess);
return onSuccess();
}
}
else{
if(jsh.Config.debug_params.auth_debug) jsh.Log('Auth: User not found', { source: 'authentication' });
return auth_cb(false);
}
});
}
else {
if(jsh.Config.debug_params.auth_debug) jsh.Log('Login: An unexpected error has occurred', { source: 'authentication' });
return auth_cb(false);
}
});
}
else {
if(jsh.Config.debug_params.auth_debug) jsh.Log('Login: An unexpected error has occurred', { source: 'authentication' });
return auth_cb(false);
}
});
}
}
else {
if(jsh.Config.debug_params.auth_debug){
jsh.Log('Login: User account not found', { source: 'authentication' });
if(err) jsh.Log(err);
}
return auth_cb(false);
}
});
};
jsHarmonyCMSSFTPServer.prototype.Log = function(txt){
var _this = this;
if(!_this.cms || !_this.cms.Config.debug_params.sftp_log) return;
_this.jsh.Log.info(txt);
};
jsHarmonyCMSSFTPServer.prototype.DebugLog = function(txt){
var _this = this;
if(!_this.cms || !_this.cms.Config.debug_params.sftp_log) return;
_this.jsh.Log.debug(txt);
};
jsHarmonyCMSSFTPServer.prototype.Run = function(run_cb){
if(!run_cb) run_cb = function(){};
var _this = this;
var jsh = _this.jsh;
var cms = _this.cms;
var dbtypes = jsh.AppSrv.DB.types;
var hostKeys = [];
if(_this.cms.Config.sftp.serverKey){
hostKeys.push(fs.readFileSync(_this.cms.Config.sftp.serverKey));
}
else if(jsh.Config.server.https_key){
hostKeys.push(fs.readFileSync(jsh.Config.server.https_key));
}
if(!_this.cms.Config.sftp.serverIp){
jsh.Log.error('Invalid CMS Config: SFTP is enabled, but missing cms.Config.sftp.serverIp');
return run_cb();
}
var serverParams = { };
if(hostKeys.length) serverParams.hostKeys = hostKeys;
var failedAuth = { };
new ssh2.Server(serverParams, function(client, clientInfo) {
var clientIp = null;
var clientIpStr = null;
if (clientInfo) {
clientIp = clientInfo.ip;
if(clientIp) clientIpStr = clientIp.toString();
}
var clientDesc = 'SFTP Client ' + clientIp + ' ';
var req = {
jshsite: jsh.Sites['main']
};
var isValidIp = false;
_.each(cms.Config.sftp.clientIp, function(subnet){
if(clientIp && Helper.ValidateSubnet(clientIp, subnet)) isValidIp = true;
});
if(!isValidIp){
_this.DebugLog(clientDesc + 'Connection dropped because no subnet match in cms.Config.sftp.clientIp');
//Ignore Connection Errors
client.on('error', function(){ });
return;
}
var clientSessions = [];
function onAuthSuccess(ctx, username){
var clientIndex = (clientIpStr) ? (clientIpStr + '-' + username.toLowerCase()) : '';
if(clientIndex){
if(failedAuth[clientIndex]){
delete failedAuth[clientIndex];
}
}
try{
ctx.accept();
}
catch(ex){
/* Do nothing */
}
}
function onAuthReject(ctx, username, authMethods){
var clientIndex = (clientIpStr) ? (clientIpStr + '-' + username.toLowerCase()) : '';
if(clientIndex){
if(!(clientIndex in failedAuth)){
failedAuth[clientIndex] = {
count: 0,
lastLogin: new Date().getTime()
};
}
failedAuth[clientIndex].count++;
failedAuth[clientIndex].lastLogin = new Date().getTime();
}
try{
ctx.reject(authMethods);
}
catch(ex){
/* Do nothing */
}
}
client.on('authentication', function(ctx) {
var username = Buffer.from(ctx.username).toString() || '';
var sleepTime = 0;
var clientIndex = (clientIpStr) ? (clientIpStr + '-' + username.toLowerCase()) : '';
if(clientIndex in failedAuth){
var failedAuthInfo = failedAuth[clientIndex];
if(failedAuthInfo.count > 10){
sleepTime = 1000*Math.pow(1.1,(failedAuthInfo.count-10))-1000;
}
sleepTime = Math.round(Math.min(sleepTime, 15000));
//Expire after one hour
if(failedAuth[clientIndex].lastLogin < (new Date().getTime() - 1000 * 60 * 60)){
delete failedAuth[clientIndex];
sleepTime = 0;
}
}
Helper.execif((sleepTime > 0),
function(f){
setTimeout(f, sleepTime);
},
function(){
if(!username) return onAuthReject(ctx, username);
var authMethods = ['password'];
if(ctx.method=='password'){
var password = Buffer.from(ctx.password).toString();
_this.Auth(req, username, password, clientIp, function(success){
if(success){
if(!_.intersection(['SYSADMIN','WEBMASTER','SFTP'], _.keys(req._roles)).length) return onAuthReject(ctx, username);
return onAuthSuccess(ctx, username);
}
else {
return onAuthReject(ctx, username, authMethods);
}
});
return;
}
else{
return onAuthReject(ctx, username, authMethods);
}
}
);
}).on('ready', function() {
_this.Log(clientDesc + 'Authenticated');
client.on('session', function(accept, reject) {
var session = accept();
session.on('sftp', function(accept, reject) {
_this.DebugLog(clientDesc + 'Session Started');
var handles = {};
var maxHandle = 1;
var basePath = '/';
var sitePath = fspath.join(jsh.Config.datadir,'site');
var clientSession = {
handles: handles
};
clientSessions.push(clientSession);
var sftpStream = accept();
var SFTPSTREAM = sftpStream.constructor;
//SFTPStream.STATUS_CODE { OK, EOF, NO_SUCH_FILE, PERMISSION_DENIED, FAILURE, BAD_MESSAGE, OP_UNSUPPORTED }
//SFTPStream.OPEN_MODE { READ, WRITE, APPEND, CREAT, TRUNC, EXCL } SFTPStream.stringToFlags, SFTPStream.flagsToString
//.status(reqid, statusCode, [message]) -- Wait for continue event
//.handle(reqid, handle) -- If return false, wait for continue event
//.data(reqid, data, encoding) -- If return false, wait for continue event
//.name(reqid, names) names = array of { filename, longname, attrs }
//.attrs(reqid, attrs) attrs = { mode, uid, gid, size, atime, mtime }
function normalizePath(fpath){
var vpath = fpath;
try{
if(!vpath) vpath = basePath;
if(vpath[0] != '/') vpath = ftppath.join(basePath, vpath);
vpath = ftppath.normalize(vpath);
}
catch(ex){
_this.DebugLog(clientDesc + 'REALPATH - Invalid Path: '+fpath);
return false;
}
if(vpath.indexOf('/sites/')===0){
var vpathParts = vpath.split('/');
if(vpathParts[2]){
var siteId = vpathParts[2].split('_')[0];
if(parseInt(siteId).toString() === siteId){
vpathParts[2] = siteId;
return vpathParts.join('/');
}
}
}
return vpath;
}
function realPath(vpath, cb){
var sysPath = '';
var sysBasePath = '';
if(vpath=='/'){
return cb('');
}
else if(vpath == '/sites'){
sysBasePath = sitePath;
sysPath = sysBasePath;
}
else if(vpath.indexOf('/sites/')==0){
sysBasePath = sitePath;
sysPath = fspath.join(sysBasePath, vpath.substr(('/sites/').length));
}
else {
return cb(false);
}
fs.realpath(sysPath, function(err, fullpath){
if(err) return cb(false);
if(fullpath.indexOf(sysBasePath + fspath.sep)==0) return cb(fullpath);
if(fullpath === sysBasePath) return cb(fullpath);
return cb(false);
});
}
function validateAccess(reqid, vpath, options, cb){
options = _.extend({ softFail: false }, options);
function handleError(code, msg){
if(options.softFail) return cb({ isSFTPError: true, code: code, msg: msg });
return sftpStream.status(reqid, code, msg);
}
if(vpath.indexOf('/sites/')==0){
var site_str = vpath.substr(('/sites/').length);
if(!site_str) return cb();
site_str = site_str.split('/')[0];
site_str = site_str.split('_')[0];
if(!site_str) return handleError(ssh2.SFTP_STATUS_CODE.FAILURE, 'Invalid path');
var site_id = parseInt(site_str);
if(site_id.toString() != site_str) return handleError(ssh2.SFTP_STATUS_CODE.FAILURE, 'Invalid path');
jsh.AppSrv.ExecRow(req._DBContext, cms.funcs.replaceSchema("select site_id from {schema}.v_my_site where site_id=@site_id and site_sts='ACTIVE' order by site_id"), [dbtypes.BigInt], { site_id: site_id }, function (err, rslt) {
if (err) { jsh.Log.error(err); return handleError(ssh2.SFTP_STATUS_CODE.FAILURE, 'Error retrieving site listing'); }
if (!rslt || !rslt.length || !rslt[0] || !rslt[0].site_id) return handleError(ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
//If it is a site folder and it does not exist, create it
fs.exists(fspath.join(sitePath,site_id.toString()), function(exists){
if(!exists){
async.waterfall([
function(create_cb){ HelperFS.createFolderIfNotExists(fspath.join(sitePath,site_id.toString()), create_cb); },
function(create_cb){ HelperFS.createFolderIfNotExists(fspath.join(sitePath,site_id.toString(),'templates'), create_cb); },
function(create_cb){ HelperFS.createFolderIfNotExists(fspath.join(sitePath,site_id.toString(),'templates','pages'), create_cb); },
function(create_cb){ HelperFS.createFolderIfNotExists(fspath.join(sitePath,site_id.toString(),'templates','components'), create_cb); },
function(create_cb){ HelperFS.createFolderIfNotExists(fspath.join(sitePath,site_id.toString(),'templates','websnippets'), create_cb); },
], cb);
}
else return cb();
});
});
}
else if(vpath == '/') return cb();
else if(vpath == '/sites') return cb();
else return handleError(ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
}
function initPath(reqid, fpath, options, cb){
options = _.extend({ softFail: false }, options);
function handleError(code, msg){
if(options.softFail) return cb({ isSFTPError: true, code: code, msg: msg });
return sftpStream.status(reqid, code, msg);
}
//Normalize path
var vpath = normalizePath(fpath);
if(vpath === false) return handleError(ssh2.SFTP_STATUS_CODE.FAILURE, 'Invalid path');
async.waterfall([
function(open_cb){
////Check if a user has access to the folder
validateAccess(reqid, vpath, options, open_cb);
},
function(open_cb){
//Check if it is a real path
realPath(vpath, function(syspath){
if(syspath === false) return handleError(ssh2.SFTP_STATUS_CODE.NO_SUCH_FILE, 'Path not found');
if(syspath === '') return cb(null, vpath, syspath, {
size: 4096,
atimeMs: moment().valueOf(),
mtimeMs: moment().valueOf(),
mode: 0777 + fs.constants.S_IFDIR, // eslint-disable-line no-octal
isFile: function(){ return false; },
isDirectory: function(){ return true; },
isSymbolicLink: function(){ return false; },
});
//Get stats
fs.lstat(syspath, function(err, stats){
if(err) return open_cb(err);
//Verify it is a file or directory
if(!stats.isDirectory() && !stats.isFile()) return handleError(ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
return cb(null, vpath, syspath, stats);
});
});
},
], function(err){
if(err){
if(options.softFail && err.isSFTPError) return cb(err);
jsh.Log.error(err);
return handleError(ssh2.SFTP_STATUS_CODE.FAILURE, 'An error occurred');
}
});
}
function updateHandleAccess(handleInfo){
handleInfo.lastAccess = new Date().getTime();
}
function getHandleInfo(handle){
handle = (handle||'').toString();
var handleInfo = handles[handle];
if(!handleInfo) return false;
updateHandleAccess(handleInfo);
return handleInfo;
}
function waitForLock(handleInfo, cb){
if(handleInfo.closed) return;
if(cb){
updateHandleAccess(handleInfo);
handleInfo.lockQueue.push(cb);
if(handleInfo.lockQueue.length > jsHarmonyCMSSFTPServer.MAX_HANDLE_QUEUE_LENGTH){
handleInfo.lockPaused = true;
sftpStream.pause();
}
}
var retry = function(){ setTimeout(function(){ waitForLock(handleInfo, null); }, 1); };
if(handleInfo.lock){
if(!cb && !handleInfo.lockQueue.length) return;
if(!cb) retry(); //Standard retry
if(handleInfo.lockQueue.length==1) retry(); //First time
}
else {
updateHandleAccess(handleInfo);
if(handleInfo.lockQueue.length) handleInfo.lockQueue.shift()();
if(handleInfo.lockPaused && (handleInfo.lockQueue.length < jsHarmonyCMSSFTPServer.MAX_HANDLE_QUEUE_LENGTH)){
handleInfo.lockPaused = false;
sftpStream.resume();
}
if(!cb && handleInfo.lockQueue.length) retry(); //Get next item
}
}
var handleTimer = null;
function runHandleTimer(tick){
var HANDLE_TIMEOUT = 60;
if(!tick && handleTimer) return;
var hasHandles = false;
var curdt = new Date().getTime();
var closedt = curdt - HANDLE_TIMEOUT * 1000;
for(var handle in handles){
hasHandles = true;
var handleInfo = handles[handle];
if(!handleInfo.lastAccess) updateHandleAccess(handleInfo);
else {
if(handleInfo.lastAccess < closedt){
_this.DebugLog(clientDesc + 'Auto-closing '+handleInfo.syspath);
closeHandle(handle);
if(handleInfo.lockPaused){
handleInfo.lockPaused = false;
sftpStream.resume();
}
}
}
}
handleTimer = hasHandles ? setTimeout(function(){ runHandleTimer(true); }, 1000) : null;
}
function addHandle(handleInfo){
var handle = (maxHandle++).toString();
_.extend(handleInfo, {
lock: false,
lockQueue: [],
lockPaused: false,
closed: false,
closing: false,
lastAccess: new Date().getTime(),
});
handles[handle] = handleInfo;
runHandleTimer();
return handle;
}
function closeHandle(handle, cb){
if(!cb) cb = function(){};
var handleInfo = getHandleInfo(handle);
if(!handleInfo) return cb();
if(handleInfo.closing) return cb();
handleInfo.closing = true;
Helper.execif(handleInfo.type=='file',
function(f){
waitForLock(handleInfo, function(){
if(handleInfo.lockPaused){
handleInfo.lockPaused = false;
sftpStream.resume();
}
if(handleInfo.closed) return f();
handleInfo.closed = true;
fs.close(handleInfo.fd, function(err){
if(err) return cb(err);
return f();
});
});
},
function(){
delete handles[(handle||'').toString()];
return cb();
}
);
}
clientSession.closeAllHandles = function(){
for(var handle in handles){
closeHandle(handle);
}
};
function setStats(reqid, fpath, attrs){
initPath(reqid, fpath, null, function(err, vpath, syspath, stats){
if(!stats.isFile()) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Path is not a file');
if(_.includes(['/','/sites/'], vpath)) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
if(_.includes(['/','/sites'], ftppath.dirname(vpath))) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
if(attrs.atime || attrs.mtime){
var atime = attrs.atime || Math.round(stats.atimeMs/1000);
var mtime = attrs.mtime || Math.round(stats.mtimeMs/1000);
fs.utimes(syspath, atime, mtime, function(err){
if (err){
jsh.Log.error(err);
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error updating stats');
}
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
});
}
else return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
});
}
sftpStream.on('OPEN', function(reqid, fpath, flags, attrs) {
_this.DebugLog(clientDesc + 'OPEN '+fpath);
//.handle(reqid, handle) -- If return false, wait for continue event
//.status(reqid, statusCode, [message])
var fsflags = SFTPSTREAM.flagsToString(flags);
var createIfNotExists = ((flags & ssh2.SFTP_OPEN_MODE.CREAT) > 0);
var createFile = false;
initPath(reqid, fpath, { softFail: true }, function(err, vpath, syspath, stats){
async.waterfall([
function(open_cb){
if(err){
if(err.isSFTPError){
if(createIfNotExists && (err.code == ssh2.SFTP_STATUS_CODE.NO_SUCH_FILE)){
//Create file - check if parent folder is accessible
createFile = true;
return open_cb();
}
return sftpStream.status(reqid, err.code, err.msg);
}
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error opening file');
}
if(!stats.isFile()) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Path is not a file');
return open_cb();
},
//If creating a new file, validate the parent folder
function(open_cb){
if(!createFile) return open_cb();
var parentDir = ftppath.dirname(fpath);
var newName = ftppath.basename(fpath);
if(!newName) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Invalid directory name');
initPath(reqid, parentDir, null, function(err, parentvpath, parentsyspath, parentstats){
if(!parentstats.isDirectory()) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Parent path is not a directory');
if(_.includes(['/','/sites'], parentvpath)) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
vpath = ftppath.join(parentvpath, newName);
syspath = fspath.join(parentsyspath, newName);
return open_cb();
});
},
function(open_cb){
//Open the file with target access
fs.open(syspath, fsflags, function(err, fd){
if(err) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error opening file');
//Return the handle to read folder contents
var handle = addHandle({
type: 'file',
path: vpath,
syspath: syspath,
fsflags: fsflags,
page: 0,
fd: fd,
});
sftpStream.handle(reqid, Buffer.from(handle));
});
},
], function(err){
if(err) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error opening file');
});
});
}).on('READ', function(reqid, handle, offset, length) {
_this.DebugLog(clientDesc + 'READ '+(offset||'').toString()+' '+(length||'').toString());
//.data(reqid, data, encoding) -- If return false, wait for continue event
//.status(reqid, statusCode, [message])
var handleInfo = getHandleInfo(handle);
if(!handleInfo) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Handle not found');
handleInfo.page++;
fs.fstat(handleInfo.fd, function(err, stats){
if(err) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error reading file');
//EOF
if (offset >= stats.size){
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.EOF);
return;
}
//Read from file
var buffer = Buffer.alloc(length);
fs.read(handleInfo.fd, buffer, 0, length, offset, function(err, bytesRead, readBuffer){
if(err) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error reading file');
sftpStream.data(reqid, readBuffer.slice(0, bytesRead));
updateHandleAccess(handleInfo);
});
});
}).on('WRITE', function(reqid, handle, offset, data) {
_this.DebugLog(clientDesc + 'WRITE to file at offset '+offset+': '+(data ? data.length : 0)+' bytes');
var handleInfo = getHandleInfo(handle);
if(!handleInfo) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Handle not found');
//var startTime = new Date().getTime(); //Diagnostic Logging
waitForLock(handleInfo, function(){
if(handleInfo.closing || handleInfo.closed){
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Handle is being closed');
}
handleInfo.lock = true;
handleInfo.page++;
fs.write(handleInfo.fd, data, 0, data.length, offset, function(err, bytesWritten, writeBuffer){
if(err){
handleInfo.lock = false;
jsh.Log.error(err);
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error writing file');
closeHandle(handle);
return;
}
Helper.execif((handleInfo.page % jsHarmonyCMSSFTPServer.WRITES_BEFORE_FLUSH == 0),
function(f){
updateHandleAccess(handleInfo);
fs.fdatasync(handleInfo.fd, function(err){
if(err){
handleInfo.lock = false;
jsh.Log.error(err);
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error writing file');
closeHandle(handle);
return;
}
return f();
});
},
function(){
handleInfo.lock = false;
//var endTime = new Date().getTime(); //Diagnostic Logging
//console.log(endTime - startTime); //Diagnostic Logging
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
updateHandleAccess(handleInfo);
}
);
});
});
}).on('FSTAT', function(reqid, handle) {
_this.DebugLog(clientDesc + 'FSTAT');
//.attrs(reqid, attrs) attrs = { mode, uid, gid, size, atime, mtime }
//.status(reqid, statusCode, [message])
var handleInfo = getHandleInfo(handle);
if(!handleInfo) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Handle not found');
waitForLock(handleInfo, function(){
fs.fstat(handleInfo.fd, function(err, stats){
if(err) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error reading file');
sftpStream.attrs(reqid, {
mode: stats.mode,
uid: 0,
gid: 0,
size: (stats.isFile() ? stats.size : 4096),
atime: Math.round(stats.atimeMs/1000),
mtime: Math.round(stats.mtimeMs/1000),
});
});
});
}).on('FSETSTAT', function(reqid, handle, attrs) {
_this.DebugLog(clientDesc + 'FSETSTAT');
//.status(reqid, statusCode, [message])
var handleInfo = getHandleInfo(handle);
if(!handleInfo) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Handle not found');
waitForLock(handleInfo, function(){
if(handleInfo.closing || handleInfo.closed){
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Handle is being closed');
}
setStats(reqid, handleInfo.path, attrs);
});
}).on('CLOSE', function(reqid, handle) {
_this.DebugLog(clientDesc + 'CLOSE');
var handleInfo = getHandleInfo(handle);
if(!handleInfo) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
if(handleInfo.closed) return;
waitForLock(handleInfo, function(){
closeHandle(handle, function(err){
if(err) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error closing handle');
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
});
});
}).on('OPENDIR', function(reqid, fpath) {
_this.DebugLog(clientDesc + 'OPENDIR '+fpath);
//.handle(reqid, handle) -- If return false, wait for continue event
//.status(reqid, statusCode, [message])
initPath(reqid, fpath, null, function(err, vpath, syspath, stats){
if(!stats.isDirectory()) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Path is not a directory');
//Return the handle to read folder contents
var handle = addHandle({
type: 'dir',
path: vpath,
syspath: syspath,
page: 0,
});
sftpStream.handle(reqid, Buffer.from(handle));
});
}).on('READDIR', function(reqid, handle) {
_this.DebugLog(clientDesc + 'READDIR '+reqid);
//.name(reqid, names) names = array of { filename, longname, attrs }
//.status(reqid, statusCode, [message])
var handleInfo = getHandleInfo(handle);
if(!handleInfo) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Handle not found');
waitForLock(handleInfo, function(){
handleInfo.lock = true;
handleInfo.page++;
if(!handleInfo.syspath){
//Root folder
if(handleInfo.page > 1){
handleInfo.lock = false;
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.EOF);
closeHandle(handle);
}
else {
var filename = 'sites';
handleInfo.lock = false;
sftpStream.name(reqid, [{
filename: filename,
longname: 'drwxrwxrwx 1 sys sys 4096 '+moment().format('MMM D YYYY')+' ' + filename,
attrs: {
mode: 0777 + fs.constants.S_IFDIR, // eslint-disable-line no-octal
uid: 0,
gid: 0,
size: 4096,
atime: moment().unix(),
mtime: moment().unix(),
}
}]);
}
}
else {
fs.exists(handleInfo.syspath, function(exists){
if(!exists){
handleInfo.lock = false;
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.NO_SUCH_FILE);
}
fs.readdir(handleInfo.syspath, function(err, files){
if(err){
handleInfo.lock = false;
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error retrieving directory listing');
}
Helper.execif((handleInfo.path=='/sites'),
function(f){
files = [];
jsh.AppSrv.ExecRecordset(req._DBContext, cms.funcs.replaceSchema("select site_id,site_name from {schema}.v_my_site where site_sts='ACTIVE' order by site_id"), [], { }, function (err, rslt) {
if (err) {
jsh.Log.error(err);
handleInfo.lock = false;
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error retrieving site listing');
}
if(rslt && rslt[0]) _.each(rslt[0], function(site){
var filename_short = HelperFS.cleanFileName(site.site_id);
var filename_long = HelperFS.cleanFileName(site.site_id+'_'+site.site_name);
files.push({
filename: filename_short,
longname: 'drwxrwxrwx 0 sys sys 4096 '+moment().format('MMM D YYYY')+' ' + filename_short,
attrs: {
mode: 0777, // eslint-disable-line no-octal
uid: 0,
gid: 0,
size: 4096,
atime: moment().unix(),
mtime: moment().unix(),
}
});
files.push({
filename: filename_long,
longname: 'lrwxrwxrwx 1 sys sys 4096 '+moment().format('MMM D YYYY')+' ' + filename_long,
attrs: {
mode: 0777 + fs.constants.S_IFDIR, // eslint-disable-line no-octal
uid: 0,
gid: 0,
size: 4096,
atime: moment().unix(),
mtime: moment().unix(),
}
});
});
return f();
});
},
function(){
var filesPerPage = 1000;
var pageFiles = [];
var dirPage = [];
if((handleInfo.page-1)*filesPerPage >= files.length){
handleInfo.lock = false;
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.EOF);
closeHandle(handle);
}
else{
for(var i=filesPerPage*(handleInfo.page-1);i<filesPerPage*handleInfo.page&&i<files.length;i++){
var file = files[i];
if(_.isString(file)){
pageFiles.push(file);
dirPage.push({
filename: file,
longname: file,
attrs: {}
});
}
else {
pageFiles.push('');
dirPage.push(file);
}
}
async.eachOfLimit(pageFiles, 50, function(pageFile, idx, file_cb){
if(!pageFile) return file_cb();
fs.stat(fspath.join(handleInfo.syspath, pageFile), function(err, fstat){
if(err) return file_cb();
var longname = '';
if(fstat.isDirectory()) longname += 'd';
//else if(fstat.isSymbolicLink()) longname += 'l';
else if(fstat.isFile()) longname += '-';
else{
dirPage[idx] = undefined;
return file_cb();
}
longname += 'rwxrwxrwx 0 sys sys '+(longname=='-' ? fstat.size : 4096)+' '+moment(fstat.mtime).format('MMM D YYYY')+' '+pageFile;
dirPage[idx].longname = longname;
dirPage[idx].attrs = {
mode: 0777 + (longname[0]=='d' ? fs.constants.S_IFDIR : longname[0]=='l' ? fs.constants.S_IFLNK : fs.constants.S_IFREG), // eslint-disable-line no-octal
uid: 0,
gid: 0,
size: (longname=='-' ? fstat.size : 4096),
atime: Math.floor(fstat.atime/1000),
mtime: Math.floor(fstat.mtime/1000),
};
return file_cb();
});
}, function(err){
if(err){
jsh.Log.error(err);
handleInfo.lock = false;
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error retrieving directory listing');
}
//Remove undefined entries from dirPage
for(var i=0;i<dirPage.length;i++){
if(typeof dirPage[i]=='undefined'){
dirPage.splice(i, 1);
i--;
}
}
handleInfo.lock = false;
sftpStream.name(reqid, dirPage);
});
}
}
);
});
});
}
});
}).on('LSTAT', function(reqid, fpath) {
_this.DebugLog(clientDesc + 'LSTAT '+fpath);
//.attrs(reqid, attrs) attrs = { mode, uid, gid, size, atime, mtime }
//.status(reqid, statusCode, [message])
initPath(reqid, fpath, null, function(err, vpath, syspath, stats){
sftpStream.attrs(reqid, {
mode: stats.mode,
uid: 0,
gid: 0,
size: (stats.isFile() ? stats.size : 4096),
atime: Math.round(stats.atimeMs/1000),
mtime: Math.round(stats.mtimeMs/1000),
});
});
}).on('STAT', function(reqid, fpath) {
_this.DebugLog(clientDesc + 'STAT '+fpath);
//.attrs(reqid, attrs) attrs = { mode, uid, gid, size, atime, mtime }
//.status(reqid, statusCode, [message])
initPath(reqid, fpath, null, function(err, vpath, syspath, stats){
sftpStream.attrs(reqid, {
mode: stats.mode,
uid: 0,
gid: 0,
size: (stats.isFile() ? stats.size : 4096),
atime: Math.round(stats.atimeMs/1000),
mtime: Math.round(stats.mtimeMs/1000),
});
});
}).on('REMOVE', function(reqid, fpath) {
_this.DebugLog(clientDesc + 'REMOVE '+fpath);
//.status(reqid, statusCode, [message])
initPath(reqid, fpath, null, function(err, vpath, syspath, stats){
if(!stats.isFile()) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Path is not a file');
if(_.includes(['/','/sites/'], vpath)) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
if(_.includes(['/','/sites'], ftppath.dirname(vpath))) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
fs.unlink(syspath, function(err){
if (err) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error deleting file');
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
});
});
}).on('RMDIR', function(reqid, fpath) {
_this.DebugLog(clientDesc + 'RMDIR '+fpath);
//.status(reqid, statusCode, [message])
initPath(reqid, fpath, null, function(err, vpath, syspath, stats){
if(!stats.isDirectory()) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Path is not a directory');
if(_.includes(['/','/sites/'], vpath)) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
if(_.includes(['/','/sites'], ftppath.dirname(vpath))) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
fs.rmdir(syspath, function(err){
if (err){
if(err.code=='ENOTEMPTY') return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Cannot delete - folder is not empty');
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error deleting folder');
}
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
});
});
}).on('REALPATH', function(reqid, fpath) {
_this.DebugLog(clientDesc + 'REALPATH '+fpath);
//.name(reqid, names) names = array of { filename, longname, attrs }
//.status(reqid, statusCode, [message])
//Normalize path
var vpath = normalizePath(fpath);
if(vpath === false) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Invalid path');
//Resolve path
sftpStream.name(reqid, {
filename: vpath,
longname: vpath,
attrs: {}
});
}).on('READLINK', function(reqid, fpath) {
_this.DebugLog(clientDesc + 'READLINK '+fpath);
//.name(reqid, names) names = array of { filename, longname, attrs }
//.status(reqid, statusCode, [message])
//Resolve path
var vpath = normalizePath(fpath);
if(vpath === false) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Invalid path');
var parentDir = ftppath.dirname(vpath);
if(!_.includes(['/sites'], parentDir)) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
sftpStream.name(reqid, {
filename: vpath,
longname: vpath,
attrs: {}
});
}).on('SETSTAT', function(reqid, fpath, attrs) {
_this.DebugLog(clientDesc + 'SETSTAT '+fpath);
//.status(reqid, statusCode, [message])
setStats(reqid, fpath, attrs);
}).on('MKDIR', function(reqid, fpath, attrs) {
_this.DebugLog(clientDesc + 'MKDIR '+fpath);
//.status(reqid, statusCode, [message])
var parentDir = ftppath.dirname(fpath);
var newDirName = ftppath.basename(fpath);
if(!newDirName) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Invalid directory name');
initPath(reqid, parentDir, null, function(err, parentvpath, parentsyspath, parentstats){
if(!parentstats.isDirectory()) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Parent path is not a directory');
if(_.includes(['/','/sites'], parentvpath)) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
var syspath = fspath.join(parentsyspath, newDirName);
fs.mkdir(syspath, function(err){
if (err) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error creating folder');
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
});
});
}).on('RENAME', function(reqid, oldPath, newPath) {
_this.DebugLog(clientDesc + 'RENAME '+oldPath+' '+newPath);
//.status(reqid, statusCode, [message])
var newParentDir = ftppath.dirname(newPath);
var newName = ftppath.basename(newPath);
if(!newName) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Invalid new name');
initPath(reqid, oldPath, null, function(err, oldvpath, oldsyspath, oldstats){
initPath(reqid, newParentDir, null, function(err, newparentvpath, newparentsyspath, newparentstats){
if(_.includes(['/','/sites'], newparentvpath)) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
if(_.includes(['/','/sites','/sites/'], oldvpath)) return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
var newsyspath = fspath.join(newparentsyspath, newName);
fs.rename(oldsyspath, newsyspath, function(err){
if (err){
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.FAILURE, 'Error renaming');
}
sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.OK);
});
});
});
}).on('SYMLINK', function(reqid, linkPath, targetPath) {
_this.DebugLog(clientDesc + 'SYMLINK '+linkPath+' '+targetPath);
//.status(reqid, statusCode, [message])
return sftpStream.status(reqid, ssh2.SFTP_STATUS_CODE.PERMISSION_DENIED);
});
});
});
}).on('error', function(err){
if(err.message == 'Unable to authenticate'){
_this.DebugLog(clientDesc + 'Authentication Failed');
}
else{
_this.DebugLog(clientDesc