3 Commits

Author SHA1 Message Date
estebanthilliez
aff6300cbd fix trivy release image
All checks were successful
CI / release (push) Successful in 1m41s
2026-04-24 22:54:28 +02:00
estebanthilliez
2220b92849 return full cookie jar on request
Some checks failed
CI / release (push) Failing after 30s
2026-04-24 22:26:27 +02:00
estebanthilliez
772ed5a176 add cookie wait endpoint
Some checks failed
CI / release (push) Failing after 1m21s
2026-04-24 22:21:24 +02:00
8 changed files with 283 additions and 141 deletions

5
.trivyignore Normal file
View File

@@ -0,0 +1,5 @@
# Chrome on Debian 12 currently pulls these runtime libraries without distro fixes available.
# Keep these temporary and remove them once Debian or Google Chrome ships patched packages.
CVE-2023-45853
CVE-2025-7458
CVE-2026-40393

View File

@@ -1,47 +1,54 @@
FROM node:20-alpine AS build
ENV NODE_ENV=production
RUN apk add --no-cache python3 make g++
RUN corepack enable
FROM node:20.19.5-bookworm-slim AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack prepare pnpm@10.28.0 --activate \
&& pnpm install --frozen-lockfile --prod \
RUN node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.pnpm = pkg.pnpm || {}; pkg.pnpm.overrides = { ...(pkg.pnpm.overrides || {}), 'basic-ftp': '5.3.0', 'path-to-regexp': '8.4.0', 'qs': '6.14.2' }; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');" \
&& corepack enable \
&& corepack prepare pnpm@10.33.2 --activate \
&& pnpm install --no-frozen-lockfile --prod \
&& pnpm store prune
COPY . .
FROM node:20-alpine
FROM node:20.19.5-bookworm-slim
ENV NODE_ENV=production
ENV CHROME_PATH=/usr/bin/chromium-browser
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
RUN apk add --no-cache chromium nss freetype harfbuzz ttf-freefont
RUN savedAptMark="$(apt-mark showmanual)" \
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
ca-certificates \
fonts-liberation \
gnupg \
wget \
xvfb \
&& wget -qO- https://dl.google.com/linux/linux_signing_key.pub \
| gpg --dearmor -o /usr/share/keyrings/google-linux-signing-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-linux-signing-keyring.gpg] https://dl.google.com/linux/chrome/deb/ stable main" \
> /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends google-chrome-stable \
&& [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark \
&& apt-mark manual ca-certificates fonts-liberation google-chrome-stable xvfb \
&& rm -f /etc/apt/sources.list.d/google-chrome.list /usr/share/keyrings/google-linux-signing-keyring.gpg \
&& rm -rf /var/lib/apt/lists/*
# Remove npm/corepack to shrink attack surface and avoid bundled CVEs.
RUN rm -rf /usr/local/lib/node_modules/npm \
/usr/local/bin/npm \
/usr/local/bin/npx \
/usr/local/lib/node_modules/corepack \
/usr/local/bin/corepack
RUN useradd --create-home --home-dir /app --shell /bin/sh appuser
WORKDIR /app
COPY --from=build /app/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod 755 /usr/local/bin/docker-entrypoint.sh
COPY --from=build --chown=node:node /app/package.json /app/package.json
COPY --from=build --chown=node:node /app/index.js /app/index.js
COPY --from=build --chown=node:node /app/endpoints /app/endpoints
COPY --from=build --chown=node:node /app/node_modules /app/node_modules
COPY --from=deps --chown=appuser:appuser /app/node_modules ./node_modules
COPY --chown=appuser:appuser package.json ./
COPY --chown=appuser:appuser . .
RUN mkdir -p /app/cache && chown -R node:node /app
RUN rm -rf /usr/local/lib/node_modules/npm \
&& rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack
USER node
USER appuser
EXPOSE 10000

View File

@@ -6,6 +6,7 @@ A Node.js service that automates Chromium to bypass **Cloudflare IUAM** and **Tu
- **Cloudflare IUAM Bypass**: Automatically solves "I'm Under Attack Mode" challenges super fast within 3.337 seconds 💖
- **Turnstile Challenge Solver**: Handles Cloudflare Turnstile captchas
- **Generic Cookie Waiter**: Opens a page, waits for a named cookie, and returns it
- **Proxy Support**: Full HTTP proxy integration with authentication
- **Smart Timeout**: 20-second timeout with graceful null responses
- **Browser Management**: Concurrent browser limit control
@@ -63,7 +64,6 @@ npm start # Production mode
| `authToken` | `null` | API authentication token |
| `browserLimit` | `20` | Max concurrent browsers |
| `timeOut` | `60000` | Global timeout (ms) |
| `CACHE_ENABLED` | `true` | Enable IUAM response cache |
## 📡 API Endpoints
@@ -76,9 +76,7 @@ Bypass Cloudflare protection and get cookies/tokens.
{
"mode": "iuam",
"domain": "https://olamovies.watch/generate",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"include_html": true,
"include_cookies": true
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
```
@@ -86,8 +84,6 @@ Bypass Cloudflare protection and get cookies/tokens.
- **`mode`** (required): `"iuam"` or `"turnstile"`
- **`domain`** (required): Target website URL
- **`user_agent`** (optional): User-Agent to use for the browser session
- **`include_html`** (optional): `true` to include the final page HTML in IUAM responses
- **`include_cookies`** (optional): `true` to include all cookies in IUAM responses
- **`proxy`** (optional): Proxy configuration object
#### Response (Success)
@@ -95,11 +91,7 @@ Bypass Cloudflare protection and get cookies/tokens.
{
"cf_clearance": "eNm9UOgqoNDTP.fmAK9JfvirEmLVpmd.ZWIfdqQxuTc-1758610092-1.2-2NwZwW6nK23HrAH71MtvOek9vCiiS7pUBGIPtra_gSBxYxY2csa6hW0j7i...",
"user_agent": "Mozilla/5.0...",
"elapsed_time": 3.05,
"html": "<!DOCTYPE html><html>...</html>",
"cookies": [
{ "name": "cf_clearance", "value": "...", "domain": ".example.com" }
]
"elapsed_time": 3.05
}
```
@@ -119,6 +111,61 @@ Bypass Cloudflare protection and get cookies/tokens.
}
```
### POST /cookie
Open a page and wait for a specific cookie to appear.
#### Request Body
```json
{
"domain": "https://example.com/",
"cookieName": "session_id",
"includeCookies": true
}
```
#### Parameters
- **`domain`** (required): Target website URL
- **`cookieName`** (required): Cookie name to wait for
- **`includeCookies`** (optional): Include the full browser cookie jar in the response
- **`user_agent`** (optional): User-Agent to use for the browser session
- **`proxy`** (optional): Proxy configuration object
#### Response (Success)
```json
{
"cookie_name": "session_id",
"cookie_value": "abc123",
"cookie_domain": ".example.com",
"cookie_path": "/",
"http_only": true,
"secure": true,
"cookies": [
{
"name": "session_id",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"httpOnly": true,
"secure": true
}
],
"user_agent": "Mozilla/5.0...",
"elapsed_time": 1.42
}
```
#### Response (No Cookie Found)
```json
{
"cookie_name": "session_id",
"cookie_value": null,
"cookies": [],
"user_agent": "Mozilla/5.0...",
"elapsed_time": 60.0
}
```
## 🌐 Proxy Configuration
The service supports HTTP proxies with authentication:
@@ -150,6 +197,7 @@ python api_test.py
cf-bypass/
├── endpoints/
│ ├── cloudflare.js # IUAM bypass logic
│ ├── cookie.js # Generic cookie waiter
│ └── turnstile.js # Turnstile solver
├── cache/
│ └── cache.json # Response cache

View File

@@ -12,15 +12,25 @@ async def main():
)
print(resp1.json())
resp2 = await client.post(
"http://localhost:8080/cloudflare",
json={
"domain": "https://lksfy.com/",
"siteKey": "0x4AAAAAAA49NnPZwQijgRoi",
"mode": "turnstile",
},
)
print(resp2.json())
resp2 = await client.post(
"http://localhost:8080/cloudflare",
json={
"domain": "https://lksfy.com/",
"siteKey": "0x4AAAAAAA49NnPZwQijgRoi",
"mode": "turnstile",
},
)
print(resp2.json())
resp3 = await client.post(
"http://localhost:8080/cookie",
json={
"domain": "https://example.com/",
"cookieName": "session_id",
"includeCookies": True,
},
)
print(resp3.json())
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,46 +1,33 @@
#!/bin/sh
set -eu
resolve_chrome_path() {
if [ -n "${CHROME_PATH:-}" ] && [ -x "$CHROME_PATH" ]; then
return 0
fi
rm -f /tmp/.X99-lock
Xvfb :99 -screen 0 1024x768x24 &
xvfb_pid=$!
for candidate in \
/usr/bin/chromium-browser \
/usr/bin/chromium \
/usr/bin/google-chrome-stable \
/usr/bin/google-chrome \
/opt/google/chrome/google-chrome; do
if [ -x "$candidate" ]; then
CHROME_PATH="$candidate"
return 0
fi
done
export DISPLAY=:99
if command -v chromium-browser >/dev/null 2>&1; then
CHROME_PATH="$(command -v chromium-browser)"
return 0
fi
if command -v chromium >/dev/null 2>&1; then
CHROME_PATH="$(command -v chromium)"
return 0
fi
if command -v google-chrome-stable >/dev/null 2>&1; then
CHROME_PATH="$(command -v google-chrome-stable)"
return 0
fi
if command -v google-chrome >/dev/null 2>&1; then
CHROME_PATH="$(command -v google-chrome)"
return 0
fi
node index.js &
app_pid=$!
echo "No Chrome/Chromium executable found. Set CHROME_PATH to a valid binary." >&2
exit 1
term_handler() {
kill "$app_pid" 2>/dev/null || true
kill "$xvfb_pid" 2>/dev/null || true
}
resolve_chrome_path
export CHROME_PATH
export PUPPETEER_EXECUTABLE_PATH="${PUPPETEER_EXECUTABLE_PATH:-$CHROME_PATH}"
trap term_handler INT TERM
exec node index.js
while kill -0 "$app_pid" 2>/dev/null; do
if ! kill -0 "$xvfb_pid" 2>/dev/null; then
echo "Xvfb exited; stopping app." >&2
kill "$app_pid" 2>/dev/null || true
wait "$app_pid" 2>/dev/null || true
exit 1
fi
sleep 1
done
wait "$app_pid"
app_status=$?
kill "$xvfb_pid" 2>/dev/null || true
wait "$xvfb_pid" 2>/dev/null || true
exit "$app_status"

View File

@@ -1,25 +1,3 @@
async function ensurePageLoaded(page, url) {
try {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 })
} catch (_) {}
}
async function getPageHtml(page) {
try {
return await page.content()
} catch (_) {
return null
}
}
async function getPageCookies(page, url) {
try {
return await page.cookies(url)
} catch (_) {
return null
}
}
async function cloudflare(data, page) {
return new Promise(async (resolve, reject) => {
if (!data.domain) return reject(new Error("Missing domain parameter"))
@@ -32,14 +10,11 @@ async function cloudflare(data, page) {
if (!isResolved) {
isResolved = true
const elapsedTime = (Date.now() - startTime) / 1000
const response = {
resolve({
cf_clearance: null,
user_agent: userAgent,
elapsed_time: elapsedTime,
}
if (data.include_html) response.html = null
if (data.include_cookies) response.cookies = null
resolve(response)
})
}
}, 20000)
@@ -77,25 +52,11 @@ async function cloudflare(data, page) {
isResolved = true
clearTimeout(cl)
const response = {
resolve({
cf_clearance,
user_agent: userAgent,
elapsed_time: elapsedTime,
}
if (data.include_html || data.include_cookies) {
await ensurePageLoaded(page, data.domain)
}
if (data.include_html) {
response.html = await getPageHtml(page)
}
if (data.include_cookies) {
response.cookies = await getPageCookies(page, data.domain)
}
resolve(response)
})
}
}
}

83
endpoints/cookie.js Normal file
View File

@@ -0,0 +1,83 @@
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function logBrowserCookies({ domain, cookieName, cookies }) {
console.log(
`[cookie] Browser cookies before return for ${domain} (${cookieName}): ${JSON.stringify(cookies)}`
)
}
function buildCookieResponse({ cookieName, matchedCookie, userAgent, elapsedTime, cookies, includeCookies }) {
const result = {
cookie_name: matchedCookie?.name ?? cookieName,
cookie_value: matchedCookie?.value ?? null,
user_agent: userAgent,
elapsed_time: elapsedTime,
}
if (matchedCookie) {
result.cookie_domain = matchedCookie.domain
result.cookie_path = matchedCookie.path
result.http_only = matchedCookie.httpOnly
result.secure = matchedCookie.secure
}
if (includeCookies) {
result.cookies = cookies
}
return result
}
async function waitForCookie({ domain, proxy, cookieName, includeCookies }, page) {
if (!domain) throw new Error("Missing domain parameter")
if (!cookieName) throw new Error("Missing cookieName parameter")
const timeout = global.timeOut || 60000
const pollInterval = 250
const startTime = Date.now()
if (proxy?.username && proxy?.password) {
await page.authenticate({
username: proxy.username,
password: proxy.password,
})
}
await page.goto(domain, { waitUntil: "domcontentloaded" })
const userAgent = await page.evaluate(() => navigator.userAgent)
while (Date.now() - startTime < timeout) {
const cookies = await page.cookies()
const match = cookies.find((cookie) => cookie.name === cookieName)
if (match) {
logBrowserCookies({ domain, cookieName, cookies })
return buildCookieResponse({
cookieName,
matchedCookie: match,
userAgent,
elapsedTime: (Date.now() - startTime) / 1000,
cookies,
includeCookies,
})
}
await sleep(pollInterval)
}
const cookies = await page.cookies()
logBrowserCookies({ domain, cookieName, cookies })
return buildCookieResponse({
cookieName,
userAgent,
elapsedTime: (Date.now() - startTime) / 1000,
cookies,
includeCookies,
})
}
module.exports = waitForCookie

View File

@@ -13,9 +13,6 @@ global.timeOut = Number(process.env.timeOut) || 60000
const CACHE_DIR = path.join(__dirname, "cache")
const CACHE_FILE = path.join(CACHE_DIR, "cache.json")
const CACHE_TTL = 5 * 60 * 1000
const CACHE_ENABLED = process.env.CACHE_ENABLED
? !["0", "false", "no"].includes(process.env.CACHE_ENABLED.toLowerCase())
: true
function loadCache() {
if (!fs.existsSync(CACHE_FILE)) return {}
@@ -62,10 +59,10 @@ if (process.env.NODE_ENV !== 'development') {
async function createBrowser(proxyServer = null) {
const connectOptions = {
headless: "new",
headless: false,
turnstile: true,
connectOption: { defaultViewport: null },
disableXvfb: true,
disableXvfb: false,
}
if (proxyServer) {
@@ -94,6 +91,7 @@ async function createBrowser(proxyServer = null) {
const turnstile = require('./endpoints/turnstile')
const cloudflare = require('./endpoints/cloudflare')
const waitForCookie = require('./endpoints/cookie')
app.post('/cloudflare', async (req, res) => {
const data = req.body
@@ -103,12 +101,6 @@ app.post('/cloudflare', async (req, res) => {
if (data.user_agent && typeof data.user_agent !== 'string') {
return res.status(400).json({ message: 'Bad Request: invalid user_agent' })
}
if (typeof data.include_html !== 'undefined' && typeof data.include_html !== 'boolean') {
return res.status(400).json({ message: 'Bad Request: invalid include_html' })
}
if (typeof data.include_cookies !== 'undefined' && typeof data.include_cookies !== 'boolean') {
return res.status(400).json({ message: 'Bad Request: invalid include_cookies' })
}
if (authToken && data.authToken !== authToken) {
return res.status(401).json({ message: 'Unauthorized' })
}
@@ -118,7 +110,7 @@ app.post('/cloudflare', async (req, res) => {
}
let cacheKey, cached
if (CACHE_ENABLED && data.mode === "iuam" && !data.include_html && !data.include_cookies) {
if (data.mode === "iuam") {
cacheKey = JSON.stringify(data)
cached = readCache(cacheKey)
@@ -156,7 +148,7 @@ app.post('/cloudflare', async (req, res) => {
.then(r => ({ ...r }))
.catch(err => ({ code: 500, message: err.message }))
if (CACHE_ENABLED && !data.include_html && !data.include_cookies && (!result.code || result.code === 200)) {
if (!result.code || result.code === 200) {
writeCache(cacheKey, result)
}
break
@@ -176,6 +168,55 @@ app.post('/cloudflare', async (req, res) => {
res.status(result.code ?? 200).json(result)
})
app.post('/cookie', async (req, res) => {
const data = req.body
if (!data || typeof data.domain !== 'string' || typeof data.cookieName !== 'string') {
return res.status(400).json({ message: 'Bad Request: missing or invalid domain/cookieName' })
}
if (typeof data.includeCookies !== 'undefined' && typeof data.includeCookies !== 'boolean') {
return res.status(400).json({ message: 'Bad Request: invalid includeCookies' })
}
if (data.user_agent && typeof data.user_agent !== 'string') {
return res.status(400).json({ message: 'Bad Request: invalid user_agent' })
}
if (authToken && data.authToken !== authToken) {
return res.status(401).json({ message: 'Unauthorized' })
}
if (global.browserLimit <= 0) {
return res.status(429).json({ message: 'Too Many Requests' })
}
global.browserLimit--
let result
let browser, page
try {
const proxyServer = data.proxy ? `${data.proxy.hostname}:${data.proxy.port}` : null
const ctx = await createBrowser(proxyServer)
browser = ctx.browser
page = ctx.page
await page.goto('about:blank')
if (data.user_agent) {
await page.setUserAgent(data.user_agent)
}
result = await waitForCookie(data, page)
.then((cookie) => ({ ...cookie }))
.catch((err) => ({ code: 500, message: err.message }))
} catch (err) {
result = { code: 500, message: err.message }
} finally {
if (browser) {
try { await browser.close() } catch {}
}
global.browserLimit++
}
res.status(result.code ?? 200).json(result)
})
app.use((req, res) => {
res.status(404).json({ message: 'Not Found' })
})