TonyD365 commited on
Commit
72a95a4
·
verified ·
1 Parent(s): 35b58a2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +34 -9
app.py CHANGED
@@ -14,6 +14,8 @@ FSD Simulator 缓存中间层 (Hugging Face Space + Docker)
14
  import os
15
  import time
16
  import random
 
 
17
  from typing import Any, Optional
18
 
19
  import httpx
@@ -30,6 +32,9 @@ CACHE_TTL = 12 * 60 * 60
30
  # 抖动范围 ±30 分钟,避免所有 key 同一刻过期造成"缓存雪崩"
31
  CACHE_JITTER = 30 * 60
32
 
 
 
 
33
  app = FastAPI(title="FSD Cache Layer")
34
 
35
  # ---------- 内存缓存(进程一重启就清空,所以只当加速层,不当唯一数据源)----------
@@ -113,11 +118,29 @@ def parse_rows(execute_response: dict) -> list[dict]:
113
  return rows_out
114
 
115
 
116
- # ---------- 鉴权:校验 Roblox 传来的口令 ----------
117
- def check_app_key(x_app_key: Optional[str]):
118
- # 没设 APP_SECRET 就跳过校验方便本地测试;线上务必设置!
119
- if APP_SECRET and x_app_key != APP_SECRET:
120
- raise HTTPException(401, "X-App-Key 无")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
 
123
  # ==================== 路由 ====================
@@ -141,10 +164,11 @@ class LogPayload(BaseModel):
141
 
142
  @app.post("/log")
143
  async def log_events(payload: LogPayload,
144
- x_app_key: Optional[str] = Header(default=None)):
 
145
  """【写勤】Roblox 批量上报驾驶行为,直通写入 Turso(不缓存)。
146
  玩家数据必须尽快落库,不能压在缓存里等 12 小时——Space 一重启就丢了。"""
147
- check_app_key(x_app_key)
148
  if not payload.events:
149
  return {"written": 0}
150
 
@@ -159,10 +183,11 @@ async def log_events(payload: LogPayload,
159
 
160
  @app.get("/stats")
161
  async def scene_stats(scene: str = Query(...),
162
- x_app_key: Optional[str] = Header(default=None)):
 
163
  """【读懒】查某个场景下各操作的人气统计,结果缓存 12 小时。
164
  NPC 决策大量调这个,能命中缓存就绝不打扰 Turso。"""
165
- check_app_key(x_app_key)
166
 
167
  cache_key = f"stats:{scene}"
168
  cached = cache_get(cache_key)
 
14
  import os
15
  import time
16
  import random
17
+ import hashlib
18
+ import hmac
19
  from typing import Any, Optional
20
 
21
  import httpx
 
32
  # 抖动范围 ±30 分钟,避免所有 key 同一刻过期造成"缓存雪崩"
33
  CACHE_JITTER = 30 * 60
34
 
35
+ # 签名有效时间窗(秒):容忍 Roblox 与服务端 5 分钟时钟差,同时限制重放窗口
36
+ SIGNATURE_WINDOW = 300
37
+
38
  app = FastAPI(title="FSD Cache Layer")
39
 
40
  # ---------- 内存缓存(进程一重启就清空,所以只当加速层,不当唯一数据源)----------
 
118
  return rows_out
119
 
120
 
121
+ # ---------- 鉴权:时间戳签名校验 ----------
122
+ # Roblox 端发送两个请求头:
123
+ # X-Timestamp = 当前 Unix 秒Luau 的 os.time()
124
+ # X-Signature = sha256(APP_SECRET + X-Timestamp) 的十六进制小写
125
+ # 好处:不再明文传 APP_SECRET;签名随时间变化,超出时间窗即失
126
+ def check_signature(x_timestamp: Optional[str], x_signature: Optional[str]):
127
+ # 没设 APP_SECRET 就跳过(方便本地测试);线上务必设置!
128
+ if not APP_SECRET:
129
+ return
130
+ if not x_timestamp or not x_signature:
131
+ raise HTTPException(401, "缺少 X-Timestamp 或 X-Signature")
132
+ try:
133
+ t = int(x_timestamp)
134
+ except ValueError:
135
+ raise HTTPException(401, "X-Timestamp 格式错误")
136
+ # 超出时间窗 → 视为过期或重放,拒绝
137
+ if abs(int(time.time()) - t) > SIGNATURE_WINDOW:
138
+ raise HTTPException(401, "签名已过期(检查两端时钟)")
139
+ # 用同样的 APP_SECRET + 原始时间戳字符串重算,必须和客户端逐字符一致
140
+ expected = hashlib.sha256((APP_SECRET + x_timestamp).encode()).hexdigest()
141
+ # 恒定时间比较,防时序攻击(timing attack)
142
+ if not hmac.compare_digest(expected, x_signature):
143
+ raise HTTPException(401, "签名无效")
144
 
145
 
146
  # ==================== 路由 ====================
 
164
 
165
  @app.post("/log")
166
  async def log_events(payload: LogPayload,
167
+ x_timestamp: Optional[str] = Header(default=None),
168
+ x_signature: Optional[str] = Header(default=None)):
169
  """【写勤】Roblox 批量上报驾驶行为,直通写入 Turso(不缓存)。
170
  玩家数据必须尽快落库,不能压在缓存里等 12 小时——Space 一重启就丢了。"""
171
+ check_signature(x_timestamp, x_signature)
172
  if not payload.events:
173
  return {"written": 0}
174
 
 
183
 
184
  @app.get("/stats")
185
  async def scene_stats(scene: str = Query(...),
186
+ x_timestamp: Optional[str] = Header(default=None),
187
+ x_signature: Optional[str] = Header(default=None)):
188
  """【读懒】查某个场景下各操作的人气统计,结果缓存 12 小时。
189
  NPC 决策大量调这个,能命中缓存就绝不打扰 Turso。"""
190
+ check_signature(x_timestamp, x_signature)
191
 
192
  cache_key = f"stats:{scene}"
193
  cached = cache_get(cache_key)