├── .github ├── FUNDING.yml └── workflows │ ├── check_sb_cron.yaml~ │ ├── koyeb-alive.yml │ └── webfreecloud-alive.yml ├── LICENSE ├── README.md ├── cf-sb00-alive ├── README.md ├── _worker.js └── serv-account-alive.js ├── koyeb-alive ├── koyeb-alive.py └── worker.js ├── paas-alive ├── README.md └── worker.js ├── vps_sb00_alive ├── README.md ├── sb00-sk5.sh └── sb00_alive.sh ├── vps_sb5in1.sh └── webfreecloud ├── README.md └── login.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: https://blog.24811213.xyz/pages/thanks/ 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/check_sb_cron.yaml~: -------------------------------------------------------------------------------- 1 | name: check_sb_cron 2 | 3 | on: 4 | workflow_dispatch: # 手动触发工作流 5 | schedule: 6 | - cron: "0 */4 * * *" 7 | 8 | jobs: 9 | execute-commands: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up SSHPass 13 | run: sudo apt-get update && sudo apt-get install -y sshpass 14 | 15 | - name: Get ACCOUNTS_JSON 16 | id: get-accounts 17 | run: | 18 | echo "$ACCOUNTS_JSON" > accounts.json 19 | env: 20 | ACCOUNTS_JSON: ${{ secrets.ACCOUNTS_JSON }} 21 | # 从 GitHub Secrets 获取 ACCOUNTS_JSON 变量,并保存到文件 accounts.json 22 | 23 | - name: Generate SSH Commands 24 | id: generate-ssh-commands 25 | run: | 26 | echo "#!/bin/bash" > sshpass.sh 27 | while IFS= read -r account; do 28 | username=$(echo "$account" | jq -r '.username') 29 | password=$(echo "$account" | jq -r '.password') 30 | ssh=$(echo "$account" | jq -r '.ssh') 31 | echo "echo \"Executing for $username@$ssh\"" >> sshpass.sh 32 | echo "sshpass -p '$password' ssh -o StrictHostKeyChecking=no '$username@$ssh' 'bash <(curl -s https://raw.githubusercontent.com/yutian81/serv00-ct8-ssh/main/check_sb_cron.sh)'" >> sshpass.sh 33 | done < <(jq -c '.[]' accounts.json) 34 | chmod +x sshpass.sh 35 | 36 | - name: check_sb_cron 37 | run: ./sshpass.sh 38 | -------------------------------------------------------------------------------- /.github/workflows/koyeb-alive.yml: -------------------------------------------------------------------------------- 1 | name: Koyeb登录保活 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0' # 每周日执行一次 6 | workflow_dispatch: 7 | 8 | jobs: 9 | login: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | - name: Set up Python 15 | uses: actions/setup-python@v3 16 | with: 17 | python-version: '3.x' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install requests 22 | - name: Run Koyeb alive script 23 | env: 24 | KOYEB_ACCOUNTS: ${{ secrets.KOYEB_ACCOUNTS }} 25 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} 26 | TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} 27 | run: python koyeb-alive/koyeb-alive.py 28 | -------------------------------------------------------------------------------- /.github/workflows/webfreecloud-alive.yml: -------------------------------------------------------------------------------- 1 | name: WebFreeCloud自动登录脚本 2 | 3 | on: 4 | schedule: 5 | - cron: '0 6 * * 1' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | auth-check: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 10 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | # 新增步骤:设置 SOCKS5 代理 18 | - name: 配置 SOCKS5 代理 19 | env: 20 | SOCKS5_HOST: ${{ secrets.SOCKS5_HOST }} # 代理服务器地址 21 | SOCKS5_PORT: ${{ secrets.SOCKS5_PORT }} # 代理端口 22 | SOCKS5_USER: ${{ secrets.SOCKS5_USER }} # 代理用户名(如果有) 23 | SOCKS5_PASS: ${{ secrets.SOCKS5_PASS }} # 代理密码(如果有) 24 | run: | 25 | # 构造 SOCKS5 代理URL 26 | if [[ -n "$SOCKS5_USER" && -n "$SOCKS5_PASS" ]]; then 27 | echo "SOCKS_PROXY=socks5://$SOCKS5_USER:$SOCKS5_PASS@$SOCKS5_HOST:$SOCKS5_PORT" >> $GITHUB_ENV 28 | else 29 | echo "SOCKS_PROXY=socks5://$SOCKS5_HOST:$SOCKS5_PORT" >> $GITHUB_ENV 30 | fi 31 | 32 | - name: Set up Python 33 | uses: actions/setup-python@v4 34 | with: 35 | python-version: '3.10' 36 | 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install requests beautifulsoup4 requests[socks] # 增加socks支持 41 | 42 | # 可选:代理连通性测试 43 | - name: 测试代理连通性 44 | run: | 45 | curl --socks5 ${{ env.SOCKS_PROXY }} https://httpbin.org/ip 46 | 47 | - name: Run auth check 48 | env: 49 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} 50 | TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} 51 | USER_CONFIGS_JSON: ${{ secrets.USER_CONFIGS_JSON }} 52 | SOCKS_PROXY: ${{ env.SOCKS_PROXY }} # 传递代理变量 53 | run: | 54 | python ./webfreecloud/login.py 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CMLiussss 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目说明 2 | 各种保活项目 3 | 4 | ## Serv00保活直接用老王的项目 5 | - 老王[仓库地址](https://github.com/eooce/Sing-box) 6 | > 特点:全自动保活 7 | 8 | ---- 9 | 10 | ## VPS版一键无交互脚本 5in1 11 | vless+reality|vmess+argo|hy2|tuic|socks5 12 | ``` 13 | PORT=34766 bash <(curl -Ls https://raw.githubusercontent.com/yutian81/Keepalive/main/vps_sb5in1.sh) 14 | ``` 15 | 16 | ## 测试socks5是否通畅 17 | 运行以下命令,若正确返回服务器ip则节点通畅 18 | ``` 19 | curl ip.sb --socks5 用户名:密码@localhost:端口 20 | ``` 21 | -------------------------------------------------------------------------------- /cf-sb00-alive/README.md: -------------------------------------------------------------------------------- 1 | # cf worker 保活 serv00 节点 2 | 3 | ## 文件 4 | 本文件夹内 _worker.js 5 | 6 | ## 教程 7 | 原作者: 天诚 8 | https://linux.do/t/topic/181957 9 | 10 | ---- 11 | 12 | # cf worker 保 serv 账号,不保活 13 | 按照serv的封禁趋势,禁止搭建代理是迟早的事,且serv本身正确的用途是建站而不是代理,因此我重置了自己所有的serv服务器,用来搭建各种服务和数据库 14 | 15 | 但是serv有登录要求,90天不登录可能会被封号,因此诞生了这个项目,参考了天诚的保活项目,也就是文件夹内的_worker.js 16 | 17 | ## 文件 18 | 文件夹内 serv-account-alive.js 19 | 20 | ## 特点 21 | - 有前端可视化面板,可手动执行 22 | - 前端面板会自动记录上一次执行的时间 23 | - 支持自动化运行,需要设置corn触发器 24 | - 支持tg消息推送 25 | 26 | ## 教程 27 | ### 步骤一 28 | 在cf新建一个worker,将代码复制到其中,部署 29 | 30 | ### 添加环境变量 31 | - ACCOUNTS_URL = 存储你serv登录信息的直链(必填),格式模板: 32 | ``` 33 | { 34 | "accounts": [ 35 | { 36 | "username": "用户名1", 37 | "password": "密码1", 38 | "panelnum": "3", 39 | "type": "serv00" 40 | }, 41 | { 42 | "username": "用户名2", 43 | "password": "密码2", 44 | "panelnum": "8", 45 | "type": "serv00" 46 | }, 47 | { 48 | "username": "用户名3", 49 | "password": "密码3", 50 | "panelnum": "", 51 | "type": "ct8" 52 | } 53 | ] 54 | } 55 | ``` 56 | - PASSWORD = 前端网页访问密码(必填) 57 | - TG_ID = 你的tg机器人的chat id(可选) 58 | - TG_TOKEN = 你的tg机器人的token(可选) 59 | 60 | ### 绑定kv 61 | - 新建一个kv存储空间,命名为 SERV_LOGIN 62 | - 在worker中绑定这个kv,kv变量名SERV_LOGIN 63 | 64 | ### 设置corn触发器 65 | 建议设置为每月运行一次 66 | -------------------------------------------------------------------------------- /cf-sb00-alive/_worker.js: -------------------------------------------------------------------------------- 1 | addEventListener('fetch', event => { 2 | event.respondWith(handleRequest(event.request)) 3 | }) 4 | 5 | addEventListener('scheduled', event => { 6 | event.waitUntil(handleScheduled(event.scheduledTime)) 7 | }) 8 | 9 | async function handleRequest(request) { 10 | const url = new URL(request.url) 11 | 12 | if (url.pathname === '/login' && request.method === 'POST') { 13 | const formData = await request.formData() 14 | const password = formData.get('password') 15 | 16 | if (password === PASSWORD) { 17 | const response = new Response(JSON.stringify({ success: true }), { 18 | headers: { 'Content-Type': 'application/json' } 19 | }) 20 | response.headers.set('Set-Cookie', `auth=${PASSWORD}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`) 21 | return response 22 | } else { 23 | return new Response(JSON.stringify({ success: false }), { 24 | headers: { 'Content-Type': 'application/json' } 25 | }) 26 | } 27 | } else if (url.pathname === '/run' && request.method === 'POST') { 28 | if (!isAuthenticated(request)) { 29 | return new Response('Unauthorized', { status: 401 }) 30 | } 31 | 32 | await handleScheduled(new Date().toISOString()) 33 | const results = await CRON_RESULTS.get('lastResults', 'json') 34 | return new Response(JSON.stringify(results), { 35 | headers: { 'Content-Type': 'application/json' } 36 | }) 37 | } else if (url.pathname === '/results' && request.method === 'GET') { 38 | if (!isAuthenticated(request)) { 39 | return new Response(JSON.stringify({ authenticated: false }), { 40 | headers: { 'Content-Type': 'application/json' } 41 | }) 42 | } 43 | const results = await CRON_RESULTS.get('lastResults', 'json') 44 | return new Response(JSON.stringify({ authenticated: true, results: results || [] }), { 45 | headers: { 'Content-Type': 'application/json' } 46 | }) 47 | } else if (url.pathname === '/check-auth' && request.method === 'GET') { 48 | return new Response(JSON.stringify({ authenticated: isAuthenticated(request) }), { 49 | headers: { 'Content-Type': 'application/json' } 50 | }) 51 | } else { 52 | // 显示登录页面或结果页面的 HTML 53 | return new Response(getHtmlContent(), { 54 | headers: { 'Content-Type': 'text/html' }, 55 | }) 56 | } 57 | } 58 | 59 | function isAuthenticated(request) { 60 | const cookies = request.headers.get('Cookie') 61 | if (cookies) { 62 | const authCookie = cookies.split(';').find(c => c.trim().startsWith('auth=')) 63 | if (authCookie) { 64 | const authValue = authCookie.split('=')[1] 65 | return authValue === PASSWORD 66 | } 67 | } 68 | return false 69 | } 70 | 71 | function getHtmlContent() { 72 | return ` 73 | 74 | 75 | 76 | 77 | 78 | Worker Control Panel 79 | 132 | 133 | 134 |
135 |

Worker Control Panel

