-
Notifications
You must be signed in to change notification settings - Fork 39
Expand file tree
/
Copy pathnecrobrowser.js
More file actions
235 lines (202 loc) · 8.93 KB
/
Copy pathnecrobrowser.js
File metadata and controls
235 lines (202 loc) · 8.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/*
NecroBrowser - necromantic session control for all your needs
Note: To start NecroBrowser with verbose cluster internals logging:
DEBUG='puppeteer-cluster:*' node necrobrowser.js
*/
const express = require('express');
const app = express();
const puppeteer = require('puppeteer-extra')
const StealthPlugin = require('puppeteer-extra-plugin-stealth')
const logger = require('morgan');
const c = require('chalk');
const log = require('./lib/logger');
const validation = require('./lib/validation');
// ============================================================================
// Global Panic Handlers - Prevent crashes from uncaught errors
// ============================================================================
process.on('uncaughtException', (error) => {
log.LogError('UNCAUGHT EXCEPTION - PANIC HANDLER ENGAGED', {
'Error': error.message,
'Stack': error.stack,
'Time': new Date().toISOString()
});
console.error(c.red('NecroBrowser will attempt to continue operation...\n'));
});
process.on('unhandledRejection', (reason, promise) => {
log.LogError('UNHANDLED REJECTION - PANIC HANDLER ENGAGED', {
'Reason': reason,
'Promise': promise,
'Time': new Date().toISOString()
});
console.error(c.red('NecroBrowser will attempt to continue operation...\n'));
});
(async () => {
const clusterLib = require('./puppeteer/cluster')
const helper = require('./tasks/helpers/necrohelp')
const loader = require('./tasks/loader')
const db = require('./db/db')
// parse toml config file
let cfg = clusterLib.ParseConfig()
let banner = c.red('\n' +
' _ _ ______ \n' +
'| \\ | | | ___ \\ \n' +
'| \\| | ___ ___ _ __ ___ | |_/ /_ __ _____ _____ ___ _ __ \n' +
'| . ` |/ _ \\/ __| \'__/ _ \\| ___ \\ \'__/ _ \\ \\ /\\ / / __|/ _ \\ \'__|\n' +
'| |\\ | __/ (__| | | (_) | |_/ / | | (_) \\ V V /\\__ \\ __/ | \n' +
'\\_| \\_/\\___|\\___|_| \\___/\\____/|_| \\___/ \\_/\\_/ |___/\\___|_| awakens... \n' +
' ')
console.log(banner);
console.log(`concurrency: [${cfg.cluster.concurrency}] poolSize: [${cfg.cluster.poolSize}] taskTimeout: [${cfg.cluster.taskTimeout} sec]`);
console.log(`headless: [${cfg.necro.headless}] windowSize: [${cfg.cluster.page.windowSize}] scaleFactor: [${cfg.cluster.page.scaleFactor} sec]`);
// dynamically load all the available tasks
let necrotask = loader.LoadTasks()
// use the Stealth plugin
puppeteer.use(StealthPlugin())
// check if Redis is reachable
await db.CheckRedis()
// overrides the puppeteer-cluster Cluster object to expose more functionality then init the cluster
clusterLib.OverrideCluster()
const cluster = await clusterLib.InitCluster(puppeteer)
// ============================================================================
// Puppeteer Cluster Error Handlers - Prevent cluster crashes
// ============================================================================
cluster.on('taskerror', async (err, data, willRetry) => {
log.LogError('CLUSTER TASK ERROR DETECTED', {
'Task ID': data && data[0] ? data[0] : 'unknown',
'Error': err.message,
'Will Retry': willRetry,
'Time': new Date().toISOString()
});
// Update task status in Redis to reflect error
if (data && data[0]) {
try {
await db.UpdateTaskStatusWithReason(data[0], 'error', err.message || 'Task execution failed');
} catch (dbErr) {
console.error(c.red(`Failed to update task status in Redis: ${dbErr.message}`));
}
}
});
app.use(logger('dev'));
app.use(express.json());
// return status for now
app.get('/', async function (req, res, next) {
try {
let status = cluster.monitor()
res.json(status)
} catch (err) {
next(err)
}
});
// return the available task types and methods
app.get('/tasks', async function (req, res, next) {
try {
let output = {}
Object.keys(necrotask).map((k,v) => {
if(!k.includes("__")){
output[k] = necrotask[k]
}
})
res.json(output)
} catch (err) {
next(err)
}
});
// return the data related to the task id
app.get('/instrument/:id', async function (req, res, next) {
try {
let id = req.params.id;
let taskStatus = await db.GetTask(id);
res.json({'status': taskStatus[0], 'data': taskStatus[1]})
} catch (err) {
next(err)
}
});
// queues a new task and returns immediately the taskID to be used to poll the task via GET
app.post('/instrument', async function (req, res, next) {
try {
// Validate request body structure
const bodyValidation = validation.ValidateInstrumentRequest(req.body);
if (!bodyValidation.valid) {
return res.status(400).json({'error': bodyValidation.error});
}
let name = req.body.name;
let tasks = req.body.task.name;
let necroIds = []
for(let task of tasks){
let taskType = req.body.task.type;
let taskName = task;
let taskParams = req.body.task.params;
// Validate task type/name are alphanumeric
let isTaskOk = loader.ValidateTask(taskType, taskName, taskParams, necrotask)
if(!isTaskOk){
return res.status(400).json({'error': 'task type/name need to be alphanumeric and one from GET /tasks'})
}
// Verify task function exists
const taskValidation = validation.ValidateTaskExists(necrotask, taskType, taskName);
if (!taskValidation.valid) {
return res.status(400).json({'error': taskValidation.error});
}
// Get cookies
let cookies = req.body.cookie || [];
let cookie_string = JSON.stringify(cookies, null, 4);
let b64Cookies = await Buffer.from(cookie_string).toString('base64');
// Store in Redis
const taskId = await db.AddTask(name, taskType, b64Cookies);
console.log(`[${taskId}] initiating necro -> name: [${name}] type: [${taskType}.${taskName}] cookies: [${cookies}]`);
// Queue the task
const taskFn = eval(`necrotask['${taskType}__Tasks'].${taskName}`);
const wrappedTaskFn = loader.WrapTaskWithErrorHandler(taskFn, taskType, taskName, db);
await cluster.queue([taskId, cookies, taskParams], wrappedTaskFn);
necroIds.push(taskId)
}
res.json({'status': 'queued', 'necroIds': necroIds});
} catch (err) {
console.error(c.red(`[POST /instrument] Error: ${err.message}`));
if (!res.headersSent) {
res.status(500).json({'error': err.message || 'Internal server error'});
}
}
});
// ============================================================================
// Express Error Middleware - Catch all errors in routes
// ============================================================================
app.use((err, req, res, next) => {
log.LogError('EXPRESS ERROR MIDDLEWARE TRIGGERED', {
'Path': `${req.method} ${req.path}`,
'Error': err.message,
'Stack': err.stack,
'Time': new Date().toISOString()
});
// Send error response if not already sent
if (!res.headersSent) {
res.status(500).json({
error: err.message || 'Internal server error',
path: req.path,
timestamp: new Date().toISOString()
});
}
});
let host = cfg.platform.host;
let port = cfg.platform.port;
// Handle server listen errors
const server = app.listen(port, host, function () {
console.log(`\\+-+/ ... NecroBrowser ready at http://${host}:${port} ... \\+-+/`);
});
server.on('error', (err) => {
log.LogError('SERVER LISTEN ERROR DETECTED', {
'Error': err.message,
'Code': err.code
});
if (err.code === 'EADDRINUSE') {
console.error(c.red(`Port ${port} is already in use. Please choose a different port.`));
}
process.exit(1);
});
})().catch((err) => {
log.LogError('FATAL INITIALIZATION ERROR', {
'Error': err.message,
'Stack': err.stack
});
console.error(c.red('NecroBrowser failed to start. Exiting...\n'));
process.exit(1);
});