Spaces:
BG5
/
Running

BG5 commited on
Commit
87b9de2
·
verified ·
1 Parent(s): acaad5f

Update fun.py

Browse files
Files changed (1) hide show
  1. fun.py +219 -32
fun.py CHANGED
@@ -1,25 +1,209 @@
1
- from fastapi import FastAPI, Request, Response, WebSocket, WebSocketDisconnect
2
  import httpx
3
  import uvicorn
4
  import asyncio
5
  import websockets
6
- import logging
7
  import json
 
 
 
 
 
 
 
 
8
 
9
  app = FastAPI()
10
 
11
- TARGET_BASE = "http://127.0.0.1:9222" # Chrome DevTools HTTP 地址
12
- TARGET_WS_BASE = "ws://127.0.0.1:9222" # Chrome DevTools WebSocket 地址
 
 
 
13
 
14
- # 配置日志
15
- logging.basicConfig(level=logging.INFO)
16
- logger = logging.getLogger(__name__)
 
 
 
 
17
 
18
- @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
19
- async def proxy(request: Request, path: str):
20
- url = f"{TARGET_BASE}/{path}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  headers = dict(request.headers)
22
- headers["host"] = "127.0.0.1" # 强制修改 Host 头
23
  body = await request.body()
24
  async with httpx.AsyncClient(follow_redirects=True) as client:
25
  resp = await client.request(
@@ -34,49 +218,52 @@ async def proxy(request: Request, path: str):
34
  status_code=resp.status_code,
35
  headers=dict(resp.headers)
36
  )
37
- @app.websocket("/{path:path}")
38
- async def websocket_proxy(websocket: WebSocket, path: str):
 
 
 
 
 
 
 
 
 
 
39
  await websocket.accept()
40
- target_url = f"{TARGET_WS_BASE}/{path}"
 
 
41
  logger.info(f"Forwarding to: {target_url}")
42
-
43
  try:
44
  async with websockets.connect(target_url) as target_ws:
45
- # # 1. 先发送初始化命令(必需)
46
- # await target_ws.send(json.dumps({
47
- # "id": 1,
48
- # "method": "Runtime.enable"
49
- # }))
50
- # logger.info("Sent Runtime.enable")
51
-
52
- # 2. 启动转发任务
53
  async def forward_client_to_server():
54
  try:
55
  while True:
56
  data = await websocket.receive_text()
57
- logger.info(f"CLIENT->CHROME: {data[:200]}")
58
  await target_ws.send(data)
59
  except WebSocketDisconnect:
60
- logger.info("Client disconnected")
61
-
62
  async def forward_server_to_client():
63
  try:
64
  while True:
65
  response = await target_ws.recv()
66
- logger.info(f"CHROME->CLIENT: {response[:200]}")
67
  await websocket.send_text(response)
68
  except websockets.exceptions.ConnectionClosed:
69
- logger.info("Browser disconnected")
70
-
71
  await asyncio.gather(
72
  forward_client_to_server(),
73
  forward_server_to_client()
74
  )
75
  except Exception as e:
76
- logger.error(f"Error: {e}")
77
  finally:
78
  await websocket.close()
79
 
80
-
 
81
  if __name__ == "__main__":
82
- uvicorn.run("fun:app", host="0.0.0.0", port=8000, reload=True)
 
1
+ from fastapi import FastAPI, Request, Response, WebSocket, WebSocketDisconnect, Query
2
  import httpx
3
  import uvicorn
4
  import asyncio
5
  import websockets
 
6
  import json
7
+ import subprocess
8
+ import threading
9
+ import time
10
+ import os
11
+ import random
12
+ from typing import Dict
13
+ from loguru import logger
14
+ from starlette.responses import StreamingResponse
15
 
16
  app = FastAPI()
17
 
18
+ # 浏览器实例管理
19
+ BROWSERS: Dict[str, dict] = {} # {browser_id: {"process": Popen, "port": int, "status": str}}
20
+ PORT_RANGE = (9300, 9700)
21
+ CHROME_PATH = "google-chrome" # Linux下可用命令,或自定义绝对路径
22
+ PROFILE_BASE = "/tmp/profiles"
23
 
24
+ def get_free_port():
25
+ used_ports = {b["port"] for b in BROWSERS.values()}
26
+ for _ in range(100):
27
+ port = random.randint(*PORT_RANGE)
28
+ if port not in used_ports:
29
+ return port
30
+ raise RuntimeError("No free port available")
31
 
