├── .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 | Account |
147 | Type |
148 | Status |
149 | Message |
150 | Last Run |
151 |
152 |
153 |
154 |
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 | 
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 |
--------------------------------------------------------------------------------