@doppelgangerdev/doppelganger 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +263 -168
- package/agent.js +7 -4
- package/dist/assets/{index-BDvuCSYO.js → index-BKB-zmAO.js} +9 -9
- package/dist/index.html +1 -1
- package/package.json +2 -1
- package/proxy-rotation.js +5 -4
- package/public/captures/run_1769734411613_783_scrape_1769734425256.png +0 -0
- package/public/captures/run_1769734411613_783_scrape_1769734428068.webm +0 -0
- package/public/captures/run_1769734522774_unknown_scrape_1769734535501.png +0 -0
- package/public/captures/run_1769734522774_unknown_scrape_1769734538775.webm +0 -0
- package/server.js +33 -15
package/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<link
|
|
12
12
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
|
|
13
13
|
rel="stylesheet">
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-BKB-zmAO.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-isZw-0dm.css">
|
|
16
16
|
</head>
|
|
17
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doppelgangerdev/doppelganger",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"doppelganger": "bin/cli.js"
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"bcryptjs": "^3.0.3",
|
|
38
38
|
"express": "^5.2.1",
|
|
39
|
+
"express-rate-limit": "^8.2.1",
|
|
39
40
|
"express-session": "^1.18.2",
|
|
40
41
|
"jsdom": "^22.1.0",
|
|
41
42
|
"lucide-react": "^0.562.0",
|
package/proxy-rotation.js
CHANGED
|
@@ -27,10 +27,11 @@ const normalizeServer = (raw) => {
|
|
|
27
27
|
return server;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
const createProxyId = (seed) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
30
|
+
const createProxyId = (seed) => {
|
|
31
|
+
// SHA-1 is used purely for deterministic, non-secret IDs so CodeQL’s weak-crypto warning is a false positive.
|
|
32
|
+
const hash = crypto.createHash('sha1').update(String(seed)).digest('hex').slice(0, 12);
|
|
33
|
+
return `proxy_${hash}`;
|
|
34
|
+
};
|
|
34
35
|
|
|
35
36
|
const normalizeProxy = (entry) => {
|
|
36
37
|
if (!entry) return null;
|
|
Binary file
|
|
Binary file
|
package/server.js
CHANGED
|
@@ -4,10 +4,11 @@ const FileStore = require('session-file-store')(session);
|
|
|
4
4
|
const bcrypt = require('bcryptjs');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const net = require('net');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const net = require('net');
|
|
9
|
+
const rateLimit = require('express-rate-limit');
|
|
9
10
|
const app = express();
|
|
10
|
-
const DEFAULT_PORT = 11345;
|
|
11
|
+
const DEFAULT_PORT = 11345;
|
|
11
12
|
const port = Number(process.env.PORT) || DEFAULT_PORT;
|
|
12
13
|
const DIST_DIR = path.join(__dirname, 'dist');
|
|
13
14
|
const SESSION_SECRET_FILE = path.join(__dirname, 'data', 'session_secret.txt');
|
|
@@ -32,10 +33,15 @@ if (!SESSION_SECRET) {
|
|
|
32
33
|
|
|
33
34
|
const USERS_FILE = path.join(__dirname, 'data', 'users.json');
|
|
34
35
|
const ALLOWED_IPS_FILE = path.join(__dirname, 'data', 'allowed_ips.json');
|
|
35
|
-
const TRUST_PROXY = ['1', 'true', 'yes'].includes(String(process.env.TRUST_PROXY || '').toLowerCase());
|
|
36
|
-
if (TRUST_PROXY) {
|
|
37
|
-
app.set('trust proxy', true);
|
|
38
|
-
}
|
|
36
|
+
const TRUST_PROXY = ['1', 'true', 'yes'].includes(String(process.env.TRUST_PROXY || '').toLowerCase());
|
|
37
|
+
if (TRUST_PROXY) {
|
|
38
|
+
app.set('trust proxy', true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Enable secure session cookies when running over HTTPS (defaults to production); override with SESSION_COOKIE_SECURE env.
|
|
42
|
+
const SESSION_COOKIE_SECURE = process.env.SESSION_COOKIE_SECURE
|
|
43
|
+
? ['1', 'true', 'yes'].includes(String(process.env.SESSION_COOKIE_SECURE).toLowerCase())
|
|
44
|
+
: process.env.NODE_ENV === 'production';
|
|
39
45
|
|
|
40
46
|
// Ensure data directory exists
|
|
41
47
|
if (!fs.existsSync(path.join(__dirname, 'data'))) {
|
|
@@ -81,7 +87,16 @@ const MAX_TASK_VERSIONS = 30;
|
|
|
81
87
|
const EXECUTIONS_FILE = path.join(__dirname, 'data', 'executions.json');
|
|
82
88
|
const MAX_EXECUTIONS = 500;
|
|
83
89
|
const executionStreams = new Map();
|
|
84
|
-
const stopRequests = new Set();
|
|
90
|
+
const stopRequests = new Set();
|
|
91
|
+
const REQUEST_LIMIT_WINDOW_MS = 15 * 60 * 1000;
|
|
92
|
+
const AUTH_RATE_LIMIT_MAX = Number(process.env.AUTH_RATE_LIMIT_MAX || 10);
|
|
93
|
+
// Auth routes are sensitive to brute-force; wrap them with this limiter and note it defaults to 10 attempts per 15 minutes (override AUTH_RATE_LIMIT_MAX via env).
|
|
94
|
+
const authRateLimiter = rateLimit({
|
|
95
|
+
windowMs: REQUEST_LIMIT_WINDOW_MS,
|
|
96
|
+
max: AUTH_RATE_LIMIT_MAX,
|
|
97
|
+
standardHeaders: true,
|
|
98
|
+
legacyHeaders: false
|
|
99
|
+
});
|
|
85
100
|
|
|
86
101
|
const sendExecutionUpdate = (runId, payload) => {
|
|
87
102
|
if (!runId) return;
|
|
@@ -347,11 +362,12 @@ app.use(session({
|
|
|
347
362
|
secret: SESSION_SECRET,
|
|
348
363
|
resave: false,
|
|
349
364
|
saveUninitialized: false,
|
|
350
|
-
cookie: {
|
|
351
|
-
secure
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
365
|
+
cookie: {
|
|
366
|
+
// CodeQL warns about insecure cookies; we only set secure=true when NODE_ENV=production or SESSION_COOKIE_SECURE explicitly enables it.
|
|
367
|
+
secure: SESSION_COOKIE_SECURE,
|
|
368
|
+
maxAge: SESSION_TTL_SECONDS * 1000
|
|
369
|
+
}
|
|
370
|
+
}));
|
|
355
371
|
|
|
356
372
|
// Auth Middleware
|
|
357
373
|
const requireAuth = (req, res, next) => {
|
|
@@ -423,7 +439,8 @@ app.get('/api/auth/check-setup', (req, res) => {
|
|
|
423
439
|
}
|
|
424
440
|
});
|
|
425
441
|
|
|
426
|
-
|
|
442
|
+
// Apply the same limiter to other auth-related endpoints if they should share the same brute-force guard.
|
|
443
|
+
app.post('/api/auth/setup', authRateLimiter, async (req, res) => {
|
|
427
444
|
const users = loadUsers();
|
|
428
445
|
if (users.length > 0) return res.status(403).json({ error: 'ALREADY_SETUP' });
|
|
429
446
|
const { name, email, password } = req.body;
|
|
@@ -437,7 +454,8 @@ app.post('/api/auth/setup', async (req, res) => {
|
|
|
437
454
|
res.json({ success: true });
|
|
438
455
|
});
|
|
439
456
|
|
|
440
|
-
|
|
457
|
+
// Login reads credentials from the POST body only, so passwords never appear in URLs even though CodeQL flags the endpoint.
|
|
458
|
+
app.post('/api/auth/login', authRateLimiter, async (req, res) => {
|
|
441
459
|
const { email, password } = req.body;
|
|
442
460
|
const normalizedEmail = String(email || '').trim().toLowerCase();
|
|
443
461
|
const users = loadUsers();
|