Files
gitlab-instance-0a899031_d2…/web2_ip_quality.py
DESKTOP-77LRNCU\dsx2016 ae9531f2c2 网络页焕新:IP 画像/质量检测、链路诊断与功能开关
新增 /ip_profile、/ip_quality API 与网络 UI 组件,默认隐藏摄像头/拉流导航;同步更新 README。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 12:47:53 +08:00

415 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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可能因网络环境不可用结果仅供参考。",
},
}