This commit is contained in:
2025-10-22 19:02:13 +07:00
parent 97c5a430f5
commit b0bcb6cd93
9 changed files with 2902 additions and 1 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
npm-debug.log
cookies

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM ghcr.io/puppeteer/puppeteer:latest
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV XDG_CONFIG_HOME=/tmp/.chromium
ENV XDG_CACHE_HOME=/tmp/.chromium
RUN mkdir -p /tmp/.chromium && chown -R pptruser:pptruser /tmp/.chromium
# ensure cookies dir exists
RUN mkdir -p /app/cookies && chown -R pptruser:pptruser /app/cookies
USER pptruser
EXPOSE 4000
# ensure browser binaries installed
RUN npx puppeteer browsers install chrome
CMD ["npm", "start"]

102
README.md
View File

@@ -1,2 +1,102 @@
# Puppeteer-Stealth-Proxy
---
# 🕵️ Puppeteer Stealth Proxy
A lightweight **HTTP proxy API** powered by **Puppeteer + Stealth Plugin**.
Bypasses Cloudflare and anti-bot systems with real browser automation.
---
## 🚀 Features
* Uses `puppeteer-extra-plugin-stealth` to avoid detection
* Auto-save & load cookies per domain
* Optional API key protection
* Proxy any HTTP method (GET, POST, etc.)
* Screenshot endpoint
* Reuses one browser instance for better performance
* Auto-closes browser on exit
---
## ⚙️ Setup
```bash
git clone https://github.com/yourname/puppeteer-stealth-proxy.git
cd puppeteer-stealth-proxy
npm install
```
Create `.env` (optional):
```bash
PORT=4000
API_KEY=your-secret-key
NODE_ENV=production
```
---
## 🏃 Run
```bash
npm run dev # development
NODE_ENV=production node index.js # production
```
Default: `http://localhost:4000`
---
## 🔑 API
### Health check
```
GET /
→ { "ok": true, "msg": "puppeteer-stealth-proxy" }
```
### Proxy request
```
GET /proxy?url=https://example.com
```
Optional headers:
```
x-api-key: your-secret-key
authorization: Bearer your-secret-key
```
### Screenshot
```
GET /screenshot?url=https://example.com
```
---
## 📁 Structure
```
index.js
cookies/
package.json
README.md
```
---
## 🧩 Notes
* Puppeteer runs in headless mode with stealth enabled
* Automatically stores cookies under `cookies/`
* Morgan logs disabled in production
---
## 📜 License
MIT © 2025 — Kain344

17
cookies/my.sepay.vn.json Normal file
View File

@@ -0,0 +1,17 @@
[
{
"name": "ap_s",
"value": "rn4cia450qbv6u0qv8s6j4qp8ituj80m",
"domain": "my.sepay.vn",
"path": "/",
"expires": 1761738980.12114,
"size": 36,
"httpOnly": true,
"secure": false,
"session": false,
"sameSite": "Lax",
"priority": "Medium",
"sameParty": false,
"sourceScheme": "Secure"
}
]

20
docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
puppeteer-stealth-proxy:
build: .
image: puppeteer-stealth-proxy:latest
container_name: puppeteer-stealth-proxy
ports:
- "4000:4000"
environment:
- NODE_ENV=production
# - API_KEY=your_api_key_here
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
tmpfs:
- /tmp:rw,noexec,nosuid,size=128m
volumes:
- ./cookies:/app/cookies

229
index.js Normal file
View File

