diff --git a/README.md b/README.md index 5a3a30f..2985075 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,9 @@ and websockify as a browser telnet client (`wstelnet.html`). These are not necessary for the basic operation. +* Muliti-vncserver: Use path in URL to pass VNC server address:port for + connecting different VNC servers. + * Daemonizing: When the `-D` option is specified, websockify runs in the background as a daemon process. diff --git a/websockify/encode_url.py b/websockify/encode_url.py new file mode 100644 index 0000000..38e0f0b --- /dev/null +++ b/websockify/encode_url.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +'''The program encode and decode the string with base64 and hash to validate the integrality. +''' +import sys +import hashlib +import base64 +import datetime +import group +# Please change the salt for your own project. +#SALT = "Some salt for security. liqun@ncl.sg" +SALT = "Some salt for security. Please change it in your project. liqun@ncl.sg" +def qencode(str, create_date=datetime.date.today(), salt=SALT): + ''' The func encode str, hash it and base64 it.''' + alg = hashlib.sha256() + tstr = create_date.strftime("%Y%m%d") + alg.update(tstr) + alg.update(':') + alg.update(str) + alg.update(salt) + hash = alg.hexdigest() + return base64.urlsafe_b64encode(hash+tstr+':'+str) +def qdecode(str, valid_daynum=3, salt=SALT): + ''' The func decode str, validate with hash and base64.decode it. + If valid_daynum =0, only today is valid.''' + if str[-1] == '/': + str1 = base64.urlsafe_b64decode(str[0:-1]) + else: + str1 = base64.urlsafe_b64decode(str) + pos = str1.find(':') + if pos == -1: + return '','Err:not find :' + hash = str1[0:pos-8] + tstr = str1[pos-8:pos] + url_date = datetime.date(int(str1[pos-8:pos-4]), int(str1[pos-4:pos-2]), int(str1[pos-2:pos])) + today = datetime.date.today() + if (today - url_date) > datetime.timedelta(valid_daynum): + return '', 'Err: Timeout' + alg = hashlib.sha256() + alg.update(str1[pos-8:]) + alg.update(salt) + hash1 = alg.hexdigest() + if hash != hash1: + return '', 'Err: Wrong hash' + return str1[pos+1:], '' +def get_server_from_path(path, is_encoded, valid_daynum=3, salt=SALT, gpolicy=group.gpolicy): + '''The func decode host port from path parameter. + path looks like [/qencode(n1.soc.cloud.ncl.sg:5901)] + ''' + phost = '' + if path[-1] == '/': + path = path[0:-1] + + try: + pos = path.rfind('/') + if pos == -1: + return '', 0 + if is_encoded: + (str,err) = qdecode(path[pos+1:], valid_daynum, salt) + if err != '': + phost = err + else: + str = path[pos+1:] + if str == '': + return phost, 0 + part_list = str.split(':') + phost = part_list[0] + pport = int(part_list[1]) + if len(part_list) > 2: + username = part_list[2] + # check the access ability. + if not group.can_access(username, phost, gpolicy): + return 'Err: group policy block', 0 + except: + raise + #return phost, 0 + return phost, pport + +def test_basic(): + '''The func test some basic func.''' + assert get_server_from_path('/n1.soc.cloud.ncl.sg:5901', False) == ('n1.soc.cloud.ncl.sg', 5901) + + str = "n1.soc.cloud.ncl.sg:5901" + enc = qencode(str) + print enc + print base64.urlsafe_b64decode(enc) + (dec,err) = qdecode(enc) + assert str == dec + assert get_server_from_path('/'+enc, True) == ('n1.soc.cloud.ncl.sg', 5901) + enc = qencode(str, datetime.date(2018, 2, 24)) + assert (get_server_from_path('/'+enc, True)) == ('Err: Timeout', 0) + enc = qencode(str, datetime.date.today(), 'test salt') + (dec,err) = qdecode(enc, 3, 'test salt') + assert str == dec + (dec,err) = qdecode(enc, 3, 'test salt1') + assert dec == '' + str = "n1.soc.cloud.ncl.sg:5901:ntechni3" + enc = qencode(str) + print enc + print base64.urlsafe_b64decode(enc) + (dec,err) = qdecode(enc) + assert str == dec + assert get_server_from_path('/'+enc, True) == ('n1.soc.cloud.ncl.sg', 5901) + (dec,err) = qdecode(enc + '/') + assert str == dec + gpolicy = { + "ExperimentDomainName":"soc.cloud.ncl.sg", + 'Groups':[{ + 'Name':'Red', + 'Users': ["user1", "ntechni3"], + 'Hosts': ["n1"] + }, + { + 'Name':'Blue', + 'Users': ['user1'], + 'Hosts': ['n2','n3'] + }] + } + str = "n1.soc.cloud.ncl.sg:5901:ntechni3" + enc = qencode(str) + assert get_server_from_path('/'+enc, True, 3, SALT, gpolicy) == ('n1.soc.cloud.ncl.sg', 5901) + str = "n2.soc.cloud.ncl.sg:5901:ntechni3" + enc = qencode(str) + assert get_server_from_path('/'+enc, True, 3, SALT, gpolicy) == ('Err: group policy block', 0) + +def main(): + '''The func is the main func.''' + import sys + if (len(sys.argv) == 2) and (sys.argv[1] == "test"): + test_basic() + print "Pass all test" + exit() + elif (len(sys.argv) == 2): + print qencode(sys.argv[1]) + elif (len(sys.argv) == 3) and (sys.argv[1] == "decode"): + print get_server_from_path(sys.argv[2], True) + #print qdecode(sys.argv[2]) + elif (len(sys.argv) == 5) and (sys.argv[1] == "decode"): + print qdecode(sys.argv[2],int(sys.argv[3]),sys.argv[4]) + elif (len(sys.argv) == 5) and (sys.argv[1] == "encode"): + print qencode(sys.argv[2],int(sys.argv[3]),sys.argv[4]) + elif (len(sys.argv) == 3) and (sys.argv[1] == "debase"): + str1 = base64.urlsafe_b64decode(sys.argv[2]) + print str1 + else: + print '''%s [content to encode] [valid day number:3] [salt to encrypt] + decode [content to decode] [valid day number:3] [salt to encrypt] + test''' % sys.argv[0] + +if __name__ == "__main__": + main() diff --git a/websockify/group.py b/websockify/group.py new file mode 100644 index 0000000..3800ee1 --- /dev/null +++ b/websockify/group.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python +import subprocess + +gpolicy = { } + +def group(users, hosts, ousers, ohosts): + '''The func handle group policy for one group. Users can access hosts.''' + pass +def get_users_hosts(gpolicy): + '''The func Get the all user list, hosts, hosts_access''' + users = set() + hosts = {} + hosts_access = {} + # Get the all user list, hosts, hosts_access + for group in gpolicy['Groups']: + if not group.has_key('Name'): + print "The group do not have key [Name]" + continue; + for user in group['Users']: + users.add(user) + if not hosts_access.has_key(user): + hosts_access[user] = set() + if group.has_key('Hosts'): + hosts_access[user] = hosts_access[user].union(group['Hosts']) + # get all host list. + if group.has_key('Hosts'): + for host in group['Hosts']: + hosts[host] = '' + return users, hosts, hosts_access +def get_super_users(gpolicy, users, hosts_access): + '''The func Get all super user list. User in super user list need be + deleted in user list and hosts_access.''' + super_users = set() + # Get all super user list. + for group in gpolicy['Groups']: + if not group.has_key('Name'): + print "The group do not have key [Name]" + continue; + if not group.has_key('Hosts'): + for user in group['Users']: + # Delete super user from normal user list. + if user in users: + users.remove(user) + del hosts_access[user] + super_users.add(user) + return (super_users, users, hosts_access) +def get_unaccess_hosts(hosts_access, hosts): + hosts_unaccess = {} + for user in hosts_access.keys(): + hosts_unaccess[user] = set() + for host in hosts: + if host not in hosts_access[user]: + hosts_unaccess[user].add(host) + return hosts_unaccess +def can_access(username, node_url, gpolicy): + '''The func return true or False, based on gpolicy allowing username to node_url or not.''' + if not gpolicy.has_key('ExperimentDomainName'): + print 'Err: no ExperimentDomainName' + return True + ename = gpolicy['ExperimentDomainName'] + exp_name = ename + dnode = node_url[:node_url.find('.')] + dexp = node_url[node_url.find('.')+1:] + if dexp != exp_name : + # not the experience group return true. + return True + for group in gpolicy['Groups']: + find_host = False + find_user = False + if not group.has_key('Name'): + print "The group do not have key [Name]" + continue; + if group.has_key('Hosts'): + for host in group['Hosts']: + if host == dnode: + find_host = True + else: + #super user group, can access all nodes. + find_host = True + if group.has_key('Users'): + for user in group['Users']: + if username == user: + find_user = True + if find_host and find_user : + return True + return False + +def group_exp(gpolicy): + ename = gpolicy['ExperimentDomainName'] + users = () + super_users = () + hosts = {} + hosts_access = {} + hosts_unaccess = {} + # Get the all user list, hosts, hosts_access + (users, hosts, hosts_access) = get_users_hosts(gpolicy) + # Get all super user list. + (super_users, users, hosts_access) = get_super_users(gpolicy, users, hosts_access) + # produce unaccess hosts for every user. + hosts_unaccess = get_unaccess_hosts(hosts_access, hosts) + + # For every normal user, get all accessable host list and give access right, other give no right. + for user in users: + for host in hosts_access[user]: + hosts[host] += 'sudo usermod -e "" %s\n' % user + for user in users: + for host in hosts_unaccess[user]: + hosts[host] += 'sudo usermod -e 1 %s\n' % user + # For every super user, give all access. + for user in super_users: + for host in hosts.keys(): + hosts[host] += 'sudo usermod -e "" %s\n' % user + return hosts +def test(): + gpolicy = { + "ExperimentDomainName":"EnterpriseNetwork.NYPSOC.ncl.sg", + 'Groups':[] + } + assert get_users_hosts(gpolicy) == (set([]), {}, {}) + + gpolicy = { + "ExperimentDomainName":"EnterpriseNetwork.NYPSOC.ncl.sg", + 'Groups':[{ + 'Name':'Red', + 'Users': ["user1", "user2"], + 'Hosts': ["n1"] + }, + { + 'Name':'Blue', + 'Users': ['user3'], + 'Hosts': ['n2','n3'] + }, + { + 'Name':'Grey', + 'Users': ['user4'], + 'Hosts': [] + }, + { + 'Name':'Super', + 'Users': ['user3'] + }] + } + (users, hosts, hosts_access) = get_users_hosts(gpolicy) + assert (users, hosts, hosts_access) == (set(['user4', 'user2', 'user3', 'user1']),\ + {'n1': '', 'n2': '', 'n3': ''}, \ + {'user4': set([]), 'user2': set(['n1']), 'user3': set(['n2', 'n3']), 'user1': set(['n1'])}) + assert get_unaccess_hosts(hosts_access, hosts) == { + 'user4': set(['n1', 'n2', 'n3']), 'user2': set(['n2', 'n3']), + 'user3': set(['n1']), 'user1': set(['n2', 'n3'])} + (super_users, users, hosts_access) = get_super_users(gpolicy, users, hosts_access) + assert (super_users, users, hosts_access) == (set(['user3']), + set(['user4', 'user2', 'user1']), \ + {'user4': set([]), 'user2': set(['n1']), 'user1': set(['n1'])}) + assert get_unaccess_hosts(hosts_access, hosts) == { + 'user4': set(['n1', 'n2', 'n3']), 'user2': set(['n2', 'n3']), + 'user1': set(['n2', 'n3'])} + print group_exp(gpolicy) + group_exp(gpolicy) == {'n1': 'sudo usermod -e "" user2\nsudo usermod -e "" user1\nsudo usermod -e 1 user4\nsudo usermod -e "" user3\n', 'n2': 'sudo usermod -e 1 user4\nsudo usermod -e 1 user2\nsudo usermod -e 1 user1\nsudo usermod -e "" user3\n', 'n3': 'sudo usermod -e 1 user4\nsudo usermod -e 1 user2\nsudo usermod -e 1 user1\nsudo usermod -e "" user3\n'} + + assert get_unaccess_hosts({'user1': set(['n1'])}, {'n1': ''}) == {'user1': set([])} + assert get_unaccess_hosts({'user1': set([])}, {'n1': ''}) == {'user1': set(['n1'])} + assert get_unaccess_hosts({'user1': set(['n1', 'n2'])}, {'n1': '', 'n2': ''}) == {'user1': set([])} + + gpolicy = { + "ExperimentDomainName":"EnterpriseNetwork.NYPSOC.ncl.sg", + 'Groups':[{ + 'Name':'Red', + 'Users': ["user1", "user2"], + 'Hosts': ["n1"] + }, + { + 'Name':'Blue', + 'Users': ['user1'], + 'Hosts': ['n2','n3'] + } ] + } + (users, hosts, hosts_access) = get_users_hosts(gpolicy) + #print (users, hosts, hosts_access) + assert (users, hosts, hosts_access) == (set(['user2', 'user1']), \ + {'n1': '', 'n2': '', 'n3': ''}, {'user2': set(['n1']), 'user1': set(['n1', 'n2', 'n3'])}) + gpolicy = { + "ExperimentDomainName":"EnterpriseNetwork.NYPSOC.ncl.sg", + 'Groups':[{ + 'Name':'Red', + 'Users': ["user1", "user2"], + 'Hosts': ["n1"] + }, + { + 'Name':'Blue', + 'Users': ['user1'], + 'Hosts': ['n2','n3'] + }, + { 'Name':'superusers', 'Users':['suser']} + ] + } + assert can_access('user1', 'n1.EnterpriseNetwork.NYPSOC.ncl.sg', gpolicy) == True + assert can_access('user1', 'n1.another.NYPSOC.ncl.sg', gpolicy) == True + assert can_access('user1', 'n2.EnterpriseNetwork.NYPSOC.ncl.sg', gpolicy) == True + assert can_access('user2', 'n2.EnterpriseNetwork.NYPSOC.ncl.sg', gpolicy) == False + assert can_access('user3', 'n2.EnterpriseNetwork.NYPSOC.ncl.sg', gpolicy) == False + assert can_access('suser', 'n2.EnterpriseNetwork.NYPSOC.ncl.sg', gpolicy) == True + + +def main(): + '''The func is the main func.''' + import sys + if (len(sys.argv) == 2) and (sys.argv[1] == "test"): + test() + print "Pass all test" + exit() + if (len(sys.argv) == 2) and (sys.argv[1] == "do"): + gp1 = { } + node_cmd_list = group_exp(gp1) + ename = gp1['ExperimentDomainName'] + + for (node, cmd) in node_cmd_list.items(): + cmdline = "echo '%s' | ssh %s.%s" % (cmd, node, ename) + subprocess.call(cmdline, shell = True) + +if __name__ == "__main__": + main() diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index a28542f..4ab68b4 100644 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -30,6 +30,15 @@ try: except ImportError: from cgi import parse_qs from urlparse import urlparse +# Switch to open function let URL path define path. +# &path=vncserver.domain.name:port +URL_PATH_DEF_VNCSERVER = True +# Whether is URL path encoded +URL_PATH_ENCODED = False +# URL valid day number after creating. +URL_VALID_DAYNUM = 1 +# Salt used to valide the hash of the URL. +URL_SALT = "Some salt for security. Please change it in your project. liqun@ncl.sg" class ProxyRequestHandler(websockifyserver.WebSockifyRequestHandler): @@ -104,6 +113,15 @@ Traffic Legend: msg = "connecting to command: '%s' (port %s)" % (" ".join(self.server.wrap_cmd), self.server.target_port) elif self.server.unix_target: msg = "connecting to unix socket: %s" % self.server.unix_target + elif URL_PATH_DEF_VNCSERVER: + import encode_url + (ptarget_host, ptarget_port) = encode_url.get_server_from_path(\ + self.path, URL_PATH_ENCODED, URL_VALID_DAYNUM, URL_SALT) + if ptarget_port == 0: + print 'Error: URL encode error[%s]' % ptarget_host + return + #raise self.server.EClose('Cannot decode path.') + msg = "connecting to: %s:%s" % (ptarget_host, ptarget_port) else: msg = "connecting to: %s:%s" % ( self.server.target_host, self.server.target_port) @@ -112,7 +130,14 @@ Traffic Legend: msg += " (using SSL)" self.log_message(msg) - tsock = websockifyserver.WebSockifyServer.socket(self.server.target_host, + if URL_PATH_DEF_VNCSERVER: + tsock = websockifyserver.WebSockifyServer.socket(ptarget_host, + ptarget_port, + connect=True, + use_ssl=self.server.ssl_target, + unix_socket=self.server.unix_target) + else: + tsock = websockifyserver.WebSockifyServer.socket(self.server.target_host, self.server.target_port, connect=True, use_ssl=self.server.ssl_target,