Commit b3ffdc72967001c04217b9f9e4c172e71be19668

Authored by tangwang
1 parent 89fa3f3c

Sync legacy frontend entrypoint from 0a440fb

Showing 1 changed file with 7 additions and 271 deletions   Show diff stats
scripts/frontend_server.py 100755 → 100644
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 -"""  
3 -Simple HTTP server for saas-search frontend.  
4 -""" 2 +"""Backward-compatible frontend server entrypoint."""
5 3
6 -import http.server  
7 -import socketserver  
8 -import os  
9 -import sys  
10 -import logging  
11 -import time  
12 -import urllib.request  
13 -import urllib.error  
14 -from collections import defaultdict, deque  
15 -from pathlib import Path  
16 -from dotenv import load_dotenv  
17 -  
18 -# Load .env file  
19 -project_root = Path(__file__).parent.parent  
20 -load_dotenv(project_root / '.env')  
21 -  
22 -# Get API_BASE_URL from environment(默认不注入,避免被旧 .env 覆盖同源策略)  
23 -# 仅当显式设置 FRONTEND_INJECT_API_BASE_URL=1 时才注入 window.API_BASE_URL。  
24 -API_BASE_URL = os.getenv('API_BASE_URL') or None  
25 -INJECT_API_BASE_URL = os.getenv('FRONTEND_INJECT_API_BASE_URL', '0') == '1'  
26 -# Backend proxy target for same-origin API forwarding  
27 -BACKEND_PROXY_URL = os.getenv('BACKEND_PROXY_URL', 'http://127.0.0.1:6002').rstrip('/')  
28 -  
29 -# Change to frontend directory  
30 -frontend_dir = os.path.join(os.path.dirname(__file__), '../frontend')  
31 -os.chdir(frontend_dir)  
32 -  
33 -# FRONTEND_PORT is the canonical config; keep PORT as a secondary fallback.  
34 -PORT = int(os.getenv('FRONTEND_PORT', os.getenv('PORT', 6003)))  
35 -  
36 -# Configure logging to suppress scanner noise  
37 -logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')  
38 -  
39 -class RateLimitingMixin:  
40 - """Mixin for rate limiting requests by IP address."""  
41 - request_counts = defaultdict(deque)  
42 - rate_limit = 100 # requests per minute  
43 - window = 60 # seconds  
44 -  
45 - @classmethod  
46 - def is_rate_limited(cls, ip):  
47 - now = time.time()  
48 -  
49 - # Clean old requests  
50 - while cls.request_counts[ip] and cls.request_counts[ip][0] < now - cls.window:  
51 - cls.request_counts[ip].popleft()  
52 -  
53 - # Check rate limit  
54 - if len(cls.request_counts[ip]) > cls.rate_limit:  
55 - return True  
56 -  
57 - cls.request_counts[ip].append(now)  
58 - return False  
59 -  
60 -class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler, RateLimitingMixin):  
61 - """Custom request handler with CORS support and robust error handling."""  
62 -  
63 - def _is_proxy_path(self, path: str) -> bool:  
64 - """Return True for API paths that should be forwarded to backend service."""  
65 - return path.startswith('/search/') or path.startswith('/admin/') or path.startswith('/indexer/')  
66 -  
67 - def _proxy_to_backend(self):  
68 - """Proxy current request to backend service on the GPU server."""  
69 - target_url = f"{BACKEND_PROXY_URL}{self.path}"  
70 - method = self.command.upper()  
71 -  
72 - try:  
73 - content_length = int(self.headers.get('Content-Length', '0'))  
74 - except ValueError:  
75 - content_length = 0  
76 - body = self.rfile.read(content_length) if content_length > 0 else None 4 +from __future__ import annotations
77 5
78 - forward_headers = {}  
79 - for key, value in self.headers.items():  
80 - lk = key.lower()  
81 - if lk in ('host', 'content-length', 'connection'):  
82 - continue  
83 - forward_headers[key] = value  
84 -  
85 - req = urllib.request.Request(  
86 - target_url,  
87 - data=body,  
88 - headers=forward_headers,  
89 - method=method,  
90 - )  
91 -  
92 - try:  
93 - with urllib.request.urlopen(req, timeout=30) as resp:  
94 - resp_body = resp.read()  
95 - self.send_response(resp.getcode())  
96 - for header, value in resp.getheaders():  
97 - lh = header.lower()  
98 - if lh in ('transfer-encoding', 'connection', 'content-length'):  
99 - continue  
100 - self.send_header(header, value)  
101 - self.end_headers()  
102 - self.wfile.write(resp_body)  
103 - except urllib.error.HTTPError as e:  
104 - err_body = e.read() if hasattr(e, 'read') else b''  
105 - self.send_response(e.code)  
106 - if e.headers:  
107 - for header, value in e.headers.items():  
108 - lh = header.lower()  
109 - if lh in ('transfer-encoding', 'connection', 'content-length'):  
110 - continue  
111 - self.send_header(header, value)  
112 - self.end_headers()  
113 - if err_body:  
114 - self.wfile.write(err_body)  
115 - except Exception as e:  
116 - logging.error(f"Backend proxy error for {method} {self.path}: {e}")  
117 - self.send_response(502)  
118 - self.send_header('Content-Type', 'application/json; charset=utf-8')  
119 - self.end_headers()  
120 - self.wfile.write(b'{"error":"Bad Gateway: backend proxy failed"}')  
121 -  
122 - def do_GET(self):  
123 - """Handle GET requests with API config injection."""  
124 - path = self.path.split('?')[0]  
125 -  
126 - # Proxy API paths to backend first  
127 - if self._is_proxy_path(path):  
128 - self._proxy_to_backend()  
129 - return  
130 -  
131 - # Route / to index.html  
132 - if path == '/' or path == '':  
133 - self.path = '/index.html' + (self.path.split('?', 1)[1] if '?' in self.path else '')  
134 -  
135 - # Inject API config for HTML files  
136 - if self.path.endswith('.html'):  
137 - self._serve_html_with_config()  
138 - else:  
139 - super().do_GET()  
140 -  
141 - def _serve_html_with_config(self):  
142 - """Serve HTML with optional API_BASE_URL injected."""  
143 - try:  
144 - file_path = self.path.lstrip('/')  
145 - if not os.path.exists(file_path):  
146 - self.send_error(404)  
147 - return  
148 -  
149 - with open(file_path, 'r', encoding='utf-8') as f:  
150 - html = f.read()  
151 -  
152 - # 默认不注入 API_BASE_URL,避免历史 .env(如 http://xx:6002)覆盖同源调用。  
153 - # 仅当 FRONTEND_INJECT_API_BASE_URL=1 且 API_BASE_URL 有值时才注入。  
154 - if INJECT_API_BASE_URL and API_BASE_URL:  
155 - config_script = f'<script>window.API_BASE_URL="{API_BASE_URL}";</script>\n '  
156 - html = html.replace('<script src="/static/js/app.js', config_script + '<script src="/static/js/app.js', 1)  
157 -  
158 - self.send_response(200)  
159 - self.send_header('Content-Type', 'text/html; charset=utf-8')  
160 - self.end_headers()  
161 - self.wfile.write(html.encode('utf-8'))  
162 - except Exception as e:  
163 - logging.error(f"Error serving HTML: {e}")  
164 - self.send_error(500)  
165 -  
166 - def do_POST(self):  
167 - """Handle POST requests. Proxy API requests to backend."""  
168 - path = self.path.split('?')[0]  
169 - if self._is_proxy_path(path):  
170 - self._proxy_to_backend()  
171 - return  
172 - self.send_error(405, "Method Not Allowed")  
173 -  
174 - def setup(self):  
175 - """Setup with error handling."""  
176 - try:  
177 - super().setup()  
178 - except Exception:  
179 - pass # Silently handle setup errors from scanners  
180 -  
181 - def handle_one_request(self):  
182 - """Handle single request with error catching."""  
183 - try:  
184 - # Check rate limiting  
185 - client_ip = self.client_address[0]  
186 - if self.is_rate_limited(client_ip):  
187 - logging.warning(f"Rate limiting IP: {client_ip}")  
188 - self.send_error(429, "Too Many Requests")  
189 - return  
190 -  
191 - super().handle_one_request()  
192 - except (ConnectionResetError, BrokenPipeError):  
193 - # Client disconnected prematurely - common with scanners  
194 - pass  
195 - except UnicodeDecodeError:  
196 - # Binary data received - not HTTP  
197 - pass  
198 - except Exception as e:  
199 - # Log unexpected errors but don't crash  
200 - logging.debug(f"Request handling error: {e}")  
201 -  
202 - def log_message(self, format, *args):  
203 - """Suppress logging for malformed requests from scanners."""  
204 - message = format % args  
205 - # Filter out scanner noise  
206 - noise_patterns = [  
207 - "code 400",  
208 - "Bad request",  
209 - "Bad request version",  
210 - "Bad HTTP/0.9 request type",  
211 - "Bad request syntax"  
212 - ]  
213 - if any(pattern in message for pattern in noise_patterns):  
214 - return  
215 - # Only log legitimate requests  
216 - if message and not message.startswith(" ") and len(message) > 10:  
217 - super().log_message(format, *args)  
218 -  
219 - def end_headers(self):  
220 - # Add CORS headers  
221 - self.send_header('Access-Control-Allow-Origin', '*')  
222 - self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')  
223 - self.send_header('Access-Control-Allow-Headers', 'Content-Type')  
224 - # Add security headers  
225 - self.send_header('X-Content-Type-Options', 'nosniff')  
226 - self.send_header('X-Frame-Options', 'DENY')  
227 - self.send_header('X-XSS-Protection', '1; mode=block')  
228 - super().end_headers()  
229 -  
230 - def do_OPTIONS(self):  
231 - """Handle OPTIONS requests."""  
232 - try:  
233 - path = self.path.split('?')[0]  
234 - if self._is_proxy_path(path):  
235 - self.send_response(204)  
236 - self.end_headers()  
237 - return  
238 - self.send_response(200)  
239 - self.end_headers()  
240 - except Exception:  
241 - pass  
242 -  
243 -class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):  
244 - """Threaded TCP server with better error handling."""  
245 - allow_reuse_address = True  
246 - daemon_threads = True 6 +import runpy
  7 +from pathlib import Path