32
+ def gen_browser_id():
33
+ return str(int(time.time() * 1000)) + str(random.randint(1000, 9999))
34
+
35
+ def get_default_launch_args():
36
+ return [
37
+ "--disable-gpu",
38
+ "--disable-dev-shm-usage",
39
+ "--disable-software-rasterizer",
40
+ "--disable-extensions",
41
+ "--disable-background-networking",
42
+ "--disable-default-apps",
43
+ "--disable-sync",
44
+ "--disable-translate",
45
+ "--disable-features=TranslateUI",
46
+ "--no-first-run",
47
+ "--no-default-browser-check",
48
+ "--remote-allow-origins=*"
49
+ ]
50
+
51
+ # BitBrowser风格API
52
+ @app.post("/browser/open")
53
+ async def open_browser(request: Request):
54
+ data = await request.json()
55
+ browser_id = data.get("id") or gen_browser_id()
56
+ if browser_id in BROWSERS:
57
+ return {"code": 0, "msg": "already opened", "data": {"id": browser_id, "port": BROWSERS[browser_id]["port"]}}
58
+ port = get_free_port()
59
+ profile_dir = f"{PROFILE_BASE}/{port}"
60
+ os.makedirs(profile_dir, exist_ok=True)
61
+ # 支持自定义启动参数并去重
62
+ launch_args = data.get("launchArgs", "")
63
+ args = [CHROME_PATH, f"--remote-debugging-port={port}", f"--user-data-dir={profile_dir}"]
64
+ # 加入默认参数
65
+ args.extend(get_default_launch_args())
66
+ if launch_args:
67
+ # 拆分参数并去重,保留顺序
68
+ base_args = args[1:] # 除去chrome可执行文件
69
+ extra_args = [a for a in launch_args.split() if a]
70
+ all_args = base_args + extra_args
71
+ seen = set()
72
+ deduped_args = []
73
+ for arg in all_args:
74
+ key = arg.split('=')[0] if '=' in arg else arg
75
+ if key not in seen:
76
+ seen.add(key)
77
+ deduped_args.append(arg)
78
+ args = [CHROME_PATH] + deduped_args
79
+ proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
80
+ BROWSERS[browser_id] = {"process": proc, "port": port, "status": "open"}
81
+ return {"code": 0, "msg": "success", "data": {"id": browser_id, "port": port}}
82
+
83
+ @app.post("/browser/close")
84
+ async def close_browser(request: Request):
85
+ data = await request.json()
86
+ browser_id = data.get("id")
87
+ b = BROWSERS.get(browser_id)
88
+ if not b:
89
+ return {"code": 1, "msg": "not found"}
90
+ b["process"].terminate()
91
+ b["status"] = "closed"
92
+ return {"code": 0, "msg": "closed", "data": {"id": browser_id}}
93
+
94
+ @app.post("/browser/delete")
95
+ async def delete_browser(request: Request):
96
+ data = await request.json()
97
+ browser_id = data.get("id")
98
+ b = BROWSERS.pop(browser_id, None)
99
+ if not b:
100
+ return {"code": 1, "msg": "not found"}
101
+ try:
102
+ b["process"].terminate()
103
+ except Exception:
104
+ pass
105
+ # 删除用户数据目录
106
+ profile_dir = f"{PROFILE_BASE}/{b['port']}"
107
+ if os.path.exists(profile_dir):
108
+ import shutil
109
+ shutil.rmtree(profile_dir, ignore_errors=True)
110
+ return {"code": 0, "msg": "deleted", "data": {"id": browser_id}}
111
+
112
+ @app.post("/browser/update")
113
+ async def update_browser(request: Request):
114
+ data = await request.json()
115
+ # 兼容 BitBrowser 结构
116
+ launch_args = ""
117
+ if isinstance(data.get("browserFingerPrint"), dict):
118
+ launch_args = data["browserFingerPrint"].get("launchArgs", "")
119
+ elif "launchArgs" in data:
120
+ launch_args = data["launchArgs"]
121
+ # 合并 launchArgs 到 data
122
+ if launch_args:
123
+ data["launchArgs"] = launch_args
124
+ return await open_browser(Request({**request.scope, "body": json.dumps(data).encode()}, receive=request._receive))
125
+
126
+ @app.post("/browser/ports")
127
+ async def browser_ports():
128
+ # 返回 {id: port} 格��
129
+ data = {k: str(v["port"]) for k, v in BROWSERS.items() if v["status"] == "open"}
130
+ return {"success": True, "data": data}
131
+
132
+ @app.post("/health")
133
+ async def health():
134
+ return {"code": 0, "msg": "ok"}
135
+
136
+
137
+ CDP_PATHS = [
138
+ "/json",
139
+ "/json/list",
140
+ "/json/version",
141
+ "/json/protocol",
142
+ ]
143
+
144
+ def get_browser_by_id(browser_id: str):
145
+ b = BROWSERS.get(browser_id)
146
+ if not b or b["status"] != "open":
147
+ return None
148
+ return b
149
+
150
+
151
+ async def find_browser_by_target_id(target_id: str):
152
+ """
153
+ 根据 targetId 智能查找对应的浏览器实例
154
+
155
+ 1. 检查 target_id 是否是一个 browser_id
156
+ 2. 如果不是,查询所有浏览器实例的页面列表,找到匹配 target_id 的页面所在浏览器
157
+ 3. 如果找不到,返回第一个可用的浏览器实例
158
+ """
159
+ # 检查是否直接是 browser_id
160
+ if target_id in BROWSERS:
161
+ return BROWSERS[target_id]
162
+
163
+ # 查询所有浏览器实例,找到匹配的页面
164
+ for browser_id, browser in BROWSERS.items():
165
+ if browser["status"] != "open":
166
+ continue
167
+ try:
168
+ # 尝试获取浏览器的页面列表
169
+ port = browser["port"]
170
+ async with httpx.AsyncClient() as client:
171
+ resp = await client.get(f"http://127.0.0.1:{port}/json/list")
172
+ if resp.status_code == 200:
173
+ pages = resp.json()
174
+ # 检查页面是否包含 target_id
175
+ for page in pages:
176
+ if page.get("id") == target_id:
177
+ return browser
178
+ except Exception as e:
179
+ logger.error(f"查询浏览器 {browser_id} 页面列表失败: {e}")
180
+
181
+ # 如果找不到,返回第一个可用的浏览器实例
182
+ for browser_id, browser in BROWSERS.items():
183
+ if browser["status"] == "open":
184
+ return browser
185
+
186
+ return None
187
+ @app.api_route("/json", methods=["GET"])
188
+ @app.api_route("/json/list", methods=["GET"])
189
+ @app.api_route("/json/version", methods=["GET"])
190
+ @app.api_route("/json/protocol", methods=["GET"])
191
+ async def cdp_native_proxy(request: Request):
192
+ # 这个接口只返回第一个浏览器实例的内容
193
+ # 1. 获取第一个浏览器实例
194
+ if not BROWSERS:
195
+ return Response(content="No browser instance", status_code=404)
196
+ browser_id = next(iter(BROWSERS.keys()))
197
+ b = get_browser_by_id(browser_id)
198
+ if not b:
199
+ return Response(content="browser not found", status_code=404)
200
+ port = b["port"]
201
+ # 2. 构造目标URL
202
+ path = request.url.path
203
+ url = f"http://127.0.0.1:{port}{path}"
204
+ # 3. 代理请求
205
  headers = dict(request.headers)
