Addded README

Code cleanup
This commit is contained in:
Matias Fernandez
2019-09-11 22:00:39 -03:00
parent a6327060f9
commit aad1e08f6a
11 changed files with 476 additions and 216 deletions

23
README.md Normal file
View File

@@ -0,0 +1,23 @@
# PyVNCs
Simple command line multiplatform python VNC Server.
This is a simple command line VNC server, aimed to quick remote support situations.
This VNC Server is proven to work on:
- Linux (Xorg, Wayland not supported so far)
- Mac OS
- Windows (7 and onwards)
Supported encodings:
- raw
- zlib
TODO:
- Add support for more (efficient) encodings
- VEnCrypt support (WIP)
- Add support for uPnP (for incomming connections on supported nat gateways)
FAQ:
Q: Why a VNC server on python?
A: Because python is fun!

88
lib/auth/vencrypt.py Normal file
View File

@@ -0,0 +1,88 @@
from lib import log
from time import sleep
import ssl
import select
from struct import *
class VeNCrypt():
subtypes = [
256, # Plain
#258, # TLSVnc # FIXME: not yet implemented
#259, # TLSPlain # FIXME: not yet implemented
]
def __init__(self, sock):
self.getbuff = lambda _: None
self.sock = sock
self.client_subtype = None
self.pem_file = None
log.debug(__name__, "initialized")
# send version
version = b'\x00\x02' # 0.2
sock.send(version)
data = sock.recv(2)
if data != version:
sock.send(b'\x01')
sock.close()
raise Exception("unknown vencrypt version")
sock.send(b'\x00')
def send_subtypes(self):
# send subtypes
data = pack('!B', len(self.subtypes))
for i in self.subtypes:
data += pack('!I', i)
log.debug(__name__, "subtype", i)
self.sock.send(data)
# get client choosen subtype
data = self.sock.recv(4)
(data,) = unpack('!I', data)
log.debug("client subtype", data)
self.client_subtype = data
def auth_plain(self, userlist={}):
data = self.sock.recv(8)
user_length, pass_length = unpack('!II', data)
username = self.sock.recv(user_length).decode()
password = self.sock.recv(pass_length).decode()
#log.debug("user", username, password)
if userlist.get(username) == password:
self.sock.send(pack("!I", 0))
log.debug(__name__, "Auth OK")
return True
else:
log.debug(__name__, "Invalid auth")
sleep(3)
self.sock.send(pack("!I", 1))
return False
def auth_tls_plain(self, userlist={}):
#TODO: implement TLS plain
log.debug(__name__, 'Using TLSPlain')
self.sock.sendall(pack("!I", 1)) # send ACK
#data = self.getbuff(30)
#print("data", data)
#sslctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
sslctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER)
sslctx.protocol = ssl.PROTOCOL_TLS
#sslctx.load_cert_chain(certfile=self.pem_file, keyfile=self.pem_file)
# this is quite insecure...
sslctx.set_ciphers(":aNULL:kDHE:kEDH:ADH:DH:kECDHE:kEECDH:AECDH:ECDH")
self.sock.settimeout(30)
self.sock = sslctx.wrap_socket(self.sock, server_side=True)
self.sock.settimeout(None)
ret = self.auth_plain(userlist=userlist)
return ret

45
lib/auth/vnc_auth.py Normal file
View File

@@ -0,0 +1,45 @@
from time import sleep
from struct import *
from pyDes import *
import os
from lib import log
class VNCAuth():
def __init__(self):
self.getbuff = lambda _: None
def _mirrorBits(self, key):
newkey = []
for ki in range(len(key)):
bsrc = key[ki]
btgt = 0
for i in range(8):
if ord(bsrc) & (1 << i):
btgt = btgt | (1 << 7-i)
newkey.append(btgt)
return newkey
def auth(self, sock, password):
# el cliente encripta el challenge con la contraseña ingresada como key
pw = (password + '\0' * 8)[:8]
challenge = os.urandom(16) # challenge
sock.send(challenge) # send challenge
# obtener desde el cliente el dato encritado
data = self.getbuff(30)
# la encriptacion de challenge, con pw como key debe dar data
k = des(self._mirrorBits(pw))
crypted = k.encrypt(challenge)
if data == crypted:
# Handshake successful
sock.send(pack("!I", 0))
log.debug(__name__, "Auth OK")
return True
else:
log.debug(__name__, "Invalid auth")
sleep(3)
sock.send(pack("!I", 1))
return False