247 8
248 -if __name__ == '__main__':  
249 - # Check if port is already in use  
250 - import socket  
251 - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
252 - try:  
253 - sock.bind(("", PORT))  
254 - sock.close()  
255 - except OSError:  
256 - print(f"ERROR: Port {PORT} is already in use.")  
257 - print(f"Please stop the existing server or use a different port.")  
258 - print(f"To stop existing server: kill $(lsof -t -i:{PORT})")  
259 - sys.exit(1)  
260 -  
261 - # Create threaded server for better concurrency  
262 - with ThreadedTCPServer(("", PORT), MyHTTPRequestHandler) as httpd:  
263 - print(f"Frontend server started at http://localhost:{PORT}")  
264 - print(f"Serving files from: {os.getcwd()}")  
265 - print("\nPress Ctrl+C to stop the server")  
266 9
267 - try:  
268 - httpd.serve_forever()  
269 - except KeyboardInterrupt:  
270 - print("\nShutting down server...")  
271 - httpd.shutdown()  
272 - print("Server stopped")  
273 - sys.exit(0)  
274 - except Exception as e:  
275 - print(f"Server error: {e}")  
276 - sys.exit(1) 10 +if __name__ == "__main__":
  11 + target = Path(__file__).resolve().parent / "frontend" / "frontend_server.py"
  12 + runpy.run_path(str(target), run_name="__main__")