Asrasahar commited on
Commit
6f23192
·
verified ·
1 Parent(s): 23cff32

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -304
app.py CHANGED
@@ -7,42 +7,39 @@ import random
7
  import logging
8
  import json # برای تبدیل دیکشنری پایتون به رشته JSON
9
 
10
- app = Flask(__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)
18
- app.logger.setLevel(logging.CRITICAL + 1)
19
-
20
- print(f"APP_STARTUP: Flask logger effectively disabled. TARGET_URL_FROM_SECRET: {'SET' if TARGET_URL_FROM_SECRET else 'NOT SET'}")
21
-
 
 
 
 
 
 
22
 
23
  COMMON_USER_AGENTS = [
24
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36",
25
  "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",
 
26
  ]
27
 
28
- # دیکشنری ساده برای کلماتی که می‌خواهید جایگزین شوند
29
  SIMPLE_TEXT_REPLACEMENTS = {
30
  "Aya Vision": "تحلیل",
31
  "Chat with Aya": "چت🤖",
32
  "Visualize with Aya": "عکس",
33
  "Speak with Aya": "", # این مورد باعث مخفی شدن المان والدش می‌شود
34
- # مثال‌های دیگر برای تست
35
- "Model:": "مدل:",
36
- "Developed by:": "توسعه‌دهنده:",
37
- "License:": "مجوز:",
38
- "Input Prompt": "فرمان ورودی",
39
- "Input Image": "تصویر ورودی",
40
- "Drop Image Here": "تصویر را اینجا رها کنید",
41
- "- or -": "- یا -",
42
- "Click to Upload": "برای آپلود کلیک کنید",
43
- "Ask anything in our 23 languages ...": "به هر یک از ۲۳ زبان سوال خود را بپرسید ...",
44
- "Connecting Our World": "اتصال دنیای ما",
45
- # ... کلمات بیشتر
46
  }
47
 
48
  GRADIO_HIDE_CSS_STANDARD = """
@@ -72,349 +69,176 @@ GRADIO_HIDE_CSS_STANDARD = """
72
  </style>
