@@ -1728,7 +1728,7 @@ def fatal(msg):
17281728class VNCWebProxy :
17291729 GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
17301730
1731- def __init__ (self , vnc_host , vnc_port , web_port , vm_info = "" , qemu_pid = None , audio_enabled = False , qmon_port = None , error_log_path = None , is_console_vnc = False , listen_addr = '127.0.0.1' ):
1731+ def __init__ (self , vnc_host , vnc_port , web_port , vm_info = "" , qemu_pid = None , audio_enabled = False , qmon_port = None , error_log_path = None , is_console_vnc = False , listen_addr = '127.0.0.1' , vnc_password = "" ):
17321732 self .vnc_host = vnc_host
17331733 self .vnc_port = vnc_port
17341734 self .web_port = web_port
@@ -1739,6 +1739,7 @@ def __init__(self, vnc_host, vnc_port, web_port, vm_info="", qemu_pid=None, audi
17391739 self .error_log_path = error_log_path
17401740 self .is_console_vnc = is_console_vnc
17411741 self .listen_addr = listen_addr
1742+ self .vnc_password = vnc_password
17421743 self .clients = set ()
17431744 self .serial_buffer = collections .deque (maxlen = 1024 * 100 ) # 100KB binary buffer (optimized for refresh speed)
17441745 self .serial_writer = None
@@ -1748,20 +1749,29 @@ def __init__(self, vnc_host, vnc_port, web_port, vm_info="", qemu_pid=None, audi
17481749 async def handle_client (self , reader , writer ):
17491750 try :
17501751 request = await reader .read (4096 )
1752+ if not request : return
1753+
17511754 request_text = request .decode ('utf-8' , errors = 'ignore' )
1752- lines = request_text .split ('\r \n ' )
1753- if not lines :
1754- writer .close ()
1755- return
1755+ lines = request_text .splitlines ()
1756+ if not lines : return
1757+
1758+ parts = lines [0 ].split ()
1759+ if len (parts ) < 2 : return
1760+ path = parts [1 ]
17561761
17571762 headers = {}
17581763 for line in lines [1 :]:
17591764 if ':' in line :
1760- key , value = line .split (':' , 1 )
1761- headers [key .strip ().lower ()] = value .strip ()
1762-
1763- path = lines [0 ].split ()[1 ] if len (lines [0 ].split ()) > 1 else '/'
1765+ key , val = line .split (':' , 1 )
1766+ headers [key .strip ().lower ()] = val .strip ()
17641767
1768+ if self .vnc_password :
1769+ auth_header = headers .get ('authorization' , '' )
1770+ is_auth_ok = self .check_auth (auth_header )
1771+ if not is_auth_ok :
1772+ await self .request_auth (writer )
1773+ return
1774+
17651775 if 'upgrade' in headers and headers .get ('upgrade' , '' ).lower () == 'websocket' :
17661776 await self .handle_websocket (reader , writer , headers )
17671777 else :
@@ -1774,6 +1784,31 @@ async def handle_client(self, reader, writer):
17741784 except :
17751785 pass
17761786
1787+ def check_auth (self , auth_header ):
1788+ if not auth_header or not auth_header .lower ().startswith ('basic ' ):
1789+ return False
1790+ try :
1791+ cred_part = auth_header .split (None , 1 )[1 ]
1792+ decoded = base64 .b64decode (cred_part ).decode ('utf-8' )
1793+ pwd = decoded .split (':' , 1 )[1 ] if ':' in decoded else decoded
1794+ return pwd == self .vnc_password
1795+ except :
1796+ return False
1797+
1798+ async def request_auth (self , writer ):
1799+ body = b"401 Unauthorized"
1800+ response = (
1801+ "HTTP/1.1 401 Unauthorized\r \n "
1802+ "WWW-Authenticate: Basic realm=\" AnyVM\" \r \n "
1803+ "Content-Type: text/plain\r \n "
1804+ "Content-Length: {}\r \n "
1805+ "Connection: close\r \n "
1806+ "\r \n " .format (len (body ))
1807+ ).encode ('utf-8' ) + body
1808+ writer .write (response )
1809+ await writer .drain ()
1810+
1811+
17771812 async def handle_http (self , writer , path ):
17781813 title = "AnyVM - VNC Viewer"
17791814 if self .vm_info :
@@ -2094,7 +2129,7 @@ def strip_ansi(text):
20942129 ansi_escape = re .compile (r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])' )
20952130 return ansi_escape .sub ('' , text )
20962131
2097- def start_vnc_web_proxy (vnc_port , web_port , vm_info = "" , qemu_pid = None , audio_enabled = False , qmon_port = None , error_log_path = None , is_console_vnc = False , listen_addr = '127.0.0.1' , remote_vnc = False , debug = False , remote_vnc_link_file = None ):
2132+ def start_vnc_web_proxy (vnc_port , web_port , vm_info = "" , qemu_pid = None , audio_enabled = False , qmon_port = None , error_log_path = None , is_console_vnc = False , listen_addr = '127.0.0.1' , remote_vnc = False , debug = False , remote_vnc_link_file = None , vnc_password = "" ):
20982133 # Handle termination signals for immediate cleanup
20992134 def signal_handler (sig , frame ):
21002135 sys .exit (0 )
@@ -2344,7 +2379,7 @@ def download_and_run_manager():
23442379 t .start ()
23452380
23462381 try :
2347- proxy = VNCWebProxy ('127.0.0.1' , vnc_port , web_port , vm_info , qemu_pid , audio_enabled , qmon_port , error_log_path , is_console_vnc , listen_addr = listen_addr )
2382+ proxy = VNCWebProxy ('127.0.0.1' , vnc_port , web_port , vm_info , qemu_pid , audio_enabled , qmon_port , error_log_path , is_console_vnc , listen_addr = listen_addr , vnc_password = vnc_password )
23482383 proxy .kill_tunnels_func = kill_all_tunnels
23492384 asyncio .run (proxy .run ())
23502385 finally :
@@ -2404,6 +2439,7 @@ def print_usage():
24042439 --vnc <display> Enable VNC on specified display (e.g., 0 for :0).
24052440 Default: enabled (display 0). Web UI starts at 6080 (increments if busy).
24062441 Use "--vnc off" to disable.
2442+ --vnc-password <pwd> Set a password for the VNC Web UI. Empty or omitted means no password.
24072443 --remote-vnc Create a public URL for the VNC Web UI using Cloudflare, Localhost.run, Pinggy, or Serveo.
24082444 Usage: --remote-vnc (auto), --remote-vnc cf, --remote-vnc lhr, --remote-vnc pinggy, --remote-vnc serveo.
24092445 Enabled by default if no local browser is detected (e.g., in Cloud Shell).
@@ -3468,7 +3504,8 @@ def main():
34683504 remote_vnc = remote_vnc_val if remote_vnc_val not in ['0' , 'False' , 'false' , None ] else False
34693505 debug_vnc = sys .argv [12 ] == '1' if len (sys .argv ) > 12 else False
34703506 link_file = sys .argv [13 ] if len (sys .argv ) > 13 and sys .argv [13 ] != '0' else None
3471- start_vnc_web_proxy (vnc_port , web_port , vm_info , qemu_pid , audio_enabled , qmon_port , error_log_path , is_console_vnc , listen_addr = listen_addr , remote_vnc = remote_vnc , debug = debug_vnc , remote_vnc_link_file = link_file )
3507+ vnc_pwd = sys .argv [14 ] if len (sys .argv ) > 14 else ""
3508+ start_vnc_web_proxy (vnc_port , web_port , vm_info , qemu_pid , audio_enabled , qmon_port , error_log_path , is_console_vnc , listen_addr = listen_addr , remote_vnc = remote_vnc , debug = debug_vnc , remote_vnc_link_file = link_file , vnc_password = vnc_pwd )
34723509 except Exception as e :
34733510 # If we have an error log path, try to write to it even if startup fails
34743511 try :
@@ -3521,7 +3558,8 @@ def main():
35213558 'accept_vm_ssh' : False ,
35223559 'remote_vnc' : None ,
35233560 'remote_vnc_is_default' : False ,
3524- 'remote_vnc_link_file' : None
3561+ 'remote_vnc_link_file' : None ,
3562+ 'vnc_password' : ""
35253563 }
35263564
35273565 ssh_passthrough = []
@@ -3611,6 +3649,9 @@ def main():
36113649 config ['vnc' ] = args [i + 1 ]
36123650 vnc_user_specified = True
36133651 i += 1
3652+ elif arg == "--vnc-password" :
3653+ config ['vnc_password' ] = args [i + 1 ]
3654+ i += 1
36143655 elif arg in ["--res" , "--resolution" ]:
36153656 config ['resolution' ] = args [i + 1 ]
36163657 i += 1
@@ -4635,7 +4676,8 @@ def start_vnc_proxy_for_pid(qemu_pid):
46354676 '0.0.0.0' if (config ['public' ] or config ['public_vnc' ]) else '127.0.0.1' ,
46364677 str (config ['remote_vnc' ]) if config ['remote_vnc' ] else '0' ,
46374678 '1' if config ['debug' ] else '0' ,
4638- str (config ['remote_vnc_link_file' ]) if config ['remote_vnc_link_file' ] else '0'
4679+ str (config ['remote_vnc_link_file' ]) if config ['remote_vnc_link_file' ] else '0' ,
4680+ config ['vnc_password' ]
46394681 ]
46404682 popen_kwargs = {}
46414683 if IS_WINDOWS :
0 commit comments