diff options
Diffstat (limited to 'lib')
39 files changed, 1193 insertions, 468 deletions
diff --git a/lib/auth.js b/lib/auth.js deleted file mode 100644 index 3e129b95..00000000 --- a/lib/auth.js +++ /dev/null @@ -1,194 +0,0 @@ -'use strict' -// auth -// external modules -var passport = require('passport') -var FacebookStrategy = require('passport-facebook').Strategy -var TwitterStrategy = require('passport-twitter').Strategy -var GithubStrategy = require('passport-github').Strategy -var GitlabStrategy = require('passport-gitlab2').Strategy -var DropboxStrategy = require('passport-dropbox-oauth2').Strategy -var GoogleStrategy = require('passport-google-oauth20').Strategy -var LdapStrategy = require('passport-ldapauth') -var LocalStrategy = require('passport-local').Strategy -var validator = require('validator') - -// core -var config = require('./config.js') -var logger = require('./logger.js') -var models = require('./models') - -function callback (accessToken, refreshToken, profile, done) { - // logger.info(profile.displayName || profile.username); - var stringifiedProfile = JSON.stringify(profile) - models.User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile, - accessToken: accessToken, - refreshToken: refreshToken - } - }).spread(function (user, created) { - if (user) { - var needSave = false - if (user.profile !== stringifiedProfile) { - user.profile = stringifiedProfile - needSave = true - } - if (user.accessToken !== accessToken) { - user.accessToken = accessToken - needSave = true - } - if (user.refreshToken !== refreshToken) { - user.refreshToken = refreshToken - needSave = true - } - if (needSave) { - user.save().then(function () { - if (config.debug) { logger.info('user login: ' + user.id) } - return done(null, user) - }) - } else { - if (config.debug) { logger.info('user login: ' + user.id) } - return done(null, user) - } - } - }).catch(function (err) { - logger.error('auth callback failed: ' + err) - return done(err, null) - }) -} - -function registerAuthMethod () { -// facebook - if (config.facebook) { - passport.use(new FacebookStrategy({ - clientID: config.facebook.clientID, - clientSecret: config.facebook.clientSecret, - callbackURL: config.serverurl + '/auth/facebook/callback' - }, callback)) - } -// twitter - if (config.twitter) { - passport.use(new TwitterStrategy({ - consumerKey: config.twitter.consumerKey, - consumerSecret: config.twitter.consumerSecret, - callbackURL: config.serverurl + '/auth/twitter/callback' - }, callback)) - } -// github - if (config.github) { - passport.use(new GithubStrategy({ - clientID: config.github.clientID, - clientSecret: config.github.clientSecret, - callbackURL: config.serverurl + '/auth/github/callback' - }, callback)) - } -// gitlab - if (config.gitlab) { - passport.use(new GitlabStrategy({ - baseURL: config.gitlab.baseURL, - clientID: config.gitlab.clientID, - clientSecret: config.gitlab.clientSecret, - scope: config.gitlab.scope, - callbackURL: config.serverurl + '/auth/gitlab/callback' - }, callback)) - } -// dropbox - if (config.dropbox) { - passport.use(new DropboxStrategy({ - apiVersion: '2', - clientID: config.dropbox.clientID, - clientSecret: config.dropbox.clientSecret, - callbackURL: config.serverurl + '/auth/dropbox/callback' - }, callback)) - } -// google - if (config.google) { - passport.use(new GoogleStrategy({ - clientID: config.google.clientID, - clientSecret: config.google.clientSecret, - callbackURL: config.serverurl + '/auth/google/callback' - }, callback)) - } -// ldap - if (config.ldap) { - passport.use(new LdapStrategy({ - server: { - url: config.ldap.url || null, - bindDn: config.ldap.bindDn || null, - bindCredentials: config.ldap.bindCredentials || null, - searchBase: config.ldap.searchBase || null, - searchFilter: config.ldap.searchFilter || null, - searchAttributes: config.ldap.searchAttributes || null, - tlsOptions: config.ldap.tlsOptions || null - } - }, - function (user, done) { - var profile = { - id: 'LDAP-' + user.uidNumber, - username: user.uid, - displayName: user.displayName, - emails: user.mail ? [user.mail] : [], - avatarUrl: null, - profileUrl: null, - provider: 'ldap' - } - var stringifiedProfile = JSON.stringify(profile) - models.User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile - } - }).spread(function (user, created) { - if (user) { - var needSave = false - if (user.profile !== stringifiedProfile) { - user.profile = stringifiedProfile - needSave = true - } - if (needSave) { - user.save().then(function () { - if (config.debug) { logger.info('user login: ' + user.id) } - return done(null, user) - }) - } else { - if (config.debug) { logger.info('user login: ' + user.id) } - return done(null, user) - } - } - }).catch(function (err) { - logger.error('ldap auth failed: ' + err) - return done(err, null) - }) - })) - } -// email - if (config.email) { - passport.use(new LocalStrategy({ - usernameField: 'email' - }, - function (email, password, done) { - if (!validator.isEmail(email)) return done(null, false) - models.User.findOne({ - where: { - email: email - } - }).then(function (user) { - if (!user) return done(null, false) - if (!user.verifyPassword(password)) return done(null, false) - return done(null, user) - }).catch(function (err) { - logger.error(err) - return done(err) - }) - })) - } -} - -module.exports = { - registerAuthMethod: registerAuthMethod -} diff --git a/lib/config.js b/lib/config.js deleted file mode 100644 index 31999b07..00000000 --- a/lib/config.js +++ /dev/null @@ -1,223 +0,0 @@ -'use strict' -// external modules -var fs = require('fs') -var path = require('path') - -// configs -var env = process.env.NODE_ENV || 'development' -var config = require(path.join(__dirname, '..', 'config.json'))[env] -var debug = process.env.DEBUG ? (process.env.DEBUG === 'true') : ((typeof config.debug === 'boolean') ? config.debug : (env === 'development')) - -// Create function that reads docker secrets but fails fast in case of a non docker environment -var handleDockerSecret = fs.existsSync('/run/secrets/') ? function (secret) { - return fs.existsSync('/run/secrets/' + secret) ? fs.readFileSync('/run/secrets/' + secret) : null -} : function () { - return null -} - -// url -var domain = process.env.DOMAIN || process.env.HMD_DOMAIN || config.domain || '' -var urlpath = process.env.URL_PATH || process.env.HMD_URL_PATH || config.urlpath || '' -var port = process.env.PORT || process.env.HMD_PORT || config.port || 3000 -var alloworigin = process.env.HMD_ALLOW_ORIGIN ? process.env.HMD_ALLOW_ORIGIN.split(',') : (config.alloworigin || ['localhost']) - -var usessl = !!config.usessl -var protocolusessl = (usessl === true && typeof process.env.HMD_PROTOCOL_USESSL === 'undefined' && typeof config.protocolusessl === 'undefined') - ? true : (process.env.HMD_PROTOCOL_USESSL ? (process.env.HMD_PROTOCOL_USESSL === 'true') : !!config.protocolusessl) -var urladdport = process.env.HMD_URL_ADDPORT ? (process.env.HMD_URL_ADDPORT === 'true') : !!config.urladdport - -var usecdn = process.env.HMD_USECDN ? (process.env.HMD_USECDN === 'true') : ((typeof config.usecdn === 'boolean') ? config.usecdn : true) - -var allowanonymous = process.env.HMD_ALLOW_ANONYMOUS ? (process.env.HMD_ALLOW_ANONYMOUS === 'true') : ((typeof config.allowanonymous === 'boolean') ? config.allowanonymous : true) - -var allowfreeurl = process.env.HMD_ALLOW_FREEURL ? (process.env.HMD_ALLOW_FREEURL === 'true') : !!config.allowfreeurl - -var permissions = ['editable', 'limited', 'locked', 'protected', 'private'] -if (allowanonymous) { - permissions.unshift('freely') -} - -var defaultpermission = process.env.HMD_DEFAULT_PERMISSION || config.defaultpermission -defaultpermission = permissions.indexOf(defaultpermission) !== -1 ? defaultpermission : 'editable' - -// db -var dburl = process.env.HMD_DB_URL || process.env.DATABASE_URL || config.dburl -var db = config.db || {} - -// ssl path -var sslkeypath = (fs.existsSync('/run/secrets/key.pem') ? '/run/secrets/key.pem' : null) || config.sslkeypath || '' -var sslcertpath = (fs.existsSync('/run/secrets/cert.pem') ? '/run/secrets/cert.pem' : null) || config.sslcertpath || '' -var sslcapath = (fs.existsSync('/run/secrets/ca.pem') ? '/run/secrets/ca.pem' : null) || config.sslcapath || '' -var dhparampath = (fs.existsSync('/run/secrets/dhparam.pem') ? '/run/secrets/dhparam.pem' : null) || config.dhparampath || '' - -// other path -var tmppath = config.tmppath || './tmp' -var defaultnotepath = config.defaultnotepath || './public/default.md' -var docspath = config.docspath || './public/docs' -var indexpath = config.indexpath || './public/views/index.ejs' -var hackmdpath = config.hackmdpath || './public/views/hackmd.ejs' -var errorpath = config.errorpath || './public/views/error.ejs' -var prettypath = config.prettypath || './public/views/pretty.ejs' -var slidepath = config.slidepath || './public/views/slide.ejs' - -// session -var sessionname = config.sessionname || 'connect.sid' -var sessionsecret = handleDockerSecret('sessionsecret') || config.sessionsecret || 'secret' -var sessionlife = config.sessionlife || 14 * 24 * 60 * 60 * 1000 // 14 days - -// static files -var staticcachetime = config.staticcachetime || 1 * 24 * 60 * 60 * 1000 // 1 day - -// socket.io -var heartbeatinterval = config.heartbeatinterval || 5000 -var heartbeattimeout = config.heartbeattimeout || 10000 - -// document -var documentmaxlength = config.documentmaxlength || 100000 - -// image upload setting, available options are imgur/s3/filesystem -var imageUploadType = process.env.HMD_IMAGE_UPLOAD_TYPE || config.imageUploadType || 'imgur' - -config.s3 = config.s3 || {} -var s3 = { - accessKeyId: handleDockerSecret('s3_acccessKeyId') || process.env.HMD_S3_ACCESS_KEY_ID || config.s3.accessKeyId, - secretAccessKey: handleDockerSecret('s3_secretAccessKey') || process.env.HMD_S3_SECRET_ACCESS_KEY || config.s3.secretAccessKey, - region: process.env.HMD_S3_REGION || config.s3.region -} -var s3bucket = process.env.HMD_S3_BUCKET || config.s3.bucket - -// auth -var facebook = ((process.env.HMD_FACEBOOK_CLIENTID && process.env.HMD_FACEBOOK_CLIENTSECRET) || (fs.existsSync('/run/secrets/facebook_clientID') && fs.existsSync('/run/secrets/facebook_clientSecret'))) ? { - clientID: handleDockerSecret('facebook_clientID') || process.env.HMD_FACEBOOK_CLIENTID, - clientSecret: handleDockerSecret('facebook_clientSecret') || process.env.HMD_FACEBOOK_CLIENTSECRET -} : config.facebook || false -var twitter = ((process.env.HMD_TWITTER_CONSUMERKEY && process.env.HMD_TWITTER_CONSUMERSECRET) || (fs.existsSync('/run/secrets/twitter_consumerKey') && fs.existsSync('/run/secrets/twitter_consumerSecret'))) ? { - consumerKey: handleDockerSecret('twitter_consumerKey') || process.env.HMD_TWITTER_CONSUMERKEY, - consumerSecret: handleDockerSecret('twitter_consumerSecret') || process.env.HMD_TWITTER_CONSUMERSECRET -} : config.twitter || false -var github = ((process.env.HMD_GITHUB_CLIENTID && process.env.HMD_GITHUB_CLIENTSECRET) || (fs.existsSync('/run/secrets/github_clientID') && fs.existsSync('/run/secrets/github_clientSecret'))) ? { - clientID: handleDockerSecret('github_clientID') || process.env.HMD_GITHUB_CLIENTID, - clientSecret: handleDockerSecret('github_clientSecret') || process.env.HMD_GITHUB_CLIENTSECRET -} : config.github || false -var gitlab = ((process.env.HMD_GITLAB_CLIENTID && process.env.HMD_GITLAB_CLIENTSECRET) || (fs.existsSync('/run/secrets/gitlab_clientID') && fs.existsSync('/run/secrets/gitlab_clientSecret'))) ? { - baseURL: process.env.HMD_GITLAB_BASEURL, - clientID: handleDockerSecret('gitlab_clientID') || process.env.HMD_GITLAB_CLIENTID, - clientSecret: handleDockerSecret('gitlab_clientSecret') || process.env.HMD_GITLAB_CLIENTSECRET, - scope: process.env.HMD_GITLAB_SCOPE -} : (config.gitlab && config.gitlab.clientID && config.gitlab.clientSecret && config.gitlab) || false -var dropbox = ((process.env.HMD_DROPBOX_CLIENTID && process.env.HMD_DROPBOX_CLIENTSECRET) || (fs.existsSync('/run/secrets/dropbox_clientID') && fs.existsSync('/run/secrets/dropbox_clientSecret'))) ? { - clientID: handleDockerSecret('dropbox_clientID') || process.env.HMD_DROPBOX_CLIENTID, - clientSecret: handleDockerSecret('dropbox_clientSecret') || process.env.HMD_DROPBOX_CLIENTSECRET -} : (config.dropbox && config.dropbox.clientID && config.dropbox.clientSecret && config.dropbox) || false -var google = ((process.env.HMD_GOOGLE_CLIENTID && process.env.HMD_GOOGLE_CLIENTSECRET) || - (fs.existsSync('/run/secrets/google_clientID') && fs.existsSync('/run/secrets/google_clientSecret'))) ? { - clientID: handleDockerSecret('google_clientID') || process.env.HMD_GOOGLE_CLIENTID, - clientSecret: handleDockerSecret('google_clientSecret') || process.env.HMD_GOOGLE_CLIENTSECRET - } : (config.google && config.google.clientID && config.google.clientSecret && config.google) || false -var ldap = config.ldap || (( - process.env.HMD_LDAP_URL || - process.env.HMD_LDAP_BINDDN || - process.env.HMD_LDAP_BINDCREDENTIALS || - process.env.HMD_LDAP_TOKENSECRET || - process.env.HMD_LDAP_SEARCHBASE || - process.env.HMD_LDAP_SEARCHFILTER || - process.env.HMD_LDAP_SEARCHATTRIBUTES || - process.env.HMD_LDAP_TLS_CA || - process.env.HMD_LDAP_PROVIDERNAME -) ? {} : false) -if (process.env.HMD_LDAP_URL) { ldap.url = process.env.HMD_LDAP_URL } -if (process.env.HMD_LDAP_BINDDN) { ldap.bindDn = process.env.HMD_LDAP_BINDDN } -if (process.env.HMD_LDAP_BINDCREDENTIALS) { ldap.bindCredentials = process.env.HMD_LDAP_BINDCREDENTIALS } -if (process.env.HMD_LDAP_TOKENSECRET) { ldap.tokenSecret = process.env.HMD_LDAP_TOKENSECRET } -if (process.env.HMD_LDAP_SEARCHBASE) { ldap.searchBase = process.env.HMD_LDAP_SEARCHBASE } -if (process.env.HMD_LDAP_SEARCHFILTER) { ldap.searchFilter = process.env.HMD_LDAP_SEARCHFILTER } -if (process.env.HMD_LDAP_SEARCHATTRIBUTES) { ldap.searchAttributes = process.env.HMD_LDAP_SEARCHATTRIBUTES } -if (process.env.HMD_LDAP_TLS_CA) { - var ca = { - ca: process.env.HMD_LDAP_TLS_CA.split(',') - } - ldap.tlsOptions = ldap.tlsOptions ? Object.assign(ldap.tlsOptions, ca) : ca - if (Array.isArray(ldap.tlsOptions.ca) && ldap.tlsOptions.ca.length > 0) { - var i, len, results - results = [] - for (i = 0, len = ldap.tlsOptions.ca.length; i < len; i++) { - results.push(fs.readFileSync(ldap.tlsOptions.ca[i], 'utf8')) - } - ldap.tlsOptions.ca = results - } -} -if (process.env.HMD_LDAP_PROVIDERNAME) { - ldap.providerName = process.env.HMD_LDAP_PROVIDERNAME -} -var imgur = handleDockerSecret('imgur_clientid') || process.env.HMD_IMGUR_CLIENTID || config.imgur || false -var email = process.env.HMD_EMAIL ? (process.env.HMD_EMAIL === 'true') : !!config.email -var allowemailregister = process.env.HMD_ALLOW_EMAIL_REGISTER ? (process.env.HMD_ALLOW_EMAIL_REGISTER === 'true') : ((typeof config.allowemailregister === 'boolean') ? config.allowemailregister : true) - -function getserverurl () { - var url = '' - if (domain) { - var protocol = protocolusessl ? 'https://' : 'http://' - url = protocol + domain - if (urladdport && ((usessl && port !== 443) || (!usessl && port !== 80))) { url += ':' + port } - } - if (urlpath) { url += '/' + urlpath } - return url -} - -var version = '0.5.1' -var minimumCompatibleVersion = '0.5.0' -var maintenance = true -var cwd = path.join(__dirname, '..') - -module.exports = { - raw: config, - handleDockerSecret: handleDockerSecret, - version: version, - minimumCompatibleVersion: minimumCompatibleVersion, - maintenance: maintenance, - domain: domain, - urlpath: urlpath, - debug: debug, - port: port, - alloworigin: alloworigin, - usessl: usessl, - serverurl: getserverurl(), - usecdn: usecdn, - allowanonymous: allowanonymous, - allowfreeurl: allowfreeurl, - defaultpermission: defaultpermission, - dburl: dburl, - db: db, - sslkeypath: path.join(cwd, sslkeypath), - sslcertpath: path.join(cwd, sslcertpath), - sslcapath: path.join(cwd, sslcapath), - dhparampath: path.join(cwd, dhparampath), - tmppath: path.join(cwd, tmppath), - defaultnotepath: path.join(cwd, defaultnotepath), - docspath: path.join(cwd, docspath), - indexpath: path.join(cwd, indexpath), - hackmdpath: path.join(cwd, hackmdpath), - errorpath: path.join(cwd, errorpath), - prettypath: path.join(cwd, prettypath), - slidepath: path.join(cwd, slidepath), - sessionname: sessionname, - sessionsecret: sessionsecret, - sessionlife: sessionlife, - staticcachetime: staticcachetime, - heartbeatinterval: heartbeatinterval, - heartbeattimeout: heartbeattimeout, - documentmaxlength: documentmaxlength, - facebook: facebook, - twitter: twitter, - github: github, - gitlab: gitlab, - dropbox: dropbox, - google: google, - ldap: ldap, - imgur: imgur, - email: email, - allowemailregister: allowemailregister, - imageUploadType: imageUploadType, - s3: s3, - s3bucket: s3bucket -} diff --git a/lib/config/default.js b/lib/config/default.js new file mode 100644 index 00000000..a14a4294 --- /dev/null +++ b/lib/config/default.js @@ -0,0 +1,92 @@ +'use strict' + +module.exports = { + domain: '', + urlpath: '', + port: 3000, + urladdport: false, + alloworigin: ['localhost'], + usessl: false, + protocolusessl: false, + usecdn: true, + allowanonymous: true, + allowfreeurl: false, + defaultpermission: 'editable', + dburl: '', + db: {}, + // ssl path + sslkeypath: '', + sslcertpath: '', + sslcapath: '', + dhparampath: '', + // other path + tmppath: './tmp', + defaultnotepath: './public/default.md', + docspath: './public/docs', + indexpath: './public/views/index.ejs', + hackmdpath: './public/views/hackmd.ejs', + errorpath: './public/views/error.ejs', + prettypath: './public/views/pretty.ejs', + slidepath: './public/views/slide.ejs', + // session + sessionname: 'connect.sid', + sessionsecret: 'secret', + sessionlife: 14 * 24 * 60 * 60 * 1000, // 14 days + staticcachetime: 1 * 24 * 60 * 60 * 1000, // 1 day + // socket.io + heartbeatinterval: 5000, + heartbeattimeout: 10000, + // document + documentmaxlength: 100000, + // image upload setting, available options are imgur/s3/filesystem + imageUploadType: 'filesystem', + imgur: { + clientID: undefined + }, + s3: { + accessKeyId: undefined, + secretAccessKey: undefined, + region: undefined + }, + s3bucket: undefined, + // authentication + facebook: { + clientID: undefined, + clientSecret: undefined + }, + twitter: { + consumerKey: undefined, + consumerSecret: undefined + }, + github: { + clientID: undefined, + clientSecret: undefined + }, + gitlab: { + baseURL: undefined, + clientID: undefined, + clientSecret: undefined, + scope: undefined + }, + dropbox: { + clientID: undefined, + clientSecret: undefined + }, + google: { + clientID: undefined, + clientSecret: undefined + }, + ldap: { + providerName: undefined, + url: undefined, + bindDn: undefined, + bindCredentials: undefined, + tokenSecret: undefined, + searchBase: undefined, + searchFilter: undefined, + searchAttributes: undefined, + tlsca: undefined + }, + email: true, + allowemailregister: true +} diff --git a/lib/config/defaultSSL.js b/lib/config/defaultSSL.js new file mode 100644 index 00000000..1f1d5590 --- /dev/null +++ b/lib/config/defaultSSL.js @@ -0,0 +1,17 @@ +'use strict' + +const fs = require('fs') + +function getFile (path) { + if (fs.existsSync(path)) { + return path + } + return undefined +} + +module.exports = { + sslkeypath: getFile('/run/secrets/key.pem'), + sslcertpath: getFile('/run/secrets/cert.pem'), + sslcapath: getFile('/run/secrets/ca.pem'), + dhparampath: getFile('/run/secrets/dhparam.pem') +} diff --git a/lib/config/dockerSecret.js b/lib/config/dockerSecret.js new file mode 100644 index 00000000..eea2fafd --- /dev/null +++ b/lib/config/dockerSecret.js @@ -0,0 +1,51 @@ +'use strict' + +const fs = require('fs') +const path = require('path') + +const basePath = path.resolve('/var/run/secrets/') + +function getSecret (secret) { + const filePath = path.join(basePath, secret) + if (fs.existsSync(filePath)) return fs.readFileSync(filePath) + return undefined +} + +if (fs.existsSync(basePath)) { + module.exports = { + sessionsecret: getSecret('sessionsecret'), + sslkeypath: getSecret('sslkeypath'), + sslcertpath: getSecret('sslcertpath'), + sslcapath: getSecret('sslcapath'), + dhparampath: getSecret('dhparampath'), + s3: { + accessKeyId: getSecret('s3_acccessKeyId'), + secretAccessKey: getSecret('s3_secretAccessKey') + }, + facebook: { + clientID: getSecret('facebook_clientID'), + clientSecret: getSecret('facebook_clientSecret') + }, + twitter: { + consumerKey: getSecret('twitter_consumerKey'), + consumerSecret: getSecret('twitter_consumerSecret') + }, + github: { + clientID: getSecret('github_clientID'), + clientSecret: getSecret('github_clientSecret') + }, + gitlab: { + clientID: getSecret('gitlab_clientID'), + clientSecret: getSecret('gitlab_clientSecret') + }, + dropbox: { + clientID: getSecret('dropbox_clientID'), + clientSecret: getSecret('dropbox_clientSecret') + }, + google: { + clientID: getSecret('google_clientID'), + clientSecret: getSecret('google_clientSecret') + }, + imgur: getSecret('imgur_clientid') + } +} diff --git a/lib/config/enum.js b/lib/config/enum.js new file mode 100644 index 00000000..07cdfcfe --- /dev/null +++ b/lib/config/enum.js @@ -0,0 +1,16 @@ +'use strict' + +exports.Environment = { + development: 'development', + production: 'production', + test: 'test' +} + +exports.Permission = { + freely: 'freely', + editable: 'editable', + limited: 'limited', + locked: 'locked', + protected: 'protected', + private: 'private' +} diff --git a/lib/config/environment.js b/lib/config/environment.js new file mode 100644 index 00000000..75381ffc --- /dev/null +++ b/lib/config/environment.js @@ -0,0 +1,67 @@ +'use strict' + +const {toBooleanConfig} = require('./utils') + +module.exports = { + domain: process.env.HMD_DOMAIN, + urlpath: process.env.HMD_URL_PATH, + port: process.env.HMD_PORT, + urladdport: process.env.HMD_URL_ADDPORT, + usessl: toBooleanConfig(process.env.HMD_USESSL), + protocolusessl: toBooleanConfig(process.env.HMD_PROTOCOL_USESSL), + alloworigin: process.env.HMD_ALLOW_ORIGIN ? process.env.HMD_ALLOW_ORIGIN.split(',') : undefined, + usecdn: toBooleanConfig(process.env.HMD_USECDN), + allowanonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS), + allowfreeurl: toBooleanConfig(process.env.HMD_ALLOW_FREEURL), + defaultpermission: process.env.HMD_DEFAULT_PERMISSION, + dburl: process.env.HMD_DB_URL, + imageUploadType: process.env.HMD_IMAGE_UPLOAD_TYPE, + imgur: { + clientID: process.env.HMD_IMGUR_CLIENTID + }, + s3: { + accessKeyId: process.env.HMD_S3_ACCESS_KEY_ID, + secretAccessKey: process.env.HMD_S3_SECRET_ACCESS_KEY, + region: process.env.HMD_S3_REGION + }, + s3bucket: process.env.HMD_S3_BUCKET, + facebook: { + clientID: process.env.HMD_FACEBOOK_CLIENTID, + clientSecret: process.env.HMD_FACEBOOK_CLIENTSECRET + }, + twitter: { + consumerKey: process.env.HMD_TWITTER_CONSUMERKEY, + consumerSecret: process.env.HMD_TWITTER_CONSUMERSECRET + }, + github: { + clientID: process.env.HMD_GITHUB_CLIENTID, + clientSecret: process.env.HMD_GITHUB_CLIENTSECRET + }, + gitlab: { + baseURL: process.env.HMD_GITLAB_BASEURL, + clientID: process.env.HMD_GITLAB_CLIENTID, + clientSecret: process.env.HMD_GITLAB_CLIENTSECRET, + scope: process.env.HMD_GITLAB_SCOPE + }, + dropbox: { + clientID: process.env.HMD_DROPBOX_CLIENTID, + clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET + }, + google: { + clientID: process.env.HMD_GOOGLE_CLIENTID, + clientSecret: process.env.HMD_GOOGLE_CLIENTSECRET + }, + ldap: { + providerName: process.env.HMD_LDAP_PROVIDERNAME, + url: process.env.HMD_LDAP_URL, + bindDn: process.env.HMD_LDAP_BINDDN, + bindCredentials: process.env.HMD_LDAP_BINDCREDENTIALS, + tokenSecret: process.env.HMD_LDAP_TOKENSECRET, + searchBase: process.env.HMD_LDAP_SEARCHBASE, + searchFilter: process.env.HMD_LDAP_SEARCHFILTER, + searchAttributes: process.env.HMD_LDAP_SEARCHATTRIBUTES, + tlsca: process.env.HMD_LDAP_TLS_CA + }, + email: toBooleanConfig(process.env.HMD_EMAIL), + allowemailregister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER) +} diff --git a/lib/config/index.js b/lib/config/index.js new file mode 100644 index 00000000..6bc9a419 --- /dev/null +++ b/lib/config/index.js @@ -0,0 +1,112 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const {merge} = require('lodash') +const deepFreeze = require('deep-freeze') +const {Environment, Permission} = require('./enum') + +const appRootPath = path.join(__dirname, '../../') +const env = process.env.NODE_ENV || Environment.development +const debugConfig = { + debug: (env === Environment.development) +} + +const packageConfig = { + version: '0.5.1', + minimumCompatibleVersion: '0.5.0' +} + +const configFilePath = path.join(__dirname, '../../config.json') +const fileConfig = fs.existsSync(configFilePath) ? require(configFilePath)[env] : undefined + +let config = require('./default') +merge(config, require('./defaultSSL')) +merge(config, debugConfig) +merge(config, packageConfig) +merge(config, fileConfig) +merge(config, require('./oldEnvironment')) +merge(config, require('./environment')) +merge(config, require('./dockerSecret')) + +// load LDAP CA +if (config.ldap.tlsca) { + let ca = config.ldap.tlsca.split(',') + let caContent = [] + for (let i of ca) { + if (fs.existsSync(ca[i])) { + caContent.push(fs.readFileSync(ca[i], 'utf8')) + } + } + let tlsOptions = { + ca: caContent + } + config.ldap.tlsOptions = config.ldap.tlsOptions ? Object.assign(config.ldap.tlsOptions, tlsOptions) : tlsOptions +} + +// Permission +config.permission = Permission +if (!config.allowanonymous) { + delete config.permission.freely +} +if (!(config.defaultpermission in config.permission)) { + config.defaultpermission = config.permission.editable +} + +// cache result, cannot change config in runtime!!! +config.isStandardHTTPsPort = (function isStandardHTTPsPort () { + return config.usessl && config.port === 443 +})() +config.isStandardHTTPPort = (function isStandardHTTPPort () { + return !config.usessl && config.port === 80 +})() + +// cache serverURL +config.serverurl = (function getserverurl () { + var url = '' + if (config.domain) { + var protocol = config.protocolusessl ? 'https://' : 'http://' + url = protocol + config.domain + if (config.urladdport) { + if (!config.isStandardHTTPPort || !config.isStandardHTTPsPort) { + url += ':' + config.port + } + } + } + if (config.urlpath) { + url += '/' + config.urlpath + } + return url +})() + +config.Environment = Environment + +// auth method +config.isFacebookEnable = config.facebook.clientID && config.facebook.clientSecret +config.isGoogleEnable = config.google.clientID && config.google.clientSecret +config.isDropboxEnable = config.dropbox.clientID && config.dropbox.clientSecret +config.isTwitterEnable = config.twitter.consumerKey && config.twitter.consumerSecret +config.isEmailEnable = config.email +config.isGitHubEnable = config.github.clientID && config.github.clientSecret +config.isGitLabEnable = config.gitlab.clientID && config.gitlab.clientSecret +config.isLDAPEnable = config.ldap.url + +// generate correct path +config.sslcapath = path.join(appRootPath, config.sslcapath) +config.sslcertpath = path.join(appRootPath, config.sslcertpath) +config.sslkeypath = path.join(appRootPath, config.sslkeypath) +config.dhparampath = path.join(appRootPath, config.dhparampath) + +config.tmppath = path.join(appRootPath, config.tmppath) +config.defaultnotepath = path.join(appRootPath, config.defaultnotepath) +config.docspath = path.join(appRootPath, config.docspath) +config.indexpath = path.join(appRootPath, config.indexpath) +config.hackmdpath = path.join(appRootPath, config.hackmdpath) +config.errorpath = path.join(appRootPath, config.errorpath) +config.prettypath = path.join(appRootPath, config.prettypath) +config.slidepath = path.join(appRootPath, config.slidepath) + +// maek config readonly +config = deepFreeze(config) + +module.exports = config diff --git a/lib/config/oldEnvironment.js b/lib/config/oldEnvironment.js new file mode 100644 index 00000000..a3b13cb9 --- /dev/null +++ b/lib/config/oldEnvironment.js @@ -0,0 +1,10 @@ +'use strict' + +const {toBooleanConfig} = require('./utils') + +module.exports = { + debug: toBooleanConfig(process.env.DEBUG), + dburl: process.env.DATABASE_URL, + urlpath: process.env.URL_PATH, + port: process.env.PORT +} diff --git a/lib/config/utils.js b/lib/config/utils.js new file mode 100644 index 00000000..11bbd8cb --- /dev/null +++ b/lib/config/utils.js @@ -0,0 +1,8 @@ +'use strict' + +exports.toBooleanConfig = function toBooleanConfig (configValue) { + if (configValue && typeof configValue === 'string') { + return (configValue === 'true') + } + return configValue +} diff --git a/lib/history.js b/lib/history.js index ffed3b96..f46ff49f 100644 --- a/lib/history.js +++ b/lib/history.js @@ -3,9 +3,9 @@ // external modules // core -var config = require('./config.js') -var logger = require('./logger.js') -var response = require('./response.js') +var config = require('./config') +var logger = require('./logger') +var response = require('./response') var models = require('./models') // public diff --git a/lib/logger.js b/lib/logger.js index 2111d69a..f8b3895c 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,23 +1,23 @@ 'use strict' -var winston = require('winston') -winston.emitErrs = true +const winston = require('winston') -var logger = new winston.Logger({ +class Logger extends winston.Logger { + // Implement stream.writable.write interface + write (chunk) { + this.info(chunk) + } +} + +module.exports = new Logger({ transports: [ new winston.transports.Console({ level: 'debug', handleExceptions: true, json: false, - colorize: true, + colorize: false, timestamp: true }) ], + emitErrs: true, exitOnError: false }) - -module.exports = logger -module.exports.stream = { - write: function (message, encoding) { - logger.info(message) - } -} diff --git a/lib/models/index.js b/lib/models/index.js index 95f900b6..0679a7fc 100644 --- a/lib/models/index.js +++ b/lib/models/index.js @@ -3,13 +3,14 @@ var fs = require('fs') var path = require('path') var Sequelize = require('sequelize') +const {cloneDeep} = require('lodash') // core -var config = require('../config.js') -var logger = require('../logger.js') +var config = require('../config') +var logger = require('../logger') -var dbconfig = config.db -dbconfig.logging = config.debug ? logger.info : false +var dbconfig = cloneDeep(config.db) +dbconfig.logger = config.debug ? logger.info : false var sequelize = null diff --git a/lib/models/note.js b/lib/models/note.js index f7c25bde..c0ef1374 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -15,11 +15,11 @@ var dmp = new DiffMatchPatch() var S = require('string') // core -var config = require('../config.js') -var logger = require('../logger.js') +var config = require('../config') +var logger = require('../logger') // ot -var ot = require('../ot/index.js') +var ot = require('../ot') // permission types var permissionTypes = ['freely', 'editable', 'limited', 'locked', 'protected', 'private'] diff --git a/lib/models/revision.js b/lib/models/revision.js index 581e7ea5..6f3a746f 100644 --- a/lib/models/revision.js +++ b/lib/models/revision.js @@ -7,8 +7,8 @@ var childProcess = require('child_process') var shortId = require('shortid') // core -var config = require('../config.js') -var logger = require('../logger.js') +var config = require('../config') +var logger = require('../logger') var dmpWorker = createDmpWorker() var dmpCallbackCache = {} diff --git a/lib/models/user.js b/lib/models/user.js index 042b0d2f..14c30bc3 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -5,8 +5,8 @@ var Sequelize = require('sequelize') var scrypt = require('scrypt') // core -var logger = require('../logger.js') -var letterAvatars = require('../letter-avatars.js') +var logger = require('../logger') +var letterAvatars = require('../letter-avatars') module.exports = function (sequelize, DataTypes) { var User = sequelize.define('User', { diff --git a/lib/realtime.js b/lib/realtime.js index 618fdad0..361bbf09 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -12,13 +12,13 @@ var chance = new Chance() var moment = require('moment') // core -var config = require('./config.js') -var logger = require('./logger.js') -var history = require('./history.js') +var config = require('./config') +var logger = require('./logger') +var history = require('./history') var models = require('./models') // ot -var ot = require('./ot/index.js') +var ot = require('./ot') // public var realtime = { @@ -28,7 +28,8 @@ var realtime = { secure: secure, connection: connection, getStatus: getStatus, - isReady: isReady + isReady: isReady, + maintenance: true } function onAuthorizeSuccess (data, accept) { @@ -699,7 +700,7 @@ function updateHistory (userId, note, time) { } function connection (socket) { - if (config.maintenance) return + if (realtime.maintenance) return parseNoteIdFromSocket(socket, function (err, noteId) { if (err) { return failConnection(500, err, socket) diff --git a/lib/response.js b/lib/response.js index 13a94bbf..a9abd1d4 100755 --- a/lib/response.js +++ b/lib/response.js @@ -10,8 +10,8 @@ var request = require('request') var moment = require('moment') // core -var config = require('./config.js') -var logger = require('./logger.js') +var config = require('./config') +var logger = require('./logger') var models = require('./models') // public @@ -59,14 +59,14 @@ function showIndex (req, res, next) { url: config.serverurl, useCDN: config.usecdn, allowAnonymous: config.allowanonymous, - facebook: config.facebook, - twitter: config.twitter, - github: config.github, - gitlab: config.gitlab, - dropbox: config.dropbox, - google: config.google, - ldap: config.ldap, - email: config.email, + facebook: config.isFacebookEnable, + twitter: config.isTwitterEnable, + github: config.isGitHubEnable, + gitlab: config.isGitLabEnable, + dropbox: config.isDropboxEnable, + google: config.isGoogleEnable, + ldap: config.isLDAPEnable, + email: config.isEmailEnable, allowemailregister: config.allowemailregister, signin: req.isAuthenticated(), infoMessage: req.flash('info'), @@ -89,14 +89,14 @@ function responseHackMD (res, note) { title: title, useCDN: config.usecdn, allowAnonymous: config.allowanonymous, - facebook: config.facebook, - twitter: config.twitter, - github: config.github, - gitlab: config.gitlab, - dropbox: config.dropbox, - google: config.google, - ldap: config.ldap, - email: config.email, + facebook: config.isFacebookEnable, + twitter: config.isTwitterEnable, + github: config.isGitHubEnable, + gitlab: config.isGitLabEnable, + dropbox: config.isDropboxEnable, + google: config.isGoogleEnable, + ldap: config.isLDAPEnable, + email: config.isEmailEnable, allowemailregister: config.allowemailregister }) } diff --git a/lib/web/auth/dropbox/index.js b/lib/web/auth/dropbox/index.js new file mode 100644 index 00000000..c03fbc57 --- /dev/null +++ b/lib/web/auth/dropbox/index.js @@ -0,0 +1,29 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const DropboxStrategy = require('passport-dropbox-oauth2').Strategy +const config = require('../../../config') +const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') + +let dropboxAuth = module.exports = Router() + +passport.use(new DropboxStrategy({ + apiVersion: '2', + clientID: config.dropbox.clientID, + clientSecret: config.dropbox.clientSecret, + callbackURL: config.serverurl + '/auth/dropbox/callback' +}, passportGeneralCallback)) + +dropboxAuth.get('/auth/dropbox', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('dropbox-oauth2')(req, res, next) +}) + +// dropbox auth callback +dropboxAuth.get('/auth/dropbox/callback', + passport.authenticate('dropbox-oauth2', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) diff --git a/lib/web/auth/email/index.js b/lib/web/auth/email/index.js new file mode 100644 index 00000000..760075f8 --- /dev/null +++ b/lib/web/auth/email/index.js @@ -0,0 +1,74 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const validator = require('validator') +const LocalStrategy = require('passport-local').Strategy +const config = require('../../../config') +const models = require('../../../models') +const logger = require('../../../logger') +const {setReturnToFromReferer} = require('../utils') +const {urlencodedParser} = require('../../utils') +const response = require('../../../response') + +let emailAuth = module.exports = Router() + +passport.use(new LocalStrategy({ + usernameField: 'email' +}, function (email, password, done) { + if (!validator.isEmail(email)) return done(null, false) + models.User.findOne({ + where: { + email: email + } + }).then(function (user) { + if (!user) return done(null, false) + if (!user.verifyPassword(password)) return done(null, false) + return done(null, user) + }).catch(function (err) { + logger.error(err) + return done(err) + }) +})) + +if (config.allowemailregister) { + emailAuth.post('/register', urlencodedParser, function (req, res, next) { + if (!req.body.email || !req.body.password) return response.errorBadRequest(res) + if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res) + models.User.findOrCreate({ + where: { + email: req.body.email + }, + defaults: { + password: req.body.password + } + }).spread(function (user, created) { + if (user) { + if (created) { + logger.debug('user registered: ' + user.id) + req.flash('info', "You've successfully registered, please signin.") + } else { + logger.debug('user found: ' + user.id) + req.flash('error', 'This email has been used, please try another one.') + } + return res.redirect(config.serverurl + '/') + } + req.flash('error', 'Failed to register your account, please try again.') + return res.redirect(config.serverurl + '/') + }).catch(function (err) { + logger.error('auth callback failed: ' + err) + return response.errorInternalError(res) + }) + }) +} + +emailAuth.post('/login', urlencodedParser, function (req, res, next) { + if (!req.body.email || !req.body.password) return response.errorBadRequest(res) + if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res) + setReturnToFromReferer(req) + passport.authenticate('local', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/', + failureFlash: 'Invalid email or password.' + })(req, res, next) +}) diff --git a/lib/web/auth/facebook/index.js b/lib/web/auth/facebook/index.js new file mode 100644 index 00000000..0e5474d8 --- /dev/null +++ b/lib/web/auth/facebook/index.js @@ -0,0 +1,29 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const FacebookStrategy = require('passport-facebook').Strategy + +const config = require('../../../config') +const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') + +let facebookAuth = module.exports = Router() + +passport.use(new FacebookStrategy({ + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.serverurl + '/auth/facebook/callback' +}, passportGeneralCallback)) + +facebookAuth.get('/auth/facebook', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('facebook')(req, res, next) +}) + +// facebook auth callback +facebookAuth.get('/auth/facebook/callback', + passport.authenticate('facebook', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) diff --git a/lib/web/auth/github/index.js b/lib/web/auth/github/index.js new file mode 100644 index 00000000..2a26669c --- /dev/null +++ b/lib/web/auth/github/index.js @@ -0,0 +1,28 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const GithubStrategy = require('passport-github').Strategy +const config = require('../../../config') +const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') + +let githubAuth = module.exports = Router() + +passport.use(new GithubStrategy({ + clientID: config.github.clientID, + clientSecret: config.github.clientSecret, + callbackURL: config.serverurl + '/auth/github/callback' +}, passportGeneralCallback)) + +githubAuth.get('/auth/github', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('github')(req, res, next) +}) + +// github auth callback +githubAuth.get('/auth/github/callback', + passport.authenticate('github', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) diff --git a/lib/web/auth/gitlab/index.js b/lib/web/auth/gitlab/index.js new file mode 100644 index 00000000..51de1602 --- /dev/null +++ b/lib/web/auth/gitlab/index.js @@ -0,0 +1,36 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const GitlabStrategy = require('passport-gitlab2').Strategy +const config = require('../../../config') +const response = require('../../../response') +const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') + +let gitlabAuth = module.exports = Router() + +passport.use(new GitlabStrategy({ + baseURL: config.gitlab.baseURL, + clientID: config.gitlab.clientID, + clientSecret: config.gitlab.clientSecret, + scope: config.gitlab.scope, + callbackURL: config.serverurl + '/auth/gitlab/callback' +}, passportGeneralCallback)) + +gitlabAuth.get('/auth/gitlab', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('gitlab')(req, res, next) +}) + +// gitlab auth callback +gitlabAuth.get('/auth/gitlab/callback', + passport.authenticate('gitlab', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) + +if (!config.gitlab.scope || config.gitlab.scope === 'api') { + // gitlab callback actions + gitlabAuth.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions) +} diff --git a/lib/web/auth/google/index.js b/lib/web/auth/google/index.js new file mode 100644 index 00000000..bf2a260f --- /dev/null +++ b/lib/web/auth/google/index.js @@ -0,0 +1,27 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +var GoogleStrategy = require('passport-google-oauth20').Strategy +const config = require('../../../config') +const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') + +let facebookAuth = module.exports = Router() + +passport.use(new GoogleStrategy({ + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.serverurl + '/auth/google/callback' +}, passportGeneralCallback)) + +facebookAuth.get('/auth/google', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('google', { scope: ['profile'] })(req, res, next) +}) + // google auth callback +facebookAuth.get('/auth/google/callback', + passport.authenticate('google', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) diff --git a/lib/web/auth/index.js b/lib/web/auth/index.js new file mode 100644 index 00000000..b5ca8434 --- /dev/null +++ b/lib/web/auth/index.js @@ -0,0 +1,48 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') + +const config = require('../../config') +const logger = require('../../logger') +const models = require('../../models') + +const authRouter = module.exports = Router() + +// serialize and deserialize +passport.serializeUser(function (user, done) { + logger.info('serializeUser: ' + user.id) + return done(null, user.id) +}) + +passport.deserializeUser(function (id, done) { + models.User.findOne({ + where: { + id: id + } + }).then(function (user) { + logger.info('deserializeUser: ' + user.id) + return done(null, user) + }).catch(function (err) { + logger.error(err) + return done(err, null) + }) +}) + +if (config.isFacebookEnable) authRouter.use(require('./facebook')) +if (config.isTwitterEnable) authRouter.use(require('./twitter')) +if (config.isGitHubEnable) authRouter.use(require('./github')) +if (config.isGitLabEnable) authRouter.use(require('./gitlab')) +if (config.isDropboxEnable) authRouter.use(require('./dropbox')) +if (config.isGoogleEnable) authRouter.use(require('./google')) +if (config.isLDAPEnable) authRouter.use(require('./ldap')) +if (config.isEmailEnable) authRouter.use(require('./email')) + +// logout +authRouter.get('/logout', function (req, res) { + if (config.debug && req.isAuthenticated()) { + logger.debug('user logout: ' + req.user.id) + } + req.logout() + res.redirect(config.serverurl + '/') +}) diff --git a/lib/web/auth/ldap/index.js b/lib/web/auth/ldap/index.js new file mode 100644 index 00000000..766c5cbc --- /dev/null +++ b/lib/web/auth/ldap/index.js @@ -0,0 +1,74 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const LDAPStrategy = require('passport-ldapauth') +const config = require('../../../config') +const models = require('../../../models') +const logger = require('../../../logger') +const {setReturnToFromReferer} = require('../utils') +const {urlencodedParser} = require('../../utils') +const response = require('../../../response') + +let ldapAuth = module.exports = Router() + +passport.use(new LDAPStrategy({ + server: { + url: config.ldap.url || null, + bindDn: config.ldap.bindDn || null, + bindCredentials: config.ldap.bindCredentials || null, + searchBase: config.ldap.searchBase || null, + searchFilter: config.ldap.searchFilter || null, + searchAttributes: config.ldap.searchAttributes || null, + tlsOptions: config.ldap.tlsOptions || null + } +}, function (user, done) { + var profile = { + id: 'LDAP-' + user.uidNumber, + username: user.uid, + displayName: user.displayName, + emails: user.mail ? [user.mail] : [], + avatarUrl: null, + profileUrl: null, + provider: 'ldap' + } + var stringifiedProfile = JSON.stringify(profile) + models.User.findOrCreate({ + where: { + profileid: profile.id.toString() + }, + defaults: { + profile: stringifiedProfile + } + }).spread(function (user, created) { + if (user) { + var needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (needSave) { + user.save().then(function () { + if (config.debug) { logger.debug('user login: ' + user.id) } + return done(null, user) + }) + } else { + if (config.debug) { logger.debug('user login: ' + user.id) } + return done(null, user) + } + } + }).catch(function (err) { + logger.error('ldap auth failed: ' + err) + return done(err, null) + }) +})) + +ldapAuth.post('/auth/ldap', urlencodedParser, function (req, res, next) { + if (!req.body.username || !req.body.password) return response.errorBadRequest(res) + setReturnToFromReferer(req) + passport.authenticate('ldapauth', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/', + failureFlash: true + })(req, res, next) +}) diff --git a/lib/web/auth/twitter/index.js b/lib/web/auth/twitter/index.js new file mode 100644 index 00000000..5429522d --- /dev/null +++ b/lib/web/auth/twitter/index.js @@ -0,0 +1,29 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const TwitterStrategy = require('passport-twitter').Strategy + +const config = require('../../../config') +const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') + +let twitterAuth = module.exports = Router() + +passport.use(new TwitterStrategy({ + consumerKey: config.twitter.consumerKey, + consumerSecret: config.twitter.consumerSecret, + callbackURL: config.serverurl + '/auth/twitter/callback' +}, passportGeneralCallback)) + +twitterAuth.get('/auth/twitter', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('twitter')(req, res, next) +}) + +// twitter auth callback +twitterAuth.get('/auth/twitter/callback', + passport.authenticate('twitter', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) diff --git a/lib/web/auth/utils.js b/lib/web/auth/utils.js new file mode 100644 index 00000000..ff7a1237 --- /dev/null +++ b/lib/web/auth/utils.js @@ -0,0 +1,53 @@ +'use strict' + +const models = require('../../models') +const config = require('../../config') +const logger = require('../../logger') + +exports.setReturnToFromReferer = function setReturnToFromReferer (req) { + var referer = req.get('referer') + if (!req.session) req.session = {} + req.session.returnTo = referer +} + +exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) { + var stringifiedProfile = JSON.stringify(profile) + models.User.findOrCreate({ + where: { + profileid: profile.id.toString() + }, + defaults: { + profile: stringifiedProfile, + accessToken: accessToken, + refreshToken: refreshToken + } + }).spread(function (user, created) { + if (user) { + var needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (user.accessToken !== accessToken) { + user.accessToken = accessToken + needSave = true + } + if (user.refreshToken !== refreshToken) { + user.refreshToken = refreshToken + needSave = true + } + if (needSave) { + user.save().then(function () { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + }) + } else { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + } + } + }).catch(function (err) { + logger.error('auth callback failed: ' + err) + return done(err, null) + }) +} diff --git a/lib/web/baseRouter.js b/lib/web/baseRouter.js new file mode 100644 index 00000000..b918ce75 --- /dev/null +++ b/lib/web/baseRouter.js @@ -0,0 +1,22 @@ +'use strict' + +const Router = require('express').Router + +const response = require('../response') + +const baseRouter = module.exports = Router() + +// get index +baseRouter.get('/', response.showIndex) +// get 403 forbidden +baseRouter.get('/403', function (req, res) { + response.errorForbidden(res) +}) +// get 404 not found +baseRouter.get('/404', function (req, res) { + response.errorNotFound(res) +}) +// get 500 internal error +baseRouter.get('/500', function (req, res) { + response.errorInternalError(res) +}) diff --git a/lib/web/historyRouter.js b/lib/web/historyRouter.js new file mode 100644 index 00000000..1b22c232 --- /dev/null +++ b/lib/web/historyRouter.js @@ -0,0 +1,18 @@ +'use strict' + +const Router = require('express').Router + +const {urlencodedParser} = require('./utils') +const history = require('../history') +const historyRouter = module.exports = Router() + +// get history +historyRouter.get('/history', history.historyGet) +// post history +historyRouter.post('/history', urlencodedParser, history.historyPost) +// post history by note id +historyRouter.post('/history/:noteId', urlencodedParser, history.historyPost) +// delete history +historyRouter.delete('/history', history.historyDelete) +// delete history by note id +historyRouter.delete('/history/:noteId', history.historyDelete) diff --git a/lib/web/imageRouter.js b/lib/web/imageRouter.js new file mode 100644 index 00000000..592a497c --- /dev/null +++ b/lib/web/imageRouter.js @@ -0,0 +1,95 @@ +'use strict' +var fs = require('fs') +var url = require('url') +var path = require('path') + +const Router = require('express').Router +const formidable = require('formidable') +var imgur = require('imgur') + +const config = require('../config') +const logger = require('../logger') +const response = require('../response') + +const imageRouter = module.exports = Router() + +// upload image +imageRouter.post('/uploadimage', function (req, res) { + var form = new formidable.IncomingForm() + + form.keepExtensions = true + + if (config.imageUploadType === 'filesystem') { + form.uploadDir = 'public/uploads' + } + + form.parse(req, function (err, fields, files) { + if (err || !files.image || !files.image.path) { + response.errorForbidden(res) + } else { + if (config.debug) { logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)) } + + try { + switch (config.imageUploadType) { + case 'filesystem': + res.send({ + link: url.resolve(config.serverurl + '/', files.image.path.match(/^public\/(.+$)/)[1]) + }) + + break + + case 's3': + var AWS = require('aws-sdk') + var awsConfig = new AWS.Config(config.s3) + var s3 = new AWS.S3(awsConfig) + const {getImageMimeType} = require('../utils') + fs.readFile(files.image.path, function (err, buffer) { + if (err) { + logger.error(err) + res.status(500).end('upload image error') + return + } + var params = { + Bucket: config.s3bucket, + Key: path.join('uploads', path.basename(files.image.path)), + Body: buffer + } + + var mimeType = getImageMimeType(files.image.path) + if (mimeType) { params.ContentType = mimeType } + + s3.putObject(params, function (err, data) { + if (err) { + logger.error(err) + res.status(500).end('upload image error') + return + } + res.send({ + link: `https://s3-${config.s3.region}.amazonaws.com/${config.s3bucket}/${params.Key}` + }) + }) + }) + break + case 'imgur': + default: + imgur.setClientId(config.imgur.clientID) + imgur.uploadFile(files.image.path) + .then(function (json) { + if (config.debug) { logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) } + res.send({ + link: json.data.link.replace(/^http:\/\//i, 'https://') + }) + }) + .catch(function (err) { + logger.error(err) + return res.status(500).end('upload image error') + }) + break + } + } catch (err) { + logger.error(err) + return res.status(500).end('upload image error') + } + } + }) +}) diff --git a/lib/web/middleware/checkURIValid.js b/lib/web/middleware/checkURIValid.js new file mode 100644 index 00000000..88065e79 --- /dev/null +++ b/lib/web/middleware/checkURIValid.js @@ -0,0 +1,14 @@ +'use strict' + +const logger = require('../../logger') +const response = require('../../response') + +module.exports = function (req, res, next) { + try { + decodeURIComponent(req.path) + } catch (err) { + logger.error(err) + return response.errorBadRequest(res) + } + next() +} diff --git a/lib/web/middleware/redirectWithoutTrailingSlashes.js b/lib/web/middleware/redirectWithoutTrailingSlashes.js new file mode 100644 index 00000000..fbaba617 --- /dev/null +++ b/lib/web/middleware/redirectWithoutTrailingSlashes.js @@ -0,0 +1,17 @@ +'use strict' + +const config = require('../../config') + +module.exports = function (req, res, next) { + if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) { + const queryString = req.url.slice(req.path.length) + const urlPath = req.path.slice(0, -1) + let serverURL = config.serverurl + if (config.urlpath) { + serverURL = serverURL.slice(0, -(config.urlpath.length + 1)) + } + res.redirect(301, serverURL + urlPath + queryString) + } else { + next() + } +} diff --git a/lib/web/middleware/tooBusy.js b/lib/web/middleware/tooBusy.js new file mode 100644 index 00000000..f1b72330 --- /dev/null +++ b/lib/web/middleware/tooBusy.js @@ -0,0 +1,13 @@ +'use strict' + +const toobusy = require('toobusy-js') + +const response = require('../../response') + +module.exports = function (req, res, next) { + if (toobusy()) { + response.errorServiceUnavailable(res) + } else { + next() + } +} diff --git a/lib/web/noteRouter.js b/lib/web/noteRouter.js new file mode 100644 index 00000000..007c02c2 --- /dev/null +++ b/lib/web/noteRouter.js @@ -0,0 +1,24 @@ +'use strict' + +const Router = require('express').Router + +const response = require('../response') + +const noteRouter = module.exports = Router() + +// get new note +noteRouter.get('/new', response.newNote) +// get publish note +noteRouter.get('/s/:shortid', response.showPublishNote) +// publish note actions +noteRouter.get('/s/:shortid/:action', response.publishNoteActions) +// get publish slide +noteRouter.get('/p/:shortid', response.showPublishSlide) +// publish slide actions +noteRouter.get('/p/:shortid/:action', response.publishSlideActions) +// get note by id +noteRouter.get('/:noteId', response.showNote) +// note actions +noteRouter.get('/:noteId/:action', response.noteActions) +// note actions with action id +noteRouter.get('/:noteId/:action/:actionId', response.noteActions) diff --git a/lib/web/statusRouter.js b/lib/web/statusRouter.js new file mode 100644 index 00000000..aa3a9b79 --- /dev/null +++ b/lib/web/statusRouter.js @@ -0,0 +1,92 @@ +'use strict' + +const Router = require('express').Router + +const response = require('../response') +const realtime = require('../realtime') +const config = require('../config') +const models = require('../models') +const logger = require('../logger') + +const {urlencodedParser} = require('./utils') + +const statusRouter = module.exports = Router() + +// get status +statusRouter.get('/status', function (req, res, next) { + realtime.getStatus(function (data) { + res.set({ + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling + 'HackMD-Version': config.version + }) + res.send(data) + }) +}) +// get status +statusRouter.get('/temp', function (req, res) { + var host = req.get('host') + if (config.alloworigin.indexOf(host) === -1) { + response.errorForbidden(res) + } else { + var tempid = req.query.tempid + if (!tempid) { + response.errorForbidden(res) + } else { + models.Temp.findOne({ + where: { + id: tempid + } + }).then(function (temp) { + if (!temp) { + response.errorNotFound(res) + } else { + res.header('Access-Control-Allow-Origin', '*') + res.send({ + temp: temp.data + }) + temp.destroy().catch(function (err) { + if (err) { + logger.error('remove temp failed: ' + err) + } + }) + } + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) + } + } +}) +// post status +statusRouter.post('/temp', urlencodedParser, function (req, res) { + var host = req.get('host') + if (config.alloworigin.indexOf(host) === -1) { + response.errorForbidden(res) + } else { + var data = req.body.data + if (!data) { + response.errorForbidden(res) + } else { + if (config.debug) { + logger.info('SERVER received temp from [' + host + ']: ' + req.body.data) + } + models.Temp.create({ + data: data + }).then(function (temp) { + if (temp) { + res.header('Access-Control-Allow-Origin', '*') + res.send({ + status: 'ok', + id: temp.id + }) + } else { + response.errorInternalError(res) + } + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) + } + } +}) diff --git a/lib/web/userRouter.js b/lib/web/userRouter.js new file mode 100644 index 00000000..ecfbaf8b --- /dev/null +++ b/lib/web/userRouter.js @@ -0,0 +1,36 @@ +'use strict' + +const Router = require('express').Router + +const response = require('../response') +const models = require('../models') +const logger = require('../logger') + +const UserRouter = module.exports = Router() + +// get me info +UserRouter.get('/me', function (req, res) { + if (req.isAuthenticated()) { + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + if (!user) { return response.errorNotFound(res) } + var profile = models.User.getProfile(user) + res.send({ + status: 'ok', + id: req.user.id, + name: profile.name, + photo: profile.photo + }) + }).catch(function (err) { + logger.error('read me failed: ' + err) + return response.errorInternalError(res) + }) + } else { + res.send({ + status: 'forbidden' + }) + } +}) diff --git a/lib/web/utils.js b/lib/web/utils.js new file mode 100644 index 00000000..c9016523 --- /dev/null +++ b/lib/web/utils.js @@ -0,0 +1,9 @@ +'use strict' + +const bodyParser = require('body-parser') + +// create application/x-www-form-urlencoded parser +exports.urlencodedParser = bodyParser.urlencoded({ + extended: false, + limit: 1024 * 1024 * 10 // 10 mb +}) diff --git a/lib/workers/dmpWorker.js b/lib/workers/dmpWorker.js index b0ed0f43..60db0a12 100644 --- a/lib/workers/dmpWorker.js +++ b/lib/workers/dmpWorker.js @@ -4,8 +4,8 @@ var DiffMatchPatch = require('diff-match-patch') var dmp = new DiffMatchPatch() // core -var config = require('../config.js') -var logger = require('../logger.js') +var config = require('../config') +var logger = require('../logger') process.on('message', function (data) { if (!data || !data.msg || !data.cacheKey) { |