Asrasahar commited on
Commit
39c38d4
·
verified ·
1 Parent(s): 946d596

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +271 -0
app.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from flask import Flask, Response, request, stream_with_context
4
+ from urllib.parse import urlparse, urljoin, urlunparse
5
+ import re
6
+ import random
7
+ import logging
8
+ import json # برای تبدیل دیکشنری پایتون به رشته JSON
9
+
10
+ app = Flask(__name__) # اصلاح: __name__ به جای name
11
+ SECRET_NAME_FOR_TARGET_URL = "TARGET_HF_SPACE_URL"
12
+ TARGET_URL_FROM_SECRET = os.environ.get(SECRET_NAME_FOR_TARGET_URL)
13
+ REQUEST_TIMEOUT = 45
14
+
15
+ # --- Logging Setup (حداقل لاگ ممکن) ---
16
+ log = logging.getLogger('werkzeug')
17
+ log.setLevel(logging.ERROR) # فقط خطاهای Werkzeug را لاگ کن
18
+ # app.logger.setLevel(logging.CRITICAL + 1) # غیرفعال کردن کامل لاگ‌های اپلیکیشن Flask
19
+ # برای دیباگ می‌توانید این خط را کامنت کنید یا سطح را تغییر دهید
20
+ if not (os.environ.get('FLASK_DEBUG', '0') == '1' or os.environ.get('FLASK_ENV') == 'development'):
21
+ app.logger.setLevel(logging.CRITICAL + 1)
22
+ else:
23
+ app.logger.setLevel(logging.INFO) # لاگ در حالت دیباگ
24
+
25
+ print(f"APP_STARTUP: TARGET_URL_FROM_SECRET: {'SET' if TARGET_URL_FROM_SECRET else 'NOT SET'}")
26
+ if TARGET_URL_FROM_SECRET:
27
+ print(f"APP_STARTUP: Target URL is '{TARGET_URL_FROM_SECRET[:20]}...'") # بخشی از URL برای تایید
28
+
29
+ COMMON_USER_AGENTS = [
30
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36",
31
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15",
32
+ # ... (سایر User-Agent ها)
33
+ ]
34
+
35
+ SIMPLE_TEXT_REPLACEMENTS = {
36
+ "Aya Vision": "تحلیل",
37
+ "Chat with Aya": "چت🤖",
38
+ "Visualize with Aya": "عکس",
39
+ "Speak with Aya": "", # این مورد باعث مخفی شدن المان والدش می‌شود
40
+ # ... (سایر جایگزینی‌های متن)
41
+ "Gradio": "App", # مثال: جایگزینی کلمه Gradio
42
+ "Built with Gradio": "" # حذف این عبارت
43
+ }
44
+
45
+ GRADIO_HIDE_CSS_STANDARD = """
46
+ <style>
47
+ /* فوتر Gradio */
48
+ .gradio-container .meta-footer, .gradio-container footer, div[class*="footer"], footer, a[href*="gradio.app"],
49
+ /* دکمه Settings Gradio */
50
+ .gradio-container button[id*="settings"], .gradio-container div[class*="settings-button"],
51
+ a[href*="gradio.app/"], button[title*="Settings"], button[aria-label*="Settings"], div[data-testid*="settings"] {
52
+ display: none !important;
53
+ visibility: hidden !important;
54
+ opacity: 0 !important;
55
+ width: 0 !important;
56
+ height: 0 !important;
57
+ overflow: hidden !important;
58
+ margin: 0 !important;
59
+ padding: 0 !important;
60
+ border: none !important;
61
+ font-size: 0 !important;
62
+ line-height: 0 !important;
63
+ }
64
+ /* کلاس برای مخفی کردن المان‌هایی که متن آنها با رشته خالی جایگزین شده */
65
+ .gr-proxy-item-hidden-by-text {
66
+ display: none !important;
67
+ visibility: hidden !important;
68
+ }
69
+ </style>
70
+ """
71
+
72
+ def process_html_content_server_side(html_string):
73
+ """فقط حذف‌های اولیه سمت سرور با Regex (بدون تزریق CSS در اینجا)."""
74
+ processed_html = html_string
75
+ # ... (کد مربوط به حذف با Regex شما) ...
76
+ return processed_html
77
+
78
+ @app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'])
79
+ @app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD']) # اصلاح: <path:path>
80
+ def proxy_request_handler(path):
81
+ if not TARGET_URL_FROM_SECRET:
82
+ app.logger.critical(f"CRITICAL: Secret '{SECRET_NAME_FOR_TARGET_URL}' is not set.")
83
+ return "Proxy Configuration Error: Target URL secret is not configured.", 500
84
+
85
+ base_target_url = TARGET_URL_FROM_SECRET.rstrip('/')
86
+ # اطمینان از اینکه path با / شروع نمی‌شود اگر base_target_url خودش با / ختم شده
87
+ target_full_url = urljoin(base_target_url + "/", path.lstrip('/'))
88
+ if request.query_string:
89
+ target_full_url += "?" + request.query_string.decode()
90
+
91
+ app.logger.debug(f"Proxying request for path: /{path} to {target_full_url}")
92
+
93
+ try:
94
+ parsed_target_url_for_host = urlparse(TARGET_URL_FROM_SECRET)
95
+ target_hostname = parsed_target_url_for_host.hostname
96
+ user_agent_to_send = random.choice(COMMON_USER_AGENTS)
97
+
98
+ excluded_incoming_headers = [
99
+ 'host', 'cookie', 'connection', 'upgrade-insecure-requests',
100
+ 'if-none-match', 'if-modified-since', 'referer', 'x-hf-space-host',
101
+ 'content-length' # اجازه به requests برای محاسبه مجدد
102
+ ]
103
+ forward_headers = {key: value for key, value in request.headers.items() if key.lower() not in excluded_incoming_headers}
104
+ forward_headers['User-Agent'] = user_agent_to_send
105
+ forward_headers['Host'] = target_hostname
106
+ forward_headers['Accept-Encoding'] = 'gzip, deflate, br'
107
+
108
+ # مدیریت X-Forwarded-For
109
+ if request.headers.getlist("X-Forwarded-For"):
110
+ forward_headers["X-Forwarded-For"] = request.headers.getlist("X-Forwarded-For")[0] + ", " + request.remote_addr
111
+ else:
112
+ forward_headers["X-Forwarded-For"] = request.remote_addr
113
+
114
+ # برای POST, PUT, etc.
115
+ request_body = request.get_data() if request.method not in ['GET', 'HEAD', 'OPTIONS'] else None
116
+
117
+ with requests.Session() as s:
118
+ target_response = s.request(
119
+ method=request.method,
120
+ url=target_full_url,
121
+ headers=forward_headers,
122
+ data=request_body,
123
+ stream=True, # مهم برای stream کردن پاسخ
124
+ timeout=REQUEST_TIMEOUT,
125
+ allow_redirects=False # ریدایرکت‌ها را خودمان مدیریت می‌کنیم
126
+ )
127
+
128
+ # مدیریت ریدایرکت‌ها
129
+ if 300 <= target_response.status_code < 400 and 'Location' in target_response.headers:
130
+ location = target_response.headers['Location']
131
+ # ... (کد مدیریت ریدایرکت شما، اطمینان از بازنویسی صحیح Location) ...
132
+ # این بخش نیاز به بازبینی دقیق دارد تا Location به درستی به دامنه پروکسی بازنویسی شود
133
+ parsed_location = urlparse(location)
134
+ rewritten_location_header_val = location
135
+ # اگر ریدایرکت نسبی است یا به همان هاست هدف است، آن را برای پروکسی بازنویسی کن
136
+ if not parsed_location.scheme or parsed_location.hostname == target_hostname:
137
+ # URL کامل ریدایرکت را بر اساس URL نهایی که requests به آن رسیده، بساز
138
+ abs_target_redirect_url = urljoin(target_response.url, location)
139
+ new_path_on_target = urlparse(abs_target_redirect_url).path
140
+ new_query_on_target = urlparse(abs_target_redirect_url).query
141
+
142
+ rewritten_location_path = new_path_on_target
143
+ if new_query_on_target:
144
+ rewritten_location_path += "?" + new_query_on_target
145
+ # اطمینان از اینکه با / شروع می‌شود اگر خالی نیست
146
+ if not rewritten_location_path.startswith('/') and rewritten_location_path:
147
+ rewritten_location_path = '/' + rewritten_location_path
148
+ rewritten_location_header_val = rewritten_location_path if rewritten_location_path else "/"
149
+
150
+ final_redirect_headers = {'Location': rewritten_location_header_val}
151
+ # سایر هدرهای مهم ریدایرکت را هم منتقل کن
152
+ for h_key, h_val in target_response.headers.items():
153
+ if h_key.lower() in ['set-cookie', 'cache-control', 'expires', 'pragma', 'vary']:
154
+ final_redirect_headers[h_key] = h_val
155
+ return Response(response=None, status=target_response.status_code, headers=final_redirect_headers)
156
+
157
+ # مدیریت خطاهای سرور هدف
158
+ if target_response.status_code >= 400:
159
+ app.logger.warning(f"Target {target_full_url} error: {target_response.status_code}")
160
+ error_content = target_response.content # ممکن است بزرگ باشد
161
+ error_headers_from_target = {
162
+ k:v for k,v in target_response.headers.items()
163
+ if k.lower() not in ['content-encoding', 'transfer-encoding', 'content-length']
164
+ }
165
+ return Response(response=error_content, status=target_response.status_code, headers=error_headers_from_target)
166
+
167
+ content_type = target_response.headers.get('Content-Type', 'application/octet-stream')
168
+
169
+ excluded_response_headers = [
170
+ 'content-encoding', 'transfer-encoding', 'connection', 'keep-alive',
171
+ 'x-frame-options', 'strict-transport-security', 'public-key-pins',
172
+ 'content-length', 'server', 'x-powered-by', 'date',
173
+ 'link', # <<< مهم: حذف هدر Link از پاسخ
174
+ 'x-canonical-url' # <<< مهم: حذف هدر احتمالی دیگر برای URL کنونیکال
175
+ ]
176
+ final_response_headers = {
177
+ key: value for key, value in target_response.headers.items()
178
+ if key.lower() not in excluded_response_headers
179
+ }
180
+ final_response_headers['Content-Type'] = content_type
181
+ final_response_headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
182
+ final_response_headers['Pragma'] = 'no-cache'
183
+ final_response_headers['Expires'] = '0'
184
+ final_response_headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
185
+
186
+ def generate_response_content_stream():
187
+ # ... (کد طولانی شما برای generate_response_content_stream با تزریق CSS/JS) ...
188
+ # این بخش بسیار طولانی است و برای سادگی اینجا خلاصه شده.
189
+ # اگر این کد Flask اجرا شود، این تابع مسئول تغییر محتوای HTML خواهد بود.
190
+ # در حال حاضر با تنظیمات Nginx، این تابع اجرا نمی‌شود.
191
+ is_html_content_type = 'text/html' in content_type.lower()
192
+ base_tag_injected_flag = False
193
+ css_and_js_injected_flag = False
194
+
195
+ current_buffer = b''
196
+ charset_match = re.search(r'charset=([\w-]+)', content_type, re.IGNORECASE)
197
+ html_processing_encoding = charset_match.group(1) if charset_match else 'utf-8'
198
+
199
+ # محاسبه base href (همانطور که داشتید)
200
+ # ...
201
+ # base_tag_html_to_inject = f'<base href="{escaped_href_for_final_base_tag}">'
202
+
203
+ # js_replacements_json = json.dumps(SIMPLE_TEXT_REPLACEMENTS)
204
+ # mutation_observer_script = f"""...""" # اسکریپت JS شما
205
+
206
+ try:
207
+ for chunk_data in target_response.iter_content(chunk_size=8192): # chunk_size می‌تواند تنظیم شود
208
+ if not chunk_data: continue
209
+
210
+ if is_html_content_type:
211
+ current_buffer += chunk_data
212
+ # ... (منطق پردازش بافر و تزریق HTML/CSS/JS شما) ...
213
+ # مثال ساده:
214
+ # buffer_as_string = current_buffer.decode(html_processing_encoding, errors='replace')
215
+ # processed_chunk_string = buffer_as_string
216
+ # if not css_and_js_injected_flag and '<head>' in processed_chunk_string:
217
+ # # تزریق CSS و base tag
218
+ # css_and_js_injected_flag = True
219
+ # if '</body>' in processed_chunk_string:
220
+ # # تزریق JS
221
+ # yield processed_chunk_string.encode(html_processing_encoding, errors='replace')
222
+ # current_buffer = b''
223
+ # این بخش باید کامل از کد شما کپی شود اگر می‌خواهید از Flask استفاده کنید.
224
+ # برای سادگی، اینجا فقط محتوای خام را برمی‌گردانیم:
225
+ yield chunk_data # در حالت فعلی، برای اینکه کد خلاصه شود.
226
+ # اگر این کد Flask فعال شود، باید منطق پردازش HTML شما اینجا باشد.
227
+ else: # محتوای غیر HTML
228
+ yield chunk_data
229
+
230
+ # پردازش باقیمانده بافر (اگر چیزی مانده)
231
+ if current_buffer:
232
+ if is_html_content_type:
233
+ # ... (منطق پردازش باقیمانده بافر HTML شما) ...
234
+ yield current_buffer # باز هم، برای خلاصه بودن.
235
+ else:
236
+ yield current_buffer
237
+ except Exception as stream_err:
238
+ app.logger.error(f"Error during HTML/Stream processing for {target_full_url}: {stream_err}")
239
+ finally:
240
+ target_response.close()
241
+
242
+ if request.method == 'HEAD':
243
+ return Response(response=None, status=target_response.status_code, headers=final_response_headers)
244
+ else:
245
+ return Response(stream_with_context(generate_response_content_stream()), status=target_response.status_code, headers=final_response_headers)
246
+
247
+ except requests.exceptions.Timeout:
248
+ app.logger.error(f"Timeout ({REQUEST_TIMEOUT}s) fetching {target_full_url}")
249
+ return "Error: Request to target site timed out.", 504
250
+ except requests.exceptions.HTTPError as http_err:
251
+ app.logger.error(f"HTTPError {http_err.response.status_code} from {target_full_url}. Resp: {http_err.response.text[:200]}")
252
+ # ... (کد مدیریت خطا) ...
253
+ return "HTTP Error from target.", 502 # یا وضعیت خطای اصلی
254
+ except requests.exceptions.ConnectionError as conn_err:
255
+ app.logger.error(f"ConnectionError for {target_full_url}: {conn_err}")
256
+ return f"Error: Could not connect to target.", 502
257
+ except requests.exceptions.RequestException as req_err:
258
+ app.logger.error(f"RequestException for {target_full_url}: {req_err}")
259
+ return f"Error fetching content.", 502
260
+ except Exception as general_err:
261
+ app.logger.exception(f"Unexpected Python error proxying {target_full_url}")
262
+ return f"Unexpected server error.", 500
263
+
264
+ if __name__ == '__main__':
265
+ port = int(os.environ.get("PORT", 7860)) # پورت استاندارد Hugging Face Spaces
266
+ # debug_mode = os.environ.get('FLASK_ENV') == 'development' or os.environ.get('FLASK_DEBUG', '0') == '1'
267
+ # در Hugging Face معمولا FLASK_DEBUG تنظیم نمی‌شود، مگر اینکه خودتان در Dockerfile یا secrets تنظیم کنید
268
+ debug_mode = os.environ.get('FLASK_DEBUG', '0') == '1'
269
+
270
+ print(f"INFO: Starting Flask app on host 0.0.0.0, port {port}, debug: {debug_mode}")
271
+ app.run(host='0.0.0.0', port=port, debug=debug_mode)