diff --git a/.eslintrc b/.eslintrc index f5878ab..d2f0142 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,11 +1,11 @@ { "extends": "airbnb/base", "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": 2017, "sourceType": "module", "ecmaFeatures": { "experimentalObjectRestSpread": true - }, + } }, "env": { "node": true, @@ -14,5 +14,6 @@ "rules": { "quotes": [2, "single"], "no-underscore-dangle": [0], + "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }] } } diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..92366e9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/app.js" + } + ] +} \ No newline at end of file diff --git a/api/app.js b/api/app.js index bc2bc26..c392914 100644 --- a/api/app.js +++ b/api/app.js @@ -1,12 +1,14 @@ import Router from 'koa-router'; -import stats from '../lib/koa-statsd'; import cors from 'kcors'; import StatsD from 'statsy'; +import debugname from 'debug'; + +import stats from '../lib/koa-statsd'; import auth from './lib/auth'; import * as user from './routes/user'; import * as file from './routes/file'; import * as pro from './routes/pro'; -import debugname from 'debug'; + const debug = debugname('hostr-api'); const router = new Router(); @@ -45,17 +47,15 @@ router.use(async (ctx, next) => { code: 604, }, }; - } else { - if (!err.status) { - debug(err); - if (ctx.raven) { - ctx.raven.captureError(err); - } - throw err; - } else { - ctx.status = err.status; - ctx.body = err.message; + } else if (!err.status) { + debug(err); + if (ctx.raven) { + ctx.raven.captureError(err); } + throw err; + } else { + ctx.status = err.status; + ctx.body = err.message; } } ctx.type = 'application/json'; diff --git a/api/lib/auth.js b/api/lib/auth.js index 8a52b66..8c60357 100644 --- a/api/lib/auth.js +++ b/api/lib/auth.js @@ -1,7 +1,9 @@ import passwords from 'passwords'; import auth from 'basic-auth'; -import models from '../../models'; import debugname from 'debug'; + +import models from '../../models'; + const debug = debugname('hostr-api:auth'); const badLoginMsg = '{"error": {"message": "Incorrect login details.", "code": 607}}'; @@ -36,8 +38,10 @@ export default async (ctx, next) => { }, }); - ctx.assert(count < 25, 401, - '{"error": {"message": "Too many incorrect logins.", "code": 608}}'); + ctx.assert( + count < 25, 401, + '{"error": {"message": "Too many incorrect logins.", "code": 608}}', + ); user = await models.user.findOne({ where: { @@ -56,8 +60,10 @@ export default async (ctx, next) => { ctx.assert(user, 401, badLoginMsg); debug('Checking user is activated'); debug(user.activated); - ctx.assert(user.activated === true, 401, - '{"error": {"message": "Account has not been activated.", "code": 603}}'); + ctx.assert( + user.activated === true, 401, + '{"error": {"message": "Account has not been activated.", "code": 603}}', + ); login.successful = true; await login.save(); @@ -85,9 +91,11 @@ export default async (ctx, next) => { plan: user.plan, uploads_today: uploadedToday, }; - ctx.response.set('Daily-Uploads-Remaining', - user.type === 'Pro' ? 'unlimited' : 15 - uploadedToday); + ctx.response.set( + 'Daily-Uploads-Remaining', + user.type === 'Pro' ? 'unlimited' : 15 - uploadedToday, + ); ctx.user = normalisedUser; debug('Authenticated user: ', ctx.user.email); await next(); -} +}; diff --git a/api/routes/file.js b/api/routes/file.js index 65cc10a..c44ca71 100644 --- a/api/routes/file.js +++ b/api/routes/file.js @@ -47,8 +47,8 @@ export async function list(ctx) { processed: true, }, order: [ - ['createdAt', 'DESC'], - ], + ['createdAt', 'DESC'], + ], offset, limit, }); diff --git a/api/routes/pro.js b/api/routes/pro.js index 4dee87e..8f80146 100644 --- a/api/routes/pro.js +++ b/api/routes/pro.js @@ -1,18 +1,19 @@ import path from 'path'; import views from 'co-views'; -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); import models from '../../models'; +const render = views(path.join(__dirname, '/../views'), { default: 'ejs' }); +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); +const sendgrid = sendgridInit(process.env.SENDGRID_KEY); + const from = process.env.EMAIL_FROM; const fromname = process.env.EMAIL_NAME; export async function create(ctx) { - const stripeToken = ctx.request.body.stripeToken; + const { stripeToken } = ctx.request.body; const ip = ctx.request.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress; @@ -74,7 +75,7 @@ export async function cancel(ctx) { await stripe.customers.cancelSubscription( transaction.data.id, transaction.data.subscription.id, - { at_period_end: false } + { at_period_end: false }, ); user.plan = 'Free'; diff --git a/api/routes/user.js b/api/routes/user.js index 3d14dfc..1ac0771 100644 --- a/api/routes/user.js +++ b/api/routes/user.js @@ -2,9 +2,10 @@ import uuid from 'node-uuid'; import redis from 'redis'; import co from 'co'; import passwords from 'passwords'; +import debugname from 'debug'; + import models from '../../models'; -import debugname from 'debug'; const debug = debugname('hostr-api:user'); const redisUrl = process.env.REDIS_URL; @@ -26,31 +27,37 @@ export async function transaction(ctx) { }, }); - ctx.body = transactions.map((item) => { - return { - id: item.id, - amount: item.amount / 100, - date: item.date, - description: item.description, - type: 'direct', - }; - }); + ctx.body = transactions.map(item => ({ + id: item.id, + amount: item.amount / 100, + date: item.date, + description: item.description, + type: 'direct', + })); } export async function settings(ctx) { - ctx.assert(ctx.request.body, 400, - '{"error": {"message": "Current Password required to update account.", "code": 612}}'); - ctx.assert(ctx.request.body.current_password, 400, - '{"error": {"message": "Current Password required to update account.", "code": 612}}'); + ctx.assert( + ctx.request.body, 400, + '{"error": {"message": "Current Password required to update account.", "code": 612}}', + ); + ctx.assert( + ctx.request.body.current_password, 400, + '{"error": {"message": "Current Password required to update account.", "code": 612}}', + ); const user = await models.user.findById(ctx.user.id); - ctx.assert(await passwords.match(ctx.request.body.current_password, user.password), 400, - '{"error": {"message": "Incorrect password", "code": 606}}'); + ctx.assert( + await passwords.match(ctx.request.body.current_password, user.password), 400, + '{"error": {"message": "Incorrect password", "code": 606}}', + ); if (ctx.request.body.email && ctx.request.body.email !== user.email) { user.email = ctx.request.body.email; } if (ctx.request.body.new_password) { - ctx.assert(ctx.request.body.new_password.length >= 7, 400, - '{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}'); + ctx.assert( + ctx.request.body.new_password.length >= 7, 400, + '{"error": {"message": "Password must be 7 or more characters long.", "code": 606}}', + ); user.password = await passwords.hash(ctx.request.body.new_password); } await user.save(); diff --git a/app.js b/app.js index d73f0e0..dc35e45 100644 --- a/app.js +++ b/app.js @@ -9,11 +9,11 @@ import websockify from 'koa-websocket'; import helmet from 'koa-helmet'; import session from 'koa-session'; import raven from 'raven'; +import debugname from 'debug'; import * as redis from './lib/redis'; import api, { ws } from './api/app'; import web from './web/app'; -import debugname from 'debug'; const debug = debugname('hostr'); const app = websockify(new Koa()); diff --git a/lib/format.js b/lib/format.js index 1901268..4dfc8d9 100644 --- a/lib/format.js +++ b/lib/format.js @@ -1,5 +1,5 @@ import moment from 'moment'; -import { sniff } from './type'; +import sniff from './sniff'; const baseURL = process.env.WEB_BASE_URL; diff --git a/lib/hostr-file-stream.js b/lib/hostr-file-stream.js index bb6cd61..f2ad99f 100644 --- a/lib/hostr-file-stream.js +++ b/lib/hostr-file-stream.js @@ -1,8 +1,8 @@ import fs from 'fs'; import createError from 'http-errors'; +import debugname from 'debug'; import { get as getS3 } from './s3'; -import debugname from 'debug'; const debug = debugname('hostr:file-stream'); function writer(localPath, remoteRead) { diff --git a/lib/hostr-id.js b/lib/hostr-id.js index 5b9a6ce..3a6b0f5 100644 --- a/lib/hostr-id.js +++ b/lib/hostr-id.js @@ -4,7 +4,7 @@ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; function randomID() { let rand = ''; - for (let i = 0; i < 12; i++) { + for (let i = 0; i < 12; i += 1) { rand += chars.charAt(Math.floor((Math.random() * chars.length))); } return rand; @@ -18,7 +18,7 @@ async function checkId(Files, fileId, attempts) { if (file === null) { return fileId; } - return checkId(randomID(), ++attempts); // eslint-disable-line no-param-reassign + return checkId(Files, randomID(), attempts + 1); } export default function (Files) { diff --git a/lib/koa-statsd.js b/lib/koa-statsd.js index 7de5735..7a28f98 100644 --- a/lib/koa-statsd.js +++ b/lib/koa-statsd.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var Stats = require('statsy'); +const Stats = require('statsy'); /** * Initialize stats middleware with `opts` @@ -14,14 +14,13 @@ var Stats = require('statsy'); * @api public */ -module.exports = function(opts){ - opts = opts || {}; - var s = new Stats(opts); +export default function (opts) { + const s = new Stats(opts || {}); return async (ctx, next) => { // counters s.incr('request.count'); - s.incr('request.' + ctx.method + '.count'); + s.incr(`request.${ctx.method}.count`); // size s.histogram('request.size', ctx.request.length || 0); @@ -33,5 +32,5 @@ module.exports = function(opts){ ctx.res.on('finish', s.timer('request.duration')); await next(); - } -}; + }; +} diff --git a/lib/malware.js b/lib/malware.js index 4144728..baabf65 100644 --- a/lib/malware.js +++ b/lib/malware.js @@ -1,4 +1,4 @@ -import virustotal from './virustotal'; +import getFileReport from './virustotal'; const extensions = [ 'EXE', @@ -65,13 +65,13 @@ function getExtension(filename) { return (i < 0) ? '' : filename.substr(i + 1); } -export default function* (file) { - if (extensions.indexOf(getExtension(file.file_name.toUpperCase())) < 0) { +export default async (file) => { + if (extensions.indexOf(getExtension(file.name.toUpperCase())) < 0) { return false; } - const result = yield virustotal.getFileReport(file.md5); + const result = await getFileReport(file.md5); return { positive: result.positives >= 5, result, }; -} +}; diff --git a/lib/mongo.js b/lib/mongo.js deleted file mode 100644 index bd83f2a..0000000 --- a/lib/mongo.js +++ /dev/null @@ -1,37 +0,0 @@ -import mongodb from 'mongodb-promisified'; -const MongoClient = mongodb().MongoClient; -import debugname from 'debug'; -const debug = debugname('hostr:mongo'); - -/* eslint no-param-reassign: ["error", { "props": false }] */ -export const mongo = new Promise((resolve, reject) => { - debug('Connecting to Mongodb'); - return MongoClient.connect(process.env.MONGO_URL).then((client) => { - debug('Successfully connected to Mongodb'); - client.Users = client.collection('users'); - client.Files = client.collection('files'); - client.Transactions = client.collection('transactions'); - 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.ObjectId = client.objectId = mongodb().ObjectId; - return resolve(client); - }).catch((e) => { - reject(e); - }); -}).catch((e) => { - debug(e); -}); - -export default function () { - return function* dbMiddleware(next) { - try { - this.db = yield mongo; - } catch (e) { - debug(e); - } - yield next; - }; -} diff --git a/lib/redis.js b/lib/redis.js index 56060b5..3b93a2e 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -3,6 +3,7 @@ import coRedis from 'co-redis'; import koaRedis from 'koa-redis'; import session from 'koa-generic-session'; import debugname from 'debug'; + const debug = debugname('hostr:redis'); const connection = new Promise((resolve, reject) => { @@ -25,8 +26,7 @@ const redisSession = new Promise((resolve, reject) => }).catch((err) => { debug('koa-redis error: ', err); reject(err); - }) -); + })); const wrapped = new Promise((resolve, reject) => connection.then((client) => { @@ -40,8 +40,7 @@ const wrapped = new Promise((resolve, reject) => debug('co-redis error: ', err); reject(err); throw err; - }) -); + })); export function sessionStore() { return async (ctx, next) => { diff --git a/lib/resize.js b/lib/resize.js index 0ba03f0..ef87078 100644 --- a/lib/resize.js +++ b/lib/resize.js @@ -1,13 +1,14 @@ import fs from 'mz/fs'; import jimp from 'jimp'; import debugname from 'debug'; + const debug = debugname('hostr-api:resize'); const types = { jpg: jimp.MIME_JPEG, png: jimp.MIME_PNG, gif: jimp.MIME_JPEG, -} +}; function cover(path, type, size) { return new Promise((resolve, reject) => { diff --git a/lib/s3.js b/lib/s3.js index 19a76ef..3d5086c 100644 --- a/lib/s3.js +++ b/lib/s3.js @@ -1,5 +1,6 @@ import aws from 'aws-sdk'; import debugname from 'debug'; + const debug = debugname('hostr:s3'); const s3 = new aws.S3({ diff --git a/lib/sftp.js b/lib/sftp.js deleted file mode 100644 index 7193159..0000000 --- a/lib/sftp.js +++ /dev/null @@ -1,53 +0,0 @@ -import { dirname, join } from 'path'; -import StatsD from 'statsy'; -import Client from './ssh2-sftp-client'; -import debugname from 'debug'; -const debug = debugname('hostr:sftp'); - -const statsdOpts = { prefix: 'hostr-api', host: process.env.STATSD_HOST }; -const statsd = new StatsD(statsdOpts); - -export function get(remotePath) { - debug('fetching', join('hostr', 'uploads', remotePath)); - const sftp = new Client(); - return sftp.connect({ - host: process.env.SFTP_HOST, - port: process.env.SFTP_PORT, - username: process.env.SFTP_USERNAME, - password: process.env.SFTP_PASSWORD, - }) - .then(() => sftp.get(join('hostr', 'uploads', remotePath), { encoding: null })); -} - -function sendFile(localPath, remotePath) { - const sftp = new Client(); - return sftp.connect({ - host: process.env.SFTP_HOST, - port: process.env.SFTP_PORT, - username: process.env.SFTP_USERNAME, - password: process.env.SFTP_PASSWORD, - }) - .then(() => sftp.put(localPath, remotePath, true)) - .catch((err) => { - if (err.message === 'No such file') { - debug('Creating directory'); - return sftp.mkdir(dirname(remotePath), true) - .then(() => sftp.put(localPath, remotePath, true)); - } - throw err; - }); -} - -export function *upload(localPath, remotePath) { - let done = false; - for (let retries = 0; retries < 5; retries++) { - try { - done = yield sendFile(localPath, remotePath); - break; - } catch (err) { - statsd.incr('file.upload.retry', 1); - debug('retry'); - } - } - return done; -} diff --git a/lib/type.js b/lib/sniff.js similarity index 94% rename from lib/type.js rename to lib/sniff.js index 668c1a4..a70417e 100644 --- a/lib/type.js +++ b/lib/sniff.js @@ -26,9 +26,9 @@ const extensions = { rar: 'archive', }; -export function sniff(filename) { +export default (filename) => { if (extensions[filename.split('.').pop().toLowerCase()]) { return extensions[filename.split('.').pop().toLowerCase()]; } return 'other'; -} +}; diff --git a/lib/ssh2-sftp-client.js b/lib/ssh2-sftp-client.js deleted file mode 100644 index db44be4..0000000 --- a/lib/ssh2-sftp-client.js +++ /dev/null @@ -1,303 +0,0 @@ -/** - * ssh2 sftp client for node - */ -'use strict'; - -let Client = require('ssh2').Client; - -let SftpClient = function(){ - this.client = new Client(); -}; - -/** - * Retrieves a directory listing - * - * @param {String} path, a string containing the path to a directory - * @return {Promise} data, list info - */ -SftpClient.prototype.list = function(path) { - let reg = /-/gi; - - return new Promise((resolve, reject) => { - let sftp = this.sftp; - - if (sftp) { - sftp.readdir(path, (err, list) => { - if (err) { - reject(err); - return false; - } - // reset file info - list.forEach((item, i) => { - list[i] = { - type: item.longname.substr(0, 1), - name: item.filename, - size: item.attrs.size, - modifyTime: item.attrs.mtime * 1000, - accessTime: item.attrs.atime * 1000, - rights: { - user: item.longname.substr(1, 3).replace(reg, ''), - group: item.longname.substr(4,3).replace(reg, ''), - other: item.longname.substr(7, 3).replace(reg, '') - }, - owner: item.attrs.uid, - group: item.attrs.gid - } - }); - resolve(list); - }); - } else { - reject('sftp connect error'); - } - }); -}; - -/** - * get file - * - * @param {String} path, path - * @param {Object} useCompression, config options - * @return {Promise} stream, readable stream - */ -SftpClient.prototype.get = function(path, useCompression) { - useCompression = Object.assign({}, {encoding: 'utf8'}, useCompression); - - return new Promise((resolve, reject) => { - let sftp = this.sftp; - - if (sftp) { - try { - let stream = sftp.createReadStream(path, useCompression); - - stream.on('error', reject); - - resolve(stream); - } catch(err) { - reject(err); - } - } else { - reject('sftp connect error'); - } - }); -}; - -/** - * Create file - * - * @param {String|Buffer|stream} input - * @param {String} remotePath, - * @param {Object} useCompression [description] - * @return {[type]} [description] - */ -SftpClient.prototype.put = function(input, remotePath, useCompression) { - useCompression = Object.assign({}, {encoding: 'utf8'}, useCompression); - - return new Promise((resolve, reject) => { - let sftp = this.sftp; - - if (sftp) { - if (typeof input === 'string') { - sftp.fastPut(input, remotePath, useCompression, (err) => { - if (err) { - reject(err); - return false; - } - resolve(); - }); - return false; - } - let stream = sftp.createWriteStream(remotePath, useCompression); - let data; - - stream.on('error', reject); - stream.on('close', resolve); - - if (input instanceof Buffer) { - data = stream.end(input); - return false; - } - data = input.pipe(stream); - } else { - reject('sftp connect error'); - } - }); -}; - -SftpClient.prototype.mkdir = function(path, recursive) { - recursive = recursive || false; - - return new Promise((resolve, reject) => { - let sftp = this.sftp; - - if (sftp) { - if (!recursive) { - sftp.mkdir(path, (err) => { - if (err) { - reject(err); - return false; - } - resolve(); - }); - return false; - } - - let tokens = path.split(/\//g); - let p = ''; - - let mkdir = () => { - let token = tokens.shift(); - - if (!token && !tokens.length) { - resolve(); - return false; - } - token += '/'; - p = p + token; - sftp.mkdir(p, (err) => { - if (err && err.code !== 4) { - reject(err); - } - mkdir(); - }); - }; - return mkdir(); - } else { - reject('sftp connect error'); - } - }); -}; - -SftpClient.prototype.rmdir = function(path, recursive) { - recursive = recursive || false; - - return new Promise((resolve, reject) => { - let sftp = this.sftp; - - if (sftp) { - if (!recursive) { - return sftp.rmdir(path, (err) => { - if (err) { - reject(err); - } - resolve(); - }); - } - let rmdir = (p) => { - return this.list(p).then((list) => { - if (list.length > 0) { - let promises = []; - - list.forEach((item) => { - let name = item.name; - let promise; - var subPath; - - if (name[0] === '/') { - subPath = name; - } else { - if (p[p.length - 1] === '/') { - subPath = p + name; - } else { - subPath = p + '/' + name; - } - } - - if (item.type === 'd') { - if (name !== '.' || name !== '..') { - promise = rmdir(subPath); - } - } else { - promise = this.delete(subPath); - } - promises.push(promise); - }); - if (promises.length) { - return Promise.all(promises).then(() => { - return rmdir(p); - }); - } - } else { - return new Promise((resolve, reject) => { - return sftp.rmdir(p, (err) => { - if (err) { - reject(err); - } - else { - resolve(); - } - }); - }); - } - }); - }; - return rmdir(path).then(() => {resolve()}) - .catch((err) => {reject(err)}); - } else { - reject('sftp connect error'); - } - }); -}; - -SftpClient.prototype.delete = function(path) { - return new Promise((resolve, reject) => { - let sftp = this.sftp; - - if (sftp) { - sftp.unlink(path, (err) => { - if (err) { - reject(err); - return false; - } - resolve(); - }); - } else { - reject('sftp connect error'); - } - }); -}; - -SftpClient.prototype.rename = function(srcPath, remotePath) { - return new Promise((resolve, reject) => { - let sftp = this.sftp; - - if (sftp) { - sftp.rename(srcPath, remotePath, (err) => { - if (err) { - reject(err); - return false; - } - resolve(); - }); - } else { - reject('sftp connect error'); - } - }); -} - -SftpClient.prototype.connect = function(config) { - var c = this.client; - - return new Promise((resolve, reject) => { - this.client.on('ready', () => { - - this.client.sftp((err, sftp) => { - if (err) { - reject(err); - } - this.sftp = sftp; - resolve(sftp); - }); - }).on('error', (err) => { - reject(err); - }).connect(config); - }); -}; - -SftpClient.prototype.end = function() { - return new Promise((resolve) => { - this.client.end(); - resolve(); - }); -}; - -module.exports = SftpClient; diff --git a/lib/uploader.js b/lib/uploader.js index df40618..e17a1ab 100644 --- a/lib/uploader.js +++ b/lib/uploader.js @@ -3,16 +3,16 @@ import Busboy from 'busboy'; import crypto from 'crypto'; import fs from 'mz/fs'; import sizeOf from 'image-size'; +import debugname from 'debug'; import models from '../models'; import createHostrId from './hostr-id'; import { formatFile } from './format'; import resize from './resize'; import malware from './malware'; -import { sniff } from './type'; +import sniff from './sniff'; import { upload as s3upload } from './s3'; -import debugname from 'debug'; const debug = debugname('hostr-api:uploader'); const storePath = process.env.UPLOAD_STORAGE_PATH; @@ -66,14 +66,12 @@ export default class Uploader { highWaterMark: 10000000, }); - this.upload.on('file', async (fieldname, file, filename, encoding, mimetype) => { - debug('FILE', fieldname, file, filename, encoding, mimetype); - + this.upload.on('file', async (fieldname, file, filename) => { this.upload.filename = filename; this.file = await models.file.create({ id: await createHostrId(), - name: this.upload.filename.replace(/[^a-zA-Z0-9\.\-_\s]/g, '').replace(/\s+/g, ''), + name: this.upload.filename.replace(/[^a-zA-Z0-9\.\-_\s]/g, '').replace(/\s+/g, ''), // eslint-disable-line no-useless-escape originalName: this.upload.filename, userId: this.context.user.id, status: 'uploading', @@ -102,7 +100,7 @@ export default class Uploader { this.localStream.write(data); - this.percentComplete = Math.floor(this.receivedSize * 100 / this.expectedSize); + this.percentComplete = Math.floor((this.receivedSize * 100) / this.expectedSize); if (this.percentComplete > this.lastPercent && this.lastTick < Date.now() - 1000) { const progressEvent = `{"type": "file-progress", "data": {"id": "${this.file.id}", "complete": ${this.percentComplete}}}`; @@ -131,7 +129,6 @@ export default class Uploader { this.localStream.on('end', () => { s3upload(fs.createReadStream(join(storePath, this.path)), this.path); }); - }); this.context.req.pipe(this.upload); }); @@ -210,7 +207,7 @@ export default class Uploader { // Check in the background process.nextTick(async () => { debug('Malware Scan'); - const result = await malware(this); + const result = await malware(this.file); if (result) { this.file.malwarePositives = result.positives; this.file.save(); diff --git a/lib/virustotal.js b/lib/virustotal.js index 84f686c..53ddbcd 100644 --- a/lib/virustotal.js +++ b/lib/virustotal.js @@ -3,9 +3,9 @@ import FormData from 'form-data'; const apiRoot = 'https://www.virustotal.com/vtapi/v2'; -export function* getFileReport(resource, apiKey = process.env.VIRUSTOTAL_KEY) { +export default async (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 fetch(`${apiRoot}/file/report`, { method: 'POST' }); +}; diff --git a/migrate/migrate-activations.js b/migrate/migrate-activations.js deleted file mode 100644 index 5e5e008..0000000 --- a/migrate/migrate-activations.js +++ /dev/null @@ -1,75 +0,0 @@ -import co from 'co'; - -import models from '../models'; -import { mongo } from '../lib/mongo'; - -import debugname from 'debug'; -const debug = debugname('hostr:db'); -let db; - - -co(function *sync() { - debug('Syncing schema'); - yield models.sequelize.sync(); - debug('Schema synced'); - db = yield mongo; - const users = yield db.Users.find({}, { sort: [['joined', 'asc']] }).toArray(); - for (const user of users) { - if (user.joined === '0') { - const file = yield db.Files.findOne({ - owner: user._id, - }, { - limit: 1, - sort: [['time_added', 'asc']], - }); - if (file && file.time_added > 0) { - user.createdAt = new Date(file.time_added * 1000).getTime(); - } else { - user.createdAt = new Date().getTime(); - } - } else { - user.createdAt = new Date(user.joined * 1000).getTime(); - } - } - - users.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1)); - - for (const user of users) { - if (!user.email) { - continue; - } - - const exists = yield models.user.findOne({ - where: { - email: user.email, - }, - }); - - if (exists) { - debug('User exists, continue'); - continue; - } - - const mongoId = user._id.toString(); - - const newUser = yield models.user.create({ - email: user.email, - password: user.salted_password, - name: user.first_name ? `${user.first_name} ${user.last_name}` : null, - plan: user.type || 'Free', - activated: !user.activationCode, - banned: !!user.banned, - deletedAt: user.status === 'deleted' ? new Date().getTime() : null, - createdAt: user.createdAt, - updatedAt: user.createdAt, - mongoId, - }); - yield newUser.save({ silent: true }); - } - models.sequelize.close(); - db.close(); -}).catch((err) => { - models.sequelize.close(); - db.close(); - debug(err); -}); diff --git a/migrate/migrate-files.js b/migrate/migrate-files.js deleted file mode 100644 index 9067b24..0000000 --- a/migrate/migrate-files.js +++ /dev/null @@ -1,83 +0,0 @@ -import co from 'co'; -import validateIp from 'validate-ip'; - -import models from '../models'; -import { mongo } from '../lib/mongo'; - -import debugname from 'debug'; -const debug = debugname('hostr:db'); -let db; - - -co(function *sync() { - debug('Syncing schema'); - yield models.sequelize.sync(); - debug('Schema synced'); - db = yield mongo; - const users = yield models.user.findAll({}); - const userIds = {}; - debug('remap'); - for (const user of users) { - userIds[user.mongoId] = user.id; - } - debug('remap done'); - let files; - try { - files = db.Files.find({}, { - sort: [['time_added', 'desc']], - skip: 0, - }); - } catch (err) { - debug(err); - } - debug('fetched files'); - - while (true) { - const file = yield files.next(); - if (!file) { - break; - } - if (!file.time_added || !file.file_size) { - continue; - } - let ip = file.ip ? file.ip.split(',').pop().trim() : null; - - if (typeof ip !== 'string' || !validateIp(ip)) { - ip = null; - } - const processed = file.status !== 'uploading'; - const accessedAt = file.last_accessed ? new Date(file.last_accessed * 1000) : null; - - const mongoId = file._id.toString(); - - yield models.file.upsert({ - id: file._id.toString(), - name: file.file_name, - originalName: file.original_name || file.file_name, - size: file.file_size, - downloads: file.downloads, - deletedAt: file.status === 'deleted' ? new Date() : null, - createdAt: new Date(file.time_added * 1000), - updatedAt: new Date(file.time_added * 1000), - accessedAt, - processed, - type: file.type !== 'file' ? file.type : 'other', - width: Number.isInteger(file.width) ? file.width : null, - height: Number.isInteger(file.height) ? file.height : null, - userId: file.owner !== undefined ? userIds[file.owner] : null, - ip, - legacyId: file.system_name !== file._id ? file.system_name : null, - md5: file.md5, - malwarePositives: file.virustotal && file.virustotal.positives > 0 ? - file.virustotal.positives : null, - mongoId, - }, { /* logging: false */ }); - } - - models.sequelize.close(); - db.close(); -}).catch((err) => { - models.sequelize.close(); - db.close(); - debug(err); -}); diff --git a/migrate/migrate-logins.js b/migrate/migrate-logins.js deleted file mode 100644 index 11af8ac..0000000 --- a/migrate/migrate-logins.js +++ /dev/null @@ -1,54 +0,0 @@ -import co from 'co'; -import validateIp from 'validate-ip'; - -import models from '../models'; -import { mongo } from '../lib/mongo'; - -import debugname from 'debug'; -const debug = debugname('hostr:db'); -let db; - - -co(function *sync() { - debug('Syncing schema'); - yield models.sequelize.sync(); - debug('Schema synced'); - db = yield mongo; - const users = yield models.user.findAll({}); - const userIds = {}; - debug('remap'); - for (const user of users) { - userIds[user._id] = user.id; - } - debug('remap done'); - let logins; - try { - logins = db.Logins.find({}, { - skip: 0, - }); - } catch (err) { - debug(err); - } - debug('fetched logins'); - - while (true) { - const login = yield logins.next(); - if (!login) { - break; - } - - const newLogin = yield models.login.create({ - ip: login.ip, - createdAt: login.at * 1000, - successful: login.successful, - }, { /* logging: false */ }); - newLogin.save(); - } - - models.sequelize.close(); - db.close(); -}).catch((err) => { - models.sequelize.close(); - db.close(); - debug(err); -}); diff --git a/migrate/migrate-malwares.js b/migrate/migrate-malwares.js deleted file mode 100644 index e882578..0000000 --- a/migrate/migrate-malwares.js +++ /dev/null @@ -1,46 +0,0 @@ -import co from 'co'; - -import models from '../models'; -import { mongo } from '../lib/mongo'; - -import debugname from 'debug'; -const debug = debugname('hostr:db'); -let db; - - -co(function *sync() { - debug('Syncing schema'); - yield models.sequelize.sync(); - debug('Schema synced'); - db = yield mongo; - - const files = db.Files.find({}, { - sort: [['time_added', 'desc']], - skip: 0, - }); - debug('fetched files'); - - while (true) { - const file = yield files.next(); - if (!file) { - break; - } - - if (!file.time_added || !file.file_size || !file.malware) { - continue; - } - - yield models.malware.upsert({ - fileId: file._id, - positives: file.virustotal ? file.virustotal.positives : 100, - virustotal: file.virustotal || null, - }, { /* logging: false */ }); - } - - models.sequelize.close(); - db.close(); -}).catch((err) => { - models.sequelize.close(); - db.close(); - debug(err); -}); diff --git a/migrate/migrate-users.js b/migrate/migrate-users.js deleted file mode 100644 index 88dc3da..0000000 --- a/migrate/migrate-users.js +++ /dev/null @@ -1,77 +0,0 @@ -import co from 'co'; - -import models from '../models'; -import { mongo } from '../lib/mongo'; - -import debugname from 'debug'; -const debug = debugname('hostr:db'); -let db; - - -co(function *sync() { - debug('Syncing schema'); - yield models.sequelize.sync(); - debug('Schema synced'); - db = yield mongo; - const users = yield db.Users.find({}, { sort: [['joined', 'asc']] }).toArray(); - for (const user of users) { - if (user.joined === '0') { - const file = yield db.Files.findOne({ - owner: user._id, - }, { - limit: 1, - sort: [['time_added', 'asc']], - }); - if (file && file.time_added > 0) { - user.createdAt = new Date(file.time_added * 1000).getTime(); - } else { - user.createdAt = new Date().getTime(); - } - } else { - user.createdAt = new Date(user.joined * 1000).getTime(); - } - } - - users.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1)); - - for (const user of users) { - if (!user.email) { - continue; - } - - const exists = yield models.user.findOne({ - where: { - email: user.email, - }, - }); - - if (exists) { - debug('User exists, continue'); - continue; - } - - const mongoId = user._id.toString(); - - const newUser = yield models.user.create({ - email: user.email, - password: user.salted_password, - name: user.first_name ? `${user.first_name} ${user.last_name}` : null, - plan: user.type || 'Free', - activated: !user.activationCode, - banned: !!user.banned, - deletedAt: user.status === 'deleted' ? new Date().getTime() : null, - createdAt: user.createdAt, - updatedAt: user.createdAt, - mongoId, - }, { - include: [models.activation], - }); - yield newUser.save({ silent: true }); - } - models.sequelize.close(); - db.close(); -}).catch((err) => { - models.sequelize.close(); - db.close(); - debug(err); -}); diff --git a/models/file.js b/models/file.js index 72d0933..9607832 100644 --- a/models/file.js +++ b/models/file.js @@ -13,7 +13,7 @@ export default function (sequelize, DataTypes) { 'audio', 'video', 'archive', - 'other' + 'other', ), width: DataTypes.INTEGER, height: DataTypes.INTEGER, @@ -32,14 +32,15 @@ export default function (sequelize, DataTypes) { }); File.accessed = function accessed(id) { - sequelize.query(` + sequelize.query( + ` UPDATE files SET "downloads" = downloads + 1, "accessedAt" = NOW() WHERE "id" = :id`, - { - replacements: { id }, - type: sequelize.QueryTypes.UPDATE, - } + { + replacements: { id }, + type: sequelize.QueryTypes.UPDATE, + }, ); }; diff --git a/models/index.js b/models/index.js index 132dc65..2da3edd 100644 --- a/models/index.js +++ b/models/index.js @@ -2,10 +2,6 @@ import fs from 'fs'; import path from 'path'; import Sequelize from 'sequelize'; -import debugname from 'debug'; - -const debug = debugname('hostr:models'); - const config = { dialect: 'postgres', protocol: 'postgres', diff --git a/web/app.js b/web/app.js index e0257b3..ffdabfd 100644 --- a/web/app.js +++ b/web/app.js @@ -2,11 +2,10 @@ import path from 'path'; import Router from 'koa-router'; import CSRF from 'koa-csrf'; import views from 'koa-views'; -import stats from '../lib/koa-statsd'; import StatsD from 'statsy'; import errors from 'koa-error'; -import * as redis from '../lib/redis'; +import stats from '../lib/koa-statsd'; import * as index from './routes/index'; import * as file from './routes/file'; import * as user from './routes/user'; @@ -26,8 +25,6 @@ router.use(async (ctx, next) => { await next(); }); -//router.use(redis.sessionStore()); - router.use(async (ctx, next) => { ctx.state = { session: ctx.session, @@ -71,15 +68,15 @@ router.get('/:id', file.landing); 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}`); +router.get('/download/:id/:name', async (ctx, id) => { + ctx.redirect(`/${id}`); }); -router.get('/updaters/mac', function* macUpdater() { - this.redirect('/updaters/mac.xml'); +router.get('/updaters/mac', async (ctx) => { + ctx.redirect('/updaters/mac.xml'); }); -router.get('/updaters/mac/changelog', function* macChangelog() { - yield this.render('mac-update-changelog'); +router.get('/updaters/mac/changelog', async (ctx) => { + await ctx.render('mac-update-changelog'); }); export default router; diff --git a/web/lib/auth.js b/web/lib/auth.js index 8a8fe8e..fb9b337 100644 --- a/web/lib/auth.js +++ b/web/lib/auth.js @@ -41,7 +41,7 @@ export async function authenticate(email, password) { activated: true, }, }); - debug(user); + const login = await models.login.create({ ip: remoteIp, successful: false, @@ -52,7 +52,6 @@ export async function authenticate(email, password) { debug('Password verified'); login.successful = true; await login.save(); - debug(user); return user; } debug('Password invalid'); @@ -182,18 +181,18 @@ Visit ${process.env.WEB_BASE_URL}/forgot/${reset.id} to set a new one. export async function fromToken(token) { const userId = await this.redis.get(token); - return await models.user.findById(userId); + return models.user.findById(userId); } export async function fromCookie(rememberId) { const userId = await models.remember.findById(rememberId); - return await models.user.findById(userId); + return models.user.findById(userId); } export async function validateResetToken(resetId) { - return await models.reset.findById(resetId); + return models.reset.findById(resetId); } @@ -206,7 +205,6 @@ export async function updatePassword(userId, password) { export async function activateUser(code) { - debug(code); const activation = await models.activation.findOne({ where: { id: code, diff --git a/web/routes/file.js b/web/routes/file.js index 36b05cd..7fd5c15 100644 --- a/web/routes/file.js +++ b/web/routes/file.js @@ -20,7 +20,7 @@ function userAgentCheck(userAgent) { } function referrerCheck(referrer) { - return referrer && referrerRegexes.some((regex) => referrer.match(regex)); + return referrer && referrerRegexes.some(regex => referrer.match(regex)); } function hotlinkCheck(file, userAgent, referrer) { @@ -52,7 +52,7 @@ export async function get(ctx) { } if (file.malware) { - const alert = ctx.request.query.alert; + const { alert } = ctx.request.query; if (!alert || !alert.match(/i want to download malware/i)) { ctx.redirect(`/${file.id}`); return; diff --git a/web/routes/index.js b/web/routes/index.js index e384c89..d72b4da 100644 --- a/web/routes/index.js +++ b/web/routes/index.js @@ -1,5 +1,5 @@ import uuid from 'node-uuid'; -import auth from '../lib/auth'; +import { fromToken, fromCookie, setupSession } from '../lib/auth'; export async function main(ctx) { if (ctx.session.user) { @@ -11,18 +11,16 @@ export async function main(ctx) { await ctx.redis.set(token, ctx.session.user.id, 'EX', 604800); ctx.session.user.token = token; await ctx.render('index', { user: ctx.session.user }); + } else if (ctx.query['app-token']) { + const user = await fromToken(ctx, ctx.query['app-token']); + await setupSession(ctx, user); + ctx.redirect('/'); + } else if (ctx.cookies.r) { + const user = await fromCookie(ctx, ctx.cookies.r); + await setupSession(ctx, user); + ctx.redirect('/'); } else { - if (ctx.query['app-token']) { - const user = await auth.fromToken(ctx, ctx.query['app-token']); - await auth.setupSession(ctx, user); - ctx.redirect('/'); - } else if (ctx.cookies.r) { - const user = await auth.fromCookie(ctx, ctx.cookies.r); - await auth.setupSession(ctx, user); - ctx.redirect('/'); - } else { - await ctx.render('marketing'); - } + await ctx.render('marketing'); } } diff --git a/web/routes/user.js b/web/routes/user.js index c92ebf5..188a000 100644 --- a/web/routes/user.js +++ b/web/routes/user.js @@ -1,9 +1,11 @@ +import debugname from 'debug'; import { authenticate, setupSession, signup as signupUser, activateUser, sendResetToken, validateResetToken, updatePassword, } from '../lib/auth'; + import models from '../../models'; -import debugname from 'debug'; + const debug = debugname('hostr-web:user'); export async function signin(ctx) { @@ -44,17 +46,20 @@ export async function signup(ctx) { await ctx.render('signup', { error: 'Emails do not match.', csrf: ctx.csrf }); return; } else if (ctx.request.body.email && !ctx.request.body.terms) { - await ctx.render('signup', { error: 'You must agree to the terms of service.', - csrf: ctx.csrf }); + await ctx.render('signup', { + error: 'You must agree to the terms of service.', + csrf: ctx.csrf, + }); return; } else if (ctx.request.body.password && ctx.request.body.password.length < 7) { - await ctx.render('signup', { error: 'Password must be at least 7 characters long.', - csrf: ctx.csrf }); + await ctx.render('signup', { + error: 'Password must be at least 7 characters long.', + csrf: ctx.csrf, + }); return; } const ip = ctx.headers['x-forwarded-for'] || ctx.ip; - const email = ctx.request.body.email; - const password = ctx.request.body.password; + const { email, password } = ctx.request.body; try { await signupUser.call(ctx, email, password, ip); } catch (e) { @@ -66,12 +71,11 @@ export async function signup(ctx) { message: 'Thanks for signing up, we\'ve sent you an email to activate your account.', csrf: '', }); - return; } export async function forgot(ctx) { - const token = ctx.params.token; + const { token } = ctx.params; if (ctx.request.body.password) { if (ctx.request.body.password.length < 7) { @@ -87,7 +91,7 @@ export async function forgot(ctx) { if (user) { await updatePassword(user.userId, ctx.request.body.password); const reset = await models.reset.findById(token); - //reset.destroy(); + reset.destroy(); await setupSession.call(ctx, user); ctx.statsd.incr('auth.reset.success', 1); ctx.redirect('/'); @@ -104,11 +108,10 @@ export async function forgot(ctx) { return; } await ctx.render('forgot', { csrf: ctx.csrf, token }); - return; } else if (ctx.request.body.email) { ctx.assertCSRF(ctx.request.body); try { - const email = ctx.request.body.email; + const { email } = ctx.request.body; await sendResetToken.call(ctx, email); ctx.statsd.incr('auth.reset.request', 1); await ctx.render('forgot', { @@ -136,7 +139,7 @@ export async function logout(ctx) { export async function activate(ctx) { - const code = ctx.params.code; + const { code } = ctx.params; if (await activateUser.call(ctx, code)) { ctx.statsd.incr('auth.activation', 1); ctx.redirect('/'); diff --git a/worker.js b/worker.js index f6bbce0..3f14896 100644 --- a/worker.js +++ b/worker.js @@ -3,8 +3,6 @@ import kue from 'kue'; import raven from 'raven'; import debuglog from 'debug'; -import models from '../models'; - const debug = debuglog('hostr:worker'); raven.config(process.env.SENTRY_DSN).install();