该端点授权给 any 且无任何鉴权,会发起外网抓取(BOC 页面)并写 exchange_rate 表, 匿名可反复触发 → 外部请求放大 + 脏数据写入 + DoS。改为仅本机可调(client_ip)。 build.sh 的 crontab 本就是 curl localhost:9090,不影响定时任务。
74 lines
3.1 KiB
Plaintext
74 lines
3.1 KiB
Plaintext
# 获取中国银行外汇牌价并插入 exchange_rate 表
|
||
# GET /accounting/api/fetch_forex_rates.dspy
|
||
#
|
||
# 安全:本端点是定时任务入口(会发起外网抓取 + 写库),只允许本机调用。
|
||
# 之前无任何鉴权且授权给 any 角色,匿名可反复触发 → 外部抓取放大 + 脏数据写入 + DoS。
|
||
# 判定依据 client_ip:nginx 用 $proxy_add_x_forwarded_for 追加模式、中间件取链尾值,
|
||
# 外部伪造 X-Forwarded-For 会被追加真实 IP 到链尾,伪造不成立;且应用端口不对外开放。
|
||
_ip = request.get('client_ip') or ''
|
||
if _ip not in ('127.0.0.1', '::1', 'localhost'):
|
||
return json.dumps({'success': False, 'message': '仅允许本机调用(定时任务入口)'},
|
||
ensure_ascii=False)
|
||
|
||
import re
|
||
import urllib.request
|
||
from datetime import date
|
||
|
||
BOC_URL = 'https://www.boc.cn/sourcedb/whpj/index.html'
|
||
CURRENCIES = {'美元': 'USD', '日元': 'JPY', '英镑': 'GBP'}
|
||
|
||
async with get_sor_context(request._run_ns, 'accounting') as sor:
|
||
# 1. 抓取 BOC 页面
|
||
try:
|
||
req = urllib.request.Request(BOC_URL, headers={'User-Agent': 'Mozilla/5.0'})
|
||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||
html = resp.read().decode('utf-8', errors='replace')
|
||
except Exception as e:
|
||
return json.dumps({'success': False, 'message': f'获取BOC页面失败: {e}'}, ensure_ascii=False)
|
||
|
||
today = date.today().isoformat()
|
||
inserted = 0
|
||
|
||
for cn_name, code in CURRENCIES.items():
|
||
pos = html.find(cn_name)
|
||
if pos < 0:
|
||
continue
|
||
chunk = html[pos:pos+500]
|
||
tds = re.findall(r'<td[^>]*>(.*?)</td>', chunk, re.DOTALL)
|
||
clean = [re.sub(r'<[^>]+>', '', t).strip() for t in tds]
|
||
if len(clean) < 6:
|
||
continue
|
||
try:
|
||
buy_rate = float(clean[1]) # 现汇买入价
|
||
sell_rate = float(clean[3]) # 现汇卖出价
|
||
mid_rate = float(clean[5]) # 中行折算价
|
||
except ValueError:
|
||
continue
|
||
|
||
# BOC quotes USD/GBP per 100 units, JPY per 100 units
|
||
if code in ('USD', 'GBP'):
|
||
buy_rate = round(buy_rate / 100, 6)
|
||
sell_rate = round(sell_rate / 100, 6)
|
||
mid_rate = round(mid_rate / 100, 6)
|
||
elif code == 'JPY':
|
||
buy_rate = round(buy_rate / 100, 6)
|
||
sell_rate = round(sell_rate / 100, 6)
|
||
mid_rate = round(mid_rate / 100, 6)
|
||
|
||
record_id = f'er_{code.lower()}_cny_{today.replace("-","")}'
|
||
|
||
await sor.execute(
|
||
"""INSERT INTO exchange_rate (id, from_currency, to_currency, buy_rate, sell_rate, mid_rate, effective_date)
|
||
VALUES (${id}$, ${from_cur}$, 'CNY', ${buy}$, ${sell}$, ${mid}$, ${date}$)
|
||
ON DUPLICATE KEY UPDATE buy_rate=${buy}$, sell_rate=${sell}$, mid_rate=${mid}$""",
|
||
{'id': record_id, 'from_cur': code, 'buy': buy_rate, 'sell': sell_rate, 'mid': mid_rate, 'date': today}
|
||
)
|
||
inserted += 1
|
||
|
||
return json.dumps({
|
||
'success': True,
|
||
'message': f'已更新 {inserted} 条汇率',
|
||
'date': today,
|
||
'source': '中国银行外汇牌价'
|
||
}, ensure_ascii=False)
|