diff --git a/README.md b/README.md index 445df46..98334e4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # API-Python -> 基于Python的本地Web API项目 +> 基于Python3.9+的Web API项目 -#### v260716补丁 -1. 优化import、修改README -2. 修复import异常 +#### v260728 +1. 优化(修复)import +2. 修改README +3. 新增IP请求速率限制(默认30/min) +4. 增强兼容性 #### v260716 1. 新增代理接口(包含白名单及密码限制) @@ -19,3 +21,5 @@ #### v260512 1. 新增农历、运势、唐诗、地址、时间、谜语等接口及对应数据 + + diff --git a/main.py b/main.py index d24ea03..7291693 100755 --- a/main.py +++ b/main.py @@ -1,4 +1,6 @@ -import os, json, random, socket, logging, ipaddress, urllib.error, urllib.parse, urllib.request +import os, json, random, socket, logging, ipaddress, urllib.error, urllib.parse, urllib.request, threading +from collections import defaultdict, deque +from time import monotonic from pathlib import Path from datetime import date, datetime from zoneinfo import ZoneInfo, available_timezones @@ -90,6 +92,34 @@ for item in RIDDLE_DATA: RIDDLE_RIDDLE_MAP[riddle_lower].append(item) RIDDLE_ANSWER_MAP[answer_lower].append(item) +RATE_LIMIT = 30 +RATE_WINDOW_SECONDS = 60 +_request_times = defaultdict(deque) +_rate_limit_lock = threading.Lock() + + +@app.before_request +def enforce_rate_limit(): + now = monotonic() + forwarded_for = request.headers.get("X-Forwarded-For", "") + client_ip = forwarded_for.split(",", 1)[0].strip() or request.remote_addr or "unknown" + with _rate_limit_lock: + timestamps = _request_times[client_ip] + cutoff = now - RATE_WINDOW_SECONDS + while timestamps and timestamps[0] <= cutoff: + timestamps.popleft() + if len(timestamps) >= RATE_LIMIT: + retry_after = max(1, int(timestamps[0] + RATE_WINDOW_SECONDS - now + 0.999)) + response = jsonify({ + "error": "请求过于频繁,请稍后再试", + "limit": f"{RATE_LIMIT}/min", + "retry_after": retry_after, + }) + response.status_code = 429 + response.headers["Retry-After"] = str(retry_after) + return response + timestamps.append(now) + @app.route("/favicon.ico") def favicon(): @@ -135,7 +165,7 @@ def lunar(): args_lower = {k.lower(): v for k, v in request.args.items()} if set(args_lower.keys()) - {"date"}: return jsonify({ - "error": f"{set(args_lower.keys()) - {"date"}}参数非法", + "error": "{}参数非法".format(set(args_lower.keys()) - {"date"}), "allowed_params": ["date=YYYY-MM-DD"], }) ds = args_lower.get("date", "").strip() @@ -193,7 +223,7 @@ def luck(): args_lower = {k.lower(): v for k, v in request.args.items()} if set(args_lower.keys()) - {"id", "title", "type"}: return jsonify({ - "error": f"{set(args_lower.keys()) - {"id", "title", "type"}}参数非法", + "error": "{}参数非法".format(set(args_lower.keys()) - {"id", "title", "type"}), "allowed_params": ["id=xxx", "title=xxx", "type=xxx"], }) luck_id = args_lower.get("id", "").strip() @@ -251,7 +281,7 @@ def poem(): args_lower = {k.lower(): v for k, v in request.args.items()} if set(args_lower.keys()) - {"id", "title", "author", "type", "full"}: return jsonify({ - "error": f"{set(args_lower.keys()) - {"id", "title", "author", "type", "full"}}参数非法", + "error": "{}参数非法".format(set(args_lower.keys()) - {"id", "title", "author", "type", "full"}), "allowed_params": ["id=xxx", "title=xxx", "author=xxx", "type=xxx", "full=true/false"], }) poem_id = args_lower.get("id", "").strip() @@ -347,7 +377,7 @@ def ip(): args_lower = {k.lower(): v for k, v in request.args.items()} if set(args_lower.keys()) - {"name"}: return jsonify({ - "error": f"{set(args_lower.keys()) - {"name"}}参数非法", + "error": "{}参数非法".format(set(args_lower.keys()) - {"name"}), "allowed_params": ["name=xxx"], }) query_name = args_lower.get("name", "").strip() @@ -417,7 +447,7 @@ def time_api(): args_lower = {k.lower(): v for k, v in request.args.items()} if set(args_lower.keys()) - {"tz"}: return jsonify({ - "error": f"{set(args_lower.keys()) - {"tz"}}参数非法", + "error": "{}参数非法".format(set(args_lower.keys()) - {"tz"}), "allowed_params": ["tz=xxx"], }) tz = args_lower.get("tz", "").strip() @@ -482,7 +512,7 @@ def riddle(): args_lower = {k.lower(): v for k, v in request.args.items()} if set(args_lower.keys()) - {"riddle", "answer"}: return jsonify({ - "error": f"{set(args_lower.keys()) - {"riddle", "answer"}}参数非法", + "error": "{}参数非法".format(set(args_lower.keys()) - {"riddle", "answer"}), "allowed_params": ["riddle=xxx", "answer=xxx"], }) query_riddle = args_lower.get("riddle", "").strip() @@ -532,7 +562,7 @@ def sci(): args_lower = {k.lower(): v for k, v in request.args.items()} if set(args_lower.keys()) - {"nane", "name"}: return jsonify({ - "error": f"{set(args_lower.keys()) - {"nane", "name"}}参数非法", + "error": "{}参数非法".format(set(args_lower.keys()) - {"nane", "name"}), "allowed_params": ["nane=xxx"], }) publication_name = args_lower.get("nane", "").strip() or args_lower.get("name", "").strip() @@ -604,7 +634,7 @@ def agent(): extra_params = set(args_lower.keys()) - allowed_keys if extra_params: return jsonify({ - "error": f"{extra_params}参数非法", + "error": "{}参数非法".format(extra_params), "allowed_params": ["url=xxx", "ref=xxx", "pwd=xxx"], }) target_url = args_lower.get("url", "").strip() @@ -692,7 +722,7 @@ def pan_file_download(): extra_params = set(args_lower.keys()) - allowed_keys if extra_params: return jsonify({ - "error": f"{extra_params}参数非法", + "error": "{}参数非法".format(extra_params), "allowed_params": ["name=xxx", "download=true/false"], }) file_name = args_lower.get("name", "").strip() diff --git a/requirements.txt b/requirements.txt index 04551a1..a9877b6 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flask flask-cors lunar-python -py-ip2region \ No newline at end of file +py-ip2region +tzdata \ No newline at end of file