diff --git a/.env.example b/.env.example index 13bde7e..a9c01e8 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,10 @@ MATRIX_USERNAME=@omeglebot:jzitnik.dev MATRIX_PASSWORD= # Selenium Grid configuration -GRID_DASHBOARD_PORT=4444 # Bare in mind that by default there is no password for the VNC connection. So this shouldn't be exposed to the public. +GRID_DASHBOARD_PORT=4444 # Bare in mind that by default there is no password for the VNC connection. + +# The URL of the Selenium Grid hub. This is used in a link for the user that sends `!connect` command to manually pass the Cloudflare challange. +SELENIUM_GRID_URL=http://localhost:4444 # Miscellaneous settings CF_WAIT_TIME=60 # The time you have to manually pass the Cloudflare challenge in seconds. diff --git a/bot/src/config.rs b/bot/src/config.rs index c9a8250..9c6e5dd 100644 --- a/bot/src/config.rs +++ b/bot/src/config.rs @@ -25,6 +25,8 @@ pub struct MatrixConfig { pub struct OmegleConfig { #[serde(default)] pub websocket_url: String, + #[serde(default)] + pub selenium_url: String, } impl Config { @@ -33,11 +35,13 @@ impl Config { let username = env::var("MATRIX_USERNAME").unwrap_or_default(); let password = env::var("MATRIX_PASSWORD").unwrap_or_default(); let websocket_url = env::var("OMEGLE_WEBSOCKET_URL").unwrap_or_default(); + let selenium_url = env::var("SELENIUM_URL").unwrap_or_default(); if !homeserver.is_empty() || !username.is_empty() || !password.is_empty() || !websocket_url.is_empty() + || !selenium_url.is_empty() { return Ok(Config { matrix: MatrixConfig { @@ -45,7 +49,10 @@ impl Config { username, password, }, - omegle: OmegleConfig { websocket_url }, + omegle: OmegleConfig { + websocket_url, + selenium_url, + }, }); } diff --git a/bot/src/matrix/client.rs b/bot/src/matrix/client.rs index 4864ca7..2de8d43 100644 --- a/bot/src/matrix/client.rs +++ b/bot/src/matrix/client.rs @@ -33,8 +33,8 @@ pub enum BotCommand { Connect { msg_id: OwnedEventId }, Match { prefer_same_country: bool, user_id: String, msg_id: OwnedEventId }, Skip { user_id: String, msg_id: OwnedEventId }, - Pause, - Disconnect, + Pause { reply_to: OwnedEventId }, + Disconnect { reply_to: OwnedEventId }, SendMessage(String), SendTyping(bool), } @@ -78,6 +78,24 @@ async fn edit_status(room: &Room, event_id: OwnedEventId, text: &str, emoji: &st Ok(resp.event_id) } +async fn send_reply(room: &Room, event_id: OwnedEventId, text: &str) -> Result { + let mut content = RoomMessageEventContent::text_plain(text); + content.relates_to = Some(Relation::Reply { + in_reply_to: matrix_sdk::ruma::events::relation::InReplyTo::new(event_id), + }); + let resp = room.send(content).await?; + Ok(resp.event_id) +} + +async fn send_reply_html(room: &Room, event_id: OwnedEventId, text: &str, html: &str) -> Result { + let mut content = RoomMessageEventContent::text_html(text, html); + content.relates_to = Some(Relation::Reply { + in_reply_to: matrix_sdk::ruma::events::relation::InReplyTo::new(event_id), + }); + let resp = room.send(content).await?; + Ok(resp.event_id) +} + impl MatrixBot { pub async fn new(config: Config, db: Arc) -> Result { @@ -134,7 +152,7 @@ impl MatrixBot { if let MessageType::Text(text) = original.content.msgtype { let body = text.body.trim(); if body.starts_with('!') { - handle_command(body, &original.sender, &room, &db, &config, &handlers, &client).await; + handle_command(body, &original.sender, &room, &db, &config, &handlers, &client, original.event_id).await; } else if let Some(tx) = handlers.get(&room_id) { let _ = tx.send(BotCommand::SendMessage(body.to_string())).await; } @@ -175,6 +193,7 @@ async fn handle_command( config: &Config, handlers: &Arc>>, client: &Client, + reply_to: OwnedEventId, ) { let parts: Vec<&str> = body.split_whitespace().collect(); let cmd = parts[0]; @@ -185,24 +204,28 @@ async fn handle_command( "!help" => { let help_text = "Available commands:
\ !help - Show this help message
\ - !connect - Connect to Omgle WebSocket
\ + !connect - Connect to Omegle WebSocket
\ !match [--same-country] - Request a new match (uses your interests)
\ !skip - Skip current peer and automatch (uses your interests)
\ !stop - Skip current peer without automatching
\ - !disconnect - Disconnect from Omgle WebSocket
\ + !disconnect - Disconnect from Omegle WebSocket
\ !autoskip add/remove/list <CC> - Manage your automatic skipping list (global)
\ !interests add/remove/list/clear <interest> - Manage your interests (global)"; let plain = "Available commands:\n!help, !connect, !match, !skip, !stop, !pause, !disconnect, !autoskip, !interests"; - let content = RoomMessageEventContent::text_html(plain, help_text); - let _ = room.send(content).await; + let _ = send_reply_html(room, reply_to, plain, help_text).await; } "!connect" => { if handlers.contains_key(&room_id) { - let _ = send_info(room, "Already connected").await; + let _ = send_reply(room, reply_to, "Already connected").await; return; } - let msg_id = match send_info(room, "πŸ”„ Connecting to Omgle...").await { + let connecting_msg = if config.omegle.selenium_url.is_empty() { + "πŸ”„ Connecting to Omegle...".to_string() + } else { + format!("πŸ”„ Connecting to Omegle... (If needed, complete bot check at {})", config.omegle.selenium_url) + }; + let msg_id = match send_reply(room, reply_to, &connecting_msg).await { Ok(id) => id, Err(_) => return, }; @@ -217,29 +240,29 @@ async fn handle_command( let client_clone = client.clone(); tokio::spawn(async move { - let mut omgle_client = WsOmegleClient::new(); - if let Err(e) = omgle_client.connect(&config_clone.omegle.websocket_url).await { + let mut omegle_client = WsOmegleClient::new(); + if let Err(e) = omegle_client.connect(&config_clone.omegle.websocket_url).await { let _ = edit_info(&room_clone, msg_id, &format!("❌ Failed to connect: {}", e)).await; handlers_clone.remove(&room_id); return; } let _ = tx.send(BotCommand::Connect { msg_id }).await; - let _ = omgle_client.request_people_online().await; + let _ = omegle_client.request_people_online().await; - handle_omgle_session(omgle_client, rx, room_clone, db_struct_clone, room_id.clone(), handlers_clone, client_clone).await; + handle_omegle_session(omegle_client, rx, room_clone, db_struct_clone, room_id.clone(), handlers_clone, client_clone).await; }); } "!match" => { if let Some(tx) = handlers.get(&room_id) { let prefer_same_country = parts.contains(&"--same-country"); - let msg_id = match send_info(room, "πŸ” Matching...").await { + let msg_id = match send_reply(room, reply_to, "πŸ” Matching...").await { Ok(id) => id, Err(_) => return, }; let _ = tx.send(BotCommand::Match { prefer_same_country, user_id, msg_id }).await; } else { - let _ = send_info(room, "❌ Not connected to WebSocket. Use !connect first.").await; + let _ = send_reply(room, reply_to, "❌ Not connected to WebSocket. Use !connect first.").await; } } "!interests" => { @@ -251,49 +274,49 @@ async fn handle_command( config.interests.push(i.to_string()); } db.update_user_config(&config).unwrap(); - let _ = send_info(room, &format!("βœ… Added interests: {:?}", &parts[2..])).await; + let _ = send_reply(room, reply_to, &format!("βœ… Added interests: {:?}", &parts[2..])).await; } "remove" => { config.interests.retain(|i| !parts[2..].contains(&i.as_str())); db.update_user_config(&config).unwrap(); - let _ = send_info(room, &format!("πŸ—‘οΈ Removed interests: {:?}", &parts[2..])).await; + let _ = send_reply(room, reply_to, &format!("πŸ—‘οΈ Removed interests: {:?}", &parts[2..])).await; } "list" => { - let _ = send_info(room, &format!("πŸ“ Your interests: {:?}", config.interests)).await; + let _ = send_reply(room, reply_to, &format!("πŸ“ Your interests: {:?}", config.interests)).await; } "clear" => { config.interests.clear(); db.update_user_config(&config).unwrap(); - let _ = send_info(room, "✨ Interests cleared").await; + let _ = send_reply(room, reply_to, "✨ Interests cleared").await; } _ => { - let _ = send_info(room, "❌ Invalid !interests subcommand.").await; + let _ = send_reply(room, reply_to, "❌ Invalid !interests subcommand.").await; } } } "!skip" => { if let Some(tx) = handlers.get(&room_id) { - let msg_id = match send_info(room, "⏩ Skipping...").await { + let msg_id = match send_reply(room, reply_to, "⏩ Skipping...").await { Ok(id) => id, Err(_) => return, }; let _ = tx.send(BotCommand::Skip { user_id, msg_id }).await; } else { - let _ = send_info(room, "❌ Not connected to WebSocket.").await; + let _ = send_reply(room, reply_to, "❌ Not connected to WebSocket.").await; } } "!stop" => { if let Some(tx) = handlers.get(&room_id) { - let _ = tx.send(BotCommand::Pause).await; + let _ = tx.send(BotCommand::Pause { reply_to }).await; } else { - let _ = send_info(room, "❌ Not connected to WebSocket.").await; + let _ = send_reply(room, reply_to, "❌ Not connected to WebSocket.").await; } } "!disconnect" => { if let Some(tx) = handlers.get(&room_id) { - let _ = tx.send(BotCommand::Disconnect).await; + let _ = tx.send(BotCommand::Disconnect { reply_to }).await; } else { - let _ = send_info(room, "❌ Not connected to WebSocket.").await; + let _ = send_reply(room, reply_to, "❌ Not connected to WebSocket.").await; } } "!autoskip" => { @@ -305,29 +328,29 @@ async fn handle_command( config.autoskip_countries.push(c.to_uppercase()); } db.update_user_config(&config).unwrap(); - let _ = send_info(room, &format!("βœ… Added to your skip list: {:?}", &parts[2..])).await; + let _ = send_reply(room, reply_to, &format!("βœ… Added to your skip list: {:?}", &parts[2..])).await; } "remove" => { config.autoskip_countries.retain(|c| !parts[2..].contains(&c.as_str())); db.update_user_config(&config).unwrap(); - let _ = send_info(room, &format!("πŸ—‘οΈ Removed from your skip list: {:?}", &parts[2..])).await; + let _ = send_reply(room, reply_to, &format!("πŸ—‘οΈ Removed from your skip list: {:?}", &parts[2..])).await; } "list" => { - let _ = send_info(room, &format!("πŸ“ Your auto-skip countries: {:?}", config.autoskip_countries)).await; + let _ = send_reply(room, reply_to, &format!("πŸ“ Your auto-skip countries: {:?}", config.autoskip_countries)).await; } _ => { - let _ = send_info(room, "❌ Invalid !autoskip subcommand.").await; + let _ = send_reply(room, reply_to, "❌ Invalid !autoskip subcommand.").await; } } } _ => { - let _ = send_info(room, &format!("❌ Invalid command: {}. Type !help for a list of commands.", cmd)).await; + let _ = send_reply(room, reply_to, &format!("❌ Invalid command: {}. Type !help for a list of commands.", cmd)).await; } } } -async fn handle_omgle_session( - mut omgle: WsOmegleClient, +async fn handle_omegle_session( + mut omegle: WsOmegleClient, mut rx: mpsc::Receiver, room: Room, db: Arc, @@ -350,17 +373,17 @@ async fn handle_omgle_session( cmd = rx.recv() => { match cmd { Some(BotCommand::Connect { msg_id }) => { - let _ = edit_info(&room, msg_id, "βœ… Connected to Omgle").await; + let _ = edit_info(&room, msg_id, "βœ… Connected to Omegle").await; }, Some(BotCommand::Match { prefer_same_country, user_id, msg_id }) => { if peer_connected { - let _ = omgle.disconnect_peer().await; + let _ = omegle.disconnect_peer().await; } active_user_id = Some(user_id.clone()); last_prefer_same_country = prefer_same_country; pending_msg_id = Some(msg_id); let user_config = db.get_user_config(&user_id).unwrap(); - let _ = omgle.request_match(prefer_same_country, user_config.interests).await; + let _ = omegle.request_match(prefer_same_country, user_config.interests).await; peer_connected = false; local_typing_active = false; @@ -377,14 +400,14 @@ async fn handle_omgle_session( active_user_id = Some(user_id.clone()); pending_msg_id = Some(msg_id); let user_config = db.get_user_config(&user_id).unwrap(); - let _ = omgle.disconnect_peer().await; + let _ = omegle.disconnect_peer().await; peer_connected = false; local_typing_active = false; typing_active = false; let _ = room.typing_notice(false).await; - let _ = omgle.request_match(last_prefer_same_country, user_config.interests).await; + let _ = omegle.request_match(last_prefer_same_country, user_config.interests).await; let mut room_state = db.get_room_state(&room_id).unwrap(); room_state.active_user_id = Some(user_id); @@ -393,44 +416,44 @@ async fn handle_omgle_session( let _ = edit_info(&room, msg_id, "❌ No stranger to skip.").await; } }, - Some(BotCommand::Pause) => { + Some(BotCommand::Pause { reply_to }) => { if peer_connected { - let _ = send_info(&room, "⏸️ Paused (Skipped peer)").await; - let _ = omgle.disconnect_peer().await; + let _ = send_reply(&room, reply_to, "⏸️ Paused (Skipped peer)").await; + let _ = omegle.disconnect_peer().await; peer_connected = false; local_typing_active = false; typing_active = false; let _ = room.typing_notice(false).await; } else { - let _ = send_info(&room, "❌ No stranger to pause.").await; + let _ = send_reply(&room, reply_to, "❌ No stranger to pause.").await; } }, - Some(BotCommand::Disconnect) => { - let _ = omgle.disconnect().await; + Some(BotCommand::Disconnect { reply_to }) => { + let _ = omegle.disconnect().await; let mut room_state = db.get_room_state(&room_id).unwrap(); room_state.is_connected = false; let _ = db.update_room_state(&room_state); - let _ = send_info(&room, "πŸ”Œ Disconnected from Omgle").await; + let _ = send_reply(&room, reply_to, "πŸ”Œ Disconnected from Omegle").await; handlers.remove(&room_id); return; }, Some(BotCommand::SendMessage(text)) => { if peer_connected { local_typing_active = false; - let _ = omgle.send_message(&text).await; + let _ = omegle.send_message(&text).await; message_count += 1; } }, Some(BotCommand::SendTyping(typing)) => { if peer_connected { local_typing_active = typing; - let _ = omgle.send_typing(typing).await; + let _ = omegle.send_typing(typing).await; } }, None => break, } } - ev = omgle.next_event() => { + ev = omegle.next_event() => { match ev { Ok(Some(msg)) => { match msg.channel.as_str() { @@ -454,14 +477,14 @@ async fn handle_omgle_session( let config = db.get_user_config(user_id).unwrap(); if config.autoskip_countries.contains(&data.country.to_uppercase()) { pending_msg_id = send_info(&room, "⏩ Auto-skipping...").await.ok(); - let _ = omgle.disconnect_peer().await; + let _ = omegle.disconnect_peer().await; peer_connected = false; local_typing_active = false; typing_active = false; let _ = room.typing_notice(false).await; - let _ = omgle.request_match(last_prefer_same_country, config.interests).await; + let _ = omegle.request_match(last_prefer_same_country, config.interests).await; } } } @@ -496,7 +519,7 @@ async fn handle_omgle_session( if let Some(user_id) = &active_user_id { pending_msg_id = send_info(&room, "⏩ Automatching...").await.ok(); let config = db.get_user_config(user_id).unwrap(); - let _ = omgle.request_match(last_prefer_same_country, config.interests).await; + let _ = omegle.request_match(last_prefer_same_country, config.interests).await; } } } @@ -515,11 +538,11 @@ async fn handle_omgle_session( } if local_typing_active && peer_connected { - let _ = omgle.send_typing(true).await; + let _ = omegle.send_typing(true).await; } if last_people_online_request.elapsed().as_secs() >= 60 { - let _ = omgle.request_people_online().await; + let _ = omegle.request_people_online().await; last_people_online_request = std::time::Instant::now(); } } diff --git a/bot/src/omegle/client.rs b/bot/src/omegle/client.rs index 85a424a..45674c5 100644 --- a/bot/src/omegle/client.rs +++ b/bot/src/omegle/client.rs @@ -22,7 +22,18 @@ impl WsOmegleClient { impl OmegleProvider for WsOmegleClient { async fn connect(&mut self, url: &str) -> Result<()> { let (ws_stream, _) = connect_async(url).await?; - self.ws = Some(ws_stream); + + let mut ws = ws_stream; + let msg = ws.next().await.ok_or_else(|| anyhow!("Connection closed before receiving SUCCESS"))??; + + match msg { + Message::Text(text) if text == "SUCCESS" => {} + Message::Text(text) => return Err(anyhow!("Unexpected message: {}", text)), + Message::Close(_) => return Err(anyhow!("Server closed connection")), + _ => return Err(anyhow!("Unexpected message type")), + } + + self.ws = Some(ws); Ok(()) } diff --git a/docker-compose.yaml b/docker-compose.yaml index 203b6a0..eebf342 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ services: # Selenium Grid with Chromium (or Chrome) selenium: - image: selenium/standalone-chromium:4.43.0-20260404 + build: ./selenium-patch container_name: selenium shm_size: 2gb ports: @@ -40,6 +40,7 @@ services: - MATRIX_USERNAME=${MATRIX_USERNAME} - MATRIX_PASSWORD=${MATRIX_PASSWORD} - OMEGLE_WEBSOCKET_URL=ws://omegle-proxy:8765 + - SELENIUM_URL=${SELENIUM_GRID_URL} - DB_PATH=/data/omegle.db volumes: - omegle-bot-data:/data diff --git a/proxy/Dockerfile b/proxy/Dockerfile index 13f8929..7da3d07 100644 --- a/proxy/Dockerfile +++ b/proxy/Dockerfile @@ -1,9 +1,10 @@ FROM python:3.12-slim -RUN pip install --no-cache-dir setuptools selenium websockets - WORKDIR /app -COPY main.py . +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . CMD ["python", "main.py"] diff --git a/proxy/__init__.py b/proxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/proxy/config.py b/proxy/config.py new file mode 100644 index 0000000..9ac15fb --- /dev/null +++ b/proxy/config.py @@ -0,0 +1,11 @@ +import os + +TARGET_URL = os.getenv("TARGET_URL", "https://omegleweb.io/") +TARGET_WS_URL = os.getenv( + "TARGET_WS_URL", "wss://omegleweb.io:8443/socket.io/?EIO=4&transport=websocket" +) +LOCAL_HOST = os.getenv("LOCAL_HOST", "0.0.0.0") +LOCAL_PORT = int(os.getenv("LOCAL_PORT", "8765")) +HEADLESS = os.getenv("HEADLESS", "false").lower() in ("true", "1", "yes") +CF_WAIT_TIME = int(os.getenv("CF_WAIT_TIME", "60")) +SELENIUM_URL = os.getenv("SELENIUM_URL", "http://localhost:4444") diff --git a/proxy/main.py b/proxy/main.py index 8aec2fd..0cfa97d 100644 --- a/proxy/main.py +++ b/proxy/main.py @@ -1,182 +1,24 @@ -import os -import time import asyncio +import logging import websockets -from datetime import datetime -from selenium import webdriver -from selenium.webdriver.chrome.options import Options -from selenium.webdriver.chrome.service import Service -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -TARGET_URL = os.getenv("TARGET_URL", "https://omegleweb.io/") -TARGET_WS_URL = os.getenv("TARGET_WS_URL", "wss://omegleweb.io:8443/socket.io/?EIO=4&transport=websocket") -LOCAL_HOST = os.getenv("LOCAL_HOST", "0.0.0.0") -LOCAL_PORT = int(os.getenv("LOCAL_PORT", "8765")) -HEADLESS = os.getenv("HEADLESS", "false").lower() in ("true", "1", "yes") -CF_WAIT_TIME = int(os.getenv("CF_WAIT_TIME", "60")) -SELENIUM_URL = os.getenv("SELENIUM_URL", "http://localhost:4444") +from config import LOCAL_HOST, LOCAL_PORT +from websocket_client import handle_connection -credentials = { - "user_agent": None, - "cookies": None -} +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S", +) -def stealth_js(driver): - driver.execute_script(""" - Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); - Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]}); - Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']}); - window.chrome = {runtime: {}}; - const originalQuery = window.navigator.permissions.query; - window.navigator.permissions.query = (parameters) => ( - parameters.name === 'notifications' ? - Promise.resolve({state: Notification.permission}) : - originalQuery(parameters) - ); - Object.defineProperty(navigator, 'permissions', {get: () => window.navigator.permissions}); - """) -def extract_credentials(): - print("\n" + "="*50) - print("[*] PHASE 1: SELENIUM EXTRACTION") - print("="*50) - print("[*] Launching Chrome browser...") +async def main(): + await websockets.serve(handle_connection, LOCAL_HOST, LOCAL_PORT) + await asyncio.Future() - options = Options() - if HEADLESS: - options.add_argument("--headless=new") - options.add_argument("--no-sandbox") - options.add_argument("--disable-dev-shm-usage") - options.add_argument("--disable-gpu") - options.add_argument("--disable-software-rasterizer") - options.add_argument("--disable-extensions") - options.add_argument("--disable-blink-features=AutomationControlled") - options.add_argument("--disable-setuid-sandbox") - options.add_argument("--disable-background-networking") - options.add_argument("--disable-default-apps") - options.add_argument("--disable-sync") - options.add_argument("--disable-translate") - options.add_argument("--metrics-recording-only") - options.add_argument("--mute-audio") - options.add_argument("--no-first-run") - options.add_argument("--safebrowsing-disable-auto-update") - options.add_argument("--ignore-certificate-errors") - options.add_argument("--ignore-ssl-errors") - options.add_argument("--user-data-dir=/tmp/chrome-data") - options.add_argument("--disable-features=IsolateOrigins,site-per-process") - - driver = webdriver.Remote( - command_executor=SELENIUM_URL + "/wd/hub", - options=options - ) - - stealth_js(driver) +if __name__ == "__main__": try: - print("[*] Navigating to OmegleWeb homepage...") - driver.get(TARGET_URL) - - wait = WebDriverWait(driver, CF_WAIT_TIME) - wait.until(EC.presence_of_element_located((By.ID, "logo"))) - - print("[*] Time's up! Extracting the goods...") - user_agent = driver.execute_script("return navigator.userAgent;") - selenium_cookies = driver.get_cookies() - cookie_string = "; ".join([f"{c['name']}={c['value']}" for c in selenium_cookies]) - - credentials["user_agent"] = user_agent - credentials["cookies"] = cookie_string - - print("[+] Extraction successful!") - return True - except Exception as e: - print(f"[!] Extraction failed: {e}") - return False - finally: - print("[*] Closing browser...") - driver.quit() - -async def start_bridge(): - async def bridge_handler(local_client): - print(f"\n[{datetime.now().strftime('%H:%M:%S')}] [+] Local client connected to the bridge!") - - while True: - headers = { - "User-Agent": credentials["user_agent"], - "Cookie": credentials["cookies"] - } - - try: - print(f"[*] Attempting tunnel to OmegleWeb...") - async with websockets.connect(TARGET_WS_URL, additional_headers=headers) as target_server: - print("[+] Tunnel established! Relaying messages...") - - async def forward_local_to_target(): - async for message in local_client: - print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] [Local -> Omegle]: {message}") - await target_server.send(message) - - async def forward_target_to_local(): - async for message in target_server: - print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] [Omegle -> Local]: {message}") - await local_client.send(message) - - t1 = asyncio.create_task(forward_local_to_target()) - t2 = asyncio.create_task(forward_target_to_local()) - - done, pending = await asyncio.wait( - [t1, t2], - return_when=asyncio.FIRST_COMPLETED - ) - - for task in pending: - task.cancel() - - if t1 in done and t1.exception() is None: - print("[-] Local client disconnected.") - break - - print("[-] OmegleWeb connection closed.") - break - - except websockets.exceptions.InvalidStatusCode as e: - if e.status_code == 403: - print(f"[!] HTTP 403 Forbidden: Cookies likely expired. Refreshing...") - loop = asyncio.get_running_loop() - success = await loop.run_in_executor(None, extract_credentials) - if not success: - print("[!] Failed to refresh cookies. Retrying in 5s...") - await asyncio.sleep(5) - continue - else: - print(f"[!] Bridge error (Status {e.status_code}): {e}") - break - except websockets.exceptions.ConnectionClosed: - print("[-] OmegleWeb disconnected.") - break - except Exception as e: - print(f"[!] Bridge error: {type(e).__name__}: {e}") - break - - print(f"[*] Starting local WebSocket proxy on ws://{LOCAL_HOST}:{LOCAL_PORT}") - async with websockets.serve(bridge_handler, LOCAL_HOST, LOCAL_PORT): - await asyncio.Future() - -def main(): - if not extract_credentials(): - print("[!] Initial extraction failed. Exiting.") - return - - print("\n" + "="*50) - print("[*] PHASE 2: LAUNCH THE WEBSOCKET PROXY") - print("="*50) - - try: - asyncio.run(start_bridge()) + asyncio.run(main()) except KeyboardInterrupt: - print("\n[*] Shutting down...") - -if __name__ == '__main__': - main() + logging.info("Shutting down...") diff --git a/proxy/requirements.txt b/proxy/requirements.txt new file mode 100644 index 0000000..88449cd --- /dev/null +++ b/proxy/requirements.txt @@ -0,0 +1,2 @@ +selenium==4.43.0 +websockets==16.0 diff --git a/proxy/selenium_utils.py b/proxy/selenium_utils.py new file mode 100644 index 0000000..ced1d6a --- /dev/null +++ b/proxy/selenium_utils.py @@ -0,0 +1,86 @@ +import logging +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from config import HEADLESS, CF_WAIT_TIME, SELENIUM_URL, TARGET_URL + +log = logging.getLogger(__name__) + + +def get_chrome_options() -> Options: + options = Options() + if HEADLESS: + options.add_argument("--headless=new") + options.add_argument("--no-sandbox") + options.add_argument("--disable-dev-shm-usage") + options.add_argument("--disable-gpu") + options.add_argument("--disable-software-rasterizer") + options.add_argument("--disable-extensions") + options.add_argument("--disable-blink-features=AutomationControlled") + options.add_argument("--disable-setuid-sandbox") + options.add_argument("--disable-background-networking") + options.add_argument("--disable-default-apps") + options.add_argument("--disable-sync") + options.add_argument("--disable-translate") + options.add_argument("--metrics-recording-only") + options.add_argument("--mute-audio") + options.add_argument("--no-first-run") + options.add_argument("--safebrowsing-disable-auto-update") + options.add_argument("--ignore-certificate-errors") + options.add_argument("--ignore-ssl-errors") + options.add_argument("--user-data-dir=/tmp/chrome-data") + options.add_argument("--disable-features=IsolateOrigins,site-per-process") + return options + + +def apply_stealth_scripts(driver: webdriver.Remote) -> None: + driver.execute_script(""" + Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); + Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]}); + Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']}); + window.chrome = {runtime: {}}; + + const originalQuery = window.navigator.permissions.query; + window.navigator.permissions.query = (parameters) => ( + parameters.name === 'notifications' + ? Promise.resolve({state: Notification.permission}) + : originalQuery(parameters) + ); + Object.defineProperty(navigator, 'permissions', {get: () => window.navigator.permissions}); + """) + + +def fetch_fresh_credentials() -> tuple[str, str] | None: + log.info("Starting Selenium to fetch fresh credentials...") + + driver = None + try: + driver = webdriver.Remote( + command_executor=f"{SELENIUM_URL}/wd/hub", + options=get_chrome_options(), + ) + apply_stealth_scripts(driver) + driver.get(TARGET_URL) + + wait = WebDriverWait(driver, CF_WAIT_TIME) + wait.until(EC.presence_of_element_located((By.ID, "logo"))) + + user_agent = driver.execute_script("return navigator.userAgent;") + selenium_cookies = driver.get_cookies() + cookies = "; ".join( + f"{c['name']}={c['value']}" + for c in selenium_cookies + ) + log.info("Successfully extracted credentials via Selenium") + return (user_agent, cookies) + + except Exception as e: + log.error(f"Failed to extract credentials: {e}") + return None + + finally: + if driver is not None: + driver.quit() diff --git a/proxy/websocket_client.py b/proxy/websocket_client.py new file mode 100644 index 0000000..e1f59d1 --- /dev/null +++ b/proxy/websocket_client.py @@ -0,0 +1,85 @@ +import asyncio +import logging +import websockets + +from config import TARGET_WS_URL, LOCAL_HOST, LOCAL_PORT +from selenium_utils import fetch_fresh_credentials + +log = logging.getLogger(__name__) + + +async def handle_connection(local_client): + log.info("Local client connected, fetching credentials...") + + result = await asyncio.get_running_loop().run_in_executor( + None, fetch_fresh_credentials + ) + if result is None: + log.error("Could not fetch credentials") + await local_client.close(1011, "Failed to bypass Cloudflare") + return + + user_agent, cookies = result + headers = { + "User-Agent": user_agent, + "Cookie": cookies, + } + + try: + log.info("Connecting to OmegleWeb...") + async with websockets.connect( + TARGET_WS_URL, + additional_headers=headers, + ) as target_server: + log.info("OmegleWeb connected! Notifying client...") + await local_client.send("SUCCESS") + log.info("Starting relay") + await relay_bidirectional(local_client, target_server) + + except websockets.exceptions.InvalidStatusCode as e: + log.error(f"Omegle connection failed (Status {e.status_code})") + await local_client.close(1011, f"Connection failed: {e.status_code}") + + except websockets.exceptions.ConnectionClosed: + log.info("OmegleWeb disconnected") + + except Exception as e: + log.error(f"Connection error: {type(e).__name__}: {e}") + await local_client.close(1011, str(e)) + + +async def relay_bidirectional(local_ws, target_ws): + async def forward_local(): + try: + async for message in local_ws: + log.debug(f"[Local -> Omegle]: {message}") + await target_ws.send(message) + except websockets.exceptions.ConnectionClosed: + log.info("Local client disconnected") + except Exception as e: + log.error(f"Forward local error: {e}") + + async def forward_remote(): + try: + async for message in target_ws: + log.debug(f"[Omegle -> Local]: {message}") + await local_ws.send(message) + except websockets.exceptions.ConnectionClosed: + log.info("OmegleWeb disconnected") + except Exception as e: + log.error(f"Forward remote error: {e}") + + await asyncio.gather( + forward_local(), + forward_remote(), + ) + + +async def main(): + log.info(f"Starting WebSocket proxy on ws://{LOCAL_HOST}:{LOCAL_PORT}") + await websockets.serve(handle_connection, LOCAL_HOST, LOCAL_PORT) + await asyncio.Future() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/selenium-patch/Dockerfile b/selenium-patch/Dockerfile new file mode 100644 index 0000000..4d42276 --- /dev/null +++ b/selenium-patch/Dockerfile @@ -0,0 +1,14 @@ +FROM selenium/standalone-chromium:4.43.0-20260404 + +USER root + +RUN apt-get update && apt-get install -y python3-pip +RUN pip3 install undetected-chromedriver setuptools --break-system-packages + +RUN echo 'from undetected_chromedriver.patcher import Patcher\n\ +Patcher(executable_path="/usr/bin/chromedriver").patch()\n\ +print("Chromedriver patched inside Docker successfully!")' > /tmp/patch.py + +RUN python3 /tmp/patch.py + +USER seluser