Alvin3y1 commited on
Commit
955d8ae
·
verified ·
1 Parent(s): 7e67173

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +54 -151
app.py CHANGED
@@ -7,180 +7,83 @@ import logging
7
  from aiohttp import web
8
  from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceServer, RTCConfiguration
9
 
10
- # --- Configuration ---
11
- HOST = "0.0.0.0"
12
- PORT = 7860
13
- VNC_PORT = 5900
14
- DISPLAY_NUM = ":99"
15
-
16
- # Cloudflare TURN to bypass Hugging Face network restrictions
17
- TURN_USER = "g08abe68c81a07f098bb5f0914549bb32440e5aad0b216c7fba2b61e76fd62c6"
18
- TURN_PASS = "aed1a10dd10eba9401ad9d99e5c66036d8a970eab5ba8e6dc9845ab57c771a7d"
19
-
20
  logging.basicConfig(level=logging.INFO)
21
- logger = logging.getLogger("HF-VNC-BRIDGE")
22
-
23
- # --- System Startup ---
24
-
25
- def start_system():
26
- """Starts X11, the custom VNC tool, and the browser."""
27
- os.environ["DISPLAY"] = DISPLAY_NUM
28
-
29
- # 1. Start Xvfb with fixed resolution
30
- logger.info("Starting Xvfb (1280x720)...")
31
- subprocess.Popen([
32
- "Xvfb", DISPLAY_NUM,
33
- "-screen", "0", "1280x720x24",
34
- "-ac", "-noreset"
35
- ])
36
  time.sleep(1)
37
-
38
- # 2. Start the custom compiled minivnc binary
39
- logger.info("Starting custom minivnc tool...")
40
- subprocess.Popen(["/usr/local/bin/minivnc"])
41
  time.sleep(1)
42
-
43
- # 3. Start Window Manager (Matchbox)
44
- subprocess.Popen("matchbox-window-manager -use_titlebar no", shell=True)
45
-
46
- # 4. Start Browser (Opera)
47
- logger.info("Starting Opera...")
48
- opera_cmd = (
49
- "opera "
50
- "--no-sandbox "
51
- "--start-maximized "
52
- "--user-data-dir=/home/user/opera-data "
53
- "--disable-infobars "
54
- "--disable-dev-shm-usage "
55
- "--disable-gpu "
56
- "--no-first-run"
57
- )
58
- subprocess.Popen(opera_cmd, shell=True)
59
-
60
- # --- WebRTC to VNC TCP Bridge ---
61
-
62
- async def bridge_vnc_to_datachannel(channel):
63
- """Bridges binary VNC (RFB) data between TCP and DataChannel."""
64
  try:
65
- # Connect to the local TCP port of minivnc
66
- reader, writer = await asyncio.open_connection('127.0.0.1', VNC_PORT)
67
- logger.info("Bridge connected to local minivnc.")
68
-
69
- # Flag for connection state
70
- closed = asyncio.Event()
71
-
72
- # Task: Local VNC (TCP) -> WebRTC (DataChannel)
73
- async def vnc_to_webrtc():
74
- try:
75
- while not closed.is_set():
76
- # Read large chunks for smoothness
77
- data = await reader.read(65536)
78
- if not data:
79
- break
80
- channel.send(data)
81
- except Exception as e:
82
- logger.error(f"VNC Read Error: {e}")
83
- finally:
84
- closed.set()
85
-
86
- # Task: WebRTC (DataChannel) -> Local VNC (TCP)
87
  @channel.on("message")
88
  def on_message(message):
89
- if isinstance(message, bytes):
90
- # Standard VNC binary data (mouse/keys)
 
91
  writer.write(message)
92
- # Ignore string messages (resize disabled)
93
-
94
- @channel.on("close")
95
- def on_close():
96
- closed.set()
97
 
98
- await vnc_to_webrtc()
99
-
100
  except Exception as e:
101
- logger.error(f"Failed to bridge VNC: {e}")
102
  finally:
103
- if 'writer' in locals():
104
- writer.close()
105
- await writer.wait_closed()
106
-
107
- # --- Web Routes ---
108
-
109
- async def index(request):
110
- """Requirement: The root URL must only show 'hello world'"""
111
- return web.Response(text="hello world")
112
 
113
  async def offer(request):
114
- """Handles WebRTC signaling from the remote client."""
115
- try:
116
- params = await request.json()
117
- offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
118
- except:
119
- return web.Response(status=400)
120
-
121
- # WebRTC Config with STUN and TURN
122
- config = RTCConfiguration(iceServers=[
123
- RTCIceServer(urls=["stun:stun.l.google.com:19302"]),
124
- RTCIceServer(urls=["turns:turn.cloudflare.com:443?transport=tcp",
125
- "turn:turn.cloudflare.com:3478?transport=udp"],
126
- username=TURN_USER, credential=TURN_PASS)
127
- ])
128
 
129
- pc = RTCPeerConnection(config)
130
- pcs.add(pc)
131
-
132
- @pc.on("connectionstatechange")
133
- async def on_state():
134
- if pc.connectionState in ["failed", "closed"]:
135
- await pc.close()
136
- pcs.discard(pc)
137
-
138
  @pc.on("datachannel")
139
  def on_dc(channel):
