diff --git a/.travis.yml b/.travis.yml index f9b90b4..9838d30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: node_js node_js: +- '6' - '5' -- '5.1' - '4' -- '4.1' env: - CXX=g++-4.8 diff --git a/README.md b/README.md index 0a21975..27d0acd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# mqp-server [![Version npm](https://img.shields.io/npm/v/mqp-server.svg?style=flat-square)](https://www.npmjs.com/package/mqp-server) [![npm Downloads](https://img.shields.io/npm/dm/mqp-server.svg?style=flat-square)](https://www.npmjs.com/package/mqp-server) [![Build Status](https://img.shields.io/travis/musiqpad/mqp-server/master.svg?style=flat-square)](https://travis-ci.org/musiqpad/mqp-server) +# mqp-server [![Version npm](https://img.shields.io/npm/v/mqp-server.svg?style=flat-square)](https://www.npmjs.com/package/mqp-server) [![npm Downloads](https://img.shields.io/npm/dm/mqp-server.svg?style=flat-square)](https://www.npmjs.com/package/mqp-server) [![Build Status](https://img.shields.io/travis/musiqpad/mqp-server/master.svg?style=flat-square)](https://travis-ci.org/musiqpad/mqp-server) [![devDependency Status](https://david-dm.org/musiqpad/mqp-server/dev-status.svg?style=flat-square)](https://david-dm.org/musiqpad/mqp-server#info=devDependencies) [![NPM](https://nodei.co/npm/mqp-server.png)](https://npmjs.org/package/mqp-server) diff --git a/config.example.hjson b/config.example.hjson index 0cd6562..c667f09 100644 --- a/config.example.hjson +++ b/config.example.hjson @@ -1,5 +1,13 @@ { - /* + /* _ _ + _ __ ___ _ _ ___(_) __ _ _ __ __ _ __| | + | '_ ` _ \| | | / __| |/ _` | '_ \ / _` |/ _` | + | | | | | | |_| \__ \ | (_| | |_) | (_| | (_| | + |_| |_| |_|\__,_|___/_|\__, | .__/ \__,_|\__,_| + |_|_| + + More infos about the config syntax: https://hjson.org/ + Set this flag to false to disable web server hosting or true to enable web server hosting. This is useful if you want to host static files in another web server such as nginx. @@ -101,7 +109,7 @@ twitter: "@musiqpad" description: // A one to two sentence description for search engines & co ''' - + Real time music streaming and chat with friends. Musiqpad is a place where people can discover new music. ''' themeColor: "" // a hex color for the theme on chrome for android favicon: "/pads/lib/img/icon.png" @@ -129,9 +137,8 @@ } } - // The amount of time users stay logged in for before having to login again in days. - // 0 = login every time - loginExpire: 7 + // The amount of time users stay logged in for before having to login again, eg "2 days", "10h", "7d" + loginExpire: "5d", db: { dbType: "level" // Values "level" for LevelDB, "mysql" for MySQL and "mongo" for MongoDB dbDir: "./socketserver/db" // Only used for LevelDB. Directory to save databases. Default is ./socketserver/db @@ -413,17 +420,36 @@ color: "#964B74" } permissions: [ + "djqueue.join" + "djqueue.joinlocked" + "djqueue.leave" + "djqueue.skip.self" "djqueue.skip.other" "djqueue.lock" "djqueue.cycle" + "djqueue.limit" "djqueue.move" + "djqueue.playLiveVideos" + "djqueue.limit.bypass" + "djqueue.lock.bypass" "chat.send" + "chat.private" "chat.delete" "chat.specialMention" + "chat.broadcast" + "chat.staff" + "playlist.create" + "playlist.delete" + "playlist.rename" + "playlist.import" + "playlist.shuffle" + "room.grantroles" "room.restrict.ban" "room.restrict.mute" "room.restrict.mute_silent" "room.ratelimit.bypass" + "room.whois" + "room.whois.iphistory" ] canGrantRoles: [ ] @@ -470,4 +496,5 @@ ] } } -} \ No newline at end of file + tokenSecret: "" // This should be a random string that needs to be kept private. Automatically generated if empty. +} diff --git a/package.json b/package.json index 20b30e3..c657275 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "basic-logger": "^0.4.4", + "bcrypt-nodejs": "0.0.3", "chalk": "^1.0.0", "clean-css": "^3.4.9", "compression": "^1.6.2", @@ -37,18 +38,19 @@ "file-tail": "^0.3.0", "forever": "^0.15.1", "fs-extra": "^0.30.0", + "helmet": "^2.1.1", "hjson": "^1.8.4", + "jsonwebtoken": "^7.1.6", "leveldown": "1.4.4", "levelup": "^1.3.1", - "mongodb": "^2.1.16", + "mongodb": "^2.2.4", "mysql": "^2.10.2", "nconf": "^0.8.4", "nodemailer": "^2.1.0", - "path": "^0.12.7", "ps-tree": "^1.0.1", - "request": "^2.67.0", - "update-notifier": "^0.7.0", - "ws": "^1.0.1", + "request": "^2.74.0", + "update-notifier": "^1.0.2", + "ws": "^1.1.1", "xoauth2": "^1.1.0", "yesno": "0.0.1" }, @@ -56,8 +58,8 @@ "ava": "^0.15.2", "eslint": "^2.13.1", "eslint-config-airbnb": "^9.0.1", - "eslint-plugin-import": "^1.9.2", - "eslint-plugin-jsx-a11y": "^1.5.3", + "eslint-plugin-import": "^1.11.1", + "eslint-plugin-jsx-a11y": "^1.5.5", "eslint-plugin-react": "^5.2.2" }, "ava": { diff --git a/socketserver/YT.js b/socketserver/YT.js index e6d5812..06ae7a9 100644 --- a/socketserver/YT.js +++ b/socketserver/YT.js @@ -1,8 +1,8 @@ -var https = require('https'); -var util = require('util'); -var log = new (require('basic-logger'))({showTimestamp: true, prefix: "YT"}); -var querystring = require('querystring'); -var Duration = require("durationjs"); +const https = require('https'); +const util = require('util'); +const log = new (require('basic-logger'))({showTimestamp: true, prefix: "YT"}); +const querystring = require('querystring'); +const Duration = require("durationjs"); const nconf = require('nconf'); const key = nconf.get('apis:YT:key'); diff --git a/socketserver/database.js b/socketserver/database.js index 5f73840..da5932f 100644 --- a/socketserver/database.js +++ b/socketserver/database.js @@ -1,19 +1,100 @@ +'use strict'; const nconf = require('nconf'); +const LevelDB = require('./db_level'); +const MySQL = require('./db_mysql'); +const MongoDB = require('./db_mongo'); +const utils = require('./utils'); +let Database; -function Database() { - nconf.defaults({ - 'db:dbType': 'level' - }); - switch (nconf.get('db:dbType')) { - case 'level': - return require('./db_level'); - case 'mysql': - return require('./db_mysql'); - case 'mongo': - return require('./db_mongo'); - default: - return require('./db_level'); - } +var util = require('util'); + +switch (nconf.get('db:dbType')) { + case 'level': + Database = LevelDB; + break; + case 'mysql': + Database = MySQL; + break; + case 'mongo': + Database = MongoDB; + break; + default: + Database = LevelDB; +} + +function loginCallback(callback) { + return function (err, user, email) { + if (email) { + callback(null, user, utils.token.createToken({ email }, nconf.get('tokenSecret'), nconf.get('loginExpire'))); + return; + } + callback(err); + }; +} + +class DB extends Database { + loginUser(obj, callback) { + if (obj.token) { + try { + obj.email = utils.token.verify(obj.token, nconf.get('tokenSecret')).email; + } catch (e) { + if (e) { + callback('InvalidToken'); + return; + } + } + } + + this.getUser(obj.email, (err, user) => { + if ((err && err.notFound) || user == null) { + callback('UserNotFound'); + return; + } + + if (err) { + callback(err); + return; + } + // If the user has an old md5 password saved in the db + if (typeof user.data.pw === 'string' && utils.hash.isMD5(user.data.pw) && !obj.token) { + // And if that md5 password matches with the supplied pw + if (utils.db.makePassMD5(obj.pw, user.data.salt) !== user.data.pw) { + callback('IncorrectPassword'); + return; + } + // Update the pw to a new bcrypt password + user.pw = obj.pw; + super.loginUser(obj.email, loginCallback(callback)); + // If user has an md5 password and only supplied a token + } else if (utils.hash.isMD5(user.data.pw) && obj.token) { + // Say token is invalid so we get the password instead of the token next time + callback('InvalidToken'); + } else if (obj.token) { + // Check if the token is correct + utils.token.verify(obj.token, nconf.get('tokenSecret'), (err, decoded) => { + if (err) { + callback('InvalidToken'); + return; + } + const email = decoded.email; + super.loginUser(email, loginCallback(callback)); + }); + } else if (obj.pw && utils.hash.compareBcrypt(obj.pw, user.data.pw)) { + super.loginUser(obj.email, loginCallback(callback)); + } else { + callback('IncorrectPassword'); + } + }); + } + createUser(obj, callback) { + if (obj.pw) { + obj.pw = utils.hash.bcrypt(obj.pw); + super.createUser(obj, loginCallback(callback)); + } else { + callback('InvalidPassword'); + } + } } -module.exports = new Database(); +const db = new DB(); +module.exports = db; diff --git a/socketserver/database_util.js b/socketserver/database_util.js deleted file mode 100644 index e32b9ba..0000000 --- a/socketserver/database_util.js +++ /dev/null @@ -1,17 +0,0 @@ -const Hash = require('./hash'); - -function DBUtils() {} - -DBUtils.prototype.makePass = function (inPass, salt) { - return Hash.md5(('' + inPass) + (salt || '')).toString(); -}; - -DBUtils.prototype.validateEmail = function (email) { - return /^.+@.+\..+$/.test(email); -}; - -DBUtils.prototype.validateUsername = function (un) { - return /^[a-z0-9_-]{3,20}$/i.test(un); -}; - -module.exports = new DBUtils(); diff --git a/socketserver/db_level.js b/socketserver/db_level.js index f0fc439..c808e02 100644 --- a/socketserver/db_level.js +++ b/socketserver/db_level.js @@ -11,8 +11,9 @@ const log = new(require('basic-logger'))({ const nconf = require('nconf'); // Files -var Mailer = require('./mail/Mailer'); -const DBUtils = require('./database_util'); +const Mailer = require('./mail/mailer'); +const DBUtils = require('./utils').db; +const User = require('./user'); // Variables let currentPID = 0; @@ -86,18 +87,7 @@ function LevelDB(callback) { if (callback) callback(null, db); }); } - // TokenDB - if (!this.TokenDB) - this.TokenDB = setupDB(`${dbdir}/tokens`, - - // If new DB is created - function (newdb) {}, - - // Callback - function (err, db) { - if (err) log.error('Could not open TokenDB: ' + err); - }); - + // UserDB if (!this.UserDB) this.UserDB = setupDB(`${dbdir}/users`, @@ -212,7 +202,7 @@ LevelDB.prototype.getJSON = function (db, key, callback) { try { val = JSON.parse(val); } catch (e) { - console.log('Database key "' + key + '" returned malformed JSON object'); + console.log('Database key ' + key + ' returned malformed JSON object'); val = null; } } @@ -288,40 +278,6 @@ LevelDB.prototype.setRoom = function (slug, val, callback) { return this; }; -// TokenDB -LevelDB.prototype.deleteToken = function (tok) { - this.TokenDB.del(tok); -}; - -LevelDB.prototype.createToken = function (email) { - var tok = DBUtils.makePass(email, Date.now()); - - this.putJSON(this.TokenDB, tok, { - email, - time: Date.now(), - }); - - return tok; -}; - -LevelDB.prototype.isTokenValid = function (tok, callback) { - var that = this; - - this.getJSON(this.TokenDB, tok, function (err, data) { - if (err || data == null) { - callback('InvalidToken'); - return; - } - - if (nconf.get('loginExpire') && (Date.now() - data.time) < expires) { - callback(null, data.email); - } else { - that.deleteToken(data.token); - callback('InvalidToken'); - } - }); -}; - // UserDB function addUsername(un) { usernames.push(un.toLowerCase()); @@ -340,7 +296,6 @@ function usernameExists(un) { } LevelDB.prototype.createUser = function (obj, callback) { - var User = require('./user'); var that = this; var defaultCreateObj = { @@ -366,10 +321,6 @@ LevelDB.prototype.createUser = function (obj, callback) { callback('UsernameExists'); return; } - if (!inData.pw || inData.pw == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') { - callback('PasswordBlank'); - return; - } // Check for existing account this.userEmailExists(inData.email, function (err, res) { @@ -383,14 +334,11 @@ LevelDB.prototype.createUser = function (obj, callback) { user.data.uid = currentUID++; that.UserDB.put('UIDCOUNTER', currentUID); user.data.un = inData.un; - user.data.salt = DBUtils.makePass(Date.now()).slice(0, 10); - user.data.pw = DBUtils.makePass(inData.pw, user.data.salt); + user.data.pw = inData.pw; user.data.created = Date.now(); - if (nconf.get('room:mail:confirmation')) user.data.confirmation = DBUtils.makePass(Date.now()); + if (nconf.get('room:mail:confirmation')) user.data.confirmation = DBUtils.randomBytes(18, 'base64'); var updatedUserObj = user.makeDbObj(); - var tok = that.createToken(inData.email); - that.putJSON(that.UserDB, inData.email, updatedUserObj, function (err) { if (err) { callback(err); @@ -410,77 +358,29 @@ LevelDB.prototype.createUser = function (obj, callback) { // Do other ~messy~ stuff addUsername(inData.un); user.login(inData.email); - callback(null, user, tok); + callback(null, user, inData.email); }); }); }; -LevelDB.prototype.loginUser = function (obj, callback) { - var User = require('./user'); - var that = this; - - var defaultLoginObj = { - email: null, - pw: null, - token: null, - }; - util._extend(defaultLoginObj, obj); - - var inData = defaultLoginObj; - - if (inData.email && inData.pw) { - inData.email = inData.email.toLowerCase(); - - this.getJSON(this.UserDB, inData.email, function (err, data) { - if ((err && err.notFound) || data == null) { - callback('UserNotFound'); - return; - } - - if (err) { - callback(err); - return; - } - - if (DBUtils.makePass(inData.pw, data.salt) != data.pw) { - callback('IncorrectPassword'); - return; - } - - var tok = that.createToken(inData.email); - var user = new User(); - - user.login(inData.email, data, function () { - callback(null, user, tok); - }); - }); - } else if (inData.token) { - that.isTokenValid(inData.token, function (err, email) { - if (err) { - callback(err); - return; - } - - that.getJSON(that.UserDB, email, function (err, data) { - if ((err && err.notFound) || data == null) { - callback('UserNotFound'); - return; - } - - if (err) { - callback(err); - return; - } - - var user = new User(); - user.login(email, data, function () { - callback(null, user); - }); - }); - }); - } else { - callback('InvalidArgs'); - } +LevelDB.prototype.loginUser = function (email, callback) { + if (email) { + email = email.toLowerCase(); + this.getJSON(this.UserDB, email, (err, data) => { + if ((err && err.notFound) || data == null) { + callback('UserNotFound'); + return; + } + if (err) { + callback(err); + return; + } + const user = new User(); + user.login(email, data, () => { + callback(null, user, email); + }); + }); + } }; LevelDB.prototype.putUser = function (email, data, callback) { @@ -488,8 +388,6 @@ LevelDB.prototype.putUser = function (email, data, callback) { }; LevelDB.prototype.getUser = function (email, callback) { - var User = require('./user'); - this.getJSON(this.UserDB, email, function (err, data) { if ((err && err.notFound) || data == null) { callback('UserNotFound'); return; } @@ -515,7 +413,6 @@ LevelDB.prototype.deleteUser = function (email, callback) { }; LevelDB.prototype.getUserByUid = function (uid, opts, callback) { - var User = require('./user'); var done = false; if (typeof opts === 'function') { @@ -586,7 +483,6 @@ LevelDB.prototype.getUserByUid = function (uid, opts, callback) { }; LevelDB.prototype.getUserByName = function (name, opts, callback) { - var User = require('./user'); var done = false; if (typeof opts === 'function') { @@ -768,4 +664,4 @@ LevelDB.prototype.getIpHistory = function (uid, callback) { }); }; -module.exports = new LevelDB(); +module.exports = LevelDB; diff --git a/socketserver/db_mongo.js b/socketserver/db_mongo.js index 96ca9f2..479a8f6 100644 --- a/socketserver/db_mongo.js +++ b/socketserver/db_mongo.js @@ -3,14 +3,15 @@ const mongodb = require('mongodb').MongoClient; const util = require('util'); const log = new(require('basic-logger'))({ - showTimestamp: true, - prefix: 'MongoDB' + showTimestamp: true, + prefix: 'MongoDB' }); // Files const nconf = require('nconf'); -const Mailer = require('./mail/Mailer'); -const DBUtils = require('./database_util'); +const Mailer = require('./mail/mailer'); +const DBUtils = require('./utils').db; +const User = require('./user'); // Variables const expires = 1000 * 60 * 60 * 24 * nconf.get('loginExpire'); @@ -21,7 +22,6 @@ let ready = false; let playlistscol = null; let roomcol = null; -let tokenscol = null; let userscol = null; let chatcol = null; let pmscol = null; @@ -81,23 +81,6 @@ function createCollectionsIfNoExist(callback) { } }); - db.collection('tokens', { - strict: true - }, function (err, col) { - if (err) { - db.createCollection('tokens', function (errc, result) { - if (errc) - throw new Error('Failed to create the tokens collection'); - - tokenscol = result; - if (++step == total) callback(); - }); - } else { - tokenscol = col; - if (++step == total) callback(); - } - }); - db.collection('users', { strict: true }, function (err, col) { @@ -400,51 +383,6 @@ MongoDB.prototype.setRoom = function (slug, val, callback) { return this; }; -// TokenDB -MongoDB.prototype.deleteToken = function (tok) { - dbQueue(function () { - tokenscol.remove({ - tok - }, function () {}); - }); -}; - -MongoDB.prototype.createToken = function (email) { - var tok = DBUtils.makePass(email, Date.now()); - - dbQueue(function () { - tokenscol.insert({ - tok, - email, - time: Date.now(), - }, function () {}); - }); - - return tok; -}; - -MongoDB.prototype.isTokenValid = function (tok, callback) { - var that = this; - - dbQueue(function () { - tokenscol.findOne({ - tok - }, function (err, data) { - if (err || data == null) { - callback('InvalidToken'); - return; - } - - if (nconf.get('loginExpire') && (Date.now() - data.time) < expires) { - callback(null, data.email); - } else { - that.deleteToken(data.token); - callback('InvalidToken'); - } - }); - }); -}; - // UserDB function addUsername(un) { usernames.push(un.toLowerCase()); @@ -458,7 +396,6 @@ function usernameExists(un) { } MongoDB.prototype.createUser = function (obj, callback) { - var User = require('./user'); var that = this; var defaultCreateObj = { @@ -484,10 +421,6 @@ MongoDB.prototype.createUser = function (obj, callback) { callback('UsernameExists'); return; } - if (!inData.pw || inData.pw == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') { - callback('PasswordBlank'); - return; - } dbQueue(function () { // Check for existing account @@ -502,18 +435,14 @@ MongoDB.prototype.createUser = function (obj, callback) { user.data.uid = currentUID; user.data.un = inData.un; - user.data.salt = DBUtils.makePass(Date.now()).slice(0, 10); - user.data.pw = DBUtils.makePass(inData.pw, user.data.salt); + user.data.pw = inData.pw; user.data.created = Date.now(); - if (nconf.get('room:mail:confirmation')) { - user.data.confirmation = DBUtils.makePass(Date.now()); - } + if (nconf.get('room:mail:confirmation')) user.data.confirmation = DBUtils.randomBytes(18, 'base64'); + var updatedUserObj = user.makeDbObj(); updatedUserObj._id = currentUID; updatedUserObj.email = inData.email; - var tok = that.createToken(inData.email); - userscol.insert(updatedUserObj, function (error, data) { if (error) { callback(error); @@ -533,89 +462,34 @@ MongoDB.prototype.createUser = function (obj, callback) { // Do other ~messy~ stuff addUsername(inData.un); user.login(inData.email); - callback(null, user, tok); + callback(null, user, inData.email); }); }); }); }); }; -MongoDB.prototype.loginUser = function (obj, callback) { - var User = require('./user'); - var that = this; - - var defaultLoginObj = { - email: null, - pw: null, - token: null, - }; - util._extend(defaultLoginObj, obj); - - var inData = defaultLoginObj; - - dbQueue(function () { - if (inData.email && inData.pw) { - inData.email = inData.email.toLowerCase(); - - userscol.findOne({ - email: inData.email - }, { - _id: 0 - }, function (err, data) { - if (err) { - callback(err); - return; - } - - if (!data) { - callback('UserNotFound'); - return; - } - - if (DBUtils.makePass(inData.pw, data.salt) != data.pw) { - callback('IncorrectPassword'); - return; - } - - var tok = that.createToken(inData.email); - var user = new User(); - - user.login(inData.email, data, function () { - callback(null, user, tok); - }); - }); - } else if (inData.token) { - that.isTokenValid(inData.token, function (err, email) { - if (err) { - callback(err); - return; - } - - userscol.findOne({ - email - }, { - _id: 0 - }, function (err, data) { - if (err) { - callback(err); - return; - } - - if (!data) { - callback('UserNotFound'); - return; - } - - var user = new User(); - user.login(email, data, function () { - callback(null, user); - }); - }); - }); - } else { - callback('InvalidArgs'); - } - }); +MongoDB.prototype.loginUser = function (email, callback) { + if (email) { + email = email.toLowerCase(); + dbQueue(() => { + userscol.findOne({ email }, { _id: 0 }, (err, data) => { + if (err) { + callback(err); + return; + } + if (!data) { + callback('UserNotFound'); + return; + } + + const user = new User(); + user.login(email, data, () => { + callback(null, user, email); + }); + }); + }); + } }; MongoDB.prototype.putUser = function (email, data, callback) { @@ -636,8 +510,6 @@ MongoDB.prototype.putUser = function (email, data, callback) { }; MongoDB.prototype.getUser = function (email, callback) { - var User = require('./user'); - dbQueue(function () { userscol.findOne({ email @@ -683,8 +555,6 @@ MongoDB.prototype.deleteUser = function (email, callback) { }; MongoDB.prototype.getUserByUid = function (uid, opts, callback) { - var User = require('./user'); - if (typeof opts === 'function') { callback = opts; opts = {}; @@ -741,8 +611,6 @@ MongoDB.prototype.getUserByUid = function (uid, opts, callback) { }; MongoDB.prototype.getUserByName = function (name, opts, callback) { - var User = require('./user'); - if (typeof opts === 'function') { callback = opts; opts = {}; @@ -954,4 +822,4 @@ MongoDB.prototype.getIpHistory = function (uid, callback) { }); }; -module.exports = new MongoDB(); +module.exports = MongoDB; diff --git a/socketserver/db_mysql.js b/socketserver/db_mysql.js index 603f233..d6be746 100644 --- a/socketserver/db_mysql.js +++ b/socketserver/db_mysql.js @@ -1,7 +1,7 @@ +'use strict'; //Modules const mysql = require('mysql'); const util = require('util'); -const _ = require('underscore'); const log = new(require('basic-logger'))({ showTimestamp: true, prefix: "MysqlDB" @@ -9,18 +9,18 @@ const log = new(require('basic-logger'))({ const nconf = require('nconf'); //Files -var Hash = require('./hash'); -const Mailer = require('./mail/Mailer'); -var DBUtils = require('./database_util'); -var Roles = require('./role.js'); +const Mailer = require('./mail/mailer'); +const DBUtils = require('./utils').db; +const Roles = require('./role.js'); +const User = require('./user'); -var db = null; -var pool = null; +let db = null; +let pool = null; -var MysqlDB = function(){ - var that = this; +const MysqlDB = function(){ + const that = this; - var mysqlConfig = { + const mysqlConfig = { host: nconf.get('db:mysqlHost'), user: nconf.get('db:mysqlUser'), password: nconf.get('db:mysqlPassword'), @@ -429,36 +429,9 @@ MysqlDB.prototype.setRoom = function(slug, val, callback) { return that; }; -//TokenDB -MysqlDB.prototype.deleteToken = function(tok) { - this.execute("DELETE FROM `tokens` WHERE ?;", { token: tok, }); -}; - -MysqlDB.prototype.createToken = function(email) { - var tok = DBUtils.makePass(email, Date.now()); - - this.execute("DELETE FROM `tokens` WHERE ?; INSERT INTO `tokens` SET ?;", [ { email: email, }, { token: tok, email: email, created: new Date() } ]); - - return tok; -}; - -MysqlDB.prototype.isTokenValid = function(tok, callback) { - this.execute("SELECT `token`, `email` FROM `tokens` WHERE ? AND DATEDIFF(NOW(), `created`) < ?;", [{ token: tok, }, nconf.get('loginExpire') || 365], function(err, res) { - if (err || res.length == 0) { - callback('InvalidToken'); - return; - } - - callback(null, res[0].email); - }); -}; - //UserDB MysqlDB.prototype.getUserNoLogin = function(uid, callback){ var that = this; - - var User = require('./user'); - this.execute("SELECT `salt`, `lastdj`, `uptime`, `recovery`, UNIX_TIMESTAMP(`recovery_timeout`) as `recovery_timeout`, `confirmation`, `badge_top`, `badge_bottom`, `created`, `activepl`, `pw`, `un`, `id` FROM `users` WHERE ?", { id: uid, }, function(err, res){ if (err || res.length == 0) { callback('UserNotFound'); return; } @@ -506,7 +479,6 @@ MysqlDB.prototype.usernameExists = function(name, callback){ }; MysqlDB.prototype.createUser = function(obj, callback) { - var User = require('./user'); var that = this; var defaultCreateObj = { @@ -547,14 +519,11 @@ MysqlDB.prototype.createUser = function(obj, callback) { var user = new User(); user.data.un = inData.un; - user.data.salt = DBUtils.makePass(Date.now()).slice(0, 10); - user.data.pw = DBUtils.makePass(inData.pw, user.data.salt); + user.data.pw = inData.pw; user.data.created = Date.now(); - if (nconf.get('room:mail:confirmation')) user.data.confirmation = DBUtils.makePass(Date.now()); + if (nconf.get('room:mail:confirmation')) user.data.confirmation = DBUtils.randomBytes(18, 'base64'); var updatedUserObj = user.makeDbObj(); - var tok = that.createToken(inData.email); - delete updatedUserObj.uid; that.putUser(inData.email, updatedUserObj, function(err, id) { @@ -576,71 +545,30 @@ MysqlDB.prototype.createUser = function(obj, callback) { //Login user user.data.uid = id; user.login(inData.email); - callback(null, user, tok); + callback(null, user, inData.email); }); }); } }); }; -MysqlDB.prototype.loginUser = function(obj, callback) { - var User = require('./user'); - var that = this; - - var defaultLoginObj = { - email: null, - pw: null, - token: null, - }; - util._extend(defaultLoginObj, obj); - var inData = defaultLoginObj; - - if (inData.email && inData.pw) { - inData.email = inData.email.toLowerCase(); - that.execute("SELECT `id` FROM `users` WHERE ?;", { email: inData.email, }, function(err, res) { - if(err || res.length == 0) callback("UserNotFound"); - else { - that.getUserNoLogin(res[0].id, function(err, data) { - if (DBUtils.makePass(inData.pw, data.salt) != data.pw) { - callback('IncorrectPassword'); - return; - } - - var tok = that.createToken(inData.email); - - var user = new User(); - - user.login(inData.email, data, function() { - - callback(null, user, tok); - }); - }); - } - }); - } else if (inData.token) { - that.isTokenValid(inData.token, function(err, email) { - if (err) { - callback(err); - return; - } - - that.execute("SELECT `id` FROM `users` WHERE ?;", { email: email, }, function(err, res) { - if(err || res.length == 0) callback("UserNotFound"); - else { - that.getUserNoLogin(res[0].id, function(err, data) { - var user = new User(); - - user.login(email, data, function() { - - callback(null, user); - }); - }); - } - }); - }); - } else { - callback('InvalidArgs'); - } +MysqlDB.prototype.loginUser = function (email, callback) { + const that = this; + if (email) { + email = email.toLowerCase(); + that.execute('SELECT `id` FROM `users` WHERE ?;', { email }, (err, res) => { + if (err || res.length === 0) { + callback('UserNotFound'); + } else { + that.getUserNoLogin(res[0].id, (err, data) => { + const user = new User(); + user.login(email, data, () => { + callback(null, user, email); + }); + }); + } + }); + } }; MysqlDB.prototype.putUser = function(email, data, callback) { @@ -679,9 +607,6 @@ MysqlDB.prototype.putUser = function(email, data, callback) { MysqlDB.prototype.getUser = function(email, callback){ var that = this; - - var User = require('./user'); - this.execute("SELECT `id` FROM `users` WHERE ?;", { email: email, }, function(err, res) { if(err || res.length == 0){ callback('UserNotFound'); @@ -722,9 +647,6 @@ MysqlDB.prototype.getUserByUid = function(uid, opts, callback) { return; } } - - var User = require('./user'); - if(Array.isArray(uid)){ var out = {}; var initialized = 0; @@ -764,9 +686,6 @@ MysqlDB.prototype.getUserByUid = function(uid, opts, callback) { MysqlDB.prototype.getUserByName = function(name, opts, callback) { var that = this; - - var User = require('./user'); - if (typeof opts === 'function') { callback = opts; opts = {}; @@ -889,4 +808,4 @@ MysqlDB.prototype.getIpHistory = function(uid, callback) { }); }; -module.exports = new MysqlDB; +module.exports = MysqlDB; diff --git a/socketserver/hash.js b/socketserver/hash.js deleted file mode 100644 index ac2489b..0000000 --- a/socketserver/hash.js +++ /dev/null @@ -1,9 +0,0 @@ -function md5(str){ - var crypto = require('crypto'); - - var hash = crypto.createHash('md5'); - hash.update(str); - return hash.digest('hex'); -} - -module.exports.md5 = md5; \ No newline at end of file diff --git a/socketserver/playlist.js b/socketserver/playlist.js index f0c7c73..1ae09eb 100644 --- a/socketserver/playlist.js +++ b/socketserver/playlist.js @@ -1,7 +1,7 @@ -var util = require('util'); -var DB = require('./database'); -var YT = require('./YT'); -var log = new (require('basic-logger'))({showTimestamp: true, prefix: "Playlist"}); +const util = require('util'); +const DB = require('./database'); +const YT = require('./YT'); +const log = new (require('basic-logger'))({showTimestamp: true, prefix: "Playlist"}); // Every user obj starts with this, then gets extended by what's in the db diff --git a/socketserver/socketserver.js b/socketserver/socketserver.js index 55cb67d..5beec20 100644 --- a/socketserver/socketserver.js +++ b/socketserver/socketserver.js @@ -1,27 +1,24 @@ 'use strict'; //Modules -var ws = require('ws'); -var http = require('http'); -var https = require('https'); -var Duration = require('durationjs'); -var request = require('request'); -var util = require('util'); -var extend = require('extend'); -var updateNotifier = require('update-notifier'); -var _ = require('underscore'); +const ws = require('ws'); +const http = require('http'); +const https = require('https'); +const Duration = require('durationjs'); +const request = require('request'); +const extend = require('extend'); +const updateNotifier = require('update-notifier'); const fs = require('fs-extra'); const nconf = require('nconf'); +const crypto = require('crypto'); //Files -var DB = require("./database"); -var Room = require('./room'); -var User = require('./user'); -var Mailer = require('./mail/Mailer'); -var YT = require('./YT'); -var Roles = require('./role'); -var Hash = require('./hash'); -var log = new (require('basic-logger'))({showTimestamp: true, prefix: "SocketServer"}); -var WebSocketServer = ws.Server; +const DB = require("./database"); +const Room = require('./room'); +const Mailer = require('./mail/Mailer'); +const YT = require('./YT'); +const Roles = require('./role'); +const log = new (require('basic-logger'))({showTimestamp: true, prefix: "SocketServer"}); +const WebSocketServer = ws.Server; ws.prototype.sendJSON = function(obj){ @@ -427,7 +424,7 @@ var SocketServer = function(server){ console.log("Sending recovery email"); var sendRecovery = function(user){ //Generate new code and send email - user.recovery = Hash.md5(Date.now() + '', user.un); + user.recovery = utils.db.randomBytes(36, 'base64'); Mailer.sendEmail('recovery', { user: user.un, code: user.recovery.code, diff --git a/socketserver/user.js b/socketserver/user.js index a37a2c9..5ab67ee 100644 --- a/socketserver/user.js +++ b/socketserver/user.js @@ -1,13 +1,14 @@ -var util = require('util'); -var DB = require('./database'); -var DBUtils = require('./database_util'); +'use strict' +const util = require('util'); +let DB; +const utils = require('./utils'); // Every user obj starts with this, then gets extended by what's in the db var defaultObj = function(){ return { uid: 0, un: "", - pw: "", // MD5(SHA256(pass) + SALT) + pw: "", role: null, activepl: null, created: 0, @@ -85,6 +86,7 @@ function removeFields(obj, fields){ * */ function User(){ + DB = require('./database'); this.userExists = false; this.data = new defaultObj; } @@ -270,10 +272,8 @@ Object.defineProperty( User.prototype, 'pw', { return this.data.pw; }, set: function(val) { - - this.data.pw = DBUtils.makePass(val, this.data.salt); + this.data.pw = utils.hash.bcrypt(val); this.updateUser(); - return this; } }); @@ -402,4 +402,4 @@ Object.defineProperty( User.prototype, 'blocked', { } }); -module.exports = User; \ No newline at end of file +module.exports = User; diff --git a/socketserver/utils/database.js b/socketserver/utils/database.js new file mode 100644 index 0000000..6588897 --- /dev/null +++ b/socketserver/utils/database.js @@ -0,0 +1,22 @@ +const Hash = require('./hash'); +const crypto = require("crypto"); + +const DBUtils = { + validateEmail(email) { + return /^.+@.+\..+$/.test(email); + }, + + validateUsername(un) { + return /^[a-z0-9_-]{3,20}$/i.test(un); + }, + + makePassMD5(inPass, salt) { + return Hash.md5(('' + inPass) + (salt || '')).toString(); + }, + + randomBytes(bytes, format) { + return crypto.randomBytes(bytes).toString(format); + } +}; + +module.exports = DBUtils; diff --git a/socketserver/utils/hash.js b/socketserver/utils/hash.js new file mode 100644 index 0000000..85d8134 --- /dev/null +++ b/socketserver/utils/hash.js @@ -0,0 +1,18 @@ +const crypto = require('crypto'); +const bcrypt = require('bcrypt-nodejs'); + +module.exports = { + md5(str) { + const hash = crypto.createHash('md5'); + hash.update(str); + return hash.digest('hex'); + }, + isMD5(hash) { + return (/[a-fA-F0-9]{32}/).test(hash); + }, + bcrypt(str) { + const salt = bcrypt.genSaltSync(12); + return bcrypt.hashSync(str, salt); + }, + compareBcrypt: bcrypt.compareSync, +} \ No newline at end of file diff --git a/socketserver/utils/index.js b/socketserver/utils/index.js new file mode 100644 index 0000000..d864eab --- /dev/null +++ b/socketserver/utils/index.js @@ -0,0 +1,8 @@ +const database = require('./database'); +const hash = require('./hash'); +const token = require('./token'); + +const utils = { + +}; +module.exports = Object.assign(utils, { db: database }, { hash }, { token }); diff --git a/socketserver/utils/token.js b/socketserver/utils/token.js new file mode 100644 index 0000000..b8824d0 --- /dev/null +++ b/socketserver/utils/token.js @@ -0,0 +1,11 @@ +const jwt = require("jsonwebtoken"); + +module.exports = { + createToken(payload, secret, expires) { + return jwt.sign(payload, secret, { + expiresIn: expires + }); + }, + verify: jwt.verify, + decode: jwt.decode, +} \ No newline at end of file diff --git a/start.js b/start.js index 3b7357a..a118484 100644 --- a/start.js +++ b/start.js @@ -4,6 +4,7 @@ const nconf = require('nconf'); const fs = require('fs-extra'); const hjson = require('hjson'); +const crypto = require('crypto'); const hjsonWrapper = { parse: (text) => hjson.parse(text, { keepWsc: true, }), @@ -14,6 +15,12 @@ if (!fileExistsSync('config.hjson')) { } nconf.argv().env().file({ file: 'config.hjson', format: hjsonWrapper }); +if (!nconf.get('tokenSecret')) { + const random = crypto.randomBytes(256); + nconf.set('tokenSecret', random.toString('hex')); + nconf.save(); +} + // Modules const SocketServer = require('./socketserver/socketserver'); const path = require('path'); diff --git a/test/socketserver/hash.js b/test/socketserver/hash.js deleted file mode 100644 index ecfc9ba..0000000 --- a/test/socketserver/hash.js +++ /dev/null @@ -1,9 +0,0 @@ -const test = require('ava'); -const md5 = require('./../../socketserver/hash.js').md5; - -test.before(() => { -}); - -test('hash', t => { - t.is(md5('test'), '098f6bcd4621d373cade4e832627b4f6'); -}); diff --git a/test/socketserver/database_util.js b/test/socketserver/utils/database.js similarity index 60% rename from test/socketserver/database_util.js rename to test/socketserver/utils/database.js index 055b9ab..83e3ed9 100644 --- a/test/socketserver/database_util.js +++ b/test/socketserver/utils/database.js @@ -1,28 +1,20 @@ -const test = require('ava'); -const DBUtils = require('./../../socketserver/database_util'); - -function makePass(t, input, expected) { - t.is(DBUtils.makePass(input[0], input[1]), expected); -} - -function validateEmail(t, input, expected) { - t.is(DBUtils.validateEmail(input), expected); -} - -function validateUsername(t, input, expected) { - t.is(DBUtils.validateUsername(input), expected); -} - -test('Creates correct password hash with salt', makePass, ['test', 'randomSalt'], '4b4e47738ba3b7aab65e421787b519ff'); -test('Creates correct hash without salt', makePass, ['test', null], '098f6bcd4621d373cade4e832627b4f6'); -test('Hash is always converted to a string', makePass, ['ximaz', null], '61529519452809720693702583126814'); - -test('user@example.com is a valid email', validateEmail, 'user@example.com', true); -test('@example.com isn\'t a valid email', validateEmail, '@example.com', false); -test('user@example. isn\'t a valid email', validateEmail, 'user@example.', false); - -test('123 is a valid username', validateUsername, '123', true); -test('123456789101112131415 is\n a valid username', validateUsername, '123456789101112131415', false); -test('*user* is\n a valid username', validateUsername, '*user*', false); -test('test_user is a valid username', validateUsername, 'test_user', true); - +const test = require('ava'); +const DBUtils = require('./../../../socketserver/utils/index').db; + +function validateEmail(t, input, expected) { + t.is(DBUtils.validateEmail(input), expected); +} + +function validateUsername(t, input, expected) { + t.is(DBUtils.validateUsername(input), expected); +} + +test('user@example.com is a valid email', validateEmail, 'user@example.com', true); +test('@example.com isn\'t a valid email', validateEmail, '@example.com', false); +test('user@example. isn\'t a valid email', validateEmail, 'user@example.', false); + +test('123 is a valid username', validateUsername, '123', true); +test('123456789101112131415 is\n a valid username', validateUsername, '123456789101112131415', false); +test('*user* is\n a valid username', validateUsername, '*user*', false); +test('test_user is a valid username', validateUsername, 'test_user', true); + diff --git a/webserver/app.js b/webserver/app.js index c0c4ea4..fdf669e 100644 --- a/webserver/app.js +++ b/webserver/app.js @@ -7,6 +7,7 @@ const https = require('https'); const fs = require('fs'); const nconf = require('nconf'); const ejs = require('ejs'); +const helmet = require('helmet'); const app = express(); let server2 = null; @@ -42,6 +43,9 @@ app.set('view engine', 'html'); app.set('views', `${__dirname}/public`); app.engine('html', ejs.renderFile); app.use(compression()); +app.use(helmet.frameguard()); +app.use(helmet.xssFilter()); +app.use(helmet.hidePoweredBy()); app.get(['/', '/index.html'], (req, res) => { res.render('index', {