├── xilriws ├── js │ ├── __init__.py │ ├── load.py │ └── recaptcha.py ├── ptc │ ├── __init__.py │ └── ptc_utils.py ├── mode │ ├── __init__.py │ ├── basic_mode.py │ ├── cion_mode.py │ └── auth_mode.py ├── browser │ ├── __init__.py │ ├── browser_join.py │ ├── browser_auth.py │ └── browser.py ├── debug.py ├── constants.py ├── __init__.py ├── ptc_join.py ├── proxy_dispenser.py ├── task_creator.py ├── extension_comm.py ├── proxy.py ├── reese_cookie.py └── ptc_auth.py ├── config.json.example ├── app_cion.py ├── package.json ├── docker-compose.yml.example ├── .gitignore ├── xilriws-targetfp ├── background.js ├── contentScript.js ├── funcToString.js ├── inject.js ├── utils.js ├── general.js ├── manifest.json ├── canvas.js ├── webgl.js └── screen.js ├── xilriws-proxy ├── manifest.json └── background.js ├── pyproject.toml ├── README.md ├── DockerfileCion ├── .github └── workflows │ ├── build_xilriws.yml │ └── build_cion_xilriws.yml ├── Dockerfile └── app.py /xilriws/js/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xilriws/ptc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "host": "0.0.0.0", 3 | "port": 5090 4 | } -------------------------------------------------------------------------------- /app_cion.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import app 4 | 5 | asyncio.run(app.main(cion_mode=True)) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@types/chrome": "^0.0.266" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /xilriws/mode/__init__.py: -------------------------------------------------------------------------------- 1 | from .cion_mode import CionMode 2 | from .auth_mode import AuthMode 3 | -------------------------------------------------------------------------------- /xilriws/browser/__init__.py: -------------------------------------------------------------------------------- 1 | from .browser import Browser 2 | from .browser_auth import BrowserAuth 3 | from .browser_join import BrowserJoin, CionResponse 4 | -------------------------------------------------------------------------------- /xilriws/debug.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | parser = argparse.ArgumentParser() 5 | parser.add_argument("--debug", action="store_true") 6 | args = parser.parse_args() 7 | 8 | IS_DEBUG = args.debug 9 | -------------------------------------------------------------------------------- /xilriws/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | ACCESS_URL = "https://access.pokemon.com/" 4 | JOIN_URL = "https://join.pokemon.com/" 5 | 6 | EXPIRATION = 18 * 60 7 | MAX_USES = 7 8 | COOKIE_STORAGE = 2 9 | AUTH_TIMEOUT = 60 10 | -------------------------------------------------------------------------------- /docker-compose.yml.example: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | xilriws: 5 | image: ghcr.io/unownhash/xilriws:main 6 | restart: unless-stopped 7 | volumes: 8 | - ./proxies.txt:/xilriws/proxies.txt 9 | ports: 10 | - "5090:5090" 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | **/__pycache__/** 3 | config.json 4 | docker-compose.yml 5 | dist/* 6 | build/* 7 | *.spec 8 | requirements.txt 9 | fingerprint-random* 10 | xilriws-cookie-delete* 11 | test 12 | /*.js 13 | /*.txt 14 | node_modules/* 15 | test* 16 | chrome_win_fp.json 17 | xilriws/companion.py 18 | -------------------------------------------------------------------------------- /xilriws-targetfp/background.js: -------------------------------------------------------------------------------- 1 | chrome.webNavigation.onCompleted.addListener((details) => { 2 | console.log(details.tabId) 3 | }) 4 | 5 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 6 | if (message === "getTabId") { 7 | sendResponse(sender.tab.id) 8 | } 9 | return true 10 | }) 11 | -------------------------------------------------------------------------------- /xilriws/mode/basic_mode.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Protocol 4 | 5 | from litestar import Litestar 6 | 7 | 8 | class BasicMode(Protocol): 9 | async def prepare(self) -> None: 10 | pass 11 | 12 | def get_litestar(self) -> Litestar: 13 | pass 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /xilriws-targetfp/contentScript.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const tabId = await chrome.runtime.sendMessage("getTabId") 3 | const div = document.createElement("div") 4 | div.setAttribute("data-xil-tab-id", tabId) 5 | 6 | while (!document.body) { 7 | console.log("body is null, waiting 0.1s") 8 | await new Promise(resolve => setTimeout(resolve, 100)) 9 | } 10 | document.body.appendChild(div) 11 | })() -------------------------------------------------------------------------------- /xilriws-proxy/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "xilriws-proxy", 4 | "version": "1.0", 5 | "permissions": [ 6 | "proxy", 7 | "tabs", 8 | "unlimitedStorage", 9 | "storage", 10 | "", 11 | "webRequest", 12 | "webRequestBlocking", 13 | "devtools", 14 | "cookies", 15 | "storage" 16 | ], 17 | "background": { 18 | "scripts": ["background.js"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "xilriws" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Malte <42342921+ccev@users.noreply.github.com>"] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.10,<4" 10 | litestar = {extras = ["standard"], version = "^2.7.0"} 11 | httpx = "^0.27.0" 12 | loguru = "^0.7.2" 13 | websockets = "^14.0" 14 | curl-cffi = "0.6.3" 15 | zendriver = "^0.14.2" 16 | 17 | 18 | [build-system] 19 | requires = ["poetry-core"] 20 | build-backend = "poetry.core.masonry.api" 21 | 22 | -------------------------------------------------------------------------------- /xilriws/js/load.py: -------------------------------------------------------------------------------- 1 | SRC = """ 2 | ReCaptchaLoader.load('6LdD428fAAAAAONHdz5Ltgi-mOyfN_QUPj9JDb7O') 3 | .then(recaptcha => { 4 | return Promise.all([ 5 | recaptcha.execute('post/create_user'), 6 | recaptcha.execute('post/create_user'), 7 | 8 | recaptcha.execute('post/activate_user'), 9 | recaptcha.execute('post/activate_user'), 10 | ]).then(( 11 | [c1, 12 | c2, 13 | a1, 14 | a2 15 | ]) => { 16 | return { 17 | 'create': [c1, c2], 18 | 'activate': [a1, a2] 19 | } 20 | }) 21 | }) 22 | """ -------------------------------------------------------------------------------- /xilriws-targetfp/funcToString.js: -------------------------------------------------------------------------------- 1 | const realToString = Function.prototype.toString; 2 | const realToLocaleString = Function.prototype.toLocaleString; 3 | const fakeSources = new WeakMap(); 4 | 5 | Function.prototype.toString = function () { 6 | if (fakeSources.has(this)) { 7 | return fakeSources.get(this); 8 | } 9 | return realToString.call(this); 10 | }; 11 | 12 | Function.prototype.toLocaleString = function () { 13 | if (fakeSources.has(this)) { 14 | return fakeSources.get(this); 15 | } 16 | return realToLocaleString.call(this); 17 | }; 18 | 19 | export function set(func) { 20 | fakeSources.set(func, `function ${func.name}() { [native code] }`); 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xilriws 2 | 3 | ## Installation 4 | 5 | 1. `mkdir xilriws && cd xilriws` 6 | 2. `wget https://raw.githubusercontent.com/UnownHash/Xilriws/refs/heads/main/docker-compose.yml.example -O docker-compose.yml` 7 | 3. `touch proxies.txt` file. Each line should have one proxy url. (i.e. `ip:port` or `http://user:pass@ip:port`) 8 | 4. `docker compose pull`, then `docker compose up -d` 9 | 10 | in your Dragonite config, add: 11 | 12 | ```toml 13 | [general] 14 | remote_auth_url = "http://xilriws:5090/api/v1/login-code" 15 | ``` 16 | 17 | this assumes you have everything in the same docker network. if you're hosting it externally, change the hostname to 18 | something else accordingly. 19 | 20 | To update: `docker compose pull && docker compose restart` 21 | -------------------------------------------------------------------------------- /DockerfileCion: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | RUN apt update -y && \ 3 | apt clean 4 | 5 | WORKDIR /xilriws 6 | 7 | ENV DEBIAN_FRONTEND noninteractive 8 | 9 | RUN apt install -y software-properties-common 10 | RUN apt update && apt install -y python3 python3-venv 11 | RUN python3 -m venv /opt/venv 12 | ENV PATH="/opt/venv/bin:$PATH" 13 | RUN pip install --upgrade pip==25.0.1 14 | RUN pip install poetry 15 | 16 | RUN apt install -y wget 17 | 18 | RUN wget -q -O - https://github.com/NDViet/google-chrome-stable/releases/download/125.0.6422.141-1/google-chrome-stable_125.0.6422.141-1_amd64.deb > ./chrome.deb 19 | RUN apt install -y ./chrome.deb 20 | RUN rm ./chrome.deb 21 | 22 | COPY . . 23 | RUN poetry install --no-root 24 | 25 | ENTRYPOINT ["poetry", "run", "python", "app_cion.py"] 26 | -------------------------------------------------------------------------------- /xilriws/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import sys 5 | 6 | from loguru import logger 7 | from .debug import IS_DEBUG 8 | 9 | console_format = " | ".join( 10 | ( 11 | "{time:HH:mm:ss.SS}", 12 | "{level: >1.1}", 13 | "{extra[name]: <10.10}", 14 | "{message}", 15 | ) 16 | ) 17 | 18 | logger.remove() 19 | 20 | logger.add( 21 | sink=sys.stdout, 22 | format=console_format, 23 | colorize=True, 24 | level=logging.DEBUG if IS_DEBUG else logging.INFO, 25 | filter=lambda record: record["level"].no < logging.ERROR, 26 | enqueue=True, 27 | ) 28 | logger.add(sink=sys.stderr, format=console_format, colorize=True, level=logging.ERROR, backtrace=True, enqueue=True) 29 | -------------------------------------------------------------------------------- /xilriws/ptc_join.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from .browser import Browser, CionResponse, BrowserJoin 3 | from .task_creator import task_creator 4 | from loguru import logger 5 | import asyncio 6 | from .proxy_dispenser import ProxyDispenser 7 | from .proxy import ProxyDistributor, Proxy 8 | from time import time 9 | 10 | logger = logger.bind(name="Tokens") 11 | 12 | 13 | class PtcJoin: 14 | def __init__(self, browser: BrowserJoin): 15 | self.browser = browser 16 | self.responses: list[CionResponse] = [] 17 | self.last_cion_call = time() 18 | self.is_running = False 19 | 20 | async def get_join_tokens(self, proxy: str | None) -> CionResponse | None: 21 | self.is_running = True 22 | 23 | try: 24 | logger.info(f"Getting cion tokens using proxy {proxy}") 25 | resp = await self.browser.get_join_tokens(Proxy(proxy)) 26 | self.is_running = False 27 | return resp 28 | except Exception as e: 29 | logger.exception("unhandled exception while getting tokens", e) 30 | 31 | self.is_running = False 32 | -------------------------------------------------------------------------------- /.github/workflows/build_xilriws.yml: -------------------------------------------------------------------------------- 1 | name: build_xilriws 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: ${{ github.repository }} 9 | 10 | jobs: 11 | build-xilriws: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v2 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v2 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Extract metadata (tags, labels) for Docker 30 | id: meta 31 | uses: docker/metadata-action@v5 32 | with: 33 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 34 | - name: Build and push Docker image 35 | uses: docker/build-push-action@v5 36 | with: 37 | context: . 38 | labels: ${{ steps.meta.outputs.labels }} 39 | platforms: linux/amd64 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | -------------------------------------------------------------------------------- /.github/workflows/build_cion_xilriws.yml: -------------------------------------------------------------------------------- 1 | name: build_cion_xilriws 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | CION_IMAGE_NAME: ${{ github.repository }}/cion 9 | 10 | jobs: 11 | build-cion-xilriws: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v2 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v2 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Extract metadata (tags, labels) for Docker 30 | id: meta 31 | uses: docker/metadata-action@v5 32 | with: 33 | images: ${{ env.REGISTRY }}/${{ env.CION_IMAGE_NAME }} 34 | - name: Build and push Docker image 35 | uses: docker/build-push-action@v5 36 | with: 37 | context: . 38 | file: ./DockerfileCion 39 | labels: ${{ steps.meta.outputs.labels }} 40 | platforms: linux/amd64 41 | push: true 42 | tags: ${{ steps.meta.outputs.tags }} 43 | -------------------------------------------------------------------------------- /xilriws-targetfp/inject.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | "use strict" 3 | 4 | const utils = await import("./utils.js") 5 | const screen = await import("./screen.js") 6 | const general = await import("./general.js") 7 | const canvas = await import("./canvas.js") 8 | const webgl = await import("./webgl.js") 9 | 10 | let div = null 11 | while (!div) { 12 | div = document.querySelector("[data-xil-tab-id]") 13 | if (!div) { 14 | console.log("didn't find seed element, waiting 0.1s") 15 | await new Promise(resolve => setTimeout(resolve, 100)) 16 | } 17 | } 18 | const seed = div.getAttribute("data-xil-tab-id") 19 | document.body.removeChild(div) 20 | utils.setSeed(seed) 21 | 22 | utils.sendWs("debug:seed", seed) 23 | 24 | screen.block() 25 | general.block() 26 | webgl.block() 27 | 28 | if (window.top.document.location.hostname !== "join.pokemon.com") { 29 | // hotfix. signups don't work with canvas blocking, but signigns require them 30 | // should investigate why 31 | canvas.block() 32 | } 33 | 34 | document.fpLoaded = true 35 | })() 36 | 37 | HTMLIFrameElement.prototype.addEventListener = async function (eventType, callback) { 38 | if (eventType !== "load") { 39 | return 40 | } 41 | 42 | let fpLoaded = false 43 | while (!fpLoaded) { 44 | console.log("waiting for plugin load") 45 | await new Promise(resolve => setTimeout(resolve, 200)) 46 | if (this.contentDocument) { 47 | fpLoaded = this.contentDocument.fpLoaded 48 | } 49 | } 50 | callback() 51 | } 52 | -------------------------------------------------------------------------------- /xilriws/proxy_dispenser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .proxy import Proxy, ProxyDistributor 4 | from loguru import logger 5 | import time 6 | import asyncio 7 | 8 | logger = logger.bind(name="Proxy Dispenser") 9 | AUTH_TIMEOUT = 60 * 60 10 | 11 | 12 | class ProxyDispenser: 13 | def __init__(self, list_path: str): 14 | self.proxies: list[Proxy] = [] 15 | with open(list_path, "r") as f: 16 | for proxy_url in f.readlines(): 17 | if not proxy_url: 18 | continue 19 | 20 | proxy_url = proxy_url.strip() 21 | 22 | if proxy_url.lower() == "local": 23 | proxy_url = None 24 | 25 | try: 26 | self.proxies.append(Proxy(proxy_url)) 27 | except Exception as e: 28 | logger.error(f"There was a problem parsing proxy {proxy_url}: {str(e)}") 29 | 30 | if not self.proxies: 31 | logger.warning("No configured proxies! Using local IP only") 32 | self.proxies.append(Proxy(None)) 33 | 34 | self.current_auth_index = 0 35 | self.current_proxy_uses = 0 36 | 37 | async def get_auth_proxy(self) -> Proxy: 38 | self.current_proxy_uses += 1 39 | if self.current_proxy_uses > 100: 40 | self.current_auth_index = (self.current_auth_index + 1) % len(self.proxies) 41 | self.current_proxy_uses = 0 42 | 43 | while True: 44 | for i, proxy in enumerate(self.proxies): 45 | if i >= self.current_auth_index and proxy.is_good(): 46 | return proxy 47 | 48 | logger.error("No free Proxies!") 49 | self.current_auth_index = 0 50 | self.current_proxy_uses = 0 51 | await asyncio.sleep(5) 52 | -------------------------------------------------------------------------------- /xilriws-targetfp/utils.js: -------------------------------------------------------------------------------- 1 | const ws = new WebSocket('ws://127.0.0.1:9091'); 2 | 3 | let random = null 4 | 5 | export function sendWs(action, detail = null) { 6 | try { 7 | ws.send(JSON.stringify({action: action, detail: detail})) 8 | } catch (e) {} 9 | } 10 | 11 | export function setSeed(newSeed) { 12 | random = seededRandom(newSeed) 13 | } 14 | 15 | function seededRandom(seed) { 16 | let m = 0x80000000; // 2^31 17 | let a = 1103515245; 18 | let c = 12345; 19 | let state = seed ? seed : Math.floor(Math.random() * (m - 1)); 20 | 21 | return function() { 22 | state = (a * state + c) % m; 23 | return state / (m - 1); 24 | }; 25 | } 26 | 27 | /** 28 | * @param {Object} object 29 | * @param {string} propName 30 | * @param {any} propValue 31 | */ 32 | export function overwriteProp(object, propName, propValue) { 33 | Object.defineProperty(object, propName, { 34 | get: () => propValue, 35 | set: () => {}, 36 | configurable: true 37 | }); 38 | } 39 | 40 | /** 41 | * @template T 42 | * @param {T[]} array 43 | * @returns {T} 44 | */ 45 | export function randomChoose(array) { 46 | return array[Math.floor(random() * array.length)] 47 | } 48 | 49 | /** 50 | * Generates a random number within the given bounds. 51 | * @param {number} min - The lower bound (inclusive). 52 | * @param {number} max - The upper bound (exclusive). 53 | * @returns {number} A random number between min (inclusive) and max (exclusive). 54 | */ 55 | export function randomNumber(min, max) { 56 | return Math.floor(random() * (max - min)) + min; 57 | } 58 | 59 | /** 60 | * @template T 61 | * @param {T[]} arr 62 | * @param {number} amount 63 | * @returns {T[]} 64 | */ 65 | export function randomChooseMultiple(arr, amount) { 66 | const shuffledArray = arr.sort(() => 0.5 - random()); 67 | return shuffledArray.slice(0, amount); 68 | } -------------------------------------------------------------------------------- /xilriws/mode/cion_mode.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import litestar.logging 4 | from litestar import Litestar, post 5 | from litestar.di import Provide 6 | from litestar.exceptions import HTTPException 7 | from litestar.status_codes import HTTP_503_SERVICE_UNAVAILABLE 8 | from loguru import logger 9 | from dataclasses import dataclass 10 | 11 | from xilriws.browser import Browser, CionResponse, BrowserJoin 12 | from xilriws.ptc_join import PtcJoin 13 | from .basic_mode import BasicMode 14 | from xilriws.proxy import ProxyDistributor, Proxy 15 | from xilriws.proxy_dispenser import ProxyDispenser 16 | 17 | logger = logger.bind(name="Xilriws") 18 | 19 | 20 | @dataclass 21 | class CionRequest: 22 | proxy: str | None 23 | 24 | 25 | @post("/api/v1/cion") 26 | async def cion_endpoint(ptc_join: PtcJoin, data: CionRequest) -> list[CionResponse]: 27 | if ptc_join.is_running: 28 | raise HTTPException(status_code=HTTP_503_SERVICE_UNAVAILABLE) 29 | 30 | try: 31 | tokens = await ptc_join.get_join_tokens(data.proxy) 32 | if tokens: 33 | logger.success("200: Returned tokens to Cion") 34 | return [tokens] 35 | 36 | return [] 37 | 38 | except Exception as e: 39 | logger.exception(e) 40 | logger.error("500: Internal Xilriws error, look above") 41 | raise HTTPException("internal error") 42 | 43 | 44 | class CionMode(BasicMode): 45 | def __init__(self, browser: BrowserJoin): 46 | self.ptc_join = PtcJoin(browser) 47 | self.current_proxy_index = 0 48 | 49 | async def prepare(self) -> None: 50 | pass 51 | 52 | async def _get_ptc_join(self): 53 | return self.ptc_join 54 | 55 | def get_litestar(self) -> Litestar: 56 | return Litestar( 57 | route_handlers=[cion_endpoint], 58 | dependencies={"ptc_join": Provide(self._get_ptc_join)}, 59 | ) 60 | -------------------------------------------------------------------------------- /xilriws/task_creator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import asyncio 3 | from typing import Coroutine, Any, TypeVar, Generic 4 | 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | class AwaitableSet(Generic[T]): 10 | def __init__(self): 11 | self.set: set[T] = set() 12 | self.cond = asyncio.Condition() 13 | 14 | def __len__(self): 15 | return len(self.set) 16 | 17 | def __bool__(self): 18 | return bool(len(self)) 19 | 20 | async def add(self, element: T) -> None: 21 | async with self.cond: 22 | self.set.add(element) 23 | self.cond.notify_all() 24 | 25 | async def remove(self, element: T) -> None: 26 | if element not in self.set: 27 | return 28 | 29 | async with self.cond: 30 | self.set.remove(element) 31 | self.cond.notify_all() 32 | 33 | async def wait_until_shorter_than(self, threshold: int) -> None: 34 | async with self.cond: 35 | while len(self) >= threshold: 36 | await self.cond.wait() 37 | 38 | 39 | class TaskCreator: 40 | def __init__(self, limit: int | None = None): 41 | self.coro_set = AwaitableSet() 42 | self.tasks: set[asyncio.Task] = set() 43 | self.limit = limit 44 | 45 | async def run_coro(self, coro: Coroutine[Any, Any, Any]) -> None: 46 | if self.limit: 47 | await self.coro_set.wait_until_shorter_than(self.limit) 48 | 49 | await self.coro_set.add(coro) 50 | await coro 51 | await self.coro_set.remove(coro) 52 | 53 | def create_task(self, coro: Coroutine[Any, Any, Any], loop: asyncio.AbstractEventLoop | None = None): 54 | internal_coro = self.run_coro(coro) 55 | 56 | if loop is None: 57 | task = asyncio.create_task(internal_coro) 58 | else: 59 | task = loop.create_task(internal_coro) 60 | 61 | self.tasks.add(task) 62 | task.add_done_callback(self.tasks.discard) 63 | 64 | 65 | task_creator = TaskCreator() 66 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | RUN apt update -y && \ 3 | apt clean 4 | 5 | WORKDIR /xilriws 6 | 7 | ENV DEBIAN_FRONTEND noninteractive 8 | #RUN apt install -y git-all 9 | #RUN git clone https://github.com/UnownHash/Xilriws-Public 10 | #RUN cp -r Xilriws-Public/xilriws-fingerprint-random /xilriws/xilriws-fingerprint-random 11 | #RUN cp -r Xilriws-Public/xilriws-cookie-delete /xilriws/xilriws-cookie-delete 12 | #RUN cp -r Xilriws-Public/xilriws-proxy /xilriws/xilriws-proxy 13 | #RUN cp -r Xilriws-Public/xilriws-targetfp /xilriws/xilriws-targetfp 14 | 15 | RUN apt install -y software-properties-common 16 | RUN apt update && apt install -y python3 python3-venv 17 | RUN python3 -m venv /opt/venv 18 | ENV PATH="/opt/venv/bin:$PATH" 19 | RUN pip install --upgrade pip==25.0.1 20 | RUN pip install poetry 21 | 22 | RUN apt install -y wget 23 | 24 | RUN wget -q -O - https://github.com/NDViet/google-chrome-stable/releases/download/125.0.6422.141-1/google-chrome-stable_125.0.6422.141-1_amd64.deb > ./chrome.deb 25 | RUN apt install -y ./chrome.deb 26 | RUN rm ./chrome.deb 27 | 28 | #RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub > linux_signing_key.pub 29 | #RUN install -D -o root -g root -m 644 linux_signing_key.pub /etc/apt/keyrings/linux_signing_key.pub 30 | #RUN sh -c 'echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/linux_signing_key.pub] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list' 31 | #RUN apt update -y 32 | #RUN apt install -y google-chrome-stable 33 | 34 | #RUN apt install -y curl 35 | #RUN curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg 36 | #RUN echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main"| tee /etc/apt/sources.list.d/brave-browser-release.list 37 | #RUN apt update -y 38 | #RUN apt install -y brave-browser 39 | 40 | COPY . . 41 | RUN poetry install --no-root 42 | 43 | ENTRYPOINT ["poetry", "run", "python", "app.py"] 44 | -------------------------------------------------------------------------------- /xilriws/extension_comm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import json 5 | from typing import Any, Literal 6 | 7 | import websockets 8 | from loguru import logger 9 | 10 | logger = logger.bind(name="ExtComm") 11 | 12 | FINISH_PROXY = "finish:setProxy" 13 | FINISH_COOKIE_PURGE = "finish:cookiePurge" 14 | 15 | 16 | class ExtensionComm: 17 | def __init__(self): 18 | self.clients: set[websockets.WebSocketServerProtocol] = set() 19 | self.futures: dict[str, list[asyncio.Future]] = {} 20 | 21 | async def echo(self, websocket: websockets.WebSocketServerProtocol): 22 | self.clients.add(websocket) 23 | try: 24 | async for message in websocket: 25 | logger.debug(f"Received WS data: {message}") 26 | data = json.loads(message) 27 | action = data["action"] 28 | detail = data["detail"] 29 | 30 | futures = self.futures.get(action) 31 | if not futures: 32 | continue 33 | 34 | for future in futures: 35 | try: 36 | future.set_result(detail) 37 | except asyncio.InvalidStateError: 38 | continue 39 | del self.futures[action] 40 | 41 | await websocket.wait_closed() 42 | except websockets.exceptions.ConnectionClosedError: 43 | self.clients.remove(websocket) 44 | except Exception as e: 45 | logger.exception("Error in WS server", e) 46 | finally: 47 | self.clients.remove(websocket) 48 | 49 | async def send(self, action: str, data: dict[str, Any] | None = None): 50 | message = json.dumps( 51 | { 52 | "action": action, 53 | "data": data 54 | } 55 | ) 56 | logger.debug(f"Sending WS data: {message}") 57 | for client in self.clients: 58 | await client.send(message) 59 | 60 | async def add_listener(self, action: str) -> asyncio.Future: 61 | future = asyncio.get_running_loop().create_future() 62 | if action in self.futures: 63 | self.futures[action].append(future) 64 | else: 65 | self.futures[action] = [future] 66 | return future 67 | 68 | async def start(self): 69 | async with websockets.serve(self.echo, "127.0.0.1", 9091): 70 | await asyncio.Future() 71 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import json 5 | import logging 6 | import os.path 7 | import signal 8 | import sys 9 | 10 | import uvicorn 11 | from loguru import logger 12 | 13 | from xilriws.browser import BrowserAuth, BrowserJoin 14 | from xilriws.extension_comm import ExtensionComm 15 | from xilriws.mode import AuthMode, CionMode 16 | from xilriws.proxy import ProxyDistributor 17 | from xilriws.proxy_dispenser import ProxyDispenser 18 | from xilriws.task_creator import task_creator 19 | 20 | httpx_logger = logging.getLogger("httpx") 21 | httpx_logger.setLevel(logging.CRITICAL) 22 | uvicorn_logger = logging.getLogger("uvicorn") 23 | uvicorn_logger.setLevel(logging.CRITICAL) 24 | zendriver_logger = logging.getLogger("zendriver") 25 | zendriver_logger.setLevel(logging.CRITICAL) 26 | ws_logger = logging.getLogger("websockets") 27 | ws_logger.setLevel(logging.CRITICAL) 28 | conn_logger = logging.getLogger("uc.connection") 29 | conn_logger.setLevel(logging.CRITICAL) 30 | 31 | logger = logger.bind(name="Xilriws") 32 | 33 | if sys.platform != "win32": 34 | signal.signal(signal.SIGCHLD, signal.SIG_IGN) 35 | # else: 36 | # asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 37 | 38 | 39 | async def main(cion_mode: bool): 40 | if os.path.exists("config.json"): 41 | with open("config.json", "r") as f: 42 | config: dict[str, str | int] = json.load(f) 43 | else: 44 | config = {} 45 | 46 | ext_comm = ExtensionComm() 47 | task_creator.create_task(ext_comm.start()) 48 | 49 | extenstion_paths = [ 50 | config.get("proxy", "/xilriws/xilriws-proxy"), 51 | config.get("targetfp_path", "/xilriws/xilriws-targetfp"), 52 | ] 53 | 54 | if cion_mode: 55 | logger.info("Starting in Cion Mode") 56 | mode = CionMode( 57 | BrowserJoin(extension_paths=extenstion_paths, ext_comm=ext_comm) 58 | ) 59 | else: 60 | proxies = ProxyDistributor(ext_comm) 61 | proxy_dispenser = ProxyDispenser( 62 | config.get("proxies_list_path", "/xilriws/proxies.txt") 63 | ) 64 | 65 | mode = AuthMode(BrowserAuth(extension_paths=extenstion_paths, ext_comm=ext_comm, proxies=proxies), proxies, proxy_dispenser) 66 | 67 | await mode.prepare() 68 | 69 | port = config.get("port", 5090) 70 | host = config.get("host", "0.0.0.0") 71 | 72 | app = mode.get_litestar() 73 | server_config = uvicorn.Config(app, port=port, host=host, log_config=None) 74 | server = uvicorn.Server(server_config) 75 | 76 | logger.info(f"Starting Xilriws on http://{host}:{port}") 77 | 78 | await server.serve() 79 | 80 | 81 | if __name__ == "__main__": 82 | asyncio.run(main(cion_mode=False)) 83 | -------------------------------------------------------------------------------- /xilriws/proxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import time 4 | from copy import copy 5 | from typing import TYPE_CHECKING 6 | from urllib.parse import ParseResult, urlparse 7 | 8 | from loguru import logger 9 | 10 | if TYPE_CHECKING: 11 | from .extension_comm import ExtensionComm 12 | 13 | 14 | PROXY_TIMEOUT = 60 * 60 15 | 16 | 17 | class Proxy: 18 | def __init__(self, url: str | None): 19 | if isinstance(url, str): 20 | if "://" not in url: 21 | url = "http://" + url 22 | url = urlparse(url) 23 | elif url is None: 24 | url = urlparse(url) 25 | 26 | self.full_url: ParseResult = url 27 | 28 | self.host = url.hostname 29 | self.port = url.port 30 | self.scheme = url.scheme 31 | self.username = url.username 32 | self.password = url.password 33 | 34 | self.last_limited: float = 0 35 | self.invalidated: bool = False 36 | 37 | @property 38 | def url(self): 39 | return f"{self.host}:{self.port}" 40 | 41 | def is_good(self): 42 | return not self.invalidated and self.last_limited + PROXY_TIMEOUT < time.time() 43 | 44 | def rate_limited(self): 45 | self.last_limited = time.time() 46 | 47 | def invalidate(self): 48 | self.invalidated = True 49 | 50 | 51 | logger = logger.bind(name="Proxy") 52 | 53 | 54 | class ProxyDistributor: 55 | def __init__(self, ext_comm: ExtensionComm): 56 | self.next_proxy: Proxy | None = None 57 | self.current_proxy: Proxy | None = None 58 | self.ext_comm = ext_comm 59 | 60 | def set_next_proxy(self, proxy: Proxy) -> bool: 61 | if self.current_proxy and self.current_proxy.host == proxy.host and self.current_proxy.port == proxy.port: 62 | return False 63 | 64 | self.next_proxy = proxy 65 | return True 66 | 67 | async def change_proxy(self, proxy: Proxy | None = None) -> bool: 68 | if proxy is not None: 69 | self.set_next_proxy(proxy) 70 | 71 | if self.next_proxy is not None: 72 | self.current_proxy = copy(self.next_proxy) 73 | 74 | if self.current_proxy is None: 75 | return False 76 | 77 | if not self.current_proxy.host: 78 | return False 79 | 80 | logger.info(f"Switching to Proxy {self.current_proxy.host}:{self.current_proxy.port}") 81 | 82 | await self.ext_comm.send( 83 | "setProxy", 84 | { 85 | "host": self.current_proxy.host, 86 | "port": self.current_proxy.port, 87 | "scheme": self.current_proxy.scheme, 88 | "password": self.current_proxy.password, 89 | "username": self.current_proxy.username, 90 | } 91 | ) 92 | return True 93 | -------------------------------------------------------------------------------- /xilriws-targetfp/general.js: -------------------------------------------------------------------------------- 1 | import * as utils from "./utils.js" 2 | import * as funcToString from "./funcToString.js" 3 | 4 | const baseLanguages = [ 5 | "en-US", 6 | "fr", 7 | "de", 8 | "es" 9 | ] 10 | 11 | const extraLanguages = [ 12 | "en-GB", 13 | "pt-BR", 14 | "ru", 15 | "tr", 16 | "de", 17 | "fr", 18 | "es", 19 | "hr", 20 | "el", 21 | "hu", 22 | "no", 23 | "ro", 24 | "sr" 25 | ] 26 | 27 | const englishes = [ 28 | "en-US", 29 | "en-GB" 30 | ] 31 | 32 | const timezones = [ 33 | 0, 1, 2, 3, 4, 5, 6, 8, 9, -4, -5, -6, -7 34 | ] 35 | 36 | export function block() { 37 | utils.overwriteProp(navigator, "platform", "Win32") 38 | utils.overwriteProp(navigator, "doNotTrack", utils.randomChoose(["unknown", "unknown", "1"])) 39 | utils.overwriteProp(navigator, "maxTouchPoints", utils.randomChoose([0, 5, 10, 20])) 40 | utils.overwriteProp(navigator, "productSub", "20030107") 41 | utils.overwriteProp(navigator.connection, "rtt", utils.randomChoose([undefined, 0, 50, 100])) 42 | utils.overwriteProp(navigator, "hardwareConcurrency", utils.randomChoose([4, 8, 12, 16, 24, 32])) 43 | 44 | utils.overwriteProp(window.history, "length", utils.randomNumber(1, 5)) 45 | 46 | const timezone = utils.randomChoose(timezones) * -60 47 | utils.overwriteProp(Date.prototype, "getTimezoneOffset", () => timezone) 48 | funcToString.set(Date.prototype.getTimezoneOffset) 49 | 50 | utils.overwriteProp(Navigator.prototype, "mimeTypes", { 51 | 0: { 52 | suffixes: "pdf", 53 | type: "application/pdf", 54 | enabledPlugin: { filename: "internal-pdf-viewer" }, 55 | }, 56 | 1: { 57 | suffixes: "pdf", 58 | type: "text/pdf", 59 | enabledPlugin: { filename: "internal-pdf-viewer" }, 60 | }, 61 | "application/pdf": { 62 | suffixes: "pdf", 63 | type: "application/pdf", 64 | enabledPlugin: { filename: "internal-pdf-viewer" }, 65 | }, 66 | "text/pdf": { 67 | suffixes: "pdf", 68 | type: "text/pdf", 69 | enabledPlugin: { filename: "internal-pdf-viewer" }, 70 | }, 71 | 72 | }) 73 | 74 | // language 75 | const baseLanguage = utils.randomChoose(baseLanguages) 76 | const languages = [baseLanguage] 77 | 78 | const randomExtraLangs = utils.randomNumber(0, 10) 79 | if (randomExtraLangs > 3) { 80 | if (englishes.includes(baseLanguage)) { 81 | languages.push(utils.randomChoose(extraLanguages)) 82 | } else { 83 | languages.push(utils.randomChoose(englishes)) 84 | } 85 | } 86 | console.log("languages are " + languages.join(",")) 87 | 88 | utils.overwriteProp(navigator, "language", baseLanguage) 89 | utils.overwriteProp(navigator, "languages", languages) 90 | } -------------------------------------------------------------------------------- /xilriws-targetfp/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "xilriws-targetfp", 4 | "version": "1.0", 5 | "permissions": [ 6 | "proxy", 7 | "tabs", 8 | "unlimitedStorage", 9 | "storage", 10 | "", 11 | "webRequest", 12 | "webRequestBlocking", 13 | "devtools", 14 | "cookies", 15 | "storage", 16 | "scripting", 17 | "tabs", 18 | "activeTab", 19 | "webNavigation", 20 | "runtime" 21 | ], 22 | "host_permissions": [ 23 | "" 24 | ], 25 | "background": { 26 | "service_worker": "background.js", 27 | "type": "module" 28 | }, 29 | "content_scripts": [ 30 | { 31 | "matches": [ 32 | "" 33 | ], 34 | "js": [ 35 | "contentScript.js" 36 | ], 37 | "run_at": "document_start", 38 | "all_frames": true, 39 | "match_origin_as_fallback": true 40 | }, 41 | { 42 | "matches": [ 43 | "" 44 | ], 45 | "js": [ 46 | "inject.js" 47 | ], 48 | "run_at": "document_start", 49 | "all_frames": true, 50 | "match_origin_as_fallback": true, 51 | "world": "MAIN", 52 | "type": "module" 53 | } 54 | ], 55 | "web_accessible_resources": [ 56 | { 57 | "resources": [ 58 | "inject.js" 59 | ], 60 | "matches": [ 61 | "" 62 | ] 63 | }, 64 | { 65 | "resources": [ 66 | "utils.js" 67 | ], 68 | "matches": [ 69 | "" 70 | ] 71 | }, 72 | { 73 | "resources": [ 74 | "screen.js" 75 | ], 76 | "matches": [ 77 | "" 78 | ] 79 | }, 80 | { 81 | "resources": [ 82 | "general.js" 83 | ], 84 | "matches": [ 85 | "" 86 | ] 87 | }, 88 | { 89 | "resources": [ 90 | "canvas.js" 91 | ], 92 | "matches": [ 93 | "" 94 | ] 95 | }, 96 | { 97 | "resources": [ 98 | "webgl.js" 99 | ], 100 | "matches": [ 101 | "" 102 | ] 103 | }, 104 | { 105 | "resources": [ 106 | "funcToString.js" 107 | ], 108 | "matches": [ 109 | "" 110 | ] 111 | } 112 | ] 113 | } 114 | -------------------------------------------------------------------------------- /xilriws-targetfp/canvas.js: -------------------------------------------------------------------------------- 1 | import * as utils from "./utils.js" 2 | import * as funcToString from "./funcToString.js" 3 | 4 | const typeValues = { 5 | " monospace": 1, " sans-serif": 2, " serif": 3 6 | } 7 | 8 | const possibleFonts = ["ArialUnicodeMS", "Calibri", "Century", "Haettenschweiler", "Marlett", "Pristina", "Bauhaus93", "FuturaBkBT", "HelveticaNeue", "LucidaSans", "MYRIADPRO", "SegoeUILight"] 9 | 10 | export function block() { 11 | utils.overwriteProp(CanvasRenderingContext2D.prototype, "isPointInPath", () => false) 12 | utils.overwriteProp(CanvasRenderingContext2D.prototype, "globalCompositeOperation", "screen") 13 | 14 | function zeroOrOne() { 15 | return utils.randomNumber(0, 2) 16 | } 17 | 18 | utils.overwriteProp(CanvasRenderingContext2D.prototype, "measureText", (s) => { 19 | const metrics = {} 20 | metrics.width = zeroOrOne() 21 | metrics.actualBoundingBoxAscent = zeroOrOne() 22 | metrics.actualBoundingBoxDescent = zeroOrOne() 23 | metrics.actualBoundingBoxLeft = zeroOrOne() 24 | metrics.actualBoundingBoxRight = zeroOrOne() 25 | return metrics 26 | }) 27 | 28 | const goodFonts = utils.randomChooseMultiple(possibleFonts, utils.randomNumber(4, 7)) 29 | console.log("good fonts are " + goodFonts.join(",")) 30 | 31 | CanvasRenderingContext2D.prototype.measureText = function (text) { 32 | let value = -10 33 | for (const typeValue of Object.keys(typeValues)) { 34 | if (this.font.includes(typeValue)) { 35 | value = typeValues[typeValue] 36 | } 37 | } 38 | 39 | for (const goodFont of goodFonts) { 40 | if (this.font.includes(" " + goodFont + ",")) { 41 | value = -10 42 | } 43 | } 44 | 45 | const metrics = {} 46 | metrics.width = value 47 | metrics.actualBoundingBoxAscent = value 48 | metrics.actualBoundingBoxDescent = value 49 | metrics.actualBoundingBoxLeft = value 50 | metrics.actualBoundingBoxRight = value 51 | return metrics 52 | } 53 | funcToString.set(CanvasRenderingContext2D.prototype.measureText) 54 | 55 | const originalArc = CanvasRenderingContext2D.prototype.arc 56 | CanvasRenderingContext2D.prototype.arc = function (n1, n2, n3, zero, pi2, bool) { 57 | n1 += utils.randomNumber(-1, 2) 58 | n2 += utils.randomNumber(-1, 2) 59 | n3 += utils.randomNumber(-1, 2) 60 | return originalArc.bind(this, n1, n2, n3, zero, pi2, bool)() 61 | } 62 | funcToString.set(CanvasRenderingContext2D.prototype.arc) 63 | 64 | const originalPutImageData = CanvasRenderingContext2D.prototype.putImageData 65 | CanvasRenderingContext2D.prototype.putImageData = function (img, x, y, ...args) { 66 | // this doesn't actually do anything. however, it doesn't appear this canvas differs between different chromiums 67 | x += utils.randomNumber(-1, 2) 68 | y += utils.randomNumber(-1, 2) 69 | return originalPutImageData.bind(this, img, x, y, ...args)() 70 | } 71 | funcToString.set(CanvasRenderingContext2D.prototype.putImageData) 72 | } -------------------------------------------------------------------------------- /xilriws/ptc/ptc_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | USER_AGENT = ( 4 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" 5 | ) 6 | 7 | 8 | IMPERVA_ERROR_CODES = { 9 | "3": "There was an error in processing the request", 10 | "4": "The request could not be fully read", 11 | "5": "There was an error in processing the server response", 12 | "8": "The proxy failed to connect to the web server, due to TCP connection rejection (TCP Reset)", 13 | "9": "Error code 9", 14 | "12": "Captcha", 15 | "14": "This request was blocked by our security service", 16 | "15": "General bot protection", 17 | "16": "IP is permanently blocked", 18 | "17": "IP is rate-limited", 19 | "18": "Requests to the web site you are trying to access cannot be served (The site was probably removed from the service because it is in violation of our terms of service or if it is under a DDoS attack and site service plan does not cover DDoS mitigation)", 20 | "20": "The proxy failed to connect to the web server, due to TCP connection timeout", 21 | "22": "The proxy failed to resolve site from host name, if this site was recently added please allow a few minutes before trying again", 22 | "23": "The proxy failed to resolve site from host name - duplicate sites with same host name exist. To resolve this issue, complete the DNS changes as instructed", 23 | "24": "The proxy failed to resolve site from host name - CNAME is invalid. To resolve this issue, complete the DNS changes as instructed", 24 | "26": "The proxy failed to connect to the web server, SSL connection failed", 25 | "29": "SSL is not supported", 26 | "30": "The proxy failed to connect to the web server, no web server IP is defined", 27 | "31": "Port not supported", 28 | "32": "The proxy failed to connect to the web server", 29 | "33": "Timeout reading request POST/PUT body", 30 | "35": "The certificate on the web server is not valid.", 31 | "36": "This site does not have an IPV6 address, please use IPV4 instead", 32 | "37": "The site is using an origin server which is reserved for another account.", 33 | "38": "The domain was blacklisted as it violates Imperva terms of use.", 34 | "39": "The domain is pointing to the wrong DNS records.", 35 | "40": "The SSL certificate on the origin server was issued to a different domain.", 36 | "41": "The site is not currently configured to support non-SNI connections.", 37 | "42": "The client did not provide a client certificate, and the site requires one in all connections.", 38 | "43": "Too many connections are open simultaneously between the Imperva proxy and the origin server.", 39 | "44": "The proxy failed to connect to the web server. Detected loop in CDN." 40 | } 41 | 42 | 43 | def get_imperva_error_code(text: str) -> tuple[str, str]: 44 | code_match = re.search(r"edet=(\d*)&", text) 45 | if code_match and code_match.group(1): 46 | code = code_match.group(1) 47 | error = IMPERVA_ERROR_CODES.get(code, "Unknown reason") 48 | else: 49 | code = "?" 50 | error = "unknown reason" 51 | 52 | return code, error 53 | -------------------------------------------------------------------------------- /xilriws-targetfp/webgl.js: -------------------------------------------------------------------------------- 1 | import * as utils from "./utils.js" 2 | import * as funcToString from "./funcToString.js" 3 | 4 | export function block() { 5 | const glProto = WebGLRenderingContext.prototype 6 | 7 | utils.overwriteProp(glProto, "getSupportedExtensions", () => ["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_disjoint_timer_query","EXT_float_blend","EXT_frag_depth","EXT_polygon_offset_clamp","EXT_shader_texture_lod","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","EXT_texture_mirror_clamp_to_edge","EXT_sRGB","KHR_parallel_shader_compile","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_float_linear","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_blend_func_extended","WEBGL_color_buffer_float","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_depth_texture","WEBGL_draw_buffers","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"]) 8 | 9 | const originalGetParameter = glProto.getParameter 10 | glProto.getParameter = function (parameter) { 11 | if (parameter === glProto.MAX_VERTEX_UNIFORM_VECTORS) { 12 | return utils.randomChoose([127, 128, 255, 256, 511, 512, 1023, 1024, 2047, 2048, 4095, 4096]) 13 | } else if (parameter === glProto.MAX_VIEWPORT_DIMS) { 14 | return utils.randomChoose([[16384, 16384], [32767, 32767], [65536, 65536]]) 15 | } else if (parameter === glProto.RENDERER) { 16 | return utils.randomChoose(["WebKit WebGL", "WebKit WebGL", "WebKit WebGL", "WebKit WebGL", "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar", "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar", "Adreno (TM) 650, or similar"]) 17 | } else { 18 | const debug = this.getExtension("WEBGL_debug_renderer_info") 19 | if (debug) { 20 | if (parameter === debug.UNMASKED_VENDOR_WEBGL) { 21 | return utils.randomChoose([ 22 | "Google Inc. (Microsoft)", "Google Inc. (Intel)", "Google Inc. (NVIDIA Corporation)", "Google Inc. (ARM)", "Google Inc. (NVIDIA)", "Google Inc. (AMD)" 23 | ]) 24 | } else if (parameter === debug.UNMASKED_RENDERER_WEBGL) { 25 | let randomHex = "" 26 | for (let i = 0; i < 4; i++) { 27 | randomHex += utils.randomChoose(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E"]) 28 | } 29 | const graphicsCard = utils.randomChoose([ 30 | "NVIDIA, NVIDIA GeForce MX450", 31 | "NVIDIA, NVIDIA GeForce 710M", 32 | "NVIDIA, NVIDIA GeForce RTX 2050", 33 | "NVIDIA, NVIDIA GeForce GTX 950M", 34 | "Intel, Intel(R) UHD Graphics 620", 35 | "Intel, Intel(R) HD Graphics 630", 36 | "Intel, Intel(R) UHD Graphics", 37 | "Intel, Intel(R) Iris(R) Xe Graphics", 38 | "AMD, Radeon RX 570 Series", 39 | "AMD, Radeon R9 380 Series", 40 | "AMD, Radeon X800 Series", 41 | ]) 42 | 43 | return "ANGLE(" + graphicsCard + " (0x0000" + randomHex + ") Direct3D11 vs_5_0 ps_5_0, D3D11)" 44 | } 45 | } 46 | } 47 | return originalGetParameter.bind(this, parameter)() 48 | } 49 | 50 | utils.overwriteProp(glProto.getParameter, "name", "getParameter") 51 | 52 | funcToString.set(glProto.getSupportedExtensions) 53 | funcToString.set(glProto.getParameter) 54 | } -------------------------------------------------------------------------------- /xilriws/reese_cookie.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import time 5 | from typing import TYPE_CHECKING 6 | 7 | from loguru import logger 8 | 9 | from .constants import EXPIRATION, MAX_USES, COOKIE_STORAGE 10 | from .proxy import ProxyDistributor, Proxy 11 | from .proxy_dispenser import ProxyDispenser 12 | from .task_creator import task_creator, AwaitableSet 13 | 14 | if TYPE_CHECKING: 15 | from .browser.browser_auth import BrowserAuth 16 | 17 | logger = logger.bind(name="Cookie") 18 | 19 | 20 | class ReeseCookie: 21 | def __init__(self, cookies: dict[str, str], proxy: Proxy): 22 | self.value: str = "value" 23 | self.expiration: float = time.time() + EXPIRATION 24 | self.uses: int = 0 25 | self.cookies = cookies 26 | self.proxy = proxy 27 | 28 | def is_good(self) -> bool: 29 | return time.time() < self.expiration and self.uses < MAX_USES 30 | 31 | def use(self) -> None: 32 | self.uses += 1 33 | 34 | 35 | class CookieMonster: 36 | fill_event: asyncio.Event 37 | 38 | def __init__(self, browser: BrowserAuth, proxies: ProxyDistributor, proxy_dispenser: ProxyDispenser): 39 | self.browser: BrowserAuth = browser 40 | self.cookies: AwaitableSet[ReeseCookie] = AwaitableSet() 41 | self.proxies = proxies 42 | self.proxy_dispenser = proxy_dispenser 43 | 44 | async def prepare(self): 45 | self.fill_event = asyncio.Event() 46 | task_creator.create_task(self.fill_task()) 47 | self.fill_event.set() 48 | 49 | async def get_reese_cookie(self) -> ReeseCookie: 50 | logger.info("Getting a reese cookie from storage") 51 | cookie: ReeseCookie | None = None 52 | 53 | while not cookie: 54 | possible_cookie = await self.get_next_cookie() 55 | if not possible_cookie.is_good(): 56 | await self.cookies.remove(possible_cookie) 57 | self.fill_event.set() 58 | else: 59 | cookie = possible_cookie 60 | 61 | cookie.use() 62 | logger.info("Cookie selected") 63 | 64 | return cookie 65 | 66 | async def remove_cookie(self, cookie: ReeseCookie) -> None: 67 | await self.cookies.remove(cookie) 68 | self.fill_event.set() 69 | 70 | async def fill_task(self): 71 | while True: 72 | await self.fill_event.wait() 73 | logger.info("Filling cookie storage in the background") 74 | 75 | try: 76 | while len(self.cookies) < COOKIE_STORAGE: 77 | await self.__get_one_cookie() 78 | logger.info(f"Cookie storage at {len(self.cookies)}/{COOKIE_STORAGE}") 79 | except Exception as e: 80 | logger.exception("unhandled exception while filling cookie storage, please report", e) 81 | 82 | self.fill_event.clear() 83 | 84 | async def get_next_cookie(self) -> ReeseCookie: 85 | async with self.cookies.cond: 86 | while not len(self.cookies): 87 | await self.cookies.cond.wait() 88 | 89 | next_cookie: ReeseCookie | None = None 90 | for potential_cookie in self.cookies.set: 91 | if next_cookie is None: 92 | next_cookie = potential_cookie 93 | elif potential_cookie.expiration < next_cookie.expiration: 94 | next_cookie = potential_cookie 95 | 96 | return next_cookie 97 | 98 | async def __get_one_cookie(self) -> ReeseCookie | None: 99 | logger.info("Opening browser to get a cookie") 100 | proxy = await self.proxy_dispenser.get_auth_proxy() 101 | proxy_changed = self.proxies.set_next_proxy(proxy) 102 | 103 | cookie = await self.browser.get_reese_cookie(proxy_changed) 104 | 105 | if not cookie: 106 | return None 107 | 108 | await self.cookies.add(cookie) 109 | return cookie 110 | 111 | -------------------------------------------------------------------------------- /xilriws/mode/auth_mode.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from dataclasses import dataclass 5 | from enum import Enum 6 | 7 | from litestar import Litestar, Request, Response, post 8 | from litestar.di import Provide 9 | from litestar.status_codes import ( 10 | HTTP_200_OK, 11 | HTTP_400_BAD_REQUEST, 12 | HTTP_418_IM_A_TEAPOT, 13 | HTTP_500_INTERNAL_SERVER_ERROR, 14 | HTTP_408_REQUEST_TIMEOUT, 15 | ) 16 | from loguru import logger 17 | 18 | from xilriws.browser import Browser 19 | from xilriws.constants import AUTH_TIMEOUT 20 | from xilriws.proxy import ProxyDistributor 21 | from xilriws.proxy_dispenser import ProxyDispenser 22 | from xilriws.ptc_auth import InvalidCredentials, LoginException, PtcAuth, PtcBanned 23 | from xilriws.reese_cookie import CookieMonster 24 | 25 | from .basic_mode import BasicMode 26 | 27 | logger = logger.bind(name="Xilriws") 28 | 29 | 30 | @dataclass 31 | class AuthRequest: 32 | username: str 33 | password: str 34 | url: str 35 | 36 | 37 | class AuthResponseStatus(Enum): 38 | SUCCESS = 1 39 | ERROR = 2 40 | INVALID = 3 41 | BANNED = 4 42 | TIMEOUT = 5 43 | 44 | 45 | @dataclass 46 | class AuthResponse: 47 | status: str 48 | login_code: str = "" 49 | 50 | 51 | @post("/api/v1/login-code") 52 | async def auth_endpoint(request: Request, ptc_auth: PtcAuth, data: AuthRequest) -> Response[AuthResponse]: 53 | try: 54 | login_code = await asyncio.wait_for(ptc_auth.auth(data.username, data.password, data.url), timeout=AUTH_TIMEOUT) 55 | 56 | logger.success("200 OK: successful auth") 57 | return Response( 58 | AuthResponse(login_code=login_code, status=AuthResponseStatus.SUCCESS.name), status_code=HTTP_200_OK 59 | ) 60 | except InvalidCredentials: 61 | logger.warning("400 Bad Request: Invalid credentials") 62 | return Response(AuthResponse(status=AuthResponseStatus.INVALID.name), status_code=HTTP_400_BAD_REQUEST) 63 | except PtcBanned: 64 | logger.warning("418: account is ptc-banned") 65 | return Response(AuthResponse(status=AuthResponseStatus.BANNED.name), status_code=HTTP_418_IM_A_TEAPOT) 66 | except LoginException as e: 67 | logger.error(f"Error: {str(e)}") 68 | except asyncio.TimeoutError: 69 | logger.error("408: Exceeded timeout") 70 | return Response( 71 | AuthResponse(status=AuthResponseStatus.TIMEOUT.name), 72 | status_code=HTTP_408_REQUEST_TIMEOUT, 73 | ) 74 | except Exception as e: 75 | logger.exception(e) 76 | 77 | logger.warning("500 Internal Server Error: Additional output above") 78 | return Response(AuthResponse(status=AuthResponseStatus.ERROR.name), status_code=HTTP_500_INTERNAL_SERVER_ERROR) 79 | 80 | 81 | @dataclass 82 | class ActivateRequest: 83 | email: str 84 | code: str 85 | 86 | 87 | class ActivateResponseStatus(Enum): 88 | SUCCESS = 1 89 | NO_OPEN_ACTIVATION = 2 90 | 91 | 92 | @dataclass 93 | class ActivateResponse: 94 | status: str 95 | username: str | None = None 96 | email: str | None = None 97 | password: str | None = None 98 | 99 | 100 | @post("/api/v1/activate") 101 | async def activate_endpoint(data: ActivateRequest) -> ActivateResponse: 102 | return ActivateResponse(status=ActivateResponseStatus.NO_OPEN_ACTIVATION.name) 103 | 104 | 105 | class AuthMode(BasicMode): 106 | def __init__(self, browser: Browser, proxies: ProxyDistributor, proxy_dispenser: ProxyDispenser): 107 | self.cookie_monster = CookieMonster(browser, proxies, proxy_dispenser) 108 | self.ptc_auth = PtcAuth(self.cookie_monster) 109 | 110 | async def prepare(self) -> None: 111 | await self.cookie_monster.prepare() 112 | 113 | async def _get_ptc_auth(self): 114 | return self.ptc_auth 115 | 116 | def get_litestar(self) -> Litestar: 117 | return Litestar( 118 | route_handlers=[auth_endpoint, activate_endpoint], 119 | dependencies={"ptc_auth": Provide(self._get_ptc_auth)} 120 | ) 121 | -------------------------------------------------------------------------------- /xilriws-proxy/background.js: -------------------------------------------------------------------------------- 1 | const ws = new WebSocket('ws://127.0.0.1:9091'); 2 | 3 | let currentProxyCreds = { 4 | "username": null, 5 | "password": null 6 | } 7 | 8 | const blockedAccessScripts = [ 9 | "login-util.js", 10 | "screen-name-script.js", 11 | "set-state-script.js", 12 | ] 13 | const blockedFiles = ["woff", "ttf", "css", "png", "jpg", "jpeg", "svg", "ico"] 14 | 15 | 16 | const BLOCK_URLS = [ 17 | "*://gstatic.com/*", 18 | "*://fonts.googleapis.com/*", 19 | "*://optimizationguide-pa.googleapis.com/*", 20 | "*://*.launchdarkly.com/*", 21 | "*://*.browser-intake-datadoghq.com/*", 22 | "*://join.pokemon.com/manifest.json" 23 | ] 24 | 25 | blockedAccessScripts.forEach(name => BLOCK_URLS.push("*://access.pokemon.com/scripts/" + name)) 26 | blockedFiles.forEach(name => BLOCK_URLS.push("*://*/*." + name + "*")) 27 | 28 | chrome.webRequest.onBeforeRequest.addListener( 29 | () => { return {cancel: true} }, 30 | {urls: BLOCK_URLS}, 31 | ["blocking"] 32 | ) 33 | 34 | ws.onmessage = (event) => { 35 | console.log('Message from server: ', event.data); 36 | const message = JSON.parse(event.data) 37 | const action = message.action 38 | const data = message.data 39 | 40 | if (action === 'setProxy') { 41 | currentProxyCreds = { 42 | username: data.username, 43 | password: data.password 44 | } 45 | startProxy(data.host, data.port, data.scheme) 46 | } 47 | } 48 | 49 | function sendWs(action, detail = null) { 50 | ws.send(JSON.stringify({action: action, detail: detail})) 51 | } 52 | 53 | function startProxy(host, port, scheme) { 54 | const proxyConfig = { 55 | mode: 'fixed_servers', 56 | rules: { 57 | singleProxy: { 58 | scheme: scheme, 59 | host: host, 60 | port: port, 61 | }, 62 | bypassList: [], 63 | }, 64 | }; 65 | chrome.proxy.settings.set( 66 | {value: proxyConfig}, 67 | () => { 68 | sendWs('finish:setProxy', host + ':' + port) 69 | }, 70 | ) 71 | } 72 | 73 | chrome.webRequest.onAuthRequired.addListener( 74 | () => { 75 | return { 76 | authCredentials: currentProxyCreds 77 | } 78 | }, 79 | {urls: [""]}, 80 | ["blocking"], 81 | ) 82 | 83 | chrome.tabs.onUpdated.addListener((tabId, details) => { 84 | if (!details.url) { 85 | return 86 | } 87 | 88 | chrome.tabs.executeScript( 89 | tabId, 90 | {code: 'localStorage.clear()', runAt: 'document_start'}, 91 | (result) => { 92 | console.log('Cleared local storage') 93 | } 94 | ) 95 | }) 96 | 97 | chrome.tabs.onRemoved.addListener( 98 | (tabId) => { 99 | chrome.cookies.getAll({}, cookies => { 100 | console.log('Deleting ' + cookies.length + ' cookies') 101 | let goal = cookies.length 102 | 103 | function deleteCallback(details) { 104 | console.log(details) 105 | goal -= 1 106 | 107 | if (goal <= 0) { 108 | console.log('Deleted all cookies') 109 | sendWs('finish:cookiePurge', null) 110 | } 111 | } 112 | 113 | cookies.forEach(cookie => { 114 | let domain = cookie.domain 115 | if (domain.startsWith('.')) { 116 | domain = domain.substring(1, domain.length) 117 | } 118 | const protocol = cookie.secure ? 'https' : 'http' 119 | console.log(`${protocol}://${domain}${cookie.path}`) 120 | 121 | chrome.cookies.remove({ 122 | name: cookie.name, 123 | url: `${protocol}://${domain}${cookie.path}`, 124 | storeId: cookie.storeId 125 | }, deleteCallback) 126 | }) 127 | } 128 | ) 129 | } 130 | ) 131 | -------------------------------------------------------------------------------- /xilriws-targetfp/screen.js: -------------------------------------------------------------------------------- 1 | import * as utils from "./utils.js" 2 | import * as funcToString from "./funcToString.js" 3 | 4 | const screenSizes = [ 5 | [1680, 1050], 6 | [1776, 1000], 7 | [1600, 1200], 8 | [1600, 1280], 9 | [1920, 1080], 10 | [1440, 1440], 11 | [2048, 1080], 12 | [1920, 1200], 13 | [2048, 1152], 14 | [1792, 1344], 15 | [1920, 1280], 16 | [2280, 1080], 17 | [1856, 1392], 18 | [2400, 1080], 19 | [1800, 1440], 20 | [2880, 900], 21 | [2160, 1200], 22 | [2048, 1280], 23 | [1920, 1400], 24 | [2520, 1080], 25 | [2436, 1125], 26 | [2538, 1080], 27 | [1920, 1440], 28 | [2560, 1080], 29 | [2160, 1440], 30 | [2048, 1536], 31 | [2304, 1440], 32 | [2256, 1504], 33 | [2560, 1440], 34 | [2576, 1450], 35 | [2304, 1728], 36 | [2560, 1600], 37 | [2880, 1440], 38 | [2960, 1440], 39 | [2560, 1700], 40 | [2560, 1800], 41 | [2880, 1620], 42 | [2560, 1920], 43 | [3440, 1440], 44 | [2736, 1824], 45 | [2880, 1800], 46 | [2880, 1920], 47 | [2560, 2048], 48 | [2732, 2048] 49 | ] 50 | 51 | export function block() { 52 | const [screenWidth, screenHeight] = utils.randomChoose(screenSizes) 53 | console.log("screen size is " + screenWidth + "x" + screenHeight) 54 | 55 | utils.overwriteProp(window.screen, "width", screenWidth) 56 | utils.overwriteProp(window.screen, "height", screenHeight) 57 | utils.overwriteProp(window.screen, "availWidth", screenWidth) 58 | utils.overwriteProp(window.screen, "availHeight", screenHeight - 48) 59 | utils.overwriteProp(window.screen, "availLeft", 0) 60 | utils.overwriteProp(window.screen, "availTop", 0) 61 | utils.overwriteProp(window.screen, "pixelDepth", 24) 62 | utils.overwriteProp(window.screen.orientation, "type", "landscape-primary") 63 | 64 | utils.overwriteProp(window, "outerWidth", screenWidth) 65 | utils.overwriteProp(window, "outerHeight", screenHeight) 66 | utils.overwriteProp(window, "innerWidth", screenWidth) 67 | utils.overwriteProp(window, "innerHeight", screenHeight - 86) 68 | utils.overwriteProp(window, "screenX", 0) 69 | utils.overwriteProp(window, "screenY", 0) 70 | utils.overwriteProp(window, "devicePixelRatio", 1) 71 | 72 | utils.overwriteProp(window.visualViewport, "scale", 1) 73 | utils.overwriteProp(window.visualViewport, "width", utils.randomNumber(150, screenWidth - 100)) 74 | utils.overwriteProp(window.visualViewport, "height", utils.randomNumber(150, screenHeight - 100)) 75 | 76 | // mouse events 77 | let mouseEventsActive = true 78 | let currentMouseX = utils.randomNumber(1, screenWidth) 79 | let currentMouseY = utils.randomNumber(1, screenHeight - 60) 80 | let mouseOutCallback = null 81 | let mouseOverCallback = null 82 | 83 | async function randomMouseOver() { 84 | console.log("faking mouseover & mouseout") 85 | let max = 2 86 | 87 | while (mouseEventsActive && max > 0) { 88 | max -= 1 89 | await new Promise(resolve => setTimeout(resolve, utils.randomNumber(50, 150))) 90 | const eventData = { 91 | clientX: currentMouseX, 92 | clientY: currentMouseY, 93 | screenX: currentMouseX, 94 | screenY: currentMouseY - 13 95 | } 96 | 97 | mouseOutCallback(new MouseEvent("mouseout", eventData)) 98 | mouseOverCallback(new MouseEvent("mouseover", eventData)) 99 | } 100 | } 101 | 102 | /** 103 | * @param {(event: MouseEvent) => {}} callback 104 | */ 105 | async function randomMouseMove(callback) { 106 | console.log("faking mousemove") 107 | let max = 4 108 | 109 | while (mouseEventsActive && max > 0) { 110 | max -= 1 111 | await new Promise(resolve => setTimeout(resolve, utils.randomNumber(50, 200))) 112 | 113 | currentMouseX += utils.randomNumber(2, 20) 114 | currentMouseY += utils.randomNumber(4, 27) 115 | callback(new MouseEvent("mousemove", { 116 | clientX: currentMouseX, 117 | clientY: currentMouseY, 118 | screenX: currentMouseX, 119 | screenY: currentMouseY - 13 120 | })) 121 | } 122 | } 123 | 124 | const anyMouseEvents = utils.randomNumber(0, 10) > 2 125 | const anyMouseOver = utils.randomNumber(0, 10) > 2 126 | 127 | const originalAddEventListener = Document.prototype.addEventListener 128 | const originalRemoveEventListener = Document.prototype.removeEventListener 129 | 130 | Document.prototype.addEventListener = function(eventType, callback) { 131 | if (!anyMouseEvents) { 132 | return 133 | } 134 | 135 | if (eventType === "mousemove") { 136 | randomMouseMove(callback).then() 137 | } 138 | 139 | if (anyMouseOver && eventType === "mouseout") { 140 | mouseOutCallback = callback 141 | 142 | if (mouseOverCallback) { 143 | randomMouseOver().then() 144 | } 145 | } 146 | if (anyMouseOver && eventType === "mouseover") { 147 | mouseOverCallback = callback 148 | 149 | if (mouseOutCallback) { 150 | randomMouseOver().then() 151 | } 152 | } 153 | 154 | return originalAddEventListener.bind(this, eventType, callback)() 155 | } 156 | 157 | Document.prototype.removeEventListener = function(eventType, callback) { 158 | mouseEventsActive = false 159 | return originalRemoveEventListener.bind(this, eventType, callback)() 160 | } 161 | 162 | funcToString.set(Document.prototype.addEventListener) 163 | funcToString.set(Document.prototype.removeEventListener) 164 | } -------------------------------------------------------------------------------- /xilriws/browser/browser_join.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import time 5 | from dataclasses import dataclass 6 | 7 | import zendriver 8 | from loguru import logger 9 | 10 | from xilriws.constants import JOIN_URL 11 | from xilriws.extension_comm import FINISH_PROXY, FINISH_COOKIE_PURGE 12 | from xilriws.js import load, recaptcha 13 | from xilriws.proxy import Proxy 14 | from xilriws.ptc import ptc_utils 15 | from xilriws.ptc_auth import LoginException 16 | from .browser import Browser, ProxyException 17 | 18 | logger = logger.bind(name="Browser") 19 | 20 | 21 | @dataclass 22 | class CionResponse: 23 | reese_cookie: dict 24 | create_tokens: list[str] 25 | activate_tokens: list[str] 26 | timestamp: int 27 | proxy: str 28 | 29 | 30 | class BrowserJoin(Browser): 31 | async def get_join_tokens(self, proxy: Proxy) -> CionResponse | None: 32 | try: 33 | await self.start_browser() 34 | except Exception: 35 | return None 36 | 37 | try: 38 | timestamp = int(time.time()) 39 | js_future, js_check_handler = await self.get_js_check_handler(JOIN_URL) 40 | cookie_future = await self.ext_comm.add_listener(FINISH_COOKIE_PURGE) 41 | 42 | await self.new_tab() 43 | 44 | proxy_future = await self.ext_comm.add_listener(FINISH_PROXY) 45 | 46 | await self.ext_comm.send( 47 | "setProxy", 48 | { 49 | "host": proxy.host, 50 | "port": proxy.port, 51 | "scheme": proxy.scheme if proxy.scheme else None, 52 | "password": proxy.password, 53 | "username": proxy.username, 54 | } 55 | ) 56 | 57 | try: 58 | await asyncio.wait_for(proxy_future, 2) 59 | except asyncio.TimeoutError: 60 | logger.info("Didn't get confirmation that proxy changed, continuing anyway") 61 | 62 | if not self.first_run and cookie_future and not cookie_future.done(): 63 | try: 64 | await asyncio.wait_for(cookie_future, 2) 65 | except asyncio.TimeoutError: 66 | logger.info("Didn't get confirmation that cookies were cleared, continuing anyway") 67 | 68 | self.first_run = False 69 | 70 | self.tab.add_handler(zendriver.cdp.network.ResponseReceived, js_check_handler) 71 | logger.info("Opening Join page") 72 | 73 | await self.tab.get(url=JOIN_URL) 74 | 75 | html = await self.tab.get_content() 76 | if "neterror" in html.lower(): 77 | raise ProxyException(f"Page couldn't be reached (Proxy: {proxy.url})") 78 | 79 | try: 80 | await asyncio.wait_for(js_future, timeout=100) 81 | self.tab.handlers.clear() 82 | logger.info("JS check done. reloading") 83 | except asyncio.TimeoutError: 84 | raise LoginException("Timeout on JS challenge") 85 | 86 | await self.tab.reload() 87 | 88 | logger.debug("Waiting for imperva or recaptcha iframe") 89 | found_captcha = False 90 | found_error = False 91 | loop = asyncio.get_running_loop() 92 | start_time = loop.time() 93 | while not found_captcha and not found_error and start_time + 100 > loop.time(): 94 | try: 95 | found_captcha = await self.tab.query_selector("iframe[title='reCAPTCHA']") 96 | found_error = await self.tab.query_selector("iframe#main-iframe") 97 | except Exception as e: 98 | # this is handles by the while loop 99 | logger.debug(f"Exception in query_selector: {e}") 100 | 101 | if found_error: 102 | # TODO check for error 16, mark proxies as dead 103 | imp_code, imp_reason = ptc_utils.get_imperva_error_code(await self.tab.get_content()) 104 | raise LoginException(f"Error code {imp_code} ({imp_reason}) with Proxy ({proxy.url})") 105 | 106 | if not found_captcha: 107 | raise LoginException("Timeout waiting for captcha") 108 | 109 | obj, error = await self.tab.send(zendriver.cdp.runtime.evaluate(recaptcha.SRC)) 110 | 111 | logger.info("Preparing token retreiving") 112 | 113 | obj, errors = await self.tab.send(zendriver.cdp.runtime.evaluate(load.SRC)) 114 | obj: zendriver.cdp.runtime.RemoteObject 115 | 116 | logger.info("Getting captcha tokens") 117 | r, errors = await self.tab.send(zendriver.cdp.runtime.await_promise(obj.object_id, return_by_value=True)) 118 | 119 | logger.info("Getting cookies from browser") 120 | all_cookies = await self.get_cookies() 121 | recaptcha_tokens = r.value 122 | 123 | self.consecutive_failures = 0 124 | 125 | return CionResponse( 126 | reese_cookie=all_cookies, 127 | create_tokens=recaptcha_tokens["create"], 128 | activate_tokens=recaptcha_tokens["activate"], 129 | timestamp=timestamp, 130 | proxy=proxy.full_url.geturl(), 131 | ) 132 | except LoginException as e: 133 | logger.error(f"{str(e)} while getting tokens") 134 | self.consecutive_failures += 1 135 | await asyncio.sleep(1) 136 | return None 137 | except ProxyException as e: 138 | proxy.invalidate() 139 | logger.error(f"{str(e)} while getting tokens") 140 | await self.stop_browser() 141 | return None 142 | except Exception as e: 143 | logger.exception("Exception during browser", e) 144 | 145 | logger.error("Error while getting cookie from browser, it will be restarted next time") 146 | await self.stop_browser() 147 | return None 148 | -------------------------------------------------------------------------------- /xilriws/browser/browser_auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | 5 | import zendriver 6 | from loguru import logger 7 | 8 | from xilriws.constants import ACCESS_URL 9 | from xilriws.extension_comm import FINISH_COOKIE_PURGE, ExtensionComm, FINISH_PROXY 10 | from xilriws.proxy import ProxyDistributor 11 | from xilriws.ptc import ptc_utils 12 | from xilriws.ptc_auth import LoginException 13 | from xilriws.reese_cookie import ReeseCookie 14 | 15 | from .browser import Browser, ProxyException 16 | 17 | logger = logger.bind(name="Browser") 18 | 19 | 20 | class BrowserAuth(Browser): 21 | def __init__(self, extension_paths: list[str], proxies: ProxyDistributor, ext_comm: ExtensionComm): 22 | super().__init__(extension_paths=extension_paths, ext_comm=ext_comm) 23 | self.proxies = proxies 24 | 25 | async def get_reese_cookie(self, proxy_changed: bool) -> ReeseCookie | None: 26 | proxy = self.proxies.next_proxy 27 | 28 | try: 29 | await self.start_browser() 30 | except Exception as e: 31 | logger.exception("Exception while starting browser", e) 32 | return None 33 | 34 | try: 35 | js_future, js_check_handler = await self.get_js_check_handler(ACCESS_URL) 36 | cookie_future = await self.ext_comm.add_listener(FINISH_COOKIE_PURGE) 37 | 38 | await self.new_tab() 39 | if proxy_changed: 40 | await self.change_proxy() 41 | 42 | if not self.first_run and cookie_future and not cookie_future.done(): 43 | try: 44 | await asyncio.wait_for(cookie_future, 2) 45 | except asyncio.TimeoutError: 46 | logger.info("Didn't get confirmation that cookies were cleared, continuing anyway") 47 | 48 | self.first_run = False 49 | 50 | if self.last_cookies: 51 | await self.browser.cookies.set_all(self.last_cookies) 52 | 53 | # if IS_DEBUG: 54 | # await self.log_ip() 55 | 56 | self.tab.add_handler(zendriver.cdp.network.ResponseReceived, js_check_handler) 57 | logger.info("Opening PTC") 58 | 59 | try: 60 | await asyncio.wait_for(self.tab.get(url=ACCESS_URL + "login"), timeout=20) 61 | html = await asyncio.wait_for(self.tab.get_content(), timeout=20) 62 | except asyncio.TimeoutError: 63 | raise ProxyException(f"Page timed out (Proxy: {proxy.url})") 64 | 65 | if "neterror" in html.lower(): 66 | raise ProxyException(f"Page couldn't be reached (Proxy: {proxy.url})") 67 | 68 | imp_code, imp_reason = ptc_utils.get_imperva_error_code(html) 69 | if imp_code not in ("15", "?"): 70 | proxy.rate_limited() 71 | raise LoginException(f"Error code {imp_code} ({imp_reason}) with (Proxy: {proxy.url})") 72 | else: 73 | logger.info("Successfully got error 15 page") 74 | if not js_future.done(): 75 | try: 76 | logger.info("Waiting for JS check") 77 | await asyncio.wait_for(js_future, timeout=100) 78 | self.tab.handlers.clear() 79 | logger.info("JS check done. reloading") 80 | except asyncio.TimeoutError: 81 | raise LoginException("Timeout on JS challenge") 82 | else: 83 | logger.debug("JS check already done, continuing") 84 | 85 | logger.debug("Reloading now") 86 | await self.tab.reload() 87 | 88 | attempts = 0 89 | finished_reloading = False 90 | while attempts < 10 and not finished_reloading: 91 | # This while loop checks the html until it finds "log in" or an imperva error code. 92 | # Before adding it, it would often log an error code "?". These seem to have been imperva 93 | # error pages that weren't loaded properly. But to make absolutely sure, we'll just retry. 94 | attempts += 1 95 | logger.debug(f"Checking reload content #{attempts}") 96 | 97 | new_html = await self.tab.get_content() 98 | if "log in" not in new_html.lower(): 99 | logger.debug(new_html) 100 | proxy.rate_limited() 101 | imp_code, imp_reason = ptc_utils.get_imperva_error_code(new_html) 102 | if imp_code != "?": 103 | raise LoginException(f"Didn't pass JS check. Code {imp_code} ({imp_reason})") 104 | 105 | await self.tab.sleep(0.5) 106 | else: 107 | logger.info("Finished reloading") 108 | finished_reloading = True 109 | 110 | if not finished_reloading: 111 | raise LoginException("Timed out while waiting for reload to finish") 112 | 113 | logger.info("Getting cookies from browser") 114 | all_cookies = await self.get_cookies() 115 | 116 | self.consecutive_failures = 0 117 | return ReeseCookie(all_cookies, proxy) 118 | except LoginException as e: 119 | logger.error(f"{str(e)} while getting cookie") 120 | self.consecutive_failures += 1 121 | return None 122 | except ProxyException as e: 123 | # proxy.invalidate() 124 | proxy.rate_limited() 125 | logger.error(f"{str(e)} while getting cookie") 126 | await self.stop_browser() 127 | return None 128 | except Exception as e: 129 | logger.exception("Exception in browser", e) 130 | 131 | logger.error( 132 | "Error while getting cookie from browser, it will be restarted next time" 133 | ) 134 | self.consecutive_failures += 1 135 | await self.stop_browser() 136 | return None 137 | 138 | async def change_proxy(self): 139 | proxy_future = await self.ext_comm.add_listener(FINISH_PROXY) 140 | # TODO: add try/except and restart the browser 141 | used_proxy = await self.proxies.change_proxy() 142 | 143 | if used_proxy: 144 | try: 145 | await asyncio.wait_for(proxy_future, 2) 146 | except asyncio.TimeoutError: 147 | logger.info("Didn't get confirmation that proxy changed, continuing anyway") 148 | -------------------------------------------------------------------------------- /xilriws/ptc_auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from typing import TYPE_CHECKING 5 | 6 | import httpx 7 | from loguru import logger 8 | from curl_cffi import requests 9 | 10 | from .constants import ACCESS_URL, COOKIE_STORAGE 11 | from xilriws.ptc import ptc_utils 12 | 13 | if TYPE_CHECKING: 14 | from .reese_cookie import CookieMonster, ReeseCookie 15 | 16 | logger = logger.bind(name="PTC") 17 | 18 | 19 | class LoginException(Exception): 20 | """generic login exception, don't log the traceback""" 21 | 22 | pass 23 | 24 | 25 | class InvalidCredentials(LoginException): 26 | """Invalid account credentials""" 27 | 28 | pass 29 | 30 | 31 | class PtcBanned(Exception): 32 | """account is ptc banned, report as such""" 33 | pass 34 | 35 | 36 | class PtcAuth: 37 | def __init__(self, cookie_monster: CookieMonster): 38 | self.cookie_monster = cookie_monster 39 | 40 | async def auth(self, username: str, password: str, full_url: str) -> str: 41 | logger.info(f"Starting auth for {username}") 42 | 43 | # proxies = None 44 | # if proxy: 45 | # proxies = {"http://": proxy, "https://": proxy} 46 | 47 | attempts = COOKIE_STORAGE + 1 48 | while attempts > 0: 49 | attempts -= 1 50 | cookie = await self.cookie_monster.get_reese_cookie() 51 | 52 | async with requests.AsyncSession( 53 | headers={ 54 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 55 | "Accept-Language": "en-us", 56 | "Connection": "keep-alive", 57 | "Accept-Encoding": "gzip, deflate, br", 58 | "User-Agent": ptc_utils.USER_AGENT, 59 | }, 60 | allow_redirects=True, 61 | verify=False, 62 | timeout=10, 63 | proxy=cookie.proxy.full_url.geturl(), 64 | cookies=cookie.cookies, 65 | impersonate="chrome", 66 | ) as client: 67 | logger.info("Calling OAUTH page") 68 | 69 | try: 70 | resp = await client.get(full_url) 71 | except Exception as e: 72 | logger.error(f"Error {str(e)} during OAUTH") 73 | continue 74 | 75 | if not await self.__check_status(resp, cookie): 76 | continue 77 | 78 | csrf, challenge = self.__extract_csrf_and_challenge(resp.text) 79 | 80 | logger.info("Calling LOGIN page") 81 | 82 | try: 83 | login_resp = await client.post( 84 | ACCESS_URL + "login", 85 | headers={"Content-Type": "application/x-www-form-urlencoded"}, 86 | data={"_csrf": csrf, "challenge": challenge, "email": username, "password": password}, 87 | ) 88 | except Exception as e: 89 | logger.error(f"Error {str(e)} during LOGIN") 90 | continue 91 | 92 | if not await self.__check_status(login_resp, cookie): 93 | continue 94 | 95 | login_code = self.__extract_login_code(login_resp.text) 96 | 97 | if not login_code: 98 | if "error-message" in login_resp.text: 99 | self.check_error_on_login_page(login_resp.text) 100 | logger.error( 101 | f"Please send this to Malte on Discord (error page after login)\n{login_resp.text}" 102 | ) 103 | raise LoginException("Login failed, probably invalid credentials") 104 | 105 | logger.info("Calling CONSENT page") 106 | 107 | try: 108 | logger.debug(login_resp.text) 109 | csrf_consent, challenge_consent = self.__extract_csrf_and_challenge(login_resp.text) 110 | except LoginException: 111 | logger.error(f"Could not find a CSRF token for account {username} - it's probably unactivated") 112 | raise InvalidCredentials() 113 | 114 | try: 115 | resp_consent = await client.post( 116 | ACCESS_URL + "consent", 117 | data={"challenge": challenge_consent, "_csrf": csrf_consent, "allow_submit": "Allow"}, 118 | ) 119 | except Exception as e: 120 | logger.error(f"Error {str(e)} during CONSENT") 121 | continue 122 | 123 | if not await self.__check_status(resp_consent, cookie): 124 | continue 125 | 126 | login_code = self.__extract_login_code(resp_consent.text) 127 | if not login_code: 128 | raise LoginException("No Login Code after consent, please check account") 129 | return login_code 130 | 131 | raise LoginException("Exceeded max retries during PTC auth") 132 | 133 | async def __check_status(self, resp: httpx.Response, cookie: ReeseCookie) -> bool: 134 | if resp.status_code == 403 or "Request unsuccessful. Incapsula" in resp.text: 135 | await self.handle_imperva_error(resp.text, cookie) 136 | return False 137 | 138 | logger.debug(f"PTC response: {resp.status_code} | {resp.text}") 139 | 140 | if resp.status_code == 418: 141 | raise PtcBanned() 142 | 143 | if resp.status_code != 200: 144 | raise LoginException(f"PTC: {resp.status_code} but expected 200 - {resp.text}") 145 | 146 | return True 147 | 148 | async def handle_imperva_error(self, html: str, cookie: ReeseCookie): 149 | imp_code, imp_reason = ptc_utils.get_imperva_error_code(html) 150 | await self.cookie_monster.remove_cookie(cookie) 151 | cookie.proxy.rate_limited() 152 | logger.warning( 153 | f"Error code {imp_code} ({imp_reason}) during PTC request, trying again with another proxy (Proxy: {cookie.proxy.url})" 154 | ) 155 | 156 | def check_error_on_login_page(self, content: str): 157 | if "Your username or password is incorrect." in content: 158 | logger.warning("BROWSER: Incorrect credentials") 159 | raise InvalidCredentials("Incorrect account credentials") 160 | elif "your account has been disabled for" in content: 161 | logger.error("BROWSER: Account is temporarily disabled") 162 | raise InvalidCredentials("Account temporarily disabled") 163 | elif "We are unable to log you in to this account. Please contact Customer Service for additional details." in content: 164 | raise PtcBanned() 165 | 166 | def __extract_login_code(self, html) -> str | None: 167 | matches = re.search(r"pokemongo://state=(.*?)(?:,code=(.*?))?(?='|$)", html) 168 | 169 | if matches and len(matches.groups()) == 2: 170 | return matches.group(2) 171 | 172 | def __extract_csrf_and_challenge(self, html: str) -> tuple[str, str]: 173 | csrf_regex = re.compile(r'name="_csrf" value="(.*?)">') 174 | challenge_regex = re.compile(r'name="challenge" value="(.*?)">') 175 | 176 | csrf_matches = csrf_regex.search(html) 177 | challenge_matches = challenge_regex.search(html) 178 | 179 | if csrf_matches and challenge_matches: 180 | return csrf_matches.group(1), challenge_matches.group(1) 181 | 182 | raise LoginException("Couldn't find CSRF or challenge in Auth response") 183 | -------------------------------------------------------------------------------- /xilriws/js/recaptcha.py: -------------------------------------------------------------------------------- 1 | SRC = """ 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var ReCaptchaInstance = (function () { 39 | function ReCaptchaInstance(siteKey, recaptchaID, recaptcha) { 40 | this.siteKey = siteKey; 41 | this.recaptchaID = recaptchaID; 42 | this.recaptcha = recaptcha; 43 | this.styleContainer = null; 44 | } 45 | ReCaptchaInstance.prototype.execute = function (action) { 46 | return __awaiter(this, void 0, void 0, function () { 47 | return __generator(this, function (_a) { 48 | return [2, this.recaptcha.enterprise.execute(this.recaptchaID, { action: action })]; 49 | }); 50 | }); 51 | }; 52 | ReCaptchaInstance.prototype.getSiteKey = function () { 53 | return this.siteKey; 54 | }; 55 | ReCaptchaInstance.prototype.hideBadge = function () { 56 | if (this.styleContainer !== null) { 57 | return; 58 | } 59 | this.styleContainer = document.createElement('style'); 60 | this.styleContainer.innerHTML = '.grecaptcha-badge{display:none !important;}'; 61 | document.head.appendChild(this.styleContainer); 62 | }; 63 | ReCaptchaInstance.prototype.showBadge = function () { 64 | if (this.styleContainer === null) { 65 | return; 66 | } 67 | document.head.removeChild(this.styleContainer); 68 | this.styleContainer = null; 69 | }; 70 | return ReCaptchaInstance; 71 | }()); 72 | 73 | ////////// 74 | 75 | 76 | var ELoadingState; 77 | (function (ELoadingState) { 78 | ELoadingState[ELoadingState["NOT_LOADED"] = 0] = "NOT_LOADED"; 79 | ELoadingState[ELoadingState["LOADING"] = 1] = "LOADING"; 80 | ELoadingState[ELoadingState["LOADED"] = 2] = "LOADED"; 81 | })(ELoadingState || (ELoadingState = {})); 82 | var ReCaptchaLoader = (function () { 83 | function ReCaptchaLoader() { 84 | } 85 | ReCaptchaLoader.load = function (siteKey, options) { 86 | if (options === void 0) { options = {}; } 87 | if (typeof document === 'undefined') { 88 | return Promise.reject(new Error('This is a library for the browser!')); 89 | } 90 | if (ReCaptchaLoader.getLoadingState() === ELoadingState.LOADED) { 91 | if (ReCaptchaLoader.instance.getSiteKey() === siteKey) { 92 | return Promise.resolve(ReCaptchaLoader.instance); 93 | } 94 | else { 95 | return Promise.reject(new Error('reCAPTCHA already loaded with different site key!')); 96 | } 97 | } 98 | if (ReCaptchaLoader.getLoadingState() === ELoadingState.LOADING) { 99 | if (siteKey !== ReCaptchaLoader.instanceSiteKey) { 100 | return Promise.reject(new Error('reCAPTCHA already loaded with different site key!')); 101 | } 102 | return new Promise(function (resolve, reject) { 103 | ReCaptchaLoader.successfulLoadingConsumers.push(function (instance) { return resolve(instance); }); 104 | ReCaptchaLoader.errorLoadingRunnable.push(function (reason) { return reject(reason); }); 105 | }); 106 | } 107 | ReCaptchaLoader.instanceSiteKey = siteKey; 108 | ReCaptchaLoader.setLoadingState(ELoadingState.LOADING); 109 | var loader = new ReCaptchaLoader(); 110 | return new Promise(function (resolve, reject) { 111 | loader.loadScript(siteKey, options.useRecaptchaNet || false, options.renderParameters ? options.renderParameters : {}, options.customUrl).then(function () { 112 | ReCaptchaLoader.setLoadingState(ELoadingState.LOADED); 113 | // var widgetID = loader.doExplicitRender(grecaptcha, siteKey, options.explicitRenderParameters ? options.explicitRenderParameters : {}); 114 | var instance = new ReCaptchaInstance(siteKey, 0, grecaptcha); 115 | ReCaptchaLoader.successfulLoadingConsumers.forEach(function (v) { return v(instance); }); 116 | ReCaptchaLoader.successfulLoadingConsumers = []; 117 | if (options.autoHideBadge) { 118 | instance.hideBadge(); 119 | } 120 | ReCaptchaLoader.instance = instance; 121 | resolve(instance); 122 | }).catch(function (error) { 123 | ReCaptchaLoader.errorLoadingRunnable.forEach(function (v) { return v(error); }); 124 | ReCaptchaLoader.errorLoadingRunnable = []; 125 | reject(error); 126 | }); 127 | }); 128 | }; 129 | ReCaptchaLoader.getInstance = function () { 130 | return ReCaptchaLoader.instance; 131 | }; 132 | ReCaptchaLoader.setLoadingState = function (state) { 133 | ReCaptchaLoader.loadingState = state; 134 | }; 135 | ReCaptchaLoader.getLoadingState = function () { 136 | if (ReCaptchaLoader.loadingState === null) { 137 | return ELoadingState.NOT_LOADED; 138 | } 139 | else { 140 | return ReCaptchaLoader.loadingState; 141 | } 142 | }; 143 | ReCaptchaLoader.prototype.loadScript = function (siteKey, useRecaptchaNet, renderParameters, customUrl) { 144 | var _this = this; 145 | if (useRecaptchaNet === void 0) { useRecaptchaNet = false; } 146 | if (renderParameters === void 0) { renderParameters = {}; } 147 | if (customUrl === void 0) { customUrl = ''; } 148 | var scriptElement = document.createElement('script'); 149 | scriptElement.setAttribute('recaptcha-v3-enterprise-script', ''); 150 | var scriptBase = 'https://www.google.com/recaptcha/enterprise.js'; 151 | if (useRecaptchaNet) { 152 | scriptBase = 'https://recaptcha.net/recaptcha/enterprise.js'; 153 | } 154 | if (customUrl) { 155 | scriptBase = customUrl; 156 | } 157 | if (renderParameters.render) { 158 | renderParameters.render = undefined; 159 | } 160 | var parametersQuery = this.buildQueryString(renderParameters); 161 | scriptElement.src = scriptBase + '?render=explicit' + parametersQuery; 162 | return new Promise(function (resolve, reject) { 163 | scriptElement.addEventListener('load', _this.waitForScriptToLoad(function () { 164 | resolve(scriptElement); 165 | }), false); 166 | scriptElement.onerror = function (error) { 167 | ReCaptchaLoader.setLoadingState(ELoadingState.NOT_LOADED); 168 | reject(error); 169 | }; 170 | document.head.appendChild(scriptElement); 171 | }); 172 | }; 173 | ReCaptchaLoader.prototype.buildQueryString = function (parameters) { 174 | var parameterKeys = Object.keys(parameters); 175 | if (parameterKeys.length < 1) { 176 | return ''; 177 | } 178 | return '&' + Object.keys(parameters) 179 | .filter(function (parameterKey) { 180 | return !!parameters[parameterKey]; 181 | }) 182 | .map(function (parameterKey) { 183 | return parameterKey + '=' + parameters[parameterKey]; 184 | }).join('&'); 185 | }; 186 | ReCaptchaLoader.prototype.waitForScriptToLoad = function (callback) { 187 | var _this = this; 188 | return function () { 189 | if (window.grecaptcha === undefined) { 190 | setTimeout(function () { 191 | _this.waitForScriptToLoad(callback); 192 | }, ReCaptchaLoader.SCRIPT_LOAD_DELAY); 193 | } 194 | else { 195 | window.grecaptcha.enterprise.ready(function () { 196 | callback(); 197 | }); 198 | } 199 | }; 200 | }; 201 | ReCaptchaLoader.prototype.doExplicitRender = function (grecaptcha, siteKey, parameters) { 202 | var augmentedParameters = { 203 | sitekey: siteKey, 204 | badge: parameters.badge, 205 | size: parameters.size, 206 | tabindex: parameters.tabindex 207 | }; 208 | if (parameters.container) { 209 | return grecaptcha.enterprise.render(parameters.container, augmentedParameters); 210 | } 211 | else { 212 | return grecaptcha.enterprise.render(augmentedParameters); 213 | } 214 | }; 215 | ReCaptchaLoader.loadingState = null; 216 | ReCaptchaLoader.instance = null; 217 | ReCaptchaLoader.instanceSiteKey = null; 218 | ReCaptchaLoader.successfulLoadingConsumers = []; 219 | ReCaptchaLoader.errorLoadingRunnable = []; 220 | ReCaptchaLoader.SCRIPT_LOAD_DELAY = 25; 221 | return ReCaptchaLoader; 222 | }()); 223 | """ -------------------------------------------------------------------------------- /xilriws/browser/browser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import os 5 | import re 6 | import sys 7 | from typing import Callable 8 | 9 | import zendriver 10 | from loguru import logger 11 | 12 | from xilriws.debug import IS_DEBUG 13 | from xilriws.extension_comm import ExtensionComm 14 | from xilriws.proxy import ProxyDistributor 15 | from xilriws.ptc_auth import LoginException 16 | from xilriws.ptc.ptc_utils import USER_AGENT 17 | 18 | logger = logger.bind(name="Browser") 19 | HEADLESS = not IS_DEBUG 20 | 21 | 22 | class ProxyException(Exception): 23 | pass 24 | 25 | 26 | class Browser: 27 | browser: zendriver.Browser | None = None 28 | tab: zendriver.Tab | None = None 29 | consecutive_failures = 0 30 | last_cookies: list[zendriver.cdp.network.CookieParam] | None = None 31 | session_count = 0 32 | first_run = True 33 | 34 | def __init__(self, extension_paths: list[str], ext_comm: ExtensionComm): 35 | self.extension_paths: list[str] = extension_paths 36 | self.ext_comm = ext_comm 37 | 38 | async def start_browser(self): 39 | if self.consecutive_failures >= 30: 40 | logger.critical(f"{self.consecutive_failures} consecutive failures in the browser! this is really bad") 41 | await asyncio.sleep(60 * 30) 42 | self.consecutive_failures -= 1 43 | return None 44 | 45 | logger.info("Browser starting") 46 | 47 | if self.browser: 48 | self.session_count += 1 49 | 50 | if self.session_count % 60 == 0: 51 | logger.info("Time for a browser restart") 52 | await self.stop_browser() 53 | elif not await self.health_check(): 54 | logger.info("Browser seems stale. Restarting") 55 | await self.stop_browser() 56 | 57 | if not self.browser: 58 | config = zendriver.Config(headless=HEADLESS, browser_executable_path=self.__find_chrome_executable()) 59 | config.add_argument(f"--user-agent={USER_AGENT}") 60 | if not IS_DEBUG: 61 | config.add_argument("--window-size=1,1") 62 | 63 | disabled_features = [ 64 | "OptimizationHints", 65 | "OptimizationHintsFetching", 66 | "OptimizationHintsFetchingAnonymousDataConsent", 67 | "ContextMenuPerformanceInfoAndRemoteHintFetching", 68 | "OptimizationTargetPrediction", 69 | "OptimizationGuideModelDownloading", 70 | "OptimizationGuidePageContentExtraction", 71 | "OptimizationHintsComponent", 72 | "OptimizationHintsFetchingSRP", 73 | "OptimizationPersonalizedHintsFetching", 74 | "OptimizationGuideModelExecution", 75 | "Translate", 76 | "BackForwardCache", 77 | "AcceptCHFrame", 78 | "MediaRouter", 79 | "DialMediaRouteProvider" 80 | ] 81 | config.add_argument(f"--disable-features={','.join(disabled_features)}") 82 | config.add_argument("--disable-hang-monitor") 83 | config.add_argument("--disable-background-networking") 84 | config.add_argument("--disable-breakpad") 85 | config.add_argument("--disable-default-apps") 86 | config.add_argument("--disable-renderer-backgrounding") 87 | config.add_argument("--no-first-run") 88 | 89 | 90 | try: 91 | for path in self.extension_paths: 92 | config.add_extension(path) 93 | 94 | self.browser = await zendriver.start(config) 95 | full_command = f"{config.browser_executable_path} {' '.join(config())}" 96 | logger.info(f"Starting browser: `{full_command}`") 97 | 98 | if "brave" in self.browser.config.browser_executable_path.lower(): 99 | self.tab = await self.browser.get("brave://settings/shields") 100 | await self.__set_setting( 101 | shadow_roots=[ 102 | "settings-ui", 103 | "settings-main", 104 | "settings-basic-page", 105 | "settings-default-brave-shields-page", 106 | ], 107 | element_id="fingerprintingSelectControlType", 108 | new_value="allow", 109 | tab=self.tab, 110 | ) 111 | 112 | await self.tab.get("brave://settings/privacy") 113 | await self.__set_setting( 114 | shadow_roots=[ 115 | "settings-ui", 116 | "settings-main", 117 | "settings-basic-page", 118 | "settings-privacy-page", 119 | "settings-brave-personalization-options", 120 | "settings-dropdown-menu", 121 | ], 122 | element_id="dropdownMenu", 123 | new_value="disable_non_proxied_udp", 124 | tab=self.tab, 125 | ) 126 | except Exception as e: 127 | full_command = f"{config.browser_executable_path} {' '.join(config())}" 128 | logger.error(str(e)) 129 | logger.error( 130 | f"Error while starting the browser. Please confirm you can start it manually by running " 131 | f"`{full_command}`" 132 | ) 133 | raise e 134 | 135 | async def __set_setting(self, shadow_roots: list[str], element_id: str, new_value: str, tab: zendriver.Tab): 136 | await tab.wait_for(shadow_roots[0]) 137 | 138 | inject_js = "const element=document." 139 | inject_js += ".".join(f"querySelector('{s}').shadowRoot" for s in shadow_roots) 140 | inject_js += f".getElementById('{element_id}');" 141 | inject_js += f"element.value='{new_value}';" 142 | inject_js += "element.dispatchEvent(new Event('change'));" 143 | 144 | try: 145 | await tab.evaluate(inject_js) 146 | except Exception as e: 147 | logger.warning(f"{str(e)} while changing setting {element_id}, ignoring") 148 | 149 | async def health_check(self) -> bool: 150 | async def _check(): 151 | if not self.tab: 152 | self.tab = await self.browser.get("about:blank") 153 | resp = await self.tab.send(zendriver.cdp.browser.get_version()) 154 | try: 155 | logger.debug(f"Health Check - Chrome version is {resp[1]}") 156 | except IndexError: 157 | pass 158 | 159 | try: 160 | await asyncio.wait_for(_check(), timeout=10) 161 | return True 162 | except Exception: 163 | return False 164 | 165 | async def get_cookies(self) -> dict[str, str]: 166 | reese_value: str | None = None 167 | attempts = 10 168 | while not reese_value and attempts > 0: 169 | attempts -= 1 170 | 171 | cookies = await self.tab.send(zendriver.cdp.network.get_cookies()) 172 | for cookie in cookies: 173 | if cookie.name == "reese84": 174 | logger.info("Got cookies") 175 | reese_value = cookie.value 176 | continue 177 | 178 | if not reese_value: 179 | await self.tab.wait(0.3) 180 | else: 181 | self.last_cookies = cookies 182 | 183 | if not reese_value: 184 | raise LoginException("Didn't find reese cookie in browser") 185 | 186 | return {c.name: c.value for c in self.last_cookies} 187 | 188 | async def get_js_check_handler(self, url: str) -> tuple[asyncio.Future, Callable]: 189 | js_future = asyncio.get_running_loop().create_future() 190 | basic_url = url.replace("https://", "").replace("/", "") 191 | 192 | async def js_check_handler(event: zendriver.cdp.network.ResponseReceived): 193 | handler_url = event.response.url 194 | if not handler_url.startswith(url): 195 | return 196 | if not handler_url.endswith(f"?d={basic_url}"): 197 | return 198 | if not js_future.done(): 199 | logger.debug(f"Passed JS check ({handler_url})") 200 | js_future.set_result(True) 201 | 202 | return js_future, js_check_handler 203 | 204 | async def new_tab(self): 205 | logger.info("Opening tab") 206 | if not self.tab: 207 | self.tab = await self.browser.get("about:blank") 208 | else: 209 | tab = await self.tab.get("about:blank", new_tab=True) 210 | await self.tab.close() 211 | self.tab = tab 212 | await self.tab.sleep(0.4) 213 | 214 | async def new_private_window(self): 215 | context_id = await self.browser.connection.send(zendriver.cdp.target.create_browser_context()) 216 | target_id = await self.browser.connection.send( 217 | zendriver.cdp.target.create_target("about:blank", browser_context_id=context_id) 218 | ) 219 | if self.tab: 220 | await self.tab.close() 221 | self.tab = next( 222 | filter( 223 | lambda item: item.type_ == "page" and item.target_id == target_id, 224 | self.browser.targets, 225 | ) 226 | ) 227 | 228 | async def __enable_private_extension(self, tab: zendriver.Tab): 229 | await tab.get("brave://extensions/") 230 | await tab.wait_for("extensions-manager") 231 | await tab.evaluate( 232 | "document.querySelector('extensions-manager').shadowRoot" 233 | ".querySelector('extensions-item-list').shadowRoot" 234 | ".querySelector('extensions-item').shadowRoot" 235 | ".querySelector('cr-button')" 236 | ".click()" 237 | ) 238 | await tab.wait_for("extensions-manager") 239 | 240 | await tab.evaluate( 241 | "document.querySelector('extensions-manager').shadowRoot" 242 | ".querySelector('#viewManager > extensions-detail-view.active').shadowRoot" 243 | ".querySelector('#allow-incognito').shadowRoot" 244 | ".querySelector('label#label input')" 245 | ".click()" 246 | ) 247 | 248 | async def log_ip(self): 249 | await self.tab.get(url="https://api.ipify.org/") 250 | ip_html = await self.tab.get_content() 251 | ip = re.search(r"\d*\.\d*\.\d*\.\d*", ip_html) 252 | if ip and ip.group(0): 253 | logger.info(f"Browser IP check: {ip.group(0)}") 254 | else: 255 | logger.info("Browser IP check failed") 256 | 257 | async def log_canvas_fingerprint(self): 258 | await self.tab.get("https://browserleaks.com/canvas") 259 | await self.tab.wait_for("#canvas-hash") 260 | c = await self.tab.get_content() 261 | for line in c.split("\n"): 262 | if 'id="canvas-hash"' in line: 263 | logger.info(f"Canvas fingerprint: {line}") 264 | 265 | async def stop_browser(self): 266 | await self.browser.stop() 267 | self.first_run = True 268 | self.tab = None 269 | self.browser = None 270 | 271 | def __find_chrome_executable(self, return_all=False): 272 | candidates = [] 273 | if sys.platform.startswith(("darwin", "cygwin", "linux", "linux2")): 274 | for item in os.environ.get("PATH").split(os.pathsep): 275 | for subitem in ( 276 | "brave", 277 | "brave-browser", 278 | "google-chrome", 279 | "chromium", 280 | "chromium-browser", 281 | "chrome", 282 | "google-chrome-stable", 283 | ): 284 | candidates.append(os.sep.join((item, subitem))) 285 | if "darwin" in sys.platform: 286 | candidates += [ 287 | "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 288 | "/Applications/Chromium.app/Contents/MacOS/Chromium", 289 | ] 290 | 291 | else: 292 | for item in map( 293 | os.environ.get, 294 | ("PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA", "PROGRAMW6432"), 295 | ): 296 | if item is not None: 297 | for subitem in ( 298 | # "BraveSoftware/Brave-Browser/Application", 299 | # "Google/Chrome/Application", 300 | # "Google/Chrome Beta/Application", 301 | # "Google/Chrome Canary/Application", 302 | "Chromium/Application", 303 | ): 304 | # candidates.append(os.sep.join((item, subitem, "brave.exe"))) 305 | candidates.append(os.sep.join((item, subitem, "chrome.exe"))) 306 | rv = [] 307 | for candidate in candidates: 308 | if os.path.exists(candidate) and os.access(candidate, os.X_OK): 309 | logger.debug("%s is a valid candidate... " % candidate) 310 | rv.append(candidate) 311 | 312 | winner = None 313 | 314 | if return_all and rv: 315 | return rv 316 | 317 | winner = next((r for r in rv if "brave" in r.lower()), None) 318 | 319 | if not winner: 320 | if rv and len(rv) > 1: 321 | # assuming the shortest path wins 322 | winner = min(rv, key=lambda x: len(x)) 323 | 324 | elif len(rv) == 1: 325 | winner = rv[0] 326 | 327 | if winner: 328 | return os.path.normpath(winner) 329 | 330 | raise FileNotFoundError( 331 | "could not find a valid chrome browser binary. please make sure chrome is installed." 332 | "or use the keyword argument 'browser_executable_path=/path/to/your/browser' " 333 | ) 334 | --------------------------------------------------------------------------------