-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathapp.js
More file actions
410 lines (348 loc) · 11.5 KB
/
app.js
File metadata and controls
410 lines (348 loc) · 11.5 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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
import axios from 'axios'
import chalk from 'chalk'
import consoleStamp from 'console-stamp'
import fs from 'fs/promises'
import ora from 'ora'
import { v4 as uuidV4 } from 'uuid'
import WebSocket from 'ws'
import { getIpAddress, getProxyAgent, getRandomInt } from './utils.js'
consoleStamp(console, {
format: ':date(yyyy/mm/dd HH:MM:ss.l)'
})
process.setMaxListeners(0)
const getUnixTimestamp = () => Math.floor(Date.now() / 1000)
const PING_INTERVAL = 5 * 1000
const CHECKIN_INTERVAL = 2 * 60 * 1000 // 2 minutes
const DIRECTOR_SERVER = "https://director.getgrass.io"
const DEVICE_FILE = "devices.json"
// 错误模式,用于识别需要禁用代理的情况
const ERROR_PATTERNS = [
"Host unreachable",
"[SSL: WRONG_VERSION_NUMBER]",
"invalid length of packed IP address string",
"Empty connect reply",
"Device creation limit exceeded",
"sent 1011 (internal error) keepalive ping timeout"
]
class App {
constructor(user, proxy, deviceId = null, version = '5.2.0') {
this.proxy = proxy
this.userId = user.id
this.version = version
this.browserId = deviceId || uuidV4()
this.websocket = null
this.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
this.pingInterval = null
this.checkinInterval = null
this.spinner = null
}
/**
* 从director服务器获取WebSocket端点和token
*/
async getWsEndpoints() {
console.info(`[checkin] Getting WebSocket endpoints from director...`)
try {
const response = await axios.post(`${DIRECTOR_SERVER}/checkin`, {
browserId: this.browserId,
userId: this.userId,
version: this.version,
// extensionId: "lkbnfiajjmbhnfledhphioinpickokdi",
userAgent: this.userAgent,
deviceType: "desktop"
})
if (response.status === 201) {
const destinations = response.data.destinations || []
const token = response.data.token || ""
const websocketUrls = destinations.map(dest => `wss://${dest}?token=${token}`)
console.info(`[checkin] Received WebSocket endpoints: ${chalk.blue(websocketUrls)}`)
return { websocketUrls, token }
} else {
console.error(`[checkin] Failed with status: ${response.status}`)
return { websocketUrls: [], token: "" }
}
} catch (error) {
console.error(`[checkin] Error: ${chalk.red(error.message)}`)
return { websocketUrls: [], token: "" }
}
}
async start() {
if (this.proxy) {
console.info(`Request with proxy: ${chalk.blue(this.proxy)}...`)
}
// 获取代理的IP地址
console.info(`Getting IP address...`, this.proxy)
try {
const ipAddress = await getIpAddress(this.proxy)
console.info(`IP address: ${chalk.blue(ipAddress)}`)
if (this.proxy && !ipAddress.includes(new URL(this.proxy).hostname)) {
console.error(`[Warning] Proxy IP address does not match! maybe the proxy is not working...`)
// return false
}
} catch (e) {
console.error(`[ERROR] Could not get IP address! ${chalk.red(e)}`)
if (this.isErrorCritical(e.message)) {
console.error(`[ERROR] Critical proxy error.`)
return false
}
}
// 从director获取WebSocket端点
const { websocketUrls, token } = await this.getWsEndpoints()
if (websocketUrls.length === 0) {
console.error(`[ERROR] No WebSocket endpoints available`)
return false
}
// 随机选择一个WebSocket端点
const websocketUrl = websocketUrls[getRandomInt(0, websocketUrls.length - 1)]
const isWindows = this.userAgent.includes('Windows') || this.userAgent.includes('Win64') || this.userAgent.includes('Win32')
let options = {
headers: {
"Pragma": "no-cache",
"User-Agent": this.userAgent,
OS: 'Mac',
Browser: 'Mozilla',
Platform: 'Desktop',
"Origin": "chrome-extension://lkbnfiajjmbhnfledhphioinpickokdi",
"Sec-WebSocket-Version": "13",
'Accept-Language': 'uk-UA,uk;q=0.9,en-US;q=0.8,en;q=0.7',
"Cache-Control": "no-cache",
"priority": "u=1, i",
},
handshakeTimeout: 30000,
rejectUnauthorized: false,
}
if (this.proxy) {
console.log(`Configuring websocket proxy agent...(${this.proxy})`)
options.agent = await getProxyAgent(this.proxy)
console.log('Websocket proxy agent configured.')
}
this.websocket = new WebSocket(websocketUrl, options)
this.websocket.on('open', async function () {
console.log(`[wss] Websocket connected to ${chalk.green(websocketUrl)}!`)
this.startPing()
this.startCheckinInterval()
}.bind(this))
this.websocket.on('message', async function (data) {
let message = data.toString()
let parsedMessage
try {
parsedMessage = JSON.parse(message)
} catch (e) {
console.error(`[wss] Could not parse WebSocket message! ${chalk.red(message)}`)
console.error(`[wss] ${chalk.red(e)}`)
return
}
switch (parsedMessage.action) {
case 'AUTH':
const authResponse = JSON.stringify({
id: parsedMessage.id,
origin_action: parsedMessage.action,
result: {
browser_id: this.browserId,
user_id: this.userId,
user_agent: this.userAgent,
timestamp: getUnixTimestamp(),
device_type: "desktop",
version: this.version,
}
})
this.sendMessage(authResponse)
console.log(`[wss] (AUTH) -->: ${chalk.green(authResponse)}`)
break
case 'PONG':
console.log(`[wss] <--: ${chalk.green(message)}`)
break
case 'HTTP_REQUEST':
await this.handleHttpRequest(parsedMessage)
break
default:
console.error(`[wss] No handler for message: ${chalk.blue(message)}`)
console.error(`[wss] No handler for action ${chalk.red(parsedMessage.action)}!`)
break
}
}.bind(this))
this.websocket.on('close', async function (code) {
console.log(`[wss] Connection closed: ${chalk.red(code)}`)
this.clearIntervals()
setTimeout(() => {
this.start()
}, PING_INTERVAL)
}.bind(this))
this.websocket.on('error', function (error) {
console.error(`[wss] ${chalk.red(error.message)}`)
this.websocket.terminate()
this.clearIntervals()
setTimeout(() => {
this.start()
}, PING_INTERVAL)
}.bind(this))
return true
}
isErrorCritical(errorMessage) {
return ERROR_PATTERNS.some(pattern => errorMessage.includes(pattern)) ||
errorMessage.includes('Rate limited')
}
clearIntervals() {
if (this.pingInterval) {
clearInterval(this.pingInterval)
this.pingInterval = null
}
if (this.checkinInterval) {
clearInterval(this.checkinInterval)
this.checkinInterval = null
}
}
startPing() {
// 清除之前的interval
if (this.pingInterval) {
clearInterval(this.pingInterval)
}
this.pingInterval = setInterval(() => {
const message = JSON.stringify({
id: uuidV4(),
version: '1.0.0',
action: 'PING',
data: {},
})
this.sendMessage(message)
}, PING_INTERVAL)
}
startCheckinInterval() {
// 清除之前的interval
if (this.checkinInterval) {
clearInterval(this.checkinInterval)
}
this.checkinInterval = setInterval(async () => {
console.log(`[checkin] Performing periodic checkin...`)
await this.getWsEndpoints()
}, CHECKIN_INTERVAL)
}
async handleHttpRequest(message) {
try {
const data = message.data || {}
const method = (data.method || 'GET').toUpperCase()
const url = data.url
const headers = data.headers || {}
const body = data.body
console.log(`[http] Handling HTTP request: ${method} ${url}`)
const response = await axios({
method,
url,
headers,
data: body,
responseType: 'arraybuffer',
validateStatus: () => true, // 接受所有状态码
})
// 将响应体转换为base64
const bodyBase64 = Buffer.from(response.data).toString('base64')
const result = {
url,
status: response.status,
status_text: '',
headers: response.headers,
body: bodyBase64
}
const reply = {
id: message.id,
origin_action: 'HTTP_REQUEST',
result
}
this.sendMessage(JSON.stringify(reply))
console.log(`[http] HTTP response sent for ${method} ${url}, status: ${response.status}`)
if (response.status === 429) {
console.error(`[http] Rate limited! Status 429 returned.`)
throw new Error('Rate limited')
}
} catch (error) {
console.error(`[http] Error handling HTTP request: ${chalk.red(error.message)}`)
throw error
}
}
async sendMessage(message) {
if (this.websocket.readyState !== WebSocket.OPEN) {
console.error(`[wss] WebSocket is not open!`)
return
}
this.websocket.send(message)
console.log(`[wss] -->: ${chalk.green(typeof message === 'string' ? message : JSON.stringify(message))}`)
}
}
/**
* 加载设备ID映射,每个代理对应一个设备ID
*/
async function loadDeviceMapping() {
try {
const data = await fs.readFile(DEVICE_FILE, 'utf8')
const mappings = JSON.parse(data)
return mappings || {}
} catch (error) {
console.log(`No device mappings found, will create a new one.`)
return {}
}
}
/**
* 保存设备ID映射
*/
async function saveDeviceMapping(deviceMapping) {
try {
await fs.writeFile(DEVICE_FILE, JSON.stringify(deviceMapping, null, 2))
console.log(`Device mappings saved successfully.`)
} catch (error) {
console.error(`Error saving device mappings: ${error.message}`)
}
}
/**
* 为代理获取或创建设备ID
*/
async function getDeviceIdForProxy(proxy) {
const deviceMapping = await loadDeviceMapping()
if (deviceMapping[proxy]) {
console.log(`Using existing device ID for proxy ${proxy}: ${deviceMapping[proxy]}`)
return deviceMapping[proxy]
}
// 如果该代理没有对应的设备ID,创建一个新的
const newDeviceId = uuidV4()
deviceMapping[proxy] = newDeviceId
await saveDeviceMapping(deviceMapping)
console.log(`Created new device ID for proxy ${proxy}: ${newDeviceId}`)
return newDeviceId
}
export async function run(user, proxy = null) {
let deviceId = null
// 如果提供了代理,为该代理获取或创建专用设备ID
if (proxy) {
deviceId = await getDeviceIdForProxy(proxy)
} else {
// 没有代理的情况下,创建一个通用设备ID
deviceId = uuidV4()
}
const app = new App(user, proxy, deviceId)
const spinner = ora({ text: 'Loading…' }).start()
let prefixText = `[user:${chalk.green(user.id.substring(0, 12))}][device:${chalk.green(deviceId.substring(0, 8))}]`
if (proxy) {
try {
const [ip, port] = new URL(proxy).host.split(':')
prefixText += `[proxy:${chalk.green(ip)}:${chalk.green(port)}]`
} catch (e) {
prefixText += `[proxy:${chalk.green(proxy)}]`
}
}
spinner.prefixText = prefixText
spinner.succeed(`Started!`)
app.spinner = spinner
try {
const success = await app.start()
if (!success) {
console.error(`Failed to start.`)
return false
}
} catch (e) {
console.error(e)
return false
}
return true
}
// 为了确保程序能够干净地退出
process.on('SIGINT', function () {
console.log('Caught interrupt signal')
process.exit()
})