diff --git a/code/info.py b/code/info.py new file mode 100644 index 0000000..df3e022 --- /dev/null +++ b/code/info.py @@ -0,0 +1,565 @@ +#!/usr/bin/env python3 + +import asyncio +import json +import uuid +import requests +from urllib.parse import quote +import websockets +from datetime import datetime + +CONFERENCE_ID = "33734896687006" +CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}" +API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost" + +session = requests.Session() + +def log(msg, level="INFO"): + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + log_msg = f"[{timestamp}] [{level}] {msg}" + print(log_msg) + +def generate_uuid(): + return str(uuid.uuid4()) + +def get_connection_info(display_name): + url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/connection" + params = { + "next_gen_media_platform_allowed": "true", + "display_name": display_name, + "waiting_room_supported": "true" + } + + headers = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", + "Accept": "*/*", + "content-type": "application/json", + "Client-Instance-Id": generate_uuid(), + "X-Telemost-Client-Version": "187.1.0", + "idempotency-key": generate_uuid(), + "Origin": "https://telemost.yandex.ru", + "Referer": "https://telemost.yandex.ru/" + } + + log(f"GET {url}", "HTTP") + log(f"Params: {params}", "DEBUG") + + response = session.get(url, params=params, headers=headers) + response.raise_for_status() + + log(f"Response: {response.status_code}", "HTTP") + log(f"Cookies: {list(session.cookies.keys())}", "DEBUG") + + return response.json() + +def get_participants_list(peer_ids=None): + url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/request-states" + + headers = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", + "Accept": "*/*", + "content-type": "application/json", + "Client-Instance-Id": generate_uuid(), + "idempotency-key": generate_uuid(), + "Origin": "https://telemost.yandex.ru", + "Referer": "https://telemost.yandex.ru/" + } + + if peer_ids: + payload = { + "peers": [{"peer_id": pid} for pid in peer_ids], + "permissions": {}, + "conference": {"version": -1} + } + else: + payload = { + "peers": [], + "permissions": {}, + "conference": {"version": -1} + } + + log(f"POST {url}", "HTTP") + log(f"Payload: {json.dumps(payload)[:100]}...", "DEBUG") + + response = session.post(url, json=payload, headers=headers) + response.raise_for_status() + + log(f"Response: {response.status_code}", "HTTP") + + result = response.json() + log(f"Response body: {json.dumps(result)[:500]}...", "DEBUG") + + return result + +async def collect_webrtc_info(): + log(r""" + + WebRTC Full Information Collector + Complete conference & peer analysis + by zowue for olc + +""", "INFO") + + info = { + "connection": {}, + "participants": {}, + "webrtc": { + "ice": {}, + "sdp": {}, + "datachannel": {}, + "audio": {}, + "video": {} + }, + "server": {} + } + + log("[1/5] Getting connection info...") + try: + conn_info = get_connection_info("InfoCollector") + + cookies_dict = session.cookies.get_dict() + if cookies_dict: + log(f" :P Cookies: {', '.join(cookies_dict.keys())}", "INFO") + + info["connection"] = { + "connection_type": conn_info.get("connection_type"), + "uri": conn_info.get("uri"), + "room_id": conn_info.get("room_id"), + "safe_room_id": conn_info.get("safe_room_id"), + "peer_id": conn_info.get("peer_id"), + "session_id": conn_info.get("session_id"), + "peer_session_id": conn_info.get("peer_session_id"), + "expiration_time": conn_info.get("expiration_time"), + "conference_limit": conn_info.get("conference_limit"), + "media_platform": conn_info.get("media_platform") + } + + client_config = conn_info.get("client_configuration", {}) + info["connection"]["client_config"] = { + "media_server_url": client_config.get("media_server_url"), + "service_name": client_config.get("service_name"), + "session_timeout_ms": client_config.get("goloom_session_open_ms"), + "reconnect_wait_ms": client_config.get("wait_time_to_reconnect_ms") + } + + log(f" :P Room: {info['connection']['room_id']}") + log(f" :P Peer: {info['connection']['peer_id']}") + log(f" :P Limit: {info['connection']['conference_limit']}") + except Exception as e: + log(f" X Error: {e}", "ERROR") + return info + + log("\n[2/5] Connecting to WebSocket...") + try: + ws_url = client_config.get("media_server_url") + log(f" -> WS URL: {ws_url}", "DEBUG") + ws = await websockets.connect(ws_url) + + hello_msg = { + "uid": generate_uuid(), + "hello": { + "participantMeta": {"name": "InfoCollector", "role": "SPEAKER", "sendAudio": False, "sendVideo": False}, + "participantAttributes": {"name": "InfoCollector", "role": "SPEAKER"}, + "sendAudio": False, + "sendVideo": False, + "sendSharing": False, + "participantId": conn_info["peer_id"], + "roomId": conn_info["room_id"], + "serviceName": "telemost", + "credentials": conn_info["credentials"], + "capabilitiesOffer": { + "offerAnswerMode": ["SEPARATE"], + "initialSubscriberOffer": ["ON_HELLO"], + "slotsMode": ["FROM_CONTROLLER"], + "simulcastMode": ["STATIC"], + "selfVadStatus": ["FROM_SERVER"], + "dataChannelSharing": ["TO_RTP"] + }, + "sdkInfo": {"implementation": "python", "version": "1.0.0", "userAgent": "InfoCollector"}, + "sdkInitializationId": generate_uuid(), + "disablePublisher": False, + "disableSubscriber": False + } + } + + await ws.send(json.dumps(hello_msg)) + log(" :P Connected") + log(f" -> Sent hello message", "DEBUG") + + log("\n[3/5] Collecting participants...") + info["participants"]["list"] = [] + collected_peer_ids = set() + + log("\n[4/5] Collecting WebRTC details...") + + for i in range(25): + try: + data = json.loads(await asyncio.wait_for(ws.recv(), timeout=1.0)) + + msg_type = next((k for k in data.keys() if k != "uid"), "unknown") + log(f" <- WS message #{i+1}: {msg_type}", "WS") + + if "updateDescription" in data: + update_desc = data["updateDescription"] + log(f" -> updateDescription: {json.dumps(update_desc)[:200]}...", "DEBUG") + + if "description" in update_desc: + for desc_item in update_desc["description"]: + peer_id = desc_item.get("id") + + if peer_id: + log(f" -> Found peer in updateDescription: {peer_id}", "DEBUG") + + if peer_id not in collected_peer_ids: + collected_peer_ids.add(peer_id) + + meta = desc_item.get("meta", {}) + peer_data = { + "peer_id": peer_id, + "display_name": meta.get("name"), + "role": meta.get("role"), + "send_audio": meta.get("sendAudio"), + "send_video": meta.get("sendVideo"), + "send_sharing": desc_item.get("sendSharing", False) + } + + info["participants"]["list"].append(peer_data) + audio_status = "ON" if peer_data['send_audio'] else "OFF" + video_status = "ON" if peer_data['send_video'] else "OFF" + log(f" :P {peer_data['display_name']} (A:{audio_status} V:{video_status})") + + await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}})) + + if "serverHello" in data: + server_hello = data["serverHello"] + + if "capabilitiesAnswer" in server_hello: + caps = server_hello["capabilitiesAnswer"] + info["webrtc"]["capabilities"] = { + "offerAnswerMode": caps.get("offerAnswerMode"), + "initialSubscriberOffer": caps.get("initialSubscriberOffer"), + "slotsMode": caps.get("slotsMode"), + "simulcastMode": caps.get("simulcastMode"), + "selfVadStatus": caps.get("selfVadStatus"), + "dataChannelSharing": caps.get("dataChannelSharing"), + "videoEncoderConfig": caps.get("videoEncoderConfig"), + "dataChannelVideoCodec": caps.get("dataChannelVideoCodec"), + "bandwidthLimitationReason": caps.get("bandwidthLimitationReason"), + "publisherVp9": caps.get("publisherVp9"), + "svcMode": caps.get("svcMode") + } + + if "servingComponents" in server_hello: + info["server"]["components"] = [] + for comp in server_hello["servingComponents"]: + info["server"]["components"].append({ + "type": comp.get("type"), + "host": comp.get("host"), + "version": comp.get("version") + }) + + if "rtcConfiguration" in server_hello: + rtc_config = server_hello["rtcConfiguration"] + info["webrtc"]["ice"]["servers"] = [] + + for server in rtc_config.get("iceServers", []): + info["webrtc"]["ice"]["servers"].append({ + "urls": server.get("urls"), + "has_credentials": bool(server.get("credential")) + }) + + if "pingPongConfiguration" in server_hello: + ping_config = server_hello["pingPongConfiguration"] + info["webrtc"]["ping"] = { + "interval_ms": ping_config.get("pingInterval"), + "ack_timeout_ms": ping_config.get("ackTimeout") + } + + if "telemetryConfiguration" in server_hello: + telem_config = server_hello["telemetryConfiguration"] + info["webrtc"]["telemetry"] = { + "sending_interval_ms": telem_config.get("sendingInterval") + } + + await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}})) + log(" :P Server hello") + + if "subscriberSdpOffer" in data: + sdp = data["subscriberSdpOffer"]["sdp"] + + info["webrtc"]["sdp"]["raw"] = sdp + info["webrtc"]["sdp"]["lines"] = sdp.count('\r\n') + + info["webrtc"]["audio"]["codecs"] = [] + info["webrtc"]["video"]["codecs"] = [] + info["webrtc"]["datachannel"]["supported"] = False + + for line in sdp.split('\r\n'): + if line.startswith('a=rtpmap:') and 'm=audio' in sdp[:sdp.find(line)]: + parts = line.split() + if len(parts) >= 2: + codec_info = parts[1].split('/') + info["webrtc"]["audio"]["codecs"].append({ + "name": codec_info[0], + "rate": int(codec_info[1]) if len(codec_info) > 1 else None, + "channels": int(codec_info[2]) if len(codec_info) > 2 else None + }) + + if line.startswith('a=rtpmap:') and 'm=video' in sdp[:sdp.find(line)]: + parts = line.split() + if len(parts) >= 2: + codec_info = parts[1].split('/') + codec_name = codec_info[0].upper() + if codec_name not in ['RTX', 'RED', 'ULPFEC']: + if codec_name not in [c["name"] for c in info["webrtc"]["video"]["codecs"]]: + info["webrtc"]["video"]["codecs"].append({ + "name": codec_name, + "rate": int(codec_info[1]) if len(codec_info) > 1 else None + }) + + if 'm=application' in line and 'SCTP' in line: + info["webrtc"]["datachannel"]["supported"] = True + + if 'sctp-port:' in line: + info["webrtc"]["datachannel"]["sctp_port"] = int(line.split(':')[1].strip()) + + if 'max-message-size:' in line: + size = int(line.split(':')[1].strip()) + info["webrtc"]["datachannel"]["max_message_size"] = size + info["webrtc"]["datachannel"]["max_message_size_mb"] = size / 1024 / 1024 + + if line.startswith('a=fmtp:') and 'opus' in sdp[max(0, sdp.find(line)-200):sdp.find(line)].lower(): + params = line.split(':', 1)[1].strip().split(';') + info["webrtc"]["audio"]["opus_params"] = {} + for param in params: + if '=' in param: + key, val = param.strip().split('=') + info["webrtc"]["audio"]["opus_params"][key] = val + + if line.startswith('a=extmap:'): + if "rtp_extensions" not in info["webrtc"]: + info["webrtc"]["rtp_extensions"] = [] + ext_info = line.split()[1] + info["webrtc"]["rtp_extensions"].append(ext_info) + + await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}})) + log(" :P SDP analyzed") + + except asyncio.TimeoutError: + log(f" -> Timeout on message #{i+1}, continuing...", "DEBUG") + break + except Exception as e: + log(f" X Error processing message: {e}", "ERROR") + break + + info["participants"]["count"] = len(info["participants"]["list"]) + + await ws.close() + log(" :P Closed") + + log("\n -> Fetching all participants via API...") + try: + all_participants = get_participants_list() + + log(f" -> API returned: peers={len(all_participants.get('peers', []))}, permissions={bool(all_participants.get('permissions'))}", "DEBUG") + + if "peers" in all_participants and all_participants["peers"]: + log(f" :P Found {len(all_participants['peers'])} participants via API") + + for peer in all_participants["peers"]: + peer_id = peer.get("peer_id") + if peer_id and peer_id not in collected_peer_ids: + peer_data = { + "peer_id": peer_id, + "peer_type": peer.get("peer_type"), + "send_audio": None, + "send_video": None, + "send_sharing": None + } + + if "state" in peer and "user_data" in peer["state"]: + user_data = peer["state"]["user_data"] + peer_data["display_name"] = user_data.get("display_name") + peer_data["role"] = user_data.get("role") + peer_data["uid"] = user_data.get("uid") + if "avatar_placeholder" in user_data: + peer_data["avatar"] = user_data["avatar_placeholder"] + + info["participants"]["list"].append(peer_data) + collected_peer_ids.add(peer_id) + log(f" :P {peer_data.get('display_name', 'Unknown')} ({peer_data.get('role', 'N/A')})") + + info["participants"]["count"] = len(info["participants"]["list"]) + else: + log(f" X No peers in API response", "WARN") + + if "permissions" in all_participants: + info["conference"] = {"permissions": all_participants["permissions"]} + if "conference" in all_participants: + info["conference"]["state"] = all_participants["conference"].get("state") + + except Exception as e: + log(f" X API request failed: {e}", "ERROR") + + log(f"\n :P Total participants: {info['participants']['count']}") + + except Exception as e: + log(f" X Error: {e}", "ERROR") + + return info + +def print_full_report(info): + print("CONNECTION INFO") + conn = info["connection"] + print(f"Type: {conn.get('connection_type')}") + print(f"URI: {conn.get('uri')}") + print(f"Room ID: {conn.get('room_id')}") + print(f"Peer ID: {conn.get('peer_id')}") + print(f"Session ID: {conn.get('session_id')}") + print(f"Media Platform: {conn.get('media_platform')}") + print(f"Max Participants: {conn.get('conference_limit')}") + + if "client_config" in conn: + cfg = conn["client_config"] + print(f"\nClient Configuration:") + print(f" Media Server: {cfg.get('media_server_url')}") + print(f" Session Timeout: {cfg.get('session_timeout_ms')}ms ({cfg.get('session_timeout_ms', 0)/1000/60:.1f}min)") + print(f" Reconnect Wait: {cfg.get('reconnect_wait_ms')}ms") + + print("PARTICIPANTS") + if "list" in info["participants"] and info["participants"]["list"]: + for i, peer in enumerate(info["participants"]["list"], 1): + print(f"\n{i}. {peer.get('display_name', 'Unknown')}") + print(f" Peer ID: {peer.get('peer_id')}") + print(f" Role: {peer.get('role')}") + if peer.get('uid'): + print(f" UID: {peer.get('uid')}") + print(f" Audio: {peer.get('send_audio')}") + print(f" Video: {peer.get('send_video')}") + print(f" Sharing: {peer.get('send_sharing')}") + if "avatar" in peer: + avatar = peer["avatar"] + print(f" Avatar: {avatar.get('abbreviation')} ({avatar.get('background_color')})") + print(f"\nTotal: {info['participants'].get('count', 0)} participants") + else: + print("No participants in conference") + + if "conference" in info and "permissions" in info["conference"]: + print("\n" + "=" * 70) + print("CONFERENCE PERMISSIONS") + print("=" * 70) + perms = info["conference"]["permissions"] + print(f"Version: {perms.get('version')}") + + if "public_role_permissions" in perms: + print("\nRole Permissions:") + for role_perm in perms["public_role_permissions"]: + role = role_perm.get("role") + allowed = role_perm.get("allowed", []) + print(f" {role}: {', '.join(allowed)}") + + if "personal_allowed" in perms: + print(f"\nPersonal: {', '.join(perms['personal_allowed'])}") + + if "state" in info["conference"]: + state = info["conference"]["state"] + print("\nConference State:") + print(f" Access Level: {state.get('access_level')}") + print(f" Recording: {state.get('local_recording_allowed')}") + print(f" Cloud Recording: {state.get('cloud_recording_allowed')}") + print(f" Chat: {state.get('chat_allowed')}") + print(f" Control: {state.get('control_allowed')}") + print(f" Broadcast: {state.get('broadcast_allowed')}") + + print("WEBRTC CAPABILITIES") + if "capabilities" in info["webrtc"]: + caps = info["webrtc"]["capabilities"] + print(f"Offer/Answer Mode: {caps.get('offerAnswerMode')}") + print(f"Initial Subscriber Offer: {caps.get('initialSubscriberOffer')}") + print(f"Slots Mode: {caps.get('slotsMode')}") + print(f"Simulcast Mode: {caps.get('simulcastMode')}") + print(f"VAD Status: {caps.get('selfVadStatus')}") + print(f"DataChannel Sharing: {caps.get('dataChannelSharing')}") + print(f"Video Encoder Config: {caps.get('videoEncoderConfig')}") + print(f"DC Video Codec: {caps.get('dataChannelVideoCodec')}") + print(f"Bandwidth Limitation: {caps.get('bandwidthLimitationReason')}") + print(f"Publisher VP9: {caps.get('publisherVp9')}") + print(f"SVC Mode: {caps.get('svcMode')}") + + print("AUDIO") + if "codecs" in info["webrtc"]["audio"]: + print("Codecs:") + for codec in info["webrtc"]["audio"]["codecs"]: + print(f" - {codec['name']}: {codec.get('rate', 'N/A')}Hz, {codec.get('channels', 'N/A')} channels") + + if "opus_params" in info["webrtc"]["audio"]: + print("\nOpus Parameters:") + for key, val in info["webrtc"]["audio"]["opus_params"].items(): + print(f" {key}: {val}") + + print("VIDEO") + if "codecs" in info["webrtc"]["video"]: + print("Codecs:") + for codec in info["webrtc"]["video"]["codecs"]: + print(f" - {codec['name']}: {codec.get('rate', 'N/A')}Hz") + + print("DATACHANNEL") + dc = info["webrtc"]["datachannel"] + print(f"Supported: {':P YES' if dc.get('supported') else 'X NO'}") + if dc.get("supported"): + print(f"SCTP Port: {dc.get('sctp_port')}") + print(f"Max Message Size: {dc.get('max_message_size_mb', 0):.0f}MB ({dc.get('max_message_size', 0):,} bytes)") + print(f"Note: Actual limit is 8KB due to server fragmentation") + + print("ICE/NETWORK") + if "servers" in info["webrtc"]["ice"]: + stun_count = sum(1 for s in info["webrtc"]["ice"]["servers"] if any('stun:' in u for u in s.get("urls", []))) + turn_count = sum(1 for s in info["webrtc"]["ice"]["servers"] if any('turn:' in u for u in s.get("urls", []))) + + print(f"STUN Servers: {stun_count}") + print(f"TURN Servers: {turn_count}") + print("\nServers:") + for server in info["webrtc"]["ice"]["servers"]: + for url in server.get("urls", []): + cred_status = "with credentials" if server.get("has_credentials") else "no credentials" + print(f" - {url} ({cred_status})") + + if "ping" in info["webrtc"]: + ping = info["webrtc"]["ping"] + print(f"\nPing Configuration:") + print(f" Interval: {ping.get('interval_ms')}ms") + print(f" ACK Timeout: {ping.get('ack_timeout_ms')}ms") + + if "rtp_extensions" in info["webrtc"]: + print(f"\nRTP Extensions: {len(info['webrtc']['rtp_extensions'])}") + for ext in info["webrtc"]["rtp_extensions"][:5]: + print(f" - {ext}") + + print("SERVER COMPONENTS") + if "components" in info["server"]: + for comp in info["server"]["components"]: + print(f"{comp.get('type'):20} {comp.get('host'):30} v{comp.get('version')}") + + if "telemetry" in info["webrtc"]: + telem = info["webrtc"]["telemetry"] + print(f"\nTelemetry:") + print(f" Sending Interval: {telem.get('sending_interval_ms')}ms") + + print("SDP STATISTICS") + if "lines" in info["webrtc"]["sdp"]: + print(f"Total SDP Lines: {info['webrtc']['sdp']['lines']}") + print(f"SDP Size: {len(info['webrtc']['sdp'].get('raw', ''))} bytes") + +async def main(): + log(f"Starting WebRTC info collection...") + + info = await collect_webrtc_info() + + log("\n[5/5] Generating report...\n") + print_full_report(info) + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + log("\n\nInfo collection interrupted.", "WARN")