73
  """
74
 
75
- # --- تابع برای حذف لینک‌ها و فوتر Gradio (با تزریق CSS) ---
76
  def process_html_content_server_side(html_string):
77
  """فقط حذف‌های اولیه سمت سرور با Regex (بدون تزریق CSS در اینجا)."""
78
  processed_html = html_string
79
-
80
- gradio_built_with_patterns = [
81
- re.compile(r'<[^>]*(?:class|id)\s*=\s*["\'][^"\']*(?:footer|meta-footer|built-with|gradio-footer)[^"\']*["\'][^>]*>.*?Built with Gradio.*?<\/[^>]+>', re.IGNORECASE | re.DOTALL),
82
- re.compile(r'<footer\b[^>]*>.*?Built with Gradio.*?</footer>', re.IGNORECASE | re.DOTALL),
83
- re.compile(r'<a\b[^>]*href="https?://(?:www\.)?gradio\.app[^"]*"[^>]*>.*?Built with Gradio.*?</a>', re.IGNORECASE | re.DOTALL),
84
- re.compile(r'<div\b[^>]*>.*?Built with Gradio.*?gradio\.app.*?</div>', re.IGNORECASE | re.DOTALL),
85
- re.compile(r'<a\b[^>]*href="https?://(?:www\.)?gradio\.app[^"]*"[^>]*>.*?</a>', re.IGNORECASE | re.DOTALL),
86
- ]
87
- for pattern in gradio_built_with_patterns:
88
- processed_html = pattern.sub('', processed_html)
89
-
90
- settings_patterns = [
91
- re.compile(r'<[^>]*(?:class|id)\s*=\s*["\'][^"\']*(?:settings|options)[^"\']*["\'][^>]*>.*?Settings.*?<\/[^>]+>', re.IGNORECASE | re.DOTALL),
92
- re.compile(r'<(?:button|a)\b[^>]*>.*?Settings.*?</(?:button|a)>', re.IGNORECASE | re.DOTALL),
93
- ]
94
- for pattern in settings_patterns:
95
- processed_html = pattern.sub('', processed_html)
96
-
97
- link_pattern = re.compile(r'<a\b[^>]*>.*?</a>', re.IGNORECASE | re.DOTALL) # حذف همه لینک‌ها
98
- processed_html = link_pattern.sub('', processed_html)
99
-
100
  return processed_html
101
 
102
  @app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'])
103
- @app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'])
104
  def proxy_request_handler(path):
105
- # ... (بخش زیادی از کد شما بدون تغییر باقی می‌ماند) ...
106
- current_request_path_with_query = f"/{path}"
107
- if request.query_string:
108
- current_request_path_with_query += "?" + request.query_string.decode()
109
-
110
  if not TARGET_URL_FROM_SECRET:
111
  app.logger.critical(f"CRITICAL: Secret '{SECRET_NAME_FOR_TARGET_URL}' is not set.")
112
  return "Proxy Configuration Error: Target URL secret is not configured.", 500
113
 
114
  base_target_url = TARGET_URL_FROM_SECRET.rstrip('/')
115
- target_full_url = urljoin(base_target_url + "/", path)
116
- if request.query_string: target_full_url += "?" + request.query_string.decode()
 
 
 
 
117
 
118
  try:
119
  parsed_target_url_for_host = urlparse(TARGET_URL_FROM_SECRET)
120
  target_hostname = parsed_target_url_for_host.hostname
121
  user_agent_to_send = random.choice(COMMON_USER_AGENTS)
122
- excluded_incoming_headers = ['host', 'cookie', 'connection', 'upgrade-insecure-requests', 'if-none-match', 'if-modified-since', 'referer', 'x-hf-space-host']
 
 
 
 
 
123
  forward_headers = {key: value for key, value in request.headers.items() if key.lower() not in excluded_incoming_headers}
124
  forward_headers['User-Agent'] = user_agent_to_send
125
  forward_headers['Host'] = target_hostname
126
- forward_headers['Accept-Encoding'] = 'gzip, deflate, br' # اجازه به سرور برای ارسال فشرده
127
- if request.headers.getlist("X-Forwarded-For"): forward_headers["X-Forwarded-For"] = request.headers.getlist("X-Forwarded-For")[0] + ", " + request.remote_addr
128
- else: forward_headers["X-Forwarded-For"] = request.remote_addr
 
 
 
 
 
 
129
  request_body = request.get_data() if request.method not in ['GET', 'HEAD', 'OPTIONS'] else None
130
 
131
  with requests.Session() as s:
132
- target_response = s.request(method=request.method, url=target_full_url, headers=forward_headers, data=request_body, stream=True, timeout=REQUEST_TIMEOUT, allow_redirects=False)
133
-
 
 
 
 
 
 
 
 
 
134
  if 300 <= target_response.status_code < 400 and 'Location' in target_response.headers:
135
  location = target_response.headers['Location']
 
 
136
  parsed_location = urlparse(location)
137
- rewritten_location_header_val = location
138
- if not parsed_location.scheme or parsed_location.hostname == target_hostname: # Relative redirect or same-host
139
- abs_target_redirect_url = urljoin(target_response.url, location) # Make absolute based on target's last URL
 
 
140
  new_path_on_target = urlparse(abs_target_redirect_url).path
141
  new_query_on_target = urlparse(abs_target_redirect_url).query
 
142
  rewritten_location_path = new_path_on_target
143
- if new_query_on_target: rewritten_location_path += "?" + new_query_on_target
144
- if not rewritten_location_path.startswith('/') and rewritten_location_path: rewritten_location_path = '/' + rewritten_location_path
 
 
 
145
  rewritten_location_header_val = rewritten_location_path if rewritten_location_path else "/"
146
-
147
  final_redirect_headers = {'Location': rewritten_location_header_val}
 
148
  for h_key, h_val in target_response.headers.items():
149
  if h_key.lower() in ['set-cookie', 'cache-control', 'expires', 'pragma', 'vary']:
150
  final_redirect_headers[h_key] = h_val
151
  return Response(response=None, status=target_response.status_code, headers=final_redirect_headers)
152
-
 
153
  if target_response.status_code >= 400:
154
  app.logger.warning(f"Target {target_full_url} error: {target_response.status_code}")
155
- error_content = target_response.content # Could be large, but send it
156
- 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']}
 
 
 
157
  return Response(response=error_content, status=target_response.status_code, headers=error_headers_from_target)
158
 
159
  content_type = target_response.headers.get('Content-Type', 'application/octet-stream')
160
- 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'] # Keep 'expires', 'pragma', 'cache-control', 'set-cookie' for now
161
- final_response_headers = {key: value for key, value in target_response.headers.items() if key.lower() not in excluded_response_headers}
 
 
 
 
 
 
 
 
 
 
162
  final_response_headers['Content-Type'] = content_type
163
- # کنترل کش برای جلوگیری از کش شدن توسط مرورگر یا پراکسی‌های میانی
164
  final_response_headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
165
  final_response_headers['Pragma'] = 'no-cache'
166
  final_response_headers['Expires'] = '0'
167
  final_response_headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
168
 
169
  def generate_response_content_stream():
 
 
 
 
170
  is_html_content_type = 'text/html' in content_type.lower()
171
- # فلگ‌ها برای اطمینان از تزریق فقط یک بار
172
  base_tag_injected_flag = False
173
- css_and_js_injected_flag = False # یک فلگ برای CSS و JS با هم
174
 
175
  current_buffer = b''
176
  charset_match = re.search(r'charset=([\w-]+)', content_type, re.IGNORECASE)
177
  html_processing_encoding = charset_match.group(1) if charset_match else 'utf-8'
178
 
179
- # محاسبه base href
180
- effective_url_for_base_tag_calc = target_response.url # URL نهایی پس از ریدایرکت‌های داخلی requests
181
- parsed_eff_url_for_base_tag = urlparse(effective_url_for_base_tag_calc)
182
- path_for_base_tag_calc = parsed_eff_url_for_base_tag.path.rstrip('/') + '/' if parsed_eff_url_for_base_tag.path else "/"
183
- # اگر آخرین بخش مسیر یک فایل است، از دایرکتوری والد استفاده کن
184
- if '.' in os.path.basename(parsed_eff_url_for_base_tag.path.rstrip('/')) and path_for_base_tag_calc != '/':
185
- path_for_base_tag_calc = os.path.dirname(path_for_base_tag_calc.rstrip('/')) + '/'
186
- if path_for_base_tag_calc == "//": path_for_base_tag_calc = "/" # اصلاح حالت //
187
-
188
- calculated_final_base_href = urljoin(f"{parsed_eff_url_for_base_tag.scheme}://{parsed_eff_url_for_base_tag.netloc}", path_for_base_tag_calc)
189
- escaped_href_for_final_base_tag = calculated_final_base_href.replace('"', '"') # escape کردن "
190
- base_tag_html_to_inject = f'<base href="{escaped_href_for_final_base_tag}">'
191
-
192
- # آماده‌سازی اسکریپت جاوا اسکریپت
193
- js_replacements_json = json.dumps(SIMPLE_TEXT_REPLACEMENTS)
194
- mutation_observer_script = f"""
195
- <script id="grProxyTextReplacerScript">
196
- (function() {{
197
- if (document.getElementById('grProxyTextReplacerScriptLoaded')) return; // جلوگیری از اجرای مجدد
198
- const el = document.createElement('div');
199
- el.id = 'grProxyTextReplacerScriptLoaded';
200
- el.style.display = 'none';
201
- document.body.appendChild(el);
202
-
203
- const replacements = {js_replacements_json};
204
-
205
- function escapeRegExp(string) {{
206
- return string.replace(/[.*+?^${{}}()|[\]\\\\]/g, '\\\\$&');
207
- }}
208
-
209
- function applyReplacementsToNode(node) {{
210
- if (!node) return;
211
-
212
- if (node.nodeType === Node.ELEMENT_NODE) {{
213
- for (let i = 0; i < node.childNodes.length; i++) {{
214
- applyReplacementsToNode(node.childNodes[i]);
215
- }}
216
- }} else if (node.nodeType === Node.TEXT_NODE) {{
217
- if (!node.nodeValue || node.nodeValue.trim() === '') return;
218
-
219
- let textContent = node.nodeValue;
220
- let textChangedInLoop = false;
221
-
222
- for (const originalKey in replacements) {{
223
- const replacementValue = replacements[originalKey];
224
- const regex = new RegExp(escapeRegExp(originalKey), 'gi');
225
-
226
- if (regex.test(textContent)) {{
227
- if (replacementValue === "" && node.parentElement && node.parentElement.tagName !== 'BODY') {{
228
- // اگر متن اصلی گره فقط یا عمدتا شامل کلید اصلی بود، والد را مخفی کن
229
- let normalizedNodeText = node.nodeValue.trim().toLowerCase();
230
- let normalizedOriginalKey = originalKey.trim().toLowerCase();
231
- // این شرط را ساده‌تر می‌کنیم: اگر کلید در متن بود و جایگزین خالی بود، والد را مخفی کن
232
- if (normalizedNodeText.includes(normalizedOriginalKey)) {{
233
- node.parentElement.classList.add('gr-proxy-item-hidden-by-text');
234
- }}
235
- // متن را نیز خالی می‌کنیم
236
- textContent = textContent.replace(regex, '');
237
- textChangedInLoop = true;
238
- }} else if (replacementValue !== "") {{
239
- textContent = textContent.replace(regex, replacementValue);
240
- textChangedInLoop = true;
241
- }}
242
- }}
243
- }}
244
-
245
- if (textChangedInLoop) {{
246
- node.nodeValue = textContent;
247
- }}
248
- }}
249
- }}
250
-
251
- function processMutations(mutationsList, observer) {{
252
- applyReplacementsToNode(document.body); // همیشه کل body را بررسی کن
253
- }}
254
 
255
- const observerConfig = {{
256
- childList: true,
257
- subtree: true,
258
- characterData: true
259
- }};
260
 
261
- const observer = new MutationObserver(processMutations);
262
-
263
- function initialLoadAndObserve() {{
264
- if (document.body) {{
265
- applyReplacementsToNode(document.body);
266
- observer.observe(document.body, observerConfig);
267
- }} else {{
268
- setTimeout(initialLoadAndObserve, 50);
269
- }}
270
- }}
271
-
272
- if (document.readyState === 'loading') {{
273
- document.addEventListener('DOMContentLoaded', initialLoadAndObserve);
274
- }} else {{
275
- initialLoadAndObserve();
276
- }}
277
- }})();
278
- </script>
279
- """
280
  try:
281
  for chunk_data in target_response.iter_content(chunk_size=8192): # chunk_size می‌تواند تنظیم شود
282
  if not chunk_data: continue
283
 
284
  if is_html_content_type:
285
  current_buffer += chunk_data
286
- # سعی در decode کردن chunk های بزرگتر برای پیدا کردن تگ head/body
287
- # یک حد آستانه برای پردازش بافر فعلی (مثلاً 10KB)
288
- if len(current_buffer) < 10240 and not (b"</head>" in current_buffer or b"</body>" in current_buffer or b"<body" in current_buffer):
289
- # اگر چانک کوچک است و تگ‌های پایانی مهم نیامده‌اند، ادامه بده تا بافر بزرگتر شود
290
- # مگر اینکه این آخرین چانک باشد (که در خارج از حلقه مدیریت می‌شود)
291
- continue
292
-
293
- try:
294
- buffer_as_string = current_buffer.decode(html_processing_encoding, errors='replace')
295
- except UnicodeDecodeError: # Fallback encoding
296
- buffer_as_string = current_buffer.decode('latin-1', errors='replace')
297
-
298
- processed_chunk_string = buffer_as_string
299
-
300
- # تزریق base tag, CSS و JS فقط یکبار در ابتدای head
301
- if not css_and_js_injected_flag: # این فلگ برای CSS و JS با هم است
302
- head_tag_match = re.search(r'(<head\b[^>]*>)', processed_chunk_string, re.IGNORECASE)
303
- if head_tag_match:
304
- insert_position = head_tag_match.end(1)
305
- injection_payload = ""
306
- if not base_tag_injected_flag:
307
- injection_payload += base_tag_html_to_inject
308
- base_tag_injected_flag = True
309
- injection_payload += GRADIO_HIDE_CSS_STANDARD
310
- # اسکریپت JS را اینجا تزریق نمی‌کنیم، به انتهای body می‌رود
311
-
312
- processed_chunk_string = processed_chunk_string[:insert_position] + injection_payload + processed_chunk_string[insert_position:]
313
- # css_and_js_injected_flag = True # فقط CSS تزریق شد، JS بعدا
314
- # فلگ را فعلا True می‌کنیم تا CSS دوباره تزریق نشود.
315
- # یا دو فلگ جدا برای CSS و JS داشته باشیم. بهتر است.
316
- # css_injected_flag = True (جدید)
317
- # js_injected_flag = False (جدید)
318
- # اینجا چون css_and_js_injected_flag برای هر دو است، آن را True می‌کنیم
319
- # و JS را در جای دیگری مدیریت می‌کنیم
320
- css_and_js_injected_flag = True # فرض می‌کنیم JS را هم اینجا میخواهیم تزریق کنیم برای سادگی
321
- # processed_chunk_string = processed_chunk_string[:insert_position] + injection_payload + mutation_observer_script + processed_chunk_string[insert_position:]
322
-
323
- # پردازش HTML اولیه سمت سرور (حذف‌ها)
324
- processed_chunk_string = process_html_content_server_side(processed_chunk_string)
325
-
326
- # جایگزینی‌های اولیه متن سمت سرور (اینها توسط JS هم انجام خواهند شد)
327
- temp_string_for_replacement = processed_chunk_string
328
- for original_word, replacement_text in SIMPLE_TEXT_REPLACEMENTS.items():
329
- if replacement_text == "": continue # جایگزینی با خالی توسط JS و CSS انجام می‌شود
330
- try:
331
- pattern = re.compile(r'\b' + re.escape(original_word) + r'\b', re.IGNORECASE)
332
- new_string, num_replacements = pattern.subn(replacement_text, temp_string_for_replacement)
333
- if num_replacements > 0:
334
- temp_string_for_replacement = new_string
335
- except re.error: # اگر re.escape کافی نبود
336
- pass
337
- processed_chunk_string = temp_string_for_replacement
338
-
339
- # تزریق اسکریپت JS به انتهای body (اگر پیدا شد)
340
- body_end_match = re.search(r'(</body>)', processed_chunk_string, re.IGNORECASE)
341
- if body_end_match:
342
- insert_pos = body_end_match.start(1)
343
- processed_chunk_string = processed_chunk_string[:insert_pos] + mutation_observer_script + processed_chunk_string[insert_pos:]
344
- # اینجا می‌توانیم یک فلگ js_injected_flag=True قرار دهیم
345
-
346
- yield processed_chunk_string.encode(html_processing_encoding, errors='replace')
347
- current_buffer = b'' # خالی کردن بافر پس از پردازش
348
-
349
  else: # محتوای غیر HTML
350
- if current_buffer: # اگر بافری از قبل مانده (نباید اتفاق بیفتد برای غیر HTML)
351
- yield current_buffer
352
- current_buffer = b''
353
  yield chunk_data
354
 
355
  # پردازش باقیمانده بافر (اگر چیزی مانده)
356
  if current_buffer:
357
  if is_html_content_type:
358
- try:
359
- buffer_as_string = current_buffer.decode(html_processing_encoding, errors='replace')
360
- except UnicodeDecodeError:
361
- buffer_as_string = current_buffer.decode('latin-1', errors='replace')
362
-
363
- modified_buffer_string = buffer_as_string
364
-
365
- # تزریق نهایی CSS و base tag اگر هنوز انجام نشده (برای فایل‌های کوچک یا آخرین chunk)
366
- if not css_and_js_injected_flag: # یا فلگ جداگانه css_injected_flag
367
- head_tag_match = re.search(r'(<head\b[^>]*>)', modified_buffer_string, re.IGNORECASE)
368
- injection_payload = ""
369
- if not base_tag_injected_flag:
370
- injection_payload += base_tag_html_to_inject
371
- injection_payload += GRADIO_HIDE_CSS_STANDARD
372
-
373
- if head_tag_match:
374
- insert_position = head_tag_match.end(1)
375
- modified_buffer_string = modified_buffer_string[:insert_position] + injection_payload + modified_buffer_string[insert_position:]
376
- else: # اگر head نبود، یک head بساز (ساده)
377
- html_tag_match = re.search(r'(<html\b[^>]*>)', modified_buffer_string, re.IGNORECASE)
378
- if html_tag_match:
379
- insert_pos = html_tag_match.end(1)
380
- modified_buffer_string = modified_buffer_string[:insert_pos] + f"<head>{injection_payload}</head>" + modified_buffer_string[insert_pos:]
381
- else: # اگر تگ html هم نبود، به ابتدای همه چیز اضافه کن
382
- modified_buffer_string = f"<head>{injection_payload}</head>" + modified_buffer_string
383
- css_and_js_injected_flag = True # یا css_injected_flag = True
384
-
385
- modified_buffer_string = process_html_content_server_side(modified_buffer_string)
386
-
387
- temp_string_for_replacement = modified_buffer_string
388
- for original_word, replacement_text in SIMPLE_TEXT_REPLACEMENTS.items():
389
- if replacement_text == "": continue
390
- try:
391
- pattern = re.compile(r'\b' + re.escape(original_word) + r'\b', re.IGNORECASE)
392
- new_string, num_replacements = pattern.subn(replacement_text, temp_string_for_replacement)
393
- if num_replacements > 0: temp_string_for_replacement = new_string
394
- except re.error:
395
- pass
396
- modified_buffer_string = temp_string_for_replacement
397
-
398
- # تزریق نهایی اسکریپت JS (اگر هنوز تزریق نشده یا body end پیدا نشده بود)
399
- if not re.search(r'<script id="grProxyTextReplacerScript">', modified_buffer_string, re.IGNORECASE):
400
- body_end_match = re.search(r'(</body>)', modified_buffer_string, re.IGNORECASE)
401
- if body_end_match:
402
- insert_pos = body_end_match.start(1)
403
- modified_buffer_string = modified_buffer_string[:insert_pos] + mutation_observer_script + modified_buffer_string[insert_pos:]
404
- else: # اگر تگ پایانی body پیدا نشد، به آخر همه چیز اضافه می‌کنیم
405
- modified_buffer_string += mutation_observer_script
406
-
407
- yield modified_buffer_string.encode(html_processing_encoding, errors='replace')
408
- else: # غیر HTML
409
  yield current_buffer
410
  except Exception as stream_err:
411
  app.logger.error(f"Error during HTML/Stream processing for {target_full_url}: {stream_err}")
412
- # اگر خطایی در پردازش استریم رخ داد، ممکن است بخواهیم بقیه محتوای خام را ارسال کنیم
413
- # یا یک پیام خطا به کلاینت بفرستیم. برای سادگی، اینجا چیزی yield نمی‌کنیم.
414
  finally:
415
  target_response.close()
416
 
417
-
418
  if request.method == 'HEAD':
419
  return Response(response=None, status=target_response.status_code, headers=final_response_headers)
420
  else:
@@ -423,26 +247,25 @@ def proxy_request_handler(path):
423
  except requests.exceptions.Timeout:
424
  app.logger.error(f"Timeout ({REQUEST_TIMEOUT}s) fetching {target_full_url}")
425
  return "Error: Request to target site timed out.", 504
426
- except requests.exceptions.HTTPError as http_err:
427
- app.logger.error(f"HTTPError {http_err.response.status_code} from {target_full_url}. URL: {http_err.response.url}. Resp: {http_err.response.text[:200]}")
428
- error_resp_headers = {k:v for k,v in http_err.response.headers.items() if k.lower() not in ['content-encoding', 'transfer-encoding', 'content-length']}
429
- return Response(http_err.response.content, status=http_err.response.status_code, headers=error_resp_headers)
430
  except requests.exceptions.ConnectionError as conn_err:
431
  app.logger.error(f"ConnectionError for {target_full_url}: {conn_err}")
432
- return f"Error: Could not connect to target. Details: {conn_err}", 502
433
- except requests.exceptions.RequestException as req_err:
434
  app.logger.error(f"RequestException for {target_full_url}: {req_err}")
435
- return f"Error fetching content: {req_err}", 502
436
- except Exception as general_err:
437
- app.logger.exception(f"Unexpected Python error proxying {target_full_url}") # .exception برای لاگ کردن traceback
438
- return f"Unexpected server error: {str(general_err)}", 500
439
 
440
  if __name__ == '__main__':
441
- port = int(os.environ.get("PORT", 7860))
442
- flask_env = os.environ.get('FLASK_ENV', 'production')
443
- # برای دیباگ محلی، می‌توانید debug_mode را True کنید
444
- debug_mode = flask_env == 'development' or os.environ.get('FLASK_DEBUG', '0') == '1'
445
-
446
- print(f"INFO: Starting Flask app on host 0.0.0.0, port {port}, debug: {debug_mode}. App Log Level: {logging.getLevelName(app.logger.getEffectiveLevel())}, Werkzeug Log Level: {logging.getLevelName(log.getEffectiveLevel())}")
447
 
 
448
  app.run(host='0.0.0.0', port=port, debug=debug_mode)
 
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 = """
 
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:
 
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)