commit 5189ed1472a8d1de1cb8ebf9d3b79743ddfda792 Author: Nguyễn Thế Hưng Date: Sat Apr 12 19:38:54 2025 +0700 repo: init 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 0000000..3390f68 Binary files /dev/null and b/bun.lockb differ 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 + } +}