feat: simulate user input

This commit is contained in:
dessant 2019-02-18 07:14:32 +02:00
parent 3398166e83
commit 779f466a50
22 changed files with 992 additions and 84 deletions

View File

@ -1,5 +1,5 @@
{
"presets": ["@babel/env"],
"presets": [["@babel/env", {"useBuiltIns": "usage"}]],
"env": {
"production": {
"plugins": ["lodash"]

View File

@ -14,7 +14,7 @@
"build:prod:chrome": "cross-env TARGET_ENV=chrome yarn _build:prod",
"build:prod:firefox": "cross-env TARGET_ENV=firefox yarn _build:prod",
"build:prod:opera": "cross-env TARGET_ENV=opera yarn _build:prod",
"build:prod:all": "run-s 'build:@(chrome|firefox|opera)'",
"build:prod:all": "run-s 'build:prod:@(chrome|firefox|opera)'",
"_build:prod:zip": "yarn _build:prod && gulp zip",
"build:prod:zip:chrome": "cross-env TARGET_ENV=chrome yarn _build:prod:zip",
"build:prod:zip:firefox": "cross-env TARGET_ENV=firefox yarn _build:prod:zip",
@ -39,10 +39,12 @@
"@material/theme": "^0.30.0",
"@material/typography": "^0.28.0",
"audiobuffer-to-wav": "^1.0.0",
"bowser": "^2.1.0",
"ext-components": "dessant/ext-components#^0.1.6",
"ext-contribute": "dessant/ext-contribute#^0.1.6",
"storage-versions": "dessant/storage-versions#^0.2.4",
"typeface-roboto": "^0.0.54",
"uuid": "^3.3.2",
"vue": "^2.5.17",
"webextension-polyfill": "^0.3.1"
},

View File

@ -139,6 +139,11 @@
"description": "Title of the option."
},
"optionTitle_simulateUserInput": {
"message": "Simulate user input",
"description": "Title of the option."
},
"optionTitle_witSpeechApiLang": {
"message": "API language",
"description": "Title of the option."
@ -420,6 +425,16 @@
}
},
"inputLabel_appLocation": {
"message": "App location",
"description": "Label of the input."
},
"inputLabel_manifestLocation": {
"message": "Manifest location",
"description": "Label of the input."
},
"buttonText_addApi": {
"message": "Add API",
"description": "Text of the button."
@ -435,6 +450,61 @@
"description": "Text of the button."
},
"buttonText_downloadApp": {
"message": "Download app",
"description": "Text of the button."
},
"buttonText_installApp": {
"message": "Install app",
"description": "Text of the button."
},
"buttonText_goBack": {
"message": "Go back",
"description": "Text of the button."
},
"pageContent_optionClientAppDownloadDesc": {
"message": "Download and install the client app for user input simulation.",
"description": "Page content."
},
"pageContent_optionClientAppOSError": {
"message": "Your operating system is not supported.",
"description": "Page content."
},
"pageContent_installTitle": {
"message": "Install Buster Client",
"description": "Page content."
},
"pageContent_installDesc": {
"message": "The client app enables Buster to simulate user input and helps lower the occurrence of difficult challenges and temporary blocks.",
"description": "Page content."
},
"pageContent_installSuccessTitle": {
"message": "Installation finished",
"description": "Page content."
},
"pageContent_installSuccessDesc": {
"message": "The client app has been installed for the current browser.",
"description": "Page content."
},
"pageContent_installErrorTitle": {
"message": "Something went wrong",
"description": "Page content."
},
"pageContent_installErrorDesc": {
"message": "The installation has failed. Check the browser console for more details, and open an issue on GitHub if this error persists.",
"description": "Page content."
},
"pageTitle": {
"message": "$PAGETITLE$ - $EXTENSIONNAME$",
"description": "Title of the page.",
@ -475,6 +545,11 @@
"description": "Error message."
},
"error_missingNativeApp": {
"message": "Cannot connect to native app. Finish setting up the application or turn off user input simulation from the options page.",
"description": "Error message."
},
"error_internalError": {
"message": "Something went wrong. Open the browser console for more details.",
"description": "Error message."

View File

@ -2,13 +2,96 @@ import browser from 'webextension-polyfill';
import {initStorage} from 'storage/init';
import storage from 'storage/storage';
import {showNotification, showContributePage} from 'utils/app';
import {
showNotification,
showContributePage,
sendNativeMessage
} from 'utils/app';
import {
executeCode,
executeFile,
scriptsAllowed,
functionInContext
} from 'utils/common';
import {clientAppApiVersion} from 'utils/config';
let nativePort;
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};
}
async function resetCaptcha(tabId, frameId, challengeUrl) {
frameId = (await browser.webNavigation.getFrame({
tabId,
frameId: frameId
})).parentFrameId;
if (!(await scriptsAllowed(tabId, frameId))) {
await showNotification({messageId: 'error_scriptsNotAllowed'});
return;
}
if (!(await functionInContext('addListener', tabId, frameId))) {
await executeFile('/src/content/initReset.js', tabId, frameId);
}
await executeCode('addListener()', tabId, frameId);
await browser.tabs.sendMessage(
tabId,
{
id: 'resetCaptcha',
challengeUrl
},
{frameId}
);
}
function challengeRequestCallback(details) {
const url = new URL(details.url);
@ -19,12 +102,12 @@ function challengeRequestCallback(details) {
}
async function setChallengeLocale() {
const {loadEnglishChallenge} = await storage.get(
'loadEnglishChallenge',
const {loadEnglishChallenge, simulateUserInput} = await storage.get(
['loadEnglishChallenge', 'simulateUserInput'],
'sync'
);
if (loadEnglishChallenge) {
if (loadEnglishChallenge || simulateUserInput) {
if (
!browser.webRequest.onBeforeRequest.hasListener(challengeRequestCallback)
) {
@ -63,30 +146,21 @@ async function onMessage(request, sender) {
await showContributePage('use');
}
} else if (request.id === 'resetCaptcha') {
const tabId = sender.tab.id;
const frameId = (await browser.webNavigation.getFrame({
tabId,
frameId: sender.frameId
})).parentFrameId;
if (!(await scriptsAllowed(tabId, frameId))) {
await showNotification({messageId: 'error_scriptsNotAllowed'});
return;
await resetCaptcha(sender.tab.id, sender.frameId, request.challengeUrl);
} else if (request.id === 'getFramePos') {
return getFramePos(sender.tab.id, sender.frameId, request.index);
} else if (request.id === 'startNativeApp') {
nativePort = browser.runtime.connectNative('org.buster.client');
} else if (request.id === 'stopNativeApp') {
if (nativePort) {
nativePort.disconnect();
}
if (!(await functionInContext('addListener', tabId, frameId))) {
await executeFile('/src/content/initReset.js', tabId, frameId);
}
await executeCode('addListener()', tabId, frameId);
await browser.tabs.sendMessage(
tabId,
{
id: 'resetCaptcha',
challengeUrl: request.challengeUrl
},
{frameId}
);
} else if (request.id === 'sendNativeMessage') {
const message = {
apiVersion: clientAppApiVersion,
...request.message
};
return await sendNativeMessage(nativePort, message);
}
}

14
src/content/install.js Normal file
View File

@ -0,0 +1,14 @@
function install() {
const url = new URL(chrome.extension.getURL('/src/install/index.html'));
url.searchParams.set(
'session',
new URL(window.location.href).searchParams.get('session')
);
url.searchParams.set('port', window.location.port);
const frame = document.createElement('iframe');
frame.src = url.href;
document.body.appendChild(frame);
}
install();

View File

@ -1,11 +1,17 @@
(function() {
const onMessage = function(e) {
e.stopImmediatePropagation();
window.clearTimeout(timeoutId);
const challengeUrl = e.detail;
for (const [k, v] of Object.entries(___grecaptcha_cfg.clients)) {
if (v['O'].D.src === challengeUrl) {
grecaptcha.reset(k);
break;
for (const [k, client] of Object.entries(___grecaptcha_cfg.clients)) {
for (const [_, items] of Object.entries(client)) {
for (const [_, v] of Object.entries(items)) {
if (v instanceof Element && v.src === challengeUrl) {
grecaptcha.reset(k);
break;
}
}
}
}
};

239
src/install/App.vue Normal file
View File

@ -0,0 +1,239 @@
<!-- prettier-ignore -->
<template>
<div id="app" v-if="dataLoaded">
<div class="wrap" v-if="!isInstallSuccess && !isInstallError">
<div class="title">
{{ getText('buttonText_installApp') }}
</div>
<div class="desc">
{{ getText('pageContent_installDesc') }}
</div>
<v-textfield
v-model.trim="appDir"
:label="getText('inputLabel_appLocation')">
</v-textfield>
<v-textfield
v-model.trim="manifestDir"
:label="getText('inputLabel_manifestLocation')">
</v-textfield>
<v-button class="button install-button"
:unelevated="true"
:disabled="isInstalling || !appDir || !manifestDir"
@click="runInstall">
{{ getText('buttonText_installApp') }}
</v-button>
</div>
<div class="wrap" v-if="isInstallSuccess">
<div class="title">{{ getText('pageContent_installSuccessTitle') }}</div>
<div class="desc">{{ getText('pageContent_installSuccessDesc') }}</div>
<div class="success-icon">🎉</div>
</div>
<div class="wrap" v-if="isInstallError">
<div class="title error-title">{{ getText('pageContent_installErrorTitle') }}</div>
<div class="desc">{{ getText('pageContent_installErrorDesc') }}</div>
<v-button class="button error-button"
:unelevated="true"
@click="isInstallError = false">
{{ getText('buttonText_goBack') }}
</v-button>
</div>
</div>
</template>
<script>
import browser from 'webextension-polyfill';
import {Button, TextField} from 'ext-components';
import storage from 'storage/storage';
import {pingClientApp} from 'utils/app';
import {getText, getBrowser} from 'utils/common';
import {targetEnv} from 'utils/config';
export default {
components: {
[Button.name]: Button,
[TextField.name]: TextField
},
data: function() {
const urlParams = new URL(window.location.href).searchParams;
const apiURL = new URL('http://127.0.0.1/api/v1');
apiURL.port = urlParams.get('port');
return {
dataLoaded: false,
apiUrl: apiURL.href,
session: urlParams.get('session'),
appDir: '',
manifestDir: '',
isInstalling: false,
isInstallSuccess: false,
isInstallError: false
};
},
methods: {
getText,
getExtensionId: function() {
let id = browser.runtime.id;
if (targetEnv !== 'firefox') {
const scheme = window.location.protocol;
id = `${scheme}//${id}/`;
}
return id;
},
setLocation: async function() {
try {
await this.location();
} catch (err) {
this.isInstallError = true;
console.log(err.toString());
}
},
runInstall: async function() {
this.isInstalling = true;
try {
await this.install();
} catch (err) {
this.isInstallError = true;
console.log(err.toString());
} finally {
this.isInstalling = false;
}
},
location: async function() {
const data = new FormData();
data.append('session', this.session);
data.append('browser', (await getBrowser()).name);
data.append('targetEnv', targetEnv);
const rsp = await fetch(`${this.apiUrl}/install/location`, {
referrer: '',
mode: 'cors',
method: 'POST',
body: data
});
const results = await rsp.json();
if (rsp.status === 200) {
this.appDir = results.appDir;
this.manifestDir = results.manifestDir;
} else {
throw new Error(results.error);
}
},
install: async function() {
const data = new FormData();
data.append('session', this.session);
data.append('appDir', this.appDir);
data.append('manifestDir', this.manifestDir);
data.append('targetEnv', targetEnv);
data.append('extension', this.getExtensionId());
const rsp = await fetch(`${this.apiUrl}/install/run`, {
referrer: '',
mode: 'cors',
method: 'POST',
body: data
});
if (rsp.status === 200) {
await pingClientApp();
await storage.set({simulateUserInput: true}, 'sync');
this.isInstallSuccess = true;
} else {
throw new Error((await rsp.json()).error);
}
}
},
created: async function() {
await this.setLocation();
this.dataLoaded = true;
}
};
</script>
<style lang="scss">
$mdc-theme-primary: #1abc9c;
@import '@material/theme/mixins';
@import '@material/typography/mixins';
@import '@material/button/mixins';
body {
@include mdc-typography-base;
font-size: 100%;
background-color: #ecf0f1;
margin: 0;
}
#app {
display: flex;
justify-content: center;
padding: 12px;
}
.wrap {
display: flex;
flex-direction: column;
max-width: 400px;
}
.title,
.desc {
@include mdc-theme-prop('color', 'text-primary-on-light');
}
.title {
@include mdc-typography('title');
margin-top: 48px;
}
.error-title {
color: #e74c3c;
}
.desc {
@include mdc-typography('body1');
margin-top: 24px;
margin-bottom: 24px;
}
.button {
@include mdc-button-ink-color(#fff);
width: 200px;
height: 48px;
}
.install-button {
margin-top: 24px;
}
.error-button {
margin-top: 12px;
}
.success-icon {
font-size: 72px;
margin-top: 36px;
}
</style>

18
src/install/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/src/icons/app/icon-16.png" type="image/png">
<link href="/src/fonts/roboto.css" rel="stylesheet">
<link href="/src/commons-ui/style.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script src="/src/manifest.js"></script>
<script src="/src/commons-ui/script.js"></script>
<script src="script.js"></script>
</body>
</html>

20
src/install/main.js Normal file
View File

@ -0,0 +1,20 @@
import Vue from 'vue';
import App from './App';
async function init() {
try {
await document.fonts.load('400 14px Roboto');
await document.fonts.load('500 14px Roboto');
} catch (e) {}
const vm = new Vue({
el: '#app',
render: h => h(App)
});
}
// only run in a frame
if (window.top !== window) {
init();
}

View File

@ -20,14 +20,16 @@
"storage",
"tabs",
"activeTab",
"contextMenus",
"notifications",
"webRequest",
"webRequestBlocking",
"webNavigation",
"nativeMessaging",
"<all_urls>"
],
"content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src *; object-src 'none';",
"content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src *; object-src 'none'; frame-ancestors http://127.0.0.1:*;",
"icons": {
"16": "src/icons/app/icon-16.png",
@ -48,6 +50,11 @@
"run_at": "document_idle",
"css": ["src/solve/style.css"],
"js": ["src/manifest.js", "src/solve/script.js"]
},
{
"matches": ["http://127.0.0.1/buster/install?session=*"],
"run_at": "document_idle",
"js": ["src/content/install.js"]
}
],
@ -62,5 +69,5 @@
"page": "src/background/index.html"
},
"web_accessible_resources": ["src/content/reset.js"]
"web_accessible_resources": ["src/install/index.html", "src/content/reset.js"]
}

View File

@ -85,12 +85,40 @@
<v-switch id="lec" v-model="options.loadEnglishChallenge"></v-switch>
</v-form-field>
</div>
<div class="option">
<div class="option"
v-if="!options.loadEnglishChallenge">
<v-form-field input-id="esm"
:label="getText('optionTitle_tryEnglishSpeechModel')">
<v-switch id="esm" v-model="options.tryEnglishSpeechModel"></v-switch>
</v-form-field>
</div>
<div class="option">
<v-form-field input-id="si"
:label="getText('optionTitle_simulateUserInput')">
<v-switch id="si" v-model="options.simulateUserInput"></v-switch>
</v-form-field>
</div>
<div class="client-bownload" v-if="showClientAppNotice">
<div class="download-desc">
{{ getText('pageContent_optionClientAppDownloadDesc') }}
</div>
<div class="download-error" v-if="!clientAppDownloadUrl">
{{ getText('pageContent_optionClientAppOSError') }}
</div>
<v-button class="download-button"
:unelevated="true"
:disabled="!clientAppDownloadUrl"
@click="$refs.dlLink.click()">
{{ getText('buttonText_downloadApp') }}
</v-button>
<a ref="dlLink" class="download-link"
target="_blank"
rel="noreferrer"
:href="clientAppDownloadUrl"></a>
</div>
</div>
</div>
</div>
@ -101,9 +129,13 @@ import browser from 'webextension-polyfill';
import {Button, Select, Switch, FormField, TextField} from 'ext-components';
import storage from 'storage/storage';
import {getOptionLabels} from 'utils/app';
import {getText} from 'utils/common';
import {optionKeys, captchaWitSpeechApiLangCodes} from 'utils/data';
import {getOptionLabels, pingClientApp} from 'utils/app';
import {getText, getPlatform} from 'utils/common';
import {
optionKeys,
clientAppPlatforms,
captchaWitSpeechApiLangCodes
} from 'utils/data';
export default {
components: {
@ -154,6 +186,9 @@ export default {
witSpeechApiLang: '',
witSpeechApis: [],
showClientAppNotice: false,
clientAppDownloadUrl: '',
options: {
speechService: '',
googleSpeechApiKey: '',
@ -163,11 +198,37 @@ export default {
microsoftSpeechApiKey: '',
witSpeechApiKeys: {},
loadEnglishChallenge: false,
tryEnglishSpeechModel: false
tryEnglishSpeechModel: false,
simulateUserInput: false
}
};
},
watch: {
'options.simulateUserInput': async function(value) {
if (value) {
try {
await pingClientApp();
} catch (e) {
if (!this.clientAppDownloadUrl) {
const {os, arch} = await getPlatform();
if (clientAppPlatforms.includes(`${os}/${arch}`)) {
this.clientAppDownloadUrl = `https://github.com/dessant/buster-client/releases/download/v0.1.0/buster-client-v0.1.0-${os}-${arch}`;
if (os === 'windows') {
this.clientAppDownloadUrl += '.exe';
}
}
}
this.showClientAppNotice = true;
return;
}
}
this.showClientAppNotice = false;
}
},
methods: {
getText,
@ -224,6 +285,7 @@ $mdc-theme-primary: #1abc9c;
@import '@material/theme/mixins';
@import '@material/typography/mixins';
@import '@material/button/mixins';
body {
@include mdc-typography-base;
@ -289,4 +351,31 @@ body {
align-self: end;
margin-left: 36px;
}
.client-bownload {
margin-left: 48px;
}
.download-desc,
.download-error {
@include mdc-theme-prop('color', 'text-primary-on-light');
@include mdc-typography('body1');
min-width: 300px;
}
.download-error {
margin-top: 12px;
color: #e74c3c;
}
.download-link {
visibility: hidden;
}
.download-button {
@include mdc-button-ink-color(#fff);
width: 200px;
height: 48px;
margin-top: 24px;
}
</style>

View File

@ -2,7 +2,13 @@ import browser from 'webextension-polyfill';
import audioBufferToWav from 'audiobuffer-to-wav';
import storage from 'storage/storage';
import {getText, waitForElement, arrayBufferToBase64} from 'utils/common';
import {meanSleep} from 'utils/app';
import {
getText,
waitForElement,
arrayBufferToBase64,
getRandomFloat
} from 'utils/common';
import {
captchaGoogleSpeechApiLangCodes,
captchaIbmSpeechApiLangCodes,
@ -47,11 +53,6 @@ function syncUI() {
button.id = 'reset-button';
button.addEventListener('click', resetCaptcha);
button.addEventListener('keydown', e => {
if (['Enter', ' '].includes(e.key)) {
resetCaptcha();
}
});
div.appendChild(button);
document.querySelector('.rc-footer').appendChild(div);
@ -75,12 +76,7 @@ function syncUI() {
button.classList.add('working');
}
button.addEventListener('click', start);
button.addEventListener('keydown', e => {
if (['Enter', ' '].includes(e.key)) {
start(e);
}
});
button.addEventListener('click', solveChallenge);
div.appendChild(button);
document.querySelector('.rc-buttons').appendChild(div);
@ -138,6 +134,112 @@ function dispatchEnter(node) {
node.click();
}
async function navigateToElement(node, {forward = true} = {}) {
if (document.activeElement === node) {
return;
}
if (!forward) {
await sendNativeMessage({command: 'pressKey', data: 'shift'});
await meanSleep(300);
}
while (document.activeElement !== node) {
await sendNativeMessage({command: 'tapKey', data: 'tab'});
await meanSleep(300);
}
if (!forward) {
await sendNativeMessage({command: 'releaseKey', data: 'shift'});
await meanSleep(300);
}
}
async function tapEnter(node, {navigateForward = true} = {}) {
await navigateToElement(node, {forward: navigateForward});
await meanSleep(200);
await sendNativeMessage({command: 'tapKey', data: 'enter'});
}
async function clickElement(node, browserBorder) {
const targetPos = await getClickPos(node, browserBorder);
await sendNativeMessage({command: 'moveMouse', ...targetPos});
await meanSleep(100);
await sendNativeMessage({command: 'clickMouse'});
}
async function sendNativeMessage(message) {
const rsp = await browser.runtime.sendMessage({
id: 'sendNativeMessage',
message
});
if (!rsp.success) {
throw new Error(`Native response: ${rsp.text}`);
}
return rsp;
}
async function getBrowserBorder(clickEvent) {
const framePos = await getFrameClientPos();
const scale = window.devicePixelRatio;
return {
left:
clickEvent.screenX -
clickEvent.clientX * scale -
framePos.x -
window.screenX * scale,
top:
clickEvent.screenY -
clickEvent.clientY * scale -
framePos.y -
window.screenY * scale
};
}
async function getFrameClientPos() {
if (window !== window.top) {
let index;
const siblingWindows = window.parent.frames;
for (let i = 0; i < siblingWindows.length; i++) {
if (siblingWindows[i] === window) {
index = i;
break;
}
}
return await browser.runtime.sendMessage({id: 'getFramePos', index});
}
return {x: 0, y: 0};
}
// window.devicePixelRatio
async function getElementScreenRect(node, browserBorder) {
let {left: x, top: y, width, height} = node.getBoundingClientRect();
const data = await getFrameClientPos();
const scale = window.devicePixelRatio;
x *= scale;
y *= scale;
x += data.x + browserBorder.left + window.screenX * scale;
y += data.y + browserBorder.top + window.screenY * scale;
return {x, y, width: width * scale, height: height * scale};
}
async function getClickPos(node, browserBorder) {
let {x, y, width, height} = await getElementScreenRect(node, browserBorder);
return {
x: Math.round(x + width * getRandomFloat(0.4, 0.6)),
y: Math.round(y + height * getRandomFloat(0.4, 0.6))
};
}
async function getWitSpeechApiKey(speechService, language) {
if (speechService === 'witSpeechApiDemo') {
return witApiKeys[language];
@ -223,25 +325,42 @@ async function getMicrosoftSpeechApiResult(
}
}
async function solve() {
let audioUrl;
async function solve(simulateUserInput, clickEvent) {
let solution;
if (isBlocked()) {
return;
}
const audioEl = document.querySelector('#audio-source');
if (audioEl) {
audioUrl = audioEl.src;
} else {
dispatchEnter(document.querySelector('button#recaptcha-audio-button'));
let browserBorder;
let useMouse = true;
if (simulateUserInput) {
if (clickEvent.clientX || clickEvent.clientY) {
browserBorder = await getBrowserBorder(clickEvent);
} else {
useMouse = false;
}
}
const audioLinkSelector = 'a.rc-audiochallenge-tdownload-link';
let audioEl = document.querySelector(audioLinkSelector);
if (!audioEl) {
const audioButton = document.querySelector('#recaptcha-audio-button');
if (simulateUserInput) {
if (useMouse) {
await clickElement(audioButton, browserBorder);
} else {
await tapEnter(audioButton, {navigateForward: false});
}
} else {
dispatchEnter(audioButton);
}
const result = await Promise.race([
new Promise(resolve => {
waitForElement('#audio-source', {timeout: 10000}).then(el =>
resolve({audioUrl: el && el.src})
);
waitForElement(audioLinkSelector, {timeout: 10000}).then(el => {
meanSleep(500).then(() => resolve({audioEl: el}));
});
}),
new Promise(resolve => {
isBlocked({timeout: 10000}).then(blocked => resolve({blocked}));
@ -252,9 +371,27 @@ async function solve() {
return;
}
audioUrl = result.audioUrl;
audioEl = result.audioEl;
}
if (simulateUserInput) {
if (useMouse) {
audioEl.addEventListener('click', e => e.preventDefault(), {
capture: true,
once: true
});
await clickElement(audioEl, browserBorder);
} else {
audioEl.addEventListener('keydown', e => e.preventDefault(), {
capture: true,
once: true
});
await tapEnter(audioEl);
}
}
const audioUrl = audioEl.href;
const lang = document.documentElement.lang;
const audioRsp = await fetch(audioUrl, {referrer: ''});
const audioContent = await prepareAudio(await audioRsp.arrayBuffer());
@ -422,35 +559,81 @@ async function solve() {
return;
}
document.querySelector('#audio-response').value = solution;
dispatchEnter(document.querySelector('#recaptcha-verify-button'));
const input = document.querySelector('#audio-response');
if (simulateUserInput) {
if (useMouse) {
await clickElement(input, browserBorder);
} else {
await navigateToElement(input, {forward: false});
}
await meanSleep(200);
await sendNativeMessage({command: 'typeText', data: solution});
} else {
input.value = solution;
}
const submitButton = document.querySelector('#recaptcha-verify-button');
if (simulateUserInput) {
if (useMouse) {
await clickElement(submitButton, browserBorder);
} else {
await tapEnter(submitButton);
}
} else {
dispatchEnter(submitButton);
}
browser.runtime.sendMessage({id: 'captchaSolved'});
}
function start(e) {
e.preventDefault();
e.stopImmediatePropagation();
function solveChallenge(ev) {
ev.preventDefault();
ev.stopImmediatePropagation();
if (solverWorking) {
if (!ev.isTrusted || solverWorking) {
return;
}
setSolverState({working: true});
solve()
.then(() => {
setSolverState({working: false});
})
runSolver(ev)
.catch(err => {
setSolverState({working: false});
console.log(err.toString());
browser.runtime.sendMessage({
id: 'notification',
messageId: 'error_internalError'
});
console.log(err.toString());
throw err;
})
.finally(() => {
setSolverState({working: false});
});
}
async function runSolver(ev) {
const {simulateUserInput} = await storage.get('simulateUserInput', 'sync');
if (simulateUserInput) {
try {
await browser.runtime.sendMessage({id: 'startNativeApp'});
} catch (err) {
browser.runtime.sendMessage({
id: 'notification',
messageId: 'error_missingNativeApp'
});
return;
}
}
try {
await solve(simulateUserInput, ev);
} finally {
if (simulateUserInput) {
await browser.runtime.sendMessage({id: 'stopNativeApp'});
}
}
}
function init() {
const observer = new MutationObserver(syncUI);
observer.observe(document.body, {

View File

@ -0,0 +1,27 @@
import browser from 'webextension-polyfill';
const message = 'Add simulateUserInput option';
const revision = 't335iRDhZ8';
const downRevision = 'ZtLMLoh1ag';
const storage = browser.storage.local;
async function upgrade() {
const changes = {
simulateUserInput: false
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['simulateUserInput']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

View File

@ -4,6 +4,7 @@
"ONiJBs00o",
"UidMDYaYA",
"nOedd0Txqd",
"ZtLMLoh1ag"
"ZtLMLoh1ag",
"t335iRDhZ8"
]
}

View File

@ -0,0 +1,27 @@
import browser from 'webextension-polyfill';
const message = 'Add simulateUserInput option';
const revision = 't335iRDhZ8';
const downRevision = 'ZtLMLoh1ag';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
simulateUserInput: false
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['simulateUserInput']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

View File

@ -4,6 +4,7 @@
"ONiJBs00o",
"UidMDYaYA",
"nOedd0Txqd",
"ZtLMLoh1ag"
"ZtLMLoh1ag",
"t335iRDhZ8"
]
}

View File

@ -1,6 +1,13 @@
import browser from 'webextension-polyfill';
import uuidV4 from 'uuid/v4';
import {getText, createTab, getActiveTab} from 'utils/common';
import {
getText,
createTab,
getActiveTab,
getRandomInt,
sleep
} from 'utils/common';
function showNotification({message, messageId, title, type = 'info'}) {
if (!title) {
@ -40,4 +47,58 @@ async function showContributePage(action = false) {
await createTab(url, {index: activeTab.index + 1});
}
export {showNotification, getOptionLabels, showContributePage};
function meanSleep(ms) {
const maxDeviation = (10 / 100) * ms;
return sleep(getRandomInt(ms - maxDeviation, ms + maxDeviation));
}
function sendNativeMessage(port, message, {timeout = 10000} = {}) {
return new Promise((resolve, reject) => {
const id = uuidV4();
message.id = id;
const messageCallback = function(msg) {
if (msg.id !== id) {
return;
}
removeListeners();
resolve(msg);
};
const errorCallback = function() {
removeListeners();
reject('No response from native app');
};
const removeListeners = function() {
window.clearTimeout(timeoutId);
port.onMessage.removeListener(messageCallback);
port.onDisconnect.removeListener(errorCallback);
};
const timeoutId = window.setTimeout(function() {
errorCallback();
}, timeout);
port.onMessage.addListener(messageCallback);
port.onDisconnect.addListener(errorCallback);
port.postMessage(message);
});
}
async function pingClientApp() {
await browser.runtime.sendMessage({id: 'startNativeApp'});
await browser.runtime.sendMessage({
id: 'sendNativeMessage',
message: {command: 'ping'}
});
await browser.runtime.sendMessage({id: 'stopNativeApp'});
}
export {
showNotification,
getOptionLabels,
showContributePage,
meanSleep,
sendNativeMessage,
pingClientApp
};

View File

@ -1,4 +1,5 @@
import browser from 'webextension-polyfill';
import Bowser from 'bowser';
import {targetEnv} from 'utils/config';
@ -23,6 +24,40 @@ async function isAndroid() {
return os === 'android';
}
async function getPlatform() {
let {os, arch} = await browser.runtime.getPlatformInfo();
if (os === 'win') {
os = 'windows';
} else if (os === 'mac') {
os = 'macos';
}
if (arch === 'x86-32') {
arch = '386';
} else if (arch === 'x86-64') {
arch = 'amd64';
}
return {os, arch};
}
async function getBrowser() {
let name, version;
try {
({name, version} = await browser.runtime.getBrowserInfo());
} catch (e) {}
if (!name) {
({name, version} = Bowser.getParser(
window.navigator.userAgent
).getBrowser());
}
name = name.toLowerCase();
return {name, version};
}
async function getActiveTab() {
const [tab] = await browser.tabs.query({
lastFocusedWindow: true,
@ -113,15 +148,32 @@ async function functionInContext(
return isFunction;
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
}
function sleep(ms) {
return new Promise(resolve => window.setTimeout(resolve, ms));
}
export {
getText,
createTab,
isAndroid,
getPlatform,
getBrowser,
getActiveTab,
waitForElement,
arrayBufferToBase64,
executeCode,
executeFile,
scriptsAllowed,
functionInContext
functionInContext,
getRandomInt,
getRandomFloat,
sleep
};

View File

@ -1,5 +1,7 @@
const targetEnv = process.env.TARGET_ENV;
const clientAppApiVersion = '1';
const witApiKeys = {
afrikaans: 'T3T7A2WS3TQJVBB4L4CTK2EEUI6N7YGZ',
arabic: 'AD6RLFYBWRGGJD76SWKALZMUFVGMVCTB',
@ -55,4 +57,4 @@ const witApiKeys = {
zulu: 'B6OMGRZUYIJ5WLDQZODKCFCXCTH7PHB3'
};
export {targetEnv, witApiKeys};
export {targetEnv, clientAppApiVersion, witApiKeys};

View File

@ -7,9 +7,12 @@ const optionKeys = [
'microsoftSpeechApiKey',
'witSpeechApiKeys',
'loadEnglishChallenge',
'tryEnglishSpeechModel'
'tryEnglishSpeechModel',
'simulateUserInput'
];
const clientAppPlatforms = ['windows/amd64', 'linux/amd64', 'macos/amd64'];
// https://developers.google.com/recaptcha/docs/language
// https://cloud.google.com/speech-to-text/docs/languages
const captchaGoogleSpeechApiLangCodes = {
@ -341,6 +344,7 @@ const microsoftSpeechApiUrls = {
export {
optionKeys,
clientAppPlatforms,
captchaGoogleSpeechApiLangCodes,
captchaIbmSpeechApiLangCodes,
captchaMicrosoftSpeechApiLangCodes,

View File

@ -29,7 +29,8 @@ module.exports = {
background: './src/background/main.js',
options: './src/options/main.js',
contribute: './src/contribute/main.js',
solve: './src/solve/main.js'
solve: './src/solve/main.js',
install: './src/install/main.js'
},
output: {
path: path.resolve(__dirname, 'dist', targetEnv, 'src'),
@ -45,7 +46,7 @@ module.exports = {
commonsUi: {
name: 'commons-ui',
chunks: chunk => {
return ['options', 'contribute'].includes(chunk.name);
return ['options', 'contribute', 'install'].includes(chunk.name);
},
minChunks: 2
}

View File

@ -2353,6 +2353,11 @@ boom@2.x.x:
dependencies:
hoek "2.x.x"
bowser@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.1.0.tgz#76cc094f97578ba4858fb4359445ee1317d1be6f"
integrity sha512-tP90ci4QY8PRBQjU0+iTsoO3DMNYtXCM0aVxeKhjxXF8IH9xTXUmjcTECPN+y5v0BGeRDfMcSLeohPiUZuz37g==
boxen@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"