新增 LINE 渠道扩展,支持在 Yuxi 平台中集成 LINE 即时通讯渠道。 包含以下功能模块: - bot: LINE Bot 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - signature: 请求签名验证 - token_manager: Token 管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - flex_templates: Flex 模板消息 - card_command: 卡片指令处理 - template_messages: 模板消息 - rich_menu: 富菜单管理 - actions: 动作处理 - directives: 指令处理 - delivery: 消息送达确认 - loading: 加载动画 - media: 媒体资源处理 - types: 类型定义
85 lines
2.2 KiB
Python
85 lines
2.2 KiB
Python
from __future__ import annotations
|
|
|
|
import ipaddress
|
|
import logging
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ALLOWED_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".heic", ".heif", ".avif"}
|
|
ALLOWED_VIDEO_EXTENSIONS = {".mp4", ".mov", ".m4v", ".webm"}
|
|
ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".m4a", ".aac", ".wav", ".ogg", ".oga"}
|
|
|
|
MAX_URL_LENGTH = 2000
|
|
DEFAULT_MEDIA_MAX_MB = 10
|
|
|
|
_SSRF_BLOCKED_NETWORKS = [
|
|
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.IPv6Network("::1/128"),
|
|
ipaddress.IPv6Network("fc00::/7"),
|
|
ipaddress.IPv6Network("fe80::/10"),
|
|
]
|
|
|
|
|
|
def validate_media_url(url: str) -> bool:
|
|
if not url:
|
|
return False
|
|
|
|
if len(url) > MAX_URL_LENGTH:
|
|
logger.warning("LINE media URL too long: %d characters", len(url))
|
|
return False
|
|
|
|
parsed = urlparse(url)
|
|
if parsed.scheme != "https":
|
|
logger.warning("LINE media URL must use HTTPS: %s", parsed.scheme)
|
|
return False
|
|
|
|
if not parsed.hostname:
|
|
return False
|
|
|
|
return _check_ssrf(parsed.hostname)
|
|
|
|
|
|
def _check_ssrf(hostname: str) -> bool:
|
|
try:
|
|
addr = ipaddress.ip_address(hostname)
|
|
except ValueError:
|
|
return True
|
|
|
|
for network in _SSRF_BLOCKED_NETWORKS:
|
|
if addr in network:
|
|
logger.warning("LINE SSRF blocked: hostname=%s network=%s", hostname, network)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def detect_media_type(filename: str) -> str:
|
|
ext = Path(filename).suffix.lower()
|
|
|
|
if ext in ALLOWED_IMAGE_EXTENSIONS:
|
|
return "image"
|
|
if ext in ALLOWED_VIDEO_EXTENSIONS:
|
|
return "video"
|
|
if ext in ALLOWED_AUDIO_EXTENSIONS:
|
|
return "audio"
|
|
|
|
return "file"
|
|
|
|
|
|
def validate_media_size(data: bytes, max_mb: int = DEFAULT_MEDIA_MAX_MB) -> bool:
|
|
max_bytes = max_mb * 1024 * 1024
|
|
return len(data) <= max_bytes
|
|
|
|
|
|
def sanitize_filename(filename: str) -> str:
|
|
name = Path(filename).name
|
|
name = re.sub(r'[<>:"/\\|?*]', "_", name)
|
|
return name or "file" |