136 |
137 | 138 | 139 |
140 |
141 | 142 |
143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
AccountTypeStatusMessageLast Run
155 |
156 |
157 | 260 | 261 | 262 | `; 263 | } 264 | 265 | async function handleScheduled(scheduledTime) { 266 | const accountsData = JSON.parse(ACCOUNTS_JSON); 267 | const accounts = accountsData.accounts; 268 | 269 | let results = []; 270 | for (const account of accounts) { 271 | const result = await loginAccount(account); 272 | results.push(result); 273 | await delay(Math.floor(Math.random() * 8000) + 1000); 274 | } 275 | 276 | // 保存结果到 KV 存储 277 | await CRON_RESULTS.put('lastResults', JSON.stringify(results)); 278 | } 279 | 280 | function generateRandomUserAgent() { 281 | const browsers = ['Chrome', 'Firefox', 'Safari', 'Edge', 'Opera']; 282 | const browser = browsers[Math.floor(Math.random() * browsers.length)]; 283 | const version = Math.floor(Math.random() * 100) + 1; 284 | const os = ['Windows NT 10.0', 'Macintosh', 'X11']; 285 | const selectedOS = os[Math.floor(Math.random() * os.length)]; 286 | const osVersion = selectedOS === 'X11' ? 'Linux x86_64' : selectedOS === 'Macintosh' ? 'Intel Mac OS X 10_15_7' : 'Win64; x64'; 287 | 288 | return `Mozilla/5.0 (${selectedOS}; ${osVersion}) AppleWebKit/537.36 (KHTML, like Gecko) ${browser}/${version}.0.0.0 Safari/537.36`; 289 | } 290 | 291 | async function loginAccount(account) { 292 | const { username, password, panelnum, type, cronCommands } = account 293 | let baseUrl = type === 'ct8' 294 | ? 'https://panel.ct8.pl' 295 | : `https://panel${panelnum}.serv00.com` 296 | let loginUrl = `${baseUrl}/login/?next=/cron/` 297 | 298 | const userAgent = generateRandomUserAgent(); 299 | 300 | try { 301 | const response = await fetch(loginUrl, { 302 | method: 'GET', 303 | headers: { 304 | 'User-Agent': userAgent, 305 | }, 306 | }) 307 | 308 | const pageContent = await response.text() 309 | const csrfMatch = pageContent.match(/name="csrfmiddlewaretoken" value="([^"]*)"/) 310 | const csrfToken = csrfMatch ? csrfMatch[1] : null 311 | 312 | if (!csrfToken) { 313 | throw new Error('CSRF token not found') 314 | } 315 | 316 | const initialCookies = response.headers.get('set-cookie') || '' 317 | 318 | const formData = new URLSearchParams({ 319 | 'username': username, 320 | 'password': password, 321 | 'csrfmiddlewaretoken': csrfToken, 322 | 'next': '/cron/' 323 | }) 324 | 325 | const loginResponse = await fetch(loginUrl, { 326 | method: 'POST', 327 | headers: { 328 | 'Content-Type': 'application/x-www-form-urlencoded', 329 | 'Referer': loginUrl, 330 | 'User-Agent': userAgent, 331 | 'Cookie': initialCookies, 332 | }, 333 | body: formData.toString(), 334 | redirect: 'manual' 335 | }) 336 | 337 | if (loginResponse.status === 302 && loginResponse.headers.get('location') === '/cron/') { 338 | const loginCookies = loginResponse.headers.get('set-cookie') || '' 339 | const allCookies = combineCookies(initialCookies, loginCookies) 340 | 341 | // 访问 cron 列表页面 342 | const cronListUrl = `${baseUrl}/cron/` 343 | const cronListResponse = await fetch(cronListUrl, { 344 | headers: { 345 | 'Cookie': allCookies, 346 | 'User-Agent': userAgent, 347 | } 348 | }) 349 | const cronListContent = await cronListResponse.text() 350 | 351 | console.log(`Cron list URL: ${cronListUrl}`) 352 | console.log(`Cron list response status: ${cronListResponse.status}`) 353 | console.log(`Cron list content (first 1000 chars): ${cronListContent.substring(0, 1000)}`) 354 | 355 | let cronResults = []; 356 | for (const cronCommand of cronCommands) { 357 | if (!cronListContent.includes(cronCommand)) { 358 | // 访问添加 cron 任务页面 359 | const addCronUrl = `${baseUrl}/cron/add` 360 | const addCronPageResponse = await fetch(addCronUrl, { 361 | headers: { 362 | 'Cookie': allCookies, 363 | 'User-Agent': userAgent, 364 | 'Referer': cronListUrl, 365 | } 366 | }) 367 | const addCronPageContent = await addCronPageResponse.text() 368 | 369 | console.log(`Add cron page URL: ${addCronUrl}`) 370 | console.log(`Add cron page response status: ${addCronPageResponse.status}`) 371 | console.log(`Add cron page content (first 1000 chars): ${addCronPageContent.substring(0, 1000)}`) 372 | 373 | const newCsrfMatch = addCronPageContent.match(/name="csrfmiddlewaretoken" value="([^"]*)"/) 374 | const newCsrfToken = newCsrfMatch ? newCsrfMatch[1] : null 375 | 376 | if (!newCsrfToken) { 377 | throw new Error('New CSRF token not found for adding cron task') 378 | } 379 | 380 | const formData = new URLSearchParams({ 381 | 'csrfmiddlewaretoken': newCsrfToken, 382 | 'spec': 'manual', 383 | 'minute_time_interval': 'on', 384 | 'minute': '15', 385 | 'hour_time_interval': 'each', 386 | 'hour': '*', 387 | 'day_time_interval': 'each', 388 | 'day': '*', 389 | 'month_time_interval': 'each', 390 | 'month': '*', 391 | 'dow_time_interval': 'each', 392 | 'dow': '*', 393 | 'command': cronCommand, 394 | 'comment': 'Auto added cron job' 395 | }) 396 | 397 | console.log('Form data being sent:', formData.toString()) 398 | 399 | const { success, response: addCronResponse, content: addCronResponseContent } = await addCronWithRetry(addCronUrl, { 400 | method: 'POST', 401 | headers: { 402 | 'Content-Type': 'application/x-www-form-urlencoded', 403 | 'Cookie': allCookies, 404 | 'User-Agent': userAgent, 405 | 'Referer': addCronUrl, 406 | 'Origin': baseUrl, 407 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 408 | 'Accept-Language': 'en-US,en;q=0.5', 409 | 'Upgrade-Insecure-Requests': '1' 410 | }, 411 | body: formData.toString(), 412 | }) 413 | 414 | console.log('Full response content:', addCronResponseContent) 415 | 416 | if (success) { 417 | if (addCronResponseContent.includes('Cron job has been added') || addCronResponseContent.includes('Zadanie cron zostało dodane')) { 418 | const message = `添加了新的 cron 任务:${cronCommand}`; 419 | console.log(message); 420 | await sendTelegramMessage(`账号 ${username} (${type}) ${message}`); 421 | cronResults.push({ success: true, message }); 422 | } else { 423 | // 如果响应中没有成功信息,再次检查cron列表 424 | const checkCronListResponse = await fetch(cronListUrl, { 425 | headers: { 426 | 'Cookie': allCookies, 427 | 'User-Agent': userAgent, 428 | } 429 | }); 430 | const checkCronListContent = await checkCronListResponse.text(); 431 | 432 | if (checkCronListContent.includes(cronCommand)) { 433 | const message = `确认添加了新的 cron 任务:${cronCommand}`; 434 | console.log(message); 435 | await sendTelegramMessage(`账号 ${username} (${type}) ${message}`); 436 | cronResults.push({ success: true, message }); 437 | } else { 438 | const message = `尝试添加 cron 任务:${cronCommand},但在列表中未找到。可能添加失败。`; 439 | console.error(message); 440 | cronResults.push({ success: false, message }); 441 | } 442 | } 443 | } else { 444 | const message = `添加 cron 任务失败:${cronCommand}`; 445 | console.error(message); 446 | cronResults.push({ success: false, message }); 447 | } 448 | } else { 449 | const message = `cron 任务已存在:${cronCommand}`; 450 | console.log(message); 451 | cronResults.push({ success: true, message }); 452 | } 453 | } 454 | return { username, type, cronResults, lastRun: new Date().toISOString() }; 455 | } else { 456 | const message = `登录失败,未知原因。请检查账号和密码是否正确。`; 457 | console.error(message); 458 | return { username, type, cronResults: [{ success: false, message }], lastRun: new Date().toISOString() }; 459 | } 460 | } catch (error) { 461 | const message = `登录或添加 cron 任务时出现错误: ${error.message}`; 462 | console.error(message); 463 | return { username, type, cronResults: [{ success: false, message }], lastRun: new Date().toISOString() }; 464 | } 465 | } 466 | 467 | async function addCronWithRetry(url, options, maxRetries = 3) { 468 | for (let i = 0; i < maxRetries; i++) { 469 | try { 470 | const response = await fetch(url, options); 471 | const responseContent = await response.text(); 472 | console.log(`Attempt ${i + 1} response status:`, response.status); 473 | console.log(`Attempt ${i + 1} response content (first 1000 chars):`, responseContent.substring(0, 1000)); 474 | 475 | if (response.status === 200 || response.status === 302 || responseContent.includes('Cron job has been added') || responseContent.includes('Zadanie cron zostało dodane')) { 476 | return { success: true, response, content: responseContent }; 477 | } 478 | } catch (error) { 479 | console.error(`Attempt ${i + 1} failed:`, error); 480 | } 481 | await delay(2000); // Wait 2 seconds before retrying 482 | } 483 | return { success: false }; 484 | } 485 | 486 | function combineCookies(cookies1, cookies2) { 487 | const cookieMap = new Map() 488 | 489 | const parseCookies = (cookieString) => { 490 | cookieString.split(',').forEach(cookie => { 491 | const [fullCookie] = cookie.trim().split(';') 492 | const [name, value] = fullCookie.split('=') 493 | if (name && value) { 494 | cookieMap.set(name.trim(), value.trim()) 495 | } 496 | }) 497 | } 498 | 499 | parseCookies(cookies1) 500 | parseCookies(cookies2) 501 | 502 | return Array.from(cookieMap.entries()).map(([name, value]) => `${name}=${value}`).join('; ') 503 | } 504 | 505 | async function sendTelegramMessage(message) { 506 | const telegramConfig = JSON.parse(TELEGRAM_JSON) 507 | const { telegramBotToken, telegramBotUserId } = telegramConfig 508 | const url = `https://api.telegram.org/bot${telegramBotToken}/sendMessage` 509 | 510 | try { 511 | await fetch(url, { 512 | method: 'POST', 513 | headers: { 'Content-Type': 'application/json' }, 514 | body: JSON.stringify({ 515 | chat_id: telegramBotUserId, 516 | text: message 517 | }) 518 | }) 519 | } catch (error) { 520 | console.error('Error sending Telegram message:', error) 521 | } 522 | } 523 | 524 | function delay(ms) { 525 | return new Promise(resolve => setTimeout(resolve, ms)) 526 | } 527 | -------------------------------------------------------------------------------- /cf-sb00-alive/serv-account-alive.js: -------------------------------------------------------------------------------- 1 | // 配置常量 2 | const CONFIG = { 3 | RETRY_ATTEMPTS: 3, // 重试次数 4 | RETRY_DELAY: { MIN: 1000, MAX: 9000 }, // 延迟时间(单位:毫秒) 5 | RATE_LIMIT: { MAX: 100, WINDOW: 3600000 }, // 限流:每小时最多100请求 6 | COOKIE_MAX_AGE: 86400 // Cookie 过期时间(24小时,单位:秒) 7 | }; 8 | 9 | // 延迟函数 10 | function delay(ms) { 11 | return new Promise(resolve => setTimeout(resolve, ms)); 12 | } 13 | 14 | // 创建结果对象 15 | function createResult(username, type, panelnum, success, message, retryCount = 0) { 16 | return { 17 | username, 18 | type, 19 | panelnum, 20 | cronResults: [{ success, message, ...(retryCount ? { retryCount } : {}) }], 21 | lastRun: new Date().toISOString() 22 | }; 23 | } 24 | 25 | // 错误日志记录 26 | async function logError(error, context, env) { 27 | const timestamp = new Date().toISOString(); 28 | const logMessage = `[${timestamp}] ${context}: ${error.message}`; 29 | console.error(logMessage); 30 | await sendTelegramMessage(`错误警告: ${logMessage}`, env); 31 | } 32 | 33 | // 生成随机 User-Agent 34 | function generateRandomUserAgent() { 35 | const browsers = ['Chrome', 'Firefox', 'Safari', 'Edge', 'Opera']; 36 | const browser = browsers[Math.floor(Math.random() * browsers.length)]; 37 | const version = Math.floor(Math.random() * 100) + 1; 38 | const os = ['Windows NT 10.0', 'Macintosh', 'X11']; 39 | const selectedOS = os[Math.floor(Math.random() * os.length)]; 40 | const osVersion = selectedOS === 'X11' ? 'Linux x86_64' : 41 | selectedOS === 'Macintosh' ? 'Intel Mac OS X 10_15_7' : 42 | 'Win64; x64'; 43 | 44 | return `Mozilla/5.0 (${selectedOS}; ${osVersion}) AppleWebKit/537.36 (KHTML, like Gecko) ${browser}/${version}.0.0.0 Safari/537.36`; 45 | } 46 | 47 | // 请求频率限制 48 | const rateLimit = { 49 | requests: new Map(), 50 | checkLimit: function(ip) { 51 | const now = Date.now(); 52 | const userRequests = this.requests.get(ip) || []; 53 | const recentRequests = userRequests.filter(time => now - time < CONFIG.RATE_LIMIT.WINDOW); 54 | this.requests.set(ip, [...recentRequests, now]); 55 | return recentRequests.length >= CONFIG.RATE_LIMIT.MAX; 56 | } 57 | }; 58 | 59 | // User-Agent 缓存 60 | const userAgentCache = { 61 | cache: new Map(), 62 | get: function() { 63 | const now = Math.floor(Date.now() / 3600000); 64 | if (!this.cache.has(now)) { 65 | this.cache.clear(); 66 | this.cache.set(now, generateRandomUserAgent()); 67 | } 68 | return this.cache.get(now); 69 | } 70 | }; 71 | 72 | export default { 73 | // 处理 HTTP 请求 74 | async fetch(request, env, ctx) { 75 | return handleRequest(request, env); 76 | }, 77 | // 处理定时任务 78 | async scheduled(event, env, ctx) { 79 | return handleScheduled(event.scheduledTime, env); 80 | } 81 | }; 82 | 83 | // 处理 HTTP 请求的主函数 84 | async function handleRequest(request, env) { 85 | if (!env.PASSWORD || env.PASSWORD.trim() === "") { 86 | throw new Error("未设置有效的 PASSWORD 环境变量"); 87 | } 88 | 89 | try { 90 | const url = new URL(request.url); 91 | const clientIP = request.headers.get('CF-Connecting-IP'); 92 | 93 | if (rateLimit.checkLimit(clientIP)) { 94 | return new Response('请求过多', { status: 429 }); 95 | } 96 | 97 | switch(url.pathname) { 98 | case '/login': 99 | return handleLogin(request, env); 100 | case '/run': 101 | return handleRun(request, env); 102 | case '/results': 103 | return handleResults(request, env); 104 | case '/check-auth': 105 | return handleCheckAuth(request, env); 106 | default: 107 | return new Response(getHtmlContent(), { 108 | headers: { 'Content-Type': 'text/html' }, 109 | }); 110 | } 111 | } catch (error) { 112 | await logError(error, `请求处理错误 (路径: ${request.url})`, env); 113 | return new Response('服务器内部错误', { status: 500 }); 114 | } 115 | } 116 | 117 | // 添加这个函数 118 | async function handleCheckAuth(request, env) { 119 | return new Response(JSON.stringify({ 120 | authenticated: isAuthenticated(request, env) 121 | }), { 122 | headers: { 'Content-Type': 'application/json' } 123 | }); 124 | } 125 | 126 | // 处理登录请求 127 | async function handleLogin(request, env) { 128 | if (request.method !== 'POST') { 129 | return new Response('不允许的方式', { status: 405 }); 130 | } 131 | 132 | try { 133 | const formData = await request.formData(); 134 | const password = formData.get('password'); 135 | 136 | if (password === env.PASSWORD) { 137 | const response = new Response(JSON.stringify({ success: true }), { 138 | headers: { 'Content-Type': 'application/json' } 139 | }); 140 | response.headers.set('Set-Cookie', 141 | `auth=${env.PASSWORD}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${CONFIG.COOKIE_MAX_AGE}` 142 | ); 143 | return response; 144 | } 145 | 146 | return new Response(JSON.stringify({ success: false }), { 147 | headers: { 'Content-Type': 'application/json' } 148 | }); 149 | } catch (error) { 150 | await logError(error, 'Login Handler', env); 151 | return new Response('服务器内部错误', { status: 500 }); 152 | } 153 | } 154 | 155 | // 处理运行脚本请求 156 | async function handleRun(request, env) { 157 | if (!isAuthenticated(request, env)) { 158 | return new Response('未授权的访问', { status: 401 }); 159 | } 160 | 161 | const encoder = new TextEncoder(); 162 | const stream = new TransformStream(); 163 | const writer = stream.writable.getWriter(); 164 | 165 | // 创建异步执行函数 166 | const executeScript = async () => { 167 | try { 168 | const response = await fetch(env.ACCOUNTS_URL); 169 | const accountsData = await response.json(); 170 | const accounts = accountsData.accounts; 171 | 172 | let results = []; 173 | let successCount = 0; 174 | let failureCount = 0; 175 | 176 | for (let i = 0; i < accounts.length; i++) { 177 | const account = accounts[i]; 178 | // 发送开始处理某个账号的消息 179 | await writer.write(encoder.encode(JSON.stringify({ 180 | type: 'processing', 181 | message: `正在登录服务器: ${account.type}-${account.panelnum} (用户名: ${account.username})...`, 182 | current: i + 1, 183 | total: accounts.length 184 | }) + '\n')); 185 | 186 | const result = await loginWithRetry(account, env); 187 | results.push(result); 188 | 189 | // 更新统计 190 | if (result.cronResults[0].success) { 191 | successCount++; 192 | } else { 193 | failureCount++; 194 | } 195 | 196 | // 发送进度更新 197 | await writer.write(encoder.encode(JSON.stringify({ 198 | type: 'progress', 199 | completed: i + 1, 200 | total: accounts.length, 201 | result: result, 202 | stats: { 203 | success: successCount, 204 | failure: failureCount, 205 | total: accounts.length 206 | } 207 | }) + '\n')); 208 | 209 | await delay( 210 | Math.floor(Math.random() * 211 | (CONFIG.RETRY_DELAY.MAX - CONFIG.RETRY_DELAY.MIN)) + 212 | CONFIG.RETRY_DELAY.MIN 213 | ); 214 | } 215 | 216 | // 发送完成消息 217 | const summary = `总共${accounts.length}个账号,成功${successCount}个,失败${failureCount}个`; 218 | await writer.write(encoder.encode(JSON.stringify({ 219 | type: 'complete', 220 | message: summary, 221 | stats: { 222 | success: successCount, 223 | failure: failureCount, 224 | total: accounts.length 225 | } 226 | }) + '\n')); 227 | 228 | await env.SERV_LOGIN.put('lastResults', JSON.stringify(results)); 229 | // 发送 TG 汇总消息 230 | await sendTelegramMessage(null, env, results); // 传入 results 参数来生成完整报告 231 | } catch (error) { 232 | await writer.write(encoder.encode(JSON.stringify({ 233 | type: 'error', 234 | message: error.message 235 | }) + '\n')); 236 | } finally { 237 | await writer.close(); 238 | } 239 | }; 240 | 241 | // 启动异步执行 242 | executeScript(); 243 | 244 | return new Response(stream.readable, { 245 | headers: { 246 | 'Content-Type': 'text/event-stream', 247 | 'Cache-Control': 'no-cache', 248 | 'Connection': 'keep-alive' 249 | } 250 | }); 251 | } 252 | 253 | // 处理结果请求 254 | async function handleResults(request, env) { 255 | if (!isAuthenticated(request, env)) { 256 | return new Response(JSON.stringify({ authenticated: false }), { 257 | headers: { 'Content-Type': 'application/json' } 258 | }); 259 | } 260 | 261 | try { 262 | const results = await env.SERV_LOGIN.get('lastResults', 'json'); 263 | return new Response(JSON.stringify({ 264 | authenticated: true, 265 | results: results || [] 266 | }), { 267 | headers: { 'Content-Type': 'application/json' } 268 | }); 269 | } catch (error) { 270 | await logError(error, 'Results Handler', env); 271 | return new Response('Internal Server Error', { status: 500 }); 272 | } 273 | } 274 | 275 | // 定时任务处理函数 276 | async function handleScheduled(scheduledTime, env) { 277 | try { 278 | console.log(`定时任务开始执行,计划时间:${new Date(scheduledTime).toISOString()}`); 279 | const response = await fetch(env.ACCOUNTS_URL); 280 | const accountsData = await response.json(); 281 | const accounts = accountsData.accounts; 282 | 283 | let results = []; 284 | for (const account of accounts) { 285 | const result = await loginWithRetry(account, env); // 添加 env 参数 286 | results.push(result); 287 | await delay( 288 | Math.floor(Math.random() * 289 | (CONFIG.RETRY_DELAY.MAX - CONFIG.RETRY_DELAY.MIN)) + 290 | CONFIG.RETRY_DELAY.MIN 291 | ); 292 | } 293 | 294 | await env.SERV_LOGIN.put('lastResults', JSON.stringify(results)); 295 | await sendTelegramMessage(`定时任务完成`, env, results); 296 | } catch (error) { 297 | await logError(error, `定时任务处理程序 (计划时间: ${new Date(scheduledTime).toISOString()})`, env); 298 | } 299 | } 300 | 301 | // 处理认证检查请求 302 | function isAuthenticated(request, env) { 303 | const cookies = request.headers.get('Cookie'); 304 | if (cookies) { 305 | const authCookie = cookies.split(';').find(c => c.trim().startsWith('auth=')); 306 | if (authCookie) { 307 | const authValue = authCookie.split('=')[1]; 308 | return authValue === env.PASSWORD; 309 | } 310 | } 311 | return false; 312 | } 313 | 314 | // 提取 CSRF Token 315 | function extractCsrfToken(pageContent) { 316 | const csrfMatch = pageContent.match(/name="csrfmiddlewaretoken" value="([^"]*)"/) 317 | if (!csrfMatch) { 318 | throw new Error('未找到 CSRF token'); 319 | } 320 | return csrfMatch[1]; 321 | } 322 | 323 | // 处理登录响应 324 | function handleLoginResponse(response, username, type, panelnum, env) { 325 | if (response.status === 302) { 326 | return createResult(username, type, panelnum, true, '登录成功'); 327 | } else { 328 | const message = '登录失败,未知原因。请检查账号和密码是否正确。'; 329 | console.error(message); 330 | return createResult(username, type, panelnum, false, message); 331 | } 332 | } 333 | 334 | // 账号登录检查函数 335 | async function loginAccount(account, env) { 336 | const { username, password, panelnum, type } = account; 337 | const baseUrl = type === 'ct8' 338 | ? 'https://panel.ct8.pl' 339 | : `https://panel${panelnum}.serv00.com`; 340 | const loginUrl = `${baseUrl}/login/`; 341 | const userAgent = userAgentCache.get(); 342 | 343 | try { 344 | const response = await fetch(loginUrl, { 345 | method: 'GET', 346 | headers: { 347 | 'User-Agent': userAgent, 348 | }, 349 | }); 350 | 351 | const pageContent = await response.text(); 352 | const csrfToken = extractCsrfToken(pageContent); 353 | const initialCookies = response.headers.get('set-cookie') || ''; 354 | 355 | const loginResponse = await fetch(loginUrl, { 356 | method: 'POST', 357 | headers: { 358 | 'Content-Type': 'application/x-www-form-urlencoded', 359 | 'Referer': loginUrl, 360 | 'User-Agent': userAgent, 361 | 'Cookie': initialCookies, 362 | }, 363 | body: new URLSearchParams({ 364 | 'username': username, 365 | 'password': password, 366 | 'csrfmiddlewaretoken': csrfToken, 367 | 'next': '/' 368 | }).toString(), 369 | redirect: 'manual' 370 | }); 371 | 372 | return handleLoginResponse(loginResponse, username, type, panelnum, env); 373 | } catch (error) { 374 | await logError(error, `服务器: ${type}-${panelnum}, 用户名: ${username}`, env); 375 | return createResult(username, type, panelnum, false, error.message); 376 | } 377 | } 378 | 379 | // 带重试机制的登录函数 380 | async function loginWithRetry(account, env, attempts = CONFIG.RETRY_ATTEMPTS) { 381 | for (let i = 0; i < attempts; i++) { 382 | try { 383 | const result = await loginAccount(account, env); 384 | if (result.cronResults[0].success) { 385 | return result; 386 | } 387 | } catch (error) { 388 | if (i === attempts - 1) { 389 | throw error; 390 | } 391 | } 392 | await delay(CONFIG.RETRY_DELAY.MIN * (i + 1)); 393 | } 394 | return createResult( 395 | account.username, 396 | account.type, 397 | account.panelnum, 398 | false, 399 | `登录失败,已重试 ${attempts} 次`, 400 | attempts 401 | ); 402 | } 403 | 404 | // 发送 Telegram 通知 405 | async function sendTelegramMessage(message, env, results = null) { 406 | if (!env.TG_ID || !env.TG_TOKEN) { 407 | console.warn("未设置 TG_ID 或 TG_TOKEN,跳过发送 Telegram 消息"); 408 | return; 409 | } 410 | 411 | const url = `https://api.telegram.org/bot${env.TG_TOKEN}/sendMessage`; 412 | let messageText; 413 | 414 | if (!results) { 415 | messageText = message; 416 | } else { 417 | const now = new Date().toLocaleString('zh-CN', { 418 | year: 'numeric', 419 | month: '2-digit', 420 | day: '2-digit', 421 | hour: '2-digit', 422 | minute: '2-digit', 423 | second: '2-digit' 424 | }).replace(/\//g, '-'); 425 | 426 | const successCount = results.filter(r => r.cronResults[0].success).length; 427 | const failureCount = results.length - successCount; 428 | 429 | messageText = [ 430 | `*🤖 Serv00 登录状态报告*`, 431 | `⏰ 时间: \`${now}\``, 432 | `📊 总计: \`${results.length}\` 个账户`, 433 | `✅ 成功: \`${successCount}\` | ❌ 失败: \`${failureCount}\``, 434 | '', 435 | ...results.map(result => { 436 | const success = result.cronResults[0].success; 437 | const serverinfo = result.type === 'ct8' 438 | ? `${result.type}` 439 | : `${result.type}-${result.panelnum}`; 440 | const lines = [ 441 | `*服务器: ${serverinfo}* | 用户名: ${result.username}`, 442 | `状态: ${success ? '✅ 登录成功' : '❌ 登录失败'}` 443 | ]; 444 | 445 | if (!success && result.cronResults[0].message) { 446 | lines.push(`失败原因:\`${result.cronResults[0].message}\``); 447 | } 448 | return lines.join('\n'); 449 | }) 450 | ].join('\n'); 451 | } 452 | 453 | try { 454 | await fetch(url, { 455 | method: 'POST', 456 | headers: { 'Content-Type': 'application/json' }, 457 | body: JSON.stringify({ 458 | chat_id: env.TG_ID, 459 | text: messageText, 460 | parse_mode: 'Markdown' 461 | }) 462 | }); 463 | } catch (error) { 464 | console.error('发送TG消息时发生错误:', error); 465 | } 466 | } 467 | 468 | // 最后一个函数:HTML 内容生成 469 | function getHtmlContent() { 470 | const siteIcon = 'https://pan.811520.xyz/icon/serv00.png'; 471 | const bgimgURL = 'https://bing.img.run/1920x1080.php'; 472 | return ` 473 | 474 | 475 | 476 | 477 | 478 | Serv00账户批量登录 479 | 480 | 584 | 585 | 586 |
587 |

