diff --git a/server/api/document/storage/webdav/index.js b/server/api/document/storage/webdav/index.js index d580b2f..174a790 100644 --- a/server/api/document/storage/webdav/index.js +++ b/server/api/document/storage/webdav/index.js @@ -1,453 +1,459 @@ 'use strict'; var _ = require('lodash'); var dav = require('dav'); var async = require('async'); var mongoose = require('mongoose'); var https = require('https'); var fs = require('fs'); var url = require('url'); var path = require('path'); var Grid = require('gridfs-stream'); var multer = require('multer'); var request = require('request'); var querystring = require('querystring'); +var crypto = require('crypto'); var config = require('../../../../config/environment'); var User = require('../../../user/user.model'); var Document = require('../../document.model').Document; var DocumentChunk = require('../../document.model').DocumentChunk; var Template = require('../../../template/template.model'); var gfs = Grid(mongoose.connection.db, mongoose.mongo); var serverUrl = config.storage.webdav.server, serverPath = config.storage.webdav.path; +function decrypt(password) { + var decipher = crypto.createDecipher('aes-256-cbc', config.storage.webdav.key); + return decipher.update(password, 'base64', 'utf8') + decipher.final('utf8'); +} + function makeDavClient (user) { return new dav.Client( new dav.transport.Basic(new dav.Credentials({ username: user.webdav.username, - password: user.webdav.password + password: decrypt(user.webdav.password) })), { baseUrl: serverUrl } ); } function saveToGridFS(user, href, fileId, cb) { var file = gfs.createWriteStream({ _id: fileId, filename: href.split('/').pop(), mode: 'w', chunkSize: 1024 * 4, content_type: 'application/vnd.oasis.opendocument.text', root: 'fs' }); request.get({ url: serverUrl + href, auth: { user: user.webdav.username, - pass: user.webdav.password, + pass: decrypt(user.webdav.password) } }) .on('error', function (err) { cb(err); }) .pipe(file); file.on('finish', cb); } function makeContentId(webdavDoc) { return webdavDoc.props.getetag + webdavDoc.props.getlastmodified; } // The WebDAV server is always authoritative. function synchronizeUserFilesToDB(user, webdavDocuments, objectCache, cb) { Document.find({ '$or': [ { 'creator': user._id }, { 'editors': { '$in': [user._id] } } ] }) .populate('creator', 'name email').exec(function (err, dbDocuments) { // Since we're doing things that could possibly invalidate DB documents, // we have to work with the live cache var persistenceQueue = []; var finalDocuments = []; dbDocuments.forEach(function (dbDoc) { var trackedDoc; if (dbDoc.creator._id.equals(user._id)) { trackedDoc = objectCache.getTrackedObject(dbDoc); finalDocuments.push(trackedDoc); } else { finalDocuments.push(dbDoc); return; } var sameHrefDoc = _.find(webdavDocuments, function (doc) { return doc.href === trackedDoc.webdav.href; }); var sameContentDoc = _.find(webdavDocuments, function (doc) { return makeContentId(doc) === trackedDoc.webdav.contentId; }); if (!sameContentDoc) { if (!sameHrefDoc) { // Document has been effectively deleted, take it offline and remove trackedDoc.live = false; persistenceQueue.push(function (cb) { trackedDoc.remove(cb); }); finalDocuments.pop(); } else { // Document has been modified, take it offline, throw away local changes, and update content ID trackedDoc.live = false; trackedDoc.chunks.length = 0; trackedDoc.markModified('chunks'); trackedDoc.webdav.contentId = makeContentId(sameHrefDoc); trackedDoc.date = Date.parse(sameHrefDoc.props.getlastmodified); trackedDoc.markModified('webdav'); persistenceQueue.push(function (cb) { trackedDoc.save(cb); }); } } else { if (!sameHrefDoc) { // Document has been moved, just update href and save without interrupting anything trackedDoc.webdav.href = sameContentDoc.href; trackedDoc.markModified('webdav'); persistenceQueue.push(function (cb) { trackedDoc.save(cb); }); } else { if (makeContentId(sameHrefDoc) === makeContentId(sameContentDoc)) { // nothing changed } else { // Document moved to sameContentDoc.props.href and there is a new doc at sameHrefDoc.href trackedDoc.webdav.href = sameContentDoc.href; trackedDoc.markModified('webdav'); persistenceQueue.push(function (cb) { trackedDoc.save(cb); }); } } } }); // Once modified documents are persisted, purge non-live documents from the cache async.parallel(persistenceQueue, function (err) { if (err) return cb(err); dbDocuments.forEach(function (dbDoc) { if (objectCache.isTracked(dbDoc) && !dbDoc.live) { objectCache.forgetTrackedObject(dbDoc); } }); return cb(null, finalDocuments); }); }); } exports.index = function (req, res) { var davClient = makeDavClient(req.user); // Retrieve a list of files and filter by mimetype davClient.send(dav.request.propfind({ depth: 'infinity', props: [ { name: 'getcontenttype', namespace: dav.ns.DAV }, { name: 'getetag', namespace: dav.ns.DAV }, { name: 'getlastmodified', namespace: dav.ns.DAV } ] }), serverPath) .then( function success(response) { var webdavDocuments = _(response) .filter(function (item) { return item.props.getcontenttype === 'application/vnd.oasis.opendocument.text'; }) .map(function (item) { item.href = querystring.unescape(item.href); return item; }) .value(); synchronizeUserFilesToDB(req.user, webdavDocuments, req.app.get('objectCache'), function (err, updatedDocuments) { if (err) return handleError(res, err); // Transform the webdavDocuments array into something more usable // The fake ID of this document is represented as a combination of it's href and content id webdavDocuments = webdavDocuments.map(function (doc) { return { _id: new Buffer(doc.href + '__' + makeContentId(doc)).toString('base64'), title: doc.href.split('/').pop(), creator: req.user.profile, date: Date.parse(doc.props.getlastmodified), webdav: { href: doc.href, contentId: makeContentId(doc) } }; }); // Merge the two document types, such that hrefs are unique in the resulting list. DB docs have priority here var mergedDocuments = _.uniq(updatedDocuments.concat(webdavDocuments), function (doc) { return (doc.webdav && doc.webdav.contentId) || makeContentId(doc); }); return res.json(200, mergedDocuments); }); }, function failure(error) { handleError(res, error); }); }; function createFirstChunk(user, href, cb) { var chunkId = new mongoose.Types.ObjectId(), fileId = new mongoose.Types.ObjectId(); var firstChunk = new DocumentChunk({ _id: chunkId, snapshot: { fileId: fileId } }); saveToGridFS(user, href, fileId, function(err) { if (err) { return cb(err); } firstChunk.save(function (err) { cb(err, firstChunk); }); }); } exports.show = function(req, res) { if (mongoose.Types.ObjectId.isValid(req.params.id)) { Document.findById(req.params.id, function (err, document) { if(err) { return handleError(res, err); } if(!document) { return res.send(404); } if (document.chunks.length) { return res.json(200, document); } // If the document had been invalidated, it has no chunks, so generate one createFirstChunk(req.user, document.webdav.href, function (err, firstChunk) { if (err) { return handleError(res, err); } document.chunks.push(firstChunk); document.save(function () { res.json(200, document); }) }); }); } else { var identifier = new Buffer(req.params.id, 'base64').toString('ascii').split('__'), href = identifier[0], contentId = identifier[1]; Document.findOne({ 'webdav.contentId': contentId, }, function (err, document) { if (err) { return handleError(res, err); } if (document) { return res.json(200, document); } else { var davClient = makeDavClient(req.user); davClient.send(dav.request.propfind({ props: [] }), href) .then( function success(response) { var webdavDoc = response[0]; createFirstChunk(req.user, href, function(err, firstChunk) { if (err) { return handleError(res, err); } Document.create({ title: href.split('/').pop(), creator: req.user._id, created: Date.parse(webdavDoc.props.getlastmodified), date: Date.parse(webdavDoc.props.getlastmodified), chunks: [firstChunk._id], webdav: { href: href, contentId: makeContentId(webdavDoc) } }, function (err, document) { if (err) { return handleError(res, err); } res.json(201, document); }); }) }, function failure(error) { res.send(404, error); }); } }); } }; function uploadToServer(user, readStream, href, replace, cb) { var nonConflictingPath; function upload () { readStream.pipe(request.put({ url: nonConflictingPath, auth: { user: user.webdav.username, - pass: user.webdav.password, + pass: decrypt(user.webdav.password) }, headers: { 'Content-Type': 'application/vnd.oasis.opendocument.text' } }, cb)); } if (replace) { nonConflictingPath = serverUrl + href; upload(); } else { makeDavClient(user).send(dav.request.propfind({ depth: 1, props: [ { name: 'getcontenttype', namespace: dav.ns.DAV }, { name: 'getetag', namespace: dav.ns.DAV }, { name: 'getlastmodified', namespace: dav.ns.DAV } ] }), path.dirname(href)) .then( function success(response) { function makePath(dir, basename, ext) { return dir + '/' + basename + ext; } var files = _(response) .filter(function (item) { return item.props.getcontenttype === 'application/vnd.oasis.opendocument.text'; }).map(function (item) { item.href = querystring.unescape(item.href); return item; }).value(); var iteration = 0, extension = path.extname(href), basename = path.basename(href, extension), basename_i = basename, dirname = path.dirname(href); for (var i = 0; i < files.length; i++) { if (files[i].href === makePath(dirname, basename_i, extension)) { iteration++; basename_i = basename + ' (' + iteration + ')'; i = -1; } } nonConflictingPath = serverUrl + makePath(dirname, basename_i, extension); upload(); }, function failure(err) { cb(err); }); } } exports.upload = function (req, res, next) { multer({ upload: null, limits: { fileSize: 1024 * 1024 * 20, // 20 Megabytes files: 5 }, onFileUploadComplete: function (file) { uploadToServer(req.user, fs.createReadStream(file.path), serverPath + '/' + file.originalname, false, function (err) { if (err) { console.log (err); } } ); } })(req, res, next); }; exports.createFromTemplate = function (req, res) { Template.findById(req.params.id, function (err, template) { if (err) { return handleError(res, err); } if (!template) { return res.send(404); } var chunkId = new mongoose.Types.ObjectId(); var firstChunk = new DocumentChunk({ _id: chunkId, snapshot: { fileId: template.fileId } }); var newDocument = new Document({ title: template.title, creator: req.user._id, chunks: [chunkId] }); uploadToServer( req.user, gfs.createReadStream({ _id: template.fileId }), serverPath + '/' + template.title + '.odt', false, function (err, response) { if (err) { return handleError(res.err); } newDocument.webdav = { href: response.request.uri.path, contentId: response.headers.etag + response.headers.date }; firstChunk.save(function (err) { if (!err) { newDocument.save(function (err) { return res.json(201, newDocument); }); } }); } ); }); }; exports.createChunkFromSnapshot = function (document, snapshot, cb) { var chunkId = new mongoose.Types.ObjectId(), fileId = new mongoose.Types.ObjectId(); var writeStream = gfs.createWriteStream({ _id: fileId, filename: document.title + '_' + Date.now(), mode: 'w', chunkSize: 1024 * 4, content_type: 'application/vnd.oasis.opendocument.text', root: 'fs' }); writeStream.end(new Buffer(snapshot.data), function () { User.findById(document.creator._id, function (err, user) { if (err) { return cb(err); } uploadToServer( user, gfs.createReadStream({ _id: fileId }), document.webdav.href, true, function (err, response) { if (err) { return cb(err); } var chunk = new DocumentChunk({ _id: chunkId, sequence: snapshot.sequence, snapshot: { fileId: fileId, operations: snapshot.operations } }); chunk.save(function (err) { if (err) { return cb(err); } document.webdav.contentId = response.headers.etag + response.headers.date; document.chunks.push(chunkId); document.markModified('chunks'); cb(null, chunk); }); }); }); }); }; function handleError(res, err) { console.log(err); return res.send(500, err); } diff --git a/server/auth/ldap/passport.js b/server/auth/ldap/passport.js index de4931a..cd857ba 100644 --- a/server/auth/ldap/passport.js +++ b/server/auth/ldap/passport.js @@ -1,55 +1,61 @@ var passport = require('passport'); var LdapStrategy = require('passport-ldapauth'); +var crypto = require('crypto'); exports.setup = function (User, config) { - passport.use(new LdapStrategy({ - server: { - url: config.auth.ldap.server, - searchBase: config.auth.ldap.base, - searchFilter: config.auth.ldap.filter, - bindDn: config.auth.ldap.bindDn, - bindCredentials: config.auth.ldap.bindPw, - searchAttributes: ['cn'] - }, - usernameField: 'email', - passwordField: 'password', - passReqToCallback: true, - badRequestMessage: true, - invalidCredentials: true, - userNotFound: true, - constraintViolation: true + function encrypt(password) { + var cipher = crypto.createCipher('aes-256-cbc', config.storage.webdav.key); + return cipher.update(password, 'utf8', 'base64') + cipher.final('base64') + } + + passport.use(new LdapStrategy({ + server: { + url: config.auth.ldap.server, + searchBase: config.auth.ldap.base, + searchFilter: config.auth.ldap.filter, + bindDn: config.auth.ldap.bindDn, + bindCredentials: config.auth.ldap.bindPw, + searchAttributes: ['cn'] + }, + usernameField: 'email', + passwordField: 'password', + passReqToCallback: true, + badRequestMessage: true, + invalidCredentials: true, + userNotFound: true, + constraintViolation: true }, function(req, ldapUser, done) { var email = req.body.email, password = req.body.password, fullName = ldapUser.cn; User.findOne({ email: email }, function (err, user) { if (err) { return done(err); } if (!user) { var newUser = new User({ name: fullName, email: email, provider: 'ldap', role: 'user', webdav: (config.storage.type === 'webdav') ? { username: email, - password: password + password: encrypt(password) } : undefined }); newUser.save(function (err, user) { if (err) { return done(err); } if (!err) { return done(null, user); } }); } else { return done(null, user); } }); } )); }; diff --git a/server/auth/webdav/passport.js b/server/auth/webdav/passport.js index a1173c9..7101f6b 100644 --- a/server/auth/webdav/passport.js +++ b/server/auth/webdav/passport.js @@ -1,58 +1,64 @@ var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy; var dav = require('dav'); +var crypto = require('crypto'); exports.setup = function (User, config) { + function encrypt(password) { + var cipher = crypto.createCipher('aes-256-cbc', config.storage.webdav.key); + return cipher.update(password, 'utf8', 'base64') + cipher.final('base64') + } + passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password' // this is the virtual field on the model }, function(email, password, done) { var client = new dav.Client( new dav.transport.Basic(new dav.Credentials({ username: email, password: password })), { baseUrl: config.storage.webdav.server } ); client.send( dav.request.basic({ method: 'OPTIONS', data: ''}), config.storage.webdav.path ).then( function success() { User.findOne({ email: email }, function (err, user) { if (err) { return done(err); } if (!user) { var newUser = new User({ name: email, email: email, provider: 'webdav', role: 'user', webdav: (config.storage.type === 'webdav') ? { username: email, - password: password + password: encrypt(password) } : undefined }); newUser.save(function (err, user) { if (err) { return done(err); } if (!err) { return done(null, user); } }); } else { return done(null, user); } }); }, function failure(err) { return done(err); } ); } )); }; diff --git a/server/config/environment/index.js b/server/config/environment/index.js index 1b4ea41..f1b3de7 100644 --- a/server/config/environment/index.js +++ b/server/config/environment/index.js @@ -1,71 +1,73 @@ 'use strict'; var path = require('path'); var _ = require('lodash'); function requiredProcessEnv(name) { if(!process.env[name]) { throw new Error('You must set the ' + name + ' environment variable'); } return process.env[name]; } // All configurations will extend these options // ============================================ var all = { env: process.env.NODE_ENV, // Root path of server root: path.normalize(__dirname + '/../../..'), // Server port port: process.env.PORT || 9000, // Should we populate the DB with sample data? seedDB: true, // Secret for session, you will want to change this and make it an environment variable secrets: { session: 'manticore-secret' }, // List of user roles userRoles: ['guest', 'user', 'admin'], // MongoDB connection options mongo: { options: { db: { safe: true } } }, auth: { type: process.env.AUTH || 'local', 'webdav': { server: process.env.WEBDAV_SERVER, - path: process.env.WEBDAV_PATH + path: process.env.WEBDAV_PATH, + key: process.env.WEBDAV_ENCRYPTION_KEY }, 'ldap': { server: process.env.LDAP_SERVER, base: process.env.LDAP_BASE, filter: process.env.LDAP_FILTER, bindDn: process.env.LDAP_BIND_DN, bindPw: process.env.LDAP_BIND_PW } }, storage: { type: process.env.STORAGE || 'local', 'webdav': { server: process.env.WEBDAV_SERVER, - path: process.env.WEBDAV_PATH + path: process.env.WEBDAV_PATH, + key: process.env.WEBDAV_ENCRYPTION_KEY } } }; // Export the config object based on the NODE_ENV // ============================================== module.exports = _.merge( all, require('./' + process.env.NODE_ENV + '.js') || {}); diff --git a/server/config/local.env.sample.js b/server/config/local.env.sample.js index 33ec8f0..e2f20cf 100644 --- a/server/config/local.env.sample.js +++ b/server/config/local.env.sample.js @@ -1,42 +1,46 @@ 'use strict'; // Use local.env.js for environment variables that grunt will set when the server starts locally. // Use for your api keys, secrets, etc. This file should not be tracked by git. // // You will need to set these on the server you deploy to. module.exports = { DOMAIN: 'http://localhost:9000', SESSION_SECRET: 'manticore-secret', // Control debug level for modules using visionmedia/debug DEBUG: '', /* * Supported authentication strategies. * 1. 'local' for using Manticore's built-in accounts system. Allow signups. * 2. 'webdav' for authenticating against a WebDAV server. Only login, no signups. * 3. 'ldap' for authenticating against an LDAP service. Only login, no signups. */ AUTH: 'local', /* * Supported storage backends. * 1. 'local' for storing everything in Mongo/GridFS. The fastest and most reliable way. * Can be used with any AUTH strategy. * 2. 'webdav' for two-way synchronizing of documents with a WebDAV server. * Can be used if AUTH is 'ldap' or 'webdav'; those credentials are used to talk to the webdav server. */ STORAGE: 'local', - // WebDAV server config, required iff AUTH or STORAGE is 'webdav'. + /* + * WebDAV server config, only if AUTH or STORAGE is 'webdav'. + * Make sure you provide an encryption key to protect users' webdav credentials. + */ WEBDAV_SERVER: 'https://kolabmachine', WEBDAV_PATH: '/iRony/files/Files', + WEBDAV_CREDENTIALS_KEY: 'suchweb123muchdav456', // LDAP server config, required iff AUTH is 'ldap'. {{username}} will be replaced with users' logins LDAP_SERVER: 'ldaps://kolabmachine', LDAP_BASE: 'ou=People,dc=test,dc=example,dc=org', LDAP_FILTER: '(&(objectclass=person)(|(uid={{username}})(mail={{username}})))', LDAP_BIND_DN: 'uid=kolab-service,ou=Special Users,dc=test,dc=example,dc=org', LDAP_BIND_PW: 'kolab-service-pass' };