├── entrypoint.sh ├── .gitignore ├── start-single.js ├── Dockerfile ├── run.js ├── .github └── workflows │ └── docker-image.yml ├── package.json ├── start.js ├── LICENSE ├── utils.js ├── README_CN.md ├── README.md ├── ssl └── websocket.pem └── app.js /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pm2 flush 4 | pm2 delete all 5 | 6 | USER_ID=${USER_ID} pm2 start /app/start.js 7 | 8 | pm2 logs 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | /logs 4 | /web/users/* 5 | proxies.txt 6 | example.js 7 | .env 8 | test.js 9 | devices.json 10 | -------------------------------------------------------------------------------- /start-single.js: -------------------------------------------------------------------------------- 1 | import { run } from './app.js' 2 | 3 | import { randomUserAgent } from './utils.js' 4 | 5 | const USER_ID = process.env.USER_ID 6 | 7 | if (!USER_ID) { 8 | console.error('USER_ID not set') 9 | process.exit(1) 10 | } 11 | 12 | const USER = { 13 | id: USER_ID, 14 | userAgent: randomUserAgent() 15 | } 16 | 17 | spinner.clear() 18 | spinner.info(`[${userId}] Starting with user without proxies...`).clear().start() 19 | 20 | async function main() { 21 | run(USER) 22 | } 23 | 24 | main().catch(console.error) 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | ENV NODE_ENV=production 4 | ENV USER_ID= 5 | 6 | WORKDIR /app 7 | 8 | RUN apt-get update -qq -y && \ 9 | apt-get install -y vim wget 10 | 11 | ADD . /app/ 12 | 13 | # install dependencies 14 | RUN npm install --omit=dev 15 | RUN npm install pm2 -g 16 | RUN pm2 install pm2-logrotate 17 | RUN pm2 set pm2-logrotate:compress true 18 | RUN pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss 19 | RUN pm2 set pm2-logrotate:rotateInterval '*/5 * * * *' 20 | RUN pm2 set pm2-logrotate:max_size 10M 21 | RUN pm2 set pm2-logrotate:retain 2 22 | RUN chmod +x /app/entrypoint.sh 23 | 24 | CMD ["/bin/bash", "/app/entrypoint.sh"] 25 | -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | import { input } from '@inquirer/prompts' 2 | import { exec } from 'child_process' 3 | 4 | async function main() { 5 | const userId = await input({ message: 'User ID: ' }) 6 | const appName = await input({ message: 'pm2 app name: ', default: `grass-${userId}` }) 7 | 8 | if (!userId) { 9 | console.log('User ID required') 10 | process.exit(1) 11 | } 12 | 13 | const command = `pm2 start start.js --name ${appName} --restart-delay=30000 -- --user ${userId}` 14 | 15 | exec(command, (error, stdout) => { 16 | if (error) { 17 | console.error(`exec error: ${error}`) 18 | return 19 | } 20 | 21 | console.log(`stdout: ${stdout}`) 22 | console.error(`command: ${command}`) 23 | }) 24 | } 25 | 26 | main().catch(console.error) 27 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Log in to Docker Hub 18 | uses: docker/login-action@v3.3.0 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v2 26 | - name: Build and push Docker image 27 | id: push 28 | uses: docker/build-push-action@v6.7.0 29 | with: 30 | platform: linux/amd64,linux/arm64 31 | context: . 32 | file: ./Dockerfile 33 | push: true 34 | tags: overtrue/getgrass-bot 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getgrass-bot", 3 | "version": "1.0.0", 4 | "description": "https://github.com/web3bothub/grass-bot", 5 | "exports": "./start.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@faker-js/faker": "^8.4.1", 15 | "@inquirer/prompts": "^4.3.1", 16 | "axios": "^1.7.7", 17 | "chalk": "^5.3.0", 18 | "commander": "^12.0.0", 19 | "console-stamp": "^3.1.2", 20 | "dotenv": "^16.4.5", 21 | "express": "^4.19.2", 22 | "http-proxy-agent": "^7.0.2", 23 | "https": "^1.0.0", 24 | "inquirer": "^9.2.17", 25 | "log-symbols": "^7.0.0", 26 | "node-fetch": "^3.3.2", 27 | "ora": "^8.1.1", 28 | "proxy-agent": "^6.4.0", 29 | "proxy-chain": "^2.5.4", 30 | "socks-proxy-agent": "^8.0.4", 31 | "uuid": "^9.0.1", 32 | "ws": "^8.16.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | import consoleStamp from 'console-stamp' 2 | import fs from 'fs' 3 | import { run } from './app.js' 4 | import { getRandomInt, randomUserAgent, sleep } from './utils.js' 5 | 6 | consoleStamp(console, { 7 | format: ':date(yyyy/mm/dd HH:MM:ss.l)' 8 | }) 9 | 10 | const USER_ID = process.env.USER_ID 11 | 12 | if (!USER_ID) { 13 | console.error('USER_ID not set') 14 | process.exit(1) 15 | } 16 | 17 | const USER = { 18 | id: USER_ID, 19 | userAgent: randomUserAgent() 20 | } 21 | 22 | const PROXIES = fs.readFileSync('proxies.txt').toString().split('\n').map(proxy => proxy.trim()).filter(proxy => proxy) 23 | 24 | console.info(`[${USER_ID}] Starting with user with ${PROXIES.length} proxies...`) 25 | 26 | async function main() { 27 | const promises = PROXIES.map(async proxy => { 28 | await sleep(getRandomInt(10, 6000)) 29 | console.info(`[${USER.id}] Starting with proxy ${proxy}...`) 30 | await run(USER, proxy) 31 | }) 32 | 33 | await Promise.all(promises) 34 | } 35 | 36 | main().catch(console.error) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 overtrue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { HttpsProxyAgent } from "https-proxy-agent" 3 | import { SocksProxyAgent } from "socks-proxy-agent" 4 | 5 | export const randomUserAgent = () => { 6 | const userAgents = [ 7 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", 8 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", 9 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", 10 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", 11 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", 12 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", 13 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", 14 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.3", 15 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36", 16 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", 17 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", 18 | ] 19 | 20 | return userAgents[Math.floor(Math.random() * userAgents.length)] 21 | } 22 | 23 | export const sleep = (ms) => { 24 | console.log('[SLEEP] sleeping for', ms, '...') 25 | return new Promise((resolve) => setTimeout(resolve, ms)) 26 | } 27 | 28 | export const getProxyAgent = async (proxy) => { 29 | if (proxy.startsWith('http://') || proxy.startsWith('https://')) { 30 | return new HttpsProxyAgent(proxy) 31 | } else if (proxy.startsWith('socks://') || proxy.startsWith('socks5://')) { 32 | return new SocksProxyAgent(proxy) 33 | } 34 | 35 | throw new Error(`Unsupported proxy ${proxy}`) 36 | } 37 | 38 | export function getRandomInt(min, max) { 39 | const minCeiled = Math.ceil(min) 40 | const maxFloored = Math.floor(max) 41 | return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled) // The maximum is exclusive and the minimum is inclusive 42 | } 43 | 44 | export async function getIpAddress(proxy) { 45 | let options = {} 46 | console.log(`[GET IP] Getting IP address...${proxy ? ` with proxy ${proxy}` : ''}`) 47 | 48 | if (proxy) { 49 | const agent = await getProxyAgent(proxy) 50 | console.log(`[GET IP] Using proxy agent...`) 51 | options.httpAgent = agent 52 | options.httpsAgent = agent 53 | } 54 | 55 | return await axios.get('https://myip.ipip.net', options) 56 | .then(response => response.data) 57 | } 58 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Getgrass Bot (2.x client version) 2 | 3 | getgrass-bot 是一个用于自动获取 [https://app.getgrass.io](https://app.getgrass.io/register/?referralCode=qY96g1DYyIxe3B4) 网站奖励的机器人(两倍积分)。 4 | 5 | > [!WARNING] 6 | > 我不对该机器人造成的任何损失或损害负责。使用风险自负。 7 | 8 | ## 使用方法 9 | 10 | `getgrass-bot` 可以通过 Docker 或手动运行。 11 | 12 | ## 获取您的用户 ID 13 | 14 | 您可以从 Getgrass 网站获取您的用户 ID: 15 | 16 | - 访问 [https://app.getgrass.io/dashboard](https://app.getgrass.io/register/?referralCode=qY96g1DYyIxe3B4)。 17 | - 打开浏览器的开发者工具(通常按 F12 或右键单击并选择“检查”)。 18 | - 转到“控制台”选项卡。 19 | - 粘贴以下命令并按回车: 20 | 21 | ```javascript 22 | const appUserIdKey = Object.keys(localStorage).find(key => key.endsWith('.appUserId')); 23 | if (appUserIdKey) { 24 | const appUserId = localStorage.getItem(appUserIdKey); 25 | console.log("appUserId:", appUserId); 26 | } 27 | ``` 28 | 29 | - 此动作将复制返回的值,这就是您的用户 ID(你可以粘贴到文本文件中以备将来使用)。 30 | 31 | ## 准备代理 32 | 33 | 您可以从 [ProxyCheap](https://app.proxy-cheap.com/r/ksvW8Z) 或任何其他代理提供商购买代理(不为任何代理提供商背书, 请自行选择)。 34 | 35 | ## 使用 Docker 运行机器人 36 | 37 | 1. 创建一个名为 `proxies.txt` 的文本文件,包含所需的代理 URL。你可以使用以下格式,混用也可以: 38 | 39 | ```plaintext 40 | http://username:password@hostname1:port 41 | http://username:password@hostname2:port 42 | // 或者 43 | socks5://username:password@hostname1:port 44 | socks5://username:password@hostname2:port 45 | ``` 46 | 47 | > 注意:您可以使用 HTTP 或 SOCKS5 代理,并且可以在 `proxies.txt` 文件中配置多个代理(每行一个代理)。 48 | 49 | 1. 使用 Docker 运行 `getgrass-bot`: 50 | 51 | ```bash 52 | docker run -d -v $(pwd)/proxies.txt:/app/proxies.txt -e USER_ID="your-user-id" overtrue/getgrass-bot 53 | ``` 54 | 55 | ## 手动安装 56 | 57 | > 您需要在机器上安装 Node.js 才能手动运行机器人。 58 | 59 | 1. 将此仓库克隆到您的本地机器。 60 | 61 | ```bash 62 | git clone git@github.com:web3bothub/getgrass-bot.git 63 | ``` 64 | 65 | 1. 切换到项目根目录。 66 | 67 | ```bash 68 | cd getgrass-bot 69 | ``` 70 | 71 | 1. 创建 `proxies.txt` 文件,包含所需的代理 URL。确保每个 URL 的格式为: 72 | 73 | ```plaintext 74 | http://username:password@hostname1:port 75 | http://username:password@hostname2:port 76 | // 或者 77 | socks5://username:password@hostname1:port 78 | socks5://username:password@hostname2:port 79 | ``` 80 | 81 | > 注意:您可以使用 HTTP 或 SOCKS5 代理,并且可以在 `proxies.txt` 文件中配置多个代理(每行一个代理)。 82 | 83 | 1. 通过执行以下命令运行 `getgrass-bot`: 84 | 85 | ```bash 86 | USER_ID="your-user-id" node start.js 87 | ``` 88 | 89 | 1. 如果您想在后台运行机器人,可以使用 `pm2` 包: 90 | 91 | ```bash 92 | npm install -g pm2 93 | USER_ID="your-user-id" pm2 start start.js 94 | ``` 95 | 96 | ## 注意 97 | 98 | - 运行此机器人,我不保证您会获得奖励,这取决于 Getgrass 网站。 99 | - 您可以自行承担风险运行此机器人,我不对该机器人造成的任何损失或损害负责。此机器人仅用于教育目的。 100 | - 不要在账号间共享代理,否则您的账号可能会被封禁。 101 | - 如果你有问题,请在 GitHub 上提出问题,不保证回复效率,我要上班,我会尽力回答。 102 | 103 | ## 贡献 104 | 105 | 欢迎通过创建拉取请求为此项目做出贡献。 106 | 107 | ## 支持我 108 | 109 | 如果您想支持我,创建更多优质的脚本,您可以通过以下方式打赏我: 110 | 111 | - TRC20: `TMwJhT5iCsQAfmRRKmAfasAXRaUhPWTSCE` 112 | - ERC20: `0xa2f5b8d9689d20d452c5340745a9a2c0104c40de` 113 | - SOLANA: `HCbbrqD9Xvfqx7nWjNPaejYDtXFp4iY8PT7F4i8PpE5K` 114 | - TON: `UQBD-ms1jA9cmoo8O39BXI6jqh8zwRSoBMUAl4yjEPKD6ata` 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getgrass Bot (2.x client version) 2 | 3 | A bot for [https://app.getgrass.io](https://app.getgrass.io/register/?referralCode=qY96g1DYyIxe3B4),which can automatically claim rewards(2x) for you. 4 | 5 | [中文文档](README_CN.md) | [手摸手教程](https://mirror.xyz/0xe8224b3E9C8d35b34D088BB5A216B733a5A6D9EA/OcnKeYwtHlkv66TGlt0rrhUuI2Pt--B5-UPZt0yqCPM) 6 | 7 | > [!WARNING] 8 | > I am not responsible for any loss or damage caused by this bot. Use it at your own risk. 9 | > 我不对该机器人造成的任何损失或损害负责。使用风险自负。 10 | 11 | ## Usage 12 | 13 | The `getgrass-bot` can be run using Docker or manually. 14 | 15 | ## Get your user ID 16 | 17 | you can obtain your user ID from the Getgrass website: 18 | 19 | - Visit [https://app.getgrass.io/dashboard](https://app.getgrass.io/register/?referralCode=qY96g1DYyIxe3B4). 20 | - Open the browser's developer tools (usually by pressing F12 or right-clicking and selecting "Inspect"). 21 | - Go to the "Console" tab. 22 | - Paste the following command and press Enter: 23 | 24 | ```javascript 25 | const appUserIdKey = Object.keys(localStorage).find(key => key.endsWith('.appUserId')); 26 | if (appUserIdKey) { 27 | const appUserId = localStorage.getItem(appUserIdKey); 28 | console.log("appUserId:", appUserId); 29 | } 30 | ``` 31 | 32 | - Copy the value returned, which is your user ID. 33 | 34 | ## Prepare proxies 35 | 36 | You can buy proxies from [ProxyCheap](https://app.proxy-cheap.com/r/ksvW8Z) or any other proxy provider. 37 | 38 | ## Running the Bot with Docker 39 | 40 | 1. Create a text file named `proxies.txt` with the desired proxy URLs. Ensure each URL is in the format: 41 | 42 | ```plaintext 43 | http://username:password@hostname1:port 44 | http://username:password@hostname2:port 45 | // or 46 | socks5://username:password@hostname1:port 47 | socks5://username:password@hostname2:port 48 | ``` 49 | 50 | > Note: You can use HTTP or SOCKS5 proxies, and you can config with multiple proxies in the `proxies.txt` file (one proxy per line). 51 | 52 | 1. Run the `getgrass-bot` using Docker: 53 | 54 | ```bash 55 | docker run -d -v $(pwd)/proxies.txt:/app/proxies.txt -e USER_ID="your-user-id" overtrue/getgrass-bot 56 | ``` 57 | 58 | ## Manual Installation 59 | 60 | > You need to have Node.js installed on your machine to run the bot manually. 61 | 62 | 1. Git clone this repository to your local machine. 63 | 64 | ```bash 65 | git clone git@github.com:web3bothub/getgrass-bot.git 66 | ``` 67 | 68 | 1. Navigate to the project directory. 69 | 70 | ```bash 71 | cd getgrass-bot 72 | ``` 73 | 74 | 1. Create the `proxies.txt` file with the desired proxy URLs. Ensure each URL is in the format: 75 | 76 | ```plaintext 77 | http://username:password@hostname1:port 78 | http://username:password@hostname2:port 79 | // or 80 | socks5://username:password@hostname1:port 81 | socks5://username:password@hostname2:port 82 | ``` 83 | 84 | > Note: You can use HTTP or SOCKS5 proxies, You can config with multiple proxies in the `proxies.txt` file (one proxy per line). 85 | 86 | 1. Run the `getgrass-bot` by executing the following command: 87 | 88 | ```bash 89 | USER_ID="your-user-id" node start.js 90 | ``` 91 | 92 | 1. If you want to run the bot in the background, you can use the `pm2` package: 93 | 94 | ```bash 95 | npm install -g pm2 96 | USER_ID="your-user-id" pm2 start start.js 97 | ``` 98 | 99 | ## Note 100 | 101 | - Run this bot, I don't guarantee you will get the reward, it depends on the Getgrass website. 102 | - You can just run this bot at your own risk, I'm not responsible for any loss or damage caused by this bot. This bot is for educational purposes only. 103 | 104 | ## Contribution 105 | 106 | Feel free to contribute to this project by creating a pull request. 107 | 108 | ## Support Me 109 | 110 | if you want to support me, you can donate to my address: 111 | 112 | - TRC20: `TMwJhT5iCsQAfmRRKmAfasAXRaUhPWTSCE` 113 | - ERC20: `0xa2f5b8d9689d20d452c5340745a9a2c0104c40de` 114 | - SOLANA: `HCbbrqD9Xvfqx7nWjNPaejYDtXFp4iY8PT7F4i8PpE5K` 115 | - TON: `UQBD-ms1jA9cmoo8O39BXI6jqh8zwRSoBMUAl4yjEPKD6ata` 116 | -------------------------------------------------------------------------------- /ssl/websocket.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGRzCCBS+gAwIBAgIRALi666m24YIisCs7znptMZ4wDQYJKoZIhvcNAQELBQAw 3 | gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO 4 | BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE 5 | AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD 6 | QTAeFw0yMzA1MDgwMDAwMDBaFw0yNDA1MDgyMzU5NTlaMB0xGzAZBgNVBAMTEnBy 7 | b3h5Lnd5bmQubmV0d29yazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 8 | AMaqW1FJ4psLfOx4PXWVMTb6RzmkKwulyYH6Rzhq3GcDSBgeNfGbeQOiJYk+4NzS 9 | OuZ9HDGCTzO4raS7L59xMyTnUCdL0SDRl4Go8XFa6Ra7/qAFfrkjEwjykpZ3W5vF 10 | RLZS6IQSJcKBAFtaxFehrX4Vvo6vBJs8wnth+Lw4I8bIu5STHTau/ukpyvIA1UFB 11 | 6RS9mHLayyaQlbs9/c1jYm1MtYcsuBXfclcTu6aqNvBkgpsCm14xSMjc806LNiSM 12 | ROrvNzHaRtxMO1nAYT2sfwFCRUjVpsEXhMIPntDVFmFC8CW0f/woH8u8ysVmtAMX 13 | n5CQHsifSdYGI+h1+3j7O4MCAwEAAaOCAw0wggMJMB8GA1UdIwQYMBaAFI2MXsRU 14 | rYrhd+mb+ZsF4bgBjWHhMB0GA1UdDgQWBBQlX7ojhzLBAht8Jky5N7q60v4POTAO 15 | BgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcD 16 | AQYIKwYBBQUHAwIwSQYDVR0gBEIwQDA0BgsrBgEEAbIxAQICBzAlMCMGCCsGAQUF 17 | BwIBFhdodHRwczovL3NlY3RpZ28uY29tL0NQUzAIBgZngQwBAgEwgYQGCCsGAQUF 18 | BwEBBHgwdjBPBggrBgEFBQcwAoZDaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0 19 | aWdvUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEF 20 | BQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wNQYDVR0RBC4wLIIScHJveHku 21 | d3luZC5uZXR3b3JrghZ3d3cucHJveHkud3luZC5uZXR3b3JrMIIBfwYKKwYBBAHW 22 | eQIEAgSCAW8EggFrAWkAdwB2/4g/Crb7lVHCYcz1h7o0tKTNuyncaEIKn+ZnTFo6 23 | dAAAAYf84HTEAAAEAwBIMEYCIQCZYfOS4E9goKjQGBAFjhIlY2ZZttuz106NFdsA 24 | OcssCgIhAKAaJWSG+QlwP1AqiTCacHaRRxdJ7Alk1lQW5B1jJq1hAHYA2ra/az+1 25 | tiKfm8K7XGvocJFxbLtRhIU0vaQ9MEjX+6sAAAGH/OB1IwAABAMARzBFAiEA8WBX 26 | 08FP1n70ovE6K3QsgTnSAjqF25JVouvkKBZ/mcECIE+ySLKWGzvL77oSo8AFUkOw 27 | dnPTpEKbNxslhvVwgL9QAHYA7s3QZNXbGs7FXLedtM0TojKHRny87N7DUUhZRnEf 28 | tZsAAAGH/OB09AAABAMARzBFAiBbENoESzTsH3DfQq/RgKIYRlfhiXpL6qXeH5zX 29 | ZFoSvQIhAIzFFu1FadAhIKRqudQ9GlOhitBRqrFeXVqJVoaroLHUMA0GCSqGSIb3 30 | DQEBCwUAA4IBAQA+yG4u30dGwrahm+L31qvJBJN8wTB6JRQQP9ob4zwW89lWhHRS 31 | P+dHdST3iNdB1eLsQ/ujiEv6CoLjXr1R/fE8i82oBi58Np0B+zFIyyS/9r3SisjO 32 | w5Z8pzEGaZqbEPUmR58IyOyoozkPBlICu15hCPJjCxRHyLQR/AMXNuLGPIuwanhF 33 | o9N0sG/pM2tWT7CsUONGImaDoYaxeQYcPuUBYG9jDvyqFCNMTpiF4IdTwr0RNkQ+ 34 | SgOAmGzdhUjvVA13DUFAnnmlizavbWGXrWAs0kQTQINknzozHm/6HRsXhITvuZDS 35 | nkCNIcZdpSRuVNcEpHqjM5U6j13snNPq1lFS 36 | -----END CERTIFICATE----- 37 | -----BEGIN CERTIFICATE----- 38 | MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB 39 | iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl 40 | cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV 41 | BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx 42 | MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV 43 | BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE 44 | ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g 45 | VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC 46 | AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N 47 | TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj 48 | eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E 49 | oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk 50 | Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY 51 | uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j 52 | BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb 53 | +ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G 54 | A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw 55 | CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0 56 | LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr 57 | BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv 58 | bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov 59 | L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H 60 | ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH 61 | 7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi 62 | H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx 63 | RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv 64 | xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38 65 | sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL 66 | l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq 67 | 6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY 68 | LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5 69 | yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K 70 | 00u/I5sUKUErmgQfky3xxzlIPK1aEn8= 71 | -----END CERTIFICATE----- 72 | -----BEGIN CERTIFICATE----- 73 | MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB 74 | iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl 75 | cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV 76 | BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw 77 | MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV 78 | BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU 79 | aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy 80 | dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 81 | AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B 82 | 3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY 83 | tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ 84 | Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 85 | VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT 86 | 79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 87 | c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT 88 | Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l 89 | c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee 90 | UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE 91 | Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd 92 | BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G 93 | A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF 94 | Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO 95 | VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 96 | ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs 97 | 8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR 98 | iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze 99 | Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ 100 | XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ 101 | qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB 102 | VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB 103 | L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG 104 | jjxDah2nGN59PRbxYvnKkKj9 105 | -----END CERTIFICATE----- 106 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import chalk from 'chalk' 3 | import consoleStamp from 'console-stamp' 4 | import fs from 'fs/promises' 5 | import ora from 'ora' 6 | import { v4 as uuidV4 } from 'uuid' 7 | import WebSocket from 'ws' 8 | import { getIpAddress, getProxyAgent, getRandomInt } from './utils.js' 9 | 10 | consoleStamp(console, { 11 | format: ':date(yyyy/mm/dd HH:MM:ss.l)' 12 | }) 13 | process.setMaxListeners(0) 14 | 15 | const getUnixTimestamp = () => Math.floor(Date.now() / 1000) 16 | 17 | const PING_INTERVAL = 5 * 1000 18 | const CHECKIN_INTERVAL = 2 * 60 * 1000 // 2 minutes 19 | const DIRECTOR_SERVER = "https://director.getgrass.io" 20 | const DEVICE_FILE = "devices.json" 21 | 22 | // 错误模式,用于识别需要禁用代理的情况 23 | const ERROR_PATTERNS = [ 24 | "Host unreachable", 25 | "[SSL: WRONG_VERSION_NUMBER]", 26 | "invalid length of packed IP address string", 27 | "Empty connect reply", 28 | "Device creation limit exceeded", 29 | "sent 1011 (internal error) keepalive ping timeout" 30 | ] 31 | 32 | class App { 33 | constructor(user, proxy, deviceId = null, version = '5.2.0') { 34 | this.proxy = proxy 35 | this.userId = user.id 36 | this.version = version 37 | this.browserId = deviceId || uuidV4() 38 | this.websocket = null 39 | 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' 40 | this.pingInterval = null 41 | this.checkinInterval = null 42 | this.spinner = null 43 | } 44 | 45 | /** 46 | * 从director服务器获取WebSocket端点和token 47 | */ 48 | async getWsEndpoints() { 49 | console.info(`[checkin] Getting WebSocket endpoints from director...`) 50 | try { 51 | const response = await axios.post(`${DIRECTOR_SERVER}/checkin`, { 52 | browserId: this.browserId, 53 | userId: this.userId, 54 | version: this.version, 55 | // extensionId: "lkbnfiajjmbhnfledhphioinpickokdi", 56 | userAgent: this.userAgent, 57 | deviceType: "desktop" 58 | }) 59 | 60 | if (response.status === 201) { 61 | const destinations = response.data.destinations || [] 62 | const token = response.data.token || "" 63 | const websocketUrls = destinations.map(dest => `wss://${dest}?token=${token}`) 64 | console.info(`[checkin] Received WebSocket endpoints: ${chalk.blue(websocketUrls)}`) 65 | return { websocketUrls, token } 66 | } else { 67 | console.error(`[checkin] Failed with status: ${response.status}`) 68 | return { websocketUrls: [], token: "" } 69 | } 70 | } catch (error) { 71 | console.error(`[checkin] Error: ${chalk.red(error.message)}`) 72 | return { websocketUrls: [], token: "" } 73 | } 74 | } 75 | 76 | async start() { 77 | if (this.proxy) { 78 | console.info(`Request with proxy: ${chalk.blue(this.proxy)}...`) 79 | } 80 | 81 | // 获取代理的IP地址 82 | console.info(`Getting IP address...`, this.proxy) 83 | try { 84 | const ipAddress = await getIpAddress(this.proxy) 85 | console.info(`IP address: ${chalk.blue(ipAddress)}`) 86 | 87 | if (this.proxy && !ipAddress.includes(new URL(this.proxy).hostname)) { 88 | console.error(`[Warning] Proxy IP address does not match! maybe the proxy is not working...`) 89 | // return false 90 | } 91 | } catch (e) { 92 | console.error(`[ERROR] Could not get IP address! ${chalk.red(e)}`) 93 | if (this.isErrorCritical(e.message)) { 94 | console.error(`[ERROR] Critical proxy error.`) 95 | return false 96 | } 97 | } 98 | 99 | // 从director获取WebSocket端点 100 | const { websocketUrls, token } = await this.getWsEndpoints() 101 | if (websocketUrls.length === 0) { 102 | console.error(`[ERROR] No WebSocket endpoints available`) 103 | return false 104 | } 105 | 106 | // 随机选择一个WebSocket端点 107 | const websocketUrl = websocketUrls[getRandomInt(0, websocketUrls.length - 1)] 108 | 109 | const isWindows = this.userAgent.includes('Windows') || this.userAgent.includes('Win64') || this.userAgent.includes('Win32') 110 | 111 | let options = { 112 | headers: { 113 | "Pragma": "no-cache", 114 | "User-Agent": this.userAgent, 115 | OS: 'Mac', 116 | Browser: 'Mozilla', 117 | Platform: 'Desktop', 118 | "Origin": "chrome-extension://lkbnfiajjmbhnfledhphioinpickokdi", 119 | "Sec-WebSocket-Version": "13", 120 | 'Accept-Language': 'uk-UA,uk;q=0.9,en-US;q=0.8,en;q=0.7', 121 | "Cache-Control": "no-cache", 122 | "priority": "u=1, i", 123 | }, 124 | handshakeTimeout: 30000, 125 | rejectUnauthorized: false, 126 | } 127 | 128 | if (this.proxy) { 129 | console.log(`Configuring websocket proxy agent...(${this.proxy})`) 130 | options.agent = await getProxyAgent(this.proxy) 131 | console.log('Websocket proxy agent configured.') 132 | } 133 | 134 | this.websocket = new WebSocket(websocketUrl, options) 135 | 136 | this.websocket.on('open', async function () { 137 | console.log(`[wss] Websocket connected to ${chalk.green(websocketUrl)}!`) 138 | this.startPing() 139 | this.startCheckinInterval() 140 | }.bind(this)) 141 | 142 | this.websocket.on('message', async function (data) { 143 | let message = data.toString() 144 | 145 | let parsedMessage 146 | try { 147 | parsedMessage = JSON.parse(message) 148 | } catch (e) { 149 | console.error(`[wss] Could not parse WebSocket message! ${chalk.red(message)}`) 150 | console.error(`[wss] ${chalk.red(e)}`) 151 | return 152 | } 153 | 154 | switch (parsedMessage.action) { 155 | case 'AUTH': 156 | const authResponse = JSON.stringify({ 157 | id: parsedMessage.id, 158 | origin_action: parsedMessage.action, 159 | result: { 160 | browser_id: this.browserId, 161 | user_id: this.userId, 162 | user_agent: this.userAgent, 163 | timestamp: getUnixTimestamp(), 164 | device_type: "desktop", 165 | version: this.version, 166 | } 167 | }) 168 | this.sendMessage(authResponse) 169 | console.log(`[wss] (AUTH) -->: ${chalk.green(authResponse)}`) 170 | break 171 | 172 | case 'PONG': 173 | console.log(`[wss] <--: ${chalk.green(message)}`) 174 | break 175 | 176 | case 'HTTP_REQUEST': 177 | await this.handleHttpRequest(parsedMessage) 178 | break 179 | 180 | default: 181 | console.error(`[wss] No handler for message: ${chalk.blue(message)}`) 182 | console.error(`[wss] No handler for action ${chalk.red(parsedMessage.action)}!`) 183 | break 184 | } 185 | }.bind(this)) 186 | 187 | this.websocket.on('close', async function (code) { 188 | console.log(`[wss] Connection closed: ${chalk.red(code)}`) 189 | this.clearIntervals() 190 | 191 | setTimeout(() => { 192 | this.start() 193 | }, PING_INTERVAL) 194 | }.bind(this)) 195 | 196 | this.websocket.on('error', function (error) { 197 | console.error(`[wss] ${chalk.red(error.message)}`) 198 | 199 | this.websocket.terminate() 200 | this.clearIntervals() 201 | 202 | setTimeout(() => { 203 | this.start() 204 | }, PING_INTERVAL) 205 | }.bind(this)) 206 | 207 | return true 208 | } 209 | 210 | isErrorCritical(errorMessage) { 211 | return ERROR_PATTERNS.some(pattern => errorMessage.includes(pattern)) || 212 | errorMessage.includes('Rate limited') 213 | } 214 | 215 | clearIntervals() { 216 | if (this.pingInterval) { 217 | clearInterval(this.pingInterval) 218 | this.pingInterval = null 219 | } 220 | 221 | if (this.checkinInterval) { 222 | clearInterval(this.checkinInterval) 223 | this.checkinInterval = null 224 | } 225 | } 226 | 227 | startPing() { 228 | // 清除之前的interval 229 | if (this.pingInterval) { 230 | clearInterval(this.pingInterval) 231 | } 232 | 233 | this.pingInterval = setInterval(() => { 234 | const message = JSON.stringify({ 235 | id: uuidV4(), 236 | version: '1.0.0', 237 | action: 'PING', 238 | data: {}, 239 | }) 240 | this.sendMessage(message) 241 | }, PING_INTERVAL) 242 | } 243 | 244 | startCheckinInterval() { 245 | // 清除之前的interval 246 | if (this.checkinInterval) { 247 | clearInterval(this.checkinInterval) 248 | } 249 | 250 | this.checkinInterval = setInterval(async () => { 251 | console.log(`[checkin] Performing periodic checkin...`) 252 | await this.getWsEndpoints() 253 | }, CHECKIN_INTERVAL) 254 | } 255 | 256 | async handleHttpRequest(message) { 257 | try { 258 | const data = message.data || {} 259 | const method = (data.method || 'GET').toUpperCase() 260 | const url = data.url 261 | const headers = data.headers || {} 262 | const body = data.body 263 | 264 | console.log(`[http] Handling HTTP request: ${method} ${url}`) 265 | 266 | const response = await axios({ 267 | method, 268 | url, 269 | headers, 270 | data: body, 271 | responseType: 'arraybuffer', 272 | validateStatus: () => true, // 接受所有状态码 273 | }) 274 | 275 | // 将响应体转换为base64 276 | const bodyBase64 = Buffer.from(response.data).toString('base64') 277 | 278 | const result = { 279 | url, 280 | status: response.status, 281 | status_text: '', 282 | headers: response.headers, 283 | body: bodyBase64 284 | } 285 | 286 | const reply = { 287 | id: message.id, 288 | origin_action: 'HTTP_REQUEST', 289 | result 290 | } 291 | 292 | this.sendMessage(JSON.stringify(reply)) 293 | console.log(`[http] HTTP response sent for ${method} ${url}, status: ${response.status}`) 294 | 295 | if (response.status === 429) { 296 | console.error(`[http] Rate limited! Status 429 returned.`) 297 | throw new Error('Rate limited') 298 | } 299 | 300 | } catch (error) { 301 | console.error(`[http] Error handling HTTP request: ${chalk.red(error.message)}`) 302 | throw error 303 | } 304 | } 305 | 306 | async sendMessage(message) { 307 | if (this.websocket.readyState !== WebSocket.OPEN) { 308 | console.error(`[wss] WebSocket is not open!`) 309 | return 310 | } 311 | 312 | this.websocket.send(message) 313 | console.log(`[wss] -->: ${chalk.green(typeof message === 'string' ? message : JSON.stringify(message))}`) 314 | } 315 | } 316 | 317 | /** 318 | * 加载设备ID映射,每个代理对应一个设备ID 319 | */ 320 | async function loadDeviceMapping() { 321 | try { 322 | const data = await fs.readFile(DEVICE_FILE, 'utf8') 323 | const mappings = JSON.parse(data) 324 | return mappings || {} 325 | } catch (error) { 326 | console.log(`No device mappings found, will create a new one.`) 327 | return {} 328 | } 329 | } 330 | 331 | /** 332 | * 保存设备ID映射 333 | */ 334 | async function saveDeviceMapping(deviceMapping) { 335 | try { 336 | await fs.writeFile(DEVICE_FILE, JSON.stringify(deviceMapping, null, 2)) 337 | console.log(`Device mappings saved successfully.`) 338 | } catch (error) { 339 | console.error(`Error saving device mappings: ${error.message}`) 340 | } 341 | } 342 | 343 | /** 344 | * 为代理获取或创建设备ID 345 | */ 346 | async function getDeviceIdForProxy(proxy) { 347 | const deviceMapping = await loadDeviceMapping() 348 | 349 | if (deviceMapping[proxy]) { 350 | console.log(`Using existing device ID for proxy ${proxy}: ${deviceMapping[proxy]}`) 351 | return deviceMapping[proxy] 352 | } 353 | 354 | // 如果该代理没有对应的设备ID,创建一个新的 355 | const newDeviceId = uuidV4() 356 | deviceMapping[proxy] = newDeviceId 357 | await saveDeviceMapping(deviceMapping) 358 | 359 | console.log(`Created new device ID for proxy ${proxy}: ${newDeviceId}`) 360 | return newDeviceId 361 | } 362 | 363 | export async function run(user, proxy = null) { 364 | let deviceId = null 365 | 366 | // 如果提供了代理,为该代理获取或创建专用设备ID 367 | if (proxy) { 368 | deviceId = await getDeviceIdForProxy(proxy) 369 | } else { 370 | // 没有代理的情况下,创建一个通用设备ID 371 | deviceId = uuidV4() 372 | } 373 | 374 | const app = new App(user, proxy, deviceId) 375 | 376 | const spinner = ora({ text: 'Loading…' }).start() 377 | let prefixText = `[user:${chalk.green(user.id.substring(0, 12))}][device:${chalk.green(deviceId.substring(0, 8))}]` 378 | 379 | if (proxy) { 380 | try { 381 | const [ip, port] = new URL(proxy).host.split(':') 382 | prefixText += `[proxy:${chalk.green(ip)}:${chalk.green(port)}]` 383 | } catch (e) { 384 | prefixText += `[proxy:${chalk.green(proxy)}]` 385 | } 386 | } 387 | 388 | spinner.prefixText = prefixText 389 | spinner.succeed(`Started!`) 390 | app.spinner = spinner 391 | 392 | try { 393 | const success = await app.start() 394 | if (!success) { 395 | console.error(`Failed to start.`) 396 | return false 397 | } 398 | } catch (e) { 399 | console.error(e) 400 | return false 401 | } 402 | 403 | return true 404 | } 405 | 406 | // 为了确保程序能够干净地退出 407 | process.on('SIGINT', function () { 408 | console.log('Caught interrupt signal') 409 | process.exit() 410 | }) 411 | --------------------------------------------------------------------------------