新增 Microsoft Teams 渠道扩展,支持在 Yuxi 平台中集成 Microsoft Teams 协作平台。 包含以下功能模块: - sdk: Bot Framework SDK 封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - auth: JWT 认证 - jwks: JWKS 密钥管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - state: 状态管理 - runtime: 运行时管理 - actions: 动作处理 - adaptive_card: 自适应卡片 - task_modules: 任务模块 - message_extension: 消息扩展 - proactive: Proactive Messaging - graph: Microsoft Graph API 集成 - graph_teams: Teams 操作 - graph_members: 成员管理 - graph_messages: 消息获取 - graph_thread: 线程管理 - graph_users: 用户管理 - graph_upload: 文件上传 - files: 文件处理 - file_consent: 文件授权 - conversations: 会话存储 - mentions: @提及处理 - threading: 线程管理 - reactions: 表情反应 - polls: 投票功能 - meetings: 会议集成 - feedback: 反馈处理 - sso: 单点登录 - deep_links: 深层链接 - incoming_webhook: 入站 Webhook - localization: 本地化 - user_agent: 用户代理 - sent_message_cache: 消息缓存 - types: 类型定义
178 lines
4.8 KiB
Python
178 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
import ipaddress
|
|
import logging
|
|
import re
|
|
from enum import StrEnum
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SSRF_BLOCKED_DOMAINS: tuple[str, ...] = (
|
|
"169.254.",
|
|
"metadata.google.internal",
|
|
"metadata.tencentyun.com",
|
|
"100.64.",
|
|
"0.0.0.0",
|
|
)
|
|
SSRF_BLOCKED_NETS: tuple[ipaddress.IPv4Network | ipaddress.IPv6Network, ...] = (
|
|
ipaddress.IPv4Network("127.0.0.0/8"),
|
|
ipaddress.IPv4Network("10.0.0.0/8"),
|
|
ipaddress.IPv4Network("172.16.0.0/12"),
|
|
ipaddress.IPv4Network("192.168.0.0/16"),
|
|
ipaddress.IPv4Network("169.254.0.0/16"),
|
|
ipaddress.IPv4Network("100.64.0.0/10"),
|
|
ipaddress.IPv4Network("0.0.0.0/8"),
|
|
ipaddress.IPv6Network("::1/128"),
|
|
ipaddress.IPv6Network("fe80::/10"),
|
|
)
|
|
MAX_DOWNLOAD_SIZE = 100 * 1024 * 1024
|
|
|
|
|
|
class UploadRoute(StrEnum):
|
|
ATTACHMENT_URL = "attachment_url"
|
|
CONNECTOR = "connector"
|
|
ONEDRIVE = "onedrive"
|
|
SHAREPOINT = "sharepoint"
|
|
M365_NATIVE = "m365_native"
|
|
|
|
|
|
def is_url_microsoft_safe(url: str) -> bool:
|
|
try:
|
|
host = _extract_host(url)
|
|
if not host:
|
|
return False
|
|
|
|
host_lower = host.lower()
|
|
|
|
microsoft_domains = (
|
|
"graph.microsoft.com",
|
|
"graph.microsoft.us",
|
|
"api.teams.microsoft.com",
|
|
"login.microsoftonline.com",
|
|
"login.microsoftonline.us",
|
|
"smba.trafficmanager.net",
|
|
"sharepoint.com",
|
|
"sharepoint.cn",
|
|
"onedrive.com",
|
|
"office.com",
|
|
"office365.com",
|
|
"microsoft.com",
|
|
"microsoftonline.com",
|
|
"azure.com",
|
|
"azure.cn",
|
|
)
|
|
if host_lower == "localhost":
|
|
return False
|
|
if host_lower in ("127.0.0.1", "::1", "0.0.0.0"):
|
|
return False
|
|
|
|
for domain in microsoft_domains:
|
|
if host_lower == domain or host_lower.endswith(f".{domain}"):
|
|
return True
|
|
|
|
return False
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _validate_url_safety(url: str) -> bool:
|
|
try:
|
|
host = _extract_host(url)
|
|
if not host:
|
|
return False
|
|
|
|
host_lower = host.lower()
|
|
|
|
if host_lower == "localhost":
|
|
logger.warning("SSRF blocked: localhost")
|
|
return False
|
|
if host_lower in ("127.0.0.1", "::1", "0.0.0.0"):
|
|
logger.warning("SSRF blocked: loopback %s", host_lower)
|
|
return False
|
|
|
|
try:
|
|
ip = ipaddress.ip_address(host)
|
|
for net in SSRF_BLOCKED_NETS:
|
|
if ip in net:
|
|
logger.warning("SSRF blocked: private IP %s", host)
|
|
return False
|
|
return True
|
|
except ValueError:
|
|
pass
|
|
|
|
for domain in SSRF_BLOCKED_DOMAINS:
|
|
if domain in host_lower:
|
|
logger.warning("SSRF blocked: %s contains %s", host_lower, domain)
|
|
return False
|
|
|
|
try:
|
|
ip = ipaddress.ip_address(host)
|
|
for net in SSRF_BLOCKED_NETS:
|
|
if ip in net:
|
|
logger.warning("SSRF blocked: resolved IP %s is private", ip)
|
|
return False
|
|
except ValueError:
|
|
pass
|
|
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def resolve_upload_route(
|
|
file_size: int,
|
|
*,
|
|
url: str = "",
|
|
is_team_channel: bool = False,
|
|
) -> UploadRoute:
|
|
if url and is_url_microsoft_safe(url):
|
|
return UploadRoute.M365_NATIVE
|
|
|
|
if file_size < 4 * 1024 * 1024:
|
|
return UploadRoute.ATTACHMENT_URL
|
|
|
|
if file_size < 50 * 1024 * 1024:
|
|
return UploadRoute.CONNECTOR
|
|
|
|
if is_team_channel:
|
|
return UploadRoute.SHAREPOINT
|
|
|
|
return UploadRoute.ONEDRIVE
|
|
|
|
|
|
def _extract_host(url: str) -> str:
|
|
url = url.strip()
|
|
if "://" not in url:
|
|
url = f"https://{url}"
|
|
idx = url.index("://") + 3
|
|
host = url[idx:]
|
|
host = re.split(r"[/?#:@]", host)[0]
|
|
return host.strip()
|
|
|
|
|
|
async def download_public_file(url: str) -> bytes | None:
|
|
if not _validate_url_safety(url):
|
|
return None
|
|
|
|
try:
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
|
|
resp = await client.get(url)
|
|
resp.raise_for_status()
|
|
|
|
content_length = int(resp.headers.get("content-length", "0"))
|
|
if content_length > MAX_DOWNLOAD_SIZE:
|
|
logger.warning("File too large: %d bytes from %s", content_length, url)
|
|
return None
|
|
|
|
content = await resp.aread()
|
|
if len(content) > MAX_DOWNLOAD_SIZE:
|
|
logger.warning("File too large: %d bytes from %s", len(content), url)
|
|
return None
|
|
|
|
return content
|
|
except Exception as e:
|
|
logger.warning("Failed to download file from %s: %s", url, e)
|
|
return None
|