新增一系列安全相关功能: 1. 新增二维码生成工具,支持自定义参数导出图片/Base64/字节流 2. 新增HTML内容安全处理工具,包括标签剥离、转义、URL校验 3. 新增日志敏感信息脱敏工具,支持配置、字典、日志记录脱敏 4. 新增密钥管理运行时,支持从环境变量/文件加载密钥 5. 新增身份链接管理,支持多渠道身份绑定与解析 6. 新增SSRF防护工具,支持域名/IP校验与固定主机 7. 新增安全权限修复工具,修复文件目录权限与配置项 8. 新增外部内容安全处理,支持LLM特殊令牌剥离与注入检测 9. 新增认证限流工具,支持多维度限流与本地回环豁免 10. 新增配对管理工具,支持安全配对码生成与校验 11. 新增白名单管理工具,支持DM/群组/来源白名单校验
167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
import threading
|
|
from abc import ABC, abstractmethod
|
|
|
|
from yuxi.repositories.channel_identity_link_repo import ChannelIdentityLinkRepository
|
|
|
|
|
|
class IdentityLinkStore(ABC):
|
|
@abstractmethod
|
|
async def load_all(self) -> dict[str, list[str]]: ...
|
|
|
|
@abstractmethod
|
|
async def add(self, identity: str, channel_type: str, peer_id: str) -> None: ...
|
|
|
|
@abstractmethod
|
|
async def remove(self, channel_type: str, peer_id: str) -> None: ...
|
|
|
|
@abstractmethod
|
|
async def remove_identity(self, identity: str) -> None: ...
|
|
|
|
|
|
class PostgresIdentityLinkStore(IdentityLinkStore):
|
|
def __init__(self, repo: ChannelIdentityLinkRepository | None = None):
|
|
self._repo = repo or ChannelIdentityLinkRepository()
|
|
|
|
async def load_all(self) -> dict[str, list[str]]:
|
|
return await self._repo.find_all()
|
|
|
|
async def add(self, identity: str, channel_type: str, peer_id: str) -> None:
|
|
await self._repo.add_link(identity, channel_type, peer_id)
|
|
|
|
async def remove(self, channel_type: str, peer_id: str) -> None:
|
|
await self._repo.remove_link(channel_type, peer_id)
|
|
|
|
async def remove_identity(self, identity: str) -> None:
|
|
await self._repo.remove_identity(identity)
|
|
|
|
|
|
class InMemoryIdentityLinkStore(IdentityLinkStore):
|
|
def __init__(self):
|
|
self._data: dict[str, list[str]] = {}
|
|
|
|
async def load_all(self) -> dict[str, list[str]]:
|
|
return dict(self._data)
|
|
|
|
async def add(self, identity: str, channel_type: str, peer_id: str) -> None:
|
|
entry = f"{channel_type}:{peer_id}"
|
|
existing = self._data.setdefault(identity, [])
|
|
if entry not in existing:
|
|
existing.append(entry)
|
|
|
|
async def remove(self, channel_type: str, peer_id: str) -> None:
|
|
entry = f"{channel_type}:{peer_id}"
|
|
for identity, entries in list(self._data.items()):
|
|
if entry in entries:
|
|
entries.remove(entry)
|
|
if not entries:
|
|
del self._data[identity]
|
|
|
|
async def remove_identity(self, identity: str) -> None:
|
|
self._data.pop(identity, None)
|
|
|
|
|
|
class IdentityLinkResolver:
|
|
def __init__(self, store: IdentityLinkStore | None = None):
|
|
self._store = store or PostgresIdentityLinkStore()
|
|
self._lock = threading.Lock()
|
|
self._links: dict[str, list[str]] = {}
|
|
self._linked_id_to_identity: dict[str, str] = {}
|
|
self._resolved_cache: dict[str, str] = {}
|
|
self._loaded = False
|
|
|
|
def _rebuild_reverse_index(self) -> None:
|
|
self._linked_id_to_identity = {}
|
|
for identity, linked_ids in self._links.items():
|
|
for linked_id in linked_ids:
|
|
lid_lower = linked_id.lower()
|
|
if lid_lower not in self._linked_id_to_identity:
|
|
self._linked_id_to_identity[lid_lower] = identity
|
|
|
|
async def ensure_loaded(self) -> None:
|
|
if self._loaded:
|
|
return
|
|
with self._lock:
|
|
if self._loaded:
|
|
return
|
|
self._loaded = True
|
|
|
|
self._links = await self._store.load_all()
|
|
with self._lock:
|
|
self._rebuild_reverse_index()
|
|
self._resolved_cache.clear()
|
|
|
|
def set_links(self, links: dict[str, list[str]]) -> None:
|
|
with self._lock:
|
|
self._links = links
|
|
self._resolved_cache.clear()
|
|
self._rebuild_reverse_index()
|
|
self._loaded = True
|
|
|
|
async def add_link(self, identity: str, channel_type: str, peer_id: str) -> None:
|
|
await self._store.add(identity, channel_type, peer_id)
|
|
entry = f"{channel_type}:{peer_id}"
|
|
with self._lock:
|
|
existing = self._links.get(identity, [])
|
|
if entry not in existing:
|
|
existing.append(entry)
|
|
self._links[identity] = existing
|
|
self._resolved_cache.clear()
|
|
entry_lower = entry.lower()
|
|
if entry_lower not in self._linked_id_to_identity:
|
|
self._linked_id_to_identity[entry_lower] = identity
|
|
|
|
async def remove_link(self, channel_type: str, peer_id: str) -> None:
|
|
await self._store.remove(channel_type, peer_id)
|
|
entry = f"{channel_type}:{peer_id}"
|
|
with self._lock:
|
|
for identity, entries in list(self._links.items()):
|
|
if entry in entries:
|
|
entries.remove(entry)
|
|
if not entries:
|
|
del self._links[identity]
|
|
entry_lower = entry.lower()
|
|
self._linked_id_to_identity.pop(entry_lower, None)
|
|
self._resolved_cache.clear()
|
|
|
|
async def remove_identity(self, identity: str) -> None:
|
|
await self._store.remove_identity(identity)
|
|
with self._lock:
|
|
self._links.pop(identity, None)
|
|
self._resolved_cache.clear()
|
|
self._rebuild_reverse_index()
|
|
|
|
def resolve(self, channel: str, peer_id: str, use_cache: bool = True) -> str:
|
|
key = f"{channel}:{peer_id}"
|
|
with self._lock:
|
|
if use_cache and key in self._resolved_cache:
|
|
return self._resolved_cache[key]
|
|
|
|
if not self._linked_id_to_identity:
|
|
self._resolved_cache[key] = peer_id
|
|
return peer_id
|
|
|
|
result = self._linked_id_to_identity.get(peer_id.lower())
|
|
if result is not None:
|
|
self._resolved_cache[key] = result
|
|
return result
|
|
|
|
result = self._linked_id_to_identity.get(key.lower())
|
|
if result is not None:
|
|
self._resolved_cache[key] = result
|
|
return result
|
|
|
|
self._resolved_cache[key] = peer_id
|
|
return peer_id
|
|
|
|
def list_links(self) -> dict[str, list[str]]:
|
|
with self._lock:
|
|
return dict(self._links)
|
|
|
|
def get_resolved_peer_ids(self) -> set[str]:
|
|
with self._lock:
|
|
return set(self._resolved_cache.values())
|
|
|
|
def clear_cache(self) -> None:
|
|
with self._lock:
|
|
self._resolved_cache.clear()
|