33# SPDX-License-Identifier: Apache-2.0
44
55import base64
6+ import fcntl
67import json
78import os
9+ import pty
810import socket
11+ import struct
912import subprocess
1013import sys
14+ import termios
1115import threading
1216
1317
@@ -42,6 +46,11 @@ def validate_env(env_items):
4246 return env
4347
4448
49+ def set_winsize (fd , cols , rows ):
50+ winsize = struct .pack ("HHHH" , rows , cols , 0 , 0 )
51+ fcntl .ioctl (fd , termios .TIOCSWINSZ , winsize )
52+
53+
4554def stream_reader (pipe , frame_type , sock_file , lock ):
4655 try :
4756 while True :
@@ -79,6 +88,8 @@ def stdin_writer(proc, sock_file, sock, lock):
7988 proc .stdin .flush ()
8089 elif kind == "stdin_close" :
8190 break
91+ elif kind == "resize" :
92+ pass
8293 else :
8394 send_frame (
8495 sock_file ,
@@ -96,14 +107,10 @@ def stdin_writer(proc, sock_file, sock, lock):
96107 pass
97108
98109
99- def handle_client (conn ):
100- sock_file = conn . makefile ( "rwb" , buffering = 0 )
110+ def handle_client_pipe (conn , request , sock_file ):
111+ """Handle a client connection using pipes (non-TTY mode)."""
101112 lock = threading .Lock ()
102113 try :
103- request = recv_line (sock_file )
104- if request is None :
105- return
106-
107114 argv = request .get ("argv" ) or ["sh" ]
108115 cwd = request .get ("cwd" )
109116 env = os .environ .copy ()
@@ -153,6 +160,148 @@ def handle_client(conn):
153160 conn .close ()
154161
155162
163+ def handle_client_tty (conn , request , sock_file ):
164+ """Handle a client connection with PTY allocation."""
165+ lock = threading .Lock ()
166+ master_fd = - 1
167+ try :
168+ argv = request .get ("argv" ) or ["sh" ]
169+ cwd = request .get ("cwd" )
170+ env = os .environ .copy ()
171+ env .update (validate_env (request .get ("env" ) or []))
172+ env .setdefault ("TERM" , "xterm-256color" )
173+
174+ master_fd , slave_fd = pty .openpty ()
175+
176+ # Consume any resize frame sent right after the ExecRequest.
177+ # The host sends it before starting the stdin pump, so it
178+ # should arrive quickly. Use a short socket timeout.
179+ conn .settimeout (0.5 )
180+ try :
181+ pending = sock_file .readline ()
182+ if pending :
183+ frame = json .loads (pending .decode ("utf-8" ))
184+ if frame .get ("type" ) == "resize" :
185+ set_winsize (
186+ slave_fd ,
187+ frame .get ("cols" , 80 ),
188+ frame .get ("rows" , 24 ),
189+ )
190+ except (socket .timeout , ValueError , OSError ):
191+ pass
192+ finally :
193+ conn .settimeout (None )
194+
195+ proc = subprocess .Popen (
196+ argv ,
197+ cwd = cwd or "/" ,
198+ env = env ,
199+ stdin = slave_fd ,
200+ stdout = slave_fd ,
201+ stderr = slave_fd ,
202+ preexec_fn = os .setsid ,
203+ )
204+ os .close (slave_fd )
205+
206+ def pty_reader ():
207+ try :
208+ while True :
209+ try :
210+ chunk = os .read (master_fd , 8192 )
211+ except OSError :
212+ break
213+ if not chunk :
214+ break
215+ send_frame (
216+ sock_file ,
217+ lock ,
218+ {
219+ "type" : "stdout" ,
220+ "data" : base64 .b64encode (chunk ).decode ("ascii" ),
221+ },
222+ )
223+ except Exception :
224+ pass
225+
226+ def pty_stdin_writer ():
227+ try :
228+ while True :
229+ frame = recv_line (sock_file )
230+ if frame is None :
231+ break
232+ kind = frame .get ("type" )
233+ if kind == "stdin" :
234+ payload = base64 .b64decode (frame .get ("data" , "" ))
235+ try :
236+ os .write (master_fd , payload )
237+ except OSError :
238+ break
239+ elif kind == "resize" :
240+ try :
241+ set_winsize (
242+ master_fd ,
243+ frame .get ("cols" , 80 ),
244+ frame .get ("rows" , 24 ),
245+ )
246+ except OSError :
247+ pass
248+ elif kind == "stdin_close" :
249+ break
250+ else :
251+ send_frame (
252+ sock_file ,
253+ lock ,
254+ {"type" : "error" , "message" : f"unknown frame type: { kind } " },
255+ )
256+ break
257+ except (BrokenPipeError , OSError ):
258+ pass
259+
260+ reader_thread = threading .Thread (target = pty_reader , daemon = True )
261+ stdin_thread = threading .Thread (target = pty_stdin_writer , daemon = True )
262+ reader_thread .start ()
263+ stdin_thread .start ()
264+
265+ code = proc .wait ()
266+ reader_thread .join (timeout = 2 )
267+ send_frame (sock_file , lock , {"type" : "exit" , "code" : code })
268+ except Exception as exc :
269+ try :
270+ send_frame (sock_file , lock , {"type" : "error" , "message" : str (exc )})
271+ except Exception :
272+ pass
273+ finally :
274+ if master_fd >= 0 :
275+ try :
276+ os .close (master_fd )
277+ except OSError :
278+ pass
279+ try :
280+ sock_file .close ()
281+ except Exception :
282+ pass
283+ conn .close ()
284+
285+
286+ def handle_client (conn ):
287+ sock_file = conn .makefile ("rwb" , buffering = 0 )
288+ try :
289+ request = recv_line (sock_file )
290+ if request is None :
291+ sock_file .close ()
292+ conn .close ()
293+ return
294+ except Exception :
295+ sock_file .close ()
296+ conn .close ()
297+ return
298+
299+ if request .get ("tty" ):
300+ handle_client_tty (conn , request , sock_file )
301+ else :
302+ handle_client_pipe (conn , request , sock_file )
303+
304+
156305def main ():
157306 if not hasattr (socket , "AF_VSOCK" ):
158307 print ("AF_VSOCK is not available" , file = sys .stderr )
0 commit comments