From 4c924375aadc7be262702c3b8e7b2a33b099c646 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 22 Jun 2026 11:29:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=20ok?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- douyu/__init__.py | 3 +- douyu/config.py | 4 + douyu/whitelist.py | 268 +++++++++++++++++++++++++++++++++++++++++++++ gui/app.py | 268 ++++++++++++++++++++++++++++++++++++++++++++- gui/state.py | 3 + 5 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 douyu/whitelist.py diff --git a/douyu/__init__.py b/douyu/__init__.py index 61cd3a8..73ac7a9 100644 --- a/douyu/__init__.py +++ b/douyu/__init__.py @@ -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"] diff --git a/douyu/config.py b/douyu/config.py index 2f8d379..7048026 100644 --- a/douyu/config.py +++ b/douyu/config.py @@ -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: diff --git a/douyu/whitelist.py b/douyu/whitelist.py new file mode 100644 index 0000000..07cdb53 --- /dev/null +++ b/douyu/whitelist.py @@ -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 diff --git a/gui/app.py b/gui/app.py index 1dd6677..0313851 100644 --- a/gui/app.py +++ b/gui/app.py @@ -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: diff --git a/gui/state.py b/gui/state.py index d0f8ab2..ad70256 100644 --- a/gui/state.py +++ b/gui/state.py @@ -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(), )