Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
772ed5a176 |
63
Dockerfile
63
Dockerfile
@@ -1,48 +1,43 @@
|
||||
FROM node:20-alpine AS build
|
||||
FROM node:20.12.2-slim
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
# Install Chrome and dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates wget gnupg xvfb fonts-liberation \
|
||||
&& 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 \
|
||||
&& apt-get purge -y --auto-remove wget gnupg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
# Create a non-root user for running the app
|
||||
RUN useradd --create-home --home-dir /app --shell /bin/sh appuser
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN corepack prepare pnpm@10.28.0 --activate \
|
||||
# Entrypoint script
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod 755 /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Copy and install dependencies
|
||||
COPY --chown=appuser:appuser package.json pnpm-lock.yaml ./
|
||||
USER appuser
|
||||
RUN corepack prepare pnpm@9.0.0 --activate \
|
||||
&& pnpm install --frozen-lockfile --prod \
|
||||
&& pnpm store prune
|
||||
|
||||
COPY . .
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /app/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
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
|
||||
|
||||
RUN mkdir -p /app/cache && chown -R node:node /app
|
||||
|
||||
USER node
|
||||
# Copy app code
|
||||
COPY --chown=appuser:appuser . .
|
||||
|
||||
# Expose port (match your app's port)
|
||||
EXPOSE 10000
|
||||
|
||||
# Start Xvfb and run the bot
|
||||
CMD ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
|
||||
45
README.md
45
README.md
@@ -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
|
||||
|
||||
@@ -111,6 +111,48 @@ 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"
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
- **`domain`** (required): Target website URL
|
||||
- **`cookieName`** (required): Cookie name to wait for
|
||||
- **`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,
|
||||
"user_agent": "Mozilla/5.0...",
|
||||
"elapsed_time": 1.42
|
||||
}
|
||||
```
|
||||
|
||||
#### Response (No Cookie Found)
|
||||
```json
|
||||
{
|
||||
"cookie_name": "session_id",
|
||||
"cookie_value": null,
|
||||
"user_agent": "Mozilla/5.0...",
|
||||
"elapsed_time": 60.0
|
||||
}
|
||||
```
|
||||
|
||||
## 🌐 Proxy Configuration
|
||||
|
||||
The service supports HTTP proxies with authentication:
|
||||
@@ -142,6 +184,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
|
||||
|
||||
27
api_test.py
27
api_test.py
@@ -12,15 +12,24 @@ 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",
|
||||
},
|
||||
)
|
||||
print(resp3.json())
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -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
|
||||
npm start &
|
||||
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"
|
||||
|
||||
62
endpoints/cookie.js
Normal file
62
endpoints/cookie.js
Normal file
@@ -0,0 +1,62 @@
|
||||
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)}`
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForCookie({ domain, proxy, cookieName }, 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 {
|
||||
cookie_name: match.name,
|
||||
cookie_value: match.value,
|
||||
cookie_domain: match.domain,
|
||||
cookie_path: match.path,
|
||||
http_only: match.httpOnly,
|
||||
secure: match.secure,
|
||||
user_agent: userAgent,
|
||||
elapsed_time: (Date.now() - startTime) / 1000,
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(pollInterval)
|
||||
}
|
||||
|
||||
const cookies = await page.cookies()
|
||||
logBrowserCookies({ domain, cookieName, cookies })
|
||||
|
||||
return {
|
||||
cookie_name: cookieName,
|
||||
cookie_value: null,
|
||||
user_agent: userAgent,
|
||||
elapsed_time: (Date.now() - startTime) / 1000,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = waitForCookie
|
||||
58
index.js
58
index.js
@@ -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
|
||||
@@ -112,7 +110,7 @@ app.post('/cloudflare', async (req, res) => {
|
||||
}
|
||||
|
||||
let cacheKey, cached
|
||||
if (CACHE_ENABLED && data.mode === "iuam") {
|
||||
if (data.mode === "iuam") {
|
||||
|
||||
cacheKey = JSON.stringify(data)
|
||||
cached = readCache(cacheKey)
|
||||
@@ -150,7 +148,7 @@ app.post('/cloudflare', async (req, res) => {
|
||||
.then(r => ({ ...r }))
|
||||
.catch(err => ({ code: 500, message: err.message }))
|
||||
|
||||
if (CACHE_ENABLED && (!result.code || result.code === 200)) {
|
||||
if (!result.code || result.code === 200) {
|
||||
writeCache(cacheKey, result)
|
||||
}
|
||||
break
|
||||
@@ -170,6 +168,52 @@ 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 (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' })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user