diff --git a/server/src/routes/proxy.ts b/server/src/routes/proxy.ts index ff20d31e..5e3383ee 100644 --- a/server/src/routes/proxy.ts +++ b/server/src/routes/proxy.ts @@ -1,6 +1,6 @@ import { Router, Request, Response } from 'express'; import User from '../models/User'; -import { hashPassword } from '../utils/auth'; +import { encrypt, decrypt } from '../utils/auth'; import { requireSignIn } from '../middlewares/auth'; export const router = Router(); @@ -30,19 +30,20 @@ router.post('/config', requireSignIn, async (req: AuthenticatedRequest, res: Res return res.status(400).send('Proxy URL is required'); } - let hashedProxyUsername: string | null = null; - let hashedProxyPassword: string | null = null; + const encryptedProxyUrl = encrypt(server_url); + let encryptedProxyUsername: string | null = null; + let encryptedProxyPassword: string | null = null; if (username && password) { - hashedProxyUsername = await hashPassword(username); - hashedProxyPassword = await hashPassword(password); + encryptedProxyUsername = encrypt(username); + encryptedProxyPassword = encrypt(password); } else if (username && !password) { return res.status(400).send('Proxy password is required when proxy username is provided'); } - user.proxy_url = server_url; - user.proxy_username = hashedProxyUsername; - user.proxy_password = hashedProxyPassword; + user.proxy_url = encryptedProxyUrl; + user.proxy_username = encryptedProxyUsername; + user.proxy_password = encryptedProxyPassword; await user.save(); @@ -51,3 +52,24 @@ router.post('/config', requireSignIn, async (req: AuthenticatedRequest, res: Res res.status(500).send(`Could not save proxy configuration - ${error.message}`); } }); + +// TODO: Move this from here +export const getDecryptedProxyConfig = async (userId: string) => { + const user = await User.findByPk(userId, { + attributes: ['proxy_url', 'proxy_username', 'proxy_password'], + }); + + if (!user) { + throw new Error('User not found'); + } + + const decryptedProxyUrl = user.proxy_url ? decrypt(user.proxy_url) : null; + const decryptedProxyUsername = user.proxy_username ? decrypt(user.proxy_username) : null; + const decryptedProxyPassword = user.proxy_password ? decrypt(user.proxy_password) : null; + + return { + proxy_url: decryptedProxyUrl, + proxy_username: decryptedProxyUsername, + proxy_password: decryptedProxyPassword, + }; +}; diff --git a/server/src/routes/record.ts b/server/src/routes/record.ts index 0f12f5c9..adbd1afd 100644 --- a/server/src/routes/record.ts +++ b/server/src/routes/record.ts @@ -14,6 +14,7 @@ import { import { chromium } from 'playwright-extra'; import stealthPlugin from 'puppeteer-extra-plugin-stealth'; import logger from "../logger"; +import { getDecryptedProxyConfig } from './proxy'; export const router = Router(); chromium.use(stealthPlugin()); @@ -30,11 +31,27 @@ router.all('/', (req, res, next) => { * GET endpoint for starting the remote browser recording session. * returns session's id */ -router.get('/start', (req, res) => { +router.get('/start', async (req, res) => { + const proxyConfig = await getDecryptedProxyConfig(req.user.id); + // Prepare the proxy options dynamically based on the user's proxy configuration + let proxyOptions: any = {}; // Default to no proxy + + if (proxyConfig.proxy_url) { + // Set the server, and if username & password exist, set those as well + proxyOptions = { + server: proxyConfig.proxy_url, + ...(proxyConfig.proxy_username && proxyConfig.proxy_password && { + username: proxyConfig.proxy_username, + password: proxyConfig.proxy_password, + }), + }; + } + const id = initializeRemoteBrowserForRecording({ browser: chromium, launchOptions: { headless: true, + proxy: proxyOptions.server ? proxyOptions : undefined, } }); return res.send(id); diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 3d427169..46ca5d85 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -10,10 +10,11 @@ import { chromium } from "playwright"; import { browserPool } from "../server"; import fs from "fs"; import { uuid } from "uuidv4"; -import { workflowQueue } from '../workflow-management/scheduler'; +// import { workflowQueue } from '../workflow-management/scheduler'; import moment from 'moment-timezone'; import cron from 'node-cron'; import { googleSheetUpdateTasks, processGoogleSheetUpdates } from '../workflow-management/integrations/gsheet'; +import { getDecryptedProxyConfig } from './proxy'; export const router = Router(); @@ -85,9 +86,25 @@ router.delete('/runs/:fileName', async (req, res) => { */ router.put('/runs/:fileName', async (req, res) => { try { + const proxyConfig = await getDecryptedProxyConfig(req.user.id); + let proxyOptions: any = {}; + + if (proxyConfig.proxy_url) { + proxyOptions = { + server: proxyConfig.proxy_url, + ...(proxyConfig.proxy_username && proxyConfig.proxy_password && { + username: proxyConfig.proxy_username, + password: proxyConfig.proxy_password, + }), + }; + } + const id = createRemoteBrowserForRun({ browser: chromium, - launchOptions: { headless: true } + launchOptions: { + headless: true, + proxy: proxyOptions.server ? proxyOptions : undefined, + } }); const runId = uuid(); @@ -262,16 +279,16 @@ router.put('/schedule/:fileName/', async (req, res) => { const runId = uuid(); - await workflowQueue.add( - 'run workflow', - { fileName, runId }, - { - repeat: { - pattern: cronExpression, - tz: timezone - } - } - ); + // await workflowQueue.add( + // 'run workflow', + // { fileName, runId }, + // { + // repeat: { + // pattern: cronExpression, + // tz: timezone + // } + // } + // ); res.status(200).json({ message: 'success', diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index 96e121f9..b1f6850f 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -1,4 +1,6 @@ import bcrypt from "bcrypt"; +import crypto from 'crypto'; +import { getEnvVariable } from './env'; export const hashPassword = (password: string): Promise => { return new Promise((resolve, reject) => { @@ -19,4 +21,25 @@ export const hashPassword = (password: string): Promise => { // password from frontend and hash from database export const comparePassword = (password: string, hash: string): Promise => { return bcrypt.compare(password, hash) -} \ No newline at end of file +} + +export const encrypt = (text: string): string => { + const ivLength = parseInt(getEnvVariable('IV_LENGTH'), 10); + const iv = crypto.randomBytes(ivLength); + const algorithm = getEnvVariable('ALGORITHM'); + const key = Buffer.from(getEnvVariable('ENCRYPTION_KEY'), 'hex'); + const cipher = crypto.createCipheriv(algorithm, key, iv); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return `${iv.toString('hex')}:${encrypted}`; +}; + +export const decrypt = (encryptedText: string): string => { + const [iv, encrypted] = encryptedText.split(':'); + const algorithm = getEnvVariable('ALGORITHM'); + const key = Buffer.from(getEnvVariable('ENCRYPTION_KEY'), 'hex'); + const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(iv, 'hex')); + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; +}; \ No newline at end of file diff --git a/server/src/utils/env.ts b/server/src/utils/env.ts new file mode 100644 index 00000000..b4808b0c --- /dev/null +++ b/server/src/utils/env.ts @@ -0,0 +1,8 @@ +// Helper function to get environment variables and throw an error if they are not set +export const getEnvVariable = (key: string, defaultValue?: string): string => { + const value = process.env[key] || defaultValue; + if (!value) { + throw new Error(`Environment variable ${key} is not defined`); + } + return value; +}; \ No newline at end of file diff --git a/src/components/organisms/ProxyForm.tsx b/src/components/organisms/ProxyForm.tsx index 62869fc9..3c6b9a80 100644 --- a/src/components/organisms/ProxyForm.tsx +++ b/src/components/organisms/ProxyForm.tsx @@ -98,7 +98,9 @@ const ProxyForm: React.FC = () => { fullWidth required error={!!errors.server_url} - helperText={errors.server_url || 'e.g., http://proxy-server.com:8080'} + helperText={errors.server_url || `Proxy to be used for all requests. + HTTP and SOCKS proxies are supported, for example http://myproxy.com:3128 or + socks5://myproxy.com:3128. Short form myproxy.com:3128 is considered an HTTP proxy.`} />