mirror of
https://github.com/xcat2/confluent.git
synced 2026-01-11 18:42:29 +00:00
Implement plugin managed VNC
To extend beyond the OpenBmc wrapped dialect of VNC, provide mechanism for plugins to provide arbitrary cookie, password, url, and protocols parameters. Implement for ProxMox.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
|
||||
import confluent.vinzmanager as vinzmanager
|
||||
import codecs
|
||||
import confluent.util as util
|
||||
import confluent.messages as msg
|
||||
@@ -29,6 +30,42 @@ class RetainedIO(io.BytesIO):
|
||||
self.resultbuffer = self.getbuffer()
|
||||
super().close()
|
||||
|
||||
class KvmConnection:
|
||||
def __init__(self, consdata):
|
||||
#self.ws = WrappedWebSocket(host=bmc)
|
||||
#self.ws.set_verify_callback(kv)
|
||||
ticket = consdata['ticket']
|
||||
user = consdata['user']
|
||||
port = consdata['port']
|
||||
urlticket = urlparse.quote(ticket)
|
||||
host = consdata['host']
|
||||
guest = consdata['guest']
|
||||
pac = consdata['pac'] # fortunately, we terminate this on our end, but it does kind of reduce the value of the
|
||||
# 'ticket' approach, as the general cookie must be provided as cookie along with the VNC ticket
|
||||
hosturl = host
|
||||
if ':' in hosturl:
|
||||
hosturl = '[' + hosturl + ']'
|
||||
self.url = f'/api2/json/nodes/{host}/{guest}/vncwebsocket?port={port}&vncticket={urlticket}'
|
||||
self.fprint = consdata['fprint']
|
||||
self.cookies = {
|
||||
'PVEAuthCookie': pac,
|
||||
}
|
||||
self.protos = ['binary']
|
||||
self.host = host
|
||||
self.portnum = 8006
|
||||
self.password = consdata['ticket']
|
||||
|
||||
|
||||
class KvmConnHandler:
|
||||
def __init__(self, pmxclient, node):
|
||||
self.pmxclient = pmxclient
|
||||
self.node = node
|
||||
|
||||
def connect(self):
|
||||
consdata = self.pmxclient.get_vm_ikvm(self.node)
|
||||
consdata['fprint'] = self.pmxclient.fprint
|
||||
return KvmConnection(consdata)
|
||||
|
||||
class WrappedWebSocket(wso):
|
||||
|
||||
def set_verify_callback(self, callback):
|
||||
@@ -175,6 +212,7 @@ class PmxApiClient:
|
||||
pass
|
||||
self.server = server
|
||||
self.wc = webclient.SecureHTTPConnection(server, port=8006, verifycallback=cv)
|
||||
self.fprint = configmanager.get_node_attributes(server, 'pubkeys.tls').get(server, {}).get('pubkeys.tls', {}).get('value', None)
|
||||
self.vmmap = {}
|
||||
self.login()
|
||||
self.vmlist = {}
|
||||
@@ -243,11 +281,15 @@ class PmxApiClient:
|
||||
yield msg.KeyValueData({'inventory': invitems}, vm)
|
||||
|
||||
|
||||
def get_vm_ikvm(self, vm):
|
||||
return self.get_vm_consproxy(vm, 'vnc')
|
||||
|
||||
def get_vm_serial(self, vm):
|
||||
# This would be termproxy
|
||||
# Example url
|
||||
return self.get_vm_consproxy(vm, 'term')
|
||||
|
||||
def get_vm_consproxy(self, vm, constype):
|
||||
host, guest = self.get_vm(vm)
|
||||
rsp = self.wc.grab_json_response_with_status(f'/api2/json/nodes/{host}/{guest}/termproxy', method='POST')
|
||||
rsp = self.wc.grab_json_response_with_status(f'/api2/json/nodes/{host}/{guest}/{constype}proxy', method='POST')
|
||||
consdata = rsp[0]['data']
|
||||
consdata['server'] = self.server
|
||||
consdata['host'] = host
|
||||
@@ -372,20 +414,12 @@ def retrieve(nodes, element, configmanager, inputdata):
|
||||
for rsp in currclient.get_vm_inventory(node):
|
||||
yield rsp
|
||||
elif element == ['console', 'ikvm_methods']:
|
||||
dsc = {'ikvm_methods': ['screenshot']}
|
||||
dsc = {'ikvm_methods': ['vnc']}
|
||||
yield msg.KeyValueData(dsc, node)
|
||||
elif element == ['console', 'ikvm_screenshot']:
|
||||
# good background for the webui, and kitty
|
||||
imgdata = RetainedIO()
|
||||
imgformat = currclient.get_screenshot(node, imgdata)
|
||||
imgdata = imgdata.getvalue()
|
||||
if imgdata:
|
||||
yield msg.ScreenShot(imgdata, node, imgformat=imgformat)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
yield msg.ConfluentNodeError(node, "vnc available, screenshot not available")
|
||||
return
|
||||
|
||||
def update(nodes, element, configmanager, inputdata):
|
||||
clientsbynode = prep_proxmox_clients(nodes, configmanager)
|
||||
@@ -397,11 +431,29 @@ def update(nodes, element, configmanager, inputdata):
|
||||
elif element == ['boot', 'nextdevice']:
|
||||
currclient.set_vm_bootdev(node, inputdata.bootdevice(node))
|
||||
yield msg.BootDevice(node, currclient.get_vm_bootdev(node))
|
||||
elif element == ['console', 'ikvm']:
|
||||
try:
|
||||
currclient = clientsbynode[node]
|
||||
url = vinzmanager.get_url(node, inputdata, nodeparmcallback=KvmConnHandler(currclient, node).connect)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
return
|
||||
yield msg.ChildCollection(url)
|
||||
return
|
||||
|
||||
# assume this is only console for now
|
||||
def create(nodes, element, configmanager, inputdata):
|
||||
clientsbynode = prep_proxmox_clients(nodes, configmanager)
|
||||
for node in nodes:
|
||||
if element == ['console', 'ikvm']:
|
||||
try:
|
||||
currclient = clientsbynode[node]
|
||||
url = vinzmanager.get_url(node, inputdata, nodeparmcallback=KvmConnHandler(currclient, node).connect)
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
return
|
||||
yield msg.ChildCollection(url)
|
||||
return
|
||||
serialdata = clientsbynode[node].get_vm_serial(node)
|
||||
return PmxConsole(serialdata, node, configmanager, clientsbynode[node])
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@ def assure_vinz():
|
||||
startingup = False
|
||||
|
||||
_unix_by_nodename = {}
|
||||
def get_url(nodename, inputdata):
|
||||
_nodeparms = {}
|
||||
def get_url(nodename, inputdata, nodeparmcallback=None):
|
||||
_nodeparms[nodename] = nodeparmcallback
|
||||
method = inputdata.inputbynode[nodename]
|
||||
assure_vinz()
|
||||
if method == 'wss':
|
||||
@@ -89,56 +91,120 @@ def close_session(sessionid):
|
||||
'X-XSRF-TOKEN': wc.cookies['XSRF-TOKEN']})
|
||||
|
||||
|
||||
def send_grant(conn, nodename):
|
||||
cfg = configmanager.ConfigManager(None)
|
||||
c = cfg.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager'], decrypt=True)
|
||||
bmcuser = c.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
bmcpass = c.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', None)
|
||||
bmc = c.get(nodename, {}).get(
|
||||
'hardwaremanagement.manager', {}).get('value', None)
|
||||
if bmcuser and bmcpass and bmc:
|
||||
kv = util.TLSCertVerifier(cfg, nodename,
|
||||
'pubkeys.tls_hardwaremanager').verify_cert
|
||||
wc = webclient.SecureHTTPConnection(bmc, 443, verifycallback=kv)
|
||||
if not isinstance(bmcuser, str):
|
||||
bmcuser = bmcuser.decode()
|
||||
if not isinstance(bmcpass, str):
|
||||
bmcpass = bmcpass.decode()
|
||||
rsp = wc.grab_json_response_with_status(
|
||||
'/login', {'data': [bmcuser, bmcpass]},
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'})
|
||||
sessionid = wc.cookies['SESSION']
|
||||
sessiontok = wc.cookies['XSRF-TOKEN']
|
||||
def send_grant(conn, nodename, rqtype):
|
||||
parmcallback = _nodeparms.get(nodename, None)
|
||||
cookies = {}
|
||||
protos = []
|
||||
passwd = None
|
||||
sessionid = os.urandom(8).hex()
|
||||
while sessionid in _usersessions:
|
||||
sessionid = os.urandom(8).hex()
|
||||
if parmcallback: # plugin that handles the specifics of the vnc wrapping
|
||||
if rqtype == 1:
|
||||
raise Exception("Plugin managed login data not supported with legacy grant request")
|
||||
cxnmgr = parmcallback()
|
||||
_usersessions[sessionid] = {
|
||||
'webclient': wc,
|
||||
'cxnmgr': cxnmgr,
|
||||
'nodename': nodename,
|
||||
}
|
||||
url = '/kvm/0'
|
||||
fprintinfo = cfg.get_node_attributes(nodename, 'pubkeys.tls_hardwaremanager')
|
||||
fprint = fprintinfo.get(
|
||||
nodename, {}).get('pubkeys.tls_hardwaremanager', {}).get('value', None)
|
||||
if not fprint:
|
||||
return
|
||||
url = cxnmgr.url
|
||||
fprint = cxnmgr.fprint
|
||||
cookies = cxnmgr.cookies
|
||||
protos = cxnmgr.protos
|
||||
host = cxnmgr.host
|
||||
portnum = cxnmgr.portnum
|
||||
passwd = cxnmgr.password
|
||||
#url, fprint, cookies, protos = parmcallback(nodename)
|
||||
else:
|
||||
# original openbmc dialect
|
||||
portnum = 443
|
||||
cfg = configmanager.ConfigManager(None)
|
||||
c = cfg.get_node_attributes(
|
||||
nodename,
|
||||
['secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager'], decrypt=True)
|
||||
bmcuser = c.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
bmcpass = c.get(nodename, {}).get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value', None)
|
||||
host = c.get(nodename, {}).get(
|
||||
'hardwaremanagement.manager', {}).get('value', None)
|
||||
if bmcuser and bmcpass and host:
|
||||
kv = util.TLSCertVerifier(cfg, nodename,
|
||||
'pubkeys.tls_hardwaremanager').verify_cert
|
||||
wc = webclient.SecureHTTPConnection(host, 443, verifycallback=kv)
|
||||
if not isinstance(bmcuser, str):
|
||||
bmcuser = bmcuser.decode()
|
||||
if not isinstance(bmcpass, str):
|
||||
bmcpass = bmcpass.decode()
|
||||
rsp = wc.grab_json_response_with_status(
|
||||
'/login', {'data': [bmcuser, bmcpass]},
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'})
|
||||
cookies['SESSION'] = wc.cookies['SESSION']
|
||||
cookies['XSRF-TOKEN'] = wc.cookies['XSRF-TOKEN']
|
||||
if rqtype == 1:
|
||||
# unfortunately, the original protocol failed to
|
||||
# provide a means for separate tracking bmc side
|
||||
# and confluent side
|
||||
# chances are pretty good still
|
||||
sessionid = wc.cookies['SESSION']
|
||||
sessiontok = wc.cookies['XSRF-TOKEN']
|
||||
protos.append(sessiontok)
|
||||
_usersessions[sessionid] = {
|
||||
'webclient': wc,
|
||||
'nodename': nodename,
|
||||
}
|
||||
url = '/kvm/0'
|
||||
fprintinfo = cfg.get_node_attributes(nodename, 'pubkeys.tls_hardwaremanager')
|
||||
fprint = fprintinfo.get(
|
||||
nodename, {}).get('pubkeys.tls_hardwaremanager', {}).get('value', None)
|
||||
if not fprint:
|
||||
return
|
||||
if '$' in fprint:
|
||||
fprint = fprint.split('$', 1)[1]
|
||||
fprint = bytes.fromhex(fprint)
|
||||
conn.send(struct.pack('!BI', 1, len(bmc)))
|
||||
conn.send(bmc.encode())
|
||||
conn.send(struct.pack('!I', len(sessionid)))
|
||||
conn.send(sessionid.encode())
|
||||
fprint = bytes.fromhex(fprint)
|
||||
conn.send(struct.pack('!BI', rqtype, len(host)))
|
||||
conn.send(host.encode())
|
||||
conn.send(struct.pack('!I', len(sessionid)))
|
||||
conn.send(sessionid.encode())
|
||||
if rqtype == 1:
|
||||
conn.send(struct.pack('!I', len(sessiontok)))
|
||||
conn.send(sessiontok.encode())
|
||||
conn.send(struct.pack('!I', len(fprint)))
|
||||
conn.send(fprint)
|
||||
conn.send(struct.pack('!I', len(url)))
|
||||
conn.send(url.encode())
|
||||
conn.send(b'\xff')
|
||||
else: # newer TLV style protocol
|
||||
conn.send(struct.pack('!H', portnum))
|
||||
conn.send(struct.pack('!BI', 4, len(url)))
|
||||
conn.send(url.encode())
|
||||
for cook in cookies:
|
||||
v = cookies[cook]
|
||||
totlen = len(cook) + len(v) + 4
|
||||
conn.send(struct.pack('!BIH', 1, totlen, len(cook.encode())))
|
||||
conn.send(cook.encode())
|
||||
conn.send(struct.pack('!H', len(v.encode())))
|
||||
conn.send(v.encode())
|
||||
for proto in protos:
|
||||
conn.send(struct.pack('!BI', 2, len(proto.encode())))
|
||||
conn.send(proto.encode())
|
||||
conn.send(struct.pack('!BI', 3, len(fprint)))
|
||||
conn.send(fprint)
|
||||
if passwd:
|
||||
conn.send(struct.pack('!BI', 5, len(passwd.encode()[:8])))
|
||||
conn.send(passwd.encode()[:8])
|
||||
conn.send(b'\xff')
|
||||
|
||||
def recv_exact(conn, n):
|
||||
retdata = b''
|
||||
while len(retdata) < n:
|
||||
currdata = conn.recv(n - len(retdata))
|
||||
if not currdata:
|
||||
raise Exception("Error receiving")
|
||||
retdata += currdata
|
||||
return retdata
|
||||
|
||||
def evaluate_request(conn):
|
||||
allow = False
|
||||
@@ -149,33 +215,37 @@ def evaluate_request(conn):
|
||||
pid, uid, gid = struct.unpack('iII', creds)
|
||||
if uid != os.getuid():
|
||||
return
|
||||
rqcode, fieldlen = struct.unpack('!BI', conn.recv(5))
|
||||
authtoken = conn.recv(fieldlen).decode()
|
||||
rqcode, fieldlen = struct.unpack('!BI', recv_exact(conn, 5))
|
||||
authtoken = recv_exact(conn, fieldlen).decode()
|
||||
if authtoken != _vinztoken:
|
||||
return
|
||||
if rqcode == 2: # disconnect notification
|
||||
fieldlen = struct.unpack('!I', conn.recv(4))[0]
|
||||
sessionid = conn.recv(fieldlen).decode()
|
||||
fieldlen = struct.unpack('!I', recv_exact(conn, 4))[0]
|
||||
sessionid = recv_exact(conn, fieldlen).decode()
|
||||
close_session(sessionid)
|
||||
conn.recv(1) # digest 0xff
|
||||
if rqcode == 1: # request for new connection
|
||||
fieldlen = struct.unpack('!I', conn.recv(4))[0]
|
||||
nodename = conn.recv(fieldlen).decode()
|
||||
# if rqcode == 3: # new form connection request
|
||||
# this will generalize things, to allow describing
|
||||
# arbitrary cookies and subprotocols
|
||||
# for the websocket connection
|
||||
if rqcode in (1, 3): # request for new connection
|
||||
fieldlen = struct.unpack('!I', recv_exact(conn, 4))[0]
|
||||
nodename = recv_exact(conn, fieldlen).decode()
|
||||
idtype = struct.unpack('!B', conn.recv(1))[0]
|
||||
if idtype == 1:
|
||||
usernum = struct.unpack('!I', conn.recv(4))[0]
|
||||
usernum = struct.unpack('!I', recv_exact(conn, 4))[0]
|
||||
if usernum == 0: # root is a special guy
|
||||
send_grant(conn, nodename)
|
||||
send_grant(conn, nodename, rqcode)
|
||||
return
|
||||
try:
|
||||
authname = pwd.getpwuid(usernum).pw_name
|
||||
except Exception:
|
||||
return
|
||||
elif idtype == 2:
|
||||
fieldlen = struct.unpack('!I', conn.recv(4))[0]
|
||||
sessionid = conn.recv(fieldlen)
|
||||
fieldlen = struct.unpack('!I', conn.recv(4))[0]
|
||||
sessiontok = conn.recv(fieldlen)
|
||||
fieldlen = struct.unpack('!I', recv_exact(conn, 4))[0]
|
||||
sessionid = recv_exact(conn, fieldlen)
|
||||
fieldlen = struct.unpack('!I', recv_exact(conn, 4))[0]
|
||||
sessiontok = recv_exact(conn, fieldlen)
|
||||
try:
|
||||
authname = httpapi.get_user_for_session(sessionid, sessiontok)
|
||||
except Exception:
|
||||
@@ -186,7 +256,7 @@ def evaluate_request(conn):
|
||||
if authname:
|
||||
allow = auth.authorize(authname, f'/nodes/{nodename}/console/ikvm')
|
||||
if allow:
|
||||
send_grant(conn, nodename)
|
||||
send_grant(conn, nodename, rqcode)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user