From 5189ed1472a8d1de1cb8ebf9d3b79743ddfda792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Th=E1=BA=BF=20H=C6=B0ng?= Date: Sat, 12 Apr 2025 19:38:54 +0700 Subject: [PATCH] repo: init --- .gitignore | 178 ++++++++++++++++++++++++++++++++++++++++++ .swcrc | 31 ++++++++ LICENSE | 21 +++++ README.md | 24 ++++++ bun.lockb | Bin 0 -> 83340 bytes package.json | 31 ++++++++ src/config.ts | 46 +++++++++++ src/constants.ts | 12 +++ src/index.ts | 79 +++++++++++++++++++ src/logger.ts | 15 ++++ src/proxy/reflect4.ts | 43 ++++++++++ src/website/twitch.ts | 28 +++++++ tsconfig.json | 28 +++++++ 13 files changed, 536 insertions(+) create mode 100644 .gitignore create mode 100644 .swcrc create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bun.lockb create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/constants.ts create mode 100644 src/index.ts create mode 100644 src/logger.ts create mode 100644 src/proxy/reflect4.ts create mode 100644 src/website/twitch.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a351c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Castorsrm +config.json + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.swcrc b/.swcrc new file mode 100644 index 0000000..0a131da --- /dev/null +++ b/.swcrc @@ -0,0 +1,31 @@ +{ + "$schema": "https://swc.rs/schema.json", + "jsc": { + "parser": { + "syntax": "typescript", + "jsx": false, + "dynamicImport": false, + "privateMethod": false, + "functionBind": false, + "exportDefaultFrom": false, + "exportNamespaceFrom": false, + "decorators": false, + "decoratorsBeforeExport": false, + "topLevelAwait": true, + "importMeta": false + }, + "minify": { + "compress": { + "unused": true + }, + "mangle": true + }, + "transform": null, + "target": "esnext", + "loose": false, + "externalHelpers": false, + // Requires v1.2.50 or upper and requires target to be es2016 or upper. + "keepClassNames": false + }, + "minify": true +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dd40dfc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 tretrauit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1710ac2 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Castorsrm + +Rice shirt rice money :( + +## Installation + +This project uses Bun for dependency management and Node.js (or Bun on UNIX platforms) for the app execution. + +```bash +git clone https://git.tretrauit.me/tretrauit/castorsrm +cd castorsrm +bun install +bun run dev +# or execute "bun run dev-bun" to start the application with Bun instead +# Note that Bun will not work correctly with Playwright on Windows. +``` + +## Configuration + +On the first launch, it'll generate a `config.json` file. You can change the application settings by editing that file. + +## License + +[MIT](./LICENSE) diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..3390f68140eb4cf313819eeca78ba1ecdf0b6649 GIT binary patch literal 83340 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 LvM@^cpK|>_wb1KA literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..d841531 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "castorsrm", + "version": "0.1.0", + "module": "src/index.ts", + "type": "module", + "scripts": { + "dev": "node --env-file=.env --import @swc-node/register/esm-register ./src/index.ts", + "dev-bun": "bun ./src/index.ts", + "start": "node ./dist/index.js", + "start-bun": "bun ./dist/index.js", + "build": "swc ./src -d dist" + }, + "devDependencies": { + "@swc-node/register": "^1.10.10", + "@swc/cli": "^0.6.0", + "@swc/core": "^1.11.20", + "@types/bun": "latest", + "pino-pretty": "^13.0.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "patchright": "^1.51.3", + "pino": "^9.6.0", + "playwright": "^1.51.1" + }, + "trustedDependencies": [ + "@swc/core" + ] +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..55d2bf6 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,46 @@ +class Config { + playwright: { + browser: string; + headless: boolean; + cdp: string; + url: string; + } = { + browser: "chromium", + headless: true, + cdp: "ws://127.0.0.1:9222", + url: "https://www.twitch.tv/", + }; + proxy: { + mode: string, + count: number, + } = { + mode: "reflect4", + count: 0, + }; + logger: { + level: string, + } = { + level: "info", + }; + static fromJSON(json: string) { + const obj = JSON.parse(json); + const config = new Config(); + for (const [k, v] of Object.entries(obj)) { + if (k in config) { + // @ts-ignore + config[k] = v; + } else { + console.warn(`Unknown key '${k}' in configuration file. Ignoring it.`); + } + } + return config; + } + toJSON() { + const obj: any = {}; + for (const [k, v] of Object.entries(this)) { + obj[k] = v; + } + return JSON.stringify(obj, null, 4); + } +} +export default Config; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..52c301b --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,12 @@ +const REFLECT4_SERVERS = [ + "https://www.blockaway.net", + "https://www.croxyproxy.com", + "https://www.croxyproxy.rocks", + "https://www.croxy.network", + "https://www.croxy.org", + "https://www.youtubeunblocked.live", + "https://www.croxyproxy.net", + "https://proxyium.com" +]; + +export { REFLECT4_SERVERS }; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..5298570 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,79 @@ +import { chromium, type Browser, type LaunchOptions } from "patchright"; +import fs from 'node:fs'; +import * as reflect4 from "./proxy/reflect4"; +import logger from "./logger"; +import Config from "./config"; + +const version = "0.1.0"; + +logger.info(`Castorsrm v${version} - https://github.com/teppyboy/castorsrm`) +logger.warn("This software is provided by the author as is, without any warranty. Use at your own risk."); + +let config = new Config(); +if (fs.existsSync("config.json")) { + logger.info("Reading configuration from 'config.json'..."); + const text = await fs.promises.readFile("config.json", "utf-8"); + config = Config.fromJSON(text); +} else { + logger.info("No configuration file found. Using the default configuration."); + await fs.promises.writeFile("config.json", config.toJSON()); + logger.info("Default configuration file 'config.json' created."); +} + +logger.level = process.env.LOG_LEVEL || config.logger.level; +logger.info(`Logger level set to '${logger.level}'`); + +let browser: Browser; +let launchOptions: LaunchOptions = { + headless: config.playwright.headless, +} + +logger.info("Launching browser..."); +logger.debug(`Launch options: ${JSON.stringify(launchOptions)}`); + +switch (config.playwright.browser) { + case "chromium": + logger.info("Using Chromium as the browser provider."); + logger.warn("Chromium is not supported by Twitch. Use at your own risk."); + browser = await chromium.launch({ ...launchOptions }); + break; + case "chrome": + logger.info("Using Google Chrome as the browser provider."); + browser = await chromium.launch({ ...launchOptions, channel: "chrome" }); + break; + case "cdp": + logger.info("Using Chrome DevTools Protocol (CDP) for browser connection."); + browser = await chromium.connectOverCDP(config.playwright.cdp); + break; + default: + logger.warn(`Unsupported browser channel: '${config.playwright.browser}'`); + logger.warn("Castorsrm will try to launch the browser anyway, but it may not work as expected."); + browser = await chromium.launch({ ...launchOptions, channel: config.playwright.browser }); + break; +} + +logger.info("Browser launched successfully, spawning proxies"); +if (config.proxy.mode === "reflect4") { + const context = await browser.newContext(); + const tasks: Array> = []; + logger.info(`Spawning ${config.proxy.count} proxies...`); + for (let i = 0; i < config.proxy.count; i++) { + logger.debug(`Spawning proxy ${i + 1}...`); + tasks.push(reflect4.spawn(context, config.playwright.url)); + } + await Promise.all(tasks); + logger.info("All proxies spawned successfully."); +} + +process.on("SIGINT", () => { + logger.info("Received SIGINT. Closing browser..."); +}); + +process.on("SIGINT", async () => { + logger.info("Received SIGINT. Closing browser..."); + for (const context of browser.contexts()) { + await context.close(); + } + await browser.close(); + process.exit(); +}); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..2552390 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,15 @@ +import { type Logger, pino } from "pino"; +import pretty from "pino-pretty"; + +const stream = pretty({ + colorize: true, + translateTime: "SYS:standard", + ignore: "hostname,pid", +}); + +const logger: Logger = pino({ + name: "castorsrm", + level: "info", +}, stream); + +export default logger; \ No newline at end of file diff --git a/src/proxy/reflect4.ts b/src/proxy/reflect4.ts new file mode 100644 index 0000000..b575a2a --- /dev/null +++ b/src/proxy/reflect4.ts @@ -0,0 +1,43 @@ +import { type BrowserContext, type Locator } from "patchright"; +import logger from "../logger"; +import * as twitch from "../website/twitch"; +import * as constants from "../constants"; + +async function spawn(context: BrowserContext, targetUrl: string) { + const spawnId = btoa(Math.random().toString()).substring(4,10); + const server = constants.REFLECT4_SERVERS[Math.floor(Math.random() * constants.REFLECT4_SERVERS.length)]; + logger.debug(`[${spawnId}] Using reflect4 server: ${server}`); + const page = await context.newPage(); + await page.goto(server); + let targetInput: Locator | null = null; + const allInput = await page.locator("input").all(); + for (const input of allInput) { + const placeholder = await input.getAttribute("placeholder"); + if (placeholder?.includes("URL")) { + targetInput = input; + break; + } + } + if (!targetInput) { + logger.error(`[${spawnId}] Failed to find input field for URL input`); + return; + } + await targetInput.fill(targetUrl); + await targetInput.press("Enter"); + // Keep-alive the page open for 5 minutes then refresh + if (targetUrl.startsWith("https://www.twitch.tv/")) { + twitch.keepAlive(page, spawnId); + } else { + logger.warn(`[${spawnId}] Unsupported URL: ${targetUrl}`); + logger.warn(`[${spawnId}] Will try to keep alive, but no guarantees`); + while (true) { + await page.waitForTimeout(5 * 60 * 1000); // 5 minutes + await page.reload(); + logger.debug(`[${spawnId}] Reloaded page`); + } + } + await page.close(); + logger.info(`[${spawnId}] Proxy with the server ${server} closed`); +} + +export { spawn }; diff --git a/src/website/twitch.ts b/src/website/twitch.ts new file mode 100644 index 0000000..c4c0cb6 --- /dev/null +++ b/src/website/twitch.ts @@ -0,0 +1,28 @@ +import type { Page } from "patchright"; +import logger from "../logger"; + +async function keepAlive(page: Page, spawnId: string = "unknown") { + try { + let waitTime = 0; + while (true) { + // Wait for a random time between 1 and 11 seconds + const timeout = 1000 + Math.floor(Math.random() * 60 * 1000) % 10000; + logger.debug(`[${spawnId}] Waiting for ${timeout / 1000} seconds...`); + await page.waitForTimeout(timeout); + waitTime += timeout; + if ((await page.locator(".ScCoreButton-sc-ocjdkq-0.ggPgVz").all()).length > 0) { + logger.debug(`[${spawnId}] Player encountered an error, refreshing the page...`); + await page.reload({timeout: 0, waitUntil: "domcontentloaded"}); + } + if (waitTime > 5 * 60 * 1000) { + logger.debug(`[${spawnId}] Waited for more than 5 minutes, refreshing the page...`); + await page.reload({timeout: 0, waitUntil: "domcontentloaded"}); + waitTime = 0; + } + } + } catch (e) { + logger.error(`[${spawnId}] Error while keeping the page alive: ${e}`); + } +} + +export { keepAlive }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d683abe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "noImplicitAny": false + } +}