diff --git a/bun.lockb b/bun.lockb index 3390f68..ee92b9e 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 4104573..7dfb5f4 100644 --- a/package.json +++ b/package.json @@ -11,19 +11,20 @@ "build": "swc ./src -d dist --strip-leading-paths" }, "devDependencies": { - "@swc-node/register": "^1.10.10", + "@swc-node/register": "^1.11.1", "@swc/cli": "^0.6.0", - "@swc/core": "^1.11.20", + "@swc/core": "^1.15.11", "@types/bun": "latest" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.8.3" }, "dependencies": { - "patchright": "^1.51.3", - "pino": "^9.6.0", - "pino-pretty": "^13.0.0", - "playwright": "^1.51.1" + "@ghostery/adblocker-playwright": "^2.14.1", + "patchright": "^1.57.0", + "pino": "^9.14.0", + "pino-pretty": "^13.1.3", + "playwright": "^1.58.2" }, "trustedDependencies": [ "@swc/core" diff --git a/src/index.ts b/src/index.ts index 1803bf9..10d4eba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,92 +1,96 @@ -import { chromium, type Browser, type LaunchOptions } from "patchright"; -import fs from 'node:fs'; -import * as reflect4 from "./proxy/reflect4.js"; -import logger from "./logger.js"; -import Config from "./config.js"; -import { VERSION } from "./constants.js"; - -logger.info(`Castorsrm v${VERSION}`) -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."); - logger.info("Default configuration file 'config.json' created."); -} -// Write the new config file in case we updated something. -await fs.promises.writeFile("config.json", config.toJSON()); - -// Validate configuration -if (config.proxy.count < 1) { - logger.error("Proxy count must be greater than 0."); - process.exit(1); -} - -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((async () => { - const spawnId = `${i + 1}`; - while (true) { - try { - await reflect4.spawn(context, config.playwright.url, config.playwright.change_viewport, spawnId); - } catch (e) { - logger.error(`[${spawnId}] Error while running: ${e}`); - } - logger.warn(`[${spawnId}] Restarting in 3 seconds...`); - await new Promise(resolve => setTimeout(resolve, 3 * 1000)); - } - })()); - } - await Promise.all(tasks); - logger.info("All proxies spawned successfully."); -} - -process.on("SIGINT", async () => { - logger.info("Received SIGINT. Closing browser..."); - for (const context of browser.contexts()) { - await context.close(); - } - await browser.close(); - process.exit(); -}); +import { chromium, type Browser, type LaunchOptions } from "patchright"; +import { PlaywrightBlocker } from '@ghostery/adblocker-playwright'; +import fs from 'node:fs'; +import * as reflect4 from "./proxy/reflect4.js"; +import logger from "./logger.js"; +import Config from "./config.js"; +import { VERSION } from "./constants.js"; + +logger.info(`Castorsrm v${VERSION}`) +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."); + logger.info("Default configuration file 'config.json' created."); +} +// Write the new config file in case we updated something. +await fs.promises.writeFile("config.json", config.toJSON()); + +// Validate configuration +if (config.proxy.count < 1) { + logger.error("Proxy count must be greater than 0."); + process.exit(1); +} + +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, enabling adblocker..."); +let blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch); + +logger.info("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((async () => { + const spawnId = `${i + 1}`; + while (true) { + try { + await reflect4.spawn(context, blocker, config.playwright.url, config.playwright.change_viewport, spawnId); + } catch (e) { + logger.error(`[${spawnId}] Error while running: ${e}`); + } + logger.warn(`[${spawnId}] Restarting in 3 seconds...`); + await new Promise(resolve => setTimeout(resolve, 3 * 1000)); + } + })()); + } + await Promise.all(tasks); + logger.info("All proxies spawned successfully."); +} + +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/proxy/reflect4.ts b/src/proxy/reflect4.ts index 99e4edd..baec02b 100644 --- a/src/proxy/reflect4.ts +++ b/src/proxy/reflect4.ts @@ -1,59 +1,62 @@ -import { devices, type BrowserContext, type Locator } from "patchright"; -import logger from "../logger.js"; -import * as twitch from "../website/twitch.js"; -import * as constants from "../constants.js"; - -async function spawn(context: BrowserContext, targetUrl: string, changeViewport: boolean = false, spawnId: string = "unknown") { - 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(); - if (changeViewport) { - logger.debug(`[${spawnId}] Changing viewport size...`); - const deviceName = Object.keys(devices)[Math.floor(Math.random() * Object.keys(devices).length)]; - const device = devices[deviceName]; - logger.debug(`[${spawnId}] Using device: ${deviceName}`); - await page.setViewportSize(device.viewport); - } - try { - await page.goto(server); - } catch (e) { - logger.error(`[${spawnId}] Error while navigating to proxy website: ${e}`); - await page.close(); - throw e; - } - 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`); - await page.close(); - throw new Error(`Failed to find input field for URL input`); - } - await targetInput.fill(targetUrl); - await targetInput.press("Enter"); - logger.info(`[${spawnId}] Navigating to ${targetUrl}`); - await page.waitForTimeout(15000); // Wait for 15 second to let the page load - // Keep-alive the page open for 5 minutes then refresh - if (targetUrl.startsWith("https://www.twitch.tv/")) { - logger.info(`[${spawnId}] Twitch URL detected, using Twitch mode...`); - await 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 }; +import { devices, type BrowserContext, type Locator } from "patchright"; +import logger from "../logger.js"; +import * as twitch from "../website/twitch.js"; +import * as constants from "../constants.js"; +import type { PlaywrightBlocker } from "@ghostery/adblocker-playwright"; + +async function spawn(context: BrowserContext, blocker: PlaywrightBlocker, targetUrl: string, changeViewport: boolean = false, spawnId: string = "unknown") { + 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(); + logger.debug(`[${spawnId}] New page created, enabling adblocker...`); + await blocker.enableBlockingInPage(page as any); // As any because technically patchright Page != playwright Page :D + if (changeViewport) { + logger.debug(`[${spawnId}] Changing viewport size...`); + const deviceName = Object.keys(devices)[Math.floor(Math.random() * Object.keys(devices).length)]; + const device = devices[deviceName]; + logger.debug(`[${spawnId}] Using device: ${deviceName}`); + await page.setViewportSize(device.viewport); + } + try { + await page.goto(server); + } catch (e) { + logger.error(`[${spawnId}] Error while navigating to proxy website: ${e}`); + await page.close(); + throw e; + } + 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`); + await page.close(); + throw new Error(`Failed to find input field for URL input`); + } + await targetInput.fill(targetUrl); + await targetInput.press("Enter"); + logger.info(`[${spawnId}] Navigating to ${targetUrl}`); + await page.waitForTimeout(15000); // Wait for 15 second to let the page load + // Keep-alive the page open for 5 minutes then refresh + if (targetUrl.startsWith("https://www.twitch.tv/")) { + logger.info(`[${spawnId}] Twitch URL detected, using Twitch mode...`); + await 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 };