aigenai commited on
Commit
cb63927
·
verified ·
1 Parent(s): 5178f22

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +299 -0
app.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import re
4
+ import time
5
+ import logging
6
+ from functools import lru_cache
7
+ from urllib.parse import urlparse, urljoin
8
+ from typing import Dict, Any, Optional, List
9
+ from dataclasses import dataclass
10
+ from datetime import datetime
11
+
12
+ # 配置日志
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(levelname)s - %(message)s'
16
+ )
17
+
18
+ @dataclass
19
+ class ProxyResponse:
20
+ """代理响应数据类"""
21
+ status: int
22
+ content: str
23
+ headers: Dict[str, str]
24
+ redirect_url: Optional[str] = None
25
+ error: Optional[str] = None
26
+
27
+ class Config:
28
+ """配置类"""
29
+ ASSET_URL = "https://1pages.nbid.bid/"
30
+ PREFIX = "/"
31
+ JSDELIVR = 0
32
+ CACHE_TTL = 3600
33
+ MAX_RETRIES = 3
34
+ TIMEOUT = 10
35
+ RATE_LIMIT = {
36
+ "window_ms": 15 * 60 * 1000, # 15分钟
37
+ "max": 100 # 限制每个IP最多100个请求
38
+ }
39
+ WHITE_LIST: List[str] = [] # 白名单
40
+
41
+ # 请求头
42
+ DEFAULT_HEADERS = {
43
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
44
+ }
45
+
46
+ # CORS设置
47
+ CORS = {
48
+ "allow_origins": ["*"],
49
+ "allow_methods": ["GET", "POST", "OPTIONS"],
50
+ "allow_headers": ["*"],
51
+ "max_age": 1728000
52
+ }
53
+
54
+ # URL模式
55
+ PATTERNS = {
56
+ "releases": r"^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:releases|archive)\/.*$",
57
+ "blob": r"^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:blob|raw)\/.*$",
58
+ "git": r"^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:info|git-).*$",
59
+ "raw": r"^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+?\/.+$",
60
+ "gist": r"^(?:https?:\/\/)?gist\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+$",
61
+ "tags": r"^(?:https?:\/\/)?github\.com\/.+?\/.+?\/tags.*$"
62
+ }
63
+
64
+ class RateLimiter:
65
+ """请求频率限制器"""
66
+ def __init__(self):
67
+ self.request_records: Dict[str, List[float]] = {}
68
+
69
+ def is_allowed(self, ip: str) -> bool:
70
+ now = time.time() * 1000
71
+ window_start = now - Config.RATE_LIMIT["window_ms"]
72
+
73
+ if ip not in self.request_records:
74
+ self.request_records[ip] = []
75
+
76
+ # 清理过期记录
77
+ self.request_records[ip] = [t for t in self.request_records[ip] if t > window_start]
78
+
79
+ if len(self.request_records[ip]) >= Config.RATE_LIMIT["max"]:
80
+ return False
81
+
82
+ self.request_records[ip].append(now)
83
+ return True
84
+
85
+ def get_remaining(self, ip: str) -> int:
86
+ """获取剩余请求次数"""
87
+ if ip not in self.request_records:
88
+ return Config.RATE_LIMIT["max"]
89
+ return Config.RATE_LIMIT["max"] - len(self.request_records[ip])
90
+
91
+ class GitHubProxy:
92
+ """GitHub代理核心类"""
93
+ def __init__(self):
94
+ self.rate_limiter = RateLimiter()
95
+ self.session = requests.Session()
96
+ self.session.headers.update(Config.DEFAULT_HEADERS)
97
+
98
+ def check_url(self, url: str) -> bool:
99
+ """检查URL是否匹配GitHub模式"""
100
+ return any(re.search(pattern, url, re.I) for pattern in Config.PATTERNS.values())
101
+
102
+ def check_white_list(self, url: str) -> bool:
103
+ """检查白名单"""
104
+ if not Config.WHITE_LIST:
105
+ return True
106
+ return any(white_item in url for white_item in Config.WHITE_LIST)
107
+
108
+ @lru_cache(maxsize=1000)
109
+ def fetch_github_content(self, url: str, method: str = "GET") -> ProxyResponse:
110
+ """获取GitHub内容(带缓存)"""
111
+ try:
112
+ response = self.session.request(
113
+ method=method,
114
+ url=url,
115
+ timeout=Config.TIMEOUT,
116
+ allow_redirects=False
117
+ )
118
+
119
+ headers = dict(response.headers)
120
+
121
+ # 处理重定向
122
+ if response.is_redirect:
123
+ redirect_url = response.headers["Location"]
124
+ if self.check_url(redirect_url):
125
+ redirect_url = Config.PREFIX + redirect_url
126
+ return ProxyResponse(
127
+ status=response.status_code,
128
+ content="",
129
+ headers=headers,
130
+ redirect_url=redirect_url
131
+ )
132
+
133
+ return ProxyResponse(
134
+ status=response.status_code,
135
+ content=response.text,
136
+ headers=headers
137
+ )
138
+
139
+ except requests.Timeout:
140
+ return ProxyResponse(
141
+ status=504,
142
+ content="Request Timeout",
143
+ headers={},
144
+ error="请求超时"
145
+ )
146
+ except Exception as e:
147
+ logging.error(f"Fetch error: {str(e)}")
148
+ return ProxyResponse(
149
+ status=500,
150
+ content=str(e),
151
+ headers={},
152
+ error="服务器内部错误"
153
+ )
154
+
155
+ def proxy_request(self, url: str, request: gr.Request) -> Dict[str, Any]:
156
+ """处理代理请求"""
157
+ # 记录请求
158
+ logging.info(f"Proxy request from {request.client.host} to {url}")
159
+
160
+ # 检查频率限制
161
+ if not self.rate_limiter.is_allowed(request.client.host):
162
+ return {
163
+ "status": 429,
164
+ "content": "Too Many Requests",
165
+ "headers": {},
166
+ "error": "请求过于频繁,请稍后再试",
167
+ "rate_limit": {
168
+ "remaining": self.rate_limiter.get_remaining(request.client.host),
169
+ "reset": int((time.time() * 1000 + Config.RATE_LIMIT["window_ms"]) / 1000)
170
+ }
171
+ }
172
+
173
+ # 检查白名单
174
+ if not self.check_white_list(url):
175
+ return {
176
+ "status": 403,
177
+ "content": "Access Denied",
178
+ "headers": {},
179
+ "error": "访问被拒绝"
180
+ }
181
+
182
+ # 处理URL
183
+ if not url.startswith(("http://", "https://")):
184
+ url = "https://" + url
185
+
186
+ # 检查URL是否为GitHub链接
187
+ if not self.check_url(url):
188
+ return {
189
+ "status": 400,
190
+ "content": "Invalid GitHub URL",
191
+ "headers": {},
192
+ "error": "无效的GitHub URL"
193
+ }
194
+
195
+ # 处理jsDelivr重定向
196
+ if Config.JSDELIVR and re.search(Config.PATTERNS["blob"], url):
197
+ url = url.replace("/blob/", "@").replace("github.com", "cdn.jsdelivr.net/gh")
198
+ return {
199
+ "status": 302,
200
+ "content": "",
201
+ "headers": {"Location": url},
202
+ "redirect_url": url
203
+ }
204
+
205
+ # 获取内容
206
+ response = self.fetch_github_content(url)
207
+ result = {
208
+ "status": response.status,
209
+ "content": response.content,
210
+ "headers": response.headers,
211
+ "timestamp": datetime.now().isoformat()
212
+ }
213
+
214
+ if response.redirect_url:
215
+ result["redirect_url"] = response.redirect_url
216
+ if response.error:
217
+ result["error"] = response.error
218
+
219
+ return result
220
+
221
+ def create_interface():
222
+ """创建Gradio界面"""
223
+ proxy = GitHubProxy()
224
+
225
+ with gr.Blocks(title="GitHub Proxy", theme=gr.themes.Soft()) as app:
226
+ gr.Markdown("""
227
+ # 🚀 GitHub Proxy
228
+
229
+ ### 功能特点
230
+ - ✨ 支持多种GitHub URL格式
231
+ - 🔄 自动处理重定向
232
+ - 💾 响应缓存
233
+ - ⚡ CDN加速支持
234
+ - 🛡️ 请求频率限制
235
+
236
+ ### 支持的URL类型
237
+ - GitHub Release/Archive
238
+ - GitHub Raw/Blob
239
+ - GitHub Gist
240
+ - Raw GitHub Content
241
+ """)
242
+
243
+ with gr.Row():
244
+ url_input = gr.Textbox(
245
+ label="GitHub URL",
246
+ placeholder="输入GitHub URL,例如:github.com/user/repo/blob/master/file.txt",
247
+ scale=4
248
+ )
249
+ submit_btn = gr.Button("获取内容", scale=1)
250
+
251
+ with gr.Row():
252
+ with gr.Column():
253
+ status = gr.Textbox(label="状态")
254
+ headers = gr.JSON(label="响应头")
255
+ with gr.Column():
256
+ content = gr.Textbox(label="内容", max_lines=20)
257
+ error = gr.Textbox(label="错误信息", visible=False)
258
+
259
+ def handle_request(url: str, request: gr.Request):
260
+ result = proxy.proxy_request(url, request)
261
+
262
+ # 更新UI
263
+ error_visible = "error" in result
264
+ error_msg = result.get("error", "")
265
+
266
+ return {
267
+ status: f"状态码: {result['status']}",
268
+ headers: result["headers"],
269
+ content: result["content"],
270
+ error: error_msg,
271
+ error: gr.update(visible=error_visible, value=error_msg)
272
+ }
273
+
274
+ submit_btn.click(
275
+ fn=handle_request,
276
+ inputs=[url_input],
277
+ outputs=[status, headers, content, error]
278
+ )
279
+
280
+ # 添加示例
281
+ gr.Examples(
282
+ examples=[
283
+ ["github.com/microsoft/vscode/blob/main/README.md"],
284
+ ["raw.githubusercontent.com/microsoft/vscode/main/README.md"],
285
+ ["gist.github.com/username/gist_id/raw/file.txt"]
286
+ ],
287
+ inputs=url_input
288
+ )
289
+
290
+ return app
291
+
292
+ if __name__ == "__main__":
293
+ app = create_interface()
294
+ app.launch(
295
+ server_name="0.0.0.0",
296
+ server_port=7860,
297
+ show_error=True,
298
+ quiet=False
299
+ )