From 494f66d388aa28c22fa98fef7922a0feaf58e1aa Mon Sep 17 00:00:00 2001 From: Jonathan Cremin Date: Mon, 6 Jun 2016 15:37:00 +0100 Subject: [PATCH] Get linting passing again --- .eslintignore | 4 ++ api/app.js | 2 +- api/lib/auth.js | 48 ++++++++++++-------- api/routes/file.js | 4 ++ api/routes/user.js | 30 +++++++------ app.js | 12 ++--- lib/format.js | 16 +++---- lib/malware.js | 65 ++++++++++++++++++++++++--- lib/mongo.js | 7 +-- lib/redis.js | 24 +++++----- lib/resize.js | 2 +- lib/type.js | 50 ++++++++++----------- lib/uploader.js | 1 - lib/virustotal.js | 2 +- package.json | 1 + web/app.js | 8 ++-- web/lib/auth.js | 104 ++++++++++++++++++++++++++------------------ web/routes/file.js | 55 +++++++++++++++-------- web/routes/index.js | 41 ++++++++--------- web/routes/pro.js | 30 +++++++------ web/routes/user.js | 73 +++++++++++++++++++++++-------- 21 files changed, 367 insertions(+), 212 deletions(-) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..8d89c50 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +node_modules +web/public +lib/ssh2-sftp-client.js +test diff --git a/api/app.js b/api/app.js index c0532d5..c2f88ac 100644 --- a/api/app.js +++ b/api/app.js @@ -10,7 +10,7 @@ const debug = debugname('hostr-api'); const router = new Router(); -const statsdOpts = {prefix: 'hostr-api', host: process.env.STATSD_HOST}; +const statsdOpts = { prefix: 'hostr-api', host: process.env.STATSD_HOST }; router.use(stats(statsdOpts)); const statsd = new StatsD(statsdOpts); router.use(function* statsMiddleware(next) { diff --git a/api/lib/auth.js b/api/lib/auth.js index 82a1f37..67780a1 100644 --- a/api/lib/auth.js +++ b/api/lib/auth.js @@ -16,16 +16,25 @@ export default function* (next) { const userToken = yield this.redis.get(this.req.headers.authorization.substr(1)); this.assert(userToken, 401, '{"error": {"message": "Invalid token.", "code": 606}}'); debug('Token found'); - user = yield Users.findOne({'_id': this.db.objectId(userToken)}); + user = yield Users.findOne({ _id: this.db.objectId(userToken) }); } else { const authUser = auth(this); this.assert(authUser, 401, badLoginMsg); const remoteIp = this.req.headers['x-real-ip'] || this.req.connection.remoteAddress; - const count = yield Logins.count({ip: remoteIp, successful: false, at: { '$gt': Math.ceil(Date.now() / 1000) - 600}}); - this.assert(count < 25, 401, '{"error": {"message": "Too many incorrect logins.", "code": 608}}'); + const count = yield Logins.count({ + ip: remoteIp, + successful: false, + at: { $gt: Math.ceil(Date.now() / 1000) - 600 }, + }); + this.assert(count < 25, 401, + '{"error": {"message": "Too many incorrect logins.", "code": 608}}'); - yield Logins.insertOne({ip: remoteIp, at: Math.ceil(Date.now() / 1000), successful: null}); - user = yield Users.findOne({'email': authUser.name, 'banned': {'$exists': false}, 'status': {'$ne': 'deleted'}}); + yield Logins.insertOne({ ip: remoteIp, at: Math.ceil(Date.now() / 1000), successful: null }); + user = yield Users.findOne({ + email: authUser.name, + banned: { $exists: false }, + status: { $ne: 'deleted' }, + }); this.assert(user, 401, badLoginMsg); const authenticated = yield passwords.match(authUser.pass, user.salted_password); this.assert(authenticated, 401, badLoginMsg); @@ -33,22 +42,27 @@ export default function* (next) { debug('Checking user'); this.assert(user, 401, badLoginMsg); debug('Checking user is activated'); - this.assert(!user.activationCode, 401, '{"error": {"message": "Account has not been activated.", "code": 603}}'); + this.assert(!user.activationCode, 401, + '{"error": {"message": "Account has not been activated.", "code": 603}}'); - const uploadedTotal = yield Files.count({owner: user._id, status: {'$ne': 'deleted'}}); - const uploadedToday = yield Files.count({owner: user._id, 'time_added': {'$gt': Math.ceil(Date.now() / 1000) - 86400}}); + const uploadedTotal = yield Files.count({ owner: user._id, status: { $ne: 'deleted' } }); + const uploadedToday = yield Files.count({ + owner: user._id, + time_added: { $gt: Math.ceil(Date.now() / 1000) - 86400 }, + }); const normalisedUser = { - 'id': user._id, - 'email': user.email, - 'daily_upload_allowance': user.type === 'Pro' ? 'unlimited' : 15, - 'file_count': uploadedTotal, - 'max_filesize': user.type === 'Pro' ? 524288000 : 20971520, - 'plan': user.type || 'Free', - 'uploads_today': uploadedToday, + id: user._id, + email: user.email, + daily_upload_allowance: user.type === 'Pro' ? 'unlimited' : 15, + file_count: uploadedTotal, + max_filesize: user.type === 'Pro' ? 524288000 : 20971520, + plan: user.type || 'Free', + uploads_today: uploadedToday, }; - this.response.set('Daily-Uploads-Remaining', user.type === 'Pro' ? 'unlimited' : 15 - uploadedToday); + this.response.set('Daily-Uploads-Remaining', + user.type === 'Pro' ? 'unlimited' : 15 - uploadedToday); this.user = normalisedUser; - debug('Authenticated user: ' + this.user.email); + debug('Authenticated user: ', this.user.email); yield next; } diff --git a/api/routes/file.js b/api/routes/file.js index 3fcda0e..25e3f0d 100644 --- a/api/routes/file.js +++ b/api/routes/file.js @@ -13,9 +13,13 @@ export function* post(next) { const uploader = new Uploader(this); + yield uploader.checkLimit(); yield uploader.accept(); + uploader.acceptedEvent(); + uploader.receive(); + yield uploader.save(); yield uploader.promise; diff --git a/api/routes/user.js b/api/routes/user.js index e71dd6d..c806551 100644 --- a/api/routes/user.js +++ b/api/routes/user.js @@ -15,12 +15,12 @@ export function* get() { export function* token() { const token = uuid.v4(); // eslint-disable-line no-shadow yield this.redis.set(token, this.user.id, 'EX', 86400); - this.body = {token: token}; + this.body = { token }; } export function* transaction() { const Transactions = this.db.Transactions; - const transactions = yield Transactions.find({'user_id': this.user.id}).toArray(); + const transactions = yield Transactions.find({ user_id: this.user.id }).toArray(); this.body = transactions.map((transaction) => { // eslint-disable-line no-shadow const type = transaction.paypal ? 'paypal' : 'direct'; @@ -29,17 +29,20 @@ export function* transaction() { amount: transaction.paypal ? transaction.amount : transaction.amount / 100, date: transaction.date, description: transaction.desc, - type: type, + type, }; }); } export function* settings() { - this.assert(this.request.body, 400, '{"error": {"message": "Current Password required to update account.", "code": 612}}'); - this.assert(this.request.body.current_password, 400, '{"error": {"message": "Current Password required to update account.", "code": 612}}'); + this.assert(this.request.body, 400, + '{"error": {"message": "Current Password required to update account.", "code": 612}}'); + this.assert(this.request.body.current_password, 400, + '{"error": {"message": "Current Password required to update account.", "code": 612}}'); const Users = this.db.Users; - const user = yield Users.findOne({'_id': this.user.id}); - this.assert(yield passwords.match(this.request.body.current_password, user.salted_password), 400, '{"error": {"message": "Incorrect password", "code": 606}}'); + const user = yield Users.findOne({ _id: this.user.id }); + this.assert(yield passwords.match(this.request.body.current_password, user.salted_password), 400, + '{"error": {"message": "Incorrect password", "code": 606}}'); const data = {}; if (this.request.body.email && this.request.body.email !== user.email) { data.email = this.request.body.email; @@ -48,10 +51,11 @@ export function* settings() { } } if (this.request.body.new_password) { - this.assert(this.request.body.new_password.length >= 7, 400, '{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}'); - data.salted_password = yield passwords.hash(this.request.body.new_password); // eslint-disable-line camelcase + this.assert(this.request.body.new_password.length >= 7, 400, + '{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}'); + data.salted_password = yield passwords.hash(this.request.body.new_password); } - Users.updateOne({_id: user._id}, {'$set': data}); + Users.updateOne({ _id: user._id }, { $set: data }); this.body = {}; } @@ -65,7 +69,7 @@ export function* events() { let json; try { json = JSON.parse(message); - } catch(err) { + } catch (err) { debug('Invalid JSON for socket auth'); this.websocket.send('Invalid authentication message. Bad JSON?'); this.raven.captureError(err); @@ -73,13 +77,13 @@ export function* events() { try { const reply = yield this.redis.get(json.authorization); if (reply) { - pubsub.subscribe('/user/' + reply); + pubsub.subscribe(`/user/${reply}`); this.websocket.send('{"status":"active"}'); debug('Subscribed to: /user/%s', reply); } else { this.websocket.send('Invalid authentication token.'); } - } catch(err) { + } catch (err) { debug(err); this.raven.captureError(err); } diff --git a/app.js b/app.js index 29739e7..4bee4cd 100644 --- a/app.js +++ b/app.js @@ -10,8 +10,7 @@ import helmet from 'koa-helmet'; import raven from 'raven'; import mongo from './lib/mongo'; import * as redis from './lib/redis'; -import api from './api/app'; -import { ws } from './api/app'; +import api, { ws } from './api/app'; import web from './web/app'; import debugname from 'debug'; @@ -38,7 +37,8 @@ app.use(helmet()); app.use(function* errorMiddleware(next) { this.set('Server', 'Nintendo 64'); if (this.req.headers['x-forwarded-proto'] === 'http') { - return this.redirect('https://' + this.req.headers.host + this.req.url); + this.redirect(`https://${this.req.headers.host}${this.req.url}`); + return; } try { yield next; @@ -57,7 +57,7 @@ app.use(compress()); app.use(bodyparser()); app.use(favicon(path.join(__dirname, 'web/public/images/favicon.png'))); -app.use(serve(path.join(__dirname, 'web/public/'), {maxage: 31536000000})); +app.use(serve(path.join(__dirname, 'web/public/'), { maxage: 31536000000 })); app.use(api.prefix('/api').routes()); app.use(web.prefix('').routes()); @@ -66,8 +66,8 @@ app.ws.use(redis.middleware()); app.ws.use(ws.prefix('/api').routes()); if (!module.parent) { - app.listen(process.env.PORT || 4040, function listen() { - debug('Koa HTTP server listening on port ' + (process.env.PORT || 4040)); + app.listen(process.env.PORT || 4040, () => { + debug('Koa HTTP server listening on port ', (process.env.PORT || 4040)); }); } diff --git a/lib/format.js b/lib/format.js index 4f87224..206921b 100644 --- a/lib/format.js +++ b/lib/format.js @@ -9,15 +9,15 @@ export function formatDate(timestamp) { export function formatSize(size) { if (size >= 1073741824) { - return Math.round((size / 1073741824) * 10) / 10 + 'GB'; + return `${Math.round((size / 1073741824) * 10) / 10}GB`; } if (size >= 1048576) { - return Math.round((size / 1048576) * 10) / 10 + 'MB'; + return `${Math.round((size / 1048576) * 10) / 10}MB`; } if (size >= 1024) { - return Math.round((size / 1024) * 10) / 10 + 'KB'; + return `${Math.round((size / 1024) * 10) / 10}KB`; } - return Math.round(size) + 'B'; + return `${Math.round(size)}B`; } export function formatFile(file) { @@ -25,8 +25,8 @@ export function formatFile(file) { added: moment.unix(file.time_added).format(), readableAdded: formatDate(file.time_added), downloads: file.downloads !== undefined ? file.downloads : 0, - href: baseURL + '/' + file._id, // eslint-disable-line no-underscore-dangle - id: file._id, // eslint-disable-line no-underscore-dangle + href: `${baseURL}/${file._id}`, + id: file._id, name: file.file_name, size: file.file_size, readableSize: formatSize(file.file_size), @@ -40,8 +40,8 @@ export function formatFile(file) { formattedFile.width = file.width; const ext = (file.file_name.split('.').pop().toLowerCase() === 'psd' ? '.png' : ''); formattedFile.direct = { - '150x': baseURL + '/file/150/' + file._id + '/' + file.file_name + ext, // eslint-disable-line no-underscore-dangle - '970x': baseURL + '/file/970/' + file._id + '/' + file.file_name + ext, // eslint-disable-line no-underscore-dangle + '150x': `${baseURL}/file/150/${file._id}/${file.file_name}${ext}`, + '970x': `${baseURL}/file/970/${file._id}/${file.file_name}${ext}`, }; } return formattedFile; diff --git a/lib/malware.js b/lib/malware.js index e628474..4144728 100644 --- a/lib/malware.js +++ b/lib/malware.js @@ -1,10 +1,63 @@ import virustotal from './virustotal'; -const extensions = ['EXE', 'PIF', 'APPLICATION', 'GADGET', 'MSI', 'MSP', 'COM', 'SCR', 'HTA', 'CPL', 'MSC', - 'JAR', 'BAT', 'CMD', 'VB', 'VBS', 'VBE', 'JS', 'JSE', 'WS', 'WSF', 'WSC', 'WSH', 'PS1', 'PS1XML', 'PS2', - 'PS2XML', 'PSC1', 'PSC2', 'MSH', 'MSH1', 'MSH2', 'MSHXML', 'MSH1XML', 'MSH2XML', 'SCF', 'LNK', 'INF', 'REG', - 'PDF', 'DOC', 'XLS', 'PPT', 'DOCM', 'DOTM', 'XLSM', 'XLTM', 'XLAM', 'PPTM', 'POTM', 'PPAM', 'PPSM', 'SLDM', - 'RAR', 'TAR', 'ZIP', 'GZ', +const extensions = [ + 'EXE', + 'PIF', + 'APPLICATION', + 'GADGET', + 'MSI', + 'MSP', + 'COM', + 'SCR', + 'HTA', + 'CPL', + 'MSC', + 'JAR', + 'BAT', + 'CMD', + 'VB', + 'VBS', + 'VBE', + 'JS', + 'JSE', + 'WS', + 'WSF', + 'WSC', + 'WSH', + 'PS1', + 'PS1XML', + 'PS2', + 'PS2XML', + 'PSC1', + 'PSC2', + 'MSH', + 'MSH1', + 'MSH2', + 'MSHXML', + 'MSH1XML', + 'MSH2XML', + 'SCF', + 'LNK', + 'INF', + 'REG', + 'PDF', + 'DOC', + 'XLS', + 'PPT', + 'DOCM', + 'DOTM', + 'XLSM', + 'XLTM', + 'XLAM', + 'PPTM', + 'POTM', + 'PPAM', + 'PPSM', + 'SLDM', + 'RAR', + 'TAR', + 'ZIP', + 'GZ', ]; function getExtension(filename) { @@ -19,6 +72,6 @@ export default function* (file) { const result = yield virustotal.getFileReport(file.md5); return { positive: result.positives >= 5, - result: result, + result, }; } diff --git a/lib/mongo.js b/lib/mongo.js index 949e428..eac7144 100644 --- a/lib/mongo.js +++ b/lib/mongo.js @@ -3,6 +3,7 @@ const MongoClient = mongodb().MongoClient; import debugname from 'debug'; const debug = debugname('hostr:mongo'); +/* eslint no-param-reassign: ["error", { "props": false }] */ const configuredClient = new Promise((resolve, reject) => { debug('Connecting to Mongodb'); return MongoClient.connect(process.env.MONGO_URL).then((client) => { @@ -13,8 +14,8 @@ const configuredClient = new Promise((resolve, reject) => { client.Logins = client.collection('logins'); client.Remember = client.collection('remember'); client.Reset = client.collection('reset'); - client.Remember.ensureIndex({'created': 1}, {expireAfterSeconds: 2592000}); - client.Files.ensureIndex({'owner': 1, 'status': 1, 'time_added': -1}); + client.Remember.ensureIndex({ created: 1 }, { expireAfterSeconds: 2592000 }); + client.Files.ensureIndex({ owner: 1, status: 1, time_added: -1 }); client.ObjectId = client.objectId = mongodb().ObjectId; return resolve(client); }).catch((e) => { @@ -24,7 +25,7 @@ const configuredClient = new Promise((resolve, reject) => { debug(e); }); -export default function() { +export default function mongo() { return function* dbMiddleware(next) { try { this.db = yield configuredClient; diff --git a/lib/redis.js b/lib/redis.js index 50c9b8c..8316865 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -11,25 +11,25 @@ const connection = new Promise((resolve, reject) => { client.on('error', reject); resolve(client); }).catch((err) => { - debug('Connection error: ' + err); + debug('Connection error: ', err); throw err; }); -const redisSession = new Promise((resolve, reject) => { - return connection.then((client) => { - const sessionClient = koaRedis({client: client}); +const redisSession = new Promise((resolve, reject) => + connection.then((client) => { + const sessionClient = koaRedis({ client }); resolve(session({ key: 'hid', store: sessionClient, })); }).catch((err) => { - debug('koa-redis error: ' + err); + debug('koa-redis error: ', err); reject(err); - }); -}); + }) +); -const wrapped = new Promise((resolve, reject) => { - return connection.then((client) => { +const wrapped = new Promise((resolve, reject) => + connection.then((client) => { const asyncClient = coRedis(client); asyncClient.on('error', reject); asyncClient.on('ready', () => { @@ -37,11 +37,11 @@ const wrapped = new Promise((resolve, reject) => { resolve(asyncClient); }); }).catch((err) => { - debug('co-redis error: ' + err); + debug('co-redis error: ', err); reject(err); throw err; - }); -}); + }) +); export function sessionStore() { return function* sessionStoreMiddleware(next) { diff --git a/lib/resize.js b/lib/resize.js index 75c427e..dcdbd07 100644 --- a/lib/resize.js +++ b/lib/resize.js @@ -63,7 +63,7 @@ export default function resize(path, type, currentSize, newSize) { return cover(path, type, newSize); } else if (newSize.width > 970 && ratio > 1) { debug('Scale'); - newSize.height = currentSize.height * ratio; + newSize.height = currentSize.height * ratio; // eslint-disable-line no-param-reassign return scale(path, type, newSize); } debug('Copy'); diff --git a/lib/type.js b/lib/type.js index 099ea80..668c1a4 100644 --- a/lib/type.js +++ b/lib/type.js @@ -1,29 +1,29 @@ const extensions = { - 'jpg': 'image', - 'jpeg': 'image', - 'png': 'image', - 'gif': 'image', - 'bmp': 'image', - 'tiff': 'image', - 'psd': 'image', - 'mp3': 'audio', - 'm4a': 'audio', - 'ogg': 'audio', - 'flac': 'audio', - 'aac': 'audio', - 'mpg': 'video', - 'mkv': 'video', - 'avi': 'video', - 'divx': 'video', - 'mpeg': 'video', - 'flv': 'video', - 'mp4': 'video', - 'mov': 'video', - 'zip': 'archive', - 'gz': 'archive', - 'tgz': 'archive', - 'bz2': 'archive', - 'rar': 'archive', + jpg: 'image', + jpeg: 'image', + png: 'image', + gif: 'image', + bmp: 'image', + tiff: 'image', + psd: 'image', + mp3: 'audio', + m4a: 'audio', + ogg: 'audio', + flac: 'audio', + aac: 'audio', + mpg: 'video', + mkv: 'video', + avi: 'video', + divx: 'video', + mpeg: 'video', + flv: 'video', + mp4: 'video', + mov: 'video', + zip: 'archive', + gz: 'archive', + tgz: 'archive', + bz2: 'archive', + rar: 'archive', }; export function sniff(filename) { diff --git a/lib/uploader.js b/lib/uploader.js index a3a3346..8bb4402 100644 --- a/lib/uploader.js +++ b/lib/uploader.js @@ -35,7 +35,6 @@ export default class Uploader { } *accept() { - yield this.checkLimit(); this.upload = yield parse(this.context, { autoFields: true, headers: this.context.request.headers, diff --git a/lib/virustotal.js b/lib/virustotal.js index b7bb36e..84f686c 100644 --- a/lib/virustotal.js +++ b/lib/virustotal.js @@ -7,5 +7,5 @@ export function* getFileReport(resource, apiKey = process.env.VIRUSTOTAL_KEY) { const form = new FormData(); form.append('apikey', apiKey); form.append('resource', resource); - return yield fetch(`${apiRoot}/file/report`, { method: 'POST'}); + return yield fetch(`${apiRoot}/file/report`, { method: 'POST' }); } diff --git a/package.json b/package.json index f95b9c7..98a1b81 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "cover": "istanbul cover _mocha -- -r babel-register test/**/*.spec.js", "init": "babel-node -e \"require('./lib/storage').default();\"", "jspm": "jspm install", + "lint": "eslint .", "start": "npm run build && node -r babel-register app.js", "test": "npm run test-seed && mocha -r babel-register test/**/*.spec.js", "test-seed": "node test/fixtures/mongo-user.js && node test/fixtures/mongo-file.js", diff --git a/web/app.js b/web/app.js index ba6189e..761e3e7 100644 --- a/web/app.js +++ b/web/app.js @@ -15,10 +15,10 @@ const router = new Router(); router.use(errors({ engine: 'ejs', - template: path.join(__dirname, 'public', 'error.html') + template: path.join(__dirname, 'public', 'error.html'), })); -const statsdOpts = {prefix: 'hostr-web', host: process.env.STATSD_HOST}; +const statsdOpts = { prefix: 'hostr-web', host: process.env.STATSD_HOST }; router.use(stats(statsdOpts)); const statsd = new StatsD(statsdOpts); router.use(function* statsMiddleware(next) { @@ -41,7 +41,7 @@ router.use(function* stateMiddleware(next) { router.use(csrf()); router.use(views(path.join(__dirname, 'views'), { - extension: 'ejs' + extension: 'ejs', })); router.get('/', index.main); @@ -75,7 +75,7 @@ router.get('/file/:id/:name', file.get); router.get('/file/:size/:id/:name', file.get); router.get('/files/:id/:name', file.get); router.get('/download/:id/:name', function* downloadRedirect(id) { - this.redirect('/' + id); + this.redirect(`/${id}`); }); router.get('/updaters/mac', function* macUpdater() { diff --git a/web/lib/auth.js b/web/lib/auth.js index c9e6bfe..3eb6c51 100644 --- a/web/lib/auth.js +++ b/web/lib/auth.js @@ -2,12 +2,16 @@ import crypto from 'crypto'; import passwords from 'passwords'; import uuid from 'node-uuid'; import views from 'co-views'; -const render = views(__dirname + '/../views', { default: 'ejs'}); +import { join } from 'path'; +const render = views(join(__dirname, '..', 'views'), { default: 'ejs' }); import debugname from 'debug'; const debug = debugname('hostr-web:auth'); import sendgridInit from 'sendgrid'; const sendgrid = sendgridInit(process.env.SENDGRID_KEY); +const from = process.env.EMAIL_FROM; +const fromname = process.env.EMAIL_NAME; + export function* authenticate(email, password) { const Users = this.db.Users; const Logins = this.db.Logins; @@ -17,30 +21,38 @@ export function* authenticate(email, password) { debug('No password, or password too short'); return new Error('Invalid login details'); } - const count = yield Logins.count({ip: remoteIp, successful: false, at: { '$gt': Math.ceil(Date.now() / 1000) - 600}}); + const count = yield Logins.count({ + ip: remoteIp, + successful: false, + at: { $gt: Math.ceil(Date.now() / 1000) - 600 }, + }); if (count > 25) { debug('Throttling brute force'); return new Error('Invalid login details'); } - const login = {ip: remoteIp, at: Math.ceil(Date.now() / 1000), successful: null}; + const login = { ip: remoteIp, at: Math.ceil(Date.now() / 1000), successful: null }; yield Logins.save(login); - const user = yield Users.findOne({email: email.toLowerCase(), banned: {'$exists': false}, status: {'$ne': 'deleted'}}); + const user = yield Users.findOne({ + email: email.toLowerCase(), + banned: { $exists: false }, status: { $ne: 'deleted' }, + }); if (user) { const verified = yield passwords.verify(password, user.salted_password); if (verified) { debug('Password verified'); login.successful = true; - yield Logins.updateOne({_id: login._id}, login); + yield Logins.updateOne({ _id: login._id }, login); return user; } debug('Password invalid'); login.successful = false; - yield Logins.updateOne({_id: login._id}, login); + yield Logins.updateOne({ _id: login._id }, login); } else { debug('Email invalid'); login.successful = false; - yield Logins.updateOne({_id: login._id}, login); + yield Logins.updateOne({ _id: login._id }, login); } + return new Error('Invalid login details'); } @@ -50,15 +62,17 @@ export function* setupSession(user) { yield this.redis.set(token, user._id, 'EX', 604800); const sessionUser = { - 'id': user._id, - 'email': user.email, - 'dailyUploadAllowance': 15, - 'maxFileSize': 20971520, - 'joined': user.joined, - 'plan': user.type || 'Free', - 'uploadsToday': yield this.db.Files.count({owner: user._id, 'time_added': {'$gt': Math.ceil(Date.now() / 1000) - 86400}}), - 'token': token, - 'md5': crypto.createHash('md5').update(user.email).digest('hex'), + id: user._id, + email: user.email, + dailyUploadAllowance: 15, + maxFileSize: 20971520, + joined: user.joined, + plan: user.type || 'Free', + uploadsToday: yield this.db.Files.count({ + owner: user._id, time_added: { $gt: Math.ceil(Date.now() / 1000) - 86400 }, + }), + md5: crypto.createHash('md5').update(user.email).digest('hex'), + token, }; if (sessionUser.plan === 'Pro') { @@ -70,8 +84,8 @@ export function* setupSession(user) { if (this.request.body.remember && this.request.body.remember === 'on') { const Remember = this.db.Remember; const rememberToken = uuid(); - Remember.save({_id: rememberToken, 'user_id': user.id, created: new Date().getTime()}); - this.cookies.set('r', rememberToken, { maxAge: 1209600000, httpOnly: true}); + Remember.save({ _id: rememberToken, user_id: user.id, created: new Date().getTime() }); + this.cookies.set('r', rememberToken, { maxAge: 1209600000, httpOnly: true }); } debug('Session set up'); } @@ -79,36 +93,38 @@ export function* setupSession(user) { export function* signup(email, password, ip) { const Users = this.db.Users; - const existingUser = yield Users.findOne({email: email, status: {'$ne': 'deleted'}}); + const existingUser = yield Users.findOne({ email, status: { $ne: 'deleted' } }); if (existingUser) { debug('Email already in use.'); throw new Error('Email already in use.'); } const cryptedPassword = yield passwords.crypt(password); const user = { - email: email, - 'salted_password': cryptedPassword, + email, + salted_password: cryptedPassword, joined: Math.round(new Date().getTime() / 1000), - 'signup_ip': ip, + signup_ip: ip, activationCode: uuid(), }; Users.insertOne(user); - const html = yield render('email/inlined/activate', {activationUrl: process.env.WEB_BASE_URL + '/activate/' + user.activationCode}); + const html = yield render('email/inlined/activate', { + activationUrl: `${process.env.WEB_BASE_URL}/activate/${user.activationCode}`, + }); const text = `Thanks for signing up to Hostr! Please confirm your email address by clicking the link below. -${process.env.WEB_BASE_URL + '/activate/' + user.activationCode} +${process.env.WEB_BASE_URL}/activate/${user.activationCode} — Jonathan Cremin, Hostr Founder `; const mail = new sendgrid.Email({ to: user.email, - from: 'jonathan@hostr.co', - fromname: 'Jonathan from Hostr', - html: html, - text: text, subject: 'Welcome to Hostr', + from, + fromname, + html, + text, }); mail.addCategory('activate'); sendgrid.send(mail); @@ -118,25 +134,27 @@ ${process.env.WEB_BASE_URL + '/activate/' + user.activationCode} export function* sendResetToken(email) { const Users = this.db.Users; const Reset = this.db.Reset; - const user = yield Users.findOne({email: email}); + const user = yield Users.findOne({ email }); if (user) { const token = uuid.v4(); Reset.save({ - '_id': user._id, - 'token': token, - 'created': Math.round(new Date().getTime() / 1000), + _id: user._id, + created: Math.round(new Date().getTime() / 1000), + token, + }); + const html = yield render('email/inlined/forgot', { + forgotUrl: `${process.env.WEB_BASE_URL}/forgot/${token}`, }); - const html = yield render('email/inlined/forgot', {forgotUrl: process.env.WEB_BASE_URL + '/forgot/' + token}); const text = `It seems you've forgotten your password :( -Visit ${process.env.WEB_BASE_URL + '/forgot/' + token} to set a new one. +Visit ${process.env.WEB_BASE_URL}/forgot/${token} to set a new one. `; const mail = new sendgrid.Email({ to: user.email, from: 'jonathan@hostr.co', fromname: 'Jonathan from Hostr', - html: html, - text: text, subject: 'Hostr Password Reset', + html, + text, }); mail.addCategory('password-reset'); sendgrid.send(mail); @@ -149,36 +167,36 @@ Visit ${process.env.WEB_BASE_URL + '/forgot/' + token} to set a new one. export function* fromToken(token) { const Users = this.db.Users; const reply = yield this.redis.get(token); - return yield Users.findOne({_id: reply}); + return yield Users.findOne({ _id: reply }); } export function* fromCookie(cookie) { const Remember = this.db.Remember; const Users = this.db.Users; - const remember = yield Remember.findOne({_id: cookie}); - return yield Users.findOne({_id: remember.user_id}); + const remember = yield Remember.findOne({ _id: cookie }); + return yield Users.findOne({ _id: remember.user_id }); } export function* validateResetToken() { const Reset = this.db.Reset; - return yield Reset.findOne({token: this.params.token}); + return yield Reset.findOne({ token: this.params.token }); } export function* updatePassword(userId, password) { const Users = this.db.Users; const cryptedPassword = yield passwords.crypt(password); - yield Users.updateOne({_id: userId}, {'$set': {'salted_password': cryptedPassword}}); + yield Users.updateOne({ _id: userId }, { $set: { salted_password: cryptedPassword } }); } export function* activateUser(code) { const Users = this.db.Users; - const user = yield Users.findOne({activationCode: code}); + const user = yield Users.findOne({ activationCode: code }); if (user) { - Users.updateOne({_id: user._id}, {'$unset': {activationCode: ''}}); + Users.updateOne({ _id: user._id }, { $unset: { activationCode: '' } }); yield setupSession.call(this, user); return true; } diff --git a/web/routes/file.js b/web/routes/file.js index 105b5d8..47aa915 100644 --- a/web/routes/file.js +++ b/web/routes/file.js @@ -1,10 +1,16 @@ -import path from 'path'; +import { join } from 'path'; import mime from 'mime-types'; import hostrFileStream from '../../lib/hostr-file-stream'; import { formatFile } from '../../lib/format'; const storePath = process.env.UPLOAD_STORAGE_PATH; +const referrerRegexes = [ + /^https:\/\/hostr.co/, + /^https:\/\/localhost.hostr.co/, + /^http:\/\/localhost:4040/, +]; + function userAgentCheck(userAgent) { if (!userAgent) { return false; @@ -12,34 +18,45 @@ function userAgentCheck(userAgent) { return userAgent.match(/^(wget|curl|vagrant)/i); } +function referrerCheck(referrer) { + return referrer && referrerRegexes.some((regex) => referrer.match(regex)); +} + function hotlinkCheck(file, userAgent, referrer) { - return !userAgentCheck(userAgent) && !file.width && (!referrer || !(referrer.match(/^https:\/\/hostr.co/) || referrer.match(/^http:\/\/localhost:4040/))); + return userAgentCheck(userAgent) || file.width || referrerCheck(referrer); } export function* get() { - const file = yield this.db.Files.findOne({_id: this.params.id, 'file_name': this.params.name, 'status': 'active'}); + const file = yield this.db.Files.findOne({ + _id: this.params.id, + file_name: this.params.name, + status: 'active', + }); this.assert(file, 404); - if (hotlinkCheck(file, this.headers['user-agent'], this.headers.referer)) { - return this.redirect('/' + file._id); + if (!hotlinkCheck(file, this.headers['user-agent'], this.headers.referer)) { + this.redirect(`/${file._id}`); + return; } if (!file.width && this.request.query.warning !== 'on') { - return this.redirect('/' + file._id); + this.redirect(`/${file._id}`); + return; } if (file.malware) { const alert = this.request.query.alert; if (!alert || !alert.match(/i want to download malware/i)) { - return this.redirect('/' + file._id); + this.redirect(`/${file._id}`); + return; } } - let localPath = path.join(storePath, file._id[0], file._id + '_' + file.file_name); - let remotePath = path.join(file._id[0], file._id + '_' + file.file_name); + let localPath = join(storePath, file._id[0], `${file._id}_${file.file_name}`); + let remotePath = join(file._id[0], `${file._id}_${file.file_name}`); if (this.params.size > 0) { - localPath = path.join(storePath, file._id[0], this.params.size, file._id + '_' + file.file_name); - remotePath = path.join(file._id[0], this.params.size, file._id + '_' + file.file_name); + localPath = join(storePath, file._id[0], this.params.size, `${file._id}_${file.file_name}`); + remotePath = join(file._id[0], this.params.size, `${file._id}_${file.file_name}`); } if (file.malware) { @@ -57,7 +74,7 @@ export function* get() { } if (userAgentCheck(this.headers['user-agent'])) { - this.set('Content-Disposition', 'attachment; filename=' + file.file_name); + this.set('Content-Disposition', `attachment; filename=${file.file_name}`); } this.set('Content-type', type); @@ -66,10 +83,9 @@ export function* get() { if (!this.params.size || (this.params.size && this.params.size > 150)) { this.db.Files.updateOne( - {'_id': file._id}, - {'$set': {'last_accessed': Math.ceil(Date.now() / 1000)}, '$inc': {downloads: 1}}, - {'w': 0} - ); + { _id: file._id }, + { $set: { last_accessed: Math.ceil(Date.now() / 1000) }, $inc: { downloads: 1 } }, + { w: 0 }); } this.body = yield hostrFileStream(localPath, remotePath); @@ -80,14 +96,15 @@ export function* resized() { } export function* landing() { - const file = yield this.db.Files.findOne({_id: this.params.id, status: 'active'}); + const file = yield this.db.Files.findOne({ _id: this.params.id, status: 'active' }); this.assert(file, 404); if (userAgentCheck(this.headers['user-agent'])) { this.params.name = file.file_name; - return yield get.call(this); + yield get.call(this); + return; } this.statsd.incr('file.landing', 1); const formattedFile = formatFile(file); - yield this.render('file', {file: formattedFile}); + yield this.render('file', { file: formattedFile }); } diff --git a/web/routes/index.js b/web/routes/index.js index 3079cc4..27b95c9 100644 --- a/web/routes/index.js +++ b/web/routes/index.js @@ -4,12 +4,13 @@ import auth from '../lib/auth'; export function* main() { if (this.session.user) { if (this.query['app-token']) { - return this.redirect('/'); + this.redirect('/'); + return; } const token = uuid.v4(); yield this.redis.set(token, this.session.user.id, 'EX', 604800); this.session.user.token = token; - yield this.render('index', {user: this.session.user}); + yield this.render('index', { user: this.session.user }); } else { if (this.query['app-token']) { const user = yield auth.fromToken(this, this.query['app-token']); @@ -30,26 +31,26 @@ export function* staticPage(next) { const token = uuid.v4(); yield this.redis.set(token, this.session.user.id, 'EX', 604800); this.session.user.token = token; - yield this.render('index', {user: this.session.user}); + yield this.render('index', { user: this.session.user }); } else { switch (this.originalUrl) { - case '/terms': - yield this.render('terms'); - break; - case '/privacy': - yield this.render('privacy'); - break; - case '/pricing': - yield this.render('pricing'); - break; - case '/apps': - yield this.render('apps'); - break; - case '/stats': - yield this.render('index', {user: {}}); - break; - default: - yield next; + case '/terms': + yield this.render('terms'); + break; + case '/privacy': + yield this.render('privacy'); + break; + case '/pricing': + yield this.render('pricing'); + break; + case '/apps': + yield this.render('apps'); + break; + case '/stats': + yield this.render('index', { user: {} }); + break; + default: + yield next; } } } diff --git a/web/routes/pro.js b/web/routes/pro.js index bd820e2..a59c7bd 100644 --- a/web/routes/pro.js +++ b/web/routes/pro.js @@ -1,13 +1,13 @@ import path from 'path'; import views from 'co-views'; -const render = views(path.join(__dirname, '/../views'), { default: 'ejs'}); +const render = views(path.join(__dirname, '/../views'), { default: 'ejs' }); import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); import sendgridInit from 'sendgrid'; const sendgrid = sendgridInit(process.env.SENDGRID_KEY); -const fromEmail = process.env.EMAIL_FROM; -const fromName = process.env.EMAIL_NAME; +const from = process.env.EMAIL_FROM; +const fromname = process.env.EMAIL_NAME; export function* create() { const Users = this.db.Users; @@ -26,10 +26,11 @@ export function* create() { delete customer.subscriptions; - yield Users.updateOne({_id: this.session.user.id}, {'$set': {'stripe_customer': customer, type: 'Pro'}}); + yield Users.updateOne({ _id: this.session.user.id }, + { $set: { stripe_customer: customer, type: 'Pro' } }); const transaction = { - 'user_id': this.session.user.id, + user_id: this.session.user.id, amount: customer.subscription.plan.amount, desc: customer.subscription.plan.name, date: new Date(customer.subscription.plan.created * 1000), @@ -38,7 +39,7 @@ export function* create() { yield Transactions.insertOne(transaction); this.session.user.plan = 'Pro'; - this.body = {status: 'active'}; + this.body = { status: 'active' }; const html = yield render('email/inlined/pro'); const text = `Hey, thanks for upgrading to Hostr Pro! @@ -50,11 +51,11 @@ export function* create() { const mail = new sendgrid.Email({ to: this.session.user.email, - from: fromEmail, - fromname: fromName, - html: html, - text: text, subject: 'Hostr Pro', + from, + fromname, + html, + text, }); mail.addCategory('pro-upgrade'); sendgrid.send(mail); @@ -63,16 +64,17 @@ export function* create() { export function* cancel() { this.assertCSRF(); const Users = this.db.Users; - const user = yield Users.findOne({_id: this.session.user.id}); + const user = yield Users.findOne({ _id: this.session.user.id }); const confirmation = yield stripe.customers.cancelSubscription( user.stripe_customer.id, user.stripe_customer.subscription.id, - { 'at_period_end': true } + { at_period_end: true } ); - yield Users.updateOne({_id: this.session.user.id}, {'$set': {'stripe_customer.subscription': confirmation, type: 'Free'}}); + yield Users.updateOne({ _id: this.session.user.id }, + { $set: { 'stripe_customer.subscription': confirmation, type: 'Free' } }); this.session.user.plan = 'Pro'; - this.body = {status: 'inactive'}; + this.body = { status: 'inactive' }; } diff --git a/web/routes/user.js b/web/routes/user.js index 7b3abae..86e24f8 100644 --- a/web/routes/user.js +++ b/web/routes/user.js @@ -1,10 +1,14 @@ -import { authenticate, setupSession, signup as signupUser, activateUser, sendResetToken, validateResetToken, updatePassword } from '../lib/auth'; +import { + authenticate, setupSession, signup as signupUser, activateUser, sendResetToken, + validateResetToken, updatePassword, +} from '../lib/auth'; import debugname from 'debug'; const debug = debugname('hostr-web:user'); export function* signin() { if (!this.request.body.email) { - return yield this.render('signin', {csrf: this.csrf}); + yield this.render('signin', { csrf: this.csrf }); + return; } this.statsd.incr('auth.attempt', 1); @@ -12,9 +16,14 @@ export function* signin() { const user = yield authenticate.call(this, this.request.body.email, this.request.body.password); if (!user) { this.statsd.incr('auth.failure', 1); - return yield this.render('signin', {error: 'Invalid login details', csrf: this.csrf}); + yield this.render('signin', { error: 'Invalid login details', csrf: this.csrf }); + return; } else if (user.activationCode) { - return yield this.render('signin', {error: 'Your account hasn\'t been activated yet. Check your for an activation email.', csrf: this.csrf}); + yield this.render('signin', { + error: 'Your account hasn\'t been activated yet. Check your for an activation email.', + csrf: this.csrf, + }); + return; } this.statsd.incr('auth.success', 1); yield setupSession.call(this, user); @@ -24,16 +33,22 @@ export function* signin() { export function* signup() { if (!this.request.body.email) { - return yield this.render('signup', {csrf: this.csrf}); + yield this.render('signup', { csrf: this.csrf }); + return; } this.assertCSRF(this.request.body); if (this.request.body.email !== this.request.body.confirm_email) { - return yield this.render('signup', {error: 'Emails do not match.', csrf: this.csrf}); + yield this.render('signup', { error: 'Emails do not match.', csrf: this.csrf }); + return; } else if (this.request.body.email && !this.request.body.terms) { - return yield this.render('signup', {error: 'You must agree to the terms of service.', csrf: this.csrf}); + yield this.render('signup', { error: 'You must agree to the terms of service.', + csrf: this.csrf }); + return; } else if (this.request.body.password && this.request.body.password.length < 7) { - return yield this.render('signup', {error: 'Password must be at least 7 characters long.', csrf: this.csrf}); + yield this.render('signup', { error: 'Password must be at least 7 characters long.', + csrf: this.csrf }); + return; } const ip = this.headers['x-real-ip'] || this.ip; const email = this.request.body.email; @@ -41,10 +56,15 @@ export function* signup() { try { yield signupUser.call(this, email, password, ip); } catch (e) { - return yield this.render('signup', {error: e.message, csrf: this.csrf}); + yield this.render('signup', { error: e.message, csrf: this.csrf }); + return; } this.statsd.incr('auth.signup', 1); - return yield this.render('signup', {message: 'Thanks for signing up, we\'ve sent you an email to activate your account.', csrf: ''}); + yield this.render('signup', { + message: 'Thanks for signing up, we\'ve sent you an email to activate your account.', + csrf: '', + }); + return; } @@ -55,14 +75,19 @@ export function* forgot() { if (this.request.body.password) { if (this.request.body.password.length < 7) { - return yield this.render('forgot', {error: 'Password needs to be at least 7 characters long.', token: token, csrf: this.csrf}); + yield this.render('forgot', { + error: 'Password needs to be at least 7 characters long.', + csrf: this.csrf, + token, + }); + return; } this.assertCSRF(this.request.body); const tokenUser = yield validateResetToken.call(this, token); const userId = tokenUser._id; yield updatePassword.call(this, userId, this.request.body.password); - yield Reset.deleteOne({_id: userId}); - const user = yield Users.findOne({_id: userId}); + yield Reset.deleteOne({ _id: userId }); + const user = yield Users.findOne({ _id: userId }); yield setupSession.call(this, user); this.statsd.incr('auth.reset.success', 1); this.redirect('/'); @@ -70,28 +95,40 @@ export function* forgot() { const tokenUser = yield validateResetToken.call(this, token); if (!tokenUser) { this.statsd.incr('auth.reset.fail', 1); - return yield this.render('forgot', {error: 'Invalid password reset token. It might be expired, or has already been used.', token: null, csrf: this.csrf}); + yield this.render('forgot', { + error: 'Invalid password reset token. It might be expired, or has already been used.', + csrf: this.csrf, + token: null, + }); + return; } - return yield this.render('forgot', {token: token, csrf: this.csrf}); + yield this.render('forgot', { csrf: this.csrf, token }); + return; } else if (this.request.body.email) { this.assertCSRF(this.request.body); try { const email = this.request.body.email; yield sendResetToken.call(this, email); this.statsd.incr('auth.reset.request', 1); - return yield this.render('forgot', {message: 'We\'ve sent an email with a link to reset your password. Be sure to check your spam folder if you it doesn\'t appear within a few minutes', token: null, csrf: this.csrf}); + yield this.render('forgot', { + message: `We've sent an email with a link to reset your password. + Be sure to check your spam folder if you it doesn't appear within a few minutes`, + csrf: this.csrf, + token: null, + }); + return; } catch (error) { debug(error); } } else { - yield this.render('forgot', {token: null, csrf: this.csrf}); + yield this.render('forgot', { csrf: this.csrf, token: null }); } } export function* logout() { this.statsd.incr('auth.logout', 1); - this.cookies.set('r', {expires: new Date(1), path: '/'}); + this.cookies.set('r', { expires: new Date(1), path: '/' }); this.session = null; this.redirect('/'); }