Skip to content

Commit 540b5e3

Browse files
author
neil
committed
support vnc password
1 parent be134bc commit 540b5e3

File tree

2 files changed

+60
-14
lines changed

2 files changed

+60
-14
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ AnyVM includes a built-in, premium VNC Web UI that allows you to access the VM's
140140
- **Fullscreen**: Toggle fullscreen mode for an immersive experience.
141141
- **Stats**: Real-time FPS and latency monitoring.
142142
- **Accessibility**: Available at `http://localhost:6080` by default. If the port is occupied, AnyVM will automatically try the next available port (e.g., 6081, 6082).
143+
- **Security**: Protect your VNC session with `--vnc-password <pwd>`. When set, the browser will prompt for credentials when accessing the Web UI.
143144
- **Remote Access**: Use `--remote-vnc` to automatically create a public, secure tunnel (via Cloudflare, Localhost.run, Pinggy, or Serveo) to access your VM's display from anywhere in the world. (In Google Cloud Shell, this is enabled by default; use `--remote-vnc no` to disable).
144145

145146
## 9. CLI options (with examples)
@@ -237,6 +238,9 @@ All examples below use `python3 anyvm.py ...`. You can also run `python3 anyvm.p
237238
- **VNC Web UI**: Enabled by default starting at port `6080` (auto-increments if busy). Use `--vnc off` to disable.
238239
- Example: `python3 anyvm.py --os freebsd --vnc 0`
239240

241+
- `--vnc-password <pwd>`: Set a password for the VNC Web UI. Empty or omitted means no password.
242+
- Example: `python3 anyvm.py --os freebsd --vnc-password mysecret`
243+
240244
- `--remote-vnc`: Create a public tunnel for the VNC Web UI using Cloudflare, Localhost.run, Pinggy, or Serveo.
241245
- Example: `python3 anyvm.py --os freebsd --remote-vnc`
242246
- Advanced: Use `cf`, `lhr`, `pinggy`, or `serveo` to specify a service: `python3 anyvm.py --os freebsd --remote-vnc cf`

anyvm.py

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,7 +1728,7 @@ def fatal(msg):
17281728
class 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

Comments
 (0)