Create app.py
Browse files
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)
|