140
- # Start the TCP bridge when the DataChannel is ready
141
- asyncio.create_task(bridge_vnc_to_datachannel(channel))
142
 
143
- await pc.setRemoteDescription(offer)
144
  answer = await pc.createAnswer()
145
  await pc.setLocalDescription(answer)
146
-
147
  return web.Response(
148
- content_type="application/json",
149
  text=json.dumps({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}),
150
- headers={
151
- "Access-Control-Allow-Origin": "*",
152
- "Access-Control-Allow-Methods": "POST, OPTIONS",
153
- "Access-Control-Allow-Headers": "Content-Type"
154
- }
155
  )
156
 
157
- async def options(request):
158
- """Handle CORS pre-flight requests."""
159
- return web.Response(headers={
160
- "Access-Control-Allow-Origin": "*",
161
- "Access-Control-Allow-Methods": "POST, OPTIONS",
162
- "Access-Control-Allow-Headers": "Content-Type"
163
- })
164
-
165
- # --- Main App ---
166
-
167
- pcs = set()
168
-
169
- async def on_shutdown(app):
170
- coros = [pc.close() for pc in pcs]
171
- await asyncio.gather(*coros)
172
- pcs.clear()
173
-
174
  if __name__ == "__main__":
175
- # Start the X11 and VNC backend
176
- start_system()
177
-
178
- # Start the Web server
179
  app = web.Application()
180
- app.on_shutdown.append(on_shutdown)
181
-
182
- app.router.add_get("/", index)
183
  app.router.add_post("/offer", offer)
184
- app.router.add_options("/offer", options)
185
-
186
  web.run_app(app, host=HOST, port=PORT)
 
7
  from aiohttp import web
8
  from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceServer, RTCConfiguration
9
 
 
 
 
 
 
 
 
 
 
 
10
  logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger("X11-FORWARD-BRIDGE")
12
+
13
+ # Settings
14
+ HOST, PORT = "0.0.0.0", 7860
15
+ DISPLAY = ":99"
16
+ GUACD_PORT = 4822
17
+
18
+ def start_backend():
19
+ """Starts Xvfb, guacd (The Protocol Engine), and the App."""
20
+ os.environ["DISPLAY"] = DISPLAY
21
+ # 1. Start Xvfb
22
+ subprocess.Popen(["Xvfb", DISPLAY, "-screen", "0", "1280x720x24", "-ac"])
 
 
 
23
  time.sleep(1)
24
+ # 2. Start guacd (Converts X11 to binary drawing commands)
25
+ subprocess.Popen(["guacd", "-b", "127.0.0.1", "-l", str(GUACD_PORT)])
 
 
26
  time.sleep(1)
27
+ # 3. Start Browser
28
+ subprocess.Popen(f"opera --no-sandbox --start-maximized", shell=True)
29
+
30
+ async def bridge_guacamole(channel):
31
+ """
32
+ Bridges the Guacamole binary protocol (Drawing Commands)
33
+ directly to the WebRTC DataChannel.
34
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  try:
36
+ # Connect to local Guacamole Proxy
37
+ reader, writer = await asyncio.open_connection('127.0.0.1', GUACD_PORT)
38
+
39
+ # Initial Handshake for Guacamole (X11 Connection)
40
+ # Format: length.value,length.value,...;
41
+ handshake = f"6.select,3.x11;"
42
+ writer.write(handshake.encode())
43
+ await writer.drain()
44
+
45
+ # Pipe Guacamole Binary -> WebRTC
46
+ async def pipe_to_webrtc():
47
+ while True:
48
+ data = await reader.read(16384)
49
+ if not data: break
50
+ channel.send(data)
51
+
52
+ # Pipe WebRTC -> Guacamole Binary
 
 
 
 
 
53
  @channel.on("message")
54
  def on_message(message):
55
+ if isinstance(message, str):
56
+ writer.write(message.encode())
57
+ elif isinstance(message, bytes):
58
  writer.write(message)
 
 
 
 
 
59
 
60
+ await pipe_to_webrtc()
 
61
  except Exception as e:
62
+ logger.error(f"Bridge Error: {e}")
63
  finally:
64
+ writer.close()
 
 
 
 
 
 
 
 
65
 
66
  async def offer(request):
67
+ params = await request.json()
68
+ pc = RTCPeerConnection()
 
 
 
 
 
 
 
 
 
 
 
 
69
 
 
 
 
 
 
 
 
 
 
70
  @pc.on("datachannel")
71
  def on_dc(channel):
72
+ asyncio.create_task(bridge_guacamole(channel))
 
73
 
74
+ await pc.setRemoteDescription(RTCSessionDescription(sdp=params["sdp"], type=params["type"]))
75
  answer = await pc.createAnswer()
76
  await pc.setLocalDescription(answer)
77
+
78
  return web.Response(
79
+ content_type="application/json",
80
  text=json.dumps({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}),
81
+ headers={"Access-Control-Allow-Origin": "*"}
 
 
 
 
82
  )
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  if __name__ == "__main__":
85
+ start_backend()
 
 
 
86
  app = web.Application()
87
+ app.router.add_get("/", lambda r: web.Response(text="hello world"))
 
 
88
  app.router.add_post("/offer", offer)
 
 
89
  web.run_app(app, host=HOST, port=PORT)