206
+ headers["host"] = "127.0.0.1"
207
  body = await request.body()
208
  async with httpx.AsyncClient(follow_redirects=True) as client:
209
  resp = await client.request(
 
218
  status_code=resp.status_code,
219
  headers=dict(resp.headers)
220
  )
221
+
222
+ @app.websocket("/devtools/{tab_type}/{target_id}")
223
+ async def cdp_native_ws_proxy(websocket: WebSocket, tab_type: str, target_id: str):
224
+ if not target_id:
225
+ await websocket.close()
226
+ return
227
+ # 智能查找 target_id 对应的浏览器实例
228
+ browser = await find_browser_by_target_id(target_id)
229
+ if not browser:
230
+ await websocket.close(code=1008, reason="无法找到有效的浏览器实例")
231
+ return
232
+
233
  await websocket.accept()
234
+ tab_type = "page" if tab_type == "page" else "browser"
235
+ port = browser["port"]
236
+ target_url = f"ws://127.0.0.1:{port}/devtools/{tab_type}/{target_id}"
237
  logger.info(f"Forwarding to: {target_url}")
238
+
239
  try:
240
  async with websockets.connect(target_url) as target_ws:
 
 
 
 
 
 
 
 
241
  async def forward_client_to_server():
242
  try:
243
  while True:
244
  data = await websocket.receive_text()
 
245
  await target_ws.send(data)
246
  except WebSocketDisconnect:
247
+ pass
248
+
249
  async def forward_server_to_client():
250
  try:
251
  while True:
252
  response = await target_ws.recv()
 
253
  await websocket.send_text(response)
254
  except websockets.exceptions.ConnectionClosed:
255
+ pass
256
+
257
  await asyncio.gather(
258
  forward_client_to_server(),
259
  forward_server_to_client()
260
  )
261
  except Exception as e:
262
+ logger.error(f"WebSocket代理错误: {e}")
263
  finally:
264
  await websocket.close()
265
 
266
+
267
+
268
  if __name__ == "__main__":
269
+ uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True)