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)