// ==UserScript== // @name Ome.tv IP Info Tool // @namespace http://tampermonkey.net/ // @version 1.0 // @description Displays IP and location info (incl. Org) for Ome.tv users. // @match https://ome.tv/* // @grant none // @require https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css // ==/UserScript== (function() { 'use strict'; // --- Global Variables --- // Stores state like current IP, API key, auto-fetch preference, and map link. let currentIP = null; let apiKey = localStorage.getItem('omeTvIpToolApiKey') || ''; let autoFetch = localStorage.getItem('omeTvIpToolAutoFetch') === 'true'; let mapLink = null; // --- UI Creation --- // Dynamically creates the HTML structure for the overlay window, // applies CSS styles for appearance and layout (including fixed size), // and attaches necessary event listeners to UI elements (inputs, buttons). function createUI() { const overlay = document.createElement('div'); overlay.id = 'ipInfoOverlay'; overlay.innerHTML = `
Ome.tv IP Info (Drag Me)
Status: Waiting for connection...
IP: -
Details: -
`; let targetElement = document.querySelector('main#about') || document.body; targetElement.appendChild(overlay); // Add close button event overlay.querySelector('#ipInfoOverlayClose').addEventListener('click', () => { overlay.remove(); }); const style = document.createElement('style'); // --- CSS Modifications Below --- style.textContent = ` @import url('https://fonts.googleapis.com/css2?family=Segoe+UI&display=swap'); #ipInfoOverlay { position: fixed; bottom: 15px; right: 15px; width: 300px; height: 280px; overflow-y: auto; background-color: #f0f0f0; border: 2px solid #0078D4; /* border-radius removed */ padding: 10px 15px; z-index: 10000; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 13px; color: #333; box-shadow: 0 2px 5px rgba(0,0,0,0.2); line-height: 1.4; display: flex; flex-direction: column; } #ipInfoOverlayTitle { font-size: 16px; font-weight: bold; color: #005a9e; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid #ccc; text-align: center; user-select: none; cursor: move; flex-shrink: 0; } .ipInfoSection { margin-bottom: 8px; flex-shrink: 0; } .ipInfoSection label { display: inline-block; width: 80px; font-weight: 600; vertical-align: middle; } .ipInfoSection input[type="checkbox"] { vertical-align: middle; margin-left: 5px; } #ipInfoLocation { display: block; margin-left: 75px; line-height: 1.5; word-wrap: break-word; } .apiKeyWrapper { display: inline-flex; align-items: center; border: 1px solid #ccc; border-radius: 3px; padding-right: 2px; background-color: #fff; max-width: calc(100% - 85px); } #apiKeyInput { border: none; outline: none; padding: 4px 6px; flex-grow: 1; font-size: 12px; min-width: 80px; } #ipInfoOverlay strong { font-weight: 600; display: inline-block; width: 70px; vertical-align: top; } .ipInfoButtons { margin-top: auto; padding-top: 10px; border-top: 1px solid #ccc; flex-shrink: 0; display: flex; /* Added for horizontal alignment */ justify-content: flex-end; /* Added to push buttons right */ } .ipInfoBtn, .ipInfoBtn-Icon { padding: 5px 10px; margin-left: 5px; border: 1px solid #0078D4; background-color: #e1e1e1; color: #333; border-radius: 3px; cursor: pointer; font-size: 12px; transition: background-color 0.2s ease; } .ipInfoBtn:hover:not(:disabled), .ipInfoBtn-Icon:hover { background-color: #cce4f7; } .ipInfoBtn:disabled { cursor: not-allowed; opacity: 0.6; border-color: #ccc; background-color: #e9e9e9; } .ipInfoBtn-Icon { background: none; border: none; padding: 4px; vertical-align: middle; line-height: 1; color: #0078D4; } .ipInfoBtn-Icon i { font-size: 14px; } `; // --- End of CSS Modifications --- document.head.appendChild(style); if (!document.querySelector('link[href*="font-awesome"]')) { const faLink = document.createElement("link"); faLink.rel = "stylesheet"; faLink.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"; faLink.integrity = "sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="; faLink.crossOrigin = "anonymous"; faLink.referrerPolicy = "no-referrer"; document.head.appendChild(faLink); } const apiKeyInput = document.getElementById('apiKeyInput'); const toggleApiKeyBtn = document.getElementById('toggleApiKey'); const autoFetchCheckbox = document.getElementById('autoFetchCheckbox'); const fetchDetailsBtn = document.getElementById('fetchDetailsBtn'); const openMapBtn = document.getElementById('openMapBtn'); const manualHookBtn = document.getElementById('manualHookBtn'); apiKeyInput.value = apiKey; autoFetchCheckbox.checked = autoFetch; apiKeyInput.addEventListener('input', () => { apiKey = apiKeyInput.value; localStorage.setItem('omeTvIpToolApiKey', apiKey); updateStatus(apiKey ? 'API Key set.' : 'API Key missing!', apiKey ? 'info' : 'warn'); if (currentIP && apiKey && fetchDetailsBtn) { fetchDetailsBtn.disabled = false; } else if (!apiKey && fetchDetailsBtn) { fetchDetailsBtn.disabled = true; } }); toggleApiKeyBtn.addEventListener('click', toggleApiKeyVisibility); autoFetchCheckbox.addEventListener('change', () => { autoFetch = autoFetchCheckbox.checked; localStorage.setItem('omeTvIpToolAutoFetch', autoFetch); }); fetchDetailsBtn.addEventListener('click', () => { if (currentIP) { gather(currentIP); } }); openMapBtn.addEventListener('click', () => { if (mapLink) { openMap(mapLink); } }); manualHookBtn.addEventListener('click', () => { console.log("Manual hook setup triggered."); setupWebRTCHook(); }); dragElement(overlay, document.getElementById('ipInfoOverlayTitle')); } // --- Drag Element Functionality --- // Makes the specified element (elmnt) draggable by its handle (dragHandle). // It calculates the new position based on mouse movement and updates the // element's top/left styles, ensuring it stays within viewport bounds. function dragElement(elmnt, dragHandle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; const header = dragHandle || elmnt; header.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'LABEL') { return; } e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; if (elmnt.style.bottom || elmnt.style.right) { const rect = elmnt.getBoundingClientRect(); elmnt.style.top = rect.top + "px"; elmnt.style.left = rect.left + "px"; elmnt.style.bottom = ""; elmnt.style.right = ""; } } function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; let newTop = elmnt.offsetTop - pos2; let newLeft = elmnt.offsetLeft - pos1; const maxX = window.innerWidth - elmnt.offsetWidth; const maxY = window.innerHeight - elmnt.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, maxX)); newTop = Math.max(0, Math.min(newTop, maxY)); elmnt.style.top = newTop + "px"; elmnt.style.left = newLeft + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } // --- Helper Functions --- // Contains utility functions for updating specific parts of the UI (status, IP, location), // toggling API key visibility, and opening the map link. function updateStatus(message, type = 'info') { const statusEl = document.getElementById('ipInfoStatus'); if (statusEl) { statusEl.textContent = message; switch (type) { case 'success': statusEl.style.color = 'green'; break; case 'warn': statusEl.style.color = 'orange'; break; case 'error': statusEl.style.color = 'red'; break; default: statusEl.style.color = '#555'; } } else { console.log(`Status [${type}]: ${message}`); } } function updateIpDisplay(ip) { const ipEl = document.getElementById('ipInfoIP'); if(ipEl) ipEl.textContent = ip || '-'; } function updateLocationDisplay(detailsHtml) { const locEl = document.getElementById('ipInfoLocation'); if(locEl) locEl.innerHTML = detailsHtml || '-'; } function toggleApiKeyVisibility() { const apiKeyInput = document.getElementById('apiKeyInput'); const icon = document.querySelector('#toggleApiKey i'); if (apiKeyInput.type === 'password') { apiKeyInput.type = 'text'; icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); } else { apiKeyInput.type = 'password'; icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); } } function openMap(link) { if (link) { window.open(link, '_blank'); } } // --- Core Logic (IP Details Fetching) --- // Handles fetching detailed IP information from the ipinfo.io API using the provided IP and API key. // Updates the UI with status messages, location details, and enables/disables buttons accordingly. // Handles potential errors during the API call. async function gather(ip) { const fetchBtn = document.getElementById('fetchDetailsBtn'); const mapBtn = document.getElementById('openMapBtn'); mapLink = null; if (!apiKey) { updateStatus('Error: ipinfo.io API Key is missing.', 'error'); if (fetchBtn) fetchBtn.disabled = true; return; } updateStatus('Fetching details...', 'info'); if (fetchBtn) fetchBtn.disabled = true; if (mapBtn) mapBtn.disabled = true; updateLocationDisplay('Loading...'); try { const url = `https://ipinfo.io/${ip}/json?token=${apiKey}`; const response = await fetch(url); if (!response.ok) { let errorDetail = ''; try { const errorJson = await response.json(); errorDetail = errorJson.error?.message || JSON.stringify(errorJson.error) || response.statusText; } catch (e) { errorDetail = await response.text(); } throw new Error(`API Error (${response.status}): ${errorDetail.substring(0, 150)}`); } const json = await response.json(); if (json.bogon) { updateStatus('Private/Reserved IP detected.', 'warn'); updateLocationDisplay('N/A (Bogon IP)'); currentIP = ip; updateIpDisplay(ip) if (fetchBtn && ip && apiKey) fetchBtn.disabled = false; return; } if (json.ip) { currentIP = json.ip; updateIpDisplay(currentIP) let details = []; if (json.city) details.push(`City: ${json.city}`); if (json.region) details.push(`Region: ${json.region}`); if (json.country) details.push(`Country: ${json.country}`); if (json.org) details.push(`Org: ${json.org}`); if (json.postal) details.push(`Postal: ${json.postal}`); if (json.timezone) details.push(`Timezone: ${json.timezone}`); updateLocationDisplay(details.length > 0 ? details.join('
') : 'Details unavailable'); if (json.loc) { mapLink = `https://www.google.com/maps?q=${json.loc}`; if (mapBtn) { mapBtn.disabled = false; } } else { if (mapBtn) mapBtn.disabled = true; } updateStatus('Details loaded successfully.', 'success'); } else { throw new Error("Invalid data received from API."); } } catch (error) { console.error("Failed to get IP information:", error); updateStatus(`Error: ${error.message}`, 'error'); updateLocationDisplay('Failed to load'); if (mapBtn) mapBtn.disabled = true; } finally { if (fetchBtn && currentIP && apiKey) { fetchBtn.disabled = false; } else if (fetchBtn) { fetchBtn.disabled = true; } } } // --- WebRTC Hooking --- // Overrides the browser's default RTCPeerConnection functions (`addIceCandidate`, `setRemoteDescription`) // to intercept WebRTC negotiation details. It specifically looks for 'srflx' candidates, which // usually contain the peer's public IP address. When found, it updates the UI and potentially // triggers an automatic detail fetch if configured. Includes checks to prevent re-hooking. function setupWebRTCHook() { if (!window.RTCPeerConnection) { console.warn("RTCPeerConnection not found. Cannot intercept IP."); updateStatus("WebRTC not supported by browser.", "error"); return; } if (window.oRTCPeerConnection) { console.log("WebRTC hook already active."); if (document.getElementById('ipInfoStatus')) { const currentStatus = document.getElementById('ipInfoStatus').textContent; if (currentStatus.includes('Waiting') || currentStatus.includes('Ready') || currentStatus.includes('captured') || currentStatus.includes('Hook active')) { // Added 'Hook active' check updateStatus("WebRTC hook already active.", "info"); } } return; } window.oRTCPeerConnection = window.RTCPeerConnection; window.RTCPeerConnection = function(...args) { console.log("RTCPeerConnection created (hooked)"); const pc = new window.oRTCPeerConnection(...args); const originalAddIceCandidate = pc.addIceCandidate; const originalSetRemoteDescription = pc.setRemoteDescription; pc.addIceCandidate = function(iceCandidate, ...rest) { if (iceCandidate && iceCandidate.candidate) { const candidateString = iceCandidate.candidate; const ipMatch = candidateString.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/); const typeMatch = candidateString.match(/typ\s+(srflx|prflx|relay|host)/); if (ipMatch && typeMatch && typeMatch[1] === "srflx") { const ip = ipMatch[0]; const type = typeMatch[1]; console.log(`Captured ${type} IP: ${ip} from addIceCandidate`); if (currentIP !== ip) { currentIP = ip; updateIpDisplay(ip); updateLocationDisplay('-'); mapLink = null; const fetchBtn = document.getElementById('fetchDetailsBtn'); const mapBtn = document.getElementById('openMapBtn'); if (fetchBtn) fetchBtn.disabled = !apiKey; if (mapBtn) mapBtn.disabled = true; if (autoFetch && apiKey) { console.log("Auto-fetching details for:", ip); gather(ip); } else if (autoFetch && !apiKey) { updateStatus('IP captured. Auto-fetch needs API Key.', 'warn'); } else { updateStatus('IP captured. Click "Fetch Details".', 'info'); } } } } return originalAddIceCandidate.apply(this, [iceCandidate, ...rest]); }; pc.setRemoteDescription = function(sdp, ...rest) { if (sdp && sdp.sdp) { const sdpLines = sdp.sdp.split('\r\n'); for (const line of sdpLines) { if (line.startsWith('a=candidate:') && line.includes(' typ srflx ')) { const fields = line.split(' '); if (fields.length >= 8) { const ip = fields[4]; const type = fields[7]; console.log(`Captured ${type} IP: ${ip} from setRemoteDescription`); if (currentIP !== ip) { currentIP = ip; updateIpDisplay(ip); updateLocationDisplay('-'); mapLink = null; const fetchBtn = document.getElementById('fetchDetailsBtn'); const mapBtn = document.getElementById('openMapBtn'); if (fetchBtn) fetchBtn.disabled = !apiKey; if (mapBtn) mapBtn.disabled = true; if (autoFetch && apiKey) { console.log("Auto-fetching details for:", ip); gather(ip); } else if (autoFetch && !apiKey) { updateStatus('IP captured. Auto-fetch needs API Key.', 'warn'); } else { updateStatus('IP captured. Click "Fetch Details".', 'info'); } } // break; // Optional: stop after first srflx in SDP } } } } return originalSetRemoteDescription.apply(this, [sdp, ...rest]); }; return pc; }; console.log("RTCPeerConnection hook installed successfully."); updateStatus(apiKey ? 'Hook active. Waiting for connection...' : 'Hook active. API Key missing!', apiKey ? 'info' : 'warn'); } // --- Initialization --- // Sets up the script when the page loads. It ensures the DOM is ready, // then creates the UI elements and attempts to set up the WebRTC hook automatically. // Includes a check to prevent creating duplicate UI if the script runs multiple times. function initialize() { if (!document.getElementById('ipInfoOverlay')) { createUI(); setupWebRTCHook(); } else { console.log("IP Info Tool UI already exists. Skipping creation."); setupWebRTCHook(); // ensure the hook is always attempted on run } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { // DOM is already ready initialize(); } })();