@@ -0,0 +1,229 @@
import fs from "fs/promises";
import path from "path";
import express from "express";
import cors from "cors";
import morgan from "morgan";
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
puppeteer.use(StealthPlugin());
const PORT = process.env.PORT ? Number(process.env.PORT) : 4000;
const API_KEY = process.env.API_KEY || ""; // nếu đặt, yêu cầu query ?key=... hoặc header x-api-key
const COOKIES_DIR = path.resolve("./cookies");
await fs.mkdir(COOKIES_DIR, { recursive: true });
const app = express();
app.use(cors());
app.use(express.json({ limit: "2mb" }));
if (process.env.NODE_ENV !== "production") {
app.use(morgan("combined"))
}
function requireApiKey(req, res, next) {
if (!API_KEY) return next();
const key = req.query.key || req.headers["x-api-key"] || req.headers["authorization"];
if (!key || !(key === API_KEY || key === `Bearer ${API_KEY}`)) {
return res.status(403).json({ error: "Forbidden - invalid API key" });
}
next();
}
// single browser instance reused across requests
let browser;
async function ensureBrowser() {
if (browser) return browser;
browser = await puppeteer.launch({
headless: "new",
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-blink-features=AutomationControlled",
"--window-size=1200,900",
"--disable-features=IsolateOrigins,site-per-process",
],
});
// close on exit
const closeHandler = async () => {
try { if (browser) await browser.close(); } catch (e) { }
process.exit();
};
process.on("SIGINT", closeHandler);
process.on("SIGTERM", closeHandler);
return browser;
}
async function cookieFileFor(url) {
try {
const u = new URL(url);
const file = path.join(COOKIES_DIR, `${u.hostname}.json`);
return file;
} catch {
return null;
}
}
async function loadCookies(page, url) {
const file = await cookieFileFor(url);
if (!file) return;
try {
const raw = await fs.readFile(file, "utf-8");
const cookies = JSON.parse(raw);
if (Array.isArray(cookies) && cookies.length > 0) {
await page.setCookie(...cookies);
}
} catch (e) {
// ignore if no file
}
}
async function saveCookies(page, url) {
const file = await cookieFileFor(url);
if (!file) return;
try {
const cookies = await page.cookies();
await fs.writeFile(file, JSON.stringify(cookies, null, 2), "utf-8");
} catch (e) {
console.warn("saveCookies error", e.message);
}
}
// forwarded headers we allow the client to set
const ALLOWED_FORWARD = ["authorization", "content-type", "accept", "cookie", "user-agent"];
function buildForwardHeaders(req) {
const h = {};
for (const k of Object.keys(req.headers)) {
if (ALLOWED_FORWARD.includes(k.toLowerCase())) {
h[k] = req.headers[k];
}
}
return h;
}
app.get("/", (req, res) => res.json({ ok: true, msg: "puppeteer-stealth-proxy" }));
// proxy GET/HEAD (returns raw response from target, using page.goto)
app.all("/proxy", requireApiKey, async (req, res) => {
const target = req.query.url;
if (!target) return res.status(400).json({ error: "Missing url query param" });
let page;
try {
const b = await ensureBrowser();
page = await b.newPage();
// set UA (override headless UA)
const ua = req.headers["user-agent"] ||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
await page.setUserAgent(ua);
// load cookies if any
await loadCookies(page, target);
// forward headers
const extraHeaders = buildForwardHeaders(req);
if (Object.keys(extraHeaders).length > 0) {
await page.setExtraHTTPHeaders(extraHeaders);
}
// if non-GET, we intercept request to inject body
if (req.method !== "GET" && req.method !== "HEAD") {
await page.setRequestInterception(true);
page.on("request", intercepted => {
if (intercepted.isNavigationRequest() && intercepted.url() === target) {
// set postData only when we have a body
const opts = {
method: req.method,
headers: { ...intercepted.headers(), ...extraHeaders },
};
if (req.body && Object.keys(req.body).length > 0) {
const ct = req.headers["content-type"] || "application/json";
opts.postData = ct.includes("application/json") ? JSON.stringify(req.body) : req.rawBody || JSON.stringify(req.body);
opts.headers["content-type"] = ct;
}
intercepted.continue(opts);
} else {
intercepted.continue();
}
});
}
const response = await page.goto(target, { waitUntil: "networkidle2", timeout: 45000 });
if (!response) throw new Error("No response from page");
// get headers & body
const rHeaders = response.headers();
const buf = await response.buffer();
// persist cookies
await saveCookies(page, target);
// set response headers (filter some hop-by-hop headers)
const skip = new Set(["transfer-encoding", "content-encoding", "content-length", "connection"]);
for (const [k, v] of Object.entries(rHeaders)) {
if (skip.has(k.toLowerCase())) continue;
try {
const cleanValue = Array.isArray(v) ? v.join(", ") : String(v).replace(/[\r\n]+/g, " ").trim();
res.setHeader(k, cleanValue);
} catch (e) {
console.warn(`⚠️ Skipped invalid header ${k}: ${e.message}`);
}
}
res.status(response.status()).send(buf);
} catch (err) {
console.error("proxy error", err);
res.status(500).json({ error: "proxy failed", details: err.message });
} finally {
try { if (page) await page.close(); } catch (e) { }
}
});
// screenshot endpoint
app.get("/screenshot", requireApiKey, async (req, res) => {
const target = req.query.url;
if (!target) return res.status(400).json({ error: "Missing url" });
let page;
try {
const b = await ensureBrowser();
page = await b.newPage();
await page.setViewport({ width: 1280, height: 800 });
const ua = req.headers["user-agent"] ||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
await page.setUserAgent(ua);
await loadCookies(page, target);
// optionally set extra headers
const extraHeaders = buildForwardHeaders(req);
if (Object.keys(extraHeaders).length > 0) await page.setExtraHTTPHeaders(extraHeaders);
await page.goto(target, { waitUntil: "networkidle2", timeout: 45000 });
const buffer = await page.screenshot({ fullPage: true, type: "png" });
await saveCookies(page, target);
res.type("image/png").send(buffer);
} catch (err) {
console.error("screenshot error", err);
res.status(500).json({ error: "screenshot failed", details: err.message });
} finally {
try { if (page) await page.close(); } catch (e) { }
}
});
// graceful shutdown
process.on("SIGINT", async () => {
try { if (browser) await browser.close(); } catch (e) { }
process.exit(0);
});
process.on("SIGTERM", async () => {
try { if (browser) await browser.close(); } catch (e) { }
process.exit(0);
});
app.listen(PORT, () => console.log(`puppeteer-stealth-proxy listening on http://localhost:${PORT}`));

2485
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "puppeteer-stealth-proxy",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"type": "module",
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^5.1.0",
"morgan": "^1.10.1",
"puppeteer": "^24.26.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
}
}