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 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__")
... ...