30
lib/clipboardctrl.py Normal file
View File

@@ -0,0 +1,30 @@
from struct import *
class ClipboardController():
def __init__(self):
pass
def client_cut_text(self, sock):
"""
The client has new ISO 8859-1 (Latin-1) text in its cut buffer.
Ends of lines are represented by the linefeed / newline character (value 10) alone. No carriage-return (value 13) is needed.
No. of bytes Type [Value] Description
1 U8 6 message-type
3 padding
4 U32 length
length U8 array text
"""
# read padding
_ = sock.recv(3)
# read length
length = sock.recv(4)
(length, ) = unpack('!I', length)
# read text
text = sock.recv(length)
return text

View File

@@ -66,3 +66,9 @@ class proc:
def waitproc(self):
while psutil.pid_exists(self.pid):
time.sleep(.25)
def reshape(a, cols):
for i in range(0, int(len(a)/cols), cols):
print(i)
print(a[i:i+cols])

View File

@@ -15,14 +15,41 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import inspect
import logging
logging.basicConfig(level=logging.DEBUG, format='[%(threadName)s] %(message)s')
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s: %(message)s')
logger = logging.getLogger('pyvncs')
def _log(*args, logtype='debug'):
func = inspect.stack()[2][3]
if func[0] != '<':
func = "%s():" % func
_str = func
def debug(*args):
str = ""
for s in args:
str = "%s %s" % (str, s)
str = str.strip()
logger.debug(str)
_str = "%s %s" % (_str, s)
_str = _str.strip()
f = getattr(logger, logtype)
#logger.debug(str)
f(_str)
def __getattr__(name):
def method(*args):
_str = ''
if args:
for s in args:
_str = "%s %s" % (_str, s)
_str = _str.strip()
_log(_str, logtype=name)
return method

109
lib/rfb_bitmap.py Normal file
View File

@@ -0,0 +1,109 @@
import numpy as np
from PIL import Image, ImageChops, ImageDraw, ImagePalette
from lib import bgr233_palette
__all__ = ['RfbBitmap']
class RfbBitmap():
def __init__(self):
self.bpp = None
self.depth = None
self.truecolor = None
self.primaryOrder = 'rgb'
self.dither = False
self.red_shift = None
self.green_shift = None
self.blue_shift = None
def __quantizetopalette(self, silf, palette, dither=False):
"""Converts an RGB or L mode image to use a given P image's palette."""
silf.load()
# use palette from reference image
palette.load()
if palette.mode != "P":
raise ValueError("bad mode for palette image")
if silf.mode != "RGB" and silf.mode != "L":
raise ValueError(
"only RGB or L mode images can be quantized to a palette"
)
im = silf.im.convert("P", 1 if dither else 0, palette.im)
# the 0 above means turn OFF dithering
# Later versions of Pillow (4.x) rename _makeself to _new
try:
return silf._new(im)
except AttributeError:
return silf._makeself(im)
def get_bitmap(self, rectangle):
if self.bpp == 32:
redBits = 8
greenBits = 8
blueBits = 8
# image array
a = np.asarray(rectangle).copy()
redMask = ((1 << redBits) - 1) << self.red_shift
greenMask = ((1 << greenBits) - 1) << self.green_shift
blueMask = ((1 << blueBits) - 1) << self.blue_shift
a[..., 0] = ( a[..., 0] ) & redMask >> self.red_shift
a[..., 1] = ( a[..., 1] ) & greenMask >> self.green_shift
a[..., 2] = ( a[..., 2] ) & blueMask >> self.blue_shift
image = Image.fromarray(a)
if self.primaryOrder == "rgb":
(b, g, r) = image.split()
image = Image.merge("RGB", (r, g, b))
del b,g,r
image = image.convert("RGBX")
return image
elif self.bpp == 16: #BGR565
greenBits = 5
blueBits = 6
redBits = 5
image = rectangle
if self.primaryOrder == "bgr": # FIXME: does not work
(b, g, r) = image.split()
image = Image.merge("RGB", (r, g, b))
if self.depth == 16:
image = image.convert('BGR;16')
if self.depth == 15:
image = image.convert('BGR;15')
return image
elif self.bpp == 8: #bgr233
redBits = 3
greenBits = 3
blueBits = 2
image = rectangle
palette = bgr233_palette.palette
if self.primaryOrder == "rgb":
#(b, g, r) = image.split()
#image = Image.merge("RGB", (r, g, b))
palette = np.reshape(palette, (-3,3))
palette[:,[0, 2]] = palette[:,[2, 0]]
palette = palette.flatten()
palette = list(palette)
p = Image.new('P',(16,16))
p.putpalette(palette)
image = self.__quantizetopalette(image, p, dither=self.dither)
#image = image.convert('RGB', colors=4).quantize(palette=p)
#log.debug(image)
return image
else:
# unsupported BPP
return None

