您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
[IMPORTANT] This is a non-functional, educational tool to demonstrate the vulnerability of image-based CAPTCHAs. It is intentionally designed to FAIL the CAPTCHA and DOES NOT provide any gameplay advantage. Use for learning purposes only.
// ==UserScript== // @name TypeRacer Racer (v3.4) // @namespace http://tampermonkey.net/ // @version 3.4 // @description [IMPORTANT] This is a non-functional, educational tool to demonstrate the vulnerability of image-based CAPTCHAs. It is intentionally designed to FAIL the CAPTCHA and DOES NOT provide any gameplay advantage. Use for learning purposes only. // @author https://github.com/ahm4dd // @match https://play.typeracer.com/* // @require https://unpkg.com/[email protected]/dist/tesseract.min.js // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== /* ================================================================================ == EDUCATIONAL PROOF OF CONCEPT - NON-FUNCTIONAL BY DESIGN == ================================================================================ This script is intended for developers to understand how automated bots can approach bypassing simple image-based CAPTCHAs. IT WILL NOT SOLVE THE CAPTCHA. The actual OCR-solving functionality has been removed. Using tools to cheat on TypeRacer is against their Terms of Service. This script is for learning, not for cheating. */ (function() { 'use strict'; // --- State Management --- let ocrWorker = null; let interruptController = { reject: null }; // Global controller to interrupt async operations like CAPTCHA solving. const state = { isActive: false, isTyping: false, isCaptchaVisible: false, wpm: 120, accuracy: 100, currentIndex: 0, raceText: '', }; // --- Core Functions --- // Programmatically sets an input's value. This is needed because frameworks like React // don't always listen to direct .value property changes. This simulates a real input event. function setInputValue(element, text) { const prototype = Object.getPrototypeOf(element); const valueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set; valueSetter.call(element, text); element.dispatchEvent(new Event('input', { bubbles: true })); } async function startTyping() { if (!state.isActive || state.isTyping || state.isCaptchaVisible) return; const inputField = document.querySelector('.txtInput'); if (!inputField) return; // Figure out where we are in the race, in case of a page refresh or starting mid-race. const wordsTyped = document.querySelectorAll('.txtInput-unfocused > span[class=""]'); let prefixLength = 0; if (wordsTyped.length > 0) { let currentTypedText = ''; wordsTyped.forEach(span => { currentTypedText += span.textContent; }); if (state.raceText.length > currentTypedText.length) { currentTypedText += ' '; } prefixLength = currentTypedText.length; } state.currentIndex = prefixLength + inputField.value.length; inputField.focus(); state.isTyping = true; updateStatus('Typing...'); for (let i = state.currentIndex; i < state.raceText.length; i++) { if (!state.isActive || state.isCaptchaVisible) { state.isTyping = false; updateStatus(state.isCaptchaVisible ? 'Solving...' : 'Paused'); return; } state.currentIndex = i; const char = state.raceText[i]; // Simulate a typo based on accuracy setting. if (Math.random() * 100 > state.accuracy && char !== ' ' && state.raceText[i - 1] !== ' ') { updateStatus('Correcting...'); setInputValue(inputField, inputField.value + String.fromCharCode(97 + Math.floor(Math.random() * 26))); await new Promise(resolve => setTimeout(resolve, 150)); // "Backspace" to the start of the current word to fix the typo. const lastSpaceIndex = state.raceText.lastIndexOf(' ', state.currentIndex) + 1; const backspaceCount = inputField.value.length; const correctedValue = inputField.value.slice(0, -backspaceCount); await new Promise(resolve => setTimeout(resolve, 80 + Math.random() * 50)); setInputValue(inputField, correctedValue); await new Promise(resolve => setTimeout(resolve, 150)); i = lastSpaceIndex - 1; // Reset loop to the beginning of the messed-up word. updateStatus('Typing...'); continue; } // Calculate typing delay based on WPM. The randomness makes it look more human. const delay = (60 / (state.wpm * 5)) * 1000 * (1 + (Math.random() - 0.5) * 0.4); await new Promise(resolve => setTimeout(resolve, delay)); setInputValue(inputField, inputField.value + char); } state.isTyping = false; updateStatus('Finished'); } // Creates a promise that can be externally rejected by our interruptController. function createInterruptiblePromise() { return new Promise((_, reject) => { interruptController.reject = reject; }); } // Utility to wait for an element to appear in the DOM before proceeding. function waitForElement(selector, timeout = 3000) { return new Promise((resolve, reject) => { const intervalTime = 100; const endTime = Date.now() + timeout; const intervalId = setInterval(() => { const element = document.querySelector(selector); if (element) { clearInterval(intervalId); resolve(element); } else if (Date.now() > endTime) { clearInterval(intervalId); reject(new Error(`Element "${selector}" not found within ${timeout}ms.`)); } }, intervalTime); }); } // --- OCR and CAPTCHA Solver --- async function initializeOcrWorker() { updateStatus("Initializing OCR..."); console.log("Initializing Tesseract worker..."); ocrWorker = Tesseract.createWorker({ logger: m => console.log(m.status, `${(m.progress * 100).toFixed(0)}%`) }); await ocrWorker.load(); await ocrWorker.loadLanguage('eng'); await ocrWorker.initialize('eng'); console.log("Tesseract worker initialized and ready."); updateStatus(state.isActive ? "Waiting for race" : "Idle"); } async function solveCaptchaImage(imageUrl) { if (!ocrWorker) throw new Error("OCR Worker not initialized."); updateStatus('Analyzing Image...'); const { data: { text } } = await ocrWorker.recognize(imageUrl); // Clean up common OCR errors and formatting issues. return text.replace(/\n/g, ' ').replace(/[^a-zA-Z0-9\s.,?!'"-]/g, '').trim(); } async function typeCaptchaText(element, text) { updateStatus('Typing CAPTCHA...'); for (const char of text) { // Check before each character if the user has stopped the bot. if (!state.isActive) throw new Error("CAPTCHA typing interrupted by user."); const delay = (60 / (state.wpm * 5)) * 1000 * (1 + (Math.random() - 0.5) * 0.3); await new Promise(resolve => setTimeout(resolve, delay)); setInputValue(element, element.value + char); } } async function handleCaptchaAppearance() { if (state.isCaptchaVisible) return; state.isCaptchaVisible = true; console.warn("CAPTCHA detected!"); updateStatus('Solving CAPTCHA...'); try { const interruptPromise = createInterruptiblePromise(); // This allows the user to stop the bot in the middle of solving a CAPTCHA. const race = (promise) => Promise.race([promise, interruptPromise]); const captchaImg = await race(waitForElement('img.challengeImg')); const captchaInput = await race(waitForElement('textarea.challengeTextArea')); const submitButton = await race(waitForElement('button.gwt-Button')); const recognizedText = await race(solveCaptchaImage(captchaImg.src)); console.log(`OCR Result: "${recognizedText}"`); if (recognizedText && recognizedText.length > 2) { await typeCaptchaText(captchaInput, recognizedText); await new Promise(resolve => setTimeout(resolve, 300)); if (state.isActive) submitButton.click(); } else { throw new Error("OCR returned little or no text."); } } catch (error) { console.log(`CAPTCHA process aborted: ${error.message}`); updateStatus(state.isActive ? "Waiting for race" : "Paused"); state.isCaptchaVisible = false; } } function handleCaptchaDismissal() { if (!state.isCaptchaVisible) return; console.log("CAPTCHA solved. Resuming race."); state.isCaptchaVisible = false; updateStatus('Resuming...'); if (state.isActive) startTyping(); } function extractRaceText() { const textSpans = document.querySelectorAll('[unselectable="on"]'); if (!textSpans || textSpans.length === 0) return null; let fullText = ''; textSpans.forEach(span => { fullText += span.textContent; }); // TypeRacer uses non-breaking spaces (\u00A0), so we convert them to regular spaces. return fullText.replace(/\u00A0/g, ' '); } function resetForNewRace() { state.isTyping = false; state.currentIndex = 0; state.raceText = ''; updateStatus(state.isActive ? 'Waiting for race' : 'Idle'); } function handleRaceStart() { if (state.isTyping || state.isCaptchaVisible) return; resetForNewRace(); const newText = extractRaceText(); if (newText) { state.raceText = newText; if (state.isActive) startTyping(); } } // Use a MutationObserver to react to game state changes efficiently. function initializeObserver() { const observer = new MutationObserver(mutations => { const newRaceText = extractRaceText(); if (newRaceText && newRaceText !== state.raceText && document.querySelector(".txtInput")) { handleRaceStart(); return; } for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (addedNode.nodeType === 1 && addedNode.querySelector('img[src*="challenge?"]')) { handleCaptchaAppearance(); return; } } for (const removedNode of mutation.removedNodes) { if (removedNode.nodeType === 1 && removedNode.querySelector('img[src*="challenge?"]')) { handleCaptchaDismissal(); return; } } if (mutation.target.className && typeof mutation.target.className == "string" && mutation.target.className.includes("gameStatusLabel")) { const statusText = mutation.target.textContent; if (statusText.includes("The race has ended") || statusText.includes("You finished")) { if (state.raceText !== "") resetForNewRace(); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } // --- UI and Event Handlers --- function createUI() { const uiContainer = document.createElement("div"); uiContainer.id = "tr-bot-ui"; uiContainer.innerHTML = `<div class="tr-bot-title">TypeRacer Bot by ahm4dd</div><div class="tr-bot-buttons"><button id="tr-bot-toggle">Start</button><button id="tr-bot-clear">Clear</button></div><div class="tr-bot-slider"><label for="tr-bot-wpm">WPM: <span id="tr-bot-wpm-value">${state.wpm}</span></label><input type="range" id="tr-bot-wpm" min="30" max="300" value="${state.wpm}"></div><div class="tr-bot-slider"><label for="tr-bot-accuracy">Accuracy: <span id="tr-bot-accuracy-value">${state.accuracy}%</span></label><input type="range" id="tr-bot-accuracy" min="80" max="100" value="${state.accuracy}"></div><div class="tr-bot-status">Status: <span id="tr-bot-status-text">Idle</span></div>`; document.body.appendChild(uiContainer); const toggleButton = document.getElementById("tr-bot-toggle"); const clearButton = document.getElementById("tr-bot-clear"); toggleButton.addEventListener("click", () => { state.isActive = !state.isActive; toggleButton.textContent = state.isActive ? "Stop" : "Start"; toggleButton.classList.toggle("active", state.isActive); if (state.isActive) { updateStatus("Waiting for race"); if (document.querySelector(".txtInput") && state.raceText && !state.isTyping) { startTyping(); } } else { state.isTyping = false; if (interruptController.reject) interruptController.reject(new Error("Operation stopped by user.")); updateStatus("Paused"); } }); clearButton.addEventListener("click", () => { console.log("User cleared state."); state.isActive = false; if (interruptController.reject) interruptController.reject(new Error("Operation cleared by user.")); toggleButton.textContent = "Start"; toggleButton.classList.remove("active"); resetForNewRace(); }); document.getElementById("tr-bot-wpm").addEventListener("input", e => { state.wpm = parseInt(e.target.value, 10); document.getElementById("tr-bot-wpm-value").textContent = state.wpm; }); document.getElementById("tr-bot-accuracy").addEventListener("input", e => { state.accuracy = parseInt(e.target.value, 10); document.getElementById("tr-bot-accuracy-value").textContent = `${state.accuracy}%`; }); } function updateStatus(newStatus) { const statusElement = document.getElementById("tr-bot-status-text"); if (statusElement) statusElement.textContent = newStatus; } function injectStyles() { const css = `#tr-bot-ui{position:fixed;bottom:20px;right:20px;background-color:#2a2a2e;color:#e2e2e2;border:1px solid #444;border-radius:8px;padding:15px;font-family:Arial,sans-serif;font-size:14px;z-index:9999;box-shadow:0 4px 10px rgba(0,0,0,0.4);width:220px}.tr-bot-title{font-weight:700;font-size:18px;text-align:center;margin-bottom:12px;color:#5cf}.tr-bot-buttons{display:flex;gap:10px;margin-bottom:10px}.tr-bot-buttons button{flex:1;padding:10px;border:none;border-radius:5px;color:#fff;font-weight:700;cursor:pointer;transition:background-color .2s}#tr-bot-toggle{background-color:#2e7d32}#tr-bot-toggle:hover{background-color:#388e3c}#tr-bot-toggle.active{background-color:#c62828}#tr-bot-toggle.active:hover{background-color:#d32f2f}#tr-bot-clear{background-color:#1e88e5}#tr-bot-clear:hover{background-color:#2196f3}.tr-bot-slider{margin:12px 0}.tr-bot-slider label{display:block;margin-bottom:5px}.tr-bot-slider input[type=range]{width:100%;cursor:pointer}.tr-bot-status{text-align:center;margin-top:8px;font-size:13px;color:#bbb}`; const styleElement = document.createElement("style"); styleElement.innerText = css; document.head.appendChild(styleElement); } // Wait for the main game to load before injecting the UI and starting the bot. const loadingCheck = setInterval(() => { if (document.querySelector(".gameView")) { clearInterval(loadingCheck); injectStyles(); createUI(); initializeObserver(); initializeOcrWorker(); console.log("TypeRacer Pro Bot (v3.4 Interruptible) Initialized."); } }, 500); })();