You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buster/src/background/main.js

739 lines
19 KiB
JavaScript

import audioBufferToWav from 'audiobuffer-to-wav';
import aes from 'crypto-js/aes';
import sha256 from 'crypto-js/sha256';
import utf8 from 'crypto-js/enc-utf8';
import {initStorage, migrateLegacyStorage} from 'storage/init';
import {isStorageReady} from 'storage/storage';
import storage from 'storage/storage';
import {
showNotification,
sendNativeMessage,
processMessageResponse,
processAppUse
} from 'utils/app';
import {
executeCode,
scriptsAllowed,
getBrowser,
getPlatform,
getRandomInt,
arrayBufferToBase64,
normalizeAudio,
sliceAudio
} from 'utils/common';
import {
recaptchaChallengeUrlRx,
captchaGoogleSpeechApiLangCodes,
captchaIbmSpeechApiLangCodes,
captchaMicrosoftSpeechApiLangCodes,
captchaWitSpeechApiLangCodes
} from 'utils/data';
import {targetEnv, clientAppVersion} from 'utils/config';
let nativePort;
let secrets;
function getFrameClientPos(index) {
let currentIndex = -1;
if (window !== window.top) {
const siblingWindows = window.parent.frames;
for (let i = 0; i < siblingWindows.length; i++) {
if (siblingWindows[i] === window) {
currentIndex = i;
break;
}
}
}
const targetWindow = window.frames[index];
for (const frame of document.querySelectorAll('iframe')) {
if (frame.contentWindow === targetWindow) {
let {left: x, top: y} = frame.getBoundingClientRect();
const scale = window.devicePixelRatio;
return {x: x * scale, y: y * scale, currentIndex};
}
}
}
async function getFramePos(tabId, frameId, frameIndex) {
let x = 0;
let y = 0;
while (true) {
frameId = (
await browser.webNavigation.getFrame({
tabId,
frameId
})
).parentFrameId;
if (frameId === -1) {
break;
}
const [data] = await executeCode(
`(${getFrameClientPos.toString()})(${frameIndex})`,
tabId,
frameId
);
frameIndex = data.currentIndex;
x += data.x;
y += data.y;
}
return {x, y};
}
function initResetCaptcha() {
const initReset = function (challengeUrl) {
const script = document.createElement('script');
script.onload = function (ev) {
ev.target.remove();
document.dispatchEvent(
new CustomEvent('___resetCaptcha', {detail: challengeUrl})
);
};
script.src = chrome.runtime.getURL('/src/scripts/reset.js');
document.documentElement.appendChild(script);
};
const onMessage = function (request) {
if (request.id === 'resetCaptcha') {
removeCallbacks();
initReset(request.challengeUrl);
}
};
const removeCallbacks = function () {
window.clearTimeout(timeoutId);
chrome.runtime.onMessage.removeListener(onMessage);
};
const timeoutId = window.setTimeout(removeCallbacks, 10000); // 10 seconds
chrome.runtime.onMessage.addListener(onMessage);
}
async function resetCaptcha(tabId, frameId, challengeUrl) {
frameId = (await browser.webNavigation.getFrame({tabId, frameId}))
.parentFrameId;
if (!(await scriptsAllowed(tabId, frameId))) {
await showNotification({messageId: 'error_scriptsNotAllowed'});
return;
}
await executeCode(`(${initResetCaptcha.toString()})()`, tabId, frameId);
await browser.tabs.sendMessage(
tabId,
{
id: 'resetCaptcha',
challengeUrl
},
{frameId}
);
}
function challengeRequestCallback(details) {
const url = new URL(details.url);
if (url.searchParams.get('hl') !== 'en') {
url.searchParams.set('hl', 'en');
return {redirectUrl: url.toString()};
}
}
async function setChallengeLocale() {
const {loadEnglishChallenge, simulateUserInput} = await storage.get([
'loadEnglishChallenge',
'simulateUserInput'
]);
if (loadEnglishChallenge || simulateUserInput) {
if (
!browser.webRequest.onBeforeRequest.hasListener(challengeRequestCallback)
) {
browser.webRequest.onBeforeRequest.addListener(
challengeRequestCallback,
{
urls: [
'https://google.com/recaptcha/api2/anchor*',
'https://google.com/recaptcha/api2/bframe*',
'https://www.google.com/recaptcha/api2/anchor*',
'https://www.google.com/recaptcha/api2/bframe*',
'https://google.com/recaptcha/enterprise/anchor*',
'https://google.com/recaptcha/enterprise/bframe*',
'https://www.google.com/recaptcha/enterprise/anchor*',
'https://www.google.com/recaptcha/enterprise/bframe*',
'https://recaptcha.net/recaptcha/api2/anchor*',
'https://recaptcha.net/recaptcha/api2/bframe*',
'https://www.recaptcha.net/recaptcha/api2/anchor*',
'https://www.recaptcha.net/recaptcha/api2/bframe*',
'https://recaptcha.net/recaptcha/enterprise/anchor*',
'https://recaptcha.net/recaptcha/enterprise/bframe*',
'https://www.recaptcha.net/recaptcha/enterprise/anchor*',
'https://www.recaptcha.net/recaptcha/enterprise/bframe*'
],
types: ['sub_frame']
},
['blocking']
);
}
} else if (
browser.webRequest.onBeforeRequest.hasListener(challengeRequestCallback)
) {
browser.webRequest.onBeforeRequest.removeListener(challengeRequestCallback);
}
}
function removeRequestOrigin(details) {
const origin = window.location.origin;
const headers = details.requestHeaders;
for (const header of headers) {
if (header.name.toLowerCase() === 'origin' && header.value === origin) {
headers.splice(headers.indexOf(header), 1);
break;
}
}
return {requestHeaders: headers};
}
function addBackgroundRequestListener() {
if (
!browser.webRequest.onBeforeSendHeaders.hasListener(removeRequestOrigin)
) {
const urls = [
'https://google.com/*',
'https://www.google.com/*',
'https://recaptcha.net/*',
'https://www.recaptcha.net/*',
'https://api.wit.ai/*',
'https://speech.googleapis.com/*',
'https://*.speech-to-text.watson.cloud.ibm.com/*',
'https://*.stt.speech.microsoft.com/*'
];
const extraInfo = ['blocking', 'requestHeaders'];
if (
targetEnv !== 'firefox' &&
Object.values(browser.webRequest.OnBeforeSendHeadersOptions).includes(
'extraHeaders'
)
) {
extraInfo.push('extraHeaders');
}
browser.webRequest.onBeforeSendHeaders.addListener(
removeRequestOrigin,
{
urls,
types: ['xmlhttprequest']
},
extraInfo
);
}
}
function removeBackgroundRequestListener() {
if (browser.webRequest.onBeforeSendHeaders.hasListener(removeRequestOrigin)) {
browser.webRequest.onBeforeSendHeaders.removeListener(removeRequestOrigin);
}
}
async function prepareAudio(audio) {
const audioBuffer = await normalizeAudio(audio);
const audioSlice = await sliceAudio({
audioBuffer,
start: 1.5,
end: audioBuffer.duration - 1.5
});
return audioBufferToWav(audioSlice);
}
async function loadSecrets() {
try {
const ciphertext = await (await fetch('/secrets.txt')).text();
const key = sha256(
(await (await fetch('/src/background/script.js')).text()) +
(await (await fetch('/src/solve/script.js')).text())
).toString();
secrets = JSON.parse(aes.decrypt(ciphertext, key).toString(utf8));
} catch (err) {
secrets = {};
const {speechService} = await storage.get('speechService');
if (speechService === 'witSpeechApiDemo') {
await storage.set({speechService: 'witSpeechApi'});
}
}
}
async function getWitSpeechApiKey(speechService, language) {
if (speechService === 'witSpeechApiDemo') {
if (!secrets) {
await loadSecrets();
}
const apiKeys = secrets.witApiKeys;
if (apiKeys) {
const apiKey = apiKeys[language];
if (Array.isArray(apiKey)) {
return apiKey[getRandomInt(1, apiKey.length) - 1];
}
return apiKey;
}
} else {
const {witSpeechApiKeys: apiKeys} = await storage.get('witSpeechApiKeys');
return apiKeys[language];
}
}
async function getWitSpeechApiResult(apiKey, audioContent) {
const result = {};
const rsp = await fetch('https://api.wit.ai/speech?v=20221114', {
mode: 'cors',
method: 'POST',
headers: {
Authorization: 'Bearer ' + apiKey
},
body: new Blob([audioContent], {type: 'audio/wav'})
});
if (rsp.status !== 200) {
if (rsp.status === 429) {
result.errorId = 'error_apiQuotaExceeded';
result.errorTimeout = 6000;
} else {
throw new Error(`API response: ${rsp.status}, ${await rsp.text()}`);
}
} else {
const data = JSON.parse((await rsp.text()).split('\r\n').at(-1)).text;
if (data) {
result.text = data.trim();
}
}
return result;
}
async function getGoogleSpeechApiResult(
apiKey,
audioContent,
language,
detectAltLanguages
) {
const data = {
audio: {
content: arrayBufferToBase64(audioContent)
},
config: {
encoding: 'LINEAR16',
languageCode: language,
model: 'video',
sampleRateHertz: 16000
}
};
if (!['en-US', 'en-GB'].includes(language) && detectAltLanguages) {
data.config.model = 'default';
data.config.alternativeLanguageCodes = ['en-US'];
}
const rsp = await fetch(
`https://speech.googleapis.com/v1p1beta1/speech:recognize?key=${apiKey}`,
{
mode: 'cors',
method: 'POST',
body: JSON.stringify(data)
}
);
if (rsp.status !== 200) {
throw new Error(`API response: ${rsp.status}, ${await rsp.text()}`);
}
const results = (await rsp.json()).results;
if (results) {
return results[0].alternatives[0].transcript.trim();
}
}
async function getIbmSpeechApiResult(apiUrl, apiKey, audioContent, model) {
const rsp = await fetch(
`${apiUrl}/v1/recognize?model=${model}&profanity_filter=false`,
{
mode: 'cors',
method: 'POST',
headers: {
Authorization: 'Basic ' + window.btoa('apikey:' + apiKey),
'X-Watson-Learning-Opt-Out': 'true'
},
body: new Blob([audioContent], {type: 'audio/wav'})
}
);
if (rsp.status !== 200) {
throw new Error(`API response: ${rsp.status}, ${await rsp.text()}`);
}
const results = (await rsp.json()).results;
if (results && results.length) {
return results[0].alternatives[0].transcript.trim();
}
}
async function getMicrosoftSpeechApiResult(
apiLocation,
apiKey,
audioContent,
language
) {
const rsp = await fetch(
`https://${apiLocation}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1?language=${language}&format=detailed&profanity=raw`,
{
mode: 'cors',
method: 'POST',
headers: {
'Ocp-Apim-Subscription-Key': apiKey,
'Content-type': 'audio/wav; codec=audio/pcm; samplerate=16000'
},
body: new Blob([audioContent], {type: 'audio/wav'})
}
);
if (rsp.status !== 200) {
throw new Error(`API response: ${rsp.status}, ${await rsp.text()}`);
}
const results = (await rsp.json()).NBest;
if (results) {
return results[0].Lexical.trim();
}
}
async function transcribeAudio(audioUrl, lang) {
let solution;
const audioRsp = await fetch(audioUrl);
const audioContent = await prepareAudio(await audioRsp.arrayBuffer());
const {speechService, tryEnglishSpeechModel} = await storage.get([
'speechService',
'tryEnglishSpeechModel'
]);
if (['witSpeechApiDemo', 'witSpeechApi'].includes(speechService)) {
const language = captchaWitSpeechApiLangCodes[lang] || 'english';
const apiKey = await getWitSpeechApiKey(speechService, language);
if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'});
return;
}
const result = await getWitSpeechApiResult(apiKey, audioContent);
if (result.errorId) {
showNotification({
messageId: result.errorId,
timeout: result.errorTimeout
});
return;
}
solution = result.text;
if (!solution && language !== 'english' && tryEnglishSpeechModel) {
const apiKey = await getWitSpeechApiKey(speechService, 'english');
if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'});
return;
}
const result = await getWitSpeechApiResult(apiKey, audioContent);
if (result.errorId) {
showNotification({
messageId: result.errorId,
timeout: result.errorTimeout
});
return;
}
solution = result.text;
}
} else if (speechService === 'googleSpeechApi') {
const {googleSpeechApiKey: apiKey} = await storage.get(
'googleSpeechApiKey'
);
if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'});
return;
}
const language = captchaGoogleSpeechApiLangCodes[lang] || 'en-US';
solution = await getGoogleSpeechApiResult(
apiKey,
audioContent,
language,
tryEnglishSpeechModel
);
} else if (speechService === 'ibmSpeechApi') {
const {ibmSpeechApiUrl: apiUrl, ibmSpeechApiKey: apiKey} =
await storage.get(['ibmSpeechApiUrl', 'ibmSpeechApiKey']);
if (!apiUrl) {
showNotification({messageId: 'error_missingApiUrl'});
return;
}
if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'});
return;
}
const model = captchaIbmSpeechApiLangCodes[lang] || 'en-US_Multimedia';
solution = await getIbmSpeechApiResult(apiUrl, apiKey, audioContent, model);
if (
!solution &&
!['en-US_Multimedia', 'en-GB_Multimedia'].includes(model) &&
tryEnglishSpeechModel
) {
solution = await getIbmSpeechApiResult(
apiUrl,
apiKey,
audioContent,
'en-US_Multimedia'
);
}
} else if (speechService === 'microsoftSpeechApi') {
const {microsoftSpeechApiLoc: apiLocaction, microsoftSpeechApiKey: apiKey} =
await storage.get(['microsoftSpeechApiLoc', 'microsoftSpeechApiKey']);
if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'});
return;
}
const language = captchaMicrosoftSpeechApiLangCodes[lang] || 'en-US';
solution = await getMicrosoftSpeechApiResult(
apiLocaction,
apiKey,
audioContent,
language
);
if (
!solution &&
!['en-US', 'en-GB'].includes(language) &&
tryEnglishSpeechModel
) {
solution = await getMicrosoftSpeechApiResult(
apiLocaction,
apiKey,
audioContent,
'en-US'
);
}
}
if (!solution) {
if (['witSpeechApiDemo', 'witSpeechApi'].includes(speechService)) {
showNotification({
messageId: 'error_captchaNotSolvedWitai',
timeout: 60000
});
} else {
showNotification({messageId: 'error_captchaNotSolved', timeout: 6000});
}
} else {
return solution;
}
}
async function processMessage(request, sender) {
if (request.id === 'notification') {
showNotification({
message: request.message,
messageId: request.messageId,
title: request.title,
type: request.type,
timeout: request.timeout
});
} else if (request.id === 'captchaSolved') {
await processAppUse();
} else if (request.id === 'transcribeAudio') {
addBackgroundRequestListener();
try {
return await transcribeAudio(request.audioUrl, request.lang);
} finally {
removeBackgroundRequestListener();
}
} else if (request.id === 'resetCaptcha') {
await resetCaptcha(sender.tab.id, sender.frameId, request.challengeUrl);
} else if (request.id === 'getFramePos') {
return getFramePos(sender.tab.id, sender.frameId, request.frameIndex);
} else if (request.id === 'getOsScale') {
let zoom = await browser.tabs.getZoom(sender.tab.id);
const [[scale, windowWidth]] = await browser.tabs.executeScript(
sender.tab.id,
{
code: `[window.devicePixelRatio, window.innerWidth];`,
runAt: 'document_start'
}
);
if (targetEnv === 'firefox') {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1787649
function getImageElement(url) {
return new Promise(resolve => {
const img = new Image();
img.onload = () => {
resolve(img);
};
img.onerror = () => {
resolve();
};
img.onabort = () => {
resolve();
};
img.src = url;
});
}
const screenshotWidth = (
await getImageElement(
await browser.tabs.captureVisibleTab({
format: 'jpeg',
quality: 10
})
)
).naturalWidth;
if (Math.abs(screenshotWidth / windowWidth - scale * zoom) < 0.005) {
zoom = 1;
}
}
return scale / zoom;
} else if (request.id === 'startClientApp') {
nativePort = browser.runtime.connectNative('org.buster.client');
} else if (request.id === 'stopClientApp') {
if (nativePort) {
nativePort.disconnect();
}
} else if (request.id === 'messageClientApp') {
const message = {
apiVersion: clientAppVersion,
...request.message
};
return sendNativeMessage(nativePort, message);
} else if (request.id === 'openOptions') {
browser.runtime.openOptionsPage();
} else if (request.id === 'getPlatform') {
return getPlatform({fallback: false});
} else if (request.id === 'getBrowser') {
return getBrowser();
} else if (request.id === 'optionChange') {
await onOptionChange();
}
}
function onMessage(request, sender, sendResponse) {
const response = processMessage(request, sender);
return processMessageResponse(response, sendResponse);
}
async function onOptionChange() {
await setChallengeLocale();
}
async function onActionButtonClick(tab) {
await browser.runtime.openOptionsPage();
}
async function onInstall(details) {
if (
['chrome', 'edge', 'opera'].includes(targetEnv) &&
['install', 'update'].includes(details.reason)
) {
const tabs = await browser.tabs.query({
url: ['http://*/*', 'https://*/*'],
windowType: 'normal'
});
for (const tab of tabs) {
const tabId = tab.id;
const frames = await browser.webNavigation.getAllFrames({tabId});
for (const frame of frames) {
const frameId = frame.frameId;
if (frameId && recaptchaChallengeUrlRx.test(frame.url)) {
await browser.tabs.insertCSS(tabId, {
frameId,
runAt: 'document_idle',
file: '/src/solve/style.css'
});
await browser.tabs.executeScript(tabId, {
frameId,
runAt: 'document_idle',
file: '/src/solve/script.js'
});
}
}
}
const setupTabs = await browser.tabs.query({
url: 'http://127.0.0.1/buster/setup?session=*',
windowType: 'normal'
});
for (const tab of setupTabs) {
await browser.tabs.reload(tab.id);
}
}
}
function addBrowserActionListener() {
browser.browserAction.onClicked.addListener(onActionButtonClick);
}
function addMessageListener() {
browser.runtime.onMessage.addListener(onMessage);
}
function addInstallListener() {
browser.runtime.onInstalled.addListener(onInstall);
}
async function setup() {
if (!(await isStorageReady())) {
await migrateLegacyStorage();
await initStorage();
}
await setChallengeLocale();
}
function init() {
addBrowserActionListener();
addMessageListener();
addInstallListener();
setup();
}
init();