diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/config/default.js | 9 | ||||
-rw-r--r-- | lib/config/defaultSSL.js | 8 | ||||
-rw-r--r-- | lib/config/dockerSecret.js | 3 | ||||
-rw-r--r-- | lib/config/environment.js | 6 | ||||
-rw-r--r-- | lib/config/index.js | 19 | ||||
-rw-r--r-- | lib/csp.js | 14 | ||||
-rw-r--r-- | lib/letter-avatars.js | 17 | ||||
-rw-r--r-- | lib/models/note.js | 9 | ||||
-rw-r--r-- | lib/models/user.js | 10 | ||||
-rw-r--r-- | lib/realtime.js | 2 | ||||
-rw-r--r-- | lib/response.js | 15 | ||||
-rw-r--r-- | lib/web/auth/saml/index.js | 4 | ||||
-rw-r--r-- | lib/web/imageRouter/azure.js | 35 | ||||
-rw-r--r-- | lib/web/imageRouter/filesystem.js | 3 | ||||
-rw-r--r-- | lib/web/imageRouter/imgur.js | 2 | ||||
-rw-r--r-- | lib/web/imageRouter/minio.js | 3 | ||||
-rw-r--r-- | lib/web/imageRouter/s3.js | 3 | ||||
-rw-r--r-- | lib/web/userRouter.js | 7 |
18 files changed, 136 insertions, 33 deletions
diff --git a/lib/config/default.js b/lib/config/default.js index 48916c89..1b124b3e 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -18,6 +18,8 @@ module.exports = { directives: { }, addDefaults: true, + addDisqus: true, + addGoogleAnalytics: true, upgradeInsecureRequests: 'auto', reportURI: undefined }, @@ -46,6 +48,7 @@ module.exports = { // session sessionName: 'connect.sid', sessionSecret: 'secret', + sessionSecretLen: 128, sessionLife: 14 * 24 * 60 * 60 * 1000, // 14 days staticCacheTime: 1 * 24 * 60 * 60 * 1000, // 1 day // socket.io @@ -53,7 +56,7 @@ module.exports = { heartbeatTimeout: 10000, // document documentMaxLength: 100000, - // image upload setting, available options are imgur/s3/filesystem + // image upload setting, available options are imgur/s3/filesystem/azure imageUploadType: 'filesystem', imgur: { clientID: undefined @@ -71,6 +74,10 @@ module.exports = { port: 9000 }, s3bucket: undefined, + azure: { + connectionString: undefined, + container: undefined + }, // authentication oauth2: { authorizationURL: undefined, diff --git a/lib/config/defaultSSL.js b/lib/config/defaultSSL.js index 362c62a1..ba020466 100644 --- a/lib/config/defaultSSL.js +++ b/lib/config/defaultSSL.js @@ -10,8 +10,8 @@ function getFile (path) { } module.exports = { - sslkeypath: getFile('/run/secrets/key.pem'), - sslcertpath: getFile('/run/secrets/cert.pem'), - sslcapath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [], - dhparampath: getFile('/run/secrets/dhparam.pem') + sslKeyPath: getFile('/run/secrets/key.pem'), + sslCertPath: getFile('/run/secrets/cert.pem'), + sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [], + dhParamPath: getFile('/run/secrets/dhparam.pem') } diff --git a/lib/config/dockerSecret.js b/lib/config/dockerSecret.js index b9116cd3..fd66ddfe 100644 --- a/lib/config/dockerSecret.js +++ b/lib/config/dockerSecret.js @@ -22,6 +22,9 @@ if (fs.existsSync(basePath)) { accessKeyId: getSecret('s3_acccessKeyId'), secretAccessKey: getSecret('s3_secretAccessKey') }, + azure: { + connectionString: getSecret('azure_connectionString') + }, facebook: { clientID: getSecret('facebook_clientID'), clientSecret: getSecret('facebook_clientSecret') diff --git a/lib/config/environment.js b/lib/config/environment.js index 66fa5284..e1c11569 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -26,6 +26,8 @@ module.exports = { allowFreeURL: toBooleanConfig(process.env.HMD_ALLOW_FREEURL), defaultPermission: process.env.HMD_DEFAULT_PERMISSION, dbURL: process.env.HMD_DB_URL, + sessionSecret: process.env.HMD_SESSION_SECRET, + sessionLife: toIntegerConfig(process.env.HMD_SESSION_LIFE), imageUploadType: process.env.HMD_IMAGE_UPLOAD_TYPE, imgur: { clientID: process.env.HMD_IMGUR_CLIENTID @@ -43,6 +45,10 @@ module.exports = { port: toIntegerConfig(process.env.HMD_MINIO_PORT) }, s3bucket: process.env.HMD_S3_BUCKET, + azure: { + connectionString: process.env.HMD_AZURE_CONNECTION_STRING, + container: process.env.HMD_AZURE_CONTAINER + }, facebook: { clientID: process.env.HMD_FACEBOOK_CLIENTID, clientSecret: process.env.HMD_FACEBOOK_CLIENTSECRET diff --git a/lib/config/index.js b/lib/config/index.js index 63c23f0c..484301c4 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -1,6 +1,7 @@ 'use strict' +const crypto = require('crypto') const fs = require('fs') const path = require('path') const {merge} = require('lodash') @@ -52,7 +53,7 @@ if (config.ldap.tlsca) { // Permission config.permission = Permission -if (!config.allowAnonymous && !config.allowAnonymousedits) { +if (!config.allowAnonymous && !config.allowAnonymousEdits) { delete config.permission.freely } if (!(config.defaultPermission in config.permission)) { @@ -111,16 +112,24 @@ for (let i = keys.length; i--;) { // and the config with uppercase is not set // we set the new config using the old key. if (uppercase.test(keys[i]) && - config[lowercaseKey] && - !config[keys[1]]) { + config[lowercaseKey] !== undefined && + fileConfig[keys[i]] === undefined) { logger.warn('config.js contains deprecated lowercase setting for ' + keys[i] + '. Please change your config.js file to replace ' + lowercaseKey + ' with ' + keys[i]) config[keys[i]] = config[lowercaseKey] } } +// Generate session secret if it stays on default values +if (config.sessionSecret === 'secret') { + logger.warn('Session secret not set. Using random generated one. Please set `sessionSecret` in your config.js file. All users will be logged out.') + config.sessionSecret = crypto.randomBytes(Math.ceil(config.sessionSecretLen / 2)) // generate crypto graphic random number + .toString('hex') // convert to hexadecimal format + .slice(0, config.sessionSecretLen) // return required number of characters +} + // Validate upload upload providers -if (['filesystem', 's3', 'minio', 'imgur'].indexOf(config.imageUploadType) === -1) { - logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio" or "imgur". Defaulting to "imgur"') +if (['filesystem', 's3', 'minio', 'imgur', 'azure'].indexOf(config.imageUploadType) === -1) { + logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio", "azure" or "imgur". Defaulting to "imgur"') config.imageUploadType = 'imgur' } @@ -5,7 +5,7 @@ var CspStrategy = {} var defaultDirectives = { defaultSrc: ['\'self\''], - scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', 'https://*.disqus.com', '\'unsafe-eval\''], + scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', '\'unsafe-eval\''], // ^ TODO: Remove unsafe-eval - webpack script-loader issues https://github.com/hackmdio/hackmd/issues/594 imgSrc: ['*'], styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://assets-cdn.github.com'], // unsafe-inline is required for some libs, plus used in views @@ -22,11 +22,23 @@ var cdnDirectives = { fontSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com'] } +var disqusDirectives = { + scriptSrc: ['https://*.disqus.com', 'https://*.disquscdn.com'], + styleSrc: ['https://*.disquscdn.com'], + fontSrc: ['https://*.disquscdn.com'] +} + +var googleAnalyticsDirectives = { + scriptSrc: ['https://www.google-analytics.com'] +} + CspStrategy.computeDirectives = function () { var directives = {} mergeDirectives(directives, config.csp.directives) mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives) mergeDirectivesIf(config.useCDN, directives, cdnDirectives) + mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives) + mergeDirectivesIf(config.csp.addGoogleAnalytics, directives, googleAnalyticsDirectives) if (!areAllInlineScriptsAllowed(directives)) { addInlineScriptExceptions(directives) } diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js index 7ba336b6..b5b1d9e7 100644 --- a/lib/letter-avatars.js +++ b/lib/letter-avatars.js @@ -1,16 +1,17 @@ 'use strict' // external modules -var randomcolor = require('randomcolor') +const randomcolor = require('randomcolor') +const config = require('./config') // core -module.exports = function (name) { - var color = randomcolor({ +exports.generateAvatar = function (name) { + const color = randomcolor({ seed: name, luminosity: 'dark' }) - var letter = name.substring(0, 1).toUpperCase() + const letter = name.substring(0, 1).toUpperCase() - var svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' + let svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' svg += '<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="96" width="96" version="1.1" viewBox="0 0 96 96">' svg += '<g>' svg += '<rect width="96" height="96" fill="' + color + '" />' @@ -20,5 +21,9 @@ module.exports = function (name) { svg += '</g>' svg += '</svg>' - return 'data:image/svg+xml;base64,' + new Buffer(svg).toString('base64') + return svg +} + +exports.generateAvatarURL = function (name) { + return config.serverURL + '/user/' + name + '/avatar.svg' } diff --git a/lib/models/note.js b/lib/models/note.js index 69393dd4..2a048e37 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -211,6 +211,15 @@ module.exports = function (sequelize, DataTypes) { }, // parse note id by LZString is deprecated, here for compability parseNoteIdByLZString: function (_callback) { + // Calculate minimal string length for an UUID that is encoded + // base64 encoded and optimize comparsion by using -1 + // this should make a lot of LZ-String parsing errors obsolete + // as we can assume that a nodeId that is 48 chars or longer is a + // noteID. + const base64UuidLength = ((4 * 36) / 3) - 1 + if (!(noteId.length > base64UuidLength)) { + return _callback(null, null) + } // try to parse note id by LZString Base64 try { var id = LZString.decompressFromBase64(noteId) diff --git a/lib/models/user.js b/lib/models/user.js index ea249726..0f6acd65 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -6,7 +6,7 @@ var scrypt = require('scrypt') // core var logger = require('../logger') -var letterAvatars = require('../letter-avatars') +var {generateAvatarURL} = require('../letter-avatars') module.exports = function (sequelize, DataTypes) { var User = sequelize.define('User', { @@ -108,7 +108,7 @@ module.exports = function (sequelize, DataTypes) { if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') else photo = photo.replace(/(\?s=)\d*$/i, '$196') } else { - photo = letterAvatars(profile.username) + photo = generateAvatarURL(profile.username) } break case 'mattermost': @@ -117,7 +117,7 @@ module.exports = function (sequelize, DataTypes) { if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') else photo = photo.replace(/(\?s=)\d*$/i, '$196') } else { - photo = letterAvatars(profile.username) + photo = generateAvatarURL(profile.username) } break case 'dropbox': @@ -140,7 +140,7 @@ module.exports = function (sequelize, DataTypes) { if (bigger) photo += '?s=400' else photo += '?s=96' } else { - photo = letterAvatars(profile.username) + photo = generateAvatarURL(profile.username) } break case 'saml': @@ -149,7 +149,7 @@ module.exports = function (sequelize, DataTypes) { if (bigger) photo += '?s=400' else photo += '?s=96' } else { - photo = letterAvatars(profile.username) + photo = generateAvatarURL(profile.username) } break } diff --git a/lib/realtime.js b/lib/realtime.js index d8b0b4c5..070bde2d 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -788,7 +788,7 @@ function connection (socket) { var note = notes[noteId] // Only owner can change permission if (note.owner && note.owner === socket.request.user.id) { - if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousedits) return + if (permission === 'freely' && !config.allowAnonymous && !config.allowAnonymousEdits) return note.permission = permission models.Note.update({ permission: permission diff --git a/lib/response.js b/lib/response.js index 8dc1ed35..89ade7a7 100644 --- a/lib/response.js +++ b/lib/response.js @@ -17,7 +17,13 @@ var utils = require('./utils') // public var response = { errorForbidden: function (res) { - responseError(res, '403', 'Forbidden', 'oh no.') + const {req} = res + if (req.user) { + responseError(res, '403', 'Forbidden', 'oh no.') + } else { + req.flash('error', 'You are not allowed to access this page. Maybe try logging in?') + res.redirect(config.serverURL) + } }, errorNotFound: function (res) { responseError(res, '404', 'Not Found', 'oops.') @@ -59,7 +65,7 @@ function showIndex (req, res, next) { url: config.serverURL, useCDN: config.useCDN, allowAnonymous: config.allowAnonymous, - allowAnonymousEdits: config.allowAnonymousedits, + allowAnonymousEdits: config.allowAnonymousEdits, facebook: config.isFacebookEnable, twitter: config.isTwitterEnable, github: config.isGitHubEnable, @@ -96,7 +102,7 @@ function responseHackMD (res, note) { title: title, useCDN: config.useCDN, allowAnonymous: config.allowAnonymous, - allowAnonymousEdits: config.allowAnonymousedits, + allowAnonymousEdits: config.allowAnonymousEdits, facebook: config.isFacebookEnable, twitter: config.isTwitterEnable, github: config.isGitHubEnable, @@ -230,7 +236,8 @@ function showPublishNote (req, res, next) { lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, robots: meta.robots || false, // default allow robots GA: meta.GA, - disqus: meta.disqus + disqus: meta.disqus, + cspNonce: res.locals.nonce } return renderPublish(data, res) }).catch(function (err) { diff --git a/lib/web/auth/saml/index.js b/lib/web/auth/saml/index.js index 3ecbc6f3..b8d98340 100644 --- a/lib/web/auth/saml/index.js +++ b/lib/web/auth/saml/index.js @@ -20,14 +20,14 @@ passport.use(new SamlStrategy({ identifierFormat: config.saml.identifierFormat }, function (user, done) { // check authorization if needed - if (config.saml.externalGroups && config.saml.grouptAttribute) { + if (config.saml.externalGroups && config.saml.groupAttribute) { var externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute]) if (externalGroups.length > 0) { logger.error('saml permission denied: ' + externalGroups.join(', ')) return done('Permission denied', null) } } - if (config.saml.requiredGroups && config.saml.grouptAttribute) { + if (config.saml.requiredGroups && config.saml.groupAttribute) { if (intersection(config.saml.requiredGroups, user[config.saml.groupAttribute]).length === 0) { logger.error('saml permission denied') return done('Permission denied', null) diff --git a/lib/web/imageRouter/azure.js b/lib/web/imageRouter/azure.js new file mode 100644 index 00000000..cc98e5fc --- /dev/null +++ b/lib/web/imageRouter/azure.js @@ -0,0 +1,35 @@ +'use strict' +const path = require('path') + +const config = require('../../config') +const logger = require('../../logger') + +const azure = require('azure-storage') + +exports.uploadImage = function (imagePath, callback) { + if (!imagePath || typeof imagePath !== 'string') { + callback(new Error('Image path is missing or wrong'), null) + return + } + + if (!callback || typeof callback !== 'function') { + logger.error('Callback has to be a function') + return + } + + var azureBlobService = azure.createBlobService(config.azure.connectionString) + + azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, result, response) { + if (err) { + callback(new Error(err.message), null) + } else { + azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, response) { + if (err) { + callback(new Error(err.message), null) + } else { + callback(null, azureBlobService.getUrl(config.azure.container, result.name)) + } + }) + } + }) +} diff --git a/lib/web/imageRouter/filesystem.js b/lib/web/imageRouter/filesystem.js index da76ba5e..145876a9 100644 --- a/lib/web/imageRouter/filesystem.js +++ b/lib/web/imageRouter/filesystem.js @@ -2,6 +2,7 @@ const url = require('url') const config = require('../../config') +const logger = require('../../logger') exports.uploadImage = function (imagePath, callback) { if (!imagePath || typeof imagePath !== 'string') { @@ -10,7 +11,7 @@ exports.uploadImage = function (imagePath, callback) { } if (!callback || typeof callback !== 'function') { - callback(new Error('Callback has to be a function'), null) + logger.error('Callback has to be a function') return } diff --git a/lib/web/imageRouter/imgur.js b/lib/web/imageRouter/imgur.js index 31d5f55c..2a20002c 100644 --- a/lib/web/imageRouter/imgur.js +++ b/lib/web/imageRouter/imgur.js @@ -11,7 +11,7 @@ exports.uploadImage = function (imagePath, callback) { } if (!callback || typeof callback !== 'function') { - callback(new Error('Callback has to be a function'), null) + logger.error('Callback has to be a function') return } diff --git a/lib/web/imageRouter/minio.js b/lib/web/imageRouter/minio.js index 099cb926..b921c2d2 100644 --- a/lib/web/imageRouter/minio.js +++ b/lib/web/imageRouter/minio.js @@ -4,6 +4,7 @@ const path = require('path') const config = require('../../config') const {getImageMimeType} = require('../../utils') +const logger = require('../../logger') const Minio = require('minio') const minioClient = new Minio.Client({ @@ -21,7 +22,7 @@ exports.uploadImage = function (imagePath, callback) { } if (!callback || typeof callback !== 'function') { - callback(new Error('Callback has to be a function'), null) + logger.error('Callback has to be a function') return } diff --git a/lib/web/imageRouter/s3.js b/lib/web/imageRouter/s3.js index bcd3ea60..626fe148 100644 --- a/lib/web/imageRouter/s3.js +++ b/lib/web/imageRouter/s3.js @@ -4,6 +4,7 @@ const path = require('path') const config = require('../../config') const {getImageMimeType} = require('../../utils') +const logger = require('../../logger') const AWS = require('aws-sdk') const awsConfig = new AWS.Config(config.s3) @@ -16,7 +17,7 @@ exports.uploadImage = function (imagePath, callback) { } if (!callback || typeof callback !== 'function') { - callback(new Error('Callback has to be a function'), null) + logger.error('Callback has to be a function') return } diff --git a/lib/web/userRouter.js b/lib/web/userRouter.js index ecfbaf8b..963961c7 100644 --- a/lib/web/userRouter.js +++ b/lib/web/userRouter.js @@ -5,6 +5,7 @@ const Router = require('express').Router const response = require('../response') const models = require('../models') const logger = require('../logger') +const {generateAvatar} = require('../letter-avatars') const UserRouter = module.exports = Router() @@ -34,3 +35,9 @@ UserRouter.get('/me', function (req, res) { }) } }) + +UserRouter.get('/user/:username/avatar.svg', function (req, res, next) { + res.setHeader('Content-Type', 'image/svg+xml') + res.setHeader('Cache-Control', 'public, max-age=86400') + res.send(generateAvatar(req.params.username)) +}) |