@sharelist/node-smb-server
Version:
Sharelist SMB Server Implementation Base On node-smb-server
420 lines (372 loc) • 11.5 kB
JavaScript
/*
* Copyright 2015 Adobe Systems Incorporated. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
;
var os = require('os');
var net = require('net');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var winston = require('winston')
var logging = require('./logging')
var logConfig = {
"default": {
"console": {
"level": "error",
"colorize": true,
"label": "SRV"
},
"file": {
"level": "error",
"timestamp": true,
"json": false,
"maxsize": 5242880,
"maxFiles": 10,
"zippedArchive": true,
"tailable": true,
"filename": "./server.log"
}
},
"smb": {
"console": {
"level": "error",
"colorize": true,
"label": "SMB"
},
"file": {
"level": "error",
"timestamp": true,
"json": false,
"maxsize": 5242880,
"maxFiles": 10,
"zippedArchive": true,
"tailable": true,
"filename": "./smb.log"
}
},
"spi": {
"console": {
"level": "error",
"colorize": true,
"label": "SPI"
},
"file": {
"level": "error",
"timestamp": true,
"json": false,
"maxsize": 5242880,
"maxFiles": 10,
"zippedArchive": true,
"tailable": true,
"filename": "./spi.log"
}
},
"perf": {
"console": {
"level": "error",
"colorize": true,
"label": "PERF"
},
"file": {
"level": "error",
"timestamp": true,
"json": false,
"maxsize": 5242880,
"maxFiles": 10,
"zippedArchive": true,
"tailable": true,
"filename": "./perf.log"
}
},
"request": {
"console": {
"level": "error",
"colorize": true,
"label": "REQUEST"
},
"file": {
"level": "error",
"timestamp": true,
"json": false,
"maxsize": 5242880,
"maxFiles": 10,
"zippedArchive": true,
"tailable": true,
"filename": "./request.log"
}
}
}
logging(logConfig,()=>{
console.log('SMB logger init')
})
var _ = require('lodash');
var async = require('async');
var logging = require('./logging');
var common = require('./common');
var utils = require('./utils');
var DefaultAuthenticator = require('./defaultauthenticator');
var ntlm = require('./ntlm');
var SMBConnection = require('./smbconnection');
var SMBLogin = require('./smblogin');
var SMBSession = require('./smbsession');
var SMBShare = require('./smbshare');
var IPCShare = require('./backends/ipc/share');
var logger = require('winston').loggers.get('default');
/**
* SMB Server
*
* events:
* - error: error
* - started
* - terminated
* - shareConnected: shareName
* - shareDisconnected: shareName
* - fileCreated: shareName, path
* - folderCreated: shareName, path
* - fileDeleted: shareName, path
* - folderDeleted: shareName, path
* - itemMoved: shareName, oldPath, newPath
* - folderListed: shareName, path
*
* @param {Object} config - configuration hash
* @param {Authenticator} authenticator
* @constructor
*/
function SMBServer(config, authenticator) {
// call the super constructor to initialize `this`
EventEmitter.call(this);
this.tcpServer = net.createServer();
this.connections = {};
this.logins = {};
this.sessions = {};
this.shares = {};
this.trees = {};
// todo load/persist generated server guid
this.guid = utils.generateRawUUID();
this.domainName = config && config.domainName || '';
this.hostName = os.hostname();
this.nativeOS = os.type() + ' ' + os.release();
this.nativeLanMan = common.NATIVE_LANMAN;
this.config = config && _.cloneDeep(config) || {};
this.authenticator = authenticator || new DefaultAuthenticator(config);
// init shares
var self = this;
_.forEach(config.shares,
function (shareCfg, name) {
var type = shareCfg.backend;
var Share = require('./backends/' + type + '/share');
name = name.toUpperCase(); // share names are uppercase
var share = new SMBShare(self, new Share(name, shareCfg));
self.shares[name] = share;
var shareEvents = share.getEvents();
var forwardEvent = function (eventName) {
share.on(eventName, function (arg) {
logger.debug('%d: received %s event from share. forwarding', new Date().getTime(), eventName);
self.emit(eventName, arg);
});
};
for (var i = 0; i < shareEvents.length; i++) {
logger.debug('registering share %s event', shareEvents[i]);
forwardEvent(shareEvents[i]);
}
}
);
// add IPC$ share
this.shares['IPC$'] = new SMBShare(this, new IPCShare('IPC$', {}));
this.tcpServer.on('connection', function (socket) {
socket.setNoDelay(true);
socket.id = ++SMBServer.connectionIdCounter;
logger.info('established client connection #%d from [%s:%d] -> [%s:%d]', socket.id, socket.remoteAddress, socket.remotePort, socket.localAddress, socket.localPort);
// setup socket event handlers
socket.on('end', function () {
logger.info('client #%d disconnected (received: %dkb, sent: %dkb)', socket.id, Math.floor(socket.bytesRead / 1000), Math.floor(socket.bytesWritten / 1000));
});
socket.on('error', function (err) {
logger.info('client #%d [%s:%d] connection error', socket.id, socket.remoteAddress, socket.remotePort, err);
logger.error(err);
});
socket.on('close', function (hadError) {
delete self.connections[socket.id];
});
// create a new SMBConnection instance per tcp socket connection
self.connections[socket.id] = new SMBConnection(socket, self);
});
this.tcpServer.on('error', this.onError.bind(this));
this.tcpServer.on('close', this.onClose.bind(this));
}
util.inherits(SMBServer, EventEmitter);
SMBServer.connectionIdCounter = 0;
SMBServer.prototype.onError = function (err) {
logger.error(err);
this.emit('error', err);
};
SMBServer.prototype.onClose = function () {
logger.info('[%s] SMB server stopped', process.pid);
this.emit('terminated');
};
SMBServer.prototype.start = function (port, host, cb) {
var self = this;
this.tcpServer.listen(port, host, function () {
var realPort = this.address().port;
logger.info('[%s] SMB server started listening on port %d', process.pid, realPort);
self.emit('started');
self.tsStarted = Date.now();
cb();
});
};
SMBServer.prototype.stop = function (cb) {
this.tcpServer.close(function (err) {
if (err) {
logger.error(err);
}
cb(err);
});
};
SMBServer.prototype.getGuid = function () {
return this.guid;
};
SMBServer.prototype.getStartTime = function () {
return this.tsStarted;
};
SMBServer.prototype.createLogin = function () {
var login = new SMBLogin(this, ntlm.createChallenge());
// register login
this.logins[login.key] = login;
return login;
};
SMBServer.prototype.getLogin = function (key) {
return this.logins[key];
};
SMBServer.prototype.destroyLogin = function (key) {
delete this.logins[key];
};
/**
*
* @param {SMBLogin} login
* @param {String} accountName
* @param {String} primaryDomain
* @param {Buffer} caseInsensitivePassword
* @param {Buffer} caseSensitivePassword
* @param {Function} cb callback called with the authenticated session
* @param {String|Error} cb.error error (non-null if an error occurred)
* @param {SMBSession} cb.session authenticated session
*/
SMBServer.prototype.setupSession = function (login, accountName, primaryDomain, caseInsensitivePassword, caseSensitivePassword, cb) {
var self = this;
this.authenticator.authenticate(login.challenge, caseInsensitivePassword, caseSensitivePassword, primaryDomain, accountName, function (err, session) {
if (err) {
cb(err);
return;
}
var smbSession = new SMBSession(self, accountName, primaryDomain, session);
// register session
self.sessions[smbSession.uid] = smbSession;
cb(null, smbSession);
});
};
SMBServer.prototype.getSession = function (uid) {
return this.sessions[uid];
};
SMBServer.prototype.destroySession = function (uid) {
delete this.sessions[uid];
};
SMBServer.prototype.getShareNames = function () {
return _.keys(this.shares);
};
SMBServer.prototype.listShares = function () {
var result = [];
_.forEach(this.shares, function (share, nm) {
result.push({ name: share.getName(), description: share.getDescription() });
});
return result;
};
/**
* Refresh a specific folder on a specific share.
*
* @param {String} shareName
* @param {String} folderPath
* @param {Boolean} deep
* @param {Function} cb callback called on completion
* @param {String|Error} cb.error error (non-null if an error occurred)
*/
SMBServer.prototype.refresh = function (shareName, folderPath, deep, cb) {
// share names are uppercase
shareName = shareName.toUpperCase();
if (!this.shares[shareName]) {
process.nextTick(function () { cb(new Error('share not found')); });
return;
}
// walk connected trees and find tree associated with specified share
var tree = null;
_.forOwn(this.trees, function (t) {
if (t.getShare().getName() === shareName) {
// found matching connected share
tree = t;
return false;
}
});
if (!tree) {
process.nextTick(function () { cb(new Error('share not connected')); });
return;
}
tree.refresh(folderPath, deep, cb);
};
/**
*
* @param {SMBSession} session
* @param {String} shareName
* @param {Buffer|String} shareLevelPassword optional share-level password (may be null)
* @param {Function} cb callback called with the connect tree
* @param {String|Error} cb.error error (non-null if an error occurred)
* @param {SMBSession} cb.session authenticated session
*/
SMBServer.prototype.connectTree = function (session, shareName, shareLevelPassword, cb) {
var share = this.shares[shareName];
if (!share) {
process.nextTick(function () { cb(new Error('share not found')); });
return;
}
var self = this;
share.connect(session, shareLevelPassword, function (err, tree) {
if (err) {
cb(err);
} else {
// register tree
self.trees[tree.tid] = tree;
cb(null, tree);
// emit event
self.emit('shareConnected', shareName);
}
});
};
SMBServer.prototype.getTree = function (tid) {
return this.trees[tid];
};
SMBServer.prototype.disconnectTree = function (tid) {
var tree = this.trees[tid];
if (tree) {
var shareName = tree.getShare().getName();
tree.disconnect();
delete this.trees[tid];
// emit event
this.emit('shareDisconnected', shareName);
}
};
/**
* Clears the server's cache.
* @param {function} cb Will be invoked when the operation is complete.
* @param {string|Error} cb.err Will be truthy if there were errors during the operation.
*/
SMBServer.prototype.clearCache = function (cb) {
async.each(this.trees, function (t, callback) {
t.clearCache(callback);
}, cb);
};
module.exports = SMBServer;