View File

@@ -30,94 +30,52 @@ import numpy as np
from lib import mousectrl
from lib import kbdctrl
from lib import clipboardctrl
from lib.imagegrab import ImageGrab
from lib.rfb_bitmap import RfbBitmap
from lib import log
from lib import bgr233_palette
# encodings support
import lib.encodings as encs
from lib.encodings.common import ENCODINGS
def hexdump(data):
str = ""
for d in data:
str += hex(d)
str += "(%s) " % d
return str
def quantizetopalette(silf, palette, dither=False):
"""Converts an RGB or L mode image to use a given P image's palette."""
silf.load()
# use palette from reference image
palette.load()
if palette.mode != "P":
raise ValueError("bad mode for palette image")
if silf.mode != "RGB" and silf.mode != "L":
raise ValueError(
"only RGB or L mode images can be quantized to a palette"
)
im = silf.im.convert("P", 1 if dither else 0, palette.im)
# the 0 above means turn OFF dithering
# Later versions of Pillow (4.x) rename _makeself to _new
try:
return silf._new(im)
except AttributeError:
return silf._makeself(im)
# auth support
from lib.auth.vnc_auth import VNCAuth
from lib.auth.vencrypt import VeNCrypt
class VncServer():
class CONFIG:
_8bitdither = False
class RFB_SECTYPES:
vncauth = 2 # plain VNC auth
vencrypt = 19 # VeNCrypt
unix = 129 # Unix Login Authentication
encoding_object = None
def __init__(self, socket, password):
def __init__(self, socket, password=None, auth_type=None, pem_file='', vnc_config = None):
self.RFB_VERSION = '003.008'
self.RFB_SECTYPES = [
2, # VNC auth
19 # VeNCrypt
]
self.initmsg = ("RFB %s\n" % self.RFB_VERSION)
self.socket = socket
self.framebuffer = None
self.password = password
self.sectypes = self.RFB_SECTYPES
self.cursor_support = False
self.auth_type = auth_type
self.pem_file = pem_file
self.vnc_config = vnc_config
log.debug("Configured auth type:", self.auth_type)
def __del__(self):
log.debug("VncServer died")
def encrypt(self, key, data):
k = des(key, ECB, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5)
d = k.encrypt(data)
return d
def decrypt(self, challenge, data):
k = des(challenge, ECB, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5)
return k.decrypt(data)
def mirrorBits(self, key):
newkey = []
for ki in range(len(key)):
bsrc = key[ki]
btgt = 0
for i in range(8):
if ord(bsrc) & (1 << i):
btgt = btgt | (1 << 7-i)
newkey.append(btgt)
return newkey
def sendmessage(self, message):
''' sends a RFB message, usually an error message '''
sock = self.socket
message = bytes(message, 'iso8859-1')
# 4 bytes lenght and string
buff = pack("I%ds" % (len(message),), len(message), message)
sock.send(message)
sock.send(buff)
def getbuff(self, timeout):
sock = self.socket
@@ -149,17 +107,27 @@ class VncServer():
log.debug("client, server:", client_version, server_version)
# security types handshake
sendbuff = pack("B", len(self.sectypes)) # number of security types
sendbuff += pack('%sB' % len(self.sectypes), *self.sectypes) # send available sec types
# sectypes = [
# self.RFB_SECTYPES.vncauth,
# self.RFB_SECTYPES.vencrypt
# ]
sectypes = [
self.auth_type
]
log.debug('sectypes', sectypes)
sendbuff = pack("B", len(sectypes)) # number of security types
sendbuff += pack('%sB' % len(sectypes), *sectypes) # send available sec types
sock.send(sendbuff)
# get client choosen security type
data = self.getbuff(30)
try:
sectype = unpack("B", data)[0]
except:
sectype = None
if sectype not in self.sectypes:
if sectype not in sectypes:
log.debug("Incompatible security type: %s" % data)
sock.send(pack("B", 1)) # failed handshake
self.sendmessage("Incompatible security type")
@@ -169,28 +137,57 @@ class VncServer():
log.debug("sec type data: %s" % data)
# VNC Auth
if sectype == 2:
# el cliente encripta el challenge con la contraseña ingresada como key
pw = (self.password + '\0' * 8)[:8]
challenge = os.urandom(16) # challenge
sock.send(challenge) # send challenge
# obtener desde el cliente el dato encritado
data = self.getbuff(30)
# la encriptacion de challenge, con pw como key debe dar data
k = des(self.mirrorBits(pw))
crypted = k.encrypt(challenge)
if data == crypted:
# Handshake successful
sock.send(pack("I", 0))
log.debug("Auth OK")
else:
log.debug("Invalid auth")
if sectype == self.RFB_SECTYPES.vncauth:
auth = VNCAuth()
auth.getbuff = self.getbuff
if not auth.auth(sock, self.password):
msg = "Auth failed."
sendbuff = pack("I", len(msg))
sendbuff += msg.encode()
sock.send(sendbuff)
sock.close()
return False
# VeNCrypt
elif sectype == self.RFB_SECTYPES.vencrypt:
userlist = {}
try:
userlist[self.password.split(':')[0]] = self.password.split(':')[1]
except Exception as ex:
log.debug("Unable to parse username:password combination.\n%s" % ex)
sock.close()
return False
auth = VeNCrypt(sock)
auth.getbuff = self.getbuff
auth.send_subtypes()
client_subtype = auth.client_subtype
if client_subtype == 256: # Vencrypt Plain auth
if not auth.auth_plain(userlist):
sock.close()
return False
if client_subtype == 259: # Vencrypt TLSPlain auth
auth.pem_file = self.pem_file
auth.socket = self.socket
if not auth.auth_tls_plain(userlist):
sock.close()
return False
else:
# unsupported subtype
log.debug("Unsuported client_subtype", client_subtype)
sock.close()
return False
#elif sectype == self.RFB_SECTYPES.unix:
# log.debug("UNIX!!")
#unsupported VNC auth type
else:
log.debug("Unsupported auth type")
sock.close()
return False
# get ClientInit
@@ -216,7 +213,7 @@ class VncServer():
height = size[1]
self.height = height
bpp = 32 # FIXME: get real bpp
depth = 32 # FIXME: get real depth
depth = 24 # FIXME: get real depth
self.depth = depth
self.bpp = bpp
bigendian = 0
@@ -233,13 +230,14 @@ class VncServer():
self.green_shift = green_shift
blue_shift = 0
self.blue_shift = blue_shift
self.rfb_bitmap = RfbBitmap()
sendbuff = pack("!HH", width, height)
sendbuff += pack("!BBBB", bpp, depth, bigendian, truecolor)
sendbuff += pack("!HHHBBB", red_maximum, green_maximum, blue_maximum, red_shift, green_shift, blue_shift)
sendbuff += pack("!xxx") # padding
desktop_name = "Test VNC"
desktop_name = self.vnc_config.win_title
desktop_name_len = len(desktop_name)
sendbuff += pack("!I", desktop_name_len)
@@ -261,6 +259,7 @@ class VncServer():
mousecontroller = mousectrl.MouseController()
kbdcontroller = kbdctrl.KeyboardController()
clipboardcontroller = clipboardctrl.ClipboardController()
self.primaryOrder = "rgb"
self.encoding = ENCODINGS.raw
@@ -358,6 +357,10 @@ class VncServer():
mousecontroller.process_event(sock.recv(5, socket.MSG_WAITALL))
continue
if data[0] == 6: # ClientCutText
text = clipboardcontroller.client_cut_text(sock)
log.debug("ClientCutText:", text)
else:
data2 = sock.recv(4096)
log.debug("RAW Server received data:", repr(data[0]) , data+data2)
@@ -421,66 +424,17 @@ class VncServer():
sock.settimeout(None)
if self.bpp == 32 or self.bpp == 16 or self.bpp == 8:
bitmap = self.rfb_bitmap
bitmap.bpp = self.bpp
bitmap.depth = self.depth
bitmap.dither = self.vnc_config.eightbitdither
bitmap.primaryOrder = self.primaryOrder
bitmap.truecolor = self.truecolor
bitmap.red_shift = self.red_shift
bitmap.green_shift = self.green_shift
bitmap.blue_shift = self.blue_shift
if self.bpp == 32:
redBits = 8
greenBits = 8
blueBits = 8
# image array
a = np.asarray(rectangle).copy()
if self.primaryOrder == "bgr": # bit shifting
blueMask = (1 << blueBits) - 1
greenMask = ((1 << greenBits) - 1) << self.green_shift
redMask = ((1 << redBits) - 1) << self.red_shift
a[..., 0] = ( a[..., 0] ) & blueMask >> self.blue_shift
a[..., 1] = ( a[..., 1] ) & greenMask >> self.green_shift
a[..., 2] = ( a[..., 2] ) & redMask >> self.red_shift
else: # RGB
redMask = ((1 << redBits) - 1) << self.red_shift
greenMask = ((1 << greenBits) - 1) << self.green_shift
blueMask = ((1 << blueBits) - 1) << self.blue_shift
a[..., 0] = ( a[..., 0] ) & redMask >> self.red_shift
a[..., 1] = ( a[..., 1] ) & greenMask >> self.green_shift
a[..., 2] = ( a[..., 2] ) & blueMask >> self.blue_shift
image = Image.fromarray(a)
if self.primaryOrder == "rgb":
(b, g, r) = image.split()
image = Image.merge("RGB", (r, g, b))
del b,g,r
image = image.convert("RGBX")
if self.bpp == 16: #BGR565
greenBits = 5
blueBits = 6
redBits = 5
image = rectangle
if self.depth == 16:
image = image.convert('BGR;16')
if self.depth == 15:
image = image.convert('BGR;15')
elif self.bpp == 8: #bgr233
redBits = 3
greenBits = 3
blueBits = 2
image = rectangle
p = Image.new('P',(16,16))
p.putpalette(bgr233_palette.palette)
image = quantizetopalette(image, p, dither=self.CONFIG._8bitdither)
#image = image.convert('RGB', colors=4).quantize(palette=p)
#log.debug(image)
image = bitmap.get_bitmap(rectangle)
# send image with client defined encoding
sendbuff.extend(self.encoding_object.send_image(x, y, w, h, image))

