diff --git a/app/api/routes/chat.py b/app/api/routes/chat.py index 68e3f5b..0c3c8c5 100644 --- a/app/api/routes/chat.py +++ b/app/api/routes/chat.py @@ -22,9 +22,9 @@ async def ws_chat(websocket: WebSocket): # Collect client info client_ip = websocket.client.host if websocket.client else "" - forwarded = websocket.headers.get("x-forwarded-for", "") + forwarded = websocket.headers.get("x-forwarded-for", "").split(",")[0].strip() if forwarded: - client_ip = forwarded.split(",")[0].strip() + client_ip = forwarded user_agent = websocket.headers.get("user-agent", "") client_info = {"ip": client_ip, "user_agent": user_agent} @@ -58,6 +58,10 @@ async def ws_chat(websocket: WebSocket): init_msg.get("source", "standalone") ) merged_info = {**client_info, **(init_msg.get("client_info") or {})} + # 服务端 XFF 优先于客户端自报 ip(客户端自报可伪造,可伪造面最小); + # 无 XFF 时保留客户端自报,user_agent 等其他字段不受影响 + if forwarded: + merged_info["ip"] = forwarded conv = await manager.create_conversation( source=source, client_info=merged_info, diff --git a/app/api/routes/chat_rest.py b/app/api/routes/chat_rest.py index 7d7b059..f27644a 100644 --- a/app/api/routes/chat_rest.py +++ b/app/api/routes/chat_rest.py @@ -32,13 +32,19 @@ class SessionRequest(BaseModel): @router.post("/session") -async def chat_session(req: SessionRequest): +async def chat_session(req: SessionRequest, request: Request): """Resume an active conversation by session_id, or create a new app one. `context` flows into `create_conversation` where it is whitelisted into `raw_info` (only when non-None). `client_info` is stored for analytics only (ip/ua/device-id/lang) and never fed to the AI prompt. """ + # 服务端 XFF 优先于客户端自报 ip(客户端自报可伪造,可伪造面最小) + xf = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + client_info = dict(req.client_info or {}) + if xf: + client_info["ip"] = xf + async with async_session() as db: manager = ConversationManager(db) if req.session_id: @@ -51,7 +57,7 @@ async def chat_session(req: SessionRequest): } conv = await manager.create_conversation( source=ConversationSource.app, - client_info=req.client_info, + client_info=client_info, context=req.context, ) return { diff --git a/app/services/ip_service.py b/app/services/ip_service.py index 79ec787..fb04730 100644 --- a/app/services/ip_service.py +++ b/app/services/ip_service.py @@ -9,7 +9,7 @@ COUNTRY_NAMES = { "VN": "越南", "ID": "印尼", "PH": "菲律宾", "MY": "马来西亚", "JP": "日本", "KR": "韩国", "GB": "英国", "DE": "德国", "FR": "法国", "RU": "俄罗斯", "BR": "巴西", "MX": "墨西哥", - "AU": "加拿大", "AU": "澳大利亚", "TR": "土耳其", "IR": "伊朗", + "AU": "澳大利亚", "CA": "加拿大", "TR": "土耳其", "IR": "伊朗", "MM": "缅甸", "KH": "柬埔寨", "LA": "老挝", "LK": "斯里兰卡", "NP": "尼泊尔", "UG": "乌干达", "ET": "埃塞俄比亚", "SN": "塞内加尔", "CI": "科特迪瓦", "CM": "喀麦隆", "CD": "刚果(金)", "MZ": "莫桑比克", @@ -26,3 +26,21 @@ def resolve_country(ip: str) -> str: return "未知" except Exception: return "未知" + + +def resolve_region_code(ip: str) -> str | None: + """解析 ip 为 ISO 3166-1 alpha-2 国家码(如 "US"),查不到返回 None。 + + 与 resolve_country 的分工:resolve_country 返回中文名(含"本地"/"未知" + 兜底占位,仅供展示/记录);本函数返回 ISO alpha-2 或 None,供电话区号 + 推断等逻辑消费——绝无中文或占位值,调用方无需再过滤。 + """ + if not ip or ip in ("127.0.0.1", "::1", "localhost"): + return None + try: + code = country_code(ip) + if code and code != "--": + return code + return None + except Exception: + return None diff --git a/requirements.txt b/requirements.txt index 218a5db..46836ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ pydantic-settings>=2.0.0 python-dotenv>=1.0.0 sniffio>=1.3.0 iptocc>=0.1.0 +phonenumberslite diff --git a/tests/test_ip_service.py b/tests/test_ip_service.py new file mode 100644 index 0000000..6d6a06c --- /dev/null +++ b/tests/test_ip_service.py @@ -0,0 +1,13 @@ +def test_resolve_region_code(): + from app.services.ip_service import resolve_region_code + # 公网IP返回 ISO alpha-2 或 None(查不到),绝不返回中文/"本地"/"未知" + r = resolve_region_code("104.28.41.169") + assert r is None or (isinstance(r, str) and len(r) == 2 and r.isupper() and r.isalpha()) + assert resolve_region_code("") is None + assert resolve_region_code("127.0.0.1") is None + assert resolve_region_code("::1") is None + +def test_country_names_ca_bug_fixed(): + from app.services import ip_service + assert ip_service.COUNTRY_NAMES.get("CA") == "加拿大" + assert ip_service.COUNTRY_NAMES.get("AU") == "澳大利亚"