init
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
cookies
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
25
Dockerfile
Normal file
25
Dockerfile
Normal 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
102
README.md
@@ -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
17
cookies/my.sepay.vn.json
Normal 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
20
docker-compose.yml
Normal 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
229
index.js
Normal 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
2485
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user