From ae9531f2c22994bc78c87d8fd5eb07d2c2731c44 Mon Sep 17 00:00:00 2001 From: "DESKTOP-77LRNCU\\dsx2016" Date: Wed, 8 Jul 2026 12:47:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E7=BB=9C=E9=A1=B5=E7=84=95=E6=96=B0?= =?UTF-8?q?=EF=BC=9AIP=20=E7=94=BB=E5=83=8F/=E8=B4=A8=E9=87=8F=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E3=80=81=E9=93=BE=E8=B7=AF=E8=AF=8A=E6=96=AD=E4=B8=8E?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 /ip_profile、/ip_quality API 与网络 UI 组件,默认隐藏摄像头/拉流导航;同步更新 README。 Co-authored-by: Cursor --- README.md | 85 +- electron/pnpm-lock.yaml | 153 - electron/src/main/appHost.ts | 11 +- electron/src/renderer/src/App.css | 223 ++ electron/src/renderer/src/App.tsx | 43 +- .../src/renderer/src/NetworkDashboard.tsx | 591 +++- electron/src/renderer/src/ProxySettings.tsx | 129 +- electron/src/renderer/src/SystemMetrics.tsx | 274 +- .../src/components/network/IpProfilePanel.tsx | 188 ++ .../src/components/network/IpQualityPanel.tsx | 270 ++ .../src/components/network/NetworkIcons.tsx | 124 + .../components/network/NetworkPipeline.tsx | 98 + .../components/network/NetworkSpeedChart.tsx | 135 + .../src/renderer/src/hooks/useWebRtcLeak.ts | 72 + electron/src/renderer/src/main.tsx | 1 + electron/src/renderer/src/network-polish.css | 1414 +++++++++ electron/src/shared/featureFlags.ts | 29 + third_party/IPQuality/ip.sh | 2642 +++++++++++++++++ web2.py | 64 +- web2_feature_flags.py | 22 + web2_ip_profile.py | 432 +++ web2_ip_quality.py | 414 +++ web2_system_metrics.py | 95 +- 23 files changed, 6966 insertions(+), 543 deletions(-) create mode 100644 electron/src/renderer/src/components/network/IpProfilePanel.tsx create mode 100644 electron/src/renderer/src/components/network/IpQualityPanel.tsx create mode 100644 electron/src/renderer/src/components/network/NetworkIcons.tsx create mode 100644 electron/src/renderer/src/components/network/NetworkPipeline.tsx create mode 100644 electron/src/renderer/src/components/network/NetworkSpeedChart.tsx create mode 100644 electron/src/renderer/src/hooks/useWebRtcLeak.ts create mode 100644 electron/src/renderer/src/network-polish.css create mode 100644 electron/src/shared/featureFlags.ts create mode 100644 third_party/IPQuality/ip.sh create mode 100644 web2_feature_flags.py create mode 100644 web2_ip_profile.py create mode 100644 web2_ip_quality.py diff --git a/README.md b/README.md index 3d5e0e6..d4c1342 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Windows 桌面端 **抖音 → YouTube 无人转播** 控制台:内置 Electro - [web2 API](#web2-api) - [配置与数据路径](#配置与数据路径) - [单频道与多频道 Pro](#单频道与多频道-pro) +- [网络诊断 / IP 画像 / 代理](#网络诊断--ip-画像--代理) - [推流诊断 / 质量守卫 / 转播历史](#推流诊断--质量守卫--转播历史) - [youLIVE 目标端监控](#youlive-目标端监控) - [摄像头直推](#摄像头直推) @@ -116,6 +117,10 @@ uvicorn web2:app --host 127.0.0.1 --port 8001 --reload | 控制面 | `web2.py` | HTTP API | | 运行时诊断 | `web2_process_runtime.py` | `business_status`、FFmpeg 速度/帧数 | | 质量守卫 | `web2_quality_guard.py` | 推流质量评分 | +| 网络测速 | `web2_network.py` | 境内外可达性、上下行测速、分流判断 | +| IP 画像 | `web2_ip_profile.py` | 出口 IP 类型(住宅/机房/代理)、洁净度评分 | +| IP 质量 | `web2_ip_quality.py` | 代理/VPN/机房风险、流媒体解锁探测 | +| 功能开关 | `web2_feature_flags.py` | 摄像头/拉流 API 与 worker 显隐 | | 转播历史 | `web2_relay_log.py` | 会话 SQLite、日志导入 | | 配置治理 | `web2_config_governance.py` | 保存前备份、恢复 | | 多频道 | `web2_relay_pro.py` | `youtube_channels.json`、`config/relay_pro/` | @@ -135,11 +140,14 @@ uvicorn web2:app --host 127.0.0.1 --port 8001 --reload │ ├── src/main/ # 主进程 │ ├── src/preload/ # contextBridge │ ├── src/renderer/ # UI(直播 / 摄像头 / 拉流 / 网络 / 系统) -│ │ └── components/worker/ # 业务工作台(链路条 / 清单 / 指标) +│ │ ├── components/worker/ # 业务工作台(链路条 / 清单 / 指标) +│ │ └── components/network/ # 网络页(链路条 / IP 画像 / IP 质量 / 测速图) +│ ├── src/shared/ # 前后端共享常量(功能开关 featureFlags) │ ├── src/youlive/ # 内嵌 youLIVE(主进程服务 + 共享类型) │ └── scripts/ # 离线运行时、图标 ├── web2.py # FastAPI 入口 -├── web2_*.py # 后端子模块 +├── web2_*.py # 后端子模块(含 ip_profile / ip_quality / feature_flags) +├── third_party/IPQuality/ # IP 质量检测脚本参考(ip.sh) ├── config/ # 运行配置 ├── backup_config/__web__/ # 配置自动备份 ├── runtime/youtube_relay/ # 转播会话 sessions.sqlite @@ -161,8 +169,8 @@ uvicorn web2:app --host 127.0.0.1 --port 8001 --reload | **直播** | 抖音源站无人转播:单频道 / Pro、链路诊断、转播历史、youLIVE 目标端监控 | | **摄像头** | 本机采集直推:设备预览、采集/编码配置、独立控制台(进程 `camera_youtube`) | | **拉流** | 外部流转推:RTMP/SRT/HLS/FLV/WHEP 拉流、转码或流复制(进程 `pull_stream_youtube`) | -| **网络** | 测速、代理 | -| **系统** | 负载、开机启动、路径、会员登录限制开关 | +| **网络** | 链路诊断、境内外测速、IP 画像与质量、WebRTC 泄露检测、FFmpeg 代理 | +| **系统** | CPU/内存/磁盘/GPU、开机启动、路径、会员登录限制开关 | **直播** 页面向抖音无人转播业务,含 Pro 多频道、三层链路诊断、转播历史等。 @@ -194,6 +202,19 @@ uvicorn web2:app --host 127.0.0.1 --port 8001 --reload - **源站预览**:解析抖音 URL 并尝试显示主播昵称 - **配对校验**:Pro 多线路保存时检测抖音地址 / YouTube 密钥跨频道重复,冲突时展示横幅并阻止启动 +**网络** 页面向转播前的出口与链路自检,含: + +- **链路快检**:国内源站(抖音)/ Google / 线路分流 / 海外上行 / 录制代理五项状态芯片 +- **NetworkPipeline**:源站 → 分流 → YouTube → 上行四段链路条,按可达性与测速结果着色 +- **境内外站点卡**:Google 与抖音的延迟、上下行测速、解析 IP 与地区 +- **IP 出口画像**(`GET /ip_profile`):本机默认出口与海外出口的类型(住宅/机房/代理/移动)、洁净度评分、WebRTC 本地泄露参考 +- **IP 质量检测**(`GET /ip_quality`):代理/VPN/Tor/机房风险因子、Abuser 评分、YouTube/Netflix 等流媒体解锁状态 +- **测速趋势图**:`NetworkSpeedChart` 记录近期上下行采样 +- **FFmpeg 代理**:录制/推流专用 HTTP/SOCKS 代理,与系统代理设置联动 +- **自动刷新**:约 45s 静默轮询;WebRTC 检测完成后自动刷新 IP 画像 + +> 摄像头 / 拉流功能默认在导航中**隐藏**(见 [功能开关](#功能开关)),代码与 API 仍保留,可按需开启。 + --- ## web2 API @@ -220,7 +241,9 @@ uvicorn web2:app --host 127.0.0.1 --port 8001 --reload | 历史 | `GET /youtube/relay/sessions` | 转播会话 | | 历史 | `POST /youtube/relay/sessions/import` | 从 PM2 日志导入 | | 网络 | `GET /network_status` `GET/POST /proxy_config` | 测速与代理 | -| 系统 | `GET /system_metrics` `GET /host/gpu_status` | 主机与 NVENC | +| 网络 | `GET /ip_profile` | 出口 IP 画像(可选 `client_tz`、`client_lang`、`webrtc_leak`) | +| 网络 | `GET /ip_quality` | IP 质量与流媒体解锁探测 | +| 系统 | `GET /system_metrics` `GET /host/gpu_status` | 主机、磁盘、NVENC | | OAuth | `GET /youtube/oauth/*` | Google 拉取串流密钥(UI 可关) | **可选鉴权**:`config/web2_api_token.txt` 或 `WEB2_API_TOKEN` 存在时,除 `/health` 外需: @@ -287,6 +310,42 @@ Electron 主进程自动注入 API 鉴权头。 --- +## 网络诊断 / IP 画像 / 代理 + +**网络** 页帮助在开播前确认「抖音源站可达 + 海外出口可用 + 上行带宽足够」。 + +### 链路诊断(`GET /network_status`) + +| 检测项 | 说明 | +|--------|------| +| 国内源站 | 抖音域名可达性与延迟 | +| Google 出口 | YouTube 推流路径可达性 | +| 线路分流 | 对比境内外解析 IP,判断代理/TUN 是否生效 | +| 海外测速 | 参考上下行 Mbps(测速失败不代表无法推流) | +| 录制代理 | `config/ffmpeg_proxy.ini` 是否已配置 | + +### IP 出口画像(`GET /ip_profile`) + +参考 whoer / ping0.cc 维度,聚合 ip-api.com、ping0.cc 等数据源: + +- **默认出口**与**海外出口**(经 Google 探测路径采样)的类型标签 +- 住宅 / 机房 / 代理 / 移动判定与洁净度评分 +- 浏览器侧 **WebRTC 本地候选**检测(`useWebRtcLeak`),结果通过 `webrtc_leak` 参数回传后端 + +### IP 质量检测(`GET /ip_quality`) + +集成 [xykt/IPQuality](https://github.com/xykt/IPQuality) 思路(参考 `third_party/IPQuality/ip.sh`): + +- 代理、VPN、Tor、机房等风险因子 +- 多源 Abuser / IPQS 评分 +- YouTube、TikTok、Netflix、Disney+ 等流媒体解锁状态 + +### FFmpeg 代理 + +`GET/POST /proxy_config` 读写 `config/ffmpeg_proxy.ini`,供 `youtube2` 等 worker 拉流时使用,与 Windows 系统代理设置独立。 + +--- + ## 推流诊断 / 质量守卫 / 转播历史 `GET /status` 的 `business_status` 示例: @@ -398,6 +457,19 @@ Electron 主进程自动注入 API 鉴权头。 --- +## 功能开关 + +摄像头直推与外部拉流转推默认**关闭**(导航隐藏、相关 API 返回 404、PM2 忽略对应 worker),以降低首屏复杂度。需要时分别修改: + +| 位置 | 常量 | 默认 | +|------|------|------| +| `electron/src/shared/featureFlags.ts` | `SHOW_CAMERA_UI`、`SHOW_PULL_STREAM_UI` | `false` | +| `web2_feature_flags.py` | `SHOW_CAMERA_FEATURE`、`SHOW_PULL_STREAM_FEATURE` | `False` | + +前后端开关需保持一致。深链 `--nav=camera` / `--nav=pull` 在关闭时会回落到 **直播** 页。 + +--- + ## 开发与构建 | 组件 | 版本 | @@ -453,6 +525,9 @@ pnpm dist | 拉流连接失败 | 核对协议与 URL;SRT 检查 caller/streamid;WebRTC 建议经 SRS 转 RTMP | | 拉流有画面 YouTube 无 | 流复制模式下源可能非 H.264+AAC,改转码模式 | | 目标端监听无数据 | 直播页确认 `youtubeWatchUrl` 已填写;youLIVE 桌面通知已开启 | +| Google 不可达 / 分流未生效 | **网络** 页查看链路快检;配置 TUN/规则代理;填写 FFmpeg 代理 | +| IP 画像显示机房/代理 | 更换住宅出口或调整代理节点;关注 WebRTC 泄露提示 | +| IP 质量检测超时 | 外网 API 偶发慢;稍后刷新;检查本机防火墙 | | 配置误改 | 直播页 **历史备份** 或 `POST /config/restore` | 日志: diff --git a/electron/pnpm-lock.yaml b/electron/pnpm-lock.yaml index bde21fe..72b98f5 100644 --- a/electron/pnpm-lock.yaml +++ b/electron/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: lucide-react: specifier: ^0.525.0 version: 0.525.0(react@18.3.1) - qrcode: - specifier: ^1.5.4 - version: 1.5.4 react: specifier: ^18.3.1 version: 18.3.1 @@ -54,9 +51,6 @@ importers: '@types/node': specifier: ^22.10.0 version: 22.19.15 - '@types/qrcode': - specifier: ^1.5.6 - version: 1.5.6 '@types/react': specifier: ^18.3.12 version: 18.3.28 @@ -758,9 +752,6 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/qrcode@1.5.6': - resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} - '@types/react-dom@18.3.7': resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} peerDependencies: @@ -969,10 +960,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - caniuse-lite@1.0.30001781: resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} @@ -1007,9 +994,6 @@ packages: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} - cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1225,10 +1209,6 @@ packages: supports-color: optional: true - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -1265,9 +1245,6 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - dijkstrajs@1.0.3: - resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} @@ -1503,10 +1480,6 @@ packages: filelist@1.0.6: resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1794,10 +1767,6 @@ packages: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -2023,33 +1992,17 @@ packages: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -2081,10 +2034,6 @@ packages: engines: {node: '>=8'} hasBin: true - pngjs@5.0.0: - resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} - engines: {node: '>=10.13.0'} - pngjs@6.0.0: resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} engines: {node: '>=12.13.0'} @@ -2119,11 +2068,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qrcode@1.5.4: - resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} - engines: {node: '>=10.13.0'} - hasBin: true - quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -2159,9 +2103,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - resedit@1.7.2: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} @@ -2444,9 +2385,6 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2455,10 +2393,6 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2474,9 +2408,6 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} - y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2487,18 +2418,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} - yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -3163,10 +3086,6 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/qrcode@1.5.6': - dependencies: - '@types/node': 22.19.15 - '@types/react-dom@18.3.7(@types/react@18.3.28)': dependencies: '@types/react': 18.3.28 @@ -3468,8 +3387,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - camelcase@5.3.1: {} - caniuse-lite@1.0.30001781: {} chalk@4.1.2: @@ -3497,12 +3414,6 @@ snapshots: string-width: 4.2.3 optional: true - cliui@6.0.0: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -3732,8 +3643,6 @@ snapshots: dependencies: ms: 2.1.3 - decamelize@1.2.0: {} - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -3771,8 +3680,6 @@ snapshots: detect-node@2.1.0: optional: true - dijkstrajs@1.0.3: {} - dir-compare@4.2.0: dependencies: minimatch: 3.1.5 @@ -3989,11 +3896,6 @@ snapshots: dependencies: minimatch: 5.1.9 - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4312,10 +4214,6 @@ snapshots: dependencies: readable-stream: 2.3.8 - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - lodash.defaults@4.2.0: {} lodash.difference@4.5.0: {} @@ -4543,28 +4441,16 @@ snapshots: p-cancelable@2.1.1: {} - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-map@4.0.0: dependencies: aggregate-error: 3.1.0 - p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -4592,8 +4478,6 @@ snapshots: minimist: 1.2.8 pngjs: 6.0.0 - pngjs@5.0.0: {} - pngjs@6.0.0: {} postcss@8.5.8: @@ -4620,12 +4504,6 @@ snapshots: punycode@2.3.1: {} - qrcode@1.5.4: - dependencies: - dijkstrajs: 1.0.3 - pngjs: 5.0.0 - yargs: 15.4.1 - quick-lru@5.1.1: {} react-dom@18.3.1(react@18.3.1): @@ -4668,8 +4546,6 @@ snapshots: require-directory@2.1.1: {} - require-main-filename@2.0.0: {} - resedit@1.7.2: dependencies: pe-library: 0.4.1 @@ -4953,8 +4829,6 @@ snapshots: dependencies: defaults: 1.0.4 - which-module@2.0.1: {} - which@2.0.2: dependencies: isexe: 2.0.0 @@ -4963,12 +4837,6 @@ snapshots: dependencies: string-width: 4.2.3 - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4985,35 +4853,14 @@ snapshots: xmlbuilder@15.1.1: {} - y18n@4.0.3: {} - y18n@5.0.8: {} yallist@3.1.1: {} yallist@4.0.0: {} - yargs-parser@18.1.3: - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - yargs-parser@21.1.1: {} - yargs@15.4.1: - dependencies: - cliui: 6.0.0 - decamelize: 1.2.0 - find-up: 4.1.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - require-main-filename: 2.0.0 - set-blocking: 2.0.0 - string-width: 4.2.3 - which-module: 2.0.1 - y18n: 4.0.3 - yargs-parser: 18.1.3 - yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/electron/src/main/appHost.ts b/electron/src/main/appHost.ts index 479983b..4cee80f 100644 --- a/electron/src/main/appHost.ts +++ b/electron/src/main/appHost.ts @@ -5,16 +5,14 @@ import { app, BrowserWindow, ipcMain, shell } from 'electron' import fs from 'fs' import path from 'path' +import { sanitizeNavId } from '@shared/featureFlags' -const VALID_NAV = new Set(['record', 'camera', 'pull', 'network', 'system']) export function parseNavFromArgv(argv: string[]): string | null { for (const a of argv) { const m = /^--nav=(.+)$/.exec(a) if (!m) continue - const id = m[1].trim() - if (id === 'proxy') return 'network' - if (VALID_NAV.has(id)) return id + return sanitizeNavId(m[1].trim()) } return null } @@ -270,9 +268,10 @@ export function registerAppHostIpcHandlers(getBackendRoot: () => string): void { } export function sendNavToWindow(win: BrowserWindow, nav: string): void { - if (!VALID_NAV.has(nav)) return + const safe = sanitizeNavId(nav) + if (!safe) return try { - win.webContents.send('app:nav', nav) + win.webContents.send('app:nav', safe) } catch { /* ignore */ } diff --git a/electron/src/renderer/src/App.css b/electron/src/renderer/src/App.css index 234991e..8b40583 100644 --- a/electron/src/renderer/src/App.css +++ b/electron/src/renderer/src/App.css @@ -2643,6 +2643,158 @@ button.net-badge { color: var(--text-muted); } +/* —— 网络功能页(工作台布局)—— */ +.feature-page--network { + max-width: 1080px; +} + +.feature-hero--network { + grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.8fr); +} + +@media (max-width: 900px) { + .feature-hero--network { + grid-template-columns: 1fr; + } +} + +.net-flow { + margin-top: -0.25rem; +} + +.net-hero-main { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.net-hero-aside { + justify-content: flex-start; +} + +.net-summary { + display: flex; + flex-direction: column; + gap: 0.75rem; + height: 100%; +} + +.net-summary__badge { + align-self: flex-start; +} + +.net-summary__time { + margin: 0; + font-size: 0.78rem; + color: var(--text-muted); +} + +.net-summary__hint { + margin: 0; + font-size: 0.78rem; + line-height: 1.55; + color: var(--text-muted); + flex: 1; +} + +.net-summary__actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.net-site__subtitle { + display: block; + margin-top: 2px; + font-size: 11px; + font-weight: 500; + color: var(--text-muted); +} + +.net-site__stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin-bottom: 10px; +} + +.net-site__stats--dim { + opacity: 0.72; +} + +.net-site__stat { + padding: 8px 10px; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-subtle); +} + +.net-site__stat-k { + display: block; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 4px; +} + +.net-site__stat-v { + display: block; + font-family: var(--font-mono); + font-size: 0.92rem; + font-weight: 700; + color: var(--text); +} + +.net-site__meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 12px; + margin: 0; + font-size: 11px; +} + +.net-site__meta dt { + margin: 0 0 2px; + color: var(--text-muted); + font-weight: 600; +} + +.net-site__meta dd { + margin: 0; + color: var(--text); + word-break: break-word; +} + +.net-site--google { + border-color: rgba(124, 156, 255, 0.22); +} + +.net-site--douyin { + border-color: rgba(237, 137, 54, 0.18); +} + +.feature-card--proxy { + border-color: rgba(61, 214, 140, 0.22); + background: linear-gradient(165deg, rgba(61, 214, 140, 0.05) 0%, var(--bg-card) 100%); +} + +.net-tips__list { + margin: 0; + padding-left: 1.1rem; + display: flex; + flex-direction: column; + gap: 0.45rem; + font-size: 0.82rem; + line-height: 1.55; + color: var(--text-muted); +} + +.proxy-form__msg--err { + color: var(--danger); +} + /* —— 录制代理 + 系统指标 —— */ .app-panel--network { display: flex; @@ -3192,10 +3344,75 @@ button.net-badge { margin-bottom: 12px; } +.metrics-grid--sys { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.sys-page { + max-width: 920px; +} + +.sys-info-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.sys-info-card { + padding: 14px 16px; + border-radius: var(--radius-md); + border: 1px solid var(--border-card); + background: var(--bg-card-muted); +} + +.sys-info-card__title { + margin: 0 0 10px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-muted); +} + +.sys-info-card__list { + display: grid; + gap: 8px; + margin: 0; +} + +.sys-kv dt { + margin: 0 0 2px; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); +} + +.sys-kv dd { + margin: 0; + font-size: 13px; + line-height: 1.45; + color: var(--text); + word-break: break-word; +} + +.sys-section-title { + margin: 4px 0 10px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-muted); +} + @media (max-width: 720px) { .metrics-grid { grid-template-columns: 1fr; } + + .sys-info-grid { + grid-template-columns: 1fr; + } } .metrics-block { @@ -4127,6 +4344,12 @@ button.net-badge { border-color: rgba(237, 137, 54, 0.3); } +.feature-status-badge.is-bad { + color: #f56565; + background: rgba(245, 101, 101, 0.12); + border-color: rgba(245, 101, 101, 0.35); +} + .feature-status-badge--muted { color: var(--text-muted); background: rgba(255, 255, 255, 0.04); diff --git a/electron/src/renderer/src/App.tsx b/electron/src/renderer/src/App.tsx index 211707f..2dfa307 100644 --- a/electron/src/renderer/src/App.tsx +++ b/electron/src/renderer/src/App.tsx @@ -1,4 +1,11 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { + type AppNavId, + isNavEnabled, + sanitizeNavId, + SHOW_CAMERA_UI, + SHOW_PULL_STREAM_UI, +} from '@shared/featureFlags' import { apiFetch, apiFetchJson, @@ -24,7 +31,6 @@ import LivePulse from './components/ui/LivePulse' import { getPbAuthGateEnabled, PB_AUTH_GATE_EVENT } from './pbAuthGate' import { pbAuthRefresh, type PbUserRecord } from './pbAuthClient' import NetworkDashboard from './NetworkDashboard' -import ProxySettings from './ProxySettings' import SystemMetrics from './SystemMetrics' import LiveYoutubeMonitor from './components/LiveYoutubeMonitor' import { logLiveEvent, logUserAction } from './workspace' @@ -87,7 +93,7 @@ function douyinRoomAndHref(douyinUrl: string | undefined, urlIni: string): { roo return { room, href } } -type NavId = 'record' | 'camera' | 'pull' | 'network' | 'system' +type NavId = AppNavId function IconForbidden({ className }: { className?: string }) { return ( @@ -124,17 +130,14 @@ const NAV_ITEMS: { id: NavId; label: string; hint: string }[] = [ { id: 'record', label: '直播', hint: '抖音转播推流与直播控制' }, { id: 'camera', label: '摄像头', hint: '本机采集直推 YouTube' }, { id: 'pull', label: '拉流', hint: 'RTMP / SRT / HLS 拉流转推' }, - { id: 'network', label: '网络', hint: '连通与测速' }, - { id: 'system', label: '系统', hint: '负载与网速' }, + { id: 'network', label: '网络', hint: '链路检测、分流判断与录制代理' }, + { id: 'system', label: '系统', hint: '硬件与系统信息' }, ] -/** 侧栏:直播 + 摄像头 + 拉流 + 网络 + 系统 */ -const SIDEBAR_NAV_ITEMS: { id: NavId; label: string; hint: string }[] = [ - NAV_ITEMS.find((n) => n.id === 'record')!, - NAV_ITEMS.find((n) => n.id === 'camera')!, - NAV_ITEMS.find((n) => n.id === 'pull')!, - ...NAV_ITEMS.filter((n) => ['network', 'system'].includes(n.id)), -] +/** 侧栏导航(受功能开关过滤) */ +const SIDEBAR_NAV_ITEMS: { id: NavId; label: string; hint: string }[] = NAV_ITEMS.filter((n) => + isNavEnabled(n.id), +) export default function App() { const [theme, setTheme] = useState(readInitialTheme) @@ -289,16 +292,15 @@ export default function App() { const h = window.appHost if (!h?.onNavigate) return return h.onNavigate((raw) => { - const nav = - raw === 'proxy' - ? 'network' - : raw === 'record' || raw === 'camera' || raw === 'pull' || raw === 'network' || raw === 'system' - ? raw - : null + const nav = sanitizeNavId(raw) if (nav) setNav(nav) }) }, []) + useEffect(() => { + if (!isNavEnabled(nav)) setNav('record') + }, [nav]) + const getEntry = useCallback( (name: string) => entries.find((e) => e.pm2 === name) ?? null, [entries], @@ -1736,10 +1738,9 @@ export default function App() { {nav === 'network' && (
-
)} - {nav === 'camera' && ( + {SHOW_CAMERA_UI && nav === 'camera' && (
)} - {nav === 'pull' && ( + {SHOW_PULL_STREAM_UI && nav === 'pull' && (
{douyinHints[0] && ( + -
- + + + + + {err && (

@@ -234,68 +538,95 @@ export default function NetworkDashboard() {

)} - <> -
- - -
- +
+
+ -
- 线路判断 - {routesPill[1]} -

{loading ? '检测中…' : view.routes.note || '暂无线路说明'}

-
+ -
-

测速采样

-
- {[ - ['海外', view.speed.overseas], - ['国内', view.speed.domestic], - ].map(([title, speed]) => { - const s = speed as SpeedBlock - return ( -
-
{String(title)}
-
- 下载 - {loading ? '—' : fmtMbps(s.download_mbps)} -
-
- 上传 - {loading ? '—' : fmtMbps(s.upload_mbps)} -
-
- 采样 - {loading ? '—' : fmtBytes(s.download_bytes_sampled)} -
- {!loading && (s.download_error || s.upload_error) && ( -

- {[s.download_error, s.upload_error].filter(Boolean).join(' · ')} -

- )} +
+ } + onSolutionClick={openSolution} + /> + } + /> +
+ +
+
+

速率对比

+ +
+ +
+
+ + 线路判断 + + {routesPill[1]}
- ) - })} +
+
+ 🇨🇳 + 国内 +
+
+ + +
+
+ 🌐 + 海外 +
+
+

{loading ? '检测中…' : view.routes.note || '暂无线路说明'}

+
+
-

- 上行测速依赖目标站点是否接受 POST,失败不等于无法推流;YouTube 直播更看重持续上行和代理/TUN 规则。 -

- + +
+ +
+

+ 排障提示 +

+
    +
  • + 🛡️ + Google 不可达时检查 TUN/规则是否覆盖 YouTube RTMPS。 +
  • +
  • + 🔌 + 录制代理仅影响 FFmpeg,不改 Windows 系统代理。 +
  • +
  • + 🔀 + 线路未分流时拉源与推流易争抢出口导致卡顿。 +
  • +
+
+
+ ) } diff --git a/electron/src/renderer/src/ProxySettings.tsx b/electron/src/renderer/src/ProxySettings.tsx index dfac47f..7ac504f 100644 --- a/electron/src/renderer/src/ProxySettings.tsx +++ b/electron/src/renderer/src/ProxySettings.tsx @@ -1,20 +1,38 @@ import { useCallback, useEffect, useState } from 'react' import { apiFetch, apiUrl } from './api' +import { IconProxy } from './components/network/NetworkIcons' type ProxyCfg = { enabled: boolean; http: string; socks: string } -export default function ProxySettings() { +type Props = { + /** 嵌入网络页时使用 feature-card 样式,不重复外层 card */ + embedded?: boolean + onProxyChange?: (cfg: ProxyCfg) => void +} + +export default function ProxySettings({ embedded = false, onProxyChange }: Props) { const [cfg, setCfg] = useState({ enabled: false, http: '', socks: '' }) const [saving, setSaving] = useState(false) const [msg, setMsg] = useState('') + const updateCfg = useCallback( + (next: ProxyCfg | ((prev: ProxyCfg) => ProxyCfg)) => { + setCfg((prev) => { + const value = typeof next === 'function' ? next(prev) : next + onProxyChange?.(value) + return value + }) + }, + [onProxyChange], + ) + const load = useCallback(async () => { setMsg('') try { const res = await apiFetch(apiUrl('/proxy_config')) const data = (await res.json()) as ProxyCfg if (!res.ok) throw new Error('读取失败') - setCfg({ + updateCfg({ enabled: !!data.enabled, http: data.http || '', socks: data.socks || '', @@ -22,7 +40,7 @@ export default function ProxySettings() { } catch (e) { setMsg(e instanceof Error ? e.message : String(e)) } - }, []) + }, [updateCfg]) useEffect(() => { void load() @@ -39,6 +57,7 @@ export default function ProxySettings() { }) if (!res.ok) throw new Error('保存失败') setMsg('已保存。重启直播任务后生效。') + onProxyChange?.(cfg) } catch (e) { setMsg(e instanceof Error ? e.message : String(e)) } finally { @@ -46,6 +65,66 @@ export default function ProxySettings() { } } + const form = ( +
+ + + updateCfg((c) => ({ ...c, http: e.target.value }))} + autoComplete="off" + /> + + updateCfg((c) => ({ ...c, socks: e.target.value }))} + autoComplete="off" + /> +
+ + +
+ {msg &&

{msg}

} +
+ ) + + if (embedded) { + return ( +
+

+ 录制专用代理 +

+

+ 仅对本软件拉起的 FFmpeg / 录制进程生效。若使用 Clash Verge 等,请开启「混合端口」后填写一致地址(常见 7890)。 +

+ {form} +
+ ) + } + return (

@@ -56,49 +135,7 @@ export default function ProxySettings() { 系统代理。若你使用 Clash Verge 等,请在本机开启「混合端口」后,把地址填成与之一致(常见为 7890)。 YouTube RTMP 推流若仍失败,可能需要代理软件开启 TUN 或自行调整规则。

-
- - - setCfg((c) => ({ ...c, http: e.target.value }))} - autoComplete="off" - /> - - setCfg((c) => ({ ...c, socks: e.target.value }))} - autoComplete="off" - /> -
- - -
- {msg &&

{msg}

} -
+ {form}

) } diff --git a/electron/src/renderer/src/SystemMetrics.tsx b/electron/src/renderer/src/SystemMetrics.tsx index 1e9c240..3f3ffb7 100644 --- a/electron/src/renderer/src/SystemMetrics.tsx +++ b/electron/src/renderer/src/SystemMetrics.tsx @@ -1,25 +1,41 @@ import { useCallback, useEffect, useState } from 'react' import { apiFetch, apiUrl } from './api' import { saveWorkspaceSnapshot } from './workspace' -import AppHostControls from './AppHostControls' import MetricBar, { toneFromPercent } from './components/ui/MetricBar' -import NetSpeedBars from './components/ui/NetSpeedBars' type Metrics = { ts?: number + system?: { + hostname?: string + os?: string + os_release?: string + os_version?: string + machine?: string + processor?: string | null + python?: string + boot_time_unix?: number | null + } + hardware?: { + cpu_logical?: number + cpu_physical?: number + cpu_freq?: { current_mhz?: number | null; max_mhz?: number | null } | null + memory_total?: number + gpu?: { + nvidia_detected?: boolean + nvenc_available?: boolean + } + } cpu_percent?: number | null - cpu_count_logical?: number memory?: { percent?: number; used?: number; total?: number; available?: number } swap?: { percent?: number; used?: number; total?: number } disks?: Array<{ mountpoint?: string + device?: string + fstype?: string percent?: number free?: number total?: number }> - network?: { up_mbps?: number | null; down_mbps?: number | null } - ffmpeg_processes?: Array<{ pid?: number; name?: string; rss?: number }> - boot_time_unix?: number | null error?: string } @@ -31,6 +47,37 @@ function fmtBytes(n: number | undefined): string { return `${(n / 1024 ** 3).toFixed(2)} GB` } +function fmtOsName(sys: Metrics['system']): string { + if (!sys?.os) return '—' + const parts = [sys.os, sys.os_release].filter(Boolean) + return parts.join(' ') || '—' +} + +function fmtUptime(bootUnix: number | null | undefined): string { + if (bootUnix == null) return '—' + const sec = Math.max(0, Math.floor(Date.now() / 1000 - bootUnix)) + const d = Math.floor(sec / 86400) + const h = Math.floor((sec % 86400) / 3600) + const min = Math.floor((sec % 3600) / 60) + return d > 0 ? `${d} 天 ${h} 小时` : `${h} 小时 ${min} 分` +} + +function gpuLabel(gpu?: { nvidia_detected?: boolean; nvenc_available?: boolean }): string { + if (!gpu) return '—' + if (gpu.nvenc_available) return 'NVIDIA · NVENC 可用' + if (gpu.nvidia_detected) return 'NVIDIA · 无 NVENC' + return '未检测到 NVIDIA GPU' +} + +function InfoRow({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ) +} + export default function SystemMetrics() { const [data, setData] = useState(null) const [err, setErr] = useState(null) @@ -53,133 +100,132 @@ export default function SystemMetrics() { useEffect(() => { void load() - const id = window.setInterval(() => void load(), 2000) + const id = window.setInterval(() => void load(), 3000) return () => clearInterval(id) }, [load]) - const m = data?.memory - const net = data?.network - const uptimeLabel = - data?.boot_time_unix != null - ? (() => { - const sec = Math.max(0, Math.floor(Date.now() / 1000 - data.boot_time_unix)) - const d = Math.floor(sec / 86400) - const h = Math.floor((sec % 86400) / 3600) - const min = Math.floor((sec % 3600) / 60) - return d > 0 ? `${d} 天 ${h} 小时` : `${h} 小时 ${min} 分` - })() - : '—' - const warnings = [ - data?.cpu_percent != null && data.cpu_percent >= 85 ? 'CPU 持续过高会导致推流掉帧' : '', - m?.percent != null && m.percent >= 85 ? '内存压力偏高,建议关闭无关程序' : '', - data?.disks?.some((d) => (d.percent ?? 0) >= 90) ? '至少一个磁盘空间紧张,录制缓存可能失败' : '', - data?.ffmpeg_processes?.length ? '' : '当前未检测到 FFmpeg 进程;直播中若仍为空,说明未真正推流', - ].filter(Boolean) + const sys = data?.system + const hw = data?.hardware + const mem = data?.memory + const cpuFreq = hw?.cpu_freq return ( - <> -
-
-
-

- 🖥️ - 本机负载与网速 -

-

- 网速为全机所有网卡合计的估算值(首次刷新可能无速率,约 2 秒后出现)。下方列出当前检测到的 - ffmpeg 进程。 +

+
+
+
+

+ + 🖥️ + + 硬件与系统 +

+

本机操作系统、处理器、显卡与资源占用

+
+ ⏱ 运行 {fmtUptime(sys?.boot_time_unix)} +
+ + {err ? ( +

+ {err}

+ ) : null} + +
+
+

系统信息

+
+ + + + + +
+
+ +
+

硬件信息

+
+ + + + + +
+
- ⏱ 已运行 {uptimeLabel} -
- {err && ( -

- {err} -

- )} - {warnings.length > 0 && ( -
- {warnings.map((w) => ( - ⚠️ {w} - ))} -
- )} -
+ +

资源占用

+
CPU
{data?.cpu_percent != null ? `${data.cpu_percent.toFixed(1)}%` : '—'}
- -
逻辑处理器 {data?.cpu_count_logical ?? '—'} 个
+ +
逻辑处理器 {hw?.cpu_logical ?? '—'} 个
内存
-
{m?.percent != null ? `${m.percent.toFixed(1)}%` : '—'}
- +
{mem?.percent != null ? `${mem.percent.toFixed(1)}%` : '—'}
+
- {fmtBytes(m?.used)} / {fmtBytes(m?.total)}(可用 {fmtBytes(m?.available)}) + {fmtBytes(mem?.used)} / {fmtBytes(mem?.total)}(可用 {fmtBytes(mem?.available)})
-
-
全机网速
-
- ↑ {net?.up_mbps != null ? `${net.up_mbps.toFixed(2)} Mbps` : '—'} - ↓ {net?.down_mbps != null ? `${net.down_mbps.toFixed(2)} Mbps` : '—'} -
- -
含所有网卡收发合计
+
+ + {data?.swap && data.swap.total != null && data.swap.total > 0 ? ( +
+ 交换分区 {data.swap.percent != null ? `${data.swap.percent.toFixed(0)}%` : '—'} ·{' '} + {fmtBytes(data.swap.used)} / {fmtBytes(data.swap.total)}
-
- {data?.swap && data.swap.total != null && data.swap.total > 0 && ( -
- 交换分区 {data.swap.percent != null ? `${data.swap.percent.toFixed(0)}%` : '—'} ·{' '} - {fmtBytes(data.swap.used)} / {fmtBytes(data.swap.total)} -
- )} - {data?.disks && data.disks.length > 0 && ( -
-
磁盘空间
-
    - {data.disks.slice(0, 8).map((d) => ( -
  • - {d.mountpoint} -
    - -
    - - {d.percent != null ? `${d.percent.toFixed(0)}%` : '—'} 已用 · 剩余 {fmtBytes(d.free)} - -
  • - ))} -
-
- )} - {data?.ffmpeg_processes && data.ffmpeg_processes.length > 0 && ( -
-
ffmpeg 进程
- - - - - - - - - - {data.ffmpeg_processes.map((p) => ( - - - - - + ) : null} + + {data?.disks && data.disks.length > 0 ? ( +
+
磁盘空间
+
    + {data.disks.slice(0, 8).map((d) => ( +
  • + + {d.mountpoint} + {d.fstype ? ` (${d.fstype})` : ''} + +
    + +
    + + {d.percent != null ? `${d.percent.toFixed(0)}%` : '—'} 已用 · 剩余 {fmtBytes(d.free)} + +
  • ))} -
-
PID名称内存
{p.pid}{p.name}{fmtBytes(p.rss)}
-
- )} -
- - + +
+ ) : null} + + ) } diff --git a/electron/src/renderer/src/components/network/IpProfilePanel.tsx b/electron/src/renderer/src/components/network/IpProfilePanel.tsx new file mode 100644 index 0000000..6e5f006 --- /dev/null +++ b/electron/src/renderer/src/components/network/IpProfilePanel.tsx @@ -0,0 +1,188 @@ +import ScoreRing from '../ui/ScoreRing' +import { IconGlobe, IconShield } from './NetworkIcons' + +export type IpExitProfile = { + label?: string + ip?: string + country?: string + country_code?: string + region?: string + city?: string + isp?: string + org?: string + asn?: string + asname?: string + mobile?: boolean + proxy?: boolean + hosting?: boolean + ip_type?: string + ip_type_label?: string + native?: boolean | null + native_label?: string + quality_score?: number + quality_label?: string + error?: string | null + ping0?: { + ip?: string + location?: string + asn?: string + org?: string + error?: string + } +} + +export type IpProfilePayload = { + default_exit: IpExitProfile + overseas_exit: IpExitProfile + checks: { id: string; label: string; ok: boolean; hint?: string }[] + summary?: { residential_like?: boolean; headline?: string } + meta?: { at?: number; disclaimer?: string; sources?: string[] } + error?: string +} + +function typeEmoji(ipType?: string): string { + if (ipType === 'residential') return '🏠' + if (ipType === 'datacenter') return '🏢' + if (ipType === 'proxy') return '🛡️' + if (ipType === 'mobile') return '📱' + return '🌐' +} + +function typeTone(ipType?: string): 'ok' | 'warn' | 'bad' | 'neutral' { + if (ipType === 'residential') return 'ok' + if (ipType === 'datacenter' || ipType === 'proxy') return 'bad' + if (ipType === 'mobile') return 'warn' + return 'neutral' +} + +function scoreTone(score: number): 'ok' | 'warn' | 'bad' | 'neutral' { + if (score >= 85) return 'ok' + if (score >= 70) return 'warn' + return 'bad' +} + +function ExitCard({ data, accent }: { data: IpExitProfile; accent: 'default' | 'overseas' }) { + const loading = !data.ip && !data.error + const score = data.quality_score ?? 0 + + return ( +
+
+ + {typeEmoji(data.ip_type)} + +
+

{data.label || '出口'}

+

{loading ? '检测中…' : data.ip || '—'}

+
+ {!loading && data.quality_score != null ? ( + + ) : null} +
+ + {!loading && ( + <> +
+ + {data.ip_type_label || '未知'} + + {data.native_label ? ( + + {data.native_label} + + ) : null} + {data.proxy ? 代理 : null} + {data.hosting ? 机房 : null} +
+ +
+
+
归属
+
{[data.country, data.city].filter(Boolean).join(' · ') || '—'}
+
+
+
运营商
+
{data.isp || data.org || '—'}
+
+
+
ASN
+
{data.asn || data.ping0?.asn || '—'}
+
+
+
洁净度
+
{data.quality_label || '—'}
+
+
+ + {data.ping0?.location ? ( +

+ ping0 + {data.ping0.location} +

+ ) : null} + + )} + + {data.error ?

{data.error}

: null} +
+ ) +} + +type Props = { + data: IpProfilePayload | null + loading?: boolean + error?: string | null +} + +/** 双出口卡片 + 未通过项摘要(不再占用侧栏清单) */ +export default function IpProfilePanel({ data, loading, error }: Props) { + const headline = data?.summary?.headline + const residential = data?.summary?.residential_like + + return ( +
+
+
+

+ 静态住宅 IP +

+

默认出口与海外采样对比(参考 whoer / ping0)

+
+ {headline ? ( + + + {residential ? '偏住宅' : '需关注'} + + ) : loading ? ( + 检测中 + ) : null} +
+ + {(error || data?.error) && ( +

+ {error || data?.error} +

+ )} + +
+ + +
+ + {!loading && (data?.checks ?? []).some((c) => !c.ok) ? ( +
    + {(data?.checks ?? []) + .filter((c) => !c.ok) + .map((c) => ( +
  • + {c.label} + {c.hint ? ` — ${c.hint}` : ''} +
  • + ))} +
+ ) : null} + + {data?.meta?.disclaimer ?

{data.meta.disclaimer}

: null} +
+ ) +} diff --git a/electron/src/renderer/src/components/network/IpQualityPanel.tsx b/electron/src/renderer/src/components/network/IpQualityPanel.tsx new file mode 100644 index 0000000..f1dc666 --- /dev/null +++ b/electron/src/renderer/src/components/network/IpQualityPanel.tsx @@ -0,0 +1,270 @@ +import { IconGlobe, IconShield } from './NetworkIcons' + +export type IpQualityScore = { + source: string + value: number + unit: string + risk?: string | null + risk_label?: string +} + +export type IpQualityMedia = { + status: string + status_label: string + region?: string | null + error?: string +} + +export type IpQualityPayload = { + head?: { ip?: string; version?: string; github?: string } + info?: { + ip?: string + asn?: string | number + organization?: string + city?: string + country?: string + country_code?: string + ip_type?: string + mode_lite?: boolean + } + type?: { usage?: Record } + scores?: IpQualityScore[] + factors?: Record> + media?: Record + summary?: { headline?: string; proxy_like?: boolean; hosting_like?: boolean } + meta?: { at?: number; mode?: string; disclaimer?: string; errors?: string[] } + error?: string +} + +const MEDIA_META: Record = { + Youtube: { emoji: '▶', label: 'YouTube' }, + TikTok: { emoji: '♪', label: 'TikTok' }, + Netflix: { emoji: 'N', label: 'Netflix' }, + DisneyPlus: { emoji: '✦', label: 'Disney+' }, +} + +const FACTOR_LABELS: Record = { + Proxy: '代理', + VPN: 'VPN', + Tor: 'Tor', + Hosting: '机房', +} + +function scoreTone(risk?: string | null): 'ok' | 'warn' | 'bad' | 'neutral' { + if (!risk) return 'neutral' + if (['verylow', 'low'].includes(risk)) return 'ok' + if (['elevated', 'suspicious'].includes(risk)) return 'warn' + return 'bad' +} + +function mediaTone(status: string): 'ok' | 'warn' | 'bad' | 'neutral' { + if (status === 'yes') return 'ok' + if (['maybe', 'no_premium'].includes(status)) return 'warn' + if (['error', 'bad', 'blocked', 'cn'].includes(status)) return 'bad' + return 'neutral' +} + +function summaryBadge(data: IpQualityPayload | null, loading: boolean): { label: string; cls: string } { + if (loading || !data?.info?.ip) return { label: '检测中', cls: 'is-warn' } + if (data.summary?.proxy_like) return { label: '代理/VPN 风险', cls: 'is-bad' } + if (data.summary?.hosting_like) return { label: '疑似机房', cls: 'is-warn' } + return { label: '出口较干净', cls: 'is-ready' } +} + +function FactorDot({ value }: { value: boolean | null | undefined }) { + if (value === true) { + return ( + + ● + + ) + } + if (value === false) { + return ( + + ● + + ) + } + return ( + + ○ + + ) +} + +function FactorMatrix({ factors }: { factors: Record> }) { + const rows = Object.keys(factors) + if (!rows.length) return null + + const dbs = Array.from(new Set(rows.flatMap((fk) => Object.keys(factors[fk] || {})))) + if (!dbs.length) return null + + return ( +
+

多库风险指纹

+
+
+ + {dbs.map((db) => ( + + {db} + + ))} +
+ {rows.map((fk) => ( +
+ {FACTOR_LABELS[fk] ?? fk} + {dbs.map((db) => ( + + + + ))} +
+ ))} +
+
+ ) +} + +type Props = { + data: IpQualityPayload | null + loading: boolean + error: string | null +} + +export default function IpQualityPanel({ data, loading, error }: Props) { + const info = data?.info + const usage = data?.type?.usage ?? {} + const scores = data?.scores ?? [] + const factors = data?.factors ?? {} + const media = data?.media ?? {} + const usageEntries = Object.entries(usage).filter(([k]) => !k.endsWith('_company')) + const badge = summaryBadge(data, loading) + + return ( +
+
+
+

+ IP 质量检测 +

+

+ 数据源{' '} + + xykt/IPQuality + + {data?.head?.version ? ` · ${data.head.version}` : ''} + {info?.mode_lite ? ' · 精简' : ''} +

+
+ {!loading && info?.ip ? ( + + + {badge.label} + + ) : loading ? ( + 检测中 + ) : null} +
+ + {error ? ( +

+ {error} +

+ ) : null} + + {loading && !data ? ( +
+ +
+ ) : null} + + {info?.ip ? ( +
+
+
+

{info.ip}

+

+ {[info.country, info.city].filter(Boolean).join(' · ') || '—'} + {info.country_code ? ` · ${info.country_code}` : ''} +

+
+ {info.organization ? ( +
+
组织
+
{info.organization}
+
+ ) : null} + {info.asn ? ( +
+
ASN
+
{info.asn}
+
+ ) : null} + {info.ip_type ? ( +
+
类型
+
{info.ip_type}
+
+ ) : null} +
+
+ + {scores.length > 0 ? ( +
+ {scores.map((s) => ( +
+ {s.source} + + {s.value} + {s.unit} + + {s.risk_label ? {s.risk_label} : null} +
+ ))} +
+ ) : null} +
+ ) : null} + + {usageEntries.length > 0 ? ( +
+ {usageEntries.map(([db, label]) => ( + + {db} + {label} + + ))} +
+ ) : null} + + + + {Object.keys(media).length > 0 ? ( +
+

流媒体解锁

+
+ {Object.entries(media).map(([name, m]) => { + const meta = MEDIA_META[name] ?? { emoji: '◎', label: name } + return ( +
+ + {meta.emoji} + +
+
{meta.label}
+

{m.status_label}

+ {m.region ? {m.region} : null} +
+
+ ) + })} +
+
+ ) : null} + + {data?.meta?.disclaimer ?

{data.meta.disclaimer}

: null} +
+ ) +} diff --git a/electron/src/renderer/src/components/network/NetworkIcons.tsx b/electron/src/renderer/src/components/network/NetworkIcons.tsx new file mode 100644 index 0000000..2089cbf --- /dev/null +++ b/electron/src/renderer/src/components/network/NetworkIcons.tsx @@ -0,0 +1,124 @@ +type IconProps = { className?: string; size?: number } + +function base(size: number) { + return { width: size, height: size, viewBox: '0 0 24 24', fill: 'none' as const, 'aria-hidden': true } +} + +export function IconGlobe({ className, size = 20 }: IconProps) { + const p = base(size) + return ( + + + + + ) +} + +export function IconLatency({ className, size = 18 }: IconProps) { + const p = base(size) + return ( + + + + + + ) +} + +export function IconDownload({ className, size = 18 }: IconProps) { + const p = base(size) + return ( + + + + + ) +} + +export function IconUpload({ className, size = 18 }: IconProps) { + const p = base(size) + return ( + + + + + ) +} + +export function IconRoute({ className, size = 20 }: IconProps) { + const p = base(size) + return ( + + + + + + + + ) +} + +export function IconShield({ className, size = 20 }: IconProps) { + const p = base(size) + return ( + + + + + ) +} + +export function IconProxy({ className, size = 20 }: IconProps) { + const p = base(size) + return ( + + + + + + + + ) +} + +export function IconRefresh({ className, size = 16 }: IconProps) { + const p = base(size) + return ( + + + + + ) +} + +export function IconGoogle({ className, size = 22 }: IconProps) { + const p = base(size) + return ( + + + + + + + ) +} + +export function IconDomestic({ className, size = 22 }: IconProps) { + const p = base(size) + return ( + + + + + ) +} + +export function IconTip({ className, size = 18 }: IconProps) { + const p = base(size) + return ( + + + + + ) +} diff --git a/electron/src/renderer/src/components/network/NetworkPipeline.tsx b/electron/src/renderer/src/components/network/NetworkPipeline.tsx new file mode 100644 index 0000000..849087d --- /dev/null +++ b/electron/src/renderer/src/components/network/NetworkPipeline.tsx @@ -0,0 +1,98 @@ +import type { FlowStep } from '../worker/WorkerFlowStrip' + +function FlowArrow({ active }: { active: boolean }) { + return ( +
+ + + + + + +
+ ) +} + +function IconSource() { + return ( + + + + ) +} + +function IconRouter() { + return ( + + + + + + + ) +} + +function IconYoutube() { + return ( + + + + ) +} + +function IconUploadStep() { + return ( + + + + + ) +} + +const STEP_META = [ + { id: 'source', tone: 'source', icon: IconSource, fallback: '国内源站' }, + { id: 'route', tone: 'route', icon: IconRouter, fallback: '线路分流' }, + { id: 'youtube', tone: 'youtube', icon: IconYoutube, fallback: 'YouTube' }, + { id: 'upload', tone: 'upload', icon: IconUploadStep, fallback: '上行推流' }, +] as const + +type Props = { + steps: FlowStep[] +} + +export default function NetworkPipeline({ steps }: Props) { + const byId = Object.fromEntries(steps.map((s) => [s.id, s])) as Record + + return ( +
+ {STEP_META.map((meta, idx) => { + const step = byId[meta.id] + const state = step?.state ?? 'idle' + const active = state === 'active' || state === 'pending' + const Icon = meta.icon + return ( +
+ {idx > 0 ? : null} +
+
+ +
+
+ {step?.label ?? meta.fallback} + {step?.title ?? '—'} +
+ +
+
+ ) + })} +
+ ) +} diff --git a/electron/src/renderer/src/components/network/NetworkSpeedChart.tsx b/electron/src/renderer/src/components/network/NetworkSpeedChart.tsx new file mode 100644 index 0000000..bcd4c1b --- /dev/null +++ b/electron/src/renderer/src/components/network/NetworkSpeedChart.tsx @@ -0,0 +1,135 @@ +import { useEffect, useRef } from 'react' +import * as echarts from 'echarts/core' +import { BarChart } from 'echarts/charts' +import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components' +import { CanvasRenderer } from 'echarts/renderers' + +echarts.use([BarChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]) + +type Props = { + overseasDown: number | null + overseasUp: number | null + domesticDown: number | null + domesticUp: number | null + loading?: boolean + className?: string +} + +function val(n: number | null | undefined): number { + return n != null && Number.isFinite(n) ? n : 0 +} + +export default function NetworkSpeedChart({ + overseasDown, + overseasUp, + domesticDown, + domesticUp, + loading = false, + className = '', +}: Props) { + const ref = useRef(null) + const chartRef = useRef(null) + + useEffect(() => { + if (!ref.current) return + chartRef.current ??= echarts.init(ref.current, undefined, { renderer: 'canvas' }) + const chart = chartRef.current + + const textMuted = getComputedStyle(document.documentElement).getPropertyValue('--text-muted').trim() || '#94a3b8' + const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#5b9dff' + const success = getComputedStyle(document.documentElement).getPropertyValue('--success').trim() || '#3dd68c' + const warn = getComputedStyle(document.documentElement).getPropertyValue('--warn').trim() || '#f6ad55' + + chart.setOption({ + animation: !loading, + animationDuration: 680, + animationEasing: 'cubicOut', + grid: { left: 8, right: 8, top: 36, bottom: 8, containLabel: true }, + tooltip: { + trigger: 'axis', + backgroundColor: 'rgba(15, 18, 28, 0.92)', + borderColor: 'rgba(91, 157, 255, 0.25)', + textStyle: { color: '#e8ecf4', fontSize: 12 }, + valueFormatter: (v: number) => `${v.toFixed(2)} Mbps`, + }, + legend: { + top: 0, + textStyle: { color: textMuted, fontSize: 11 }, + itemWidth: 10, + itemHeight: 10, + }, + xAxis: { + type: 'category', + data: ['海外 (Google)', '国内 (源站)'], + axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } }, + axisLabel: { color: textMuted, fontSize: 11 }, + axisTick: { show: false }, + }, + yAxis: { + type: 'value', + name: 'Mbps', + nameTextStyle: { color: textMuted, fontSize: 10 }, + splitLine: { lineStyle: { color: 'rgba(255,255,255,0.06)', type: 'dashed' } }, + axisLabel: { color: textMuted, fontSize: 10 }, + }, + series: [ + { + name: '下载', + type: 'bar', + barWidth: 18, + itemStyle: { + borderRadius: [6, 6, 0, 0], + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: accent }, + { offset: 1, color: 'rgba(91, 157, 255, 0.35)' }, + ]), + }, + data: loading ? [0, 0] : [val(overseasDown), val(domesticDown)], + }, + { + name: '上传', + type: 'bar', + barWidth: 18, + itemStyle: { + borderRadius: [6, 6, 0, 0], + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: success }, + { offset: 1, color: 'rgba(61, 214, 140, 0.35)' }, + ]), + }, + data: loading ? [0, 0] : [val(overseasUp), val(domesticUp)], + }, + ], + ...(loading + ? { + graphic: { + type: 'text', + left: 'center', + top: 'middle', + style: { text: '测速采样中…', fill: warn, fontSize: 12 }, + }, + } + : {}), + }) + + const ro = new ResizeObserver(() => chart.resize()) + ro.observe(ref.current) + return () => ro.disconnect() + }, [overseasDown, overseasUp, domesticDown, domesticUp, loading]) + + useEffect(() => { + return () => { + chartRef.current?.dispose() + chartRef.current = null + } + }, []) + + return ( +
+ ) +} diff --git a/electron/src/renderer/src/hooks/useWebRtcLeak.ts b/electron/src/renderer/src/hooks/useWebRtcLeak.ts new file mode 100644 index 0000000..aad852f --- /dev/null +++ b/electron/src/renderer/src/hooks/useWebRtcLeak.ts @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react' + +/** + * 轻量 WebRTC 本地候选检测(whoer 式泄露参考)。 + */ +export async function detectWebRtcLeak(timeoutMs = 4500): Promise<{ + leak: boolean + localCandidates: string[] + error?: string +}> { + if (typeof RTCPeerConnection === 'undefined') { + return { leak: false, localCandidates: [], error: '不支持 WebRTC' } + } + + const locals = new Set() + const privRe = /^(192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/ + + try { + const pc = new RTCPeerConnection({ + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], + }) + + pc.createDataChannel('probe') + const offer = await pc.createOffer() + await pc.setLocalDescription(offer) + + await new Promise((resolve) => { + const timer = window.setTimeout(() => resolve(), timeoutMs) + pc.onicecandidate = (ev) => { + if (!ev.candidate) { + window.clearTimeout(timer) + resolve() + return + } + const c = ev.candidate.candidate || '' + const m = c.match(/(\d{1,3}(?:\.\d{1,3}){3})/) + if (m && privRe.test(m[1])) locals.add(m[1]) + } + }) + + pc.close() + return { leak: locals.size > 0, localCandidates: [...locals] } + } catch (e) { + return { + leak: false, + localCandidates: [], + error: e instanceof Error ? e.message : String(e), + } + } +} + +export function useWebRtcLeak(enabled = true) { + const [leak, setLeak] = useState(null) + const [locals, setLocals] = useState([]) + const [error, setError] = useState(null) + + useEffect(() => { + if (!enabled) return + let cancelled = false + void detectWebRtcLeak().then((r) => { + if (cancelled) return + setLeak(r.leak) + setLocals(r.localCandidates) + setError(r.error ?? null) + }) + return () => { + cancelled = true + } + }, [enabled]) + + return { leak, locals, error } +} diff --git a/electron/src/renderer/src/main.tsx b/electron/src/renderer/src/main.tsx index 9cbe964..4f1b25e 100644 --- a/electron/src/renderer/src/main.tsx +++ b/electron/src/renderer/src/main.tsx @@ -4,6 +4,7 @@ import { initApiFromMain } from './api' import App from './App' import './App.css' import './ui-polish.css' +import './network-polish.css' const root = document.getElementById('root') if (root) { diff --git a/electron/src/renderer/src/network-polish.css b/electron/src/renderer/src/network-polish.css new file mode 100644 index 0000000..6fe56f5 --- /dev/null +++ b/electron/src/renderer/src/network-polish.css @@ -0,0 +1,1414 @@ +/* —— 网络页视觉增强 —— */ + +.net-page { + position: relative; + isolation: isolate; +} + +.net-page__bg { + position: absolute; + inset: -12px -8px 0; + z-index: -1; + pointer-events: none; + overflow: hidden; + border-radius: 18px; +} + +.net-page__orb { + position: absolute; + border-radius: 50%; + filter: blur(48px); + opacity: 0.55; + animation: net-orb-float 9s ease-in-out infinite; +} + +.net-page__orb--a { + width: 220px; + height: 220px; + top: -40px; + left: -20px; + background: rgba(91, 157, 255, 0.22); +} + +.net-page__orb--b { + width: 180px; + height: 180px; + top: 80px; + right: -10px; + background: rgba(61, 214, 140, 0.16); + animation-delay: -3s; +} + +@keyframes net-orb-float { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(8px, 12px) scale(1.06); + } +} + +/* —— 网络页布局 —— */ +.feature-page--network { + max-width: 1180px; + gap: 0.85rem; +} + +.net-page__toolbar { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 12px 16px; + padding-bottom: 4px; + animation: ui-fade-slide-up 0.38s var(--ease) both; +} + +.net-page__lead-compact { + margin-top: 2px !important; + font-size: 0.82rem !important; +} + +.net-page__toolbar-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.net-page__status { + margin-right: 4px; +} + +.net-layout { + display: flex; + flex-direction: column; + gap: 12px; +} + +.net-layout__main { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; +} + +.net-status-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 2px 0 4px; +} + +.net-status-chips--loading { + margin: 0; + font-size: 12px; + color: var(--text-muted); +} + +.net-status-chip { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + border: 1px solid var(--border-subtle); + background: rgba(255, 255, 255, 0.03); + color: var(--text-muted); +} + +.net-status-chip.is-ok { + color: var(--success); + border-color: rgba(61, 214, 140, 0.28); + background: var(--success-soft); +} + +.net-status-chip.is-warn { + color: var(--warn); + border-color: rgba(246, 173, 85, 0.28); + background: var(--warn-soft); +} + +.net-status-chip__mark { + font-size: 10px; + line-height: 1; +} + +.net-block--ip { + border-color: rgba(167, 139, 250, 0.22); + background: linear-gradient(165deg, rgba(167, 139, 250, 0.06), var(--bg-card-elevated) 50%); +} + +.net-block--ipq { + border-color: rgba(99, 102, 241, 0.28); + background: linear-gradient(165deg, rgba(99, 102, 241, 0.07), rgba(139, 92, 246, 0.03) 42%, var(--bg-card-elevated) 72%); + overflow: hidden; +} + +.ipq-summary-badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 11px; + white-space: nowrap; +} + +.ip-profile-card__badge { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.ipq-skeleton { + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap: 10px; + margin-bottom: 4px; +} + +.ipq-skeleton span { + height: 88px; + border-radius: 12px; + background: linear-gradient(105deg, rgba(255, 255, 255, 0.03) 35%, rgba(255, 255, 255, 0.07) 50%, rgba(255, 255, 255, 0.03) 65%); + background-size: 200% 100%; + animation: ui-shimmer 1.6s ease-in-out infinite; +} + +.ipq-skeleton span:nth-child(3) { + grid-column: 1 / -1; + height: 48px; +} + +.ipq-top { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(160px, 0.65fr); + gap: 12px; + margin-bottom: 12px; +} + +.ipq-identity { + position: relative; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid rgba(99, 102, 241, 0.22); + background: linear-gradient(145deg, rgba(99, 102, 241, 0.1), rgba(255, 255, 255, 0.02)); + overflow: hidden; +} + +.ipq-identity__glow { + position: absolute; + top: -30px; + right: -20px; + width: 120px; + height: 120px; + border-radius: 50%; + background: rgba(139, 92, 246, 0.18); + filter: blur(28px); + pointer-events: none; +} + +.ipq-identity__ip { + position: relative; + margin: 0; + font-size: 1.05rem; + font-weight: 800; + letter-spacing: 0.02em; +} + +.ipq-identity__geo { + position: relative; + margin: 6px 0 10px; + font-size: 12px; + color: var(--text-muted); +} + +.ipq-identity__meta { + position: relative; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px 10px; + margin: 0; + font-size: 11px; +} + +.ipq-identity__meta dt { + margin: 0 0 2px; + color: var(--text-muted); + font-weight: 600; +} + +.ipq-identity__meta dd { + margin: 0; + color: var(--text); + word-break: break-word; +} + +.ipq-metrics { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ipq-metric { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + padding: 12px 14px; + border-radius: 12px; + border: 1px solid var(--border-card); + background: rgba(0, 0, 0, 0.12); + border-left-width: 3px; + min-height: 0; +} + +.ipq-metric--ok { + border-left-color: var(--success); + background: linear-gradient(90deg, rgba(61, 214, 140, 0.08), transparent 55%); +} + +.ipq-metric--warn { + border-left-color: var(--warn); + background: linear-gradient(90deg, rgba(246, 173, 85, 0.1), transparent 55%); +} + +.ipq-metric--bad { + border-left-color: var(--danger); + background: linear-gradient(90deg, rgba(245, 101, 101, 0.1), transparent 55%); +} + +.ipq-metric--neutral { + border-left-color: var(--accent); +} + +.ipq-metric__src { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted); +} + +.ipq-metric__val { + margin-top: 4px; + font-size: 1.5rem; + font-weight: 800; + line-height: 1.1; + color: var(--text); +} + +.ipq-metric__val small { + margin-left: 3px; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); +} + +.ipq-metric__risk { + margin-top: 4px; + font-size: 10px; + font-weight: 650; + color: var(--text-muted); +} + +.ipq-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +} + +.ipq-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px 4px 4px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); + font-size: 11px; +} + +.ipq-tag__db { + padding: 2px 7px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + color: #c4b5fd; + background: rgba(139, 92, 246, 0.18); +} + +.ipq-tag__val { + font-weight: 600; + color: var(--text); +} + +.ipq-section { + margin-bottom: 12px; +} + +.ipq-section:last-child { + margin-bottom: 0; +} + +.ipq-section__title { + margin: 0 0 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-muted); +} + +.ipq-factor-board { + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(0, 0, 0, 0.14); + overflow: hidden; +} + +.ipq-factor-board__row { + display: grid; + grid-template-columns: 72px repeat(auto-fit, minmax(52px, 1fr)); + align-items: center; + gap: 4px; + padding: 8px 10px; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.ipq-factor-board__row:first-child { + border-top: none; +} + +.ipq-factor-board__row--head { + background: rgba(255, 255, 255, 0.03); + font-size: 10px; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.ipq-factor-board__label { + font-size: 11px; + font-weight: 650; + color: var(--text); +} + +.ipq-factor-board__db, +.ipq-factor-board__cell { + text-align: center; +} + +.ipq-dot { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 14px; + line-height: 1; +} + +.ipq-dot--yes { + color: #f87171; + text-shadow: 0 0 8px rgba(248, 113, 113, 0.45); +} + +.ipq-dot--no { + color: #4ade80; + text-shadow: 0 0 8px rgba(74, 222, 128, 0.35); +} + +.ipq-dot--unk { + color: var(--text-muted); + opacity: 0.45; + font-size: 12px; +} + +.ipq-stream-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); + gap: 10px; +} + +.ipq-stream { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border-radius: 12px; + border: 1px solid var(--border-card); + background: var(--bg-card); + transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease); +} + +.ipq-stream:hover { + transform: translateY(-1px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); +} + +.ipq-stream--ok { + border-color: rgba(61, 214, 140, 0.28); + background: linear-gradient(145deg, rgba(61, 214, 140, 0.07), var(--bg-card)); +} + +.ipq-stream--warn { + border-color: rgba(246, 173, 85, 0.28); + background: linear-gradient(145deg, rgba(246, 173, 85, 0.07), var(--bg-card)); +} + +.ipq-stream--bad { + border-color: rgba(245, 101, 101, 0.22); +} + +.ipq-stream__icon { + flex-shrink: 0; + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 9px; + font-size: 14px; + font-weight: 800; + color: #fff; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.28); +} + +.ipq-stream__body { + min-width: 0; +} + +.ipq-stream__body h5 { + margin: 0; + font-size: 12px; + font-weight: 700; +} + +.ipq-stream__body p { + margin: 3px 0 0; + font-size: 11px; + color: var(--text-muted); + line-height: 1.4; +} + +.ipq-stream__region { + display: inline-block; + margin-top: 4px; + padding: 1px 6px; + border-radius: 6px; + font-size: 10px; + font-weight: 700; + font-family: var(--font-mono, ui-monospace, monospace); + color: var(--accent); + background: rgba(91, 157, 255, 0.12); +} + +@media (max-width: 760px) { + .ipq-top { + grid-template-columns: 1fr; + } + + .ipq-identity__meta { + grid-template-columns: 1fr 1fr; + } + + .ipq-skeleton { + grid-template-columns: 1fr; + } + + .ip-exit-row { + grid-template-columns: 1fr; + } +} + +.ip-exit-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.ip-exit-card { + position: relative; + padding: 14px; + border-radius: 14px; + border: 1px solid var(--border-card); + background: var(--bg-card-elevated); +} + +.ip-exit-card--default { + border-color: rgba(245, 158, 11, 0.22); +} + +.ip-exit-card--overseas { + border-color: rgba(91, 157, 255, 0.22); +} + +.ip-exit-card.is-loading::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient(105deg, transparent 35%, rgba(255, 255, 255, 0.05) 50%, transparent 65%); + animation: ui-shimmer 1.6s ease-in-out infinite; + pointer-events: none; +} + +.ip-exit-card__head { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +.ip-exit-card__emoji { + font-size: 22px; + line-height: 1; +} + +.ip-exit-card__title-wrap { + flex: 1; + min-width: 0; +} + +.ip-exit-card__title { + margin: 0 0 2px; + font-size: 12px; + font-weight: 700; + color: var(--text-muted); +} + +.ip-exit-card__ip { + margin: 0; + font-size: 0.95rem; + font-weight: 700; +} + +.ip-exit-card__badges { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; +} + +.ip-type-pill { + font-size: 10px; + font-weight: 700; + padding: 3px 8px; + border-radius: var(--radius-pill); + border: 1px solid transparent; +} + +.ip-type-pill--ok { + color: var(--success); + background: var(--success-soft); + border-color: rgba(61, 214, 140, 0.3); +} + +.ip-type-pill--warn { + color: var(--warn); + background: var(--warn-soft); + border-color: rgba(246, 173, 85, 0.3); +} + +.ip-type-pill--bad { + color: var(--danger); + background: var(--danger-soft); + border-color: rgba(245, 101, 101, 0.3); +} + +.ip-type-pill--neutral { + color: var(--text-muted); + background: rgba(255, 255, 255, 0.04); + border-color: var(--border-subtle); +} + +.ip-exit-card__grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 12px; + margin: 0; + font-size: 11px; +} + +.ip-exit-card__grid dt { + margin: 0 0 2px; + color: var(--text-muted); + font-weight: 600; +} + +.ip-exit-card__grid dd { + margin: 0; + color: var(--text); + word-break: break-word; +} + +.ip-exit-card__ping0 { + margin: 10px 0 0; + font-size: 11px; + color: var(--text-muted); +} + +.ip-exit-card__ping0-tag { + display: inline-block; + margin-right: 6px; + padding: 1px 6px; + border-radius: 6px; + font-size: 9px; + font-weight: 800; + color: #a78bfa; + background: rgba(167, 139, 250, 0.12); + border: 1px solid rgba(167, 139, 250, 0.25); +} + +.ip-exit-card__err { + margin: 8px 0 0; + font-size: 11px; + color: var(--warn); +} + +.ip-profile-issues { + margin: 10px 0 0; + padding: 10px 12px; + border-radius: 10px; + list-style: none; + font-size: 11px; + line-height: 1.5; + color: var(--warn); + background: var(--warn-soft); + border: 1px solid rgba(246, 173, 85, 0.25); +} + +.ip-profile-issues li + li { + margin-top: 4px; +} + +.ip-profile-foot { + margin: 8px 0 0; + font-size: 11px; + line-height: 1.5; + color: var(--text-muted); +} + +.net-split-row { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(220px, 0.85fr); + gap: 12px; + align-items: stretch; +} + +.net-bottom { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 4px; +} + +/* 统一内容块(比 feature-card 更轻) */ +.net-block { + padding: 14px 16px; + border-radius: 14px; + border: 1px solid var(--border-card); + background: var(--bg-card-elevated); + box-shadow: var(--shadow-sm); +} + +.net-block__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + margin-bottom: 12px; +} + +.net-block__title { + display: inline-flex; + align-items: center; + gap: 7px; + margin: 0 0 4px; + font-size: 0.92rem; + font-weight: 700; + color: var(--text); +} + +.net-block__desc { + margin: 0; + font-size: 0.78rem; + line-height: 1.45; + color: var(--text-muted); +} + +.net-block--chart { + border-color: rgba(124, 156, 255, 0.2); + min-height: 100%; +} + +.net-block--routes { + display: flex; + flex-direction: column; + border-color: rgba(91, 157, 255, 0.18); +} + +.net-block--tips { + border-color: rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.02); +} + +.net-aside-card { + padding: 14px; + border-radius: 14px; + border: 1px solid var(--border-card); + background: var(--bg-card-elevated); + box-shadow: var(--shadow-sm); +} + +.net-aside-card--score { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 8px; + border-color: rgba(124, 156, 255, 0.22); + background: linear-gradient(165deg, rgba(124, 156, 255, 0.08), var(--bg-card-elevated) 60%); +} + +.net-aside-card__title { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 0 0 8px; + font-size: 0.85rem; + font-weight: 700; + color: var(--text); +} + +.net-aside-card__sub { + margin: 0; + font-size: 0.75rem; + line-height: 1.45; + color: var(--text-muted); + text-align: center; +} + +.net-tips__list--grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px 14px; + padding-left: 0; + list-style: none; +} + +.net-route-viz--compact { + padding: 8px 4px 10px; + margin-bottom: 4px; +} + +.net-page > .net-pipeline { + margin-bottom: 2px; +} + +.net-page__intro { + animation: ui-fade-slide-up 0.42s var(--ease) both; +} + +.net-page__title-row { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.net-page__title-icon { + flex-shrink: 0; + width: 48px; + height: 48px; + display: grid; + place-items: center; + border-radius: 14px; + color: var(--accent); + background: linear-gradient(145deg, rgba(91, 157, 255, 0.18), rgba(124, 92, 255, 0.1)); + border: 1px solid rgba(91, 157, 255, 0.22); + box-shadow: 0 8px 24px rgba(91, 157, 255, 0.12); + animation: ui-logo-glow 4s ease-in-out infinite; +} + +.net-page > .net-pipeline { + animation: ui-fade-slide-up 0.45s var(--ease) 0.04s both; +} + +.net-hero { + animation: ui-fade-slide-up 0.48s var(--ease) 0.08s both; +} + +.net-checklist-card, +.net-chart-card, +.net-tips, +.feature-card--proxy { + animation: ui-fade-slide-up 0.5s var(--ease) both; +} + +.net-chart-card { + animation-delay: 0.12s; +} + +.feature-card--proxy { + animation-delay: 0.16s; +} + +.net-tips { + animation-delay: 0.2s; +} + +.feature-card__title { + display: inline-flex; + align-items: center; + gap: 8px; +} + +/* 链路可视化 */ +.net-pipeline { + display: flex; + align-items: stretch; + gap: 0; + padding: 4px 0 2px; + overflow-x: auto; + scrollbar-width: thin; +} + +.net-pipeline__segment { + display: flex; + align-items: center; + flex: 1; + min-width: 140px; +} + +.net-pipeline__flow { + flex-shrink: 0; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + color: var(--accent); + opacity: 0.55; + transition: opacity 0.3s var(--ease); +} + +.net-pipeline__flow.is-active { + opacity: 1; +} + +.net-pipeline__flow-line, +.net-pipeline__flow-head { + stroke: currentColor; + stroke-opacity: 0.45; +} + +.net-pipeline__flow-dot { + fill: var(--accent); +} + +.net-pipeline__flow-dot--a { + animation: ui-flow-dot 2.4s ease-in-out infinite; +} + +.net-pipeline__flow-dot--b { + animation: ui-flow-dot 2.4s ease-in-out 0.6s infinite; +} + +.net-pipeline__step { + position: relative; + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-height: 68px; + padding: 12px 14px; + border-radius: 14px; + color: #fff; + overflow: hidden; + box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.1); + transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease); +} + +.net-pipeline__step:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.12); +} + +.net-pipeline__step--source { + background: linear-gradient(135deg, #f59e0b 0%, #ea580c 100%); +} + +.net-pipeline__step--route { + background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); +} + +.net-pipeline__step--youtube { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); +} + +.net-pipeline__step--upload { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); +} + +.net-pipeline__step--warn { + filter: saturate(0.85); +} + +.net-pipeline__step--error { + filter: saturate(0.7) brightness(0.92); +} + +.net-pipeline__step-icon { + flex-shrink: 0; + width: 36px; + height: 36px; + display: grid; + place-items: center; + border-radius: 10px; + background: rgba(255, 255, 255, 0.16); + backdrop-filter: blur(4px); +} + +.net-pipeline__step-text span { + display: block; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + opacity: 0.82; +} + +.net-pipeline__step-text strong { + display: block; + font-size: 12.5px; + font-weight: 800; + line-height: 1.3; +} + +.net-pipeline__step-glow { + position: absolute; + inset: 0; + background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.2) 50%, transparent 60%); + transform: translateX(-120%); + pointer-events: none; +} + +.net-pipeline__step-glow--active, +.net-pipeline__step-glow--pending { + animation: ui-shimmer 3.2s ease-in-out infinite; +} + +/* 站点卡片增强 */ +.net-site { + position: relative; + overflow: hidden; + transition: + transform 0.28s var(--ease), + box-shadow 0.28s var(--ease), + border-color 0.25s var(--ease); +} + +.net-site:hover { + transform: translateY(-3px); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18); +} + +.net-site__glow { + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: linear-gradient(120deg, rgba(91, 157, 255, 0.35), rgba(61, 214, 140, 0.2), transparent 70%); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s var(--ease); +} + +.net-site:hover .net-site__glow { + opacity: 0.75; +} + +.net-site.is-loading::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(105deg, transparent 35%, rgba(255, 255, 255, 0.06) 50%, transparent 65%); + transform: translateX(-120%); + animation: ui-shimmer 1.8s ease-in-out infinite; + pointer-events: none; +} + +.net-site__brand { + display: flex; + align-items: center; + gap: 10px; +} + +.net-site__brand-icon { + width: 40px; + height: 40px; + display: grid; + place-items: center; + border-radius: 12px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-subtle); +} + +.net-site--google .net-site__brand-icon { + background: rgba(66, 133, 244, 0.1); + border-color: rgba(66, 133, 244, 0.2); +} + +.net-site--douyin .net-site__brand-icon { + color: #f59e0b; + background: rgba(245, 158, 11, 0.1); + border-color: rgba(245, 158, 11, 0.22); +} + +.net-site__stat-k { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.net-site__stat-v--ok { + color: var(--success); +} + +.net-site__stat-v--warn { + color: var(--warn); +} + +.net-site__stat-v--bad { + color: var(--danger); +} + +.net-site__bars { + margin: 8px 0 10px; +} + +.net-site__skeleton { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 8px; +} + +.net-site__skeleton span { + height: 10px; + border-radius: 6px; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.04)); + background-size: 200% 100%; + animation: net-skeleton 1.4s ease-in-out infinite; +} + +.net-site__skeleton span:nth-child(2) { + width: 72%; + animation-delay: 0.15s; +} + +.net-site__skeleton span:nth-child(3) { + width: 55%; + animation-delay: 0.3s; +} + +@keyframes net-skeleton { + 0% { + background-position: 100% 0; + } + 100% { + background-position: -100% 0; + } +} + +/* 指标条 */ +.metric-bar { + margin-top: 6px; +} + +.metric-bar__track { + height: 5px; + border-radius: var(--radius-pill); + background: rgba(255, 255, 255, 0.06); + overflow: hidden; +} + +.metric-bar__fill { + height: 100%; + border-radius: inherit; + transition: width 0.65s var(--ease); + background: linear-gradient(90deg, var(--accent), rgba(91, 157, 255, 0.55)); +} + +.metric-bar--ok .metric-bar__fill { + background: linear-gradient(90deg, var(--success), rgba(61, 214, 140, 0.55)); +} + +.metric-bar--warn .metric-bar__fill { + background: linear-gradient(90deg, var(--warn), rgba(246, 173, 85, 0.55)); +} + +.metric-bar--bad .metric-bar__fill { + background: linear-gradient(90deg, var(--danger), rgba(245, 101, 101, 0.55)); +} + +.metric-bar--neutral .metric-bar__fill { + background: linear-gradient(90deg, #7c9cff, rgba(124, 156, 255, 0.5)); +} + +.net-speed-bars__row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 5px; +} + +.net-speed-bars__label { + width: 14px; + font-size: 11px; + font-weight: 800; + color: var(--text-muted); +} + +.net-speed-bars__track { + flex: 1; + height: 6px; + border-radius: var(--radius-pill); + background: rgba(255, 255, 255, 0.06); + overflow: hidden; +} + +.net-speed-bars__fill { + height: 100%; + border-radius: inherit; + transition: width 0.7s var(--ease); +} + +.net-speed-bars__fill--up { + background: linear-gradient(90deg, #34d399, #059669); +} + +.net-speed-bars__fill--down { + background: linear-gradient(90deg, #60a5fa, #3b82f6); +} + +/* 图表 */ +.net-speed-chart { + width: 100%; + height: 220px; + margin-top: 2px; +} + +.net-block--chart .net-speed-chart { + height: 196px; +} + +.net-chart-card { + border-color: rgba(124, 156, 255, 0.2); + background: linear-gradient(165deg, rgba(124, 156, 255, 0.06), var(--bg-card) 55%); +} + +/* 线路可视化 */ +.net-routes--viz { + border-color: rgba(91, 157, 255, 0.2); +} + +.net-routes__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 10px; +} + +.net-routes__label { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.net-route-viz { + display: flex; + align-items: center; + justify-content: center; + gap: 0; + padding: 12px 8px; + margin-bottom: 8px; +} + +.net-route-viz__node { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + min-width: 64px; + padding: 10px 12px; + border-radius: 12px; + font-size: 20px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border-subtle); + opacity: 0.55; + transition: opacity 0.3s var(--ease), transform 0.3s var(--ease), box-shadow 0.3s var(--ease); +} + +.net-route-viz__node small { + font-size: 10px; + font-weight: 700; + color: var(--text-muted); +} + +.net-route-viz__node.is-on { + opacity: 1; + transform: translateY(-2px); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14); +} + +.net-route-viz__node--cn.is-on { + border-color: rgba(245, 158, 11, 0.35); + background: rgba(245, 158, 11, 0.08); +} + +.net-route-viz__node--gl.is-on { + border-color: rgba(91, 157, 255, 0.35); + background: rgba(91, 157, 255, 0.08); +} + +.net-route-viz__path { + position: relative; + flex: 1; + max-width: 140px; + height: 3px; + margin: 0 6px; + border-radius: var(--radius-pill); + background: rgba(255, 255, 255, 0.08); + overflow: visible; +} + +.net-route-viz__path.is-split { + background: linear-gradient(90deg, rgba(245, 158, 11, 0.5), rgba(91, 157, 255, 0.5)); +} + +.net-route-viz__dot { + position: absolute; + top: 50%; + width: 8px; + height: 8px; + margin-top: -4px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 10px rgba(91, 157, 255, 0.6); +} + +.net-route-viz__dot--a { + left: 0; + animation: net-route-dot 2.2s ease-in-out infinite; +} + +.net-route-viz__dot--b { + left: 0; + background: var(--success); + box-shadow: 0 0 10px rgba(61, 214, 140, 0.55); + animation: net-route-dot 2.2s ease-in-out 0.55s infinite; +} + +@keyframes net-route-dot { + 0% { + left: 0; + opacity: 0; + } + 15% { + opacity: 1; + } + 85% { + opacity: 1; + } + 100% { + left: calc(100% - 8px); + opacity: 0; + } +} + +/* 摘要侧栏 */ +.net-summary__score { + display: flex; + align-items: center; + gap: 14px; +} + +.net-summary__btn { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.net-summary__btn svg.is-spinning { + animation: ui-startup-orbit 0.9s linear infinite; +} + +.net-hero-aside { + background: linear-gradient(165deg, rgba(124, 156, 255, 0.1), rgba(0, 0, 0, 0.12)); +} + +.net-checklist-card { + border-color: rgba(61, 214, 140, 0.18); +} + +.net-tips__list li { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.net-tips__emoji { + flex-shrink: 0; + font-size: 15px; + line-height: 1.45; +} + +@media (max-width: 960px) { + .net-split-row { + grid-template-columns: 1fr; + } + + .net-tips__list--grid { + grid-template-columns: 1fr; + } + + .net-page__toolbar { + flex-direction: column; + } + + .net-page__toolbar-actions { + width: 100%; + } +} + +@media (max-width: 900px) { + .net-pipeline { + flex-direction: column; + gap: 6px; + } + + .net-pipeline__segment { + flex-direction: column; + width: 100%; + min-width: 0; + } + + .net-pipeline__flow { + transform: rotate(90deg); + height: 28px; + } + + .net-summary__score { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/electron/src/shared/featureFlags.ts b/electron/src/shared/featureFlags.ts new file mode 100644 index 0000000..97061e7 --- /dev/null +++ b/electron/src/shared/featureFlags.ts @@ -0,0 +1,29 @@ +/** 是否在导航中展示「摄像头」功能页 */ +export const SHOW_CAMERA_UI = false + +/** 是否在导航中展示「拉流」功能页 */ +export const SHOW_PULL_STREAM_UI = false + +export type AppNavId = 'record' | 'camera' | 'pull' | 'network' | 'system' + +const DISABLED_NAV_IDS = new Set( + [ + !SHOW_CAMERA_UI ? ('camera' as const) : null, + !SHOW_PULL_STREAM_UI ? ('pull' as const) : null, + ].filter((id): id is AppNavId => id != null), +) + +export function isNavEnabled(id: AppNavId): boolean { + return !DISABLED_NAV_IDS.has(id) +} + +/** 深链 / IPC 导航:禁用项回落到「直播」页 */ +export function sanitizeNavId(raw: string | null | undefined): AppNavId | null { + if (!raw) return null + const id = raw === 'proxy' ? 'network' : raw + if (id !== 'record' && id !== 'camera' && id !== 'pull' && id !== 'network' && id !== 'system') { + return null + } + if (!isNavEnabled(id)) return 'record' + return id +} diff --git a/third_party/IPQuality/ip.sh b/third_party/IPQuality/ip.sh new file mode 100644 index 0000000..31a13f0 --- /dev/null +++ b/third_party/IPQuality/ip.sh @@ -0,0 +1,2642 @@ +#!/bin/bash +script_version="v2026-03-29" +check_bash(){ +current_bash_version=$(bash --version|head -n 1|awk -F ' ' '{for (i=1; i<=NF; i++) if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+/) {print $i; exit}}'|cut -d . -f 1) +if [ "$current_bash_version" = "0" ]||[ "$current_bash_version" = "1" ]||[ "$current_bash_version" = "2" ]||[ "$current_bash_version" = "3" ];then +echo "ERROR: Bash version is lower than 4.0!" +echo "Tips: Run the following script to automatically upgrade Bash." +echo "bash <(curl -sL https://raw.githubusercontent.com/xykt/IPQuality/main/ref/upgrade_bash.sh)" +exit 0 +fi +} +check_bash +Font_B="\033[1m" +Font_D="\033[2m" +Font_I="\033[3m" +Font_U="\033[4m" +Font_Black="\033[30m" +Font_Red="\033[31m" +Font_Green="\033[32m" +Font_Yellow="\033[33m" +Font_Blue="\033[34m" +Font_Purple="\033[35m" +Font_Cyan="\033[36m" +Font_White="\033[37m" +Back_Black="\033[40m" +Back_Red="\033[41m" +Back_Green="\033[42m" +Back_Yellow="\033[43m" +Back_Blue="\033[44m" +Back_Purple="\033[45m" +Back_Cyan="\033[46m" +Back_White="\033[47m" +Font_Suffix="\033[0m" +Font_LineClear="\033[2K" +Font_LineUp="\033[1A" +declare ADLines +declare -A aad +declare IP="" +declare IPhide +declare fullIP=0 +declare YY="cn" +declare -A maxmind +declare -A ipinfo +declare -A scamalytics +declare -A ipregistry +declare -A ipapi +declare -A abuseipdb +declare -A ip2location +declare -A dbip +declare -A ipdata +declare -A ipqs +declare -A tiktok +declare -A disney +declare -A netflix +declare -A youtube +declare -A amazon +declare -A reddit +declare -A chatgpt +declare IPV4 +declare IPV6 +declare IPV4check=1 +declare IPV6check=1 +declare IPV4work=0 +declare IPV6work=0 +declare ERRORcode=0 +declare shelp +declare -A swarn +declare -A sinfo +declare -A shead +declare -A sbasic +declare -A stype +declare -A sscore +declare -A sfactor +declare -A smedia +declare -A smail +declare -A smailstatus +declare -A stail +declare mode_no=0 +declare mode_yes=0 +declare mode_lite=0 +declare mode_json=0 +declare mode_menu=0 +declare mode_output=0 +declare mode_privacy=0 +declare ipjson +declare ibar=0 +declare bar_pid +declare ibar_step=0 +declare main_pid=$$ +declare PADDING="" +declare useNIC="" +declare usePROXY="" +declare CurlARG="" +declare UA_Browser +declare rawgithub +declare Media_Cookie +declare IATA_Database +shelp_lines=( +"IP QUALITY CHECK SCRIPT IP质量体检脚本" +"Interactive Interface: bash <(curl -sL https://IP.Check.Place) -EM" +"交互界面: bash <(curl -sL https://IP.Check.Place) -M" +"Parameters 参数运行: bash <(curl -sL https://IP.Check.Place) [-4] [-6] [-f] [-h] [-i iface] [-j] [-l language] [-n] [-o outputpath] [-p] [-x proxy] [-y] [-E] [-M]" +" -4 Test IPv4 测试IPv4" +" -6 Test IPv6 测试IPv6" +" -f Show full IP on reports 报告展示完整IP地址" +" -h Help information 帮助信息" +" -i eth0 Specify network interface 指定检测网卡" +" ipaddress Specify outbound IP Address 指定检测出口IP" +" -j JSON output JSON输出" +" -l cn|en|jp|es|de|fr|ru|pt Specify script language 指定报告语言" +" -n No OS or dependencies check 跳过系统检测及依赖安装" +" -o /path/to/file.ansi Output ANSI report to file 输出ANSI报告至文件" +" /path/to/file.json Output JSON result to file 输出JSON结果至文件" +" /path/to/file.anyother Output plain text report to file 输出纯文本报告至文件" +" -p Privacy mode - no generate report link 隐私模式:不生成报告链接" +" -x http://usr:pwd@proxyurl:p Specify http proxy 指定http代理" +" https://usr:pwd@proxyurl:p Specify https proxy 指定https代理" +" socks5://usr:pwd@proxyurl:p Specify socks5 proxy 指定socks5代理" +" -y Install dependencies without interupt 自动安装依赖" +" -E Specify English Output 指定英文输出" +" -M Run with Interactive Interface 交互界面方式运行") +shelp=$(printf "%s\n" "${shelp_lines[@]}") +set_language(){ +case "$YY" in +"en"|"jp"|"es"|"de"|"fr"|"ru"|"pt")swarn[1]="ERROR: Unsupported parameters!" +swarn[2]="ERROR: IP address format error!" +swarn[3]="ERROR: Dependent programs are missing. Please run as root or install sudo!" +swarn[4]="ERROR: Parameter -4 conflicts with -i or -6!" +swarn[6]="ERROR: Parameter -6 conflicts with -i or -4!" +swarn[7]="ERROR: The specified network interface or outbound IP is invalid or does not exist!" +swarn[8]="ERROR: The specified proxy parameter is invalid or not working!" +swarn[10]="ERROR: Output file already exist!" +swarn[11]="ERROR: Output file is not writable!" +swarn[40]="ERROR: IPv4 is not available!" +swarn[60]="ERROR: IPv6 is not available!" +sinfo[database]="Checking IP database " +sinfo[media]="Checking stream media " +sinfo[ai]="Checking AI provider " +sinfo[mail]="Connecting Email server " +sinfo[dnsbl]="Checking Blacklist database " +sinfo[ldatabase]=21 +sinfo[lmedia]=22 +sinfo[lai]=21 +sinfo[lmail]=24 +sinfo[ldnsbl]=28 +shead[title]="IP QUALITY CHECK REPORT: " +shead[title_lite]="IP QUALITY CHECK REPORT(LITE): " +shead[ver]="Version: $script_version" +shead[bash]="bash <(curl -sL https://Check.Place) -EI" +shead[git]="https://github.com/xykt/IPQuality" +shead[time_raw]=$(date -u +"%Y-%m-%d %H:%M:%S UTC") +shead[time]="Report Time: ${shead[time_raw]}" +shead[ltitle]=25 +shead[ltitle_lite]=31 +shead[ptime]=$(printf '%7s' '') +sbasic[title]="1. Basic Information (${Font_I}Maxmind Database$Font_Suffix)" +sbasic[title_lite]="1. Basic Information (${Font_I}IPinfo Database$Font_Suffix)" +sbasic[asn]="ASN: " +sbasic[noasn]="Not Assigned" +sbasic[org]="Organization: " +sbasic[location]="Location: " +sbasic[map]="Map: " +sbasic[city]="City: " +sbasic[country]="Actual Region: " +sbasic[regcountry]="Registered Region: " +sbasic[continent]="Continent: " +sbasic[timezone]="Time Zone: " +sbasic[type]="IP Type: " +sbasic[type0]=" Geo-consistent " +sbasic[type1]=" Geo-discrepant " +stype[business]=" $Back_Yellow$Font_White$Font_B Business $Font_Suffix " +stype[isp]=" $Back_Green$Font_White$Font_B ISP $Font_Suffix " +stype[hosting]=" $Back_Red$Font_White$Font_B Hosting $Font_Suffix " +stype[education]="$Back_Yellow$Font_White$Font_B Education $Font_Suffix " +stype[government]="$Back_Yellow$Font_White$Font_B Government $Font_Suffix" +stype[banking]=" $Back_Yellow$Font_White$Font_B Banking $Font_Suffix " +stype[organization]="$Back_Yellow$Font_White${Font_B}Organization$Font_Suffix" +stype[military]=" $Back_Yellow$Font_White$Font_B Military $Font_Suffix " +stype[library]=" $Back_Yellow$Font_White$Font_B Library $Font_Suffix " +stype[cdn]=" $Back_Red$Font_White$Font_B CDN $Font_Suffix " +stype[lineisp]=" $Back_Green$Font_White$Font_B Line ISP $Font_Suffix " +stype[mobile]="$Back_Green$Font_White$Font_B Mobile ISP $Font_Suffix" +stype[spider]="$Back_Red$Font_White$Font_B Web Spider $Font_Suffix" +stype[reserved]=" $Back_Yellow$Font_White$Font_B Reserved $Font_Suffix " +stype[other]=" $Back_Yellow$Font_White$Font_B Other $Font_Suffix " +stype[title]="2. IP Type" +stype[db]="Database: " +stype[usetype]="Usage: " +stype[comtype]="Company: " +sscore[verylow]="$Font_Green${Font_B}VeryLow$Font_Suffix" +sscore[low]="$Font_Green${Font_B}Low$Font_Suffix" +sscore[medium]="$Font_Yellow${Font_B}Medium$Font_Suffix" +sscore[high]="$Font_Red${Font_B}High$Font_Suffix" +sscore[veryhigh]="$Font_Red${Font_B}VeryHigh$Font_Suffix" +sscore[elevated]="$Font_Yellow${Font_B}Elevated$Font_Suffix" +sscore[suspicious]="$Font_Yellow${Font_B}Suspicious$Font_Suffix" +sscore[risky]="$Font_Red${Font_B}Risky$Font_Suffix" +sscore[highrisk]="$Font_Red${Font_B}HighRisk$Font_Suffix" +sscore[dos]="$Font_Red${Font_B}DoS$Font_Suffix" +sscore[colon]=": " +sscore[title]="3. Risk Score" +sscore[range]="${Font_Cyan}Levels: $Font_I$Font_White${Back_Green}VeryLow Low $Back_Yellow Medium $Back_Red High VeryHigh$Font_Suffix" +sfactor[title]="4. Risk Factors" +sfactor[factor]="DB: " +sfactor[countrycode]="Region: " +sfactor[proxy]="Proxy: " +sfactor[tor]="Tor: " +sfactor[vpn]="VPN: " +sfactor[server]="Server: " +sfactor[abuser]="Abuser: " +sfactor[robot]="Robot: " +sfactor[yes]="$Font_Red$Font_B Yes$Font_Suffix" +sfactor[no]="$Font_Green$Font_B No $Font_Suffix" +sfactor[na]="$Font_Green$Font_B N/A$Font_Suffix" +smedia[yes]=" $Back_Green$Font_White Yes $Font_Suffix " +smedia[no]=" $Back_Red$Font_White Block $Font_Suffix " +smedia[bad]="$Back_Red$Font_White Failed $Font_Suffix " +smedia[pending]="$Back_Yellow$Font_White Pending $Font_Suffix" +smedia[cn]=" $Back_Red$Font_White China $Font_Suffix " +smedia[noprem]="$Back_Red$Font_White NoPrem. $Font_Suffix" +smedia[org]="$Back_Yellow$Font_White NF.Only $Font_Suffix" +smedia[web]="$Back_Yellow$Font_White WebOnly $Font_Suffix" +smedia[app]="$Back_Yellow$Font_White APPOnly $Font_Suffix" +smedia[idc]=" $Back_Yellow$Font_White IDC $Font_Suffix " +smedia[native]="$Back_Green$Font_White Native $Font_Suffix " +smedia[dns]="$Back_Yellow$Font_White ViaDNS $Font_Suffix " +smedia[nodata]=" " +smedia[title]="5. Accessibility check for media and AI services" +smedia[meida]="Service: " +smedia[status]="Status: " +smedia[region]="Region: " +smedia[type]="Type: " +smail[title]="6. Email service availability and blacklist detection" +smail[port]="Local Port 25 Outbound: " +smail[yes]="${Font_Green}Available$Font_Suffix" +smail[no]="${Font_Red}Blocked$Font_Suffix" +smail[occupied]="${Font_Yellow}Occupied$Font_Suffix" +smail[blocked]="${Font_Red}Remote Port 25 unreachable​$Font_Suffix" +smail[provider]="Conn: " +smail[dnsbl]="DNSBL database: " +smail[available]="$Font_Suffix${Font_Cyan}Active $Font_B" +smail[clean]="$Font_Suffix${Font_Green}Clean $Font_B" +smail[marked]="$Font_Suffix${Font_Yellow}Marked $Font_B" +smail[blacklisted]="$Font_Suffix${Font_Red}Blacklisted $Font_B" +stail[stoday]="IP Checks Today: " +stail[stotal]="; Total: " +stail[thanks]=". Thanks for running xy scripts!" +stail[link]="${Font_I}Report Link: $Font_U" +;; +"cn")swarn[1]="错误:不支持的参数!" +swarn[2]="错误:IP地址格式错误!" +swarn[3]="错误:未安装依赖程序,请以root执行此脚本,或者安装sudo命令!" +swarn[4]="错误:参数-4与-i/-6冲突!" +swarn[6]="错误:参数-6与-i/-4冲突!" +swarn[7]="错误:指定的网卡或出口IP不存在!" +swarn[8]="错误:指定的代理服务器不可用!" +swarn[10]="错误:输出文件已存在!" +swarn[11]="错误:输出文件不可写!" +swarn[40]="错误:IPV4不可用!" +swarn[60]="错误:IPV6不可用!" +sinfo[database]="正在检测IP数据库 " +sinfo[media]="正在检测流媒体服务商 " +sinfo[ai]="正在检测AI服务商 " +sinfo[mail]="正在连接邮件服务商 " +sinfo[dnsbl]="正在检测黑名单数据库 " +sinfo[ldatabase]=17 +sinfo[lmedia]=21 +sinfo[lai]=17 +sinfo[lmail]=19 +sinfo[ldnsbl]=21 +shead[title]="IP质量体检报告:" +shead[title_lite]="IP质量体检报告(Lite):" +shead[ver]="脚本版本:$script_version" +shead[bash]="bash <(curl -sL https://Check.Place) -I" +shead[git]="https://github.com/xykt/IPQuality" +shead[time_raw]=$(TZ="Asia/Shanghai" date +"%Y-%m-%d %H:%M:%S CST") +shead[time]="报告时间:${shead[time_raw]}" +shead[ltitle]=16 +shead[ltitle_lite]=22 +shead[ptime]=$(printf '%8s' '') +sbasic[title]="一、基础信息(${Font_I}Maxmind 数据库$Font_Suffix)" +sbasic[title_lite]="一、基础信息(${Font_I}IPinfo 数据库$Font_Suffix)" +sbasic[asn]="自治系统号: " +sbasic[noasn]="未分配" +sbasic[org]="组织: " +sbasic[location]="坐标: " +sbasic[map]="地图: " +sbasic[city]="城市: " +sbasic[country]="使用地: " +sbasic[regcountry]="注册地: " +sbasic[continent]="洲际: " +sbasic[timezone]="时区: " +sbasic[type]="IP类型: " +sbasic[type0]=" 原生IP " +sbasic[type1]=" 广播IP " +stype[business]=" $Back_Yellow$Font_White$Font_B 商业 $Font_Suffix " +stype[isp]=" $Back_Green$Font_White$Font_B 家宽 $Font_Suffix " +stype[hosting]=" $Back_Red$Font_White$Font_B 机房 $Font_Suffix " +stype[education]=" $Back_Yellow$Font_White$Font_B 教育 $Font_Suffix " +stype[government]=" $Back_Yellow$Font_White$Font_B 政府 $Font_Suffix " +stype[banking]=" $Back_Yellow$Font_White$Font_B 银行 $Font_Suffix " +stype[organization]=" $Back_Yellow$Font_White$Font_B 组织 $Font_Suffix " +stype[military]=" $Back_Yellow$Font_White$Font_B 军队 $Font_Suffix " +stype[library]=" $Back_Yellow$Font_White$Font_B 图书馆 $Font_Suffix " +stype[cdn]=" $Back_Red$Font_White$Font_B CDN $Font_Suffix " +stype[lineisp]=" $Back_Green$Font_White$Font_B 家宽 $Font_Suffix " +stype[mobile]=" $Back_Green$Font_White$Font_B 手机 $Font_Suffix " +stype[spider]=" $Back_Red$Font_White$Font_B 蜘蛛 $Font_Suffix " +stype[reserved]=" $Back_Yellow$Font_White$Font_B 保留 $Font_Suffix " +stype[other]=" $Back_Yellow$Font_White$Font_B 其他 $Font_Suffix " +stype[title]="二、IP类型属性" +stype[db]="数据库: " +stype[usetype]="使用类型: " +stype[comtype]="公司类型: " +sscore[verylow]="$Font_Green$Font_B极低风险$Font_Suffix" +sscore[low]="$Font_Green$Font_B低风险$Font_Suffix" +sscore[medium]="$Font_Yellow$Font_B中风险$Font_Suffix" +sscore[high]="$Font_Red$Font_B高风险$Font_Suffix" +sscore[veryhigh]="$Font_Red$Font_B极高风险$Font_Suffix" +sscore[elevated]="$Font_Yellow$Font_B较高风险$Font_Suffix" +sscore[suspicious]="$Font_Yellow$Font_B可疑IP$Font_Suffix" +sscore[risky]="$Font_Red$Font_B存在风险$Font_Suffix" +sscore[highrisk]="$Font_Red$Font_B高风险$Font_Suffix" +sscore[dos]="$Font_Red$Font_B建议封禁$Font_Suffix" +sscore[colon]=":" +sscore[title]="三、风险评分" +sscore[range]="$Font_Cyan风险等级: $Font_I$Font_White$Back_Green极低 低 $Back_Yellow 中等 $Back_Red 高 极高$Font_Suffix" +sfactor[title]="四、风险因子" +sfactor[factor]="库: " +sfactor[countrycode]="地区: " +sfactor[proxy]="代理: " +sfactor[tor]="Tor: " +sfactor[vpn]="VPN: " +sfactor[server]="服务器:" +sfactor[abuser]="滥用: " +sfactor[robot]="机器人:" +sfactor[yes]="$Font_Red$Font_B 是 $Font_Suffix" +sfactor[no]="$Font_Green$Font_B 否 $Font_Suffix" +sfactor[na]="$Font_Green$Font_B 无 $Font_Suffix" +smedia[yes]=" $Back_Green$Font_White 解锁 $Font_Suffix " +smedia[no]=" $Back_Red$Font_White 屏蔽 $Font_Suffix " +smedia[bad]=" $Back_Red$Font_White 失败 $Font_Suffix " +smedia[pending]="$Back_Yellow$Font_White 待支持 $Font_Suffix " +smedia[cn]=" $Back_Red$Font_White 中国 $Font_Suffix " +smedia[noprem]="$Back_Red$Font_White 禁会员 $Font_Suffix " +smedia[org]="$Back_Yellow$Font_White 仅自制 $Font_Suffix " +smedia[web]="$Back_Yellow$Font_White 仅网页 $Font_Suffix " +smedia[app]=" $Back_Yellow$Font_White 仅APP $Font_Suffix " +smedia[idc]=" $Back_Yellow$Font_White 机房 $Font_Suffix " +smedia[native]=" $Back_Green$Font_White 原生 $Font_Suffix " +smedia[dns]=" $Back_Yellow$Font_White DNS $Font_Suffix " +smedia[nodata]=" " +smedia[title]="五、流媒体及AI服务解锁检测" +smedia[meida]="服务商: " +smedia[status]="状态: " +smedia[region]="地区: " +smedia[type]="方式: " +smail[title]="六、邮局连通性及黑名单检测" +smail[port]="本地25端口出站:" +smail[yes]="$Font_Green可用$Font_Suffix" +smail[no]="$Font_Red阻断$Font_Suffix" +smail[occupied]="$Font_Yellow占用$Font_Suffix" +smail[blocked]="$Font_Red远端25端口不可达​$Font_Suffix" +smail[provider]="通信:" +smail[dnsbl]="IP地址黑名单数据库:" +smail[available]="$Font_Suffix$Font_Cyan有效 $Font_B" +smail[clean]="$Font_Suffix$Font_Green正常 $Font_B" +smail[marked]="$Font_Suffix$Font_Yellow已标记 $Font_B" +smail[blacklisted]="$Font_Suffix$Font_Red黑名单 $Font_B" +stail[stoday]="今日IP检测量:" +stail[stotal]=";总检测量:" +stail[thanks]="。感谢使用xy系列脚本!" +stail[link]="$Font_I报告链接:$Font_U" +;; +*)echo -ne "ERROR: Language not supported!" +esac +} +countRunTimes(){ +local RunTimes=$(curl $CurlARG -s --max-time 10 "https://hits.xykt.de/ip?action=hit" 2>&1) +stail[today]=$(echo "$RunTimes"|jq '.daily') +stail[total]=$(echo "$RunTimes"|jq '.total') +} +show_progress_bar(){ +show_progress_bar_ "$@" 1>&2 +} +show_progress_bar_(){ +local bar="\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F" +local n=${#bar} +while sleep 0.1;do +if ! kill -0 $main_pid 2>/dev/null;then +echo -ne "" +exit +fi +echo -ne "\r$Font_Cyan$Font_B[$IP]# $1$Font_Cyan$Font_B$(printf '%*s' "$2" ''|tr ' ' '.') ${bar:ibar++*6%n:6} $(printf '%02d%%' $ibar_step) $Font_Suffix" +done +} +kill_progress_bar(){ +kill "$bar_pid" 2>/dev/null&&echo -ne "\r" +} +install_dependencies(){ +if ! jq --version >/dev/null 2>&1||! curl --version >/dev/null 2>&1||! bc --version >/dev/null 2>&1||! nc -h >/dev/null 2>&1||! dig -v >/dev/null 2>&1;then +echo "Detecting operating system..." +if [ "$(uname)" == "Darwin" ];then +install_packages "brew" "brew install" "no_sudo" +elif [ -f /etc/os-release ];then +. /etc/os-release +if [ $(id -u) -ne 0 ]&&! sudo -v >/dev/null 2>&1;then +ERRORcode=3 +fi +case $ID in +ubuntu|debian|linuxmint)install_packages "apt" "apt-get install -y" +;; +rhel|centos|almalinux|rocky|anolis)if +[ "$(echo $VERSION_ID|cut -d '.' -f1)" -ge 8 ] +then +install_packages "dnf" "dnf install -y" +else +install_packages "yum" "yum install -y" +fi +;; +arch|manjaro)install_packages "pacman" "pacman -S --noconfirm" +;; +alpine)install_packages "apk" "apk add" +;; +fedora)install_packages "dnf" "dnf install -y" +;; +alinux)install_packages "yum" "yum install -y" +;; +suse|opensuse*)install_packages "zypper" "zypper install -y" +;; +void)install_packages "xbps" "xbps-install -Sy" +;; +*)echo "Unsupported distribution: $ID" +exit 1 +esac +elif [ -n "$PREFIX" ];then +install_packages "pkg" "pkg install" +else +echo "Cannot detect distribution because /etc/os-release is missing." +exit 1 +fi +fi +} +install_packages(){ +local package_manager=$1 +local install_command=$2 +local no_sudo=$3 +echo "Using package manager: $package_manager" +echo -e "Lacking necessary dependencies, $Font_I${Font_Cyan}jq curl bc netcat dnsutils iproute$Font_Suffix will be installed using $Font_I$Font_Cyan$package_manager$Font_Suffix." +if [[ $mode_yes -eq 0 ]];then +prompt=$(printf "Continue? (${Font_Green}y$Font_Suffix/${Font_Red}n$Font_Suffix): ") +read -p "$prompt" choice +case "$choice" in +y|Y|yes|Yes|YES)echo "Continue to execute script..." +;; +n|N|no|No|NO)echo "Script exited." +exit 0 +;; +*)echo "Invalid input, script exited." +exit 1 +esac +else +echo -e "Detected parameter $Font_Green-y$Font_Suffix. Continue installation..." +fi +if [ "$no_sudo" == "no_sudo" ]||[ $(id -u) -eq 0 ];then +local usesudo="" +else +local usesudo="sudo" +fi +case $package_manager in +apt)$usesudo apt update +$usesudo $install_command jq curl bc netcat-openbsd dnsutils iproute2 +;; +dnf|yum)$usesudo $install_command epel-release +$usesudo $package_manager makecache +$usesudo $install_command jq curl bc nmap-ncat bind-utils iproute +;; +pacman)$usesudo pacman -Sy +$usesudo $install_command jq curl bc gnu-netcat bind-tools iproute2 +;; +apk)$usesudo apk update +$usesudo $install_command jq curl bc netcat-openbsd grep bind-tools iproute2 +;; +pkg)$usesudo $package_manager update +$usesudo $package_manager $install_command jq curl bc netcat dnsutils iproute +;; +brew)eval "$(/opt/homebrew/bin/brew shellenv)" +$install_command jq curl bc netcat bind +;; +zypper)$usesudo zypper refresh +$usesudo $install_command jq curl bc netcat bind-utils iproute2 +;; +xbps)$usesudo xbps-install -Sy +$usesudo $install_command jq curl bc netcat bind-utils iproute2 +esac +} +declare -A browsers=( +[Chrome]="145.0.0.0 144.0.0.0 143.0.0.0 142.0.0.0 141.0.0.0 140.0.0.0" +[Firefox]="147.0 146.0 145.0 144.0 143.0 142.0 141.0 140.0") +generate_random_user_agent(){ +local browsers_keys=(${!browsers[@]}) +local random_browser_index=$((RANDOM%${#browsers_keys[@]})) +local browser=${browsers_keys[random_browser_index]} +case $browser in +Chrome)local versions=(${browsers[Chrome]}) +local version=${versions[RANDOM%${#versions[@]}]} +UA_Browser="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$version Safari/537.36" +;; +Firefox)local versions=(${browsers[Firefox]}) +local version=${versions[RANDOM%${#versions[@]}]} +UA_Browser="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:$version) Gecko/20100101 Firefox/$version" +esac +} +adapt_locale(){ +local ifunicode=$(printf '\u2800') +[[ ${#ifunicode} -gt 3 ]]&&export LC_CTYPE=en_US.UTF-8 2>/dev/null +} +check_connectivity(){ +local url="https://www.google.com/generate_204" +local timeout=2 +local http_code +http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout "$timeout" "$url" 2>/dev/null) +if [[ $http_code == "204" ]];then +rawgithub="https://github.com/xykt/IPQuality/raw/" +return 0 +else +rawgithub="https://testingcf.jsdelivr.net/gh/xykt/IPQuality@" +return 1 +fi +} +is_valid_ipv4(){ +local ip=$1 +if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]];then +IFS='.' read -r -a octets <<<"$ip" +for octet in "${octets[@]}";do +if ((octet<0||octet>255));then +IPV4work=0 +return 1 +fi +done +IPV4work=1 +return 0 +else +IPV4work=0 +return 1 +fi +} +is_private_ipv4(){ +local ip_address=$1 +if [[ -z $ip_address ]];then +return 0 +fi +if [[ $ip_address =~ ^10\. ]]||[[ $ip_address =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]]||[[ $ip_address =~ ^192\.168\. ]]||[[ $ip_address =~ ^127\. ]]||[[ $ip_address =~ ^0\. ]]||[[ $ip_address =~ ^22[4-9]\. ]]||[[ $ip_address =~ ^23[0-9]\. ]];then +return 0 +fi +return 1 +} +get_ipv4(){ +local response +IPV4="" +local API_NET=("ipinfo.io/ip" "myip.check.place" "ip.sb" "ping0.cc" "icanhazip.com" "api64.ipify.org" "ifconfig.co" "ident.me") +for p in "${API_NET[@]}";do +response=$(curl $CurlARG -s4 --max-time 2 "$p") +if [[ $? -eq 0 && $response =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]];then +IPV4="$response" +break +fi +done +} +hide_ipv4(){ +if [[ -n $1 ]];then +IFS='.' read -r -a ip_parts <<<"$1" +IPhide="${ip_parts[0]}.${ip_parts[1]}.*.*" +else +IPhide="" +fi +} +is_valid_ipv6(){ +local ip=$1 +if [[ $ip =~ ^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$ || $ip =~ ^([0-9a-fA-F]{1,4}:){1,7}:$ || $ip =~ ^:([0-9a-fA-F]{1,4}:){1,7}$ || $ip =~ ^([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$ || $ip =~ ^([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}$ || $ip =~ ^([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}$ || $ip =~ ^([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}$ || $ip =~ ^([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}$ || $ip =~ ^[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})$ || $ip =~ ^:((:[0-9a-fA-F]{1,4}){1,7}|:)$ || $ip =~ ^fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}$ || $ip =~ ^::(ffff(:0{1,4}){0,1}:){0,1}(([0-9]{1,3}\.){3}[0-9]{1,3})$ || $ip =~ ^([0-9a-fA-F]{1,4}:){1,4}:(([0-9]{1,3}\.){3}[0-9]{1,3})$ ]];then +IPV6work=1 +return 0 +else +IPV6work=0 +return 1 +fi +} +is_private_ipv6(){ +local address=$1 +if [[ -z $address ]];then +return 0 +fi +if [[ $address =~ ^fe80: ]]||[[ $address =~ ^fc00: ]]||[[ $address =~ ^fd00: ]]||[[ $address =~ ^2001:db8: ]]||[[ $address == ::1 ]]||[[ $address =~ ^::ffff: ]]||[[ $address =~ ^2002: ]]||[[ $address =~ ^2001: ]];then +return 0 +fi +return 1 +} +get_ipv6(){ +local response +IPV6="" +local API_NET=("myip.check.place" "ip.sb" "ping0.cc" "icanhazip.com" "api64.ipify.org" "ifconfig.co" "ident.me") +for p in "${API_NET[@]}";do +response=$(curl $CurlARG -s6k --max-time 2 "$p") +response="${response%$'\n'}" +if [[ $? -eq 0 && $response =~ ^[0-9a-fA-F:]+$ && $response == *:* ]];then +IPV6="$response" +break +fi +done +} +hide_ipv6(){ +if [[ -n $1 ]];then +local expanded_ip=$(echo "$1"|sed 's/::/:0000:0000:0000:0000:0000:0000:0000:0000:/g'|cut -d ':' -f1-8) +IFS=':' read -r -a ip_parts <<<"$expanded_ip" +while [ ${#ip_parts[@]} -lt 8 ];do +ip_parts+=(0000) +done +IPhide="${ip_parts[0]:-0}:${ip_parts[1]:-0}:${ip_parts[2]:-0}:*:*:*:*:*" +IPhide=$(echo "$IPhide"|sed 's/:0\{1,\}/:/g'|sed 's/::\+/:/g') +else +IPhide="" +fi +} +calculate_display_width(){ +local string="$1" +local length=0 +local char +for ((i=0; i<${#string}; i++));do +char=$(echo "$string"|od -An -N1 -tx1 -j $((i))|tr -d ' ') +if [ "$(printf '%d\n' 0x$char)" -gt 127 ];then +length=$((length+2)) +i=$((i+1)) +else +length=$((length+1)) +fi +done +echo "$length" +} +calc_padding(){ +local input_text="$1" +local total_width=$2 +local title_length=$(calculate_display_width "$input_text") +local left_padding=$(((total_width-title_length)/2)) +if [[ $left_padding -gt 0 ]];then +PADDING=$(printf '%*s' $left_padding) +else +PADDING="" +fi +} +generate_dms(){ +local lat=$1 +local lon=$2 +if [[ -z $lat || $lat == "null" || -z $lon || $lon == "null" ]];then +echo "" +return +fi +convert_single(){ +local coord=$1 +local direction=$2 +local fixed_coord=$(echo "$coord"|sed 's/\.$/.0/') +local degrees=$(echo "$fixed_coord"|cut -d'.' -f1) +local fractional="0.$(echo "$fixed_coord"|cut -d'.' -f2)" +local minutes=$(echo "$fractional * 60"|bc -l|cut -d'.' -f1) +local seconds_fractional="0.$(echo "$fractional * 60"|bc -l|cut -d'.' -f2)" +local seconds=$(echo "$seconds_fractional * 60"|bc -l|awk '{printf "%.0f", $1}') +echo "$degrees°$minutes′$seconds″$direction" +} +local lat_dir='N' +if [[ $(echo "$lat < 0"|bc -l) -eq 1 ]];then +lat_dir='S' +lat=$(echo "$lat * -1"|bc -l) +fi +local lon_dir='E' +if [[ $(echo "$lon < 0"|bc -l) -eq 1 ]];then +lon_dir='W' +lon=$(echo "$lon * -1"|bc -l) +fi +local lat_dms=$(convert_single $lat $lat_dir) +local lon_dms=$(convert_single $lon $lon_dir) +echo "$lon_dms, $lat_dms" +} +generate_googlemap_url(){ +local lat=$1 +local lon=$2 +local radius=$3 +if [[ -z $lat || $lat == "null" || -z $lon || $lon == "null" || -z $radius || $radius == "null" ]];then +echo "" +return +fi +local zoom_level=15 +if [[ $radius -gt 1000 ]];then +zoom_level=12 +elif [[ $radius -gt 500 ]];then +zoom_level=13 +elif [[ $radius -gt 250 ]];then +zoom_level=14 +fi +echo "https://check.place/$lat,$lon,$zoom_level,$YY" +} +db_maxmind(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}Maxmind $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-8-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +maxmind=() +local RESPONSE=$(curl $CurlARG -Ls -$1 -m 10 "https://ipinfo.check.place/$IP?lang=$YY") +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +if [[ -z $RESPONSE ]];then +mode_lite=1 +else +mode_lite=0 +fi +maxmind[asn]=$(echo "$RESPONSE"|jq -r '.ASN.AutonomousSystemNumber') +maxmind[org]=$(echo "$RESPONSE"|jq -r '.ASN.AutonomousSystemOrganization') +maxmind[city]=$(echo "$RESPONSE"|jq -r '.City.Name') +maxmind[post]=$(echo "$RESPONSE"|jq -r '.City.PostalCode') +maxmind[lat]=$(echo "$RESPONSE"|jq -r '.City.Latitude') +maxmind[lon]=$(echo "$RESPONSE"|jq -r '.City.Longitude') +maxmind[rad]=$(echo "$RESPONSE"|jq -r '.City.AccuracyRadius') +maxmind[continentcode]=$(echo "$RESPONSE"|jq -r '.City.Continent.Code') +maxmind[continent]=$(echo "$RESPONSE"|jq -r '.City.Continent.Name') +maxmind[citycountrycode]=$(echo "$RESPONSE"|jq -r '.City.Country.IsoCode') +maxmind[citycountry]=$(echo "$RESPONSE"|jq -r '.City.Country.Name') +maxmind[timezone]=$(echo "$RESPONSE"|jq -r '.City.Location.TimeZone') +maxmind[subcode]=$(echo "$RESPONSE"|jq -r 'if .City.Subdivisions | length > 0 then .City.Subdivisions[0].IsoCode else "N/A" end') +maxmind[sub]=$(echo "$RESPONSE"|jq -r 'if .City.Subdivisions | length > 0 then .City.Subdivisions[0].Name else "N/A" end') +maxmind[countrycode]=$(echo "$RESPONSE"|jq -r '.Country.IsoCode') +maxmind[country]=$(echo "$RESPONSE"|jq -r '.Country.Name') +maxmind[regcountrycode]=$(echo "$RESPONSE"|jq -r '.Country.RegisteredCountry.IsoCode') +maxmind[regcountry]=$(echo "$RESPONSE"|jq -r '.Country.RegisteredCountry.Name') +if [[ $YY != "en" ]];then +local backup_response=$(curl $CurlARG -s -$1 -m 10 "https://ipinfo.check.place/$IP?lang=en") +[[ ${maxmind[asn]} == "null" ]]&&maxmind[asn]=$(echo "$backup_response"|jq -r '.ASN.AutonomousSystemNumber') +[[ ${maxmind[org]} == "null" ]]&&maxmind[org]=$(echo "$backup_response"|jq -r '.ASN.AutonomousSystemOrganization') +[[ ${maxmind[city]} == "null" ]]&&maxmind[city]=$(echo "$backup_response"|jq -r '.City.Name') +[[ ${maxmind[post]} == "null" ]]&&maxmind[post]=$(echo "$backup_response"|jq -r '.City.PostalCode') +[[ ${maxmind[lat]} == "null" ]]&&maxmind[lat]=$(echo "$backup_response"|jq -r '.City.Latitude') +[[ ${maxmind[lon]} == "null" ]]&&maxmind[lon]=$(echo "$backup_response"|jq -r '.City.Longitude') +[[ ${maxmind[rad]} == "null" ]]&&maxmind[rad]=$(echo "$backup_response"|jq -r '.City.AccuracyRadius') +[[ ${maxmind[continentcode]} == "null" ]]&&maxmind[continentcode]=$(echo "$backup_response"|jq -r '.City.Continent.Code') +[[ ${maxmind[continent]} == "null" ]]&&maxmind[continent]=$(echo "$backup_response"|jq -r '.City.Continent.Name') +[[ ${maxmind[citycountrycode]} == "null" ]]&&maxmind[citycountrycode]=$(echo "$backup_response"|jq -r '.City.Country.IsoCode') +[[ ${maxmind[citycountry]} == "null" ]]&&maxmind[citycountry]=$(echo "$backup_response"|jq -r '.City.Country.Name') +[[ ${maxmind[timezone]} == "null" ]]&&maxmind[timezone]=$(echo "$backup_response"|jq -r '.City.Location.TimeZone') +[[ ${maxmind[subcode]} == "null" ]]&&maxmind[subcode]=$(echo "$backup_response"|jq -r 'if .City.Subdivisions | length > 0 then .City.Subdivisions[0].IsoCode else "N/A" end') +[[ ${maxmind[sub]} == "null" ]]&&maxmind[sub]=$(echo "$backup_response"|jq -r 'if .City.Subdivisions | length > 0 then .City.Subdivisions[0].Name else "N/A" end') +[[ ${maxmind[countrycode]} == "null" ]]&&maxmind[countrycode]=$(echo "$backup_response"|jq -r '.Country.IsoCode') +[[ ${maxmind[country]} == "null" ]]&&maxmind[country]=$(echo "$backup_response"|jq -r '.Country.Name') +[[ ${maxmind[regcountrycode]} == "null" ]]&&maxmind[regcountrycode]=$(echo "$backup_response"|jq -r '.Country.RegisteredCountry.IsoCode') +[[ ${maxmind[regcountry]} == "null" ]]&&maxmind[regcountry]=$(echo "$backup_response"|jq -r '.Country.RegisteredCountry.Name') +fi +if [[ ${maxmind[lat]} != "null" && ${maxmind[lon]} != "null" ]];then +maxmind[dms]=$(generate_dms "${maxmind[lat]}" "${maxmind[lon]}") +maxmind[map]=$(generate_googlemap_url "${maxmind[lat]}" "${maxmind[lon]}" "${maxmind[rad]}") +else +maxmind[dms]="null" +maxmind[map]="null" +fi +} +db_ipinfo(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}IPinfo $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-7-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +ipinfo=() +if [[ $IP == *:* ]];then +local RESPONSE=$(curl -Ls -m 10 "https://ipinfo.io/widget/demo/$IP") +else +local RESPONSE=$(curl $CurlARG -Ls -m 10 "https://ipinfo.io/widget/demo/$IP") +fi +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +ipinfo[usetype]=$(echo "$RESPONSE"|jq -r '.data.asn.type') +ipinfo[comtype]=$(echo "$RESPONSE"|jq -r '.data.company.type') +shopt -s nocasematch +case ${ipinfo[usetype]} in +"business")ipinfo[susetype]="${stype[business]}" +;; +"isp")ipinfo[susetype]="${stype[isp]}" +;; +"hosting")ipinfo[susetype]="${stype[hosting]}" +;; +"education")ipinfo[susetype]="${stype[education]}" +;; +*)ipinfo[susetype]="${stype[other]}" +esac +case ${ipinfo[comtype]} in +"business")ipinfo[scomtype]="${stype[business]}" +;; +"isp")ipinfo[scomtype]="${stype[isp]}" +;; +"hosting")ipinfo[scomtype]="${stype[hosting]}" +;; +"education")ipinfo[scomtype]="${stype[education]}" +;; +*)ipinfo[scomtype]="${stype[other]}" +esac +shopt -u nocasematch +ipinfo[countrycode]=$(echo "$RESPONSE"|jq -r '.data.country') +ipinfo[proxy]=$(echo "$RESPONSE"|jq -r '.data.privacy.proxy') +ipinfo[tor]=$(echo "$RESPONSE"|jq -r '.data.privacy.tor') +ipinfo[vpn]=$(echo "$RESPONSE"|jq -r '.data.privacy.vpn') +ipinfo[server]=$(echo "$RESPONSE"|jq -r '.data.privacy.hosting') +local ISO3166=$(curl -sL -m 10 "${rawgithub}main/ref/iso3166.json") +ipinfo[asn]=$(echo "$RESPONSE"|jq -r '.data.asn.asn'|sed 's/^AS//') +ipinfo[org]=$(echo "$RESPONSE"|jq -r '.data.asn.name') +ipinfo[city]=$(echo "$RESPONSE"|jq -r '.data.city') +ipinfo[post]=$(echo "$RESPONSE"|jq -r '.data.postal') +ipinfo[timezone]=$(echo "$RESPONSE"|jq -r '.data.timezone') +local tmp_str=$(echo "$RESPONSE"|jq -r '.data.loc') +ipinfo[lat]=$(echo "$tmp_str"|cut -d',' -f1) +ipinfo[lon]=$(echo "$tmp_str"|cut -d',' -f2) +ipinfo[countrycode]=$(echo "$RESPONSE"|jq -r '.data.country') +ipinfo[country]=$(echo "$ISO3166"|jq --arg code "${ipinfo[countrycode]}" -r '.[] | select(.["alpha-2"] == $code) | .name') +ipinfo[continent]=$(echo "$ISO3166"|jq --arg code "${ipinfo[countrycode]}" -r '.[] | select(.["alpha-2"] == $code) | .region') +ipinfo[regcountrycode]=$(echo "$RESPONSE"|jq -r '.data.abuse.country') +ipinfo[regcountry]=$(echo "$ISO3166"|jq --arg code "${ipinfo[regcountrycode]}" -r '.[] | select(.["alpha-2"] == $code) | .name') +if [[ ${ipinfo[lat]} != "null" && ${ipinfo[lon]} != "null" ]];then +ipinfo[dms]=$(generate_dms "${ipinfo[lat]}" "${ipinfo[lon]}") +ipinfo[map]=$(generate_googlemap_url "${ipinfo[lat]}" "${ipinfo[lon]}" "1001") +else +ipinfo[dms]="null" +ipinfo[map]="null" +fi +} +db_scamalytics(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}Scamalytics $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-12-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +scamalytics=() +local RESPONSE=$(curl $CurlARG -sL -$1 -m 10 "https://ipinfo.check.place/$IP?db=scamalytics") +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +scamalytics[countrycode]=$(echo "$RESPONSE"|jq -r '.external_datasources.maxmind_geolite2.ip_country_code') +scamalytics[proxy]=$(echo "$RESPONSE"|jq -r '.external_datasources.firehol.is_proxy') +scamalytics[tor]=$(echo "$RESPONSE"|jq -r '.external_datasources.x4bnet.is_tor') +scamalytics[vpn]=$(echo "$RESPONSE"|jq -r '.scamalytics.scamalytics_proxy.is_vpn') +scamalytics[server]=$(echo "$RESPONSE"|jq -r '.scamalytics.scamalytics_proxy.is_datacenter') +scamalytics[abuser]=$(echo "$RESPONSE"|jq -r '.scamalytics.is_blacklisted_external') +scamalytics[robot1]=$(echo "$RESPONSE"|jq -r '.external_datasources.x4bnet.is_blacklisted_spambot') +scamalytics[robot2]=$(echo "$RESPONSE"|jq -r '.external_datasources.x4bnet.is_bot_operamini') +scamalytics[robot3]=$(echo "$RESPONSE"|jq -r '.external_datasources.x4bnet.is_bot_semrush') +[[ ${scamalytics[robot1]} == "true" || ${scamalytics[robot2]} == "true" || ${scamalytics[robot3]} == "true" ]]&&scamalytics[robot]="true" +[[ ${scamalytics[robot1]} == "false" && ${scamalytics[robot2]} == "false" && ${scamalytics[robot3]} == "false" ]]&&scamalytics[robot]="false" +scamalytics[score]=$(echo "$RESPONSE"|jq -r '.scamalytics.scamalytics_score') +if [[ ${scamalytics[score]} -lt 20 ]];then +scamalytics[risk]="${sscore[low]}" +elif [[ ${scamalytics[score]} -lt 60 ]];then +scamalytics[risk]="${sscore[medium]}" +elif [[ ${scamalytics[score]} -lt 90 ]];then +scamalytics[risk]="${sscore[high]}" +elif [[ ${scamalytics[score]} -ge 90 ]];then +scamalytics[risk]="${sscore[veryhigh]}" +fi +} +db_ipregistry(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}ipregistry $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-11-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +ipregistry=() +local tmpgo="sb69ksjcajfs4c" +local REGISTRY_HTML +REGISTRY_HTML=$(curl $CurlARG -sL -$1 -m 10 -H "User-Agent: $UA_Browser" "https://ipregistry.co") +if [[ -n $REGISTRY_HTML ]];then +if [[ $REGISTRY_HTML =~ apiKey=\"([a-zA-Z0-9]+)\" ]];then +tmpgo="${BASH_REMATCH[1]}" +fi +fi +local RESPONSE +RESPONSE=$(curl $CurlARG -sS -$1 --compressed -m 10 \ +-H "authority: api.ipregistry.co" \ +-H "origin: https://ipregistry.co" \ +-H "referer: https://ipregistry.co/" \ +-H "User-Agent: $UA_Browser" \ +"https://api.ipregistry.co/$IP?hostname=true&key=$tmpgo") +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +ipregistry[usetype]=$(echo "$RESPONSE"|jq -r '.connection.type') +ipregistry[comtype]=$(echo "$RESPONSE"|jq -r '.company.type') +shopt -s nocasematch +case ${ipregistry[usetype]} in +"business")ipregistry[susetype]="${stype[business]}" +;; +"isp")ipregistry[susetype]="${stype[isp]}" +;; +"hosting")ipregistry[susetype]="${stype[hosting]}" +;; +"education")ipregistry[susetype]="${stype[education]}" +;; +"government")ipregistry[susetype]="${stype[government]}" +;; +*)ipregistry[susetype]="${stype[other]}" +esac +case ${ipregistry[comtype]} in +"business")ipregistry[scomtype]="${stype[business]}" +;; +"isp")ipregistry[scomtype]="${stype[isp]}" +;; +"hosting")ipregistry[scomtype]="${stype[hosting]}" +;; +"education")ipregistry[scomtype]="${stype[education]}" +;; +"government")ipregistry[scomtype]="${stype[government]}" +;; +*)ipregistry[scomtype]="${stype[other]}" +esac +shopt -u nocasematch +ipregistry[countrycode]=$(echo "$RESPONSE"|jq -r '.location.country.code') +ipregistry[proxy]=$(echo "$RESPONSE"|jq -r '.security.is_proxy') +ipregistry[tor1]=$(echo "$RESPONSE"|jq -r '.security.is_tor') +ipregistry[tor2]=$(echo "$RESPONSE"|jq -r '.security.is_tor_exit') +[[ ${ipregistry[tor1]} == "true" || ${ipregistry[tor2]} == "true" ]]&&ipregistry[tor]="true" +[[ ${ipregistry[tor1]} == "false" && ${ipregistry[tor2]} == "false" ]]&&ipregistry[tor]="false" +ipregistry[vpn]=$(echo "$RESPONSE"|jq -r '.security.is_vpn') +ipregistry[server]=$(echo "$RESPONSE"|jq -r '.security.is_cloud_provider') +ipregistry[abuser]=$(echo "$RESPONSE"|jq -r '.security.is_abuser') +} +db_ipapi(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}ipapi $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-6-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +ipapi=() +if [[ $IP == *:* ]];then +local RESPONSE=$(curl -Ls -m 10 "https://api.ipapi.is/?q=$IP") +else +local RESPONSE=$(curl $CurlARG -sL -m 10 "https://api.ipapi.is/?q=$IP") +fi +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +ipapi[usetype]=$(echo "$RESPONSE"|jq -r '.asn.type') +ipapi[comtype]=$(echo "$RESPONSE"|jq -r '.company.type') +shopt -s nocasematch +case ${ipapi[usetype]} in +"business")ipapi[susetype]="${stype[business]}" +;; +"isp")ipapi[susetype]="${stype[isp]}" +;; +"hosting")ipapi[susetype]="${stype[hosting]}" +;; +"education")ipapi[susetype]="${stype[education]}" +;; +"government")ipapi[susetype]="${stype[government]}" +;; +"banking")ipapi[susetype]="${stype[banking]}" +;; +*)ipapi[susetype]="${stype[other]}" +esac +case ${ipapi[comtype]} in +"business")ipapi[scomtype]="${stype[business]}" +;; +"isp")ipapi[scomtype]="${stype[isp]}" +;; +"hosting")ipapi[scomtype]="${stype[hosting]}" +;; +"education")ipapi[scomtype]="${stype[education]}" +;; +"government")ipapi[scomtype]="${stype[government]}" +;; +"banking")ipapi[scomtype]="${stype[banking]}" +;; +*)ipapi[scomtype]="${stype[other]}" +esac +[[ -z $RESPONSE ]]&&return 1 +ipapi[scoretext]=$(echo "$RESPONSE"|jq -r '.company.abuser_score') +ipapi[scorenum]=$(echo "${ipapi[scoretext]}"|awk '{print $1}') +ipapi[risktext]=$(echo "${ipapi[scoretext]}"|awk -F'[()]' '{print $2}') +ipapi[score]=$(awk "BEGIN {printf \"%.2f%%\", ${ipapi[scorenum]} * 100}") +case ${ipapi[risktext]} in +"Very Low")ipapi[risk]="${sscore[verylow]}" +;; +"Low")ipapi[risk]="${sscore[low]}" +;; +"Elevated")ipapi[risk]="${sscore[elevated]}" +;; +"High")ipapi[risk]="${sscore[high]}" +;; +"Very High")ipapi[risk]="${sscore[veryhigh]}" +esac +shopt -u nocasematch +ipapi[countrycode]=$(echo "$RESPONSE"|jq -r '.location.country_code') +ipapi[proxy]=$(echo "$RESPONSE"|jq -r '.is_proxy') +ipapi[tor]=$(echo "$RESPONSE"|jq -r '.is_tor') +ipapi[vpn]=$(echo "$RESPONSE"|jq -r '.is_vpn') +ipapi[server]=$(echo "$RESPONSE"|jq -r '.is_datacenter') +ipapi[abuser]=$(echo "$RESPONSE"|jq -r '.is_abuser') +ipapi[robot]=$(echo "$RESPONSE"|jq -r '.is_crawler') +} +db_abuseipdb(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}AbuseIPDB $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-10-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +abuseipdb=() +local RESPONSE=$(curl $CurlARG -sL -$1 -m 10 "https://ipinfo.check.place/$IP?db=abuseipdb") +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +abuseipdb[usetype]=$(echo "$RESPONSE"|jq -r '.data.usageType') +shopt -s nocasematch +case ${abuseipdb[usetype]} in +"Commercial")abuseipdb[susetype]="${stype[business]}" +;; +"Data Center/Web Hosting/Transit")abuseipdb[susetype]="${stype[hosting]}" +;; +"University/College/School")abuseipdb[susetype]="${stype[education]}" +;; +"Government")abuseipdb[susetype]="${stype[government]}" +;; +"banking")abuseipdb[susetype]="${stype[banking]}" +;; +"Organization")abuseipdb[susetype]="${stype[organization]}" +;; +"Military")abuseipdb[susetype]="${stype[military]}" +;; +"Library")abuseipdb[susetype]="${stype[library]}" +;; +"Content Delivery Network")abuseipdb[susetype]="${stype[cdn]}" +;; +"Fixed Line ISP")abuseipdb[susetype]="${stype[lineisp]}" +;; +"Mobile ISP")abuseipdb[susetype]="${stype[mobile]}" +;; +"Search Engine Spider")abuseipdb[susetype]="${stype[spider]}" +;; +"Reserved")abuseipdb[susetype]="${stype[reserved]}" +;; +*)abuseipdb[susetype]="${stype[other]}" +esac +shopt -u nocasematch +abuseipdb[score]=$(echo "$RESPONSE"|jq -r '.data.abuseConfidenceScore') +if [[ ${abuseipdb[score]} -lt 25 ]];then +abuseipdb[risk]="${sscore[low]}" +elif [[ ${abuseipdb[score]} -lt 75 ]];then +abuseipdb[risk]="${sscore[high]}" +elif [[ ${abuseipdb[score]} -ge 75 ]];then +abuseipdb[risk]="${sscore[dos]}" +fi +} +db_ip2location(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}IP2LOCATION $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-12-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +ip2location=() +local RESPONSE=$(curl $CurlARG -sL -$1 -m 10 "https://ipinfo.check.place/$IP?db=ip2location") +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +ip2location[usetype]=$(echo "$RESPONSE"|jq -r '.usage_type') +ip2location[comtype]=$(echo "$RESPONSE"|jq -r '.as_info.as_usage_type') +shopt -s nocasematch +local first_use="${ip2location[usetype]%%/*}" +case $first_use in +"COM")ip2location[susetype]="${stype[business]}" +;; +"DCH")ip2location[susetype]="${stype[hosting]}" +;; +"EDU")ip2location[susetype]="${stype[education]}" +;; +"GOV")ip2location[susetype]="${stype[government]}" +;; +"ORG")ip2location[susetype]="${stype[organization]}" +;; +"MIL")ip2location[susetype]="${stype[military]}" +;; +"LIB")ip2location[susetype]="${stype[library]}" +;; +"CDN")ip2location[susetype]="${stype[cdn]}" +;; +"ISP")ip2location[susetype]="${stype[lineisp]}" +;; +"MOB")ip2location[susetype]="${stype[mobile]}" +;; +"SES")ip2location[susetype]="${stype[spider]}" +;; +"RSV")ip2location[susetype]="${stype[reserved]}" +;; +*)ip2location[susetype]="${stype[other]}" +esac +first_use="${ip2location[comtype]%%/*}" +case $first_use in +"COM")ip2location[scomtype]="${stype[business]}" +;; +"DCH")ip2location[scomtype]="${stype[hosting]}" +;; +"EDU")ip2location[scomtype]="${stype[education]}" +;; +"GOV")ip2location[scomtype]="${stype[government]}" +;; +"ORG")ip2location[scomtype]="${stype[organization]}" +;; +"MIL")ip2location[scomtype]="${stype[military]}" +;; +"LIB")ip2location[scomtype]="${stype[library]}" +;; +"CDN")ip2location[scomtype]="${stype[cdn]}" +;; +"ISP")ip2location[scomtype]="${stype[lineisp]}" +;; +"MOB")ip2location[scomtype]="${stype[mobile]}" +;; +"SES")ip2location[scomtype]="${stype[spider]}" +;; +"RSV")ip2location[scomtype]="${stype[reserved]}" +;; +*)ip2location[scomtype]="${stype[other]}" +esac +shopt -u nocasematch +ip2location[countrycode]=$(echo "$RESPONSE"|jq -r '.country_code') +ip2location[proxy0]=$(echo "$RESPONSE"|jq -r '.is_proxy') +ip2location[proxy1]=$(echo "$RESPONSE"|jq -r '.proxy.is_public_proxy') +ip2location[proxy2]=$(echo "$RESPONSE"|jq -r '.proxy.is_web_proxy') +[[ ${ip2location[proxy0]} == "true" || ${ip2location[proxy1]} == "true" || ${ip2location[proxy2]} == "true" ]]&&ip2location[proxy]="true" +[[ ${ip2location[proxy0]} == "false" && ${ip2location[proxy1]} == "false" && ${ip2location[proxy2]} == "false" ]]&&ip2location[proxy]="false" +ip2location[tor]=$(echo "$RESPONSE"|jq -r '.proxy.is_tor') +ip2location[vpn]=$(echo "$RESPONSE"|jq -r '.proxy.is_vpn') +ip2location[server]=$(echo "$RESPONSE"|jq -r '.proxy.is_data_center') +ip2location[abuser]=$(echo "$RESPONSE"|jq -r '.proxy.is_spammer') +ip2location[robot1]=$(echo "$RESPONSE"|jq -r '.proxy.is_web_crawler') +ip2location[robot2]=$(echo "$RESPONSE"|jq -r '.proxy.is_scanner') +ip2location[robot3]=$(echo "$RESPONSE"|jq -r '.proxy.is_botnet') +[[ ${ip2location[robot1]} == "true" || ${ip2location[robot2]} == "true" || ${ip2location[robot3]} == "true" ]]&&ip2location[robot]="true" +[[ ${ip2location[robot1]} == "false" && ${ip2location[robot2]} == "false" && ${ip2location[robot3]} == "false" ]]&&ip2location[robot]="false" +ip2location[score]=$(echo "$RESPONSE"|jq -r '.fraud_score') +if [[ ${ip2location[score]} -lt 33 ]];then +ip2location[risk]="${sscore[low]}" +elif [[ ${ip2location[score]} -lt 66 ]];then +ip2location[risk]="${sscore[medium]}" +elif [[ ${ip2location[score]} -ge 66 ]];then +ip2location[risk]="${sscore[high]}" +fi +} +db_dbip(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}DB-IP $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-6-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +dbip=() +if [[ $IP == *:* ]];then +local RESPONSE=$(curl -sL -m 10 "https://db-ip.com/$IP") +else +local RESPONSE=$(curl $CurlARG -sL -m 10 "https://db-ip.com/$IP") +fi +mapfile -t results < <(echo "$RESPONSE"|awk '/Crawler/ {flag=1; next} + flag && // { + if ($0 ~ /Yes/) print "true"; + else if ($0 ~ /No/) print "false"; + } + /<\/tr>/ && flag {flag=0}') +dbip[robot]="${results[0]}" +dbip[proxy]="${results[1]}" +dbip[abuser]="${results[2]}" +dbip[risktext]=$(echo "$RESPONSE"|sed -n 's/.*Estimated threat level for this IP address is[[:space:]]*]*>\([^<]*\)<.*/\1/p') +dbip[countrycode]=$(echo "$RESPONSE"|sed -n '//,/<\/code>/p'|sed -n 's/.*"countryCode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +shopt -s nocasematch +case ${dbip[risktext]} in +"low")dbip[risk]="${sscore[low]}" +dbip[score]=0 +;; +"medium")dbip[risk]="${sscore[medium]}" +dbip[score]=50 +;; +"high")dbip[risk]="${sscore[high]}" +dbip[score]=100 +esac +shopt -u nocasematch +} +db_ipdata(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}ipdata $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-7-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +ipdata=() +local RESPONSE=$(curl $CurlARG -sL -$1 -m 10 "https://ipinfo.check.place/$IP?db=ipdata") +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +ipdata[countrycode]=$(echo "$RESPONSE"|jq -r '.country_code') +ipdata[proxy]=$(echo "$RESPONSE"|jq -r '.threat.is_proxy') +ipdata[tor]=$(echo "$RESPONSE"|jq -r '.threat.is_tor') +ipdata[server]=$(echo "$RESPONSE"|jq -r '.threat.is_datacenter') +ipdata[abuser1]=$(echo "$RESPONSE"|jq -r '.threat.is_threat') +ipdata[abuser2]=$(echo "$RESPONSE"|jq -r '.threat.is_known_abuser') +ipdata[abuser3]=$(echo "$RESPONSE"|jq -r '.threat.is_known_attacker') +[[ ${ipdata[abuser1]} == "true" || ${ipdata[abuser2]} == "true" || ${ipdata[abuser3]} == "true" ]]&&ipdata[abuser]="true" +[[ ${ipdata[abuser1]} == "false" && ${ipdata[abuser2]} == "false" && ${ipdata[abuser3]} == "false" ]]&&ipdata[abuser]="false" +} +db_ipqs(){ +local temp_info="$Font_Cyan$Font_B${sinfo[database]}${Font_I}IPQS $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-5-${sinfo[ldatabase]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +ipqs=() +local RESPONSE=$(curl $CurlARG -sL -$1 -m 10 "https://ipinfo.check.place/$IP?db=ipqualityscore") +echo "$RESPONSE"|jq . >/dev/null 2>&1||RESPONSE="" +ipqs[score]=$(echo "$RESPONSE"|jq -r '.fraud_score') +if [[ ${ipqs[score]} -lt 75 ]];then +ipqs[risk]="${sscore[low]}" +elif [[ ${ipqs[score]} -lt 85 ]];then +ipqs[risk]="${sscore[suspicious]}" +elif [[ ${ipqs[score]} -lt 90 ]];then +ipqs[risk]="${sscore[risky]}" +elif [[ ${ipqs[score]} -ge 90 ]];then +ipqs[risk]="${sscore[highrisk]}" +fi +ipqs[countrycode]=$(echo "$RESPONSE"|jq -r '.country_code') +ipqs[proxy]=$(echo "$RESPONSE"|jq -r '.proxy') +ipqs[tor]=$(echo "$RESPONSE"|jq -r '.tor') +ipqs[vpn]=$(echo "$RESPONSE"|jq -r '.vpn') +ipqs[abuser]=$(echo "$RESPONSE"|jq -r '.recent_abuse') +ipqs[robot]=$(echo "$RESPONSE"|jq -r '.bot_status') +} +function check_ip_valide(){ +local IPPattern='^(\<([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\>\.){3}\<([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\>$' +IP="$1" +if [[ $IP =~ $IPPattern ]];then +return 0 +else +return 1 +fi +} +function calc_ip_net(){ +sip="$1" +snetmask="$2" +check_ip_valide "$sip" +if [ $? -ne 0 ];then +echo "" +return 1 +fi +local ipFIELD1=$(echo "$sip"|cut -d. -f1) +local ipFIELD2=$(echo "$sip"|cut -d. -f2) +local ipFIELD3=$(echo "$sip"|cut -d. -f3) +local ipFIELD4=$(echo "$sip"|cut -d. -f4) +local netmaskFIELD1=$(echo "$snetmask"|cut -d. -f1) +local netmaskFIELD2=$(echo "$snetmask"|cut -d. -f2) +local netmaskFIELD3=$(echo "$snetmask"|cut -d. -f3) +local netmaskFIELD4=$(echo "$snetmask"|cut -d. -f4) +local tmpret1=$((ipFIELD1&netmaskFIELD1)) +local tmpret2=$((ipFIELD2&netmaskFIELD2)) +local tmpret3=$((ipFIELD3&netmaskFIELD3)) +local tmpret4=$((ipFIELD4&netmaskFIELD4)) +echo "$tmpret1.$tmpret2.$tmpret3.$tmpret4" +} +function Check_DNS_IP(){ +if [ "$1" != "${1#*[0-9].[0-9]}" ];then +if [ "$(calc_ip_net "$1" 255.0.0.0)" == "10.0.0.0" ];then +echo 0 +elif [ "$(calc_ip_net "$1" 255.240.0.0)" == "172.16.0.0" ];then +echo 0 +elif [ "$(calc_ip_net "$1" 255.255.0.0)" == "169.254.0.0" ];then +echo 0 +elif [ "$(calc_ip_net "$1" 255.255.0.0)" == "192.168.0.0" ];then +echo 0 +elif [ "$(calc_ip_net "$1" 255.255.255.0)" == "$(calc_ip_net "$2" 255.255.255.0)" ];then +echo 0 +else +echo 1 +fi +elif [ "$1" != "${1#*[0-9a-fA-F]:*}" ];then +if [ "${1:0:3}" == "fe8" ];then +echo 0 +elif [ "${1:0:3}" == "FE8" ];then +echo 0 +elif [ "${1:0:2}" == "fc" ];then +echo 0 +elif [ "${1:0:2}" == "FC" ];then +echo 0 +elif [ "${1:0:2}" == "fd" ];then +echo 0 +elif [ "${1:0:2}" == "FD" ];then +echo 0 +elif [ "${1:0:2}" == "ff" ];then +echo 0 +elif [ "${1:0:2}" == "FF" ];then +echo 0 +else +echo 1 +fi +else +echo 0 +fi +} +function Check_DNS_1(){ +local resultdns=$(nslookup $1) +local resultinlines=(${resultdns//$'\n'/ }) +for i in ${resultinlines[*]};do +if [[ $i == "Name:" ]];then +local resultdnsindex=$((resultindex+3)) +break +fi +local resultindex=$((resultindex+1)) +done +echo $(Check_DNS_IP ${resultinlines[$resultdnsindex]} ${resultinlines[1]}) +} +function Check_DNS_2(){ +local resultdnstext=$(dig $1|grep "ANSWER:") +local resultdnstext=${resultdnstext#*"ANSWER: "} +local resultdnstext=${resultdnstext%", AUTHORITY:"*} +if [ "$resultdnstext" == "0" ]||[ "$resultdnstext" == "1" ]||[ "$resultdnstext" == "2" ];then +echo 0 +else +echo 1 +fi +} +function Check_DNS_3(){ +local resultdnstext=$(dig "test$RANDOM$RANDOM.$1"|grep "ANSWER:") +local resultdnstext=${resultdnstext#*"ANSWER: "} +local resultdnstext=${resultdnstext%", AUTHORITY:"*} +if [ "$resultdnstext" == "0" ];then +echo 1 +else +echo 0 +fi +} +function Get_Unlock_Type(){ +while [ $# -ne 0 ];do +if [ "$1" = "0" ];then +echo "${smedia[dns]}" +return +fi +shift +done +echo "${smedia[native]}" +} +function MediaUnlockTest_TikTok(){ +local temp_info="$Font_Cyan$Font_B${sinfo[media]}${Font_I}TikTok $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-7-${sinfo[lmedia]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +tiktok=() +local checkunlockurl="tiktok.com" +local result1=$(Check_DNS_1 $checkunlockurl) +local result3=$(Check_DNS_3 $checkunlockurl) +local resultunlocktype=$(Get_Unlock_Type $result1 $result3) +local Ftmpresult=$(curl $CurlARG -$1 --user-agent "$UA_Browser" -sL -m 10 "https://www.tiktok.com/") +[[ $Ftmpresult == *"Please wait..."* ]]&&Ftmpresult=$(curl $useNIC $usePROXY $xForward --user-agent "$UA_Browser" -sL -m 10 "https://www.tiktok.com/explore") +if [[ $Ftmpresult == "curl"* ]];then +tiktok[ustatus]="${smedia[no]}" +tiktok[uregion]="${smedia[nodata]}" +tiktok[utype]="${smedia[nodata]}" +return +fi +local FRegion=$(echo $Ftmpresult|grep '"region":'|sed 's/.*"region"//'|cut -f2 -d'"') +if [ -n "$FRegion" ];then +tiktok[ustatus]="${smedia[yes]}" +local ttpadding=$((7-${#FRegion})) +local ttleft=$((ttpadding/2)) +local ttright=$((ttpadding-ttleft)) +tiktok[uregion]="$(printf "%*s%s%*s" "$ttleft" "" "[$FRegion]" "$ttright" "")" +tiktok[utype]="$resultunlocktype" +return +fi +local STmpresult=$(curl $CurlARG -$1 --user-agent "$UA_Browser" -sL -m 10 -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" -H "Accept-Encoding: gzip" -H "Accept-Language: en" "https://www.tiktok.com"|gunzip 2>/dev/null) +[[ $Ftmpresult == *"Please wait..."* ]]&&STmpresult=$(curl $useNIC $usePROXY $xForward --user-agent "$UA_Browser" -sL -m 10 -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" -H "Accept-Encoding: gzip" -H "Accept-Language: en" "https://www.tiktok.com/explore"|gunzip 2>/dev/null) +local SRegion=$(echo $STmpresult|grep '"region":'|sed 's/.*"region"//'|cut -f2 -d'"') +if [ -n "$SRegion" ];then +tiktok[ustatus]="${smedia[idc]}" +local ttWidth=7 +local ttpadding=$((7-${#SRegion})) +local ttleft=$((ttpadding/2)) +local ttright=$((ttpadding-ttleft)) +tiktok[uregion]="$(printf "%*s%s%*s" "$ttleft" "" "[$SRegion]" "$ttright" "")" +tiktok[utype]="$resultunlocktype" +return +else +tiktok[ustatus]="${smedia[bad]}" +tiktok[uregion]="${smedia[nodata]}" +tiktok[utype]="${smedia[nodata]}" +return +fi +} +function MediaUnlockTest_DisneyPlus(){ +local temp_info="$Font_Cyan$Font_B${sinfo[media]}${Font_I}Disney+ $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-8-${sinfo[lmedia]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +disney=() +local checkunlockurl="disneyplus.com" +local result1=$(Check_DNS_1 $checkunlockurl) +local result3=$(Check_DNS_3 $checkunlockurl) +local resultunlocktype=$(Get_Unlock_Type $result1 $result3) +local PreAssertion=$(curl $CurlARG -$1 --user-agent "$UA_Browser" -s --max-time 10 -X POST "https://disney.api.edge.bamgrid.com/devices" -H "authorization: Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84" -H "content-type: application/json; charset=UTF-8" -d '{"deviceFamily":"browser","applicationRuntime":"chrome","deviceProfile":"windows","attributes":{}}' 2>&1) +if [[ $PreAssertion == "curl"* ]]&&[[ $1 == "6" ]];then +disney[ustatus]="${smedia[bad]}" +disney[uregion]="${smedia[nodata]}" +disney[utype]="${smedia[nodata]}" +return +elif [[ $PreAssertion == "curl"* ]];then +disney[ustatus]="${smedia[bad]}" +disney[uregion]="${smedia[nodata]}" +disney[utype]="${smedia[nodata]}" +return +fi +if ! (echo "$PreAssertion"|jq . >/dev/null 2>&1&&echo "$TokenContent"|jq . >/dev/null 2>&1);then +disney[ustatus]="${smedia[bad]}" +disney[uregion]="${smedia[nodata]}" +disney[utype]="${smedia[nodata]}" +return +fi +local assertion=$(echo $PreAssertion|jq -r '.assertion') +local PreDisneyCookie=$(echo "$Media_Cookie"|sed -n '1p') +local disneycookie=$(echo $PreDisneyCookie|sed "s/DISNEYASSERTION/$assertion/g") +local TokenContent=$(curl $CurlARG -$1 --user-agent "$UA_Browser" -s --max-time 10 -X POST "https://disney.api.edge.bamgrid.com/token" -H "authorization: Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84" -d "$disneycookie" 2>&1) +local isBanned=$(echo $TokenContent|jq -r 'select(.error_description == "forbidden-location") | .error_description') +local is403=$(echo $TokenContent|grep '403 ERROR') +if [ -n "$isBanned" ]||[ -n "$is403" ];then +disney[ustatus]="${smedia[no]}" +disney[uregion]="${smedia[nodata]}" +disney[utype]="${smedia[nodata]}" +return +fi +local fakecontent=$(echo "$Media_Cookie"|sed -n '8p') +local refreshToken=$(echo $TokenContent|jq -r '.refresh_token') +local disneycontent=$(echo $fakecontent|sed "s/ILOVEDISNEY/$refreshToken/g") +local tmpresult=$(curl $CurlARG -$1 --user-agent "$UA_Browser" -X POST -sSL --max-time 10 "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql" -H "authorization: ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84" -d "$disneycontent" 2>&1) +if ! (echo "$tmpresult"|jq . >/dev/null 2>&1);then +disney[ustatus]="${smedia[bad]}" +disney[uregion]="${smedia[nodata]}" +disney[utype]="${smedia[nodata]}" +return +fi +local previewcheck=$(curl $CurlARG -$1 -s -o /dev/null -L --max-time 10 -w '%{url_effective}\n' "https://disneyplus.com"|grep preview) +local isUnavailable=$(echo $previewcheck|grep 'unavailable') +local region=$(echo $tmpresult|jq -r '.extensions.sdk.session.location.countryCode') +local inSupportedLocation=$(echo $tmpresult|jq -r '.extensions.sdk.session.inSupportedLocation') +if [[ $region == "JP" ]];then +disney[ustatus]="${smedia[yes]}" +disney[uregion]=" [JP] " +disney[utype]="$resultunlocktype" +return +elif [ -n "$region" ]&&[[ $inSupportedLocation == "false" ]]&&[ -z "$isUnavailable" ];then +disney[ustatus]="${smedia[pending]}" +disney[uregion]=" [$region] " +disney[utype]="$resultunlocktype" +return +elif [ -n "$region" ]&&[ -n "$isUnavailable" ];then +disney[ustatus]="${smedia[no]}" +disney[uregion]="${smedia[nodata]}" +disney[utype]="${smedia[nodata]}" +return +elif [ -n "$region" ]&&[[ $inSupportedLocation == "true" ]];then +disney[ustatus]="${smedia[yes]}" +disney[uregion]=" [$region] " +disney[utype]="$resultunlocktype" +return +elif [ -z "$region" ];then +disney[ustatus]="${smedia[no]}" +disney[uregion]="${smedia[nodata]}" +disney[utype]="${smedia[nodata]}" +return +else +disney[ustatus]="${smedia[bad]}" +disney[uregion]="${smedia[nodata]}" +disney[utype]="${smedia[nodata]}" +return +fi +} +function MediaUnlockTest_Netflix(){ +local temp_info="$Font_Cyan$Font_B${sinfo[media]}${Font_I}Netflix $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-8-${sinfo[lmedia]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +netflix=() +local checkunlockurl="netflix.com" +local result1=$(Check_DNS_1 $checkunlockurl) +local result2=$(Check_DNS_2 $checkunlockurl) +local result3=$(Check_DNS_3 $checkunlockurl) +local resultunlocktype=$(Get_Unlock_Type $result1 $result2 $result3) +local result1=$(curl $CurlARG -$1 --user-agent "$UA_Browser" -fsL -X GET --max-time 10 --tlsv1.3 "https://www.netflix.com/title/81280792" 2>&1) +local result2=$(curl $CurlARG -$1 --user-agent "$UA_Browser" -fsL -X GET --max-time 10 --tlsv1.3 "https://www.netflix.com/title/70143836" 2>&1) +if [ -z "$result1" ]||[ -z "$result2" ];then +netflix[ustatus]="${smedia[bad]}" +netflix[uregion]="${smedia[nodata]}" +netflix[utype]="${smedia[nodata]}" +return +fi +region=$(echo "$result1"|sed -n 's/.*"id":"\([^"]*\)".*"countryName":"[^"]*".*/\1/p'|head -n1) +[[ -n $region ]]&®ion=$(echo "$result2"|sed -n 's/.*"id":"\([^"]*\)".*"countryName":"[^"]*".*/\1/p'|head -n1) +result1=$(echo $result1|grep 'Oh no!') +result2=$(echo $result2|grep 'Oh no!') +if [ -n "$result1" ]&&[ -n "$result2" ];then +netflix[ustatus]="${smedia[org]}" +netflix[uregion]=" [$region] " +netflix[utype]="$resultunlocktype" +return +fi +if [ -z "$result1" ]||[ -z "$result2" ];then +netflix[ustatus]="${smedia[yes]}" +netflix[uregion]=" [$region] " +netflix[utype]="$resultunlocktype" +return +fi +netflix[ustatus]="${smedia[no]}" +netflix[uregion]="${smedia[nodata]}" +netflix[utype]="${smedia[nodata]}" +} +function MediaUnlockTest_YouTube_Premium(){ +local temp_info="$Font_Cyan$Font_B${sinfo[media]}${Font_I}Youtube $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-8-${sinfo[lmedia]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +youtube=() +local checkunlockurl="www.youtube.com" +local result1=$(Check_DNS_1 $checkunlockurl) +local result3=$(Check_DNS_3 $checkunlockurl) +local resultunlocktype=$(Get_Unlock_Type $result1 $result3) +local tmpresult=$(curl $CurlARG -$1 --max-time 10 -sSL -H "Accept-Language: en" -b "YSC=BiCUU3-5Gdk; CONSENT=YES+cb.20220301-11-p0.en+FX+700; GPS=1; VISITOR_INFO1_LIVE=4VwPMkB7W5A; PREF=tz=Asia.Shanghai; _gcl_au=1.1.1809531354.1646633279" "https://www.youtube.com/premium" 2>&1) +if [[ $tmpresult == "curl"* ]];then +youtube[ustatus]="${smedia[bad]}" +youtube[uregion]="${smedia[nodata]}" +youtube[utype]="${smedia[nodata]}" +return +fi +local isCN=$(echo $tmpresult|grep 'www.google.cn') +if [ -n "$isCN" ];then +youtube[ustatus]="${smedia[cn]}" +youtube[uregion]=" $Font_Red[CN]$Font_Green " +youtube[utype]="${smedia[nodata]}" +return +fi +local isNotAvailable=$(echo $tmpresult|grep 'Premium is not available in your country') +local region=$(echo $tmpresult|sed -n 's/.*"contentRegion":"\([^"]*\)".*/\1/p') +local isAvailable=$(echo $tmpresult|grep 'ad-free') +if [ -n "$isNotAvailable" ];then +youtube[ustatus]="${smedia[noprem]}" +youtube[uregion]="${smedia[nodata]}" +youtube[utype]="${smedia[nodata]}" +return +elif [ -n "$isAvailable" ]&&[ -n "$region" ];then +youtube[ustatus]="${smedia[yes]}" +youtube[uregion]=" [$region] " +youtube[utype]="$resultunlocktype" +return +elif [ -z "$region" ]&&[ -n "$isAvailable" ];then +youtube[ustatus]="${smedia[yes]}" +youtube[uregion]="${smedia[nodata]}" +youtube[utype]="$resultunlocktype" +return +else +youtube[ustatus]="${smedia[bad]}" +youtube[uregion]="${smedia[nodata]}" +youtube[utype]="${smedia[nodata]}" +fi +} +function MediaUnlockTest_PrimeVideo_Region(){ +local temp_info="$Font_Cyan$Font_B${sinfo[media]}${Font_I}Amazon $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-7-${sinfo[lmedia]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +amazon=() +local checkunlockurl="www.primevideo.com" +local result1=$(Check_DNS_1 $checkunlockurl) +local result3=$(Check_DNS_3 $checkunlockurl) +local resultunlocktype=$(Get_Unlock_Type $result1 $result3) +local tmpresult=$(curl $CurlARG -$1 --user-agent "$UA_Browser" -sL --max-time 10 "https://www.primevideo.com" 2>&1) +if [[ $tmpresult == "curl"* ]];then +amazon[ustatus]="${smedia[bad]}" +amazon[uregion]="${smedia[nodata]}" +amazon[utype]="${smedia[nodata]}" +return +fi +local result=$(echo $tmpresult|grep '"currentTerritory":'|sed 's/.*currentTerritory//'|cut -f3 -d'"'|head -n 1) +if [ -n "$result" ];then +amazon[ustatus]="${smedia[yes]}" +amazon[uregion]=" [$result] " +amazon[utype]="$resultunlocktype" +return +else +amazon[ustatus]="${smedia[no]}" +amazon[uregion]="${smedia[nodata]}" +amazon[utype]="${smedia[nodata]}" +return +fi +} +function MediaUnlockTest_Reddit(){ +local temp_info="$Font_Cyan$Font_B${sinfo[media]}${Font_I}Reddit $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-7-${sinfo[lmedia]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +reddit=() +local checkunlockurl="reddit.com" +local result1=$(Check_DNS_1 $checkunlockurl) +local result2=$(Check_DNS_2 $checkunlockurl) +local resultunlocktype=$(Get_Unlock_Type $result1 $result2) +local resp http_code html region +local resolve_opt="" +if [ "$1" = "6" ];then +local dual_ipv6 +dual_ipv6=$(dig AAAA reddit.com +short|head -n 1) +[[ -z $dual_ipv6 ]]&&dual_ipv6=$(dig AAAA dualstack.reddit.map.fastly.net +short|head -n 1) +if [ -n "$dual_ipv6" ];then +resolve_opt="--resolve www.reddit.com:443:[$dual_ipv6]" +else +reddit[ustatus]="${smedia[bad]}" +reddit[uregion]="${smedia[nodata]}" +reddit[utype]="${smedia[nodata]}" +return +fi +fi +resp=$(curl $useNIC $usePROXY $xForward -$1 $ssll -fsL --user-agent "$UA_Browser" --max-time 10 $resolve_opt --write-out '\n%{http_code}' "https://www.reddit.com/") +http_code=$(printf '%s' "$resp"|tail -n 1|tr -d '\r') +html=$(printf '%s' "$resp"|sed '$d') +if [ "$http_code" = "200" ];then +region=$(printf '%s' "$html"|tr '\n' ' '|sed -n 's/.*country="\([^"]*\)".*/\1/p'|head -n 1) +fi +case "$http_code" in +000)reddit[ustatus]="${smedia[bad]}" +reddit[uregion]="${smedia[nodata]}" +reddit[utype]="${smedia[nodata]}" +;; +403)reddit[ustatus]="${smedia[no]}" +reddit[uregion]="${smedia[nodata]}" +reddit[utype]="${smedia[nodata]}" +;; +200)reddit[ustatus]="${smedia[yes]}" +reddit[uregion]=" [$region] " +reddit[utype]="$resultunlocktype" +;; +*)reddit[ustatus]="${smedia[bad]}" +reddit[uregion]="${smedia[nodata]}" +reddit[utype]="${smedia[nodata]}" +esac +} +function OpenAITest(){ +local temp_info="$Font_Cyan$Font_B${sinfo[ai]}${Font_I}ChatGPT $Font_Suffix" +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-8-${sinfo[lai]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +chatgpt=() +local checkunlockurl="chat.openai.com" +local result1=$(Check_DNS_1 $checkunlockurl) +local result2=$(Check_DNS_2 $checkunlockurl) +local result3=$(Check_DNS_3 $checkunlockurl) +local checkunlockurl="ios.chat.openai.com" +local result4=$(Check_DNS_1 $checkunlockurl) +local result5=$(Check_DNS_2 $checkunlockurl) +local result6=$(Check_DNS_3 $checkunlockurl) +local checkunlockurl="api.openai.com" +local result7=$(Check_DNS_1 $checkunlockurl) +local result8=$(Check_DNS_3 $checkunlockurl) +local resultunlocktype=$(Get_Unlock_Type $result1 $result2 $result3 $result4 $result5 $result6 $result7 $result8) +local tmpresult1=$(curl $CurlARG -$1 -sS --max-time 10 'https://api.openai.com/compliance/cookie_requirements' -H 'authority: api.openai.com' -H 'accept: */*' -H 'accept-language: zh-CN,zh;q=0.9' -H 'authorization: Bearer null' -H 'content-type: application/json' -H 'origin: https://platform.openai.com' -H 'referer: https://platform.openai.com/' -H 'sec-ch-ua: "Microsoft Edge";v="119", "Chromium";v="119", "Not?A_Brand";v="24"' -H 'sec-ch-ua-mobile: ?0' -H 'sec-ch-ua-platform: "Windows"' -H 'sec-fetch-dest: empty' -H 'sec-fetch-mode: cors' -H 'sec-fetch-site: same-site' -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0' 2>&1) +local tmpresult2=$(curl $CurlARG -$1 -sS --max-time 10 'https://ios.chat.openai.com/' -H 'authority: ios.chat.openai.com' -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' -H 'accept-language: zh-CN,zh;q=0.9' -H 'sec-ch-ua: "Microsoft Edge";v="119", "Chromium";v="119", "Not?A_Brand";v="24"' -H 'sec-ch-ua-mobile: ?0' -H 'sec-ch-ua-platform: "Windows"' -H 'sec-fetch-dest: document' -H 'sec-fetch-mode: navigate' -H 'sec-fetch-site: none' -H 'sec-fetch-user: ?1' -H 'upgrade-insecure-requests: 1' -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0' 2>&1) +local result1=$(echo $tmpresult1|grep unsupported_country) +local result2=$(echo $tmpresult2|grep VPN) +if [ -n "$result1" ];then +code=$(curl $CurlARG -$1 -o /dev/null -sS --max-time 10 \ +'https://chatgpt.com/favicon.ico' \ +-H 'accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8' \ +-H 'authority: chatgpt.com' \ +-H 'accept: */*' \ +-H 'accept-language: zh-CN,zh;q=0.9' \ +-H 'authorization: Bearer null' \ +-H 'content-type: application/json' \ +-H 'origin: https://chatgpt.com' \ +-H 'referer: https://chatgpt.com/' \ +-H 'sec-ch-ua: "Microsoft Edge";v="119", "Chromium";v="119", "Not?A_Brand";v="24"' \ +-H 'sec-ch-ua-mobile: ?0' \ +-H 'sec-ch-ua-platform: "Windows"' \ +-H 'sec-fetch-dest: empty' \ +-H 'sec-fetch-mode: cors' \ +-H 'sec-fetch-site: same-site' \ +-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0' \ +-w "%{http_code}" 2>&1) +[[ $code != "403" ]]&&result1="" +fi +local countryCode="$(curl $CurlARG --max-time 10 -sS https://chat.openai.com/cdn-cgi/trace 2>&1|grep "loc="|awk -F= '{print $2}')" +if [ -z "$result2" ]&&[ -z "$result1" ]&&[[ $tmpresult1 != "curl"* ]]&&[[ $tmpresult2 != "curl"* ]];then +chatgpt[ustatus]="${smedia[yes]}" +chatgpt[uregion]=" [$countryCode] " +chatgpt[utype]="$resultunlocktype" +elif [ -n "$result2" ]&&[ -n "$result1" ];then +chatgpt[ustatus]="${smedia[no]}" +chatgpt[uregion]="${smedia[nodata]}" +chatgpt[utype]="${smedia[nodata]}" +elif [ -z "$result1" ]&&[ -n "$result2" ]&&[[ $tmpresult1 != "curl"* ]];then +chatgpt[ustatus]="${smedia[web]}" +chatgpt[uregion]=" [$countryCode] " +chatgpt[utype]="$resultunlocktype" +elif [ -n "$result1" ]&&[ -z "$result2" ];then +chatgpt[ustatus]="${smedia[app]}" +chatgpt[uregion]=" [$countryCode] " +chatgpt[utype]="$resultunlocktype" +elif [[ $tmpresult1 == "curl"* ]]&&[ -n "$result2" ];then +chatgpt[ustatus]="${smedia[no]}" +chatgpt[uregion]="${smedia[nodata]}" +chatgpt[utype]="${smedia[nodata]}" +elif [[ $1 -eq 6 ]]&&[ -z "$result2" ]&&[[ $tmpresult2 != "curl"* ]];then +chatgpt[ustatus]="${smedia[yes]}" +chatgpt[uregion]=" [$countryCode] " +chatgpt[utype]="$resultunlocktype" +else +chatgpt[ustatus]="${smedia[bad]}" +chatgpt[uregion]="${smedia[nodata]}" +chatgpt[utype]="${smedia[nodata]}" +fi +} +get_sorted_mx_records(){ +local domain=$1 +dig +short MX $domain|sort -n|head -1|awk '{print $2}' +} +check_email_service(){ +local service=$1 +local port=25 +local expected_response="220" +local domain="" +local host="" +local response="" +local success="false" +case $service in +"Gmail")domain="gmail.com";; +"Outlook")domain="outlook.com";; +"Yahoo")domain="yahoo.com";; +"Apple")domain="me.com";; +"MailRU")domain="mail.ru";; +"AOL")domain="aol.com";; +"GMX")domain="gmx.com";; +"MailCOM")domain="mail.com";; +"163")domain="163.com";; +"Sohu")domain="sohu.com";; +"Sina")domain="sina.com";; +"QQ")domain="qq.com";; +*)return +esac +if [[ -z $host ]];then +local mx_hosts=($(get_sorted_mx_records $domain)) +for host in "${mx_hosts[@]}";do +response=$(timeout 5 bash -c "echo -e 'QUIT\r\n' | nc -s $IP -w4 $host $port 2>&1") +smail_response[$service]=$response +if [[ $response == *"$expected_response"* ]];then +success="true" +smail[$service]="$Font_Black+$Font_Suffix$Back_Green$Font_White$Font_B$service$Font_Suffix" +smailstatus[$service]="true" +smail[remote]=1 +break +fi +done +else +response=$(timeout 5 bash -c "echo -e 'QUIT\r\n' | nc -s $IP -w4 $host $port 2>&1") +if [[ $response == *"$expected_response"* ]];then +success="true" +smail[$service]="$Font_Black+$Font_Suffix$Back_Green$Font_White$Font_B$service$Font_Suffix" +smailstatus[$service]="true" +smail[remote]=1 +fi +fi +if [[ $success == "false" ]];then +smail[$service]="$Font_Black-$Font_Suffix$Back_Red$Font_White$Font_B$service$Font_Suffix" +smailstatus[$service]="false" +fi +} +check_mail(){ +ss -tano|grep -q ":25\b"&&smail[local]=2||smail[local]=0 +if [[ smail[local] -ne 2 && -z $usePROXY ]];then +local response=$(timeout 10 bash -c "echo -e 'QUIT\r\n' | nc -s $IP -p25 -w9 smtp.mailgun.org 25 2>&1") +[[ $response == *"220"* ]]&&smail[local]=1 +fi +[[ -n $usePROXY ]]&&smail[local]=0 +smail[remote]=0 +services=("Gmail" "Outlook" "Yahoo" "Apple" "QQ" "MailRU" "AOL" "GMX" "MailCOM" "163" "Sohu" "Sina") +for service in "${services[@]}";do +local temp_info="$Font_Cyan$Font_B${sinfo[mail]}$Font_I$service$Font_Suffix " +((ibar_step+=3)) +show_progress_bar "$temp_info" $((40-1-${#service}-${sinfo[lmail]}))& +bar_pid="$!"&&disown "$bar_pid" +check_email_service $service +kill_progress_bar +done +} +check_dnsbl_parallel(){ +ip_to_check=$1 +parallel_jobs=$2 +smail[t]=0 +smail[c]=0 +smail[m]=0 +smail[b]=0 +reversed_ip=$(echo "$ip_to_check"|awk -F. '{print $4"."$3"."$2"."$1}') +local total=0 +local clean=0 +local blacklisted=0 +local other=0 +curl $CurlARG -sL "${rawgithub}main/ref/dnsbl.list"|sort -u|xargs -P "$parallel_jobs" -I {} bash -c "result=\$(dig +short \"$reversed_ip.{}\" A); if [[ -z \"\$result\" ]]; then echo 'Clean'; elif [[ \"\$result\" == '127.0.0.2' ]]; then echo 'Blacklisted'; else echo 'Other'; fi"|{ +while IFS= read -r line;do +((total++)) +case "$line" in +"Clean")((clean++));; +"Blacklisted")((blacklisted++));; +*)((other++)) +esac +done +smail[t]="$total" +smail[c]="$clean" +smail[m]="$other" +smail[b]="$blacklisted" +echo "${smail[t]} ${smail[c]} ${smail[m]} ${smail[b]}" +} +} +check_dnsbl(){ +local temp_info="$Font_Cyan$Font_B${sinfo[dnsbl]} $Font_Suffix" +((ibar_step=95)) +show_progress_bar "$temp_info" $((40-1-${sinfo[ldnsbl]}))& +bar_pid="$!"&&disown "$bar_pid" +trap "kill_progress_bar" RETURN +local num_array=($(check_dnsbl_parallel "$IP" 50)) +smail[t]=${num_array[0]:-0} +smail[c]=${num_array[1]:-0} +smail[m]=${num_array[2]:-0} +smail[b]=${num_array[3]:-0} +smail[sdnsbl]="$Font_Cyan${smail[dnsbl]} ${smail[available]}${smail[t]} ${smail[clean]}${smail[c]} ${smail[marked]}${smail[m]} ${smail[blacklisted]}${smail[b]}$Font_Suffix" +} +show_head(){ +echo -ne "\r$(printf '%72s'|tr ' ' '#')\n" +if [[ $mode_lite -eq 0 ]];then +if [ $fullIP -eq 1 ];then +calc_padding "$(printf '%*s' "${shead[ltitle]}" '')$IP" 72 +echo -ne "\r$PADDING$Font_B${shead[title]}$Font_Cyan$IP$Font_Suffix\n" +else +calc_padding "$(printf '%*s' "${shead[ltitle]}" '')$IPhide" 72 +echo -ne "\r$PADDING$Font_B${shead[title]}$Font_Cyan$IPhide$Font_Suffix\n" +fi +else +if [ $fullIP -eq 1 ];then +calc_padding "$(printf '%*s' "${shead[ltitle_lite]}" '')$IP" 72 +echo -ne "\r$PADDING$Font_B${shead[title_lite]}$Font_Cyan$IP$Font_Suffix\n" +else +calc_padding "$(printf '%*s' "${shead[ltitle_lite]}" '')$IPhide" 72 +echo -ne "\r$PADDING$Font_B${shead[title_lite]}$Font_Cyan$IPhide$Font_Suffix\n" +fi +fi +calc_padding "${shead[git]}" 72 +echo -ne "\r$PADDING$Font_U${shead[git]}$Font_Suffix\n" +calc_padding "${shead[bash]}" 72 +echo -ne "\r$PADDING${shead[bash]}\n" +echo -ne "\r${shead[ptime]}${shead[time]} ${shead[ver]}\n" +echo -ne "\r$(printf '%72s'|tr ' ' '#')\n" +} +show_basic(){ +echo -ne "\r${sbasic[title]}\n" +if [[ -n ${maxmind[asn]} && ${maxmind[asn]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[asn]}${Font_Green}AS${maxmind[asn]}$Font_Suffix\n" +echo -ne "\r$Font_Cyan${sbasic[org]}$Font_Green${maxmind[org]}$Font_Suffix\n" +else +echo -ne "\r$Font_Cyan${sbasic[asn]}${sbasic[noasn]}$Font_Suffix\n" +fi +if [[ ${maxmind[dms]} != "null" && ${maxmind[map]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[location]}$Font_Green${maxmind[dms]}$Font_Suffix\n" +echo -ne "\r$Font_Cyan${sbasic[map]}$Font_U$Font_Green${maxmind[map]}$Font_Suffix\n" +fi +local city_info="" +if [[ -n ${maxmind[sub]} && ${maxmind[sub]} != "null" ]];then +city_info+="${maxmind[sub]}" +fi +if [[ -n ${maxmind[city]} && ${maxmind[city]} != "null" ]];then +[[ -n $city_info ]]&&city_info+=", " +city_info+="${maxmind[city]}" +fi +if [[ -n ${maxmind[post]} && ${maxmind[post]} != "null" ]];then +[[ -n $city_info ]]&&city_info+=", " +city_info+="${maxmind[post]}" +fi +if [[ -n $city_info ]];then +echo -ne "\r$Font_Cyan${sbasic[city]}$Font_Green$city_info$Font_Suffix\n" +fi +if [[ -n ${maxmind[countrycode]} && ${maxmind[countrycode]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[country]}$Font_Green[${maxmind[countrycode]}]${maxmind[country]}$Font_Suffix" +if [[ -n ${maxmind[continentcode]} && ${maxmind[continentcode]} != "null" ]];then +echo -ne "$Font_Green, [${maxmind[continentcode]}]${maxmind[continent]}$Font_Suffix\n" +else +echo -ne "\n" +fi +elif [[ -n ${maxmind[continentcode]} && ${maxmind[continentcode]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[continent]}$Font_Green[${maxmind[continentcode]}]${maxmind[continent]}$Font_Suffix\n" +fi +if [[ -n ${maxmind[regcountrycode]} && ${maxmind[regcountrycode]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[regcountry]}$Font_Green[${maxmind[regcountrycode]}]${maxmind[regcountry]}$Font_Suffix\n" +fi +if [[ -n ${maxmind[timezone]} && ${maxmind[timezone]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[timezone]}$Font_Green${maxmind[timezone]}$Font_Suffix\n" +fi +if [[ -n ${maxmind[countrycode]} && ${maxmind[countrycode]} != "null" ]];then +if [ "${maxmind[countrycode]}" == "${maxmind[regcountrycode]}" ];then +echo -ne "\r$Font_Cyan${sbasic[type]}$Back_Green$Font_B$Font_White${sbasic[type0]}$Font_Suffix\n" +else +echo -ne "\r$Font_Cyan${sbasic[type]}$Back_Red$Font_B$Font_White${sbasic[type1]}$Font_Suffix\n" +fi +fi +} +show_basic_lite(){ +echo -ne "\r${sbasic[title_lite]}\n" +if [[ -n ${ipinfo[asn]} && ${ipinfo[asn]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[asn]}${Font_Green}AS${ipinfo[asn]}$Font_Suffix\n" +echo -ne "\r$Font_Cyan${sbasic[org]}$Font_Green${ipinfo[org]}$Font_Suffix\n" +else +echo -ne "\r$Font_Cyan${sbasic[asn]}${sbasic[noasn]}$Font_Suffix\n" +fi +if [[ ${ipinfo[dms]} != "null" && ${ipinfo[map]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[location]}$Font_Green${ipinfo[dms]}$Font_Suffix\n" +echo -ne "\r$Font_Cyan${sbasic[map]}$Font_U$Font_Green${ipinfo[map]}$Font_Suffix\n" +fi +local city_info="" +if [[ -n ${ipinfo[sub]} && ${ipinfo[sub]} != "null" ]];then +city_info+="${ipinfo[sub]}" +fi +if [[ -n ${ipinfo[city]} && ${ipinfo[city]} != "null" ]];then +[[ -n $city_info ]]&&city_info+=", " +city_info+="${ipinfo[city]}" +fi +if [[ -n ${ipinfo[post]} && ${ipinfo[post]} != "null" ]];then +[[ -n $city_info ]]&&city_info+=", " +city_info+="${ipinfo[post]}" +fi +if [[ -n $city_info ]];then +echo -ne "\r$Font_Cyan${sbasic[city]}$Font_Green$city_info$Font_Suffix\n" +fi +if [[ -n ${ipinfo[countrycode]} && ${ipinfo[countrycode]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[country]}$Font_Green[${ipinfo[countrycode]}]${ipinfo[country]}$Font_Suffix" +if [[ -n ${ipinfo[continent]} && ${ipinfo[continent]} != "null" ]];then +echo -ne "$Font_Green, ${ipinfo[continent]}$Font_Suffix\n" +else +echo -ne "\n" +fi +elif [[ -n ${ipinfo[continent]} && ${ipinfo[continent]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[continent]}$Font_Green${ipinfo[continent]}$Font_Suffix\n" +fi +if [[ -n ${ipinfo[regcountrycode]} && ${ipinfo[regcountrycode]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[regcountry]}$Font_Green[${ipinfo[regcountrycode]}]${ipinfo[regcountry]}$Font_Suffix\n" +fi +if [[ -n ${ipinfo[timezone]} && ${ipinfo[timezone]} != "null" ]];then +echo -ne "\r$Font_Cyan${sbasic[timezone]}$Font_Green${ipinfo[timezone]}$Font_Suffix\n" +fi +if [[ -n ${ipinfo[countrycode]} && ${ipinfo[countrycode]} != "null" ]];then +if [ "${ipinfo[countrycode]}" == "${ipinfo[regcountrycode]}" ];then +echo -ne "\r$Font_Cyan${sbasic[type]}$Back_Green$Font_B$Font_White${sbasic[type0]}$Font_Suffix\n" +else +echo -ne "\r$Font_Cyan${sbasic[type]}$Back_Red$Font_B$Font_White${sbasic[type1]}$Font_Suffix\n" +fi +fi +} +show_type(){ +echo -ne "\r${stype[title]}\n" +echo -ne "\r$Font_Cyan${stype[db]}$Font_I IPinfo ipregistry ipapi IP2Location AbuseIPDB $Font_Suffix\n" +echo -ne "\r$Font_Cyan${stype[usetype]}$Font_Suffix${ipinfo[susetype]}${ipregistry[susetype]}${ipapi[susetype]}${ip2location[susetype]}${abuseipdb[susetype]}\n" +echo -ne "\r$Font_Cyan${stype[comtype]}$Font_Suffix${ipinfo[scomtype]}${ipregistry[scomtype]}${ipapi[scomtype]}${ip2location[scomtype]}\n" +} +show_type_lite(){ +echo -ne "\r${stype[title]}\n" +echo -ne "\r$Font_Cyan${stype[db]}$Font_I IPinfo ipregistry ipapi $Font_Suffix\n" +echo -ne "\r$Font_Cyan${stype[usetype]}$Font_Suffix${ipinfo[susetype]}${ipregistry[susetype]}${ipapi[susetype]}\n" +echo -ne "\r$Font_Cyan${stype[comtype]}$Font_Suffix${ipinfo[scomtype]}${ipregistry[scomtype]}${ipapi[scomtype]}\n" +} +sscore_text(){ +local text="$1" +local p2=$2 +local p3=$3 +local p4=$4 +local p5=$5 +local p6=$6 +local tmplen +local tmp +if ((p2>=p4));then +tmplen=$((49+15*(p2-p4)/(p5-p4)-p6)) +elif ((p2>=p3));then +tmplen=$((33+16*(p2-p3)/(p4-p3)-p6)) +elif ((p2>=0));then +tmplen=$((17+16*p2/p3-p6)) +else +tmplen=0 +fi +tmp=$(printf '%*s' $tmplen '') +local total_length=${#tmp} +local text_length=${#text} +local tmp1="${tmp:1:total_length-text_length}$text|" +sscore[text1]="${tmp1:1:16-p6}" +sscore[text2]="${tmp1:17-p6:16}" +sscore[text3]="${tmp1:33-p6:16}" +sscore[text4]="${tmp1:49-p6}" +} +show_score(){ +echo -ne "\r${sscore[title]}\n" +echo -ne "\r${sscore[range]}\n" +if [[ -n ${ip2location[score]} && $mode_lite -eq 0 ]];then +sscore_text "${ip2location[score]}" ${ip2location[score]} 33 66 99 13 +echo -ne "\r${Font_Cyan}IP2Location${sscore[colon]}$Font_White$Font_B${sscore[text1]}$Back_Green${sscore[text2]}$Back_Yellow${sscore[text3]}$Back_Red${sscore[text4]}$Font_Suffix${ip2location[risk]}\n" +fi +if [[ -n ${scamalytics[score]} ]];then +sscore_text "${scamalytics[score]}" ${scamalytics[score]} 20 60 100 13 +echo -ne "\r${Font_Cyan}Scamalytics${sscore[colon]}$Font_White$Font_B${sscore[text1]}$Back_Green${sscore[text2]}$Back_Yellow${sscore[text3]}$Back_Red${sscore[text4]}$Font_Suffix${scamalytics[risk]}\n" +fi +if [[ -n ${ipapi[score]} ]];then +local tmp_score=$(echo "${ipapi[scorenum]} * 10000 / 1"|bc) +sscore_text "${ipapi[score]}" $tmp_score 85 300 10000 7 +echo -ne "\r${Font_Cyan}ipapi${sscore[colon]}$Font_White$Font_B${sscore[text1]}$Back_Green${sscore[text2]}$Back_Yellow${sscore[text3]}$Back_Red${sscore[text4]}$Font_Suffix${ipapi[risk]}\n" +fi +if [[ $mode_lite -eq 0 ]];then +sscore_text "${abuseipdb[score]}" ${abuseipdb[score]} 25 25 100 11 +[[ -n ${abuseipdb[score]} ]]&&echo -ne "\r${Font_Cyan}AbuseIPDB${sscore[colon]}$Font_White$Font_B${sscore[text1]}$Back_Green${sscore[text2]}$Back_Yellow${sscore[text3]}$Back_Red${sscore[text4]}$Font_Suffix${abuseipdb[risk]}\n" +if [ -n "${ipqs[score]}" ]&&[ "${ipqs[score]}" != "null" ];then +sscore_text "${ipqs[score]}" ${ipqs[score]} 75 85 100 6 +echo -ne "\r${Font_Cyan}IPQS${sscore[colon]}$Font_White$Font_B${sscore[text1]}$Back_Green${sscore[text2]}$Back_Yellow${sscore[text3]}$Back_Red${sscore[text4]}$Font_Suffix${ipqs[risk]}\n" +fi +fi +sscore_text " " ${dbip[score]} 33 66 100 7 +[[ -n ${dbip[risk]} ]]&&echo -ne "\r${Font_Cyan}DB-IP${sscore[colon]}$Font_White$Font_B${sscore[text1]}$Back_Green${sscore[text2]}$Back_Yellow${sscore[text3]}$Back_Red${sscore[text4]}$Font_Suffix${dbip[risk]}\n" +} +format_factor(){ +local tmp_txt=" " +if [[ $1 == "true" ]];then +tmp_txt+="${sfactor[yes]}" +elif [[ $1 == "false" ]];then +tmp_txt+="${sfactor[no]}" +elif [ ${#1} -eq 2 ];then +tmp_txt+="$Font_Green[$1]$Font_Suffix" +else +tmp_txt+="${sfactor[na]}" +fi +tmp_txt+=" " +if [[ $2 == "true" ]];then +tmp_txt+="${sfactor[yes]}" +elif [[ $2 == "false" ]];then +tmp_txt+="${sfactor[no]}" +elif [ ${#2} -eq 2 ];then +tmp_txt+="$Font_Green[$2]$Font_Suffix" +else +tmp_txt+="${sfactor[na]}" +fi +tmp_txt+=" " +if [[ $3 == "true" ]];then +tmp_txt+="${sfactor[yes]}" +elif [[ $3 == "false" ]];then +tmp_txt+="${sfactor[no]}" +elif [ ${#3} -eq 2 ];then +tmp_txt+="$Font_Green[$3]$Font_Suffix" +else +tmp_txt+="${sfactor[na]}" +fi +tmp_txt+=" " +if [[ $4 == "true" ]];then +tmp_txt+="${sfactor[yes]}" +elif [[ $4 == "false" ]];then +tmp_txt+="${sfactor[no]}" +elif [ ${#4} -eq 2 ];then +tmp_txt+="$Font_Green[$4]$Font_Suffix" +else +tmp_txt+="${sfactor[na]}" +fi +if [[ $mode_lite -eq 0 ]];then +tmp_txt+=" " +if [[ $5 == "true" ]];then +tmp_txt+="${sfactor[yes]}" +elif [[ $5 == "false" ]];then +tmp_txt+="${sfactor[no]}" +elif [ ${#5} -eq 2 ];then +tmp_txt+="$Font_Green[$5]$Font_Suffix" +else +tmp_txt+="${sfactor[na]}" +fi +tmp_txt+=" " +if [[ $6 == "true" ]];then +tmp_txt+="${sfactor[yes]}" +elif [[ $6 == "false" ]];then +tmp_txt+="${sfactor[no]}" +elif [ ${#6} -eq 2 ];then +tmp_txt+="$Font_Green[$6]$Font_Suffix" +else +tmp_txt+="${sfactor[na]}" +fi +tmp_txt+=" " +if [[ $7 == "true" ]];then +tmp_txt+="${sfactor[yes]}" +elif [[ $7 == "false" ]];then +tmp_txt+="${sfactor[no]}" +elif [ ${#7} -eq 2 ];then +tmp_txt+="$Font_Green[$7]$Font_Suffix" +else +tmp_txt+="${sfactor[na]}" +fi +tmp_txt+=" " +if [[ $8 == "true" ]];then +tmp_txt+="${sfactor[yes]}" +elif [[ $8 == "false" ]];then +tmp_txt+="${sfactor[no]}" +elif [ ${#8} -eq 2 ];then +tmp_txt+="$Font_Green[$8]$Font_Suffix" +else +tmp_txt+="${sfactor[na]}" +fi +fi +echo "$tmp_txt" +} +show_factor(){ +local tmp_factor="" +echo -ne "\r${sfactor[title]}\n" +echo -ne "\r$Font_Cyan${sfactor[factor]}${Font_I}IP2Location ipapi ipregistry IPQS Scamalytics ipdata IPinfo DB-IP$Font_Suffix\n" +tmp_factor=$(format_factor "${ip2location[countrycode]}" "${ipapi[countrycode]}" "${ipregistry[countrycode]}" "${ipqs[countrycode]}" "${scamalytics[countrycode]}" "${ipdata[countrycode]}" "${ipinfo[countrycode]}" "${dbip[countrycode]}") +echo -ne "\r$Font_Cyan${sfactor[countrycode]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ip2location[proxy]}" "${ipapi[proxy]}" "${ipregistry[proxy]}" "${ipqs[proxy]}" "${scamalytics[proxy]}" "${ipdata[proxy]}" "${ipinfo[proxy]}" "${dbip[proxy]}") +echo -ne "\r$Font_Cyan${sfactor[proxy]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ip2location[tor]}" "${ipapi[tor]}" "${ipregistry[tor]}" "${ipqs[tor]}" "${scamalytics[tor]}" "${ipdata[tor]}" "${ipinfo[tor]}" "${dbip[tor]}") +echo -ne "\r$Font_Cyan${sfactor[tor]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ip2location[vpn]}" "${ipapi[vpn]}" "${ipregistry[vpn]}" "${ipqs[vpn]}" "${scamalytics[vpn]}" "${ipdata[vpn]}" "${ipinfo[vpn]}" "${dbip[vpn]}") +echo -ne "\r$Font_Cyan${sfactor[vpn]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ip2location[server]}" "${ipapi[server]}" "${ipregistry[server]}" "${ipqs[server]}" "${scamalytics[server]}" "${ipdata[server]}" "${ipinfo[server]}" "${dbip[server]}") +echo -ne "\r$Font_Cyan${sfactor[server]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ip2location[abuser]}" "${ipapi[abuser]}" "${ipregistry[abuser]}" "${ipqs[abuser]}" "${scamalytics[abuser]}" "${ipdata[abuser]}" "${ipinfo[abuser]}" "${dbip[abuser]}") +echo -ne "\r$Font_Cyan${sfactor[abuser]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ip2location[robot]}" "${ipapi[robot]}" "${ipregistry[robot]}" "${ipqs[robot]}" "${scamalytics[robot]}" "${ipdata[robot]}" "${ipinfo[robot]}" "${dbip[robot]}") +echo -ne "\r$Font_Cyan${sfactor[robot]}$Font_Suffix$tmp_factor\n" +} +show_factor_lite(){ +local tmp_factor="" +echo -ne "\r${sfactor[title]}\n" +echo -ne "\r$Font_Cyan${sfactor[factor]}$Font_I ipapi ipregistry IPinfo DB-IP$Font_Suffix\n" +tmp_factor=$(format_factor "${ipapi[countrycode]}" "${ipregistry[countrycode]}" "${ipinfo[countrycode]}" "${dbip[countrycode]}") +echo -ne "\r$Font_Cyan${sfactor[countrycode]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ipapi[proxy]}" "${ipregistry[proxy]}" "${ipinfo[proxy]}" "${dbip[proxy]}") +echo -ne "\r$Font_Cyan${sfactor[proxy]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ipapi[tor]}" "${ipregistry[tor]}" "${ipinfo[tor]}" "${dbip[tor]}") +echo -ne "\r$Font_Cyan${sfactor[tor]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ipapi[vpn]}" "${ipregistry[vpn]}" "${ipinfo[vpn]}" "${dbip[vpn]}") +echo -ne "\r$Font_Cyan${sfactor[vpn]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ipapi[server]}" "${ipregistry[server]}" "${ipinfo[server]}" "${dbip[server]}") +echo -ne "\r$Font_Cyan${sfactor[server]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ipapi[abuser]}" "${ipregistry[abuser]}" "${ipinfo[abuser]}" "${dbip[abuser]}") +echo -ne "\r$Font_Cyan${sfactor[abuser]}$Font_Suffix$tmp_factor\n" +tmp_factor=$(format_factor "${ipapi[robot]}" "${ipregistry[robot]}" "${ipinfo[robot]}" "${dbip[robot]}") +echo -ne "\r$Font_Cyan${sfactor[robot]}$Font_Suffix$tmp_factor\n" +} +show_media(){ +echo -ne "\r${smedia[title]}\n" +echo -ne "\r$Font_Cyan${smedia[meida]}$Font_I TikTok Disney+ Netflix Youtube AmazonPV Reddit ChatGPT $Font_Suffix\n" +echo -ne "\r$Font_Cyan${smedia[status]}${tiktok[ustatus]}${disney[ustatus]}${netflix[ustatus]}${youtube[ustatus]}${amazon[ustatus]}${reddit[ustatus]}${chatgpt[ustatus]}$Font_Suffix\n" +echo -ne "\r$Font_Cyan${smedia[region]}$Font_Green${tiktok[uregion]}${disney[uregion]}${netflix[uregion]}${youtube[uregion]}${amazon[uregion]}${reddit[uregion]}${chatgpt[uregion]}$Font_Suffix\n" +echo -ne "\r$Font_Cyan${smedia[type]}${tiktok[utype]}${disney[utype]}${netflix[utype]}${youtube[utype]}${amazon[utype]}${reddit[utype]}${chatgpt[utype]}$Font_Suffix\n" +} +show_mail(){ +echo -ne "\r${smail[title]}\n" +if [ ${smail[local]} -eq 1 ];then +echo -ne "\r$Font_Cyan${smail[port]}$Font_Suffix${smail[yes]}\n" +elif [ ${smail[local]} -eq 2 ];then +echo -ne "\r$Font_Cyan${smail[port]}$Font_Suffix${smail[occupied]}\n" +else +echo -ne "\r$Font_Cyan${smail[port]}$Font_Suffix${smail[no]}\n" +fi +if [ ${smail[remote]} -eq 1 ];then +echo -ne "\r$Font_Cyan${smail[provider]}$Font_Suffix" +for service in "${services[@]}";do +echo -ne "${smail[$service]}" +done +echo "" +else +echo -ne "\r$Font_Cyan${smail[provider]}${smail[blocked]}$Font_Suffix\n" +fi +[[ $1 -eq 4 ]]&&echo -ne "\r${smail[sdnsbl]}\n" +} +show_tail(){ +echo -ne "\r$(printf '%72s'|tr ' ' '=')\n" +echo -ne "\r$Font_I${stail[stoday]}${stail[today]}${stail[stotal]}${stail[total]}${stail[thanks]} $Font_Suffix\n" +echo -e "" +} +get_opts(){ +while getopts "i:l:o:x:fhjnpyEM46" opt;do +case $opt in +4)if +[[ IPV4check -ne 0 ]] +then +IPV6check=0 +else +ERRORcode=4 +fi +;; +6)if +[[ IPV6check -ne 0 ]] +then +IPV4check=0 +else +ERRORcode=6 +fi +;; +f)fullIP=1 +;; +h)show_help +;; +i)iface="$OPTARG" +useNIC=" --interface $iface" +CurlARG+="$useNIC" +get_ipv4 +get_ipv6 +is_valid_ipv4 $IPV4 +is_valid_ipv6 $IPV6 +[[ $IPV4work -eq 0 && $IPV6work -eq 0 ]]&&ERRORcode=7 +;; +j)mode_json=1 +;; +l)YY=$(echo "$OPTARG"|tr '[:upper:]' '[:lower:]') +;; +n)mode_no=1 +;; +o)mode_output=1 +outputfile="$OPTARG" +[[ -z $outputfile ]]&&{ +ERRORcode=1 +break +} +[[ -e $outputfile ]]&&{ +ERRORcode=10 +break +} +touch "$outputfile" 2>/dev/null||{ +ERRORcode=11 +break +} +;; +p)mode_privacy=1 +;; +x)xproxy="$OPTARG" +usePROXY=" -x $xproxy" +CurlARG+="$usePROXY" +get_ipv4 +get_ipv6 +is_valid_ipv4 $IPV4 +is_valid_ipv6 $IPV6 +[[ $IPV4work -eq 0 && $IPV6work -eq 0 ]]&&ERRORcode=8 +;; +y)mode_yes=1 +;; +E)YY="en" +;; +M)mode_menu=1 +;; +\?)ERRORcode=1 +esac +done +if [[ $mode_menu -eq 1 ]];then +if [[ $YY == "cn" ]];then +eval "bash <(curl -sL https://Check.Place) -I" +else +eval "bash <(curl -sL https://Check.Place) -EI" +fi +exit 0 +fi +[[ $IPV4check -eq 1 && $IPV6check -eq 0 && $IPV4work -eq 0 ]]&&ERRORcode=40 +[[ $IPV4check -eq 0 && $IPV6check -eq 1 && $IPV6work -eq 0 ]]&&ERRORcode=60 +CurlARG="$useNIC$usePROXY" +} +show_help(){ +echo -ne "\r$shelp\n" +exit 0 +} +show_ad(){ +RANDOM=$(date +%s) +local -a ads=() +local i=1 +while :;do +local content +content=$(curl -fsL --max-time 5 "${rawgithub}main/ref/ad$i.ans")||break +ads+=("$content") +((i++)) +done +ADLines=0 +local adCount=${#ads[@]} +[[ $adCount -eq 0 ]]&&return +local -a indices=() +for ((i=1; i<=adCount; i++));do indices+=("$i");done +for ((i=adCount-1; i>0; i--));do +local j=$((RANDOM%(i+1))) +local tmp=${indices[i]} +indices[i]=${indices[j]} +indices[j]=$tmp +done +local -a aad +aad[0]=$(curl -sL --max-time 5 "${rawgithub}main/ref/sponsor.ans") +for ((i=0; i/dev/null);then cols=0;fi +print_pair(){ +local left="$1" right="$2" +local -a L R +mapfile -t L <<<"$left" +mapfile -t R <<<"$right" +local i +for ((i=0; i<12; i++));do +printf "%-72s$Font_Suffix %-72s\n" "${L[i]}" "${R[i]}" 1>&2 +done +ADLines=$((ADLines+12)) +} +print_block(){ +echo "$1" 1>&2 +ADLines=$((ADLines+12)) +} +if [[ $cols -ge 150 ]];then +if ((adCount==0));then +print_block "${aad[0]}" +elif ((adCount%2==1));then +print_pair "${aad[0]}" "${aad[1]}" +local k +for ((k=2; k<=adCount; k+=2));do +print_pair "${aad[$k]}" "${aad[$((k+1))]}" +done +else +print_block "${aad[0]}" +local k +for ((k=1; k<=adCount; k+=2));do +print_pair "${aad[$k]}" "${aad[$((k+1))]}" +done +fi +else +echo "${aad[0]}" 1>&2 +for ((i=1; i<=adCount; i++));do +echo "${aad[$i]}" 1>&2 +done +ADLines=$(((adCount+1)*12)) +fi +} +read_ref(){ +Media_Cookie=$(curl $CurlARG -sL --retry 3 --max-time 10 "${rawgithub}main/ref/cookies.txt") +IATA_Database="${rawgithub}main/ref/iata-icao.csv" +} +clean_ansi(){ +local input="$1" +input=$(echo "$input"|sed 's/\\033/\x1b/g') +input=$(echo "$input"|sed 's/\x1b\[[0-9;]*m//g') +input=$(echo "$input"|sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') +echo -n "$input" +} +factor_bool(){ +local tmp_txt="" +if [[ $1 == "true" ]];then +tmp_txt+=".Factor |= . * { $3: { $2: true } } | " +elif [[ $1 == "false" ]];then +tmp_txt+=".Factor |= . * { $3: { $2: false } } | " +elif [ ${#1} -eq 2 ];then +tmp_txt+=".Factor |= . * { $3: { $2: \"$1\" } } | " +else +tmp_txt+=".Factor |= . * { $3: { $2: null } } | " +fi +[[ -z $tmp_txt ]]&&tmp_txt="null" +echo "$tmp_txt" +} +save_json(){ +local head_updates="" +local basic_updates="" +local type_updates="" +local score_updates="" +local factor_updates="" +local media_updates="" +local mail_updates="" +if [ $fullIP -eq 1 ];then +head_updates+=".Head |= . + { IP: \"${IP:-null}\" } | " +else +head_updates+=".Head |= . + { IP: \"${IPhide:-null}\" } | " +fi +head_updates+=".Head |= . + { Command: \"${shead[bash]:-null}\" } | " +head_updates+=".Head |= . + { GitHub: \"${shead[git]:-null}\" } | " +head_updates+=".Head |= . + { Time: \"${shead[time_raw]:-null}\" } | " +head_updates+=".Head |= . + { Version: \"${script_version:-null}\" } | " +if [ $mode_lite -eq 0 ];then +basic_updates+=".Info |= . + { ASN: \"${maxmind[asn]:-null}\" } | " +basic_updates+=".Info |= . + { Organization: \"${maxmind[org]:-null}\" } | " +basic_updates+=".Info |= . + { Latitude: \"${maxmind[lat]:-null}\" } | " +basic_updates+=".Info |= . + { Longitude: \"${maxmind[lon]:-null}\" } | " +basic_updates+=".Info |= . + { DMS: \"${maxmind[dms]:-null}\" } | " +basic_updates+=".Info |= . + { Map: \"${maxmind[map]:-null}\" } | " +basic_updates+=".Info |= . + { TimeZone: \"${maxmind[timezone]:-null}\" } | " +basic_updates+=".Info |= . * { City: { Name: \"${maxmind[city]:-null}\" } } | " +basic_updates+=".Info |= . * { City: { PostalCode: \"${maxmind[post]:-null}\" } } | " +basic_updates+=".Info |= . * { City: { SubCode: \"${maxmind[subcode]:-null}\" } } | " +basic_updates+=".Info |= . * { City: { Subdivisions: \"${maxmind[sub]:-null}\" } } | " +basic_updates+=".Info |= . * { Region: { Code: \"${maxmind[countrycode]:-null}\" } } | " +basic_updates+=".Info |= . * { Region: { Name: \"${maxmind[country]:-null}\" } } | " +basic_updates+=".Info |= . * { Continent: { Code: \"${maxmind[continentcode]:-null}\" } } | " +basic_updates+=".Info |= . * { Continent: { Name: \"${maxmind[continent]:-null}\" } } | " +basic_updates+=".Info |= . * { RegisteredRegion: { Code: \"${maxmind[regcountrycode]:-null}\" } } | " +basic_updates+=".Info |= . * { RegisteredRegion: { Name: \"${maxmind[regcountry]:-null}\" } } | " +if [[ -n ${maxmind[countrycode]} && ${maxmind[countrycode]} != "null" ]];then +if [ "${maxmind[countrycode]}" == "${maxmind[regcountrycode]}" ];then +basic_updates+=".Info |= . + { Type: \"$(clean_ansi "${sbasic[type0]:-null}")\" } | " +else +basic_updates+=".Info |= . + { Type: \"$(clean_ansi "${sbasic[type1]:-null}")\" } | " +fi +else +basic_updates+='.Info |= . + { Type: "null" } | ' +fi +else +basic_updates+=".Info |= . + { ASN: \"${ipinfo[asn]:-null}\" } | " +basic_updates+=".Info |= . + { Organization: \"${ipinfo[org]:-null}\" } | " +basic_updates+=".Info |= . + { Latitude: \"${ipinfo[lat]:-null}\" } | " +basic_updates+=".Info |= . + { Longitude: \"${ipinfo[lon]:-null}\" } | " +basic_updates+=".Info |= . + { DMS: \"${ipinfo[dms]:-null}\" } | " +basic_updates+=".Info |= . + { Map: \"${ipinfo[map]:-null}\" } | " +basic_updates+=".Info |= . + { TimeZone: \"${ipinfo[timezone]:-null}\" } | " +basic_updates+=".Info |= . * { City: { Name: \"${ipinfo[city]:-null}\" } } | " +basic_updates+=".Info |= . * { City: { PostalCode: \"${ipinfo[post]:-null}\" } } | " +basic_updates+='.Info |= . * { City: { SubCode: "null" } } | ' +basic_updates+='.Info |= . * { City: { Subdivisions: "null" } } | ' +basic_updates+=".Info |= . * { Region: { Code: \"${ipinfo[countrycode]:-null}\" } } | " +basic_updates+=".Info |= . * { Region: { Name: \"${ipinfo[country]:-null}\" } } | " +basic_updates+='.Info |= . * { Continent: { Code: "null" } } | ' +basic_updates+=".Info |= . * { Continent: { Name: \"${ipinfo[continent]:-null}\" } } | " +basic_updates+=".Info |= . * { RegisteredRegion: { Code: \"${ipinfo[regcountrycode]:-null}\" } } | " +basic_updates+=".Info |= . * { RegisteredRegion: { Name: \"${ipinfo[regcountry]:-null}\" } } | " +if [[ -n ${ipinfo[countrycode]} && ${ipinfo[countrycode]} != "null" ]];then +if [ "${ipinfo[countrycode]}" == "${ipinfo[regcountrycode]}" ];then +basic_updates+=".Info |= . + { Type: \"$(clean_ansi "${sbasic[type0]:-null}")\" } | " +else +basic_updates+=".Info |= . + { Type: \"$(clean_ansi "${sbasic[type1]:-null}")\" } | " +fi +else +basic_updates+='.Info |= . + { Type: "null" } | ' +fi +fi +type_updates+=".Type |= . * { Usage: { IPinfo: \"$(clean_ansi "${ipinfo[susetype]:-null}")\" } } | " +type_updates+=".Type |= . * { Usage: { ipregistry: \"$(clean_ansi "${ipregistry[susetype]:-null}")\" } } | " +type_updates+=".Type |= . * { Usage: { ipapi: \"$(clean_ansi "${ipapi[susetype]:-null}")\" } } | " +type_updates+=".Type |= . * { Usage: { AbuseIPDB: \"$(clean_ansi "${abuseipdb[susetype]:-null}")\" } } | " +type_updates+=".Type |= . * { Usage: { IP2LOCATION: \"$(clean_ansi "${ip2location[susetype]:-null}")\" } } | " +type_updates+=".Type |= . * { Company: { IPinfo: \"$(clean_ansi "${ipinfo[scomtype]:-null}")\" } } | " +type_updates+=".Type |= . * { Company: { ipregistry: \"$(clean_ansi "${ipregistry[scomtype]:-null}")\" } } | " +type_updates+=".Type |= . * { Company: { ipapi: \"$(clean_ansi "${ipapi[scomtype]:-null}")\" } } | " +score_updates+=".Score |= . + { IP2LOCATION: \"${ip2location[score]:-null}\" } | " +score_updates+=".Score |= . + { SCAMALYTICS: \"${scamalytics[score]:-null}\" } | " +score_updates+=".Score |= . + { ipapi: \"${ipapi[score]:-null}\" } | " +score_updates+=".Score |= . + { AbuseIPDB: \"${abuseipdb[score]:-null}\" } | " +score_updates+=".Score |= . + { IPQS: \"${ipapi[ipqs]:-null}\" } | " +score_updates+=".Score |= . + { DBIP: \"${dbip[score]:-null}\" } | " +factor_updates+=$(factor_bool "${ip2location[countrycode]}" "IP2LOCATION" "CountryCode") +factor_updates+=$(factor_bool "${ipapi[countrycode]}" "ipapi" "CountryCode") +factor_updates+=$(factor_bool "${ipregistry[countrycode]}" "ipregistry" "CountryCode") +factor_updates+=$(factor_bool "${ipqs[countrycode]}" "IPQS" "CountryCode") +factor_updates+=$(factor_bool "${scamalytics[countrycode]}" "SCAMALYTICS" "CountryCode") +factor_updates+=$(factor_bool "${ipdata[countrycode]}" "ipdata" "CountryCode") +factor_updates+=$(factor_bool "${ipinfo[countrycode]}" "IPinfo" "CountryCode") +factor_updates+=$(factor_bool "${ipwhois[countrycode]}" "IPWHOIS" "CountryCode") +factor_updates+=$(factor_bool "${dbip[countrycode]}" "DBIP" "CountryCode") +factor_updates+=$(factor_bool "${ip2location[proxy]}" "IP2LOCATION" "Proxy") +factor_updates+=$(factor_bool "${ipapi[proxy]}" "ipapi" "Proxy") +factor_updates+=$(factor_bool "${ipregistry[proxy]}" "ipregistry" "Proxy") +factor_updates+=$(factor_bool "${ipqs[proxy]}" "IPQS" "Proxy") +factor_updates+=$(factor_bool "${scamalytics[proxy]}" "SCAMALYTICS" "Proxy") +factor_updates+=$(factor_bool "${ipdata[proxy]}" "ipdata" "Proxy") +factor_updates+=$(factor_bool "${ipinfo[proxy]}" "IPinfo" "Proxy") +factor_updates+=$(factor_bool "${ipwhois[proxy]}" "IPWHOIS" "Proxy") +factor_updates+=$(factor_bool "${dbip[proxy]}" "DBIP" "Proxy") +factor_updates+=$(factor_bool "${ip2location[tor]}" "IP2LOCATION" "Tor") +factor_updates+=$(factor_bool "${ipapi[tor]}" "ipapi" "Tor") +factor_updates+=$(factor_bool "${ipregistry[tor]}" "ipregistry" "Tor") +factor_updates+=$(factor_bool "${ipqs[tor]}" "IPQS" "Tor") +factor_updates+=$(factor_bool "${scamalytics[tor]}" "SCAMALYTICS" "Tor") +factor_updates+=$(factor_bool "${ipdata[tor]}" "ipdata" "Tor") +factor_updates+=$(factor_bool "${ipinfo[tor]}" "IPinfo" "Tor") +factor_updates+=$(factor_bool "${ipwhois[tor]}" "IPWHOIS" "Tor") +factor_updates+=$(factor_bool "${dbip[tor]}" "DBIP" "Tor") +factor_updates+=$(factor_bool "${ip2location[vpn]}" "IP2LOCATION" "VPN") +factor_updates+=$(factor_bool "${ipapi[vpn]}" "ipapi" "VPN") +factor_updates+=$(factor_bool "${ipregistry[vpn]}" "ipregistry" "VPN") +factor_updates+=$(factor_bool "${ipqs[vpn]}" "IPQS" "VPN") +factor_updates+=$(factor_bool "${scamalytics[vpn]}" "SCAMALYTICS" "VPN") +factor_updates+=$(factor_bool "${ipdata[vpn]}" "ipdata" "VPN") +factor_updates+=$(factor_bool "${ipinfo[vpn]}" "IPinfo" "VPN") +factor_updates+=$(factor_bool "${ipwhois[vpn]}" "IPWHOIS" "VPN") +factor_updates+=$(factor_bool "${dbip[vpn]}" "DBIP" "VPN") +factor_updates+=$(factor_bool "${ip2location[server]}" "IP2LOCATION" "Server") +factor_updates+=$(factor_bool "${ipapi[server]}" "ipapi" "Server") +factor_updates+=$(factor_bool "${ipregistry[server]}" "ipregistry" "Server") +factor_updates+=$(factor_bool "${ipqs[server]}" "IPQS" "Server") +factor_updates+=$(factor_bool "${scamalytics[server]}" "SCAMALYTICS" "Server") +factor_updates+=$(factor_bool "${ipdata[server]}" "ipdata" "Server") +factor_updates+=$(factor_bool "${ipinfo[server]}" "IPinfo" "Server") +factor_updates+=$(factor_bool "${ipwhois[server]}" "IPWHOIS" "Server") +factor_updates+=$(factor_bool "${dbip[server]}" "DBIP" "Server") +factor_updates+=$(factor_bool "${ip2location[abuser]}" "IP2LOCATION" "Abuser") +factor_updates+=$(factor_bool "${ipapi[abuser]}" "ipapi" "Abuser") +factor_updates+=$(factor_bool "${ipregistry[abuser]}" "ipregistry" "Abuser") +factor_updates+=$(factor_bool "${ipqs[abuser]}" "IPQS" "Abuser") +factor_updates+=$(factor_bool "${scamalytics[abuser]}" "SCAMALYTICS" "Abuser") +factor_updates+=$(factor_bool "${ipdata[abuser]}" "ipdata" "Abuser") +factor_updates+=$(factor_bool "${ipinfo[abuser]}" "IPinfo" "Abuser") +factor_updates+=$(factor_bool "${ipwhois[abuser]}" "IPWHOIS" "Abuser") +factor_updates+=$(factor_bool "${dbip[abuser]}" "DBIP" "Abuser") +factor_updates+=$(factor_bool "${ip2location[robot]}" "IP2LOCATION" "Robot") +factor_updates+=$(factor_bool "${ipapi[robot]}" "ipapi" "Robot") +factor_updates+=$(factor_bool "${ipregistry[robot]}" "ipregistry" "Robot") +factor_updates+=$(factor_bool "${ipqs[robot]}" "IPQS" "Robot") +factor_updates+=$(factor_bool "${scamalytics[robot]}" "SCAMALYTICS" "Robot") +factor_updates+=$(factor_bool "${ipdata[robot]}" "ipdata" "Robot") +factor_updates+=$(factor_bool "${ipinfo[robot]}" "IPinfo" "Robot") +factor_updates+=$(factor_bool "${ipwhois[robot]}" "IPWHOIS" "Robot") +factor_updates+=$(factor_bool "${dbip[robot]}" "DBIP" "Robot") +media_updates+=".Media |= . * { TikTok: { Status: \"$(clean_ansi "${tiktok[ustatus]:-null}")\" } } | " +media_updates+=".Media |= . * { DisneyPlus: { Status: \"$(clean_ansi "${disney[ustatus]:-null}")\" } } | " +media_updates+=".Media |= . * { Netflix: { Status: \"$(clean_ansi "${netflix[ustatus]:-null}")\" } } | " +media_updates+=".Media |= . * { Youtube: { Status: \"$(clean_ansi "${youtube[ustatus]:-null}")\" } } | " +media_updates+=".Media |= . * { AmazonPrimeVideo: { Status: \"$(clean_ansi "${amazon[ustatus]:-null}")\" } } | " +media_updates+=".Media |= . * { Reddit: { Status: \"$(clean_ansi "${reddit[ustatus]:-null}")\" } } | " +media_updates+=".Media |= . * { ChatGPT: { Status: \"$(clean_ansi "${chatgpt[ustatus]:-null}")\" } } | " +media_updates+=".Media |= . * { TikTok: { Region: \"$(clean_ansi "${tiktok[uregion]//[][]/}")\" } } | " +media_updates+=".Media |= . * { DisneyPlus: { Region: \"$(clean_ansi "${disney[uregion]//[][]/}")\" } } | " +media_updates+=".Media |= . * { Netflix: { Region: \"$(clean_ansi "${netflix[uregion]//[][]/}")\" } } | " +media_updates+=".Media |= . * { Youtube: { Region: \"$(clean_ansi "${youtube[uregion]//[][]/}")\" } } | " +media_updates+=".Media |= . * { AmazonPrimeVideo: { Region: \"$(clean_ansi "${amazon[uregion]//[][]/}")\" } } | " +media_updates+=".Media |= . * { Reddit: { Region: \"$(clean_ansi "${reddit[uregion]//[][]/}")\" } } | " +media_updates+=".Media |= . * { ChatGPT: { Region: \"$(clean_ansi "${chatgpt[uregion]//[][]/}")\" } } | " +media_updates+=".Media |= . * { TikTok: { Type: \"$(clean_ansi "${tiktok[utype]:-null}")\" } } | " +media_updates+=".Media |= . * { DisneyPlus: { Type: \"$(clean_ansi "${disney[utype]:-null}")\" } } | " +media_updates+=".Media |= . * { Netflix: { Type: \"$(clean_ansi "${netflix[utype]:-null}")\" } } | " +media_updates+=".Media |= . * { Youtube: { Type: \"$(clean_ansi "${youtube[utype]:-null}")\" } } | " +media_updates+=".Media |= . * { AmazonPrimeVideo: { Type: \"$(clean_ansi "${amazon[utype]:-null}")\" } } | " +media_updates+=".Media |= . * { Reddit: { Type: \"$(clean_ansi "${reddit[utype]:-null}")\" } } | " +media_updates+=".Media |= . * { ChatGPT: { Type: \"$(clean_ansi "${chatgpt[utype]:-null}")\" } } | " +if [[ ${smail[local]} -eq 1 ]];then +mail_updates+=".Mail |= . + { Port25: true } | " +for service in "${services[@]}";do +if [[ ${smailstatus[$service]} == "true" ]];then +mail_updates+=".Mail |= . + { \"$service\": true } | " +else +mail_updates+=".Mail |= . + { \"$service\": false } | " +fi +done +elif [[ ${smail[local]} -eq 2 ]];then +mail_updates+=".Mail |= . + { Port25: null } | " +for service in "${services[@]}";do +mail_updates+=".Mail |= . + { \"$service\": null } | " +done +else +mail_updates+=".Mail |= . + { Port25: false } | " +for service in "${services[@]}";do +mail_updates+=".Mail |= . + { \"$service\": false } | " +done +fi +mail_updates+=".Mail |= . * { DNSBlacklist: { Total: ${smail[t]:-null} } } | " +mail_updates+=".Mail |= . * { DNSBlacklist: { Clean: ${smail[c]:-null} } } | " +mail_updates+=".Mail |= . * { DNSBlacklist: { Marked: ${smail[m]:-null} } } | " +mail_updates+=".Mail |= . * { DNSBlacklist: { Blacklisted: ${smail[b]:-null} } } | " +ipjson=$(echo "$ipjson"|jq "$head_updates$basic_updates$type_updates$score_updates$factor_updates$media_updates$mail_updates.") +} +check_IP(){ +IP=$1 +ibar_step=0 +ipjson='{ + "Head": {}, + "Info": {}, + "Type": {}, + "Score": {}, + "Factor": {}, + "Media": {}, + "Mail": {} + }' +[[ $2 -eq 4 ]]&&hide_ipv4 $IP +[[ $2 -eq 6 ]]&&hide_ipv6 $IP +countRunTimes +db_maxmind $2 +db_ipinfo +[[ $mode_lite -eq 0 ]]&&db_scamalytics $2||scamalytics=() +db_ipregistry $2 +db_ipapi $2 +[[ $mode_lite -eq 0 ]]&&db_abuseipdb $2||abuseipdb=() +[[ $mode_lite -eq 0 ]]&&db_ip2location $2||ip2location=() +db_dbip +[[ $mode_lite -eq 0 ]]&&db_ipdata $2||ipdata=() +[[ $mode_lite -eq 0 ]]&&db_ipqs $2||ipqs=() +MediaUnlockTest_TikTok $2 +MediaUnlockTest_DisneyPlus $2 +MediaUnlockTest_Netflix $2 +MediaUnlockTest_YouTube_Premium $2 +MediaUnlockTest_PrimeVideo_Region $2 +MediaUnlockTest_Reddit $2 +OpenAITest $2 +check_mail +[[ $2 -eq 4 ]]&&check_dnsbl "$IP" 50 +echo -ne "$Font_LineClear" 1>&2 +if [ $2 -eq 4 ]||[[ $IPV4work -eq 0 || $IPV4check -eq 0 ]];then +for ((i=0; i&2 +echo -ne "$Font_LineClear" 1>&2 +done +fi +if [[ $mode_lite -eq 0 ]];then +local ip_report=$(show_head +show_basic +show_type +show_score +show_factor +show_media +show_mail $2 +show_tail) +else +local ip_report=$(show_head +show_basic_lite +show_type_lite +show_score +show_factor_lite +show_media +show_mail $2 +show_tail) +fi +local report_link="" +[[ mode_json -eq 1 || mode_output -eq 1 || mode_privacy -eq 0 ]]&&save_json +[[ $mode_lite -eq 0 && mode_privacy -eq 0 ]]&&report_link=$(curl -$2 -s -X POST https://upload.check.place -d "type=ip" --data-urlencode "json=$ipjson" --data-urlencode "content=$ip_report") +[[ mode_json -eq 0 ]]&&echo -ne "\r$ip_report\n" +[[ mode_json -eq 0 && mode_privacy -eq 0 && $report_link == *"https://Report.Check.Place/"* ]]&&echo -ne "\r${stail[link]}$report_link$Font_Suffix\n" +[[ mode_json -eq 1 ]]&&echo -ne "\r$ipjson\n" +echo -ne "\r\n" +if [[ mode_output -eq 1 ]];then +case "$outputfile" in +*.[aA][nN][sS][iI])echo "$ip_report" >>"$outputfile" 2>/dev/null +;; +*.[jJ][sS][oO][nN])echo "$ipjson" >>"$outputfile" 2>/dev/null +;; +*)echo -e "$ip_report"|sed 's/\x1b\[[0-9;]*[mGKHF]//g' >>"$outputfile" 2>/dev/null +esac +fi +} +generate_random_user_agent +adapt_locale +check_connectivity +read_ref +get_ipv4 +get_ipv6 +is_valid_ipv4 $IPV4 +is_valid_ipv6 $IPV6 +get_opts "$@" +[[ mode_no -eq 0 ]]&&install_dependencies 1>&2 +set_language +if [[ $ERRORcode -ne 0 ]];then +echo -ne "\r$Font_B$Font_Red${swarn[$ERRORcode]}$Font_Suffix\n" +exit $ERRORcode +fi +clear +show_ad +[[ $IPV4work -ne 0 && $IPV4check -ne 0 ]]&&check_IP "$IPV4" 4 +[[ $IPV6work -ne 0 && $IPV6check -ne 0 ]]&&check_IP "$IPV6" 6 diff --git a/web2.py b/web2.py index 78a957e..9af87bf 100644 --- a/web2.py +++ b/web2.py @@ -20,6 +20,8 @@ from web2_supervisor import ( use_builtin, ) from web2_network import run_network_dashboard +from web2_ip_profile import run_ip_profile +from web2_ip_quality import run_ip_quality from web2_proxy_config import read_proxy_config, write_proxy_config from web2_system_metrics import snapshot as system_metrics_snapshot from web2_youtube_oauth import ( @@ -77,6 +79,11 @@ from web2_pull_stream import ( save_pull_stream_config, ) from web2_pocketbase import REQUIRE_PB, pb_auth_refresh_and_validate +from web2_feature_flags import ( + SHOW_CAMERA_FEATURE, + SHOW_PULL_STREAM_FEATURE, + is_disabled_stream_worker_pm2, +) @asynccontextmanager @@ -122,13 +129,15 @@ def discover_script_entries() -> tuple: if path.is_file() and path.resolve() != web_path: entries.append({"script": path.name, "label": _label_short("youtube")}) - for path in sorted(base.glob("camera*.py")): - if path.is_file() and path.resolve() != web_path: - entries.append({"script": path.name, "label": _label_short("camera")}) + if SHOW_CAMERA_FEATURE: + for path in sorted(base.glob("camera*.py")): + if path.is_file() and path.resolve() != web_path: + entries.append({"script": path.name, "label": _label_short("camera")}) - for path in sorted(base.glob("pull_stream*.py")): - if path.is_file() and path.resolve() != web_path: - entries.append({"script": path.name, "label": _label_short("pull")}) + if SHOW_PULL_STREAM_FEATURE: + for path in sorted(base.glob("pull_stream*.py")): + if path.is_file() and path.resolve() != web_path: + entries.append({"script": path.name, "label": _label_short("pull")}) for path in sorted(base.glob("obs*.sh")): if path.is_file(): @@ -193,6 +202,8 @@ def relay_channel_id_from_pm2_name(name: str) -> Optional[str]: def process_valid(name: str) -> bool: + if is_disabled_stream_worker_pm2(name): + return False if name in VALID_PROCESSES: return True cid = relay_channel_id_from_pm2_name(name) @@ -880,18 +891,28 @@ def _stream_worker_ready_message(process: str) -> str | None: return _camera_ready_message(process) or _pull_stream_ready_message(process) +def _disabled_feature_response(feature: str) -> JSONResponse: + return JSONResponse({"error": f"{feature} 功能已禁用"}, status_code=403) + + @app.get("/camera/devices") async def camera_devices(): + if not SHOW_CAMERA_FEATURE: + return _disabled_feature_response("摄像头") return list_capture_devices() @app.get("/camera/config") async def camera_config_get(): + if not SHOW_CAMERA_FEATURE: + return _disabled_feature_response("摄像头") return {"config": load_camera_config(CONFIG_DIR)} @app.post("/camera/config") async def camera_config_post(request: Request): + if not SHOW_CAMERA_FEATURE: + return _disabled_feature_response("摄像头") try: data = await request.json() except Exception: @@ -907,16 +928,22 @@ async def camera_config_post(request: Request): @app.get("/pull_stream/config") async def pull_stream_config_get(): + if not SHOW_PULL_STREAM_FEATURE: + return _disabled_feature_response("拉流") return {"config": load_pull_stream_config(CONFIG_DIR)} @app.get("/pull_stream/protocols") async def pull_stream_protocols(): + if not SHOW_PULL_STREAM_FEATURE: + return _disabled_feature_response("拉流") return {"protocols": protocol_help()} @app.post("/pull_stream/config") async def pull_stream_config_post(request: Request): + if not SHOW_PULL_STREAM_FEATURE: + return _disabled_feature_response("拉流") try: data = await request.json() except Exception: @@ -1077,6 +1104,31 @@ async def network_status(): return JSONResponse({"error": str(e)}, status_code=500) +@app.get("/ip_profile") +async def ip_profile( + tz: str = Query("", description="客户端 IANA 时区,如 Asia/Shanghai"), + lang: str = Query("", description="客户端语言,如 zh-CN"), + webrtc_leak: Optional[str] = Query(None, description="1/true 表示检测到 WebRTC 本地泄露"), +): + """静态住宅 IP / 出口画像(参考 whoer、ping0.cc)。""" + leak_flag: Optional[bool] = None + if webrtc_leak is not None: + leak_flag = str(webrtc_leak).strip().lower() in ("1", "true", "yes") + try: + return await run_ip_profile(client_tz=tz.strip(), client_lang=lang.strip(), webrtc_leak=leak_flag) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + +@app.get("/ip_quality") +async def ip_quality(): + """IP 质量检测(集成 xykt/IPQuality 数据源与流媒体解锁)。""" + try: + return await run_ip_quality() + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + @app.get("/proxy_config") async def get_proxy_config(): """仅作用于录制子进程的代理(不写 Windows 系统代理)。""" diff --git a/web2_feature_flags.py b/web2_feature_flags.py new file mode 100644 index 0000000..45f40fe --- /dev/null +++ b/web2_feature_flags.py @@ -0,0 +1,22 @@ +"""功能开关:隐藏/屏蔽 UI 与 API,保留底层实现文件。""" + +SHOW_CAMERA_FEATURE = False +SHOW_PULL_STREAM_FEATURE = False + + +def is_disabled_stream_worker_pm2(name: str) -> bool: + low = (name or "").strip().lower() + if not SHOW_CAMERA_FEATURE and low.startswith("camera"): + return True + if not SHOW_PULL_STREAM_FEATURE and low.startswith("pull_stream"): + return True + return False + + +def is_disabled_stream_worker_script(script: str) -> bool: + stem = (script or "").strip().lower().removesuffix(".py") + if not SHOW_CAMERA_FEATURE and stem.startswith("camera"): + return True + if not SHOW_PULL_STREAM_FEATURE and stem.startswith("pull_stream"): + return True + return False diff --git a/web2_ip_profile.py b/web2_ip_profile.py new file mode 100644 index 0000000..8ba2db3 --- /dev/null +++ b/web2_ip_profile.py @@ -0,0 +1,432 @@ +""" +静态住宅 IP / 出口画像检测(参考 whoer、ping0.cc 维度)。 + +数据源: +- ip-api.com:hosting / proxy / mobile、ISP、ASN、地理 +- ping0.cc/geo:中文归属、ASN、运营商(本机默认出口) +- ipify / ifconfig:公网出口 IP 采样 +""" + +from __future__ import annotations + +import asyncio +import re +import time +from typing import Any, Optional + +import httpx + +IP_API_FIELDS = ( + "status,message,query,country,countryCode,regionName,city,zip,isp,org,as,asname," + "mobile,proxy,hosting,reverse" +) +IP_API_SELF = f"http://ip-api.com/json/?fields={IP_API_FIELDS}" +IP_API_IP = f"http://ip-api.com/json/{{ip}}?fields={IP_API_FIELDS}" + +PING0_GEO_URL = "https://ping0.cc/geo" +PING0_IPV4_GEO = "https://ipv4.ping0.cc/geo" + +IPIFY_URLS = ( + "https://api.ipify.org?format=json", + "https://api64.ipify.org?format=json", +) +IFCONFIG_URL = "https://ifconfig.me/ip" + +IDC_KEYWORDS = ( + "hosting", + "datacenter", + "data center", + "cloud", + "colo", + "vps", + "server", + "amazon", + "google cloud", + "azure", + "digitalocean", + "linode", + "vultr", + "ovh", + "hetzner", + "leaseweb", + "psychz", + "choopa", + "contabo", + "spartan", + "colocrossing", +) + +PROXY_KEYWORDS = ( + "vpn", + "proxy", + "tor", + "mullvad", + "nordvpn", + "expressvpn", + "surfshark", + "datacamp", + "bright data", + "luminati", +) + +RESIDENTIAL_KEYWORDS = ( + "telecom", + "broadband", + "cable", + "fiber", + "fibre", + "communications", + "chinanet", + "unicom", + "cmcc", + "telecom", + "comcast", + "verizon", + "att", + "residential", + "home", + "宽带", + "电信", + "联通", + "移动", +) + +CONNECT_TIMEOUT = 8.0 +READ_TIMEOUT = 12.0 + +UA = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" +) + + +def _norm(s: Optional[str]) -> str: + return (s or "").strip().lower() + + +def _classify_ip_type(data: dict[str, Any]) -> tuple[str, str]: + """返回 (type_id, label_zh)。""" + if data.get("proxy"): + return "proxy", "代理 / VPN" + if data.get("hosting"): + return "datacenter", "IDC 机房" + if data.get("mobile"): + return "mobile", "移动网络" + + blob = " ".join( + _norm(data.get(k)) for k in ("isp", "org", "as", "asname", "reverse") if data.get(k) + ) + if any(k in blob for k in IDC_KEYWORDS): + return "datacenter", "IDC 机房" + if any(k in blob for k in PROXY_KEYWORDS): + return "proxy", "代理 / VPN" + if any(k in blob for k in RESIDENTIAL_KEYWORDS): + return "residential", "家庭宽带" + if data.get("isp"): + return "residential", "疑似 ISP 宽带" + return "unknown", "未知类型" + + +def _quality_score(data: dict[str, Any], ip_type: str) -> int: + score = 100 + if data.get("hosting") or ip_type == "datacenter": + score -= 55 + if data.get("proxy") or ip_type == "proxy": + score -= 40 + if data.get("mobile") or ip_type == "mobile": + score -= 15 + blob = _norm(data.get("org")) + " " + _norm(data.get("isp")) + if any(k in blob for k in PROXY_KEYWORDS): + score -= 25 + if ip_type == "residential": + score += 5 + return max(0, min(100, score)) + + +def _quality_label(score: int) -> str: + if score >= 85: + return "优质(偏住宅)" + if score >= 70: + return "较干净" + if score >= 45: + return "一般" + return "高风险 / 机房" + + +def _guess_native(data: dict[str, Any], ip_type: str) -> tuple[Optional[bool], str]: + if ip_type in ("datacenter", "proxy"): + return False, "广播 / 非原生" + cc = (data.get("countryCode") or "").upper() + asname = _norm(data.get("asname")) + org = _norm(data.get("org")) + if not cc: + return None, "无法判断" + if cc == "CN" and any(x in org + asname for x in ("chinanet", "unicom", "cmcc", "telecom", "电信", "联通", "移动")): + return True, "原生 IP(ISP)" + if ip_type == "residential": + return True, "疑似原生" + if data.get("hosting"): + return False, "广播 IP" + return None, "待确认" + + +def _parse_ping0_geo(text: str) -> dict[str, Any]: + lines = [ln.strip() for ln in (text or "").splitlines() if ln.strip()] + if len(lines) < 1: + return {"error": "ping0 返回为空"} + out: dict[str, Any] = {"ip": lines[0]} + if len(lines) > 1: + out["location"] = lines[1] + if len(lines) > 2: + out["asn"] = lines[2] + if len(lines) > 3: + out["org"] = lines[3] + return out + + +async def _fetch_ping0_geo(client: httpx.AsyncClient, ipv4: bool = True) -> dict[str, Any]: + url = PING0_IPV4_GEO if ipv4 else PING0_GEO_URL + try: + r = await client.get(url, timeout=8.0) + r.raise_for_status() + return _parse_ping0_geo(r.text) + except Exception as e: + return {"error": str(e)} + + +async def _ip_api_lookup(client: httpx.AsyncClient, ip: Optional[str] = None) -> dict[str, Any]: + url = IP_API_IP.format(ip=ip) if ip else IP_API_SELF + try: + r = await client.get(url, timeout=8.0) + r.raise_for_status() + data = r.json() + except Exception as e: + return {"error": str(e), "query": ip} + if data.get("status") != "success": + return { + "error": data.get("message") or "lookup failed", + "query": ip or data.get("query"), + } + ip_type, ip_type_label = _classify_ip_type(data) + score = _quality_score(data, ip_type) + native, native_label = _guess_native(data, ip_type) + return { + "ip": data.get("query"), + "country": data.get("country"), + "country_code": data.get("countryCode"), + "region": data.get("regionName"), + "city": data.get("city"), + "zip": data.get("zip"), + "isp": data.get("isp"), + "org": data.get("org"), + "asn": data.get("as"), + "asname": data.get("asname"), + "reverse": data.get("reverse"), + "mobile": bool(data.get("mobile")), + "proxy": bool(data.get("proxy")), + "hosting": bool(data.get("hosting")), + "ip_type": ip_type, + "ip_type_label": ip_type_label, + "native": native, + "native_label": native_label, + "quality_score": score, + "quality_label": _quality_label(score), + "error": None, + } + + +async def _fetch_public_ip(client: httpx.AsyncClient) -> tuple[Optional[str], Optional[str]]: + for url in IPIFY_URLS: + try: + r = await client.get(url, timeout=8.0) + r.raise_for_status() + data = r.json() + ip = (data.get("ip") or "").strip() + if ip: + return ip, None + except Exception: + continue + try: + r = await client.get(IFCONFIG_URL, timeout=8.0) + r.raise_for_status() + ip = r.text.strip() + if re.match(r"^\d{1,3}(\.\d{1,3}){3}$", ip) or ":" in ip: + return ip, None + except Exception as e: + return None, str(e) + return None, "无法获取公网 IP" + + +def _build_exit_profile( + label: str, + ip_api: dict[str, Any], + ping0: Optional[dict[str, Any]] = None, +) -> dict[str, Any]: + out = {"label": label, **ip_api} + if ping0 and not ping0.get("error"): + out["ping0"] = ping0 + return out + + +def _tz_country_hint(tz: str) -> Optional[str]: + """粗略:常见时区 → 国家码(仅用于 whoer 式一致性参考)。""" + tz = (tz or "").strip() + mapping = { + "Asia/Shanghai": "CN", + "Asia/Chongqing": "CN", + "Asia/Hong_Kong": "HK", + "Asia/Taipei": "TW", + "America/New_York": "US", + "America/Los_Angeles": "US", + "Europe/London": "GB", + "Europe/Berlin": "DE", + "Asia/Tokyo": "JP", + } + return mapping.get(tz) + + +def _build_checks( + default_exit: dict[str, Any], + overseas_exit: dict[str, Any], + *, + client_tz: str = "", + client_lang: str = "", + webrtc_leak: Optional[bool] = None, +) -> list[dict[str, Any]]: + checks: list[dict[str, Any]] = [] + + def add(cid: str, label: str, ok: bool, hint: str = "") -> None: + checks.append({"id": cid, "label": label, "ok": ok, "hint": hint}) + + d = default_exit + o = overseas_exit + + add( + "not_datacenter", + "默认出口非机房 IP", + d.get("ip_type") not in ("datacenter", "proxy") and not d.get("hosting"), + d.get("ip_type_label") or "", + ) + add( + "residential_like", + "默认出口偏住宅 / ISP", + d.get("ip_type") == "residential", + "静态住宅推流更稳" if d.get("ip_type") != "residential" else "", + ) + add("not_proxy", "默认出口非代理标记", not d.get("proxy"), "ip-api 检测到 proxy=true" if d.get("proxy") else "") + add( + "native_ip", + "原生 IP(参考)", + d.get("native") is True, + d.get("native_label") or "", + ) + add( + "quality_ok", + "IP 洁净度 ≥ 70", + (d.get("quality_score") or 0) >= 70, + f"评分 {d.get('quality_score', '—')} · {d.get('quality_label', '')}", + ) + + d_ip = d.get("ip") + o_ip = o.get("ip") + if d_ip and o_ip: + split = d_ip != o_ip + add( + "split_exit", + "默认出口与海外采样出口不同", + split, + "分流可能正常" if split else "两路出口相同,可能未分流", + ) + + if client_tz and d.get("country_code"): + hint_cc = _tz_country_hint(client_tz) + if hint_cc: + add( + "tz_match", + "系统时区与 IP 国家一致(参考)", + hint_cc == (d.get("country_code") or "").upper(), + f"时区 {client_tz}", + ) + + if client_lang and d.get("country_code"): + lang = client_lang.lower() + cc = (d.get("country_code") or "").lower() + lang_ok = (cc == "cn" and lang.startswith("zh")) or (cc == "us" and lang.startswith("en")) or True + add("lang_soft", "浏览器语言与 IP 地区不冲突", lang_ok, client_lang) + + if webrtc_leak is not None: + add( + "webrtc", + "WebRTC 未泄露本地网卡 IP", + not webrtc_leak, + "检测到额外本地候选地址" if webrtc_leak else "", + ) + + return checks + + +async def run_ip_profile( + *, + client_tz: str = "", + client_lang: str = "", + webrtc_leak: Optional[bool] = None, +) -> dict[str, Any]: + timeout = httpx.Timeout(READ_TIMEOUT, connect=CONNECT_TIMEOUT) + limits = httpx.Limits(max_keepalive_connections=6, max_connections=10) + + async with httpx.AsyncClient( + timeout=timeout, + limits=limits, + verify=True, + headers={"User-Agent": UA}, + ) as client: + ping0_task = asyncio.create_task(_fetch_ping0_geo(client)) + default_api_task = asyncio.create_task(_ip_api_lookup(client, None)) + overseas_ip_task = asyncio.create_task(_fetch_public_ip(client)) + + ping0, default_api, (overseas_ip, overseas_ip_err) = await asyncio.gather( + ping0_task, default_api_task, overseas_ip_task + ) + + if default_api.get("error") and ping0.get("ip") and not ping0.get("error"): + default_api = await _ip_api_lookup(client, ping0["ip"]) + + overseas_api: dict[str, Any] = {"error": overseas_ip_err or "unknown"} + if overseas_ip: + overseas_api = await _ip_api_lookup(client, overseas_ip) + + default_exit = _build_exit_profile("默认出口", default_api, ping0) + overseas_exit = _build_exit_profile("海外采样出口", overseas_api) + + checks = _build_checks( + default_exit, + overseas_exit, + client_tz=client_tz, + client_lang=client_lang, + webrtc_leak=webrtc_leak, + ) + + residential_ok = default_exit.get("ip_type") == "residential" and (default_exit.get("quality_score") or 0) >= 70 + + return { + "default_exit": default_exit, + "overseas_exit": overseas_exit, + "checks": checks, + "summary": { + "residential_like": residential_ok, + "headline": ( + "当前出口偏静态住宅" + if residential_ok + else ( + "出口疑似机房/代理,不适合伪装住宅" + if default_exit.get("ip_type") in ("datacenter", "proxy") + else "出口类型一般,建议结合代理规则确认" + ) + ), + }, + "meta": { + "at": time.time(), + "sources": ["ip-api.com", "ping0.cc/geo", "ipify"], + "disclaimer": "结果仅供参考;ping0 完整 IDC/原生判定需其付费 API。", + }, + } diff --git a/web2_ip_quality.py b/web2_ip_quality.py new file mode 100644 index 0000000..1303c59 --- /dev/null +++ b/web2_ip_quality.py @@ -0,0 +1,414 @@ +""" +IP 质量检测(集成 xykt/IPQuality 思路与数据源)。 + +脚本来源:third_party/IPQuality/ip.sh (v2026-03-29) +可用数据源:ipinfo.check.place、ipinfo.io、api.ipapi.is、流媒体解锁探测 +""" + +from __future__ import annotations + +import asyncio +import re +import time +from typing import Any, Optional + +import httpx + +SCRIPT_VERSION = "v2026-03-29" +UA = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +) +CONNECT_TIMEOUT = 8.0 +READ_TIMEOUT = 14.0 + +TYPE_LABELS = { + "business": "商业", + "isp": "运营商", + "hosting": "机房", + "education": "教育", + "government": "政府", + "banking": "金融", + "other": "其他", +} + +RISK_LABELS = { + "verylow": "极低", + "low": "低", + "elevated": "偏高", + "high": "高", + "veryhigh": "极高", + "suspicious": "可疑", + "risky": "风险", + "highrisk": "高危", +} + + +def _type_label(raw: Optional[str]) -> str: + if not raw: + return "未知" + return TYPE_LABELS.get(str(raw).lower(), str(raw)) + + +def _parse_abuser_score(text: Optional[str]) -> tuple[Optional[float], Optional[str]]: + if not text: + return None, None + m = re.match(r"([\d.]+)\s*\(([^)]+)\)", str(text).strip()) + if not m: + return None, None + try: + num = float(m.group(1)) + except ValueError: + num = None + risk = m.group(2).strip().lower().replace(" ", "") + risk_key = { + "verylow": "verylow", + "low": "low", + "elevated": "elevated", + "high": "high", + "veryhigh": "veryhigh", + }.get(risk, risk) + return num, risk_key + + +def _ipqs_risk(score: Optional[int]) -> Optional[str]: + if score is None: + return None + if score < 75: + return "low" + if score < 85: + return "suspicious" + if score < 90: + return "risky" + return "highrisk" + + +async def _fetch_json( + client: httpx.AsyncClient, url: str, *, label: str = "" +) -> tuple[Optional[dict[str, Any]], Optional[str]]: + try: + r = await client.get(url) + if r.status_code != 200: + return None, f"{label or url}: HTTP {r.status_code}" + data = r.json() + if not isinstance(data, dict): + return None, f"{label or url}: 非 JSON 对象" + return data, None + except Exception as e: + return None, f"{label or url}: {e}" + + +async def _fetch_text(client: httpx.AsyncClient, url: str, **kwargs: Any) -> tuple[str, Optional[str]]: + try: + r = await client.get(url, **kwargs) + if r.status_code != 200: + return "", f"HTTP {r.status_code}" + return r.text, None + except Exception as e: + return "", str(e) + + +async def _resolve_public_ip(client: httpx.AsyncClient) -> tuple[Optional[str], Optional[str]]: + for url in ("https://api.ipify.org?format=json", "https://api64.ipify.org?format=json"): + data, err = await _fetch_json(client, url, label="ipify") + if data and data.get("ip"): + return str(data["ip"]), None + if err: + last = err + return None, last if "last" in dir() else "无法获取公网 IP" + + +async def _fetch_maxmind(client: httpx.AsyncClient, ip: str) -> dict[str, Any]: + data, err = await _fetch_json(client, f"https://ipinfo.check.place/{ip}?lang=cn", label="maxmind") + if not data: + return {"error": err, "lite": True} + return { + "asn": data.get("ASN", {}).get("AutonomousSystemNumber"), + "org": data.get("ASN", {}).get("AutonomousSystemOrganization"), + "city": data.get("City", {}).get("Name"), + "postal": data.get("City", {}).get("PostalCode"), + "lat": data.get("City", {}).get("Latitude"), + "lon": data.get("City", {}).get("Longitude"), + "timezone": (data.get("City", {}).get("Location") or {}).get("TimeZone"), + "country_code": data.get("Country", {}).get("IsoCode"), + "country": data.get("Country", {}).get("Name"), + "reg_country_code": (data.get("Country", {}).get("RegisteredCountry") or {}).get("IsoCode"), + "reg_country": (data.get("Country", {}).get("RegisteredCountry") or {}).get("Name"), + "continent": (data.get("City", {}).get("Continent") or {}).get("Name"), + "lite": False, + } + + +async def _fetch_ipinfo_widget(client: httpx.AsyncClient, ip: str) -> dict[str, Any]: + data, err = await _fetch_json(client, f"https://ipinfo.io/widget/demo/{ip}", label="ipinfo") + if not data: + return {"error": err} + inner = data.get("data") if isinstance(data.get("data"), dict) else {} + privacy = inner.get("privacy") or {} + asn = inner.get("asn") or {} + loc = str(inner.get("loc") or "") + lat, lon = (loc.split(",", 1) + [None, None])[:2] + return { + "usage_type": asn.get("type"), + "company_type": (inner.get("company") or {}).get("type"), + "country_code": inner.get("country"), + "city": inner.get("city"), + "timezone": inner.get("timezone"), + "asn": str(asn.get("asn", "")).replace("AS", ""), + "org": asn.get("name"), + "lat": lat, + "lon": lon, + "proxy": privacy.get("proxy"), + "tor": privacy.get("tor"), + "vpn": privacy.get("vpn"), + "hosting": privacy.get("hosting"), + } + + +async def _fetch_ipapi(client: httpx.AsyncClient, ip: str) -> dict[str, Any]: + data, err = await _fetch_json(client, f"https://api.ipapi.is/?q={ip}", label="ipapi") + if not data: + return {"error": err} + score_num, risk_key = _parse_abuser_score((data.get("company") or {}).get("abuser_score")) + loc = data.get("location") or {} + return { + "usage_type": (data.get("asn") or {}).get("type"), + "company_type": (data.get("company") or {}).get("type"), + "country_code": loc.get("country_code"), + "city": loc.get("city"), + "country": loc.get("country"), + "timezone": loc.get("timezone"), + "asn": (data.get("asn") or {}).get("asn"), + "org": (data.get("asn") or {}).get("org"), + "lat": loc.get("latitude"), + "lon": loc.get("longitude"), + "proxy": data.get("is_proxy"), + "tor": data.get("is_tor"), + "vpn": data.get("is_vpn"), + "hosting": data.get("is_datacenter"), + "abuser": data.get("is_abuser"), + "crawler": data.get("is_crawler"), + "score_num": score_num, + "score_pct": round(score_num * 100, 2) if score_num is not None else None, + "risk_key": risk_key, + } + + +async def _fetch_check_place_db(client: httpx.AsyncClient, ip: str, db: str) -> dict[str, Any]: + data, err = await _fetch_json(client, f"https://ipinfo.check.place/{ip}?db={db}", label=db) + if not data: + return {"error": err} + return data + + +async def _test_youtube(client: httpx.AsyncClient) -> dict[str, Any]: + cookies = ( + "YSC=BiCUU3-5Gdk; CONSENT=YES+cb.20220301-11-p0.en+FX+700; " + "GPS=1; VISITOR_INFO1_LIVE=4VwPMkB7W5A; PREF=tz=Asia.Shanghai" + ) + text, err = await _fetch_text( + client, + "https://www.youtube.com/premium", + headers={"Accept-Language": "en", "Cookie": cookies}, + ) + if err: + return {"status": "error", "status_label": "检测失败", "region": None, "error": err} + if "www.google.cn" in text: + return {"status": "cn", "status_label": "中国版", "region": "CN"} + if "Premium is not available in your country" in text: + return {"status": "no_premium", "status_label": "无 Premium", "region": None} + region_m = re.search(r'"contentRegion":"([^"]+)"', text) + region = region_m.group(1) if region_m else None + if "ad-free" in text: + return {"status": "yes", "status_label": "Premium 可用", "region": region} + return {"status": "bad", "status_label": "状态未知", "region": region} + + +async def _test_tiktok(client: httpx.AsyncClient) -> dict[str, Any]: + text, err = await _fetch_text(client, "https://www.tiktok.com/", headers={"Accept-Language": "en"}) + if err: + return {"status": "error", "status_label": "检测失败", "region": None, "error": err} + if "Please wait" in text: + return {"status": "blocked", "status_label": "需验证/封锁", "region": None} + region_m = re.search(r'"region":"([A-Z]{2})"', text) or re.search(r'"countryCode":"([A-Z]{2})"', text) + region = region_m.group(1) if region_m else None + if region: + return {"status": "yes", "status_label": "可访问", "region": region} + return {"status": "maybe", "status_label": "页面可达", "region": region} + + +def _bool_factor(value: Any) -> Optional[bool]: + if value is True or value == "true": + return True + if value is False or value == "false": + return False + return None + + +def _build_info(ip: str, maxmind: dict[str, Any], ipinfo: dict[str, Any], ipapi: dict[str, Any]) -> dict[str, Any]: + if not maxmind.get("lite") and not maxmind.get("error"): + src = maxmind + ip_type = "原生 IP" if src.get("country_code") == src.get("reg_country_code") else "广播 IP" + else: + src = ipinfo if not ipinfo.get("error") else ipapi + cc = src.get("country_code") + ip_type = "原生 IP" if cc else "未知" + return { + "ip": ip, + "asn": maxmind.get("asn") or ipinfo.get("asn") or ipapi.get("asn"), + "organization": maxmind.get("org") or ipinfo.get("org") or ipapi.get("org"), + "city": maxmind.get("city") or ipinfo.get("city") or ipapi.get("city"), + "country": maxmind.get("country") or ipapi.get("country"), + "country_code": maxmind.get("country_code") or ipinfo.get("country_code") or ipapi.get("country_code"), + "timezone": maxmind.get("timezone") or ipinfo.get("timezone") or ipapi.get("timezone"), + "latitude": maxmind.get("lat") or ipinfo.get("lat") or ipapi.get("lat"), + "longitude": maxmind.get("lon") or ipinfo.get("lon") or ipapi.get("lon"), + "ip_type": ip_type, + "mode_lite": bool(maxmind.get("lite")), + } + + +def _build_type_usage(ipinfo: dict[str, Any], ipapi: dict[str, Any]) -> dict[str, str]: + out: dict[str, str] = {} + if not ipinfo.get("error"): + out["IPinfo"] = _type_label(ipinfo.get("usage_type")) + out["IPinfo_company"] = _type_label(ipinfo.get("company_type")) + if not ipapi.get("error"): + out["ipapi"] = _type_label(ipapi.get("usage_type")) + out["ipapi_company"] = _type_label(ipapi.get("company_type")) + return out + + +def _build_scores(ipapi: dict[str, Any], ipqs: dict[str, Any], scam: dict[str, Any]) -> list[dict[str, Any]]: + scores: list[dict[str, Any]] = [] + if ipapi.get("score_pct") is not None: + scores.append( + { + "source": "ipapi", + "value": ipapi["score_pct"], + "unit": "%", + "risk": ipapi.get("risk_key"), + "risk_label": RISK_LABELS.get(str(ipapi.get("risk_key") or ""), ipapi.get("risk_key")), + } + ) + if ipqs.get("fraud_score") is not None: + fs = int(ipqs["fraud_score"]) + rk = _ipqs_risk(fs) + scores.append( + { + "source": "IPQS", + "value": fs, + "unit": "分", + "risk": rk, + "risk_label": RISK_LABELS.get(rk or "", rk), + } + ) + scam_score = scam.get("score") or scam.get("scamalytics_score") + if scam_score is not None: + try: + sv = float(scam_score) + scores.append({"source": "SCAMALYTICS", "value": sv, "unit": "分", "risk": None, "risk_label": ""}) + except (TypeError, ValueError): + pass + return scores + + +def _build_factors( + ipinfo: dict[str, Any], ipapi: dict[str, Any], ipqs: dict[str, Any] +) -> dict[str, dict[str, Optional[bool]]]: + factors: dict[str, dict[str, Optional[bool]]] = { + "Proxy": {}, + "VPN": {}, + "Tor": {}, + "Hosting": {}, + } + if not ipinfo.get("error"): + factors["Proxy"]["IPinfo"] = _bool_factor(ipinfo.get("proxy")) + factors["VPN"]["IPinfo"] = _bool_factor(ipinfo.get("vpn")) + factors["Tor"]["IPinfo"] = _bool_factor(ipinfo.get("tor")) + factors["Hosting"]["IPinfo"] = _bool_factor(ipinfo.get("hosting")) + if not ipapi.get("error"): + factors["Proxy"]["ipapi"] = _bool_factor(ipapi.get("proxy")) + factors["VPN"]["ipapi"] = _bool_factor(ipapi.get("vpn")) + factors["Tor"]["ipapi"] = _bool_factor(ipapi.get("tor")) + factors["Hosting"]["ipapi"] = _bool_factor(ipapi.get("hosting")) + if not ipqs.get("error"): + factors["Proxy"]["IPQS"] = _bool_factor(ipqs.get("proxy")) + factors["VPN"]["IPQS"] = _bool_factor(ipqs.get("vpn")) + factors["Tor"]["IPQS"] = _bool_factor(ipqs.get("tor")) + return factors + + +async def run_ip_quality() -> dict[str, Any]: + timeout = httpx.Timeout(READ_TIMEOUT, connect=CONNECT_TIMEOUT) + limits = httpx.Limits(max_keepalive_connections=8, max_connections=12) + errors: list[str] = [] + + async with httpx.AsyncClient( + timeout=timeout, + limits=limits, + verify=True, + headers={"User-Agent": UA}, + follow_redirects=True, + ) as client: + ip, ip_err = await _resolve_public_ip(client) + if not ip: + return {"error": ip_err or "无法获取公网 IP", "meta": {"at": time.time()}} + + maxmind_t = asyncio.create_task(_fetch_maxmind(client, ip)) + ipinfo_t = asyncio.create_task(_fetch_ipinfo_widget(client, ip)) + ipapi_t = asyncio.create_task(_fetch_ipapi(client, ip)) + ipqs_t = asyncio.create_task(_fetch_check_place_db(client, ip, "ipqualityscore")) + scam_t = asyncio.create_task(_fetch_check_place_db(client, ip, "scamalytics")) + yt_t = asyncio.create_task(_test_youtube(client)) + tt_t = asyncio.create_task(_test_tiktok(client)) + + maxmind, ipinfo, ipapi, ipqs_raw, scam_raw, youtube, tiktok = await asyncio.gather( + maxmind_t, ipinfo_t, ipapi_t, ipqs_t, scam_t, yt_t, tt_t + ) + + ipqs = ipqs_raw if not ipqs_raw.get("error") else {} + scam = scam_raw if not scam_raw.get("error") else {} + for label, blob in (("maxmind", maxmind), ("ipinfo", ipinfo), ("ipapi", ipapi), ("ipqs", ipqs_raw), ("scam", scam_raw)): + if blob.get("error"): + errors.append(f"{label}: {blob['error']}") + + info = _build_info(ip, maxmind, ipinfo, ipapi) + usage = _build_type_usage(ipinfo, ipapi) + scores = _build_scores(ipapi, ipqs, scam) + factors = _build_factors(ipinfo, ipapi, ipqs) + + media = { + "Youtube": youtube, + "TikTok": tiktok, + } + + proxy_flags = [v for row in factors.values() for v in row.values() if v is True] + headline = "出口较干净,类型偏住宅/运营商" if not proxy_flags else "检测到代理/VPN/机房特征,录制前请确认" + + return { + "head": { + "ip": ip, + "version": SCRIPT_VERSION, + "project": "IPQuality", + "github": "https://github.com/xykt/IPQuality", + }, + "info": info, + "type": {"usage": usage}, + "scores": scores, + "factors": factors, + "media": media, + "summary": {"headline": headline, "proxy_like": bool(proxy_flags)}, + "meta": { + "at": time.time(), + "mode": "lite" if info.get("mode_lite") else "full", + "sources": [ + "ipinfo.check.place", + "ipinfo.io", + "api.ipapi.is", + "xykt/IPQuality", + ], + "errors": errors, + "disclaimer": "部分数据库(IPQS/Scamalytics)可能因网络环境不可用;结果仅供参考。", + }, + } diff --git a/web2_system_metrics.py b/web2_system_metrics.py index ad815c2..9a93fe5 100644 --- a/web2_system_metrics.py +++ b/web2_system_metrics.py @@ -1,45 +1,28 @@ """ -系统与网卡指标(供仪表盘轮询)。下行/上行为全机网卡合计速率(估算)。 +本机硬件与系统信息(供「系统」页轮询)。 """ from __future__ import annotations +import platform import time -from typing import Any, Optional +from typing import Any import psutil -_last_net_time: float = 0.0 -_last_net: Optional[dict[str, tuple[int, int]]] = None # name -> (sent, recv) +from web2_host_caps import gpu_status_payload, machine_summary_payload -def _net_rates_mbps() -> tuple[Optional[float], Optional[float], dict[str, Any]]: - """返回 (上行 Mbps, 下行 Mbps, 原始片段)。""" - global _last_net_time, _last_net - now = time.monotonic() - per = psutil.net_io_counters(pernic=True) - cur: dict[str, tuple[int, int]] = {k: (v.bytes_sent, v.bytes_recv) for k, v in per.items()} - detail: dict[str, Any] = {} - up_mbps = down_mbps = None - if _last_net is not None and _last_net_time > 0: - dt = now - _last_net_time - if dt > 0.05: - ds = dr = 0 - for name, (sent, recv) in cur.items(): - if name in _last_net: - ps0, pr0 = _last_net[name] - ds += max(0, sent - ps0) - dr += max(0, recv - pr0) - up_mbps = round((ds * 8) / dt / 1_000_000, 3) - down_mbps = round((dr * 8) / dt / 1_000_000, 3) - detail["delta_seconds"] = round(dt, 3) - _last_net = cur - _last_net_time = now - for name, (sent, recv) in cur.items(): - detail.setdefault("interfaces", {})[name] = { - "bytes_sent": sent, - "bytes_recv": recv, +def _cpu_freq() -> dict[str, float | None] | None: + try: + f = psutil.cpu_freq() + if not f: + return None + return { + "current_mhz": round(f.current, 1) if f.current else None, + "max_mhz": round(f.max, 1) if f.max else None, } - return up_mbps, down_mbps, detail + except Exception: + return None def snapshot() -> dict[str, Any]: @@ -48,6 +31,7 @@ def snapshot() -> dict[str, Any]: cpu = psutil.cpu_percent(interval=0.15) except Exception: cpu = None + try: vm = psutil.virtual_memory() mem = { @@ -58,13 +42,14 @@ def snapshot() -> dict[str, Any]: } except Exception: mem = {} + try: sm = psutil.swap_memory() swap = {"percent": sm.percent, "used": sm.used, "total": sm.total} except Exception: swap = {} - disks = [] + disks: list[dict[str, Any]] = [] try: for part in psutil.disk_partitions(all=False): try: @@ -84,46 +69,32 @@ def snapshot() -> dict[str, Any]: except Exception: pass - up_mbps, down_mbps, net_detail = _net_rates_mbps() - - boot = None + boot: int | None = None try: boot = int(psutil.boot_time()) except Exception: pass - ffmpeg_procs = [] - try: - for p in psutil.process_iter(["pid", "name", "memory_info"]): - try: - name = (p.info.get("name") or "").lower() - if "ffmpeg" not in name: - continue - mi = p.info.get("memory_info") - ffmpeg_procs.append( - { - "pid": p.info["pid"], - "name": p.info.get("name"), - "rss": mi.rss if mi else 0, - } - ) - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - except Exception: - pass + machine = machine_summary_payload() + processor = (platform.processor() or "").strip() or None return { "ts": time.time(), + "system": { + **machine, + "os_version": platform.version(), + "processor": processor, + "boot_time_unix": boot, + }, + "hardware": { + "cpu_logical": psutil.cpu_count(logical=True), + "cpu_physical": psutil.cpu_count(logical=False), + "cpu_freq": _cpu_freq(), + "memory_total": mem.get("total"), + "gpu": gpu_status_payload(), + }, "cpu_percent": cpu, - "cpu_count_logical": psutil.cpu_count(logical=True), "memory": mem, "swap": swap, "disks": disks, - "network": { - "up_mbps": up_mbps, - "down_mbps": down_mbps, - "detail": net_detail, - }, - "boot_time_unix": boot, - "ffmpeg_processes": ffmpeg_procs[:24], }