Serv00登录控制面板

588 |
589 | 590 | 591 |
592 |
593 | 594 |
595 |
596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 |
服务器用户名状态消息执行时间
608 |
609 |
610 | 617 | 856 | 857 | 858 | `; 859 | } 860 | -------------------------------------------------------------------------------- /koyeb-alive/koyeb-alive.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import json 4 | import time 5 | 6 | def validate_env_variables(): 7 | """验证必要的环境变量""" 8 | koyeb_accounts_env = os.getenv("KOYEB_ACCOUNTS") 9 | if not koyeb_accounts_env: 10 | raise ValueError("KOYEB_ACCOUNTS 环境变量未设置或格式错误") 11 | try: 12 | return json.loads(koyeb_accounts_env) 13 | except json.JSONDecodeError: 14 | raise ValueError("KOYEB_ACCOUNTS JSON 格式无效") 15 | 16 | def send_tg_message(message): 17 | bot_token = os.getenv("TG_BOT_TOKEN") 18 | chat_id = os.getenv("TG_CHAT_ID") 19 | 20 | if not bot_token or not chat_id: 21 | print("TG_BOT_TOKEN 或 TG_CHAT_ID 未设置,跳过发送 Telegram 消息") 22 | return None 23 | 24 | url = f"https://api.telegram.org/bot{bot_token}/sendMessage" 25 | data = { 26 | "chat_id": chat_id, 27 | "text": message, 28 | "parse_mode": "Markdown" 29 | } 30 | try: 31 | response = requests.post(url, data=data, timeout=30) 32 | response.raise_for_status() 33 | return response.json() 34 | except requests.RequestException as e: 35 | print(f"发送 Telegram 消息失败: {str(e)}") 36 | return None 37 | 38 | def login_koyeb(email, password): 39 | if not email or not password: 40 | return False, "邮箱或密码为空" 41 | 42 | login_url = "https://app.koyeb.com/v1/account/login" 43 | headers = { 44 | "Content-Type": "application/json", 45 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" 46 | } 47 | data = { 48 | "email": email.strip(), # 去除可能的空格 49 | "password": password 50 | } 51 | 52 | try: 53 | response = requests.post(login_url, headers=headers, json=data, timeout=30) 54 | response.raise_for_status() 55 | return True, "登录成功" 56 | except requests.Timeout: 57 | return False, "请求超时" 58 | except requests.RequestException as e: 59 | return False, str(e) 60 | 61 | def main(): 62 | try: 63 | # 验证账户信息并逐个登录 64 | KOYEB_ACCOUNTS = validate_env_variables() 65 | 66 | # 检查账户列表是否为空 67 | if not KOYEB_ACCOUNTS: 68 | raise ValueError("没有找到有效的 Koyeb 账户信息") 69 | 70 | results = [] 71 | current_time = time.strftime("%Y-%m-%d %H:%M:%S") 72 | total_accounts = len(KOYEB_ACCOUNTS) 73 | success_count = 0 74 | 75 | for index, account in enumerate(KOYEB_ACCOUNTS, 1): 76 | email = account.get('email', '').strip() # 去除可能的空格 77 | password = account.get('password', '') 78 | 79 | if not email or not password: 80 | print(f"警告: 账户信息不完整,跳过该账户") 81 | continue 82 | 83 | try: 84 | print(f"正在处理第 {index}/{total_accounts} 个账户: {email}") 85 | time.sleep(8) # 登录8秒间隔 86 | success, message = login_koyeb(email, password) 87 | if success: 88 | status_line = f"状态: ✅ {message}" 89 | success_count += 1 90 | else: 91 | status_line = f"状态: ❌ 登录失败\n原因:{message}" 92 | except Exception as e: 93 | status_line = f"状态: ❌ 登录失败\n原因:执行异常 - {str(e)}" 94 | 95 | results.append(f"账户: {email}\n{status_line}\n") 96 | 97 | # 检查是否有处理结果 98 | if not results: 99 | raise ValueError("没有任何账户处理结果") 100 | 101 | # 生成TG消息内容模板,添加统计信息 102 | summary = f"📊 总计: {total_accounts} 个账户\n✅ 成功{success_count}个 | ❌ 失败{total_accounts - success_count}个\n\n" 103 | tg_message = f"🤖 Koyeb 登录状态报告\n⏰ 检查时间: {current_time}\n\n{summary}" + "\n".join(results) 104 | print(tg_message) 105 | send_tg_message(tg_message) 106 | 107 | except Exception as e: 108 | error_message = f"程序执行出错: {str(e)}" 109 | print(error_message) 110 | send_tg_message(f"❌ {error_message}") 111 | 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /koyeb-alive/worker.js: -------------------------------------------------------------------------------- 1 | async function sendTGMessage(message, env) { 2 | const botToken = env.TG_BOT_TOKEN; 3 | const chatId = env.TG_CHAT_ID; 4 | 5 | if (!botToken || !chatId) { 6 | console.log("TG_BOT_TOKEN 或 TG_CHAT_ID 未设置,跳过发送 Telegram 消息"); 7 | return null; 8 | } 9 | 10 | const url = `https://api.telegram.org/bot${botToken}/sendMessage`; 11 | const data = { 12 | chat_id: chatId, 13 | text: message, 14 | parse_mode: 'Markdown', 15 | }; 16 | 17 | try { 18 | const response = await fetch(url, { 19 | method: 'POST', 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | }, 23 | body: JSON.stringify(data), 24 | }); 25 | if (!response.ok) { 26 | throw new Error(`HTTP error! status: ${response.status}`); 27 | } 28 | return await response.json(); 29 | } catch (e) { 30 | console.log(`发送 Telegram 消息失败: ${e.message}`); 31 | return null; 32 | } 33 | } 34 | 35 | async function loginKoyeb(email, password) { 36 | if (!email || !password) { 37 | return [false, "邮箱或密码为空"]; 38 | } 39 | 40 | const loginUrl = 'https://app.koyeb.com/v1/account/login'; 41 | const headers = { 42 | 'Content-Type': 'application/json', 43 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 44 | }; 45 | const data = { 46 | email: email.trim(), 47 | password: password 48 | }; 49 | 50 | try { 51 | const controller = new AbortController(); 52 | const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时 53 | 54 | const response = await fetch(loginUrl, { 55 | method: 'POST', 56 | headers: headers, 57 | body: JSON.stringify(data), 58 | signal: controller.signal 59 | }); 60 | 61 | clearTimeout(timeoutId); 62 | 63 | if (response.ok) { 64 | return [true, `HTTP状态码 ${response.status}`]; 65 | } 66 | return [false, `HTTP状态码 ${response.status}`]; 67 | } catch (e) { 68 | if (e.name === 'AbortError') { 69 | return [false, "请求超时"]; 70 | } 71 | return [false, e.message]; 72 | } 73 | } 74 | 75 | async function validateEnvVariables(env) { 76 | const koyebAccountsEnv = env.KOYEB_ACCOUNTS; 77 | if (!koyebAccountsEnv) { 78 | throw new Error("KOYEB_ACCOUNTS 环境变量未设置或格式错误"); 79 | } 80 | try { 81 | return JSON.parse(koyebAccountsEnv); 82 | } catch { 83 | throw new Error("KOYEB_ACCOUNTS JSON 格式无效"); 84 | } 85 | } 86 | 87 | async function scheduledEventHandler(event, env) { 88 | try { 89 | const KOYEB_ACCOUNTS = await validateEnvVariables(env); 90 | 91 | if (!KOYEB_ACCOUNTS || KOYEB_ACCOUNTS.length === 0) { 92 | throw new Error("没有找到有效的 Koyeb 账户信息"); 93 | } 94 | 95 | const results = []; 96 | const currentTime = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }); 97 | const totalAccounts = KOYEB_ACCOUNTS.length; 98 | let successCount = 0; 99 | 100 | for (let index = 0; index < totalAccounts; index++) { 101 | const account = KOYEB_ACCOUNTS[index]; 102 | const email = account.email?.trim(); 103 | const password = account.password; 104 | 105 | if (!email || !password) { 106 | console.log("警告: 账户信息不完整,跳过该账户"); 107 | continue; 108 | } 109 | 110 | try { 111 | console.log(`正在处理第 ${index + 1}/${totalAccounts} 个账户: ${email}`); 112 | if (index > 0) { 113 | await new Promise(resolve => setTimeout(resolve, 8000)); // 8秒间隔 114 | } 115 | 116 | const [success, message] = await loginKoyeb(email, password); 117 | if (success) { 118 | successCount++; 119 | results.push(`账户: ${email}\n状态: ✅ 登录成功\n`); 120 | } else { 121 | results.push(`账户: ${email}\n状态: ❌ 登录失败\n消息:${message}\n`); 122 | } 123 | } catch (e) { 124 | results.push(`账户: ${email}\n状态: ❌ 登录失败\n消息:执行异常 - ${e.message}\n`); 125 | } 126 | } 127 | 128 | if (results.length === 0) { 129 | throw new Error("没有任何账户处理结果"); 130 | } 131 | 132 | const summary = `📊 总计: ${totalAccounts} 个账户\n✅ 成功${successCount}个 | ❌ 失败${totalAccounts - successCount}个\n\n`; 133 | const tgMessage = `🤖 Koyeb 登录状态报告\n⏰ 检查时间: ${currentTime}\n\n${summary}${results.join('')}`; 134 | 135 | console.log(tgMessage); 136 | await sendTGMessage(tgMessage, env); 137 | 138 | } catch (e) { 139 | const errorMessage = `程序执行出错: ${e.message}`; 140 | console.error(errorMessage); 141 | await sendTGMessage(`❌ ${errorMessage}`, env); 142 | } 143 | } 144 | 145 | addEventListener('scheduled', event => { 146 | event.waitUntil(scheduledEventHandler(event, event.environment)); 147 | }); 148 | -------------------------------------------------------------------------------- /paas-alive/README.md: -------------------------------------------------------------------------------- 1 | # paas-alive 2 | 3 | 此脚本用于部署到 cf worker,通过设置 worker 的 corn 触发器定时访问指定的地址,包括间断访问和不间断访问两种模式一起运行,以保证容器活跃。 4 | 5 | # 使用说明 6 | 7 | 1. 部署到 cf worker 8 | 9 | 2. 设置环境变量: 10 | 11 | **变量1:**`24_URLS` = 需要24小时不间断访问的地址,`每行填写1个`,如: 12 | ``` 13 | https://www.baidu.com 14 | https://www.yahoo.com 15 | https://github.com 16 | ``` 17 | **变量2:**`NO24_URLS` = 凌晨1点至5点暂停访问,其他时间段不间断访问的地址,`每行填写1个`,格式同上 18 | 19 | 4. 设置 corn 触发器,建议设置为每 2 分钟执行一次 20 | 21 | # 适用平台 22 | * 包括但不限于Glitch,Rendenr,Back4app,clever cloud,Zeabur,codesanbox,replit。。。等等,不支持物理停机的容器。 23 | * 老王部署在 huggingface 上的[保活项目](https://huggingface.co/spaces/rides/keep-alive) 可直接复制他的 space,修改 index.js 中的地址即可。 24 | 25 | # 原作者 26 | [老王](https://github.com/eooce/Auto-keep-online/tree/main) 27 | -------------------------------------------------------------------------------- /paas-alive/worker.js: -------------------------------------------------------------------------------- 1 | import moment from 'https://cdn.jsdelivr.net/npm/moment-timezone@0.5.34/builds/moment-timezone-with-data.min.js'; 2 | 3 | // 从环境变量加载 URLs,每行一个地址 4 | async function handleRequest(event, env) { 5 | // 获取环境变量 6 | const urls = (env['24_URLS'] || '').split('\n').map(url => url.trim()).filter(url => url); // 24小时不间断访问的地址 7 | const websites = (env['NO24_URLS'] || '').split('\n').map(url => url.trim()).filter(url => url); // 01:00至05:00暂停访问的地址 8 | 9 | // 访问网站的函数 10 | async function visitWebsites(websites) { 11 | const currentMoment = moment().tz('Asia/Hong_Kong'); 12 | const formattedTime = currentMoment.format('YYYY-MM-DD HH:mm:ss'); 13 | for (let url of websites) { 14 | try { 15 | const response = await fetch(url); 16 | console.log(`${formattedTime} 访问网站成功: ${url} - Status code: ${response.status}`); 17 | } catch (error) { 18 | console.error(`${formattedTime} 访问网站失败 ${url}: ${error.message}`); 19 | } 20 | } 21 | } 22 | 23 | // 访问24小时不间断的URL数组 24 | async function scrapeAndLog(url) { 25 | try { 26 | const response = await fetch(url); 27 | console.log(`${moment().tz('Asia/Hong_Kong').format('YYYY-MM-DD HH:mm:ss')} 访问网站成功: ${url} - Status code: ${response.status}`); 28 | } catch (error) { 29 | console.error(`${moment().tz('Asia/Hong_Kong').format('YYYY-MM-DD HH:mm:ss')} 访问网站失败: ${url}: ${error.message}`); 30 | } 31 | } 32 | 33 | // 定时器函数,控制在01:00到05:00之间暂停访问指定的网站 34 | async function checkAndSetTimer() { 35 | const currentMoment = moment().tz('Asia/Hong_Kong'); 36 | const formattedTime = currentMoment.format('YYYY-MM-DD HH:mm:ss'); 37 | 38 | // 判断是否在 1:00 到 5:00 之间 39 | if (currentMoment.hours() >= 1 && currentMoment.hours() < 5) { 40 | console.log(`停止访问:1:00 到 5:00 --- ${formattedTime}`); 41 | // 在1:00到5:00之间,不访问NO24_URLS中的网站 42 | return; 43 | } else { 44 | console.log(`执行访问任务:${formattedTime}`); 45 | // 在其他时间,执行访问NO24_URLS中的网站 46 | await visitWebsites(websites); 47 | } 48 | } 49 | 50 | console.log(`Worker 激活时间:${moment().tz('Asia/Hong_Kong').format('YYYY-MM-DD HH:mm:ss')}`); 51 | 52 | // 每次请求访问24小时不间断的URLs 53 | for (let url of urls) { 54 | await scrapeAndLog(url); 55 | } 56 | 57 | // 处理在01:00至05:00暂停访问的URLs 58 | await checkAndSetTimer(); 59 | return new Response('Request processed by Cloudflare Worker!', { 60 | status: 200, 61 | headers: { 'Content-Type': 'text/plain' }, 62 | }); 63 | } 64 | 65 | // 在此添加 Cron Trigger 事件监听器 66 | addEventListener('scheduled', event => { 67 | const env = event.env; // 确保从事件中获取环境变量 68 | event.waitUntil(handleRequest(event, env)); 69 | }); 70 | -------------------------------------------------------------------------------- /vps_sb00_alive/README.md: -------------------------------------------------------------------------------- 1 | ## 用vps保活 serv00 & ct8 2 | 文件名:sb00_alive.sh 3 | 4 | ## 重要说明 5 | 6 | - 脚本用途:用vps保活 serv00 & ct8,支持多个服务器批量操作,仅支持安装我修改过的四合一无交互脚本,不支持带交互的脚本 7 | 8 | - 也可以支持安装老王原版的四合一无交互脚本,但是需要自己修改代码。因为我的代码里没有TUIC协议,而增加了socks5协议 9 | 10 | - 本人修改的[四合一无交互脚本地址](https://github.com/yutian81/Keepalive/blob/main/vps_sb00_alive/sb00-sk5.sh) 11 | 12 | - 必须将你所有的serv00服务器的ssh地址、用户名、密码以及四合一无交互脚本所需的外部变量(如端口等)存入到一个可直链下载的 json 文件,json 内容模板见下文 13 | 14 | ## 脚本原理 15 | 16 | - 使用vps每5分钟检查一次serv00服务器(已安装好四合一)上vmess端口、argo隧道、哪吒探针,判断是否可连通 17 | 18 | - 如果其中一项不可连通,则间隔 10 秒重新检查一次 19 | 20 | - 连续5次均有某一项不可连通,则远程登录serv00的SSH,并读取 json 文件内的参数,重新安装四合一无交互脚本 21 | 22 | ----- 23 | 24 | ## 如何使用 25 | 26 | ### 一、将serv00的登录信息和无交互脚本的外部变量保存在json数组中 27 | 28 | **注意:务必将 json 文件存入私库或其他支持直链的云盘,避免信息泄露。git私库文件可用CM的私库项目获取可访问的直链** 29 | 30 | json 格式如下,注意最后一组`{}`后面没有`,`: 31 | 32 | ``` 33 | [ 34 | { 35 | "HOST": "panel3.serv00.com", 36 | "SSH_USER": "用户名", 37 | "SSH_PASS": "密码", 38 | "VMESS_PORT": "tcp端口1", 39 | "SOCKS_PORT": "tcp端口2", 40 | "HY2_PORT": "udp端口", 41 | "SOCKS_USER": "socks用户名", 42 | "SOCKS_PASS": "socks密码", 43 | "ARGO_DOMAIN": "argo域名", 44 | "ARGO_AUTH": "argo的token", 45 | "NEZHA_SERVER": "哪吒域名或ip", 46 | "NEZHA_PORT": "哪吒通信端口", 47 | "NEZHA_KEY": "哪吒密钥" 48 | }, 49 | { 50 | "HOST": "s4.serv00.com", 51 | "SSH_USER": "用户名", 52 | "SSH_PASS": "密码", 53 | "VMESS_PORT": "tcp端口1", 54 | "SOCKS_PORT": "tcp端口2", 55 | "HY2_PORT": "udp端口", 56 | "SOCKS_USER": "socks用户名", 57 | "SOCKS_PASS": "socks密码", 58 | "ARGO_DOMAIN": "argo域名", 59 | "ARGO_AUTH": "argo的token", 60 | "NEZHA_SERVER": "哪吒域名或ip", 61 | "NEZHA_PORT": "哪吒通信端口", 62 | "NEZHA_KEY": "哪吒密钥" 63 | }, 64 | { 65 | "HOST": "s5.serv00.com", 66 | "SSH_USER": "用户名", 67 | "SSH_PASS": "密码", 68 | "VMESS_PORT": "tcp端口1", 69 | "SOCKS_PORT": "tcp端口2", 70 | "HY2_PORT": "udp端口", 71 | "SOCKS_USER": "socks用户名", 72 | "SOCKS_PASS": "socks密码", 73 | "ARGO_DOMAIN": "argo域名", 74 | "ARGO_AUTH": "argo的token", 75 | "NEZHA_SERVER": "哪吒域名或ip", 76 | "NEZHA_PORT": "哪吒通信端口", 77 | "NEZHA_KEY": "哪吒密钥" 78 | } 79 | ] 80 | ``` 81 | 82 | **获取这个json文件的直链地址,例如:** 83 | ``` 84 | https://raw.githubusercontent.com/yutian81/serv00/main/alive/sb00ssh.json 85 | ``` 86 | 87 | ### 二、修改脚本的全局变量 88 | > [!IMPORTANT] 89 | > **必须将变量修改为你自己的信息,变量内容中的前后的`""`不要删除** 90 | > 91 | > **以下变量必须修改,未列出的变量不要动** 92 | 93 | | 变量 | 举例 | 说明 | 94 | | ---- | ---- | ---- | 95 | | VPS_JSON_URL | `https://raw.githubusercontent.com/yutian81/serv00/main/alive/sb00ssh.json` | 第一步中的json直链地址 | 96 | | NEZHA_URL | `https://nezha.yutian.best` | 你的哪吒面板地址,必须带 `http(s)://` 前缀 | 97 | | NEZHA_APITOKEN | xxxxxxroskZcpdxxxiBxkhxxxxxJevL1 | 你的哪吒面板的 `API TOKEN` | 98 | | NEZHA_AGENT_ID | ("13" "14" "17" "23" "24" "26" "27") | 你的哪吒探针的`ID`号,只改数字,不要删除`()`和`""` | 99 | 100 | ### 三、在vps中运行本脚本 101 | 102 | ``` 103 | curl -s https://raw.githubusercontent.com/yutian81/serv00-ct8-ssh/main/vps_sb00_alive/sb00_alive.sh -o sb00_alive.sh && bash sb00_alive.sh 104 | ``` 105 | > 把其中的`https://raw.githubusercontent.com/yutian81/serv00-ct8-ssh/main/vps_sb00_alive/sb00_alive.sh`脚本地址改为你自己的`脚本直链地址` 106 | 107 | **运行截图** 108 | 109 | ![运行截图](https://github.com/user-attachments/assets/94668e6c-30de-41e4-aae1-928bd585615c) 110 | -------------------------------------------------------------------------------- /vps_sb00_alive/sb00-sk5.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 此为四协议无交互一键安装脚本,去掉tuic协议,增加sk5协议 3 | # 原作者为老王:https://github.com/eooce/Sing-box 4 | 5 | re="\033[0m" 6 | red="\033[1;91m" 7 | green="\e[1;32m" 8 | yellow="\e[1;33m" 9 | purple="\e[1;35m" 10 | red() { echo -e "\e[1;91m$1\033[0m"; } 11 | green() { echo -e "\e[1;32m$1\033[0m"; } 12 | yellow() { echo -e "\e[1;33m$1\033[0m"; } 13 | purple() { echo -e "\e[1;35m$1\033[0m"; } 14 | reading() { read -p "$(red "$1")" "$2"; } 15 | 16 | USERNAME=$(whoami) 17 | HOSTNAME=$(hostname) 18 | 19 | export LC_ALL=C 20 | export UUID=${UUID:-'bc97f674-c578-4940-9234-0a1da46041b9'} 21 | export VMESS_PORT=${VMESS_PORT:-'40000'} 22 | export SOCKS_PORT=${SOCKS_PORT:-'50000'} 23 | export HY2_PORT=${HY2_PORT:-'60000'} 24 | export SOCKS_USER=${SOCKS_USER:-'abc123'} 25 | export SOCKS_PASS=${SOCKS_PASS:-'abc456'} 26 | export ARGO_DOMAIN=${ARGO_DOMAIN:-''} 27 | export ARGO_AUTH=${ARGO_AUTH:-''} 28 | export NEZHA_SERVER=${NEZHA_SERVER:-''} 29 | export NEZHA_PORT=${NEZHA_PORT:-'5555'} 30 | export NEZHA_KEY=${NEZHA_KEY:-''} 31 | export CFIP=${CFIP:-'www.visa.com.tw'} 32 | export CFPORT=${CFPORT:-'443'} 33 | 34 | [[ "$HOSTNAME" == "s1.ct8.pl" ]] && WORKDIR="domains/${USERNAME}.ct8.pl/logs" || WORKDIR="domains/${USERNAME}.serv00.net/logs" 35 | [ -d "$WORKDIR" ] || (mkdir -p "$WORKDIR" && chmod 777 "$WORKDIR") 36 | ps aux | grep $(whoami) | grep -v "sshd\|bash\|grep" | awk '{print $2}' | xargs -r kill -9 > /dev/null 2>&1 37 | 38 | argo_configure() { 39 | clear 40 | purple "正在安装中,请稍等..." 41 | if [[ -z $ARGO_AUTH || -z $ARGO_DOMAIN ]]; then 42 | green "ARGO_DOMAIN or ARGO_AUTH is empty,use quick tunnel" 43 | return 44 | fi 45 | 46 | if [[ $ARGO_AUTH =~ TunnelSecret ]]; then 47 | echo $ARGO_AUTH > tunnel.json 48 | cat > tunnel.yml << EOF 49 | tunnel: $(cut -d\" -f12 <<< "$ARGO_AUTH") 50 | credentials-file: tunnel.json 51 | protocol: http2 52 | 53 | ingress: 54 | - hostname: $ARGO_DOMAIN 55 | service: http://localhost:$VMESS_PORT 56 | originRequest: 57 | noTLSVerify: true 58 | - service: http_status:404 59 | EOF 60 | else 61 | green "ARGO_AUTH mismatch TunnelSecret,use token connect to tunnel" 62 | fi 63 | } 64 | 65 | generate_config() { 66 | 67 | openssl ecparam -genkey -name prime256v1 -out "private.key" 68 | openssl req -new -x509 -days 3650 -key "private.key" -out "cert.pem" -subj "/CN=$USERNAME.serv00.net" 69 | 70 | cat > config.json << EOF 71 | { 72 | "log": { 73 | "disabled": true, 74 | "level": "info", 75 | "timestamp": true 76 | }, 77 | "dns": { 78 | "servers": [ 79 | { 80 | "tag": "google", 81 | "address": "tls://8.8.8.8", 82 | "strategy": "ipv4_only", 83 | "detour": "direct" 84 | } 85 | ], 86 | "rules": [ 87 | { 88 | "rule_set": [ 89 | "geosite-openai" 90 | ], 91 | "server": "wireguard" 92 | }, 93 | { 94 | "rule_set": [ 95 | "geosite-netflix" 96 | ], 97 | "server": "wireguard" 98 | }, 99 | { 100 | "rule_set": [ 101 | "geosite-category-ads-all" 102 | ], 103 | "server": "block" 104 | } 105 | ], 106 | "final": "google", 107 | "strategy": "", 108 | "disable_cache": false, 109 | "disable_expire": false 110 | }, 111 | "inbounds": [ 112 | { 113 | "tag": "hysteria-in", 114 | "type": "hysteria2", 115 | "listen": "::", 116 | "listen_port": $HY2_PORT, 117 | "users": [ 118 | { 119 | "password": "$UUID" 120 | } 121 | ], 122 | "masquerade": "https://bing.com", 123 | "tls": { 124 | "enabled": true, 125 | "alpn": [ 126 | "h3" 127 | ], 128 | "certificate_path": "cert.pem", 129 | "key_path": "private.key" 130 | } 131 | }, 132 | { 133 | "tag": "vmess-ws-in", 134 | "type": "vmess", 135 | "listen": "::", 136 | "listen_port": $VMESS_PORT, 137 | "users": [ 138 | { 139 | "uuid": "$UUID" 140 | } 141 | ], 142 | "transport": { 143 | "type": "ws", 144 | "path": "/vmess", 145 | "early_data_header_name": "Sec-WebSocket-Protocol" 146 | } 147 | }, 148 | { 149 | "tag": "socks-in", 150 | "type": "socks", 151 | "listen": "::", 152 | "listen_port": $SOCKS_PORT, 153 | "users": [ 154 | { 155 | "username": "$SOCKS_USER", 156 | "password": "$SOCKS_PASS" 157 | } 158 | ] 159 | } 160 | 161 | ], 162 | "outbounds": [ 163 | { 164 | "type": "direct", 165 | "tag": "direct" 166 | }, 167 | { 168 | "type": "block", 169 | "tag": "block" 170 | }, 171 | { 172 | "type": "dns", 173 | "tag": "dns-out" 174 | }, 175 | { 176 | "type": "wireguard", 177 | "tag": "wireguard-out", 178 | "server": "162.159.195.100", 179 | "server_port": 4500, 180 | "local_address": [ 181 | "172.16.0.2/32", 182 | "2606:4700:110:83c7:b31f:5858:b3a8:c6b1/128" 183 | ], 184 | "private_key": "mPZo+V9qlrMGCZ7+E6z2NI6NOV34PD++TpAR09PtCWI=", 185 | "peer_public_key": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", 186 | "reserved": [ 187 | 26, 188 | 21, 189 | 228 190 | ] 191 | } 192 | ], 193 | "route": { 194 | "rules": [ 195 | { 196 | "protocol": "dns", 197 | "outbound": "dns-out" 198 | }, 199 | { 200 | "ip_is_private": true, 201 | "outbound": "direct" 202 | }, 203 | { 204 | "rule_set": [ 205 | "geosite-openai" 206 | ], 207 | "outbound": "wireguard-out" 208 | }, 209 | { 210 | "rule_set": [ 211 | "geosite-netflix" 212 | ], 213 | "outbound": "wireguard-out" 214 | }, 215 | { 216 | "rule_set": [ 217 | "geosite-category-ads-all" 218 | ], 219 | "outbound": "block" 220 | } 221 | ], 222 | "rule_set": [ 223 | { 224 | "tag": "geosite-netflix", 225 | "type": "remote", 226 | "format": "binary", 227 | "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-netflix.srs", 228 | "download_detour": "direct" 229 | }, 230 | { 231 | "tag": "geosite-openai", 232 | "type": "remote", 233 | "format": "binary", 234 | "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/openai.srs", 235 | "download_detour": "direct" 236 | }, 237 | { 238 | "tag": "geosite-category-ads-all", 239 | "type": "remote", 240 | "format": "binary", 241 | "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs", 242 | "download_detour": "direct" 243 | } 244 | ], 245 | "final": "direct" 246 | }, 247 | "experimental": { 248 | "cache_file": { 249 | "path": "cache.db", 250 | "cache_id": "mycacheid", 251 | "store_fakeip": true 252 | } 253 | } 254 | } 255 | EOF 256 | } 257 | 258 | download_singbox() { 259 | ARCH=$(uname -m) && DOWNLOAD_DIR="." && mkdir -p "$DOWNLOAD_DIR" && FILE_INFO=() 260 | if [ "$ARCH" == "arm" ] || [ "$ARCH" == "arm64" ] || [ "$ARCH" == "aarch64" ]; then 261 | FILE_INFO=( 262 | "https://github.com/eooce/test/releases/download/arm64/sb web" 263 | "https://github.com/eooce/test/releases/download/arm64/bot13 bot" 264 | "https://github.com/eooce/test/releases/download/ARM/swith npm" 265 | ) 266 | elif [ "$ARCH" == "amd64" ] || [ "$ARCH" == "x86_64" ] || [ "$ARCH" == "x86" ]; then 267 | FILE_INFO=( 268 | "https://github.com/eooce/test/releases/download/freebsd/sb web" 269 | "https://github.com/eooce/test/releases/download/freebsd/server bot" 270 | "https://github.com/eooce/test/releases/download/freebsd/npm npm" 271 | ) 272 | else 273 | echo "不支持该系统架构: $ARCH" 274 | exit 1 275 | fi 276 | 277 | download_with_fallback() { 278 | local URL=$1 279 | local FILENAME_DIR=$2 280 | local FILENAME=$3 281 | curl -L -sS --max-time 2 -o "$FILENAME" "$URL" & 282 | CURL_PID=$! 283 | CURL_START_SIZE=$(stat -c%s "$FILENAME" 2>/dev/null || echo 0) 284 | sleep 1 285 | CURL_CURRENT_SIZE=$(stat -c%s "$FILENAME" 2>/dev/null || echo 0) 286 | 287 | if [ "$CURL_CURRENT_SIZE" -le "$CURL_START_SIZE" ]; then 288 | kill $CURL_PID 2>/dev/null 289 | wait $CURL_PID 2>/dev/null 290 | wget -q -O "$FILENAME" "$URL" 291 | echo -e "\e[1;32m正在使用 wget 下载 $FILENAME\e[0m" 292 | else 293 | wait $CURL_PID 294 | echo -e "\e[1;32m正在使用 curl 下载 $FILENAME\e[0m" 295 | fi 296 | chmod +x "$FILENAME" 297 | } 298 | 299 | for entry in "${FILE_INFO[@]}"; do 300 | URL=$(echo "$entry" | cut -d ' ' -f 1) 301 | FILENAME=$(echo "$entry" | cut -d ' ' -f 2) 302 | FILENAME_DIR=$DOWNLOAD_DIR/$FILENAME 303 | if [ -e "$FILENAME_DIR" ]; then 304 | echo -e "\e[1;32m$FILENAME_DIR 已经存在,跳过下载\e[0m" 305 | else 306 | download_with_fallback "$URL" "$FILENAME_DIR" "$FILENAME" 307 | fi 308 | done 309 | wait 310 | 311 | if [ -e "$DOWNLOAD_DIR/npm" ]; then 312 | tlsPorts=("443" "8443" "2096" "2087" "2083" "2053") 313 | if [[ "${tlsPorts[*]}" =~ "${NEZHA_PORT}" ]]; then 314 | NEZHA_TLS="--tls" 315 | else 316 | NEZHA_TLS="" 317 | fi 318 | if [ -n "$NEZHA_SERVER" ] && [ -n "$NEZHA_PORT" ] && [ -n "$NEZHA_KEY" ]; then 319 | export TMPDIR=$(pwd) 320 | nohup ./"$DOWNLOAD_DIR/npm" -s ${NEZHA_SERVER}:${NEZHA_PORT} -p ${NEZHA_KEY} ${NEZHA_TLS} >/dev/null 2>&1 & 321 | sleep 2 322 | pgrep -f "npm" > /dev/null && green "$DOWNLOAD_DIR/npm 正在运行" || { 323 | red "$DOWNLOAD_DIR/npm 未运行,正在重启……" 324 | pkill -f "npm" && nohup ./"$DOWNLOAD_DIR/npm" -s "${NEZHA_SERVER}:${NEZHA_PORT}" -p "${NEZHA_KEY}" ${NEZHA_TLS} >/dev/null 2>&1 & 325 | sleep 2 326 | purple "$DOWNLOAD_DIR/npm 已重启" 327 | } 328 | else 329 | purple "哪吒参数为空,跳过运行" 330 | fi 331 | fi 332 | 333 | if [ -e "$DOWNLOAD_DIR/web" ]; then 334 | nohup ./"$DOWNLOAD_DIR/web" run -c config.json >/dev/null 2>&1 & 335 | sleep 2 336 | pgrep -f "web" > /dev/null && green "$DOWNLOAD_DIR/web 正在运行" || { 337 | red "$DOWNLOAD_DIR/web 未运行,正在重启……" 338 | pkill -f "web" && nohup ./"$DOWNLOAD_DIR/web" run -c config.json >/dev/null 2>&1 & 339 | sleep 2 340 | purple "$DOWNLOAD_DIR/web 已重启" 341 | } 342 | fi 343 | 344 | if [ -e "$DOWNLOAD_DIR/bot" ]; then 345 | if [[ $ARGO_AUTH =~ ^[A-Z0-9a-z=]{120,250}$ ]]; then 346 | args="tunnel --edge-ip-version auto --no-autoupdate --protocol http2 run --token \"${ARGO_AUTH}\"" 347 | elif [[ $ARGO_AUTH =~ TunnelSecret ]]; then 348 | args="tunnel --edge-ip-version auto --config tunnel.yml run" 349 | else 350 | args="tunnel --edge-ip-version auto --no-autoupdate --protocol http2 --logfile boot.log --loglevel info --url http://localhost:$VMESS_PORT" 351 | fi 352 | nohup ./"$DOWNLOAD_DIR/bot" $args >/dev/null 2>&1 & 353 | sleep 2 354 | pgrep -f "bot" > /dev/null && green "$DOWNLOAD_DIR/bot 正在运行" || { 355 | red "$DOWNLOAD_DIR/bot 未运行,正在重启……" 356 | pkill -f "bot" && nohup ./"$DOWNLOAD_DIR/bot" "${args}" >/dev/null 2>&1 & 357 | sleep 2 358 | purple "$DOWNLOAD_DIR/bot 已重启" 359 | } 360 | fi 361 | sleep 5 362 | # rm -f "$(basename ${FILE_MAP[npm]})" "$(basename ${FILE_MAP[web]})" "$(basename ${FILE_MAP[bot]})" 363 | } 364 | 365 | get_argodomain() { 366 | if [[ -n $ARGO_AUTH ]]; then 367 | echo "$ARGO_DOMAIN" 368 | else 369 | grep -oE 'https://[[:alnum:]+\.-]+\.trycloudflare\.com' boot.log | sed 's@https://@@' 370 | fi 371 | } 372 | 373 | get_ip() { 374 | ip=$(curl -s --max-time 2 ipv4.ip.sb) 375 | if [ -z "$ip" ]; then 376 | ip=$( [[ "$HOSTNAME" =~ s[0-9]\.serv00\.com ]] && echo "${HOSTNAME/s/web}" || echo "$HOSTNAME" ) 377 | else 378 | url="https://www.toolsdaquan.com/toolapi/public/ipchecking/$ip/443" 379 | response=$(curl -s --location --max-time 3.5 --request GET "$url" --header 'Referer: https://www.toolsdaquan.com/ipcheck') 380 | if [ -z "$response" ] || ! echo "$response" | grep -q '"icmp":"success"'; then 381 | accessible=false 382 | else 383 | accessible=true 384 | fi 385 | if [ "$accessible" = false ]; then 386 | ip=$( [[ "$HOSTNAME" =~ s[0-9]\.serv00\.com ]] && echo "${HOSTNAME/s/web}" || echo "$ip" ) 387 | fi 388 | fi 389 | echo "$ip" 390 | } 391 | 392 | get_links(){ 393 | argodomain=$(get_argodomain) 394 | echo -e "\e[1;32mArgoDomain:\e[1;35m${argodomain}\e[0m\n" 395 | sleep 1 396 | IP=$(get_ip) 397 | ISP=$(curl -s https://speed.cloudflare.com/meta | awk -F\" '{print $26"-"$18}' | sed -e 's/ /_/g') 398 | sleep 1 399 | yellow "注意:v2ray或其他软件的跳过证书验证需设置为true,否则hy2或tuic节点可能不通\n" 400 | cat > list.txt < /dev/null 51 | } 52 | install_packages 53 | 54 | # 判断系统架构,添加对应的定时任务 55 | add_cron_job() { 56 | local new_cron="*/5 * * * * /bin/bash $SCRIPT_PATH >> /root/00_keep.log 2>&1" 57 | local current_cron 58 | if crontab -l | grep -q "$SCRIPT_PATH" > /dev/null 2>&1; then 59 | red "定时任务已存在,跳过添加计划任务" 60 | else 61 | if [ -f /etc/debian_version ] || [ -f /etc/redhat-release ] || [ -f /etc/fedora-release ]; then 62 | (crontab -l; echo "$new_cron") | crontab - 63 | elif [ -f /etc/alpine-release ]; then 64 | if [ -f /var/spool/cron/crontabs/root ]; then 65 | current_cron=$(cat /var/spool/cron/crontabs/root) 66 | fi 67 | echo -e "$current_cron\n$new_cron" > /var/spool/cron/crontabs/root 68 | rc-service crond restart 69 | fi 70 | green "已添加定时任务,每5分钟执行一次" 71 | fi 72 | } 73 | add_cron_job 74 | 75 | # 下载存储有服务器登录及无交互脚本外部变量信息的 JSON 文件 76 | download_json() { 77 | if ! curl -s "$VPS_JSON_URL" -o sb00ssh.json; then 78 | red "Serv00 配置文件下载失败,尝试使用 wget 下载!" 79 | if ! wget -q "$VPS_JSON_URL" -O sb00ssh.json; then 80 | red "Serv00 配置文件下载失败,请检查下载地址是否正确!" 81 | exit 1 82 | else 83 | green "Serv00 配置文件通过 wget 下载成功!" 84 | fi 85 | else 86 | green "Serv00 配置文件通过 curl 下载成功!" 87 | fi 88 | # 检查文件是否存在和非空 89 | if [[ ! -s "sb00ssh.json" ]]; then 90 | red "配置文件 sb00ssh.json 不存在或为空" 91 | exit 1 92 | fi 93 | } 94 | download_json 95 | 96 | # 检测 TCP 端口 97 | check_tcp_port() { 98 | local HOST=$1 99 | local VMESS_PORT=$2 100 | # 使用 nc 命令检测端口状态,返回0表示可用 101 | if nc -zv "$HOST" "$VMESS_PORT" &>/dev/null; then 102 | port_status=0 # 端口可用 103 | else 104 | port_status=1 # 端口不可用 105 | fi 106 | } 107 | 108 | # 检查 Argo 隧道状态 109 | check_argo_status() { 110 | local ARGO_DOMAIN=$1 111 | argo_status=$(curl -o /dev/null -s -w "%{http_code}\n" "https://$ARGO_DOMAIN") 112 | echo "$argo_status" 113 | } 114 | 115 | # 获取哪吒探针列表 116 | check_nezha_list() { 117 | agent_list=$(curl -s -H "Authorization: $NEZHA_APITOKEN" "$NEZHA_API") 118 | if [ $? -ne 0 ] || [[ -z "$agent_list" || "$agent_list" == "null" ]]; then 119 | red "获取哪吒探针列表失败,请检查 NEZHA_APITOKEN 和 NEZHA_URL 设置" 120 | exit 1 121 | fi 122 | } 123 | 124 | # 连接并执行远程命令的函数 125 | run_remote_command() { 126 | local HOST=$1 SSH_USER=$2 SSH_PASS=$3 VMESS_PORT=$4 HY2_PORT=$5 SOCKS_PORT=$6 SOCKS_USER=$7 SOCKS_PASS=$8 ARGO_DOMAIN=$9 ARGO_AUTH=${10} NEZHA_SERVER=${11} NEZHA_PORT=${12} NEZHA_KEY=${13} 127 | sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no "$SSH_USER@$HOST" \ 128 | "ps aux | grep \"$(whoami)\" | grep -v 'sshd\|bash\|grep' | awk '{print \$2}' | xargs -r kill -9 > /dev/null 2>&1 && \ 129 | VMESS_PORT=$VMESS_PORT HY2_PORT=$HY2_PORT SOCKS_PORT=$SOCKS_PORT \ 130 | SOCKS_USER=\"$SOCKS_USER\" SOCKS_PASS=\"$SOCKS_PASS\" \ 131 | ARGO_DOMAIN=$ARGO_DOMAIN ARGO_AUTH=\"$ARGO_AUTH\" \ 132 | NEZHA_SERVER=$NEZHA_SERVER NEZHA_PORT=$NEZHA_PORT NEZHA_KEY=$NEZHA_KEY \ 133 | bash <(curl -Ls ${REBOOT_URL})" #使用此脚本无需重装节点,它将直接启动原本存储在服务器中进程和配置文件,实现节点重启,仅适用于yutian81修改serv00四合一有交互脚本 134 | # bash <(curl -Ls ${SCRIPT_URL}) #使用此脚本即自动安装无交互节点脚本 135 | } 136 | 137 | # 处理服务器列表并遍历,TCP端口、Argo、哪吒探针三项检测有一项不通即连接 SSH 执行命令 138 | process_servers() { 139 | jq -c '.[]' "sb00ssh.json" | while IFS= read -r servers; do 140 | HOST=$(echo "$servers" | jq -r '.HOST') 141 | SSH_USER=$(echo "$servers" | jq -r '.SSH_USER') 142 | SSH_PASS=$(echo "$servers" | jq -r '.SSH_PASS') 143 | VMESS_PORT=$(echo "$servers" | jq -r '.VMESS_PORT') 144 | SOCKS_PORT=$(echo "$servers" | jq -r '.SOCKS_PORT') 145 | HY2_PORT=$(echo "$servers" | jq -r '.HY2_PORT') 146 | SOCKS_USER=$(echo "$servers" | jq -r '.SOCKS_USER') 147 | SOCKS_PASS=$(echo "$servers" | jq -r '.SOCKS_PASS') 148 | ARGO_DOMAIN=$(echo "$servers" | jq -r '.ARGO_DOMAIN') 149 | ARGO_AUTH=$(echo "$servers" | jq -r '.ARGO_AUTH') 150 | NEZHA_SERVER=$(echo "$servers" | jq -r '.NEZHA_SERVER') 151 | NEZHA_PORT=$(echo "$servers" | jq -r '.NEZHA_PORT') 152 | NEZHA_KEY=$(echo "$servers" | jq -r '.NEZHA_KEY') 153 | yellow "正在检查服务器 $HOST 的 [Vmess端口]、[Argo隧道]、[哪吒探针] 是否可访问" 154 | 155 | local attempt=0 156 | local max_attempts=5 # 最大尝试检测次数 157 | local time=$(TZ="Asia/Hong_Kong" date +"%Y-%m-%d %H:%M") 158 | while [ $attempt -lt $max_attempts ]; do 159 | all_checks=true 160 | 161 | # 检查 TCP 端口是否通畅,不通则 10 秒后重试 162 | check_tcp_port "$HOST" "$VMESS_PORT" 163 | if [ "$port_status" -ne 0 ]; then 164 | red "TCP 端口 $(yellow "$VMESS_PORT") 不可用!休眠 10 秒后重试" 165 | all_checks=false 166 | sleep 10 167 | attempt=$((attempt + 1)) 168 | continue 169 | fi 170 | 171 | # 检查 Argo 连接是否通畅,不通则 10 秒后重试 172 | argo_status=$(check_argo_status "$ARGO_DOMAIN") 173 | if [ "$argo_status" == "530" ]; then 174 | red "Argo $(yellow "$ARGO_DOMAIN") 不可用!状态码:$(yellow "$argo_status"),休眠 10 秒后重试" 175 | all_checks=false 176 | sleep 10 177 | attempt=$((attempt + 1)) 178 | continue 179 | fi 180 | 181 | # 检查 nezha 探针是否在线,不在线则 10 秒后重试 182 | check_nezha_list 183 | current_time=$(date +%s) 184 | echo "$agent_list" | jq -c '.result[]' | while IFS= read -r server; do 185 | server_name=$(echo "$server" | jq -r '.name') 186 | last_active=$(echo "$server" | jq -r '.last_active') 187 | valid_ip=$(echo "$server" | jq -r '.valid_ip') 188 | server_id=$(echo "$server" | jq -r '.id') 189 | # 筛选 ID 相符的探针 190 | if [[ " ${NEZHA_AGENT_ID[@]} " =~ " $server_id " ]]; then 191 | if [ $((current_time - last_active)) -gt 30 ]; then 192 | nezha_status="offline" 193 | red 服务器 "$server_name 的哪吒探针已离线,探针 ID 为 $server_id" 194 | all_checks=false 195 | sleep 10 196 | attempt=$((attempt + 1)) 197 | continue 198 | else 199 | nezha_status="online" 200 | break 201 | fi 202 | fi 203 | done 204 | 205 | # 如果所有检查都通过,则打印通畅信息并退出循环 206 | if [ "$all_checks" == true ]; then 207 | green "TCP 端口 $(yellow "$VMESS_PORT") $(green "通畅"); $(green "Argo") $(yellow "$ARGO_DOMAIN") $(green "正常") ; $(yellow "哪吒探针") $(green "正常")" 208 | green "服务器 $(yellow "$HOST") $(green "一切正常!账户:") $(yellow "$SSH_USER") [$time]" 209 | break 210 | fi 211 | done 212 | 213 | # 三项循环检测达到 5 次,远程连接 SSH 执行安装命令 214 | if [ $attempt -ge $max_attempts ]; then 215 | red "多次检测失败,开始连接服务器 $(yellow "$HOST") 重装脚本 [$time]" 216 | if sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no "$SSH_USER@$HOST" -q exit; then 217 | green "服务器 $(yellow "$HOST") 连接成功,账户:$(yellow "$SSH_USER") [$time]" 218 | run_remote_command "$HOST" "$SSH_USER" "$SSH_PASS" "$VMESS_PORT" "$HY2_PORT" "$SOCKS_PORT" "$SOCKS_USER" "$SOCKS_PASS" "$ARGO_DOMAIN" "$ARGO_AUTH" "$NEZHA_SERVER" "$NEZHA_PORT" "$NEZHA_KEY" 219 | cmd_status=$? 220 | sleep 3 221 | if [ $cmd_status -eq 0 ]; then 222 | if [ $port_status -eq 0 ] && [ $argo_status != "530" ] && [ $nezha_status == "online" ]; then 223 | green "远程命令执行成功,结果如下:" 224 | green "服务器 $(yellow "$HOST") 端口 $(yellow "$VMESS_PORT") 恢复正常; Argo $(yellow "$ARGO_DOMAIN") 恢复正常; 哪吒 $(yellow "$server_name") 恢复正常" 225 | else 226 | red "Vmess端口、Argo或哪吒状态异常,请检查服务器参数 $VPS_JSON_URL" 227 | fi 228 | else 229 | red "远程命令执行失败,请检查服务器 $(yellow "$HOST") 参数设置是否正确" 230 | fi 231 | else 232 | red "服务器: $(yellow "$HOST") 连接失败,请检查账户 $(yellow "$SSH_USER") 和 $(yellow "密码") [$time]" 233 | fi 234 | fi 235 | done 236 | } 237 | process_servers 238 | -------------------------------------------------------------------------------- /vps_sb5in1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 定义颜色 4 | re="\033[0m" 5 | red="\033[1;91m" 6 | green="\e[1;32m" 7 | yellow="\e[1;33m" 8 | purple="\e[1;35m" 9 | skyblue="\e[1;36m" 10 | red() { echo -e "\e[1;91m$1\033[0m"; } 11 | green() { echo -e "\e[1;32m$1\033[0m"; } 12 | yellow() { echo -e "\e[1;33m$1\033[0m"; } 13 | purple() { echo -e "\e[1;35m$1\033[0m"; } 14 | skyblue() { echo -e "\e[1;36m$1\033[0m"; } 15 | reading() { read -p "$(red "$1")" "$2"; } 16 | 17 | # 定义常量 18 | server_name="sing-box" 19 | work_dir="/etc/sing-box" 20 | config_dir="${work_dir}/config.json" 21 | client_dir="${work_dir}/url.txt" 22 | export vless_port=${PORT:-$(shuf -i 1000-65000 -n 1)} 23 | export CFIP=${CFIP:-'cloudflare.182682.xyz'} 24 | export CFPORT=${CFPORT:-'8443'} 25 | 26 | # 检查是否为root下运行 27 | [[ $EUID -ne 0 ]] && red "请在root用户下运行脚本" && exit 1 28 | 29 | # 检查 sing-box 是否已安装 30 | check_singbox() { 31 | if [ -f "${work_dir}/${server_name}" ]; then 32 | if [ -f /etc/alpine-release ]; then 33 | rc-service sing-box status | grep -q "started" && green "running" && return 0 || yellow "not running" && return 1 34 | else 35 | [ "$(systemctl is-active sing-box)" = "active" ] && green "running" && return 0 || yellow "not running" && return 1 36 | fi 37 | else 38 | red "not installed" 39 | return 2 40 | fi 41 | } 42 | 43 | # 检查 argo 是否已安装 44 | check_argo() { 45 | if [ -f "${work_dir}/argo" ]; then 46 | if [ -f /etc/alpine-release ]; then 47 | rc-service argo status | grep -q "started" && green "running" && return 0 || yellow "not running" && return 1 48 | else 49 | [ "$(systemctl is-active argo)" = "active" ] && green "running" && return 0 || yellow "not running" && return 1 50 | fi 51 | else 52 | red "not installed" 53 | return 2 54 | fi 55 | } 56 | 57 | #根据系统类型安装、卸载依赖 58 | manage_packages() { 59 | if [ $# -lt 2 ]; then 60 | red "Unspecified package name or action" 61 | return 1 62 | fi 63 | 64 | action=$1 65 | shift 66 | 67 | for package in "$@"; do 68 | if [ "$action" == "install" ]; then 69 | if command -v "$package" &>/dev/null; then 70 | green "${package} already installed" 71 | continue 72 | fi 73 | yellow "正在安装 ${package}..." 74 | if command -v apt &>/dev/null; then 75 | apt install -y "$package" 76 | elif command -v dnf &>/dev/null; then 77 | dnf install -y "$package" 78 | elif command -v yum &>/dev/null; then 79 | yum install -y "$package" 80 | elif command -v apk &>/dev/null; then 81 | apk update 82 | apk add "$package" 83 | else 84 | red "Unknown system!" 85 | return 1 86 | fi 87 | elif [ "$action" == "uninstall" ]; then 88 | if ! command -v "$package" &>/dev/null; then 89 | yellow "${package} is not installed" 90 | continue 91 | fi 92 | yellow "正在卸载 ${package}..." 93 | if command -v apt &>/dev/null; then 94 | apt remove -y "$package" && apt autoremove -y 95 | elif command -v dnf &>/dev/null; then 96 | dnf remove -y "$package" && dnf autoremove -y 97 | elif command -v yum &>/dev/null; then 98 | yum remove -y "$package" && yum autoremove -y 99 | elif command -v apk &>/dev/null; then 100 | apk del "$package" 101 | else 102 | red "Unknown system!" 103 | return 1 104 | fi 105 | else 106 | red "Unknown action: $action" 107 | return 1 108 | fi 109 | done 110 | 111 | return 0 112 | } 113 | 114 | # 获取ip 115 | get_realip() { 116 | ip=$(curl -s --max-time 2 ipv4.ip.sb) 117 | if [ -z "$ip" ]; then 118 | ipv6=$(curl -s --max-time 1 ipv6.ip.sb) 119 | echo "[$ipv6]" 120 | else 121 | if echo "$(curl -s http://ipinfo.io/org)" | grep -qE 'Cloudflare|UnReal|AEZA|Andrei'; then 122 | ipv6=$(curl -s --max-time 1 ipv6.ip.sb) 123 | echo "[$ipv6]" 124 | else 125 | echo "$ip" 126 | fi 127 | fi 128 | } 129 | 130 | # 下载并安装 sing-box,cloudflared 131 | install_singbox() { 132 | clear 133 | purple "正在安装sing-box中,请稍后..." 134 | # 判断系统架构 135 | ARCH_RAW=$(uname -m) 136 | case "${ARCH_RAW}" in 137 | 'x86_64') ARCH='amd64' ;; 138 | 'x86' | 'i686' | 'i386') ARCH='386' ;; 139 | 'aarch64' | 'arm64') ARCH='arm64' ;; 140 | 'armv7l') ARCH='armv7' ;; 141 | 's390x') ARCH='s390x' ;; 142 | *) red "不支持的架构: ${ARCH_RAW}"; exit 1 ;; 143 | esac 144 | 145 | # 下载sing-box,cloudflared 146 | [ ! -d "${work_dir}" ] && mkdir -p "${work_dir}" && chmod 777 "${work_dir}" 147 | # latest_version=$(curl -s "https://api.github.com/repos/SagerNet/sing-box/releases" | jq -r '[.[] | select(.prerelease==false)][0].tag_name | sub("^v"; "")') 148 | # curl -sLo "${work_dir}/${server_name}.tar.gz" "https://github.com/SagerNet/sing-box/releases/download/v${latest_version}/sing-box-${latest_version}-linux-${ARCH}.tar.gz" 149 | # curl -sLo "${work_dir}/qrencode" "https://github.com/eooce/test/releases/download/${ARCH}/qrencode-linux-${ARCH}" 150 | # curl -sLo "${work_dir}/qrencode" "https://$ARCH.ssss.nyc.mn/qrencode" 151 | curl -sLo "${work_dir}/sing-box" "https://$ARCH.ssss.nyc.mn/sbx" 152 | curl -sLo "${work_dir}/argo" "https://$ARCH.ssss.nyc.mn/bot" 153 | # tar -xzvf "${work_dir}/${server_name}.tar.gz" -C "${work_dir}/" && \ 154 | # mv "${work_dir}/sing-box-${latest_version}-linux-${ARCH}/sing-box" "${work_dir}/" && \ 155 | # rm -rf "${work_dir}/${server_name}.tar.gz" "${work_dir}/sing-box-${latest_version}-linux-${ARCH}" 156 | chown root:root ${work_dir} && chmod +x ${work_dir}/${server_name} ${work_dir}/argo # ${work_dir}/qrencode 157 | 158 | # 生成随机端口和密码 159 | socks_port=$(($vless_port + 1)) 160 | tuic_port=$(($vless_port + 2)) 161 | hy2_port=$(($vless_port + 3)) 162 | #socks_user=$(< /dev/urandom tr -dc 'A-Za-z0-9' | head -c 8) 163 | #socks_pass=$(< /dev/urandom tr -dc 'A-Za-z0-9' | head -c 8) 164 | socks_user="yutian" 165 | socks_pass="yutian=abcd" 166 | uuid=$(cat /proc/sys/kernel/random/uuid) 167 | password=$(< /dev/urandom tr -dc 'A-Za-z0-9' | head -c 24) 168 | output=$(/etc/sing-box/sing-box generate reality-keypair) 169 | private_key=$(echo "${output}" | awk '/PrivateKey:/ {print $2}') 170 | public_key=$(echo "${output}" | awk '/PublicKey:/ {print $2}') 171 | 172 | iptables -F > /dev/null 2>&1 && iptables -P INPUT ACCEPT > /dev/null 2>&1 && iptables -P FORWARD ACCEPT > /dev/null 2>&1 && iptables -P OUTPUT ACCEPT > /dev/null 2>&1 173 | command -v ip6tables &> /dev/null && ip6tables -F > /dev/null 2>&1 && ip6tables -P INPUT ACCEPT > /dev/null 2>&1 && ip6tables -P FORWARD ACCEPT > /dev/null 2>&1 && ip6tables -P OUTPUT ACCEPT > /dev/null 2>&1 174 | 175 | manage_packages uninstall ufw firewalld > /dev/null 2>&1 176 | 177 | # 生成自签名证书 178 | openssl ecparam -genkey -name prime256v1 -out "${work_dir}/private.key" 179 | openssl req -new -x509 -days 3650 -key "${work_dir}/private.key" -out "${work_dir}/cert.pem" -subj "/CN=bing.com" 180 | 181 | # 生成配置文件 182 | cat > "${config_dir}" << EOF 183 | { 184 | "log": { 185 | "disabled": false, 186 | "level": "info", 187 | "output": "$work_dir/sb.log", 188 | "timestamp": true 189 | }, 190 | "dns": { 191 | "servers": [ 192 | { 193 | "tag": "google", 194 | "address": "tls://8.8.8.8" 195 | } 196 | ] 197 | }, 198 | "inbounds": [ 199 | { 200 | "tag": "vless-reality-vesion", 201 | "type": "vless", 202 | "listen": "::", 203 | "listen_port": $vless_port, 204 | "users": [ 205 | { 206 | "uuid": "$uuid", 207 | "flow": "xtls-rprx-vision" 208 | } 209 | ], 210 | "tls": { 211 | "enabled": true, 212 | "server_name": "www.iij.ad.jp", 213 | "reality": { 214 | "enabled": true, 215 | "handshake": { 216 | "server": "www.iij.ad.jp", 217 | "server_port": 443 218 | }, 219 | "private_key": "$private_key", 220 | "short_id": [ 221 | "" 222 | ] 223 | } 224 | } 225 | }, 226 | { 227 | "tag": "vmess-ws", 228 | "type": "vmess", 229 | "listen": "::", 230 | "listen_port": 8001, 231 | "users": [ 232 | { 233 | "uuid": "$uuid" 234 | } 235 | ], 236 | "transport": { 237 | "type": "ws", 238 | "path": "/vmess-argo", 239 | "early_data_header_name": "Sec-WebSocket-Protocol" 240 | } 241 | }, 242 | { 243 | "tag": "socks-in", 244 | "type": "socks", 245 | "listen": "::", 246 | "listen_port": $socks_port, 247 | "users": [ 248 | { 249 | "username": "$socks_user", 250 | "password": "$socks_pass" 251 | } 252 | ] 253 | }, 254 | { 255 | "tag": "hysteria2", 256 | "type": "hysteria2", 257 | "listen": "::", 258 | "listen_port": $hy2_port, 259 | "sniff": true, 260 | "sniff_override_destination": false, 261 | "users": [ 262 | { 263 | "password": "$uuid" 264 | } 265 | ], 266 | "ignore_client_bandwidth":false, 267 | "masquerade": "https://bing.com", 268 | "tls": { 269 | "enabled": true, 270 | "alpn": [ 271 | "h3" 272 | ], 273 | "min_version":"1.3", 274 | "max_version":"1.3", 275 | "certificate_path": "$work_dir/cert.pem", 276 | "key_path": "$work_dir/private.key" 277 | } 278 | 279 | }, 280 | { 281 | "tag": "tuic", 282 | "type": "tuic", 283 | "listen": "::", 284 | "listen_port": $tuic_port, 285 | "users": [ 286 | { 287 | "uuid": "$uuid", 288 | "password": "$password" 289 | } 290 | ], 291 | "congestion_control": "bbr", 292 | "tls": { 293 | "enabled": true, 294 | "alpn": [ 295 | "h3" 296 | ], 297 | "certificate_path": "$work_dir/cert.pem", 298 | "key_path": "$work_dir/private.key" 299 | } 300 | } 301 | ], 302 | "outbounds": [ 303 | { 304 | "type": "direct", 305 | "tag": "direct" 306 | }, 307 | { 308 | "type": "direct", 309 | "tag": "direct-ipv4-prefer-out", 310 | "domain_strategy": "prefer_ipv4" 311 | }, 312 | { 313 | "type": "direct", 314 | "tag": "direct-ipv4-only-out", 315 | "domain_strategy": "ipv4_only" 316 | }, 317 | { 318 | "type": "direct", 319 | "tag": "direct-ipv6-prefer-out", 320 | "domain_strategy": "prefer_ipv6" 321 | }, 322 | { 323 | "type": "direct", 324 | "tag": "direct-ipv6-only-out", 325 | "domain_strategy": "ipv6_only" 326 | }, 327 | { 328 | "type": "wireguard", 329 | "tag": "wireguard-out", 330 | "server": "engage.cloudflareclient.com", 331 | "server_port": 2408, 332 | "local_address": [ 333 | "172.16.0.2/32", 334 | "2606:4700:110:812a:4929:7d2a:af62:351c/128" 335 | ], 336 | "private_key": "gBthRjevHDGyV0KvYwYE52NIPy29sSrVr6rcQtYNcXA=", 337 | "peer_public_key": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", 338 | "reserved": [ 339 | 6, 340 | 146, 341 | 6 342 | ] 343 | }, 344 | { 345 | "type": "direct", 346 | "tag": "wireguard-ipv4-prefer-out", 347 | "detour": "wireguard-out", 348 | "domain_strategy": "prefer_ipv4" 349 | }, 350 | { 351 | "type": "direct", 352 | "tag": "wireguard-ipv4-only-out", 353 | "detour": "wireguard-out", 354 | "domain_strategy": "ipv4_only" 355 | }, 356 | { 357 | "type": "direct", 358 | "tag": "wireguard-ipv6-prefer-out", 359 | "detour": "wireguard-out", 360 | "domain_strategy": "prefer_ipv6" 361 | }, 362 | { 363 | "type": "direct", 364 | "tag": "wireguard-ipv6-only-out", 365 | "detour": "wireguard-out", 366 | "domain_strategy": "ipv6_only" 367 | } 368 | ], 369 | "route": { 370 | "rule_set": [ 371 | { 372 | "tag": "geosite-netflix", 373 | "type": "remote", 374 | "format": "binary", 375 | "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-netflix.srs", 376 | "update_interval": "1d" 377 | }, 378 | { 379 | "tag": "geosite-openai", 380 | "type": "remote", 381 | "format": "binary", 382 | "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/openai.srs", 383 | "update_interval": "1d" 384 | } 385 | ], 386 | "rules": [ 387 | { 388 | "rule_set": [ 389 | "geosite-netflix" 390 | ], 391 | "outbound": "wireguard-ipv6-only-out" 392 | }, 393 | { 394 | "domain": [ 395 | "api.statsig.com", 396 | "browser-intake-datadoghq.com", 397 | "cdn.openai.com", 398 | "chat.openai.com", 399 | "auth.openai.com", 400 | "chat.openai.com.cdn.cloudflare.net", 401 | "ios.chat.openai.com", 402 | "o33249.ingest.sentry.io", 403 | "openai-api.arkoselabs.com", 404 | "openaicom-api-bdcpf8c6d2e9atf6.z01.azurefd.net", 405 | "openaicomproductionae4b.blob.core.windows.net", 406 | "production-openaicom-storage.azureedge.net", 407 | "static.cloudflareinsights.com" 408 | ], 409 | "domain_suffix": [ 410 | ".algolia.net", 411 | ".auth0.com", 412 | ".chatgpt.com", 413 | ".challenges.cloudflare.com", 414 | ".client-api.arkoselabs.com", 415 | ".events.statsigapi.net", 416 | ".featuregates.org", 417 | ".identrust.com", 418 | ".intercom.io", 419 | ".intercomcdn.com", 420 | ".launchdarkly.com", 421 | ".oaistatic.com", 422 | ".oaiusercontent.com", 423 | ".observeit.net", 424 | ".openai.com", 425 | ".openaiapi-site.azureedge.net", 426 | ".openaicom.imgix.net", 427 | ".segment.io", 428 | ".sentry.io", 429 | ".stripe.com" 430 | ], 431 | "domain_keyword": [ 432 | "openaicom-api" 433 | ], 434 | "outbound": "wireguard-ipv6-prefer-out" 435 | } 436 | ], 437 | "final": "direct" 438 | }, 439 | "experimental": { 440 | "cache_file": { 441 | "enabled": true, 442 | "path": "$work_dir/cache.db", 443 | "cache_id": "mycacheid", 444 | "store_fakeip": true 445 | } 446 | } 447 | } 448 | EOF 449 | } 450 | # debian/ubuntu/centos 守护进程 451 | main_systemd_services() { 452 | cat > /etc/systemd/system/sing-box.service << EOF 453 | [Unit] 454 | Description=sing-box service 455 | Documentation=https://sing-box.sagernet.org 456 | After=network.target nss-lookup.target 457 | 458 | [Service] 459 | User=root 460 | WorkingDirectory=/etc/sing-box 461 | CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW 462 | AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW 463 | ExecStart=/etc/sing-box/sing-box run -c /etc/sing-box/config.json 464 | ExecReload=/bin/kill -HUP \$MAINPID 465 | Restart=on-failure 466 | RestartSec=10 467 | LimitNOFILE=infinity 468 | 469 | [Install] 470 | WantedBy=multi-user.target 471 | EOF 472 | 473 | cat > /etc/systemd/system/argo.service << EOF 474 | [Unit] 475 | Description=Cloudflare Tunnel 476 | After=network.target 477 | 478 | [Service] 479 | Type=simple 480 | NoNewPrivileges=yes 481 | TimeoutStartSec=0 482 | ExecStart=/bin/sh -c "/etc/sing-box/argo tunnel --url http://localhost:8001 --no-autoupdate --edge-ip-version auto --protocol http2 > /etc/sing-box/argo.log 2>&1" 483 | Restart=on-failure 484 | RestartSec=5s 485 | 486 | [Install] 487 | WantedBy=multi-user.target 488 | EOF 489 | if [ -f /etc/centos-release ]; then 490 | yum install -y chrony 491 | systemctl start chronyd 492 | systemctl enable chronyd 493 | chronyc -a makestep 494 | yum update -y ca-certificates 495 | bash -c 'echo "0 0" > /proc/sys/net/ipv4/ping_group_range' 496 | fi 497 | systemctl daemon-reload 498 | systemctl enable sing-box 499 | systemctl start sing-box 500 | systemctl enable argo 501 | systemctl start argo 502 | } 503 | # 适配alpine 守护进程 504 | alpine_openrc_services() { 505 | cat > /etc/init.d/sing-box << 'EOF' 506 | #!/sbin/openrc-run 507 | 508 | description="sing-box service" 509 | command="/etc/sing-box/sing-box" 510 | command_args="run -c /etc/sing-box/config.json" 511 | command_background=true 512 | pidfile="/var/run/sing-box.pid" 513 | EOF 514 | 515 | cat > /etc/init.d/argo << 'EOF' 516 | #!/sbin/openrc-run 517 | 518 | description="Cloudflare Tunnel" 519 | command="/bin/sh" 520 | command_args="-c '/etc/sing-box/argo tunnel --url http://localhost:8001 --no-autoupdate --edge-ip-version auto --protocol http2 > /etc/sing-box/argo.log 2>&1'" 521 | command_background=true 522 | pidfile="/var/run/argo.pid" 523 | EOF 524 | 525 | chmod +x /etc/init.d/sing-box 526 | chmod +x /etc/init.d/argo 527 | rc-update add sing-box default 528 | rc-update add argo default 529 | 530 | } 531 | 532 | get_info() { 533 | clear 534 | server_ip=$(get_realip) 535 | isp=$(curl -s --max-time 2 https://speed.cloudflare.com/meta | awk -F\" '{print $26"-"$18}' | sed -e 's/ /_/g' || echo "vps") 536 | 537 | if [ -f "${work_dir}/argo.log" ]; then 538 | for i in {1..5}; do 539 | purple "第 $i 次尝试获取ArgoDomain中..." 540 | argodomain=$(sed -n 's|.*https://\([^/]*trycloudflare\.com\).*|\1|p' "${work_dir}/argo.log") 541 | [ -n "$argodomain" ] && break 542 | sleep 2 543 | done 544 | else 545 | restart_argo 546 | sleep 6 547 | argodomain=$(sed -n 's|.*https://\([^/]*trycloudflare\.com\).*|\1|p' "${work_dir}/argo.log") 548 | fi 549 | 550 | green "\nArgoDomain: ${purple}$argodomain${re}\n" 551 | 552 | VMESS="{ \"v\": \"2\", \"ps\": \"${isp}\", \"add\": \"${CFIP}\", \"port\": \"${CFPORT}\", \"id\": \"${uuid}\", \"aid\": \"0\", \"scy\": \"none\", \"net\": \"ws\", \"type\": \"none\", \"host\": \"${argodomain}\", \"path\": \"/vmess-argo?ed=2048\", \"tls\": \"tls\", \"sni\": \"${argodomain}\", \"alpn\": \"\", \"fp\": \"randomized\", \"allowlnsecure\": \"flase\"}" 553 | 554 | cat > ${work_dir}/url.txt < /dev/null 2>&1 753 | green "\nsing-box 卸载成功\n\n" && exit 0 754 | ;; 755 | *) 756 | purple "已取消卸载操作\n\n" 757 | ;; 758 | esac 759 | } 760 | 761 | # 创建快捷指令 762 | create_shortcut() { 763 | cat > "$work_dir/sb.sh" << EOF 764 | #!/usr/bin/env bash 765 | 766 | bash <(curl -Ls https://github.com/yutian81/Keepalive/raw/main/vps_sb5in1.sh) \$1 767 | EOF 768 | chmod +x "$work_dir/sb.sh" 769 | ln -sf "$work_dir/sb.sh" /usr/bin/sb 770 | if [ -s /usr/bin/sb ]; then 771 | green "\n快捷指令 sb 创建成功\n" 772 | else 773 | red "\n快捷指令创建失败\n" 774 | fi 775 | } 776 | 777 | # 适配alpine运行argo报错用户组和dns的问题 778 | change_hosts() { 779 | sh -c 'echo "0 0" > /proc/sys/net/ipv4/ping_group_range' 780 | sed -i '1s/.*/127.0.0.1 localhost/' /etc/hosts 781 | sed -i '2s/.*/::1 localhost/' /etc/hosts 782 | } 783 | 784 | # 变更配置 785 | change_config() { 786 | if [ ${check_singbox} -eq 0 ]; then 787 | clear 788 | echo "" 789 | green "1. 修改端口" 790 | skyblue "------------" 791 | green "2. 修改UUID" 792 | skyblue "------------" 793 | green "3. 修改Reality伪装域名" 794 | skyblue "------------" 795 | green "4. 添加hysteria2端口跳跃" 796 | skyblue "------------" 797 | green "5. 删除hysteria2端口跳跃" 798 | skyblue "------------" 799 | purple "${purple}6. 返回主菜单" 800 | skyblue "------------" 801 | reading "请输入选择: " choice 802 | case "${choice}" in 803 | 1) 804 | echo "" 805 | green "1. 修改vless-reality端口" 806 | skyblue "------------" 807 | green "2. 修改socks5端口" 808 | skyblue "------------" 809 | green "3. 修改hysteria2端口" 810 | skyblue "------------" 811 | green "4. 修改tuic端口" 812 | skyblue "------------" 813 | purple "5. 返回上一级菜单" 814 | skyblue "------------" 815 | reading "请输入选择: " choice 816 | case "${choice}" in 817 | 1) 818 | reading "\n请输入vless-reality端口 (回车跳过将使用随机端口): " new_port 819 | [ -z "$new_port" ] && new_port=$(shuf -i 2000-65000 -n 1) 820 | sed -i '/"type": "vless"/,/listen_port/ s/"listen_port": [0-9]\+/"listen_port": '"$new_port"'/' $config_dir 821 | restart_singbox 822 | sed -i 's/\(vless:\/\/[^@]*@[^:]*:\)[0-9]\{1,\}/\1'"$new_port"'/' $client_dir 823 | while IFS= read -r line; do yellow "$line"; done < ${work_dir}/url.txt 824 | green "\nvless-reality端口已修改成:${purple}$new_port${re} ${green}请手动更改vless-reality端口${re}\n" 825 | ;; 826 | 2) 827 | reading "\n请输入socks5端口 (回车跳过将使用默认的yutian): " new_port 828 | [ -z "$new_port" ] && new_port=$(shuf -i 2000-65000 -n 1) 829 | sed -i '/"type": "socks"/,/listen_port/ s/"listen_port": [0-9]\+/"listen_port": '"$new_port"'/' $config_dir 830 | restart_singbox 831 | sed -i 's/\(socks5:\/\/[^@]*@[^:]*:\)[0-9]\{1,\}/\1'"$new_port"'/' $client_dir 832 | while IFS= read -r line; do yellow "$line"; done < ${work_dir}/url.txt 833 | green "\nsock5端口已修改成:${purple}$new_port${re} ${green}请手动更改sock5端口${re}\n" 834 | ;; 835 | 3) 836 | reading "\n请输入hysteria2端口 (回车跳过将使用随机端口): " new_port 837 | [ -z "$new_port" ] && new_port=$(shuf -i 2000-65000 -n 1) 838 | sed -i '/"type": "hysteria2"/,/listen_port/ s/"listen_port": [0-9]\+/"listen_port": '"$new_port"'/' $config_dir 839 | restart_singbox 840 | sed -i 's/\(hysteria2:\/\/[^@]*@[^:]*:\)[0-9]\{1,\}/\1'"$new_port"'/' $client_dir 841 | while IFS= read -r line; do yellow "$line"; done < ${work_dir}/url.txt 842 | green "\nhysteria2端口已修改为:${purple}${new_port}${re} ${green}请手动更改hysteria2端口${re}\n" 843 | ;; 844 | 4) 845 | reading "\n请输入tuic端口 (回车跳过将使用随机端口): " new_port 846 | [ -z "$new_port" ] && new_port=$(shuf -i 2000-65000 -n 1) 847 | sed -i '/"type": "tuic"/,/listen_port/ s/"listen_port": [0-9]\+/"listen_port": '"$new_port"'/' $config_dir 848 | restart_singbox 849 | sed -i 's/\(tuic:\/\/[^@]*@[^:]*:\)[0-9]\{1,\}/\1'"$new_port"'/' $client_dir 850 | while IFS= read -r line; do yellow "$line"; done < ${work_dir}/url.txt 851 | green "\ntuic端口已修改为:${purple}${new_port}${re} ${green}请手动更改tuic端口${re}\n" 852 | ;; 853 | 5) change_config ;; 854 | *) red "无效的选项,请输入 1 到 4" ;; 855 | esac 856 | ;; 857 | 2) 858 | reading "\n请输入新的UUID: " new_uuid 859 | [ -z "$new_uuid" ] && new_uuid=$(cat /proc/sys/kernel/random/uuid) 860 | sed -i -E ' 861 | s/"uuid": "([a-f0-9-]+)"/"uuid": "'"$new_uuid"'"/g; 862 | s/"uuid": "([a-f0-9-]+)"$/\"uuid\": \"'$new_uuid'\"/g; 863 | s/"password": "([a-f0-9-]+)"/"password": "'"$new_uuid"'"/g 864 | ' $config_dir 865 | 866 | restart_singbox 867 | sed -i -E 's/(vless:\/\/|hysteria2:\/\/)[^@]*(@.*)/\1'"$new_uuid"'\2/' $client_dir 868 | sed -i "s/tuic:\/\/[0-9a-f\-]\{36\}/tuic:\/\/$new_uuid/" /etc/sing-box/url.txt  869 | isp=$(curl -s https://speed.cloudflare.com/meta | awk -F\" '{print $26"-"$18}' | sed -e 's/ /_/g') 870 | argodomain=$(grep -oE 'https://[[:alnum:]+\.-]+\.trycloudflare\.com' "${work_dir}/argo.log" | sed 's@https://@@') 871 | VMESS="{ \"v\": \"2\", \"ps\": \"${isp}\", \"add\": \"cloudflare.182682.xyz\", \"port\": \"8443\", \"id\": \"${new_uuid}\", \"aid\": \"0\", \"scy\": \"none\", \"net\": \"ws\", \"type\": \"none\", \"host\": \"${argodomain}\", \"path\": \"/vmess-argo?ed=2048\", \"tls\": \"tls\", \"sni\": \"${argodomain}\", \"alpn\": \"\", \"fp\": \"randomized\", \"allowlnsecure\": \"flase\"}" 872 | encoded_vmess=$(echo "$VMESS" | base64 -w0) 873 | sed -i -E '/vmess:\/\//{s@vmess://.*@vmess://'"$encoded_vmess"'@}' $client_dir 874 | while IFS= read -r line; do yellow "$line"; done < ${work_dir}/url.txt 875 | green "\nUUID已修改为:${purple}${new_uuid}${re} ${green}请手动更改所有节点的UUID${re}\n" 876 | ;; 877 | 3) 878 | clear 879 | green "\n1. www.joom.com\n\n2. www.stengg.com\n\n3. www.wedgehr.com\n\n4. www.cerebrium.ai\n\n5. www.nazhumi.com\n" 880 | reading "\n请输入新的Reality伪装域名(可自定义输入,回车留空将使用默认1): " new_sni 881 | if [ -z "$new_sni" ]; then 882 | new_sni="www.joom.com" 883 | elif [[ "$new_sni" == "1" ]]; then 884 | new_sni="www.joom.com" 885 | elif [[ "$new_sni" == "2" ]]; then 886 | new_sni="www.stengg.com" 887 | elif [[ "$new_sni" == "3" ]]; then 888 | new_sni="www.wedgehr.com" 889 | elif [[ "$new_sni" == "4" ]]; then 890 | new_sni="www.cerebrium.ai" 891 | elif [[ "$new_sni" == "5" ]]; then 892 | new_sni="www.nazhumi.com" 893 | else 894 | new_sni="$new_sni" 895 | fi 896 | jq --arg new_sni "$new_sni" ' 897 | (.inbounds[] | select(.type == "vless") | .tls.server_name) = $new_sni | 898 | (.inbounds[] | select(.type == "vless") | .tls.reality.handshake.server) = $new_sni 899 | ' "$config_dir" > "$config_file.tmp" && mv "$config_file.tmp" "$config_dir" 900 | restart_singbox 901 | sed -i "s/\(vless:\/\/[^\?]*\?\([^\&]*\&\)*sni=\)[^&]*/\1$new_sni/" $client_dir 902 | while IFS= read -r line; do yellow "$line"; done < ${work_dir}/url.txt 903 | echo "" 904 | green "\nReality sni已修改为:${purple}${new_sni}${re} ${green}请手动更改reality节点的sni域名${re}\n" 905 | ;; 906 | 4) 907 | purple "端口跳跃需确保跳跃区间的端口没有被占用,nat鸡请注意可用端口范围,否则可能造成节点不通\n" 908 | reading "请输入跳跃起始端口 (回车跳过将使用随机端口): " min_port 909 | [ -z "$min_port" ] && min_port=$(shuf -i 50000-65000 -n 1) 910 | yellow "你的起始端口为:$min_port" 911 | reading "\n请输入跳跃结束端口 (需大于起始端口): " max_port 912 | [ -z "$max_port" ] && max_port=$(($min_port + 100)) 913 | yellow "你的结束端口为:$max_port\n" 914 | purple "正在安装依赖,并设置端口跳跃规则中,请稍等...\n" 915 | listen_port=$(sed -n '/"tag": "hysteria2"/,/}/s/.*"listen_port": \([0-9]*\).*/\1/p' $config_dir) 916 | iptables -t nat -A PREROUTING -p udp --dport $min_port:$max_port -j DNAT --to-destination :$listen_port > /dev/null 917 | command -v ip6tables &> /dev/null && ip6tables -t nat -A PREROUTING -p udp --dport $min_port:$max_port -j DNAT --to-destination :$listen_port > /dev/null 918 | if [ -f /etc/alpine-release ]; then 919 | iptables-save > /etc/iptables/rules.v4 920 | command -v ip6tables &> /dev/null && ip6tables-save > /etc/iptables/rules.v6 921 | 922 | cat << 'EOF' > /etc/init.d/iptables 923 | #!/sbin/openrc-run 924 | 925 | depend() { 926 | need net 927 | } 928 | 929 | start() { 930 | [ -f /etc/iptables/rules.v4 ] && iptables-restore < /etc/iptables/rules.v4 931 | command -v ip6tables &> /dev/null && [ -f /etc/iptables/rules.v6 ] && ip6tables-restore < /etc/iptables/rules.v6 932 | } 933 | EOF 934 | 935 | chmod +x /etc/init.d/iptables && rc-update add iptables default && /etc/init.d/iptables start 936 | elif [ -f /etc/debian_version ]; then 937 | DEBIAN_FRONTEND=noninteractive apt install -y iptables-persistent > /dev/null 2>&1 && netfilter-persistent save > /dev/null 2>&1 938 | systemctl enable netfilter-persistent > /dev/null 2>&1 && systemctl start netfilter-persistent > /dev/null 2>&1 939 | elif [ -f /etc/redhat-release ]; then 940 | manage_packages install iptables-services > /dev/null 2>&1 && service iptables save > /dev/null 2>&1 941 | systemctl enable iptables > /dev/null 2>&1 && systemctl start iptables > /dev/null 2>&1 942 | command -v ip6tables &> /dev/null && service ip6tables save > /dev/null 2>&1 943 | systemctl enable ip6tables > /dev/null 2>&1 && systemctl start ip6tables > /dev/null 2>&1 944 | else 945 | red "未知系统,请自行将跳跃端口转发到主端口" && exit 1 946 | fi 947 | restart_singbox 948 | ip=$(get_realip) 949 | uuid=$(sed -n 's/.*hysteria2:\/\/\([^@]*\)@.*/\1/p' $client_dir) 950 | line_number=$(grep -n 'hysteria2://' $client_dir | cut -d':' -f1) 951 | isp=$(curl -s --max-time 2 https://speed.cloudflare.com/meta | awk -F\" '{print $26"-"$18}' | sed -e 's/ /_/g' || echo "vps") 952 | sed -i.bak "/hysteria2:/d" $client_dir 953 | sed -i "${line_number}i hysteria2://$uuid@$ip:$listen_port?peer=www.bing.com&insecure=1&alpn=h3&obfs=none&mport=$listen_port,$min_port-$max_port#$isp" $client_dir 954 | while IFS= read -r line; do yellow "$line"; done < ${work_dir}/url.txt 955 | green "\nhysteria2端口跳跃已开启,跳跃端口为:${purple}$min_port-$max_port${re} ${green}请手动复制以上hysteria2节点${re}\n" 956 | ;; 957 | 5) 958 | iptables -t nat -F PREROUTING > /dev/null 2>&1 959 | command -v ip6tables &> /dev/null && ip6tables -t nat -F PREROUTING > /dev/null 2>&1 960 | if [ -f /etc/alpine-release ]; then 961 | rc-update del iptables default && rm -rf /etc/init.d/iptables 962 | elif [ -f /etc/redhat-release ]; then 963 | netfilter-persistent save > /dev/null 2>&1 964 | elif [ -f /etc/redhat-release ]; then 965 | service iptables save > /dev/null 2>&1 966 | command -v ip6tables &> /dev/null && service ip6tables save > /dev/null 2>&1 967 | else 968 | manage_packages uninstall iptables ip6tables iptables-persistent iptables-service > /dev/null 2>&1 969 | fi 970 | sed -i '/hysteria2/s/&mport=[^#&]*//g' /etc/sing-box/url.txt 971 | green "\n端口跳跃已删除\n" 972 | ;; 973 | 6) menu ;; 974 | *) read "无效的选项!" ;; 975 | esac 976 | else 977 | yellow "sing-box 尚未安装!" 978 | sleep 1 979 | menu 980 | fi 981 | } 982 | 983 | # singbox 管理 984 | manage_singbox() { 985 | clear 986 | echo "" 987 | green "1. 启动sing-box服务" 988 | skyblue "-------------------" 989 | green "2. 停止sing-box服务" 990 | skyblue "-------------------" 991 | green "3. 重启sing-box服务" 992 | skyblue "-------------------" 993 | purple "4. 返回主菜单" 994 | skyblue "------------" 995 | reading "\n请输入选择: " choice 996 | case "${choice}" in 997 | 1) start_singbox ;; 998 | 2) stop_singbox ;; 999 | 3) restart_singbox ;; 1000 | 4) menu ;; 1001 | *) red "无效的选项!" ;; 1002 | esac 1003 | } 1004 | 1005 | # Argo 管理 1006 | manage_argo() { 1007 | if [ ${check_argo} -eq 2 ]; then 1008 | yellow "Argo 尚未安装!" 1009 | sleep 1 1010 | menu 1011 | else 1012 | clear 1013 | echo "" 1014 | green "1. 启动Argo服务" 1015 | skyblue "------------" 1016 | green "2. 停止Argo服务" 1017 | skyblue "------------" 1018 | green "3. 重启Argo服务" 1019 | skyblue "------------" 1020 | green "4. 添加Argo固定隧道" 1021 | skyblue "----------------" 1022 | green "5. 切换回Argo临时隧道" 1023 | skyblue "------------------" 1024 | green "6. 重新获取Argo临时域名" 1025 | skyblue "-------------------" 1026 | purple "7. 返回主菜单" 1027 | skyblue "-----------" 1028 | reading "\n请输入选择: " choice 1029 | case "${choice}" in 1030 | 1) start_argo ;; 1031 | 2) stop_argo ;; 1032 | 3) clear 1033 | if [ -f /etc/alpine-release ]; then 1034 | grep -Fq -- '--url http://localhost:8001' /etc/init.d/argo && get_quick_tunnel && change_argo_domain || { green "\n当前使用固定隧道,无需获取临时域名"; sleep 2; menu; } 1035 | else 1036 | grep -q 'ExecStart=.*--url http://localhost:8001' /etc/systemd/system/argo.service && get_quick_tunnel && change_argo_domain || { green "\n当前使用固定隧道,无需获取临时域名"; sleep 2; menu; } 1037 | fi 1038 | ;; 1039 | 4) 1040 | clear 1041 | yellow "\n固定隧道可为json或token,固定隧道端口为8001,自行在cf后台设置\n\njson在f佬维护的站点里获取,获取地址:${purple}https://fscarmen.cloudflare.now.cc${re}\n" 1042 | reading "\n请输入你的argo域名: " argo_domain 1043 | ArgoDomain=$argo_domain 1044 | reading "\n请输入你的argo密钥(token或json): " argo_auth 1045 | if [[ $argo_auth =~ TunnelSecret ]]; then 1046 | echo $argo_auth > ${work_dir}/tunnel.json 1047 | cat > ${work_dir}/tunnel.yml << EOF 1048 | tunnel: $(cut -d\" -f12 <<< "$argo_auth") 1049 | credentials-file: ${work_dir}/tunnel.json 1050 | protocol: http2 1051 | 1052 | ingress: 1053 | - hostname: $ArgoDomain 1054 | service: http://localhost:8001 1055 | originRequest: 1056 | noTLSVerify: true 1057 | - service: http_status:404 1058 | EOF 1059 | 1060 | if [ -f /etc/alpine-release ]; then 1061 | sed -i '/^command_args=/c\command_args="-c '\''/etc/sing-box/argo tunnel --edge-ip-version auto --config /etc/sing-box/tunnel.yml run 2>&1'\''"' /etc/init.d/argo 1062 | else 1063 | sed -i '/^ExecStart=/c ExecStart=/bin/sh -c "/etc/sing-box/argo tunnel --edge-ip-version auto --config /etc/sing-box/tunnel.yml run 2>&1"' /etc/systemd/system/argo.service 1064 | fi 1065 | restart_argo 1066 | sleep 1 1067 | change_argo_domain 1068 | 1069 | elif [[ $argo_auth =~ ^[A-Z0-9a-z=]{120,250}$ ]]; then 1070 | if [ -f /etc/alpine-release ]; then 1071 | sed -i "/^command_args=/c\command_args=\"-c '/etc/sing-box/argo tunnel --edge-ip-version auto --no-autoupdate --protocol http2 run --token $argo_auth 2>&1'\"" /etc/init.d/argo 1072 | else 1073 | 1074 | sed -i '/^ExecStart=/c ExecStart=/bin/sh -c "/etc/sing-box/argo tunnel --edge-ip-version auto --no-autoupdate --protocol http2 run --token '$argo_auth' 2>&1"' /etc/systemd/system/argo.service 1075 | fi 1076 | restart_argo 1077 | sleep 1 1078 | change_argo_domain 1079 | else 1080 | yellow "你输入的argo域名或token不匹配,请重新输入" 1081 | manage_argo 1082 | fi 1083 | ;; 1084 | 5) 1085 | clear 1086 | if [ -f /etc/alpine-release ]; then 1087 | alpine_openrc_services 1088 | else 1089 | main_systemd_services 1090 | fi 1091 | get_quick_tunnel 1092 | change_argo_domain 1093 | ;; 1094 | 1095 | 6) 1096 | if [ -f /etc/alpine-release ]; then 1097 | if grep -Fq -- '--url http://localhost:8001' /etc/init.d/argo; then 1098 | get_quick_tunnel 1099 | change_argo_domain 1100 | else 1101 | yellow "当前使用固定隧道,无法获取临时隧道" 1102 | sleep 2 1103 | menu 1104 | fi 1105 | else 1106 | if grep -q 'ExecStart=.*--url http://localhost:8001' /etc/systemd/system/argo.service; then 1107 | get_quick_tunnel 1108 | change_argo_domain 1109 | else 1110 | yellow "当前使用固定隧道,无法获取临时隧道" 1111 | sleep 2 1112 | menu 1113 | fi 1114 | fi 1115 | ;; 1116 | 7) menu ;; 1117 | *) red "无效的选项!" ;; 1118 | esac 1119 | fi 1120 | } 1121 | 1122 | # 获取argo临时隧道 1123 | get_quick_tunnel() { 1124 | restart_argo 1125 | yellow "获取临时argo域名中,请稍等...\n" 1126 | sleep 3 1127 | if [ -f /etc/sing-box/argo.log ]; then 1128 | for i in {1..5}; do 1129 | purple "第 $i 次尝试获取ArgoDoamin中..." 1130 | get_argodomain=$(sed -n 's|.*https://\([^/]*trycloudflare\.com\).*|\1|p' /etc/sing-box/argo.log) 1131 | [ -n "$get_argodomain" ] && break 1132 | sleep 2 1133 | done 1134 | else 1135 | restart_argo 1136 | sleep 6 1137 | get_argodomain=$(sed -n 's|.*https://\([^/]*trycloudflare\.com\).*|\1|p' /etc/sing-box/argo.log) 1138 | fi 1139 | green "ArgoDomain:${purple}$get_argodomain${re}\n" 1140 | ArgoDomain=$get_argodomain 1141 | } 1142 | 1143 | # 更新Argo域名到节点链接 1144 | change_argo_domain() { 1145 | content=$(cat "$client_dir") 1146 | vmess_url=$(grep -o 'vmess://[^ ]*' "$client_dir") 1147 | vmess_prefix="vmess://" 1148 | encoded_vmess="${vmess_url#"$vmess_prefix"}" 1149 | decoded_vmess=$(echo "$encoded_vmess" | base64 --decode) 1150 | updated_vmess=$(echo "$decoded_vmess" | jq --arg new_domain "$ArgoDomain" '.host = $new_domain | .sni = $new_domain') 1151 | encoded_updated_vmess=$(echo "$updated_vmess" | base64 | tr -d '\n') 1152 | new_vmess_url="$vmess_prefix$encoded_updated_vmess" 1153 | new_content=$(echo "$content" | sed "s|$vmess_url|$new_vmess_url|") 1154 | echo "$new_content" > "$client_dir" 1155 | green "vmess节点已更新,请手动复制以下vmess-argo节点\n" 1156 | purple "$new_vmess_url\n" 1157 | } 1158 | 1159 | # 查看节点信息 1160 | check_nodes() { 1161 | if [ ${check_singbox} -eq 0 ]; then 1162 | while IFS= read -r line; do purple "${purple}$line"; done < ${work_dir}/url.txt 1163 | else 1164 | yellow "sing-box 尚未安装或未运行,请先安装或启动sing-box" 1165 | sleep 1 1166 | menu 1167 | fi 1168 | } 1169 | 1170 | # 主菜单 1171 | menu() { 1172 | check_singbox &>/dev/null; check_singbox=$? 1173 | check_argo &>/dev/null; check_argo=$? 1174 | check_singbox_status=$(check_singbox) > /dev/null 2>&1 1175 | check_argo_status=$(check_argo) > /dev/null 2>&1 1176 | clear 1177 | echo "" 1178 | purple "=== 老王sing-box一键安装脚本 ===\n" 1179 | purple "---Argo--- 状态: ${check_argo_status}" 1180 | purple "---singbox--- 状态: ${check_singbox_status}\n" 1181 | green "1. 安装sing-box" 1182 | red "2. 卸载sing-box" 1183 | echo "===============" 1184 | green "3. sing-box管理" 1185 | green "4. Argo隧道管理" 1186 | echo "===============" 1187 | green "5. 查看节点信息" 1188 | green "6. 修改节点配置" 1189 | echo "===============" 1190 | purple "7. ssh综合工具箱" 1191 | echo "===============" 1192 | red "0. 退出脚本" 1193 | echo "===========" 1194 | reading "请输入选择(0-7): " choice 1195 | echo "" 1196 | } 1197 | 1198 | # 捕获 Ctrl+C 信号 1199 | trap 'red "已取消操作"; exit' INT 1200 | 1201 | # 主循环 1202 | while true; do 1203 | menu 1204 | case "${choice}" in 1205 | 1) 1206 | if [ ${check_singbox} -eq 0 ]; then 1207 | yellow "sing-box 已经安装!" 1208 | else 1209 | manage_packages install jq tar openssl iptables 1210 | [ -n "$(curl -s --max-time 2 ipv6.ip.sb)" ] && manage_packages install ip6tables 1211 | install_singbox 1212 | 1213 | if [ -x "$(command -v systemctl)" ]; then 1214 | main_systemd_services 1215 | elif [ -x "$(command -v rc-update)" ]; then 1216 | alpine_openrc_services 1217 | change_hosts 1218 | rc-service sing-box restart 1219 | rc-service argo restart 1220 | else 1221 | echo "Unsupported init system" 1222 | exit 1 1223 | fi 1224 | 1225 | sleep 5 1226 | get_info 1227 | create_shortcut 1228 | fi 1229 | ;; 1230 | 2) uninstall_singbox ;; 1231 | 3) manage_singbox ;; 1232 | 4) manage_argo ;; 1233 | 5) check_nodes ;; 1234 | 6) change_config ;; 1235 | 7) 1236 | clear 1237 | curl -fsSL https://raw.githubusercontent.com/eooce/ssh_tool/main/ssh_tool.sh -o ssh_tool.sh && chmod +x ssh_tool.sh && bash ssh_tool.sh 1238 | ;; 1239 | 0) exit 0 ;; 1240 | *) red "无效的选项,请输入 0 到 8" ;; 1241 | esac 1242 | read -n 1 -s -r -p $'\033[1;91m按任意键继续...\033[0m' 1243 | done 1244 | -------------------------------------------------------------------------------- /webfreecloud/README.md: -------------------------------------------------------------------------------- 1 | ## action自动脚本需要配置的变量 2 | 3 | - SOCKS5_HOST # 代理服务器地址 4 | - SOCKS5_PORT # 代理端口 5 | - SOCKS5_USER # 代理用户名(如果有) 6 | - SOCKS5_PASS # 代理用户名(如果有) 7 | - TG_BOT_TOKEN # tg机器人token 8 | - TG_CHAT_ID # tg机器人ID 9 | - USER_CONFIGS_JSON # json格式的虚拟主机登录用户名密码 10 | 11 | **USER_CONFIGS_JSON 格式示例** 12 | 13 | ```json 14 | [ 15 | { 16 | "username": "您的账号", 17 | "password": "您的密码", 18 | "expected_text": "账户信息中的特征文本" 19 | }, 20 | { 21 | "username": "另一个账号", 22 | "password": "另一个密码", 23 | "expected_text": "其他特征文本" 24 | } 25 | ] 26 | ``` 27 | 28 | **expected_text 内容示例** 29 | 30 | `#1683 Yanlin Liu` 31 | 32 | 即登陆后控制面板主页面显示的 `#订单编号+注册时的姓名`,用于验证登录是否成功 33 | -------------------------------------------------------------------------------- /webfreecloud/login.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | import re 5 | from bs4 import BeautifulSoup 6 | from datetime import datetime, timedelta 7 | import time 8 | from urllib.parse import urlparse 9 | 10 | # ---------------------------- 配置区域 ---------------------------- 11 | TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN", "") 12 | TG_CHAT_ID = os.getenv("TG_CHAT_ID", "" ) 13 | USER_CONFIGS = json.loads(os.getenv("USER_CONFIGS_JSON")) 14 | LOGIN_URL = 'https://web.freecloud.ltd/index.php?rp=/login' 15 | DASHBOARD_URL = 'https://web.freecloud.ltd/clientarea.php' 16 | HEADERS = { 17 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 18 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 19 | 'Chrome/91.0.4472.124 Safari/537.36' 20 | } 21 | # --------------------------------------------------------------- 22 | 23 | # 获取北京时间 24 | def get_beijing_time() -> str: 25 | utc_now = datetime.utcnow() 26 | return (utc_now + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S') 27 | 28 | # 发送Telegram通知 29 | def send_telegram_alert(username: str, is_success: bool, error_msg: str = None) -> None: 30 | timestamp = get_beijing_time() 31 | status = "✅ 验证成功" if is_success else "❌ 验证失败" 32 | message = ( 33 | f"*📩 WebFreeCloud 登录验证通知* \n\n" 34 | f"🔐 账户: `{username}` \n" 35 | f"🛡️ 状态: {status} \n" 36 | f"🕒 时间: {timestamp}" 37 | ) 38 | 39 | if not is_success and error_msg: 40 | message += f"\n📊 错误原因: `{error_msg}`" 41 | 42 | if not TG_BOT_TOKEN or not TG_CHAT_ID: 43 | return 44 | 45 | try: 46 | response = requests.post( 47 | f'https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage', 48 | json={ 49 | 'chat_id': TG_CHAT_ID, 50 | 'text': message, 51 | 'parse_mode': 'Markdown' 52 | }, 53 | timeout=10 54 | ) 55 | response.raise_for_status() 56 | except Exception as e: 57 | print(f"⚠️ Telegram通知发送失败: {str(e)}") 58 | 59 | # 执行用户验证,返回是否成功,及错误信息) 60 | def validate_user(session: requests.Session, user: dict) -> tuple: 61 | try: 62 | socks_proxy = os.getenv('SOCKS_PROXY') 63 | if socks_proxy: 64 | session.proxies = { 65 | 'http': socks_proxy, 66 | 'https': socks_proxy 67 | } 68 | print(f"🔧 使用 SOCKS5 代理: {socks_proxy}") 69 | 70 | print(f"\n🔑 开始验证用户: {user['username']}") 71 | 72 | # 获取登录页面 73 | login_page = session.get(LOGIN_URL) 74 | login_page.raise_for_status() 75 | 76 | # 提取CSRF Token 77 | csrf_match = re.search(r"var\s+csrfToken\s*=\s*'([a-f0-9]+)'", login_page.text) 78 | if not csrf_match: 79 | return (False, "CSRF Token提取失败") 80 | 81 | # 构造登录请求 82 | login_data = { 83 | 'username': user['username'], 84 | 'password': user['password'], 85 | 'token': csrf_match.group(1), 86 | 'rememberme': 'on' 87 | } 88 | login_res = session.post(LOGIN_URL, data=login_data) 89 | 90 | # 验证跳转 91 | parsed_url = urlparse(login_res.url) 92 | if parsed_url.path != urlparse(DASHBOARD_URL).path: 93 | return (False, f"异常跳转至 {login_res.url}") 94 | 95 | # 提取用户信息 96 | dashboard_page = session.get(DASHBOARD_URL) 97 | soup = BeautifulSoup(dashboard_page.text, 'html.parser') 98 | 99 | # 定位信息元素 100 | if not (panel := soup.find('div', class_='panel-body')): 101 | return (False, "未找到用户信息面板") 102 | 103 | if not (strong_tag := panel.find('strong')): 104 | return (False, "未找到信息标签") 105 | 106 | # 验证文本内容 107 | actual_info = strong_tag.get_text(strip=True) 108 | if user['expected_text'] not in actual_info: 109 | return (False, f"信息不匹配 | 期望: {user['expected_text']} | 实际: {actual_info}") 110 | 111 | return (True, None) 112 | 113 | except requests.exceptions.RequestException as e: 114 | return (False, f"网络请求异常: {str(e)}") 115 | except Exception as e: 116 | return (False, f"系统错误: {str(e)}") 117 | 118 | # 主流程 119 | def main(): 120 | # 环境变量校验 121 | required_vars = ["USER_CONFIGS_JSON"] 122 | missing = [var for var in required_vars if not os.getenv(var)] 123 | if missing: 124 | raise ValueError(f"缺失环境变量: {', '.join(missing)}") 125 | if not TG_BOT_TOKEN or not TG_CHAT_ID: 126 | print("⚠️ 未配置 Telegram 通知参数,结果将不会推送") 127 | 128 | try: 129 | global USER_CONFIGS 130 | USER_CONFIGS = json.loads(os.getenv("USER_CONFIGS_JSON")) 131 | except Exception as e: 132 | raise ValueError(f"用户配置解析失败: {str(e)}") 133 | 134 | total_users = len(USER_CONFIGS) 135 | print(f"✅ 加载用户数量: {total_users}") 136 | print("🔔 开始批量验证用户...") 137 | 138 | for idx, user in enumerate(USER_CONFIGS, 1): 139 | start_time = time.time() 140 | with requests.Session() as session: 141 | session.headers.update(HEADERS) 142 | username = user['username'] 143 | 144 | # 执行验证 145 | success, error_msg = validate_user(session, user) 146 | duration = time.time() - start_time 147 | 148 | # 发送通知 149 | send_telegram_alert(username, success, error_msg) 150 | 151 | # 控制台输出 152 | status = "成功" if success else f"失败 ({error_msg})" 153 | print(f"🔄 [{idx}/{total_users}] {username} 验证{status} [耗时: {duration:.2f}s]") 154 | 155 | print("\n🔔 所有用户验证完成") 156 | 157 | if __name__ == "__main__": 158 | main() 159 | --------------------------------------------------------------------------------