白名单 ok

This commit is contained in:
yml2213
2026-06-22 11:29:22 +08:00
parent 66b03a7d89
commit 4c924375aa
5 changed files with 544 additions and 2 deletions
+2 -1
View File
@@ -4,5 +4,6 @@ from .login import DouyuLogin
from .email_verifier import EmailVerifier
from .config import Config
from .proxy import ProxyManager
from .whitelist import WhitelistManager
__all__ = ["DouyuLogin", "EmailVerifier", "Config", "ProxyManager"]
__all__ = ["DouyuLogin", "EmailVerifier", "Config", "ProxyManager", "WhitelistManager"]
+4
View File
@@ -26,6 +26,10 @@ class ProxyConfig:
api_url: str = ""
http: str = ""
https: str = ""
# 白名单配置
whitelist_enabled: bool = False # 是否启用白名单自动管理
whitelist_uid: str = "" # 协固用户ID
whitelist_ukey: str = "" # 协固用户密钥
class Config:
+268
View File
@@ -0,0 +1,268 @@
"""代理IP白名单管理模块"""
import json
import re
from typing import Optional
from urllib.parse import urlencode
import requests
from loguru import logger
class WhitelistManager:
"""协固代理IP白名单管理器"""
MEMO_PREFIX = "douyu_auto"
BASE_URL = "http://op.xiequ.cn/IpWhiteList.aspx"
def __init__(self, uid: str, ukey: str):
self.uid = uid
self.ukey = ukey
self._memo = self.MEMO_PREFIX
@property
def memo(self) -> str:
"""当前机器的固定备注"""
return self._memo
def _build_url(self, **params) -> str:
"""构建请求URL"""
base_params = {
"uid": self.uid,
"ukey": self.ukey,
}
base_params.update(params)
query = urlencode(base_params)
return f"{self.BASE_URL}?{query}"
def get_whitelist_json(self) -> list[dict]:
"""
获取白名单列表(JSON格式)
Returns:
[{"IP": "x.x.x.x", "MEMO": "备注"}, ...]
"""
try:
url = self._build_url(act="getjson")
response = requests.get(url, timeout=10)
response.raise_for_status()
text = response.text.strip()
if not text or text == "[]":
return []
data = json.loads(text)
if isinstance(data, list):
return data
# 处理 {"data": [...]} 格式
if isinstance(data, dict):
items = data.get("data", [])
if isinstance(items, list):
return items
return []
except Exception as e:
logger.error(f"获取白名单失败: {e}")
return []
def add_ip(self, ip: str) -> bool:
"""
添加IP到白名单
Args:
ip: 要添加的IP地址
"""
try:
url = self._build_url(act="add", ip=ip, meno=self._memo)
response = requests.get(url, timeout=10)
response.raise_for_status()
text = response.text.strip()
logger.debug(f"添加白名单响应: {text}")
# 成功通常返回 "ok" 或类似信息
if "ok" in text.lower() or "success" in text.lower() or "添加成功" in text:
logger.info(f"白名单添加成功: {ip} (备注: {self._memo})")
return True
# 检查是否已存在
if "已存在" in text or "exist" in text.lower():
logger.info(f"白名单已存在: {ip}")
return True
logger.warning(f"白名单添加结果: {text}")
return False
except Exception as e:
logger.error(f"添加白名单失败: {e}")
return False
def delete_ip(self, ip: str) -> bool:
"""
删除指定IP
Args:
ip: 要删除的IP地址
"""
try:
url = self._build_url(act="del", ip=ip)
response = requests.get(url, timeout=10)
response.raise_for_status()
text = response.text.strip()
logger.debug(f"删除白名单响应: {text}")
if "ok" in text.lower() or "success" in text.lower() or "删除成功" in text:
logger.info(f"白名单删除成功: {ip}")
return True
logger.warning(f"白名单删除结果: {text}")
return False
except Exception as e:
logger.error(f"删除白名单失败: {e}")
return False
def get_memo_ip(self) -> Optional[str]:
"""
获取当前备注对应的IP
Returns:
IP地址,如果不存在返回None
"""
records = self.get_whitelist_json()
for record in records:
if record.get("MEMO") == self._memo:
return record.get("IP")
return None
def get_memo_records(self) -> list[dict]:
"""
获取当前备注的所有记录
Returns:
匹配备注的记录列表
"""
records = self.get_whitelist_json()
return [r for r in records if r.get("MEMO") == self._memo]
def sync_ip(self, current_ip: str) -> tuple[bool, str]:
"""
同步白名单IP
检查当前备注是否有记录:
- 如果IP相同,无需操作
- 如果IP不同,删除旧的并添加新的
- 如果IP已存在但备注不同(如手动添加无备注),删除后重新添加
- 如果无记录,添加新的
Args:
current_ip: 当前出口IP
Returns:
(是否成功, 消息)
"""
try:
existing_ip = self.get_memo_ip()
# IP相同,无需更新
if existing_ip == current_ip:
msg = f"白名单IP已是最新的: {current_ip}"
logger.info(msg)
return True, msg
# 有旧记录,先删除
if existing_ip:
logger.info(f"白名单IP变化: {existing_ip} -> {current_ip}")
self.delete_ip(existing_ip)
# IP已存在但备注不同(或备注为空),先删除后重新添加
records = self.get_whitelist_json()
if any(r.get('IP') == current_ip for r in records):
logger.info(f"白名单IP {current_ip} 已存在但备注不同,删除后重新添加")
self.delete_ip(current_ip)
# 添加新IP
if self.add_ip(current_ip):
if existing_ip:
msg = f"白名单IP已更新: {existing_ip} -> {current_ip}"
else:
msg = f"白名单IP已添加: {current_ip}"
logger.info(msg)
return True, msg
return False, "白名单添加失败"
except Exception as e:
msg = f"白名单同步失败: {e}"
logger.error(msg)
return False, msg
def test_connection(self) -> tuple[bool, str]:
"""
测试白名单API连接
Returns:
(是否成功, 消息)
"""
try:
records = self.get_whitelist_json()
count = len(records)
my_records = [r for r in records if r.get("MEMO", "").startswith(self.MEMO_PREFIX)]
msg = f"连接成功,白名单共 {count} 条记录,其中本机相关 {len(my_records)}"
logger.info(msg)
return True, msg
except Exception as e:
msg = f"连接失败: {e}"
logger.error(msg)
return False, msg
def get_exit_ip_via_proxy(proxy: str) -> Optional[str]:
"""
通过代理获取出口IP
Args:
proxy: 代理URL,格式 http://ip:port
Returns:
出口IP地址
"""
targets = [
"https://qifu-api.baidubce.com/ip/local/geo/v1/district",
"https://myip.ipip.net",
"https://4.ipw.cn",
]
proxies = {"http": proxy, "https": proxy}
for url in targets:
try:
response = requests.get(
url,
proxies=proxies,
timeout=6,
headers={"User-Agent": "Mozilla/5.0"},
)
response.raise_for_status()
# 尝试解析IP
content_type = response.headers.get("content-type", "").lower()
if "json" in content_type:
data = response.json()
ip = data.get("ip") or data.get("origin")
if ip:
return str(ip).split(",")[0].strip()
# 从文本中提取IP
text = response.text.strip()
match = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", text)
if match:
return match.group(1)
except Exception:
continue
return None
+267 -1
View File
@@ -8,12 +8,13 @@ from tkinter import ttk, messagebox, filedialog
from datetime import datetime
from pathlib import Path
from queue import Queue, Empty
from typing import List
from typing import List, Optional
import requests
from loguru import logger
from douyu.config import Account, ProxyConfig
from douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
from utils import setup_logger
from .account_importer import load_accounts_from_file
from .login_worker import LoginWorker, parse_accounts_text
@@ -67,6 +68,7 @@ class DouyuLoginApp:
self.log_queue = Queue()
self.proxy_test_queue = Queue()
self.login_preflight_queue = Queue()
self.whitelist_test_queue = Queue()
# 加载配置
self.proxy_config = self.state_store.proxy_from_state(self.state)
@@ -268,6 +270,43 @@ class DouyuLoginApp:
ttk.Label(proxy_frame, textvariable=self.proxy_test_status_var,
foreground='gray', wraplength=320).pack(anchor='w', pady=(6, 0))
# 白名单配置
whitelist_frame = ttk.LabelFrame(parent, text='白名单管理', padding=8)
whitelist_frame.pack(fill='x', pady=(0, 8))
whitelist_top_frame = ttk.Frame(whitelist_frame)
whitelist_top_frame.pack(fill='x')
self.whitelist_enabled_var = tk.BooleanVar(value=self.proxy_config.whitelist_enabled)
ttk.Checkbutton(whitelist_top_frame, text='启用白名单自动管理',
variable=self.whitelist_enabled_var,
command=self._on_whitelist_toggle).pack(side='left')
self.whitelist_test_btn = ttk.Button(whitelist_top_frame, text='测试连接',
command=self._test_whitelist_connection)
self.whitelist_test_btn.pack(side='right')
# UID
uid_frame = ttk.Frame(whitelist_frame)
uid_frame.pack(fill='x', pady=(5, 0))
ttk.Label(uid_frame, text='UID:').pack(side='left')
self.whitelist_uid_var = tk.StringVar(value=self.proxy_config.whitelist_uid)
self.whitelist_uid_entry = ttk.Entry(uid_frame, textvariable=self.whitelist_uid_var, width=20)
self.whitelist_uid_entry.pack(side='left', fill='x', expand=True, padx=(5, 0))
# UKEY
ukey_frame = ttk.Frame(whitelist_frame)
ukey_frame.pack(fill='x', pady=(5, 0))
ttk.Label(ukey_frame, text='UKEY:').pack(side='left')
self.whitelist_ukey_var = tk.StringVar(value=self.proxy_config.whitelist_ukey)
self.whitelist_ukey_entry = ttk.Entry(ukey_frame, textvariable=self.whitelist_ukey_var, width=28)
self.whitelist_ukey_entry.pack(side='left', fill='x', expand=True, padx=(5, 0))
self.whitelist_status_var = tk.StringVar(value='未配置')
ttk.Label(whitelist_frame, textvariable=self.whitelist_status_var,
foreground='gray', wraplength=320).pack(anchor='w', pady=(6, 0))
runtime_frame = ttk.LabelFrame(parent, text='运行设置', padding=8)
runtime_frame.pack(fill='x', pady=(0, 8))
runtime_frame.columnconfigure(1, weight=1)
@@ -292,6 +331,7 @@ class DouyuLoginApp:
# 初始状态
self._on_proxy_toggle()
self._on_whitelist_toggle()
def _create_workspace_section(self, parent):
"""创建可拖拽的账号/日志工作区。"""
@@ -433,17 +473,213 @@ class DouyuLoginApp:
self.proxy_https_entry.configure(state='normal')
self._schedule_save_state()
def _on_whitelist_toggle(self):
"""白名单启用/禁用切换"""
enabled = self.whitelist_enabled_var.get()
if hasattr(self, 'whitelist_uid_entry'):
state = 'normal' if enabled else 'disabled'
self.whitelist_uid_entry.configure(state=state)
self.whitelist_ukey_entry.configure(state=state)
self.whitelist_test_btn.configure(state=state)
if not enabled:
self.whitelist_status_var.set('未启用')
else:
self.whitelist_status_var.set('已启用,请填写UID和UKEY')
self._schedule_save_state()
def _test_whitelist_connection(self):
"""测试白名单API连接并将出口IP同步到白名单"""
uid = self.whitelist_uid_var.get().strip()
ukey = self.whitelist_ukey_var.get().strip()
if not uid or not ukey:
messagebox.showwarning('提示', '请填写UID和UKEY')
return
self.whitelist_test_btn.configure(state='disabled')
self.whitelist_status_var.set('测试中...')
self.log_panel.append_log('info', '测试白名单API连接...')
proxy_config = self._get_proxy_config()
def run_test():
try:
manager = WhitelistManager(uid, ukey)
success, api_msg = manager.test_connection()
self.log_queue.put(('info' if success else 'error', f'白名单API: {api_msg}'))
if not success:
self.whitelist_test_queue.put({'status': '连接失败'})
return
self.log_queue.put(('info', '正在获取出口IP...'))
exit_ip = self._get_exit_ip_for_test(proxy_config)
if not exit_ip:
self.log_queue.put(('warning', '无法获取出口IP'))
self.whitelist_test_queue.put({'status': '无法获取出口IP'})
return
self.log_queue.put(('info', f'出口IP: {exit_ip}'))
records = manager.get_whitelist_json()
in_list = any(r.get('IP') == exit_ip for r in records)
self.log_queue.put(('info', f'白名单共 {len(records)} 条记录'))
if in_list:
record = next((r for r in records if r.get('IP') == exit_ip), {})
memo = record.get('MEMO', '')
if memo == manager.memo:
self.log_queue.put(('success', f'出口IP {exit_ip} 已在白名单中 (备注正确)'))
self.whitelist_test_queue.put({'status': f'{exit_ip} 已在白名单中'})
else:
self.log_queue.put(('warning', f'出口IP {exit_ip} 已存在但备注不匹配 (当前: "{memo}"),正在更新...'))
sync_ok, sync_msg = manager.sync_ip(exit_ip)
if sync_ok:
self.log_queue.put(('success', f'白名单备注已更新: {sync_msg}'))
self.whitelist_test_queue.put({'status': f'{exit_ip} 备注已更新'})
else:
self.log_queue.put(('error', f'白名单更新失败: {sync_msg}'))
self.whitelist_test_queue.put({'status': '更新失败'})
else:
self.log_queue.put(('info', f'正在将 {exit_ip} 添加到白名单...'))
sync_ok, sync_msg = manager.sync_ip(exit_ip)
if sync_ok:
self.log_queue.put(('success', f'白名单同步: {sync_msg}'))
self.whitelist_test_queue.put({'status': f'{exit_ip} 已同步'})
else:
self.log_queue.put(('error', f'白名单同步失败: {sync_msg}'))
self.whitelist_test_queue.put({'status': '同步失败'})
except Exception as e:
self.log_queue.put(('error', f'白名单测试失败: {e}'))
self.whitelist_test_queue.put({'status': '测试失败'})
thread = threading.Thread(target=run_test, daemon=True)
thread.start()
def _on_whitelist_test_complete(self, result: dict):
"""处理白名单测试结果。"""
status = str(result.get('status') or '完成')
enabled = self.whitelist_enabled_var.get()
self.whitelist_status_var.set(status)
self.whitelist_test_btn.configure(state='normal' if enabled else 'disabled')
@staticmethod
def _get_exit_ip_for_test(proxy_config: ProxyConfig) -> Optional[str]:
"""获取出口IP用于白名单测试"""
proxy_url = None
if proxy_config.enabled:
if proxy_config.api_url:
try:
resp = requests.get(proxy_config.api_url, timeout=10)
match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)', resp.text)
if match:
proxy_url = f"http://{match.group(1)}:{match.group(2)}"
except Exception:
pass
if not proxy_url:
proxy_url = proxy_config.http or proxy_config.https
if proxy_url:
return get_exit_ip_via_proxy(proxy_url)
# 没有代理时,用本机网络获取出口IP(多个探测源fallback)
detect_urls = [
'https://qifu-api.baidubce.com/ip/local/geo/v1/district',
'https://myip.ipip.net',
'https://4.ipw.cn',
]
for url in detect_urls:
try:
resp = requests.get(url, timeout=6, headers={'User-Agent': 'Mozilla/5.0'})
text = resp.text.strip()
match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', text)
if match:
return match.group(1)
except Exception:
continue
return None
def _sync_whitelist(self, proxy_config: ProxyConfig) -> bool:
"""
同步白名单IP
获取当前代理出口IP并同步到白名单
"""
if not proxy_config.whitelist_enabled:
return True
uid = proxy_config.whitelist_uid
ukey = proxy_config.whitelist_ukey
if not uid or not ukey:
self.log_queue.put(('warning', '白名单未配置UID/UKEY,跳过同步'))
return True
# 获取代理地址
proxy_url = None
if proxy_config.http or proxy_config.https:
proxy_url = proxy_config.http or proxy_config.https
elif proxy_config.api_url:
# 从API获取代理
try:
self.log_queue.put(('info', '从代理API获取IP用于白名单同步...'))
response = requests.get(proxy_config.api_url, timeout=10)
response.raise_for_status()
text = response.text.strip()
match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)', text)
if match:
proxy_url = f"http://{match.group(1)}:{match.group(2)}"
except Exception as e:
self.log_queue.put(('error', f'获取代理失败: {e}'))
return False
if not proxy_url:
self.log_queue.put(('warning', '无法获取代理地址,跳过白名单同步'))
return True
# 获取出口IP
self.log_queue.put(('info', f'通过代理获取出口IP...'))
exit_ip = get_exit_ip_via_proxy(proxy_url)
if not exit_ip:
self.log_queue.put(('error', '无法获取出口IP,白名单同步失败'))
return False
self.log_queue.put(('info', f'当前出口IP: {exit_ip}'))
# 同步白名单
manager = WhitelistManager(uid, ukey)
success, message = manager.sync_ip(exit_ip)
if success:
self.log_queue.put(('success', f'白名单同步: {message}'))
else:
self.log_queue.put(('error', f'白名单同步: {message}'))
return success
def _get_proxy_config(self) -> ProxyConfig:
"""获取当前代理配置"""
enabled = self.proxy_enabled_var.get()
proxy_type = self.proxy_type_var.get()
# 白名单配置
whitelist_enabled = self.whitelist_enabled_var.get() if hasattr(self, 'whitelist_enabled_var') else False
whitelist_uid = self.whitelist_uid_var.get().strip() if hasattr(self, 'whitelist_uid_var') else ''
whitelist_ukey = self.whitelist_ukey_var.get().strip() if hasattr(self, 'whitelist_ukey_var') else ''
if proxy_type == 'api':
return ProxyConfig(
enabled=enabled,
api_url=self.proxy_api_var.get().strip(),
http='',
https='',
whitelist_enabled=whitelist_enabled,
whitelist_uid=whitelist_uid,
whitelist_ukey=whitelist_ukey,
)
else:
http = self._normalize_proxy_url(self.proxy_http_var.get().strip())
@@ -457,6 +693,9 @@ class DouyuLoginApp:
api_url='',
http=http,
https=https,
whitelist_enabled=whitelist_enabled,
whitelist_uid=whitelist_uid,
whitelist_ukey=whitelist_ukey,
)
@staticmethod
@@ -795,6 +1034,9 @@ class DouyuLoginApp:
self.proxy_https_var,
self.geetest_retries_var,
self.log_level_var,
self.whitelist_enabled_var,
self.whitelist_uid_var,
self.whitelist_ukey_var,
]
for variable in variables:
variable.trace_add('write', lambda *_: self._schedule_save_state())
@@ -825,6 +1067,9 @@ class DouyuLoginApp:
'api_url': proxy_config.api_url,
'http': proxy_config.http,
'https': proxy_config.https,
'whitelist_enabled': proxy_config.whitelist_enabled,
'whitelist_uid': proxy_config.whitelist_uid,
'whitelist_ukey': proxy_config.whitelist_ukey,
},
'proxy_type': self.proxy_type_var.get() if hasattr(self, 'proxy_type_var') else self.proxy_type,
'geetest_retries': self._get_geetest_retries(),
@@ -899,6 +1144,12 @@ class DouyuLoginApp:
self._save_state()
# 同步白名单(手动代理模式,代理地址已知)
if proxy_config.whitelist_enabled and not proxy_config.api_url:
if not self._sync_whitelist(proxy_config):
messagebox.showwarning('提示', '白名单同步失败,请检查配置')
return
if proxy_config.enabled:
self._prepare_login_proxy(proxy_config, geetest_retries, log_level)
return
@@ -936,6 +1187,13 @@ class DouyuLoginApp:
result = self._resolve_working_proxy_config(proxy_config)
result['geetest_retries'] = geetest_retries
result['log_level'] = log_level
# API代理模式:同步白名单(使用已验证的代理地址,避免API返回不同代理导致的不一致)
if proxy_config.whitelist_enabled and proxy_config.api_url and result.get('success'):
synced_config = result.get('proxy_config')
if synced_config:
self._sync_whitelist(synced_config)
self.login_preflight_queue.put(result)
def _on_login_preflight_complete(self, result: dict):
@@ -1067,6 +1325,14 @@ class DouyuLoginApp:
except Empty:
pass
# 处理白名单测试结果
try:
while True:
result = self.whitelist_test_queue.get_nowait()
self._on_whitelist_test_complete(result)
except Empty:
pass
# 处理结果队列
try:
while True:
+3
View File
@@ -112,4 +112,7 @@ class GuiStateStore:
api_url=str(proxy.get("api_url", "")).strip(),
http=str(proxy.get("http", "")).strip(),
https=str(proxy.get("https", "")).strip(),
whitelist_enabled=bool(proxy.get("whitelist_enabled", False)),
whitelist_uid=str(proxy.get("whitelist_uid", "")).strip(),
whitelist_ukey=str(proxy.get("whitelist_ukey", "")).strip(),
)