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