16
test.py
View File

@@ -1,16 +0,0 @@
from lib import common
import time
if __name__ == '__main__':
#c = ["/bin/ls", "-alh"]
#c = ["/bin/sleep", "5"]
c = ["./test.sh"]
r = common.proc()
r.run(c)
print("PID", r.getpid())
#print("Waiting...")
#r.waitproc()
time.sleep(1)
r.terminate()
del r
print("DONE")

View File

@@ -1,6 +0,0 @@
#!/bin/bash
for a in $(seq 1 5); do
echo "$a $$"
sleep 1
done

View File

@@ -7,11 +7,11 @@ from threading import Thread
from time import sleep
import sys
import socket
import ssl
import signal
from lib import log
_debug = log.debug
#_debug = print
def signal_handler(signal, frame):
_debug("Exiting on %s signal..." % signal)
@@ -19,79 +19,84 @@ def signal_handler(signal, frame):
signal.signal(signal.SIGINT, signal_handler)
class ControlThread(Thread):
def __init__(self, threads):
Thread.__init__(self)
self.threads = threads
self.setDaemon(True)
def run(self):
# elimina los threads muertos
while True:
sleep(1)
for t in threads:
if not t.isAlive():
_debug("ControlThread removing dead", t)
threads.remove(t)
class ClientThread(Thread):
def __init__(self, sock, ip, port):
def __init__(self, sock, ip, port, vnc_config):
Thread.__init__(self)
self.ip = ip
self.port = port
self.sock = sock
self.setDaemon(True)
self.vnc_config = vnc_config
def __del__(self):
_debug("ClientThread died")
def run(self):
_debug("[+] New server socket thread started for " + self.ip + ":" + str(self.port))
#_debug("Thread", self)
server = pyvncs.server.VncServer(self.sock, VNC_PASSWORD)
server.CONFIG._8bitdither = CONFIG._8bitdither
server = pyvncs.server.VncServer(self.sock,
auth_type=self.vnc_config.auth_type,
password=self.vnc_config.vnc_password,
pem_file=self.vnc_config.pem_file,
vnc_config=self.vnc_config
)
#server.vnc_config.eightbitdither = self.vnc_config.eightbitdither
status = server.init()
if not status:
_debug("Error negotiating client init")
return False
server.protocol()
def main(argv):
global CONFIG, TCP_IP, TCP_PORT, VNC_PASSWORD, threads, controlthread
class CONFIG:
_8bitdither = False
class vnc_config:
pass
parser = ArgumentParser()
parser.add_argument("-l", "--listen-address", dest="TCP_IP",
parser.add_argument("-l", "--listen-address", dest="listen_addr",
help="Listen in this address, default: %s" % ("0.0.0.0"), required=False, default='0.0.0.0')
parser.add_argument("-p", "--port", dest="TCP_PORT",
parser.add_argument("-p", "--port", dest="listen_port",
help="Listen in this port, default: %s" % ("5901"), type=int, required=False, default='5901')
parser.add_argument("-P", "--password", help="Sets password", required=True, dest="VNC_PASSWORD")
parser.add_argument("-A", "--auth-type",
help="Sets VNC authentication type (supported: 2(vnc), 19(vencrypt))",
required=False,
type=int,
default=2,
dest="auth_type"
)
parser.add_argument("-C", "--cert-file",
help="SSL PEM file",
required=False,
type=str,
default='',
dest='pem_file'
)
parser.add_argument("-P", "--vncpassword", help="Sets authentication password", required=True, dest="vnc_password")
parser.add_argument("-8", "--8bitdither", help="Enable 8 bit dithering", required=False, action='store_true', dest="dither")
parser.add_argument("-O", "--output-file", help="Redirects all debug output to file", required=False, dest="OUTFILE")
parser.add_argument("-O", "--output-file", help="Redirects all debug output to file", required=False, dest="outfile")
parser.add_argument("-t", "--title", help="VNC Window title", required=False, dest="win_title", default="pyvncs")
args = parser.parse_args()
if args.OUTFILE is not None:
fsock = open(args.OUTFILE, 'w')
if args.outfile is not None:
try:
fsock = open(args.outfile, 'w')
except Exception as ex:
print("Error:", ex, file=sys.stderr)
sys.exit(1)
sys.stdout = sys.stderr = fsock
# Multithreaded Python server
TCP_IP = '0.0.0.0' if not hasattr(args,"TCP_IP") else args.TCP_IP
TCP_PORT = '5901' if not hasattr(args,"TCP_PORT") else args.TCP_PORT
VNC_PASSWORD = args.VNC_PASSWORD
CONFIG._8bitdither = args.dither
vnc_config.vnc_password = args.vnc_password
vnc_config.eightbitdither = args.dither
vnc_config.auth_type = args.auth_type
vnc_config.pem_file = args.pem_file
vnc_config.win_title = args.win_title
sockServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sockServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sockServer.bind((TCP_IP, TCP_PORT))
controlthread = ControlThread(threads)
controlthread.start()
threads.append(controlthread)
sockServer.bind((args.listen_addr, args.listen_port))
_debug("Multithreaded Python server : Waiting for connections from TCP clients...")
_debug("Runing on:", sys.platform)
@@ -111,21 +116,16 @@ def main(argv):
while True:
sockServer.listen(4)
(conn, (ip,port)) = sockServer.accept()
newthread = ClientThread(conn, ip, port)
newthread = ClientThread(sock=conn, ip=ip, port=port, vnc_config=vnc_config)
newthread.setDaemon(True)
newthread.start()
threads.append(newthread)
#print(threads)
if __name__ == "__main__":
try:
threads = []
main(sys.argv)
except KeyboardInterrupt:
# quit
_debug("Exiting on ctrl+c...")
#for t in threads:
# _debug("Killing", t)
sys.exit()