cmiN

chat

Jun 26th, 2012
402
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.34 KB | None | 0 0
  1. #! /usr/bin/env python
  2. ## Simple Secure Chat
  3. ## 14.09.2011 cmiN
  4. """
  5. This is a simple CLI low level non blocking pythonic chat server and client (v1.0).
  6. In server mode accepts MAXC clients simultaneously and waits for reply.
  7. As client it works similar, but it uses a 'heartbeat' and a 'watcher' (see the classes below).
  8. Non standard modules: PyCrypto (https://www.dlitz.net/software/pycrypto/).
  9. Known bugs: in client mode if Ctrl+C is pressed it will make server crash somehow
  10. and the connection will remain open. I think timeout is changed automatically to None.
  11.  
  12. Contact: cmin764@gmail/yahoo.com
  13. """
  14.  
  15.  
  16. import time                     # heartbeat + private key seed
  17. import sys                      # some miscellaneous
  18. from socket import *            # interprocess comunication
  19. from threading import Thread    # multi client async behavior
  20. from hashlib import md5         # private key from time hash
  21. try:
  22.     from Crypto.Cipher import AES       # symmetric encryption
  23.     from Crypto.PublicKey import RSA    # asymmetric key exchange
  24. except ImportError as msg:
  25.     sys.stdout.write("[x] Error: {0}.".format(msg))
  26.     exit()
  27.  
  28.  
  29. TOUT = 5            # socket timeout in seconds
  30. PERC = 0.9          # waiting time percentage of TOUT as a pulse
  31. FILE = None         # you can replace this by a file name
  32. MAXC = 512          # max clients
  33. MAXL = 1024         # max transmitted data
  34. BUFF = 128          # limit data per send/recv
  35. TERM = "\r\n"       # data terminator
  36. BITS = 1024         # asymetric key size; smaller is faster but less secure (min 1024)
  37. NULL = '\x00'       # padding byte
  38. NAME = "guest"      # nickname initializer
  39. PORT = 51337
  40. HOST = gethostbyname(gethostname())
  41.  
  42.  
  43. class Logger:
  44.     """
  45.    Simple logging class.
  46.    Used for writing to both stdout and file.
  47.    """
  48.  
  49.     def __init__(self, fname):
  50.         self.status = False    # some kind of lock/semaphore
  51.         if fname:
  52.             self.fobj = open(fname, "a")
  53.         else:
  54.             self.fobj = fname
  55.  
  56.     def write(self, data):
  57.         while self.status:
  58.             pass
  59.         self.status = True    # lock and load :)
  60.         sys.stdout.write(data + '\n')
  61.         sys.stdout.flush()
  62.         if self.fobj:
  63.             self.fobj.write(data + '\n')
  64.             self.fobj.flush()
  65.         self.status = False
  66.  
  67.     def close(self):
  68.         if self.fobj:
  69.             self.fobj.close()
  70.  
  71.  
  72. def write(param, data):
  73.     """Bandwidth-friendly and no data loss."""
  74.     data = data[:MAXL]    # limit data
  75.     if type(param) is list:
  76.         sock = param[0]
  77.         while param[2]:    # while is busy
  78.             pass
  79.         param[2] = True    # edit globally by reference
  80.         if param[4]:
  81.             data = param[4].encrypt(data + NULL * (16 - len(data) % 16))    # encrypt it
  82.     else:
  83.         sock = param
  84.     data += TERM    # add the terminator
  85.     csize = 0
  86.     dsize = len(data)
  87.     while csize < dsize:    # split data in pieces
  88.         chunk = data[csize:(csize + BUFF)]
  89.         rem = len(chunk) - sock.send(chunk)    # and send it as chunks
  90.         while rem:    # if something remains
  91.             rem = rem - sock.send(chunk[-rem:])
  92.         csize += BUFF    # update the offset
  93.     if type(param) is list:
  94.         param[2] = False    # another lock
  95.  
  96.  
  97. def read(param, data):
  98.     """Same shit like write."""
  99.     if type(param) is list:
  100.         sock = param[0]
  101.     else:
  102.         sock = param
  103.     ind = data.find(TERM)    # set the index
  104.     while ind == -1:
  105.         data += sock.recv(BUFF)
  106.         ind = data.find(TERM)
  107.     first = data[:ind]
  108.     rem = data[(ind + len(TERM)):]
  109.     if type(param) is list and param[4]:
  110.         first = param[4].decrypt(first).strip(NULL)
  111.     return (first, rem)
  112.  
  113.  
  114. class Listener(Thread):
  115.     """
  116.    Every client is listened by a parallel thread.
  117.    When a message is received it's immediately processed.
  118.    Even the user is afk the client itself sends an 'update'.
  119.    """
  120.  
  121.     def __init__(self, ip):
  122.         Thread.__init__(self)
  123.         self.ip = ip
  124.         self.detail = clients[ip]    # [sock, nick, busy, [ignore], encryption]
  125.         self.to = ip    # who receives messages from this client (self by default)
  126.  
  127.     def run(self):
  128.         """Process all commands received from a client until an exception."""
  129.         data = str()    # a buffer for each client
  130.         try:    # key exchange
  131.             write(self.detail, full.publickey().exportKey())    # send public key as string encoded in base64 (default)
  132.             (line, data) = read(self.detail, data)    # receive client's encrypted symmetric key
  133.             key = full.decrypt(line)    # get the hexdigest hash
  134.             if len(key) == 16:
  135.                 sym = AES.new(key)    # with this we encrypt/decrypt sent/received data
  136.                 self.detail[4] = sym    # store the object in client
  137.                 write(self.detail, "Hello " + self.detail[1] + ", type /help to get all available commands.\n")
  138.                 while True:
  139.                     (line, data) = read(self.detail, data)
  140.                     if line[0] == '/':
  141.                         line = line[1:].split()
  142.                         if not line:
  143.                             write(self.detail, "Invalid command.\n")
  144.                             continue
  145.                         if line[0] == "help":
  146.                             write(self.detail, "\n".join(["/quit -> shutdown", "/mass -> to all", "/list -> see who's online",
  147.                                                           "/nick name -> change id", "/to nick -> to someone (default self)",
  148.                                                           "/block nick -> ignore", "/unblock nick -> accept"]) + '\n')
  149.                         elif line[0] == "quit":
  150.                             break
  151.                         elif line[0] == "update":
  152.                             write(self.detail, "/update")
  153.                         elif line[0] == "mass":
  154.                             if self.to:
  155.                                 self.to = None
  156.                                 write(self.detail, "Now talking to all.\n")
  157.                             else:
  158.                                 write(self.detail, "Already talking to all.\n")
  159.                         elif line[0] == "list":
  160.                             write(self.detail, "\n".join([cl[1][1] for cl in clients.items() if clients.has_key(cl[0])]) + '\n')
  161.                         elif line[0] == "to":
  162.                             if len(line) == 2:
  163.                                 ok = False
  164.                                 for cl in clients.items():
  165.                                     if clients.has_key(cl[0]) and cl[1][1] == line[1]:
  166.                                         ok = True
  167.                                         if self.to == cl[0]:
  168.                                             write(self.detail, "Already talking to {0}.\n".format(line[1]))
  169.                                         else:
  170.                                             self.to = cl[0]
  171.                                             write(self.detail, "Now talking to {0}.\n".format(line[1]))
  172.                                         break
  173.                                 if not ok:
  174.                                     write(self.detail, "Wrong user.\n")
  175.                             else:
  176.                                 write(self.detail, "Invalid command.\n")
  177.                         elif line[0] == "nick":
  178.                             if len(line) == 2:
  179.                                 ok = True
  180.                                 for cl in clients.items():
  181.                                     if clients.has_key(cl[0]) and cl[1][1] == line[1]:
  182.                                         ok = False
  183.                                         break
  184.                                 if ok:
  185.                                     self.detail[1] = line[1]
  186.                                     write(self.detail, "You are now {0}.\n".format(line[1]))
  187.                                 else:
  188.                                     write(self.detail, "Already in use.\n")
  189.                             else:
  190.                                 write(self.detail, "Invalid command.\n")
  191.                         elif line[0] == "block":
  192.                             if len(line) == 2:
  193.                                 ok = False
  194.                                 for cl in clients.items():
  195.                                     if clients.has_key(cl[0]) and cl[1][1] == line[1]:
  196.                                         ok = True
  197.                                         if cl[0] in self.detail[3]:
  198.                                             write(self.detail, "Already blocked.\n")
  199.                                         else:
  200.                                             self.detail[3].add(cl[0])
  201.                                             write(self.detail, "Added to ignore list.\n")
  202.                                         break
  203.                                 if not ok:
  204.                                     write(self.detail, "Wrong user.\n")
  205.                             else:
  206.                                 write(self.detail, "Invalid command.\n")
  207.                         elif line[0] == "unblock":
  208.                             if len(line) == 2:
  209.                                 ok = False
  210.                                 for cl in clients.items():
  211.                                     if clients.has_key(cl[0]) and cl[1][1] == line[1]:
  212.                                         ok = True
  213.                                         if cl[0] in self.detail[3]:
  214.                                             self.detail[3].remove(cl[0])
  215.                                             write(self.detail, "Removed from ignore list.\n")
  216.                                         else:
  217.                                             write(self.detail, "Isn't blocked.\n")
  218.                                         break
  219.                                 if not ok:
  220.                                     write(self.detail, "Wrong user.\n")
  221.                             else:
  222.                                 write(self.detail, "Invalid command.\n")
  223.                         else:
  224.                             write(self.detail, "Invalid command.\n")
  225.                     else:
  226.                         if self.to:
  227.                             if clients.has_key(self.to):
  228.                                 if self.ip not in clients[self.to][3]:
  229.                                     write(clients[self.to], self.detail[1] + ": " + line)
  230.                                 else:
  231.                                     write(self.detail, "User blocked you.\n")
  232.                             else:
  233.                                 write(self.detail, "User not in list.\n")
  234.                         else:
  235.                             for cl in clients.items():
  236.                                 if clients.has_key(cl[0]) and cl[0] != self.ip and self.ip not in clients[cl[0]][3]:
  237.                                     write(cl[1], self.detail[1] + ": " + line)
  238.         except error:
  239.             pass    # disconnected or timed out, so the client is dead
  240.         finally:
  241.             try:
  242.                 log.write("[i] Client {0} disconnected.".format(clients[self.ip][1]))
  243.                 del clients[self.ip]
  244.                 self.detail[0].shutdown(SHUT_RDWR)
  245.                 self.detail[0].close()
  246.             except:
  247.                 pass
  248.  
  249.  
  250. class Watcher(Thread):
  251.     """
  252.    Some kind of listener, but this time is for client.
  253.    Runs in parallel with blocking user input.
  254.    When something is received it's immediately printed.
  255.    Too bad if the console is not flushed in time, it will mix up with the output.
  256.    """
  257.  
  258.     def __init__(self, cl, data):
  259.         Thread.__init__(self)
  260.         self.cl = cl
  261.         self.data = data
  262.  
  263.     def run(self):
  264.         try:
  265.             while True:
  266.                 (line, self.data) = read(self.cl, self.data)
  267.                 if line != "/update":
  268.                     sys.stdout.write(line)
  269.         except error:
  270.             pass    # here the server is dead because the client itself sends an 'update' then server echoes it
  271.         finally:
  272.             try:
  273.                 self.cl[0].shutdown(SHUT_RDWR)
  274.                 self.cl[0].close()
  275.             except:
  276.                 pass
  277.  
  278.  
  279. class HeartBeat(Thread):
  280.     """
  281.    Parallel 'update' sender.
  282.    In this way both client and server knows about each other.
  283.    """
  284.  
  285.     def __init__(self, cl):
  286.         Thread.__init__(self)
  287.         self.cl = cl
  288.  
  289.     def run(self):
  290.         try:
  291.             while True:
  292.                 write(self.cl, "/update")
  293.                 time.sleep(PERC * TOUT)
  294.         except error:
  295.             pass    # same as above
  296.         finally:
  297.             try:
  298.                 self.cl[0].shutdown(SHUT_RDWR)
  299.                 self.cl[0].close()
  300.             except:
  301.                 pass
  302.  
  303.  
  304. def client():
  305.     """Connect to a server with this protocol."""
  306.     log.write("\n{0} {1} started as {2}.".format(time.ctime(), HOST, "client"))
  307.     data = str()    # client's buffer
  308.     key = md5(str(time.time())).digest()    # 128bit key
  309.     sym = AES.new(key)    # like above, symmetric encryption (much faster)
  310.     cl = [socket(AF_INET, SOCK_STREAM), None, False, None, sym]
  311.     ans = raw_input("Server: ")
  312.     try:
  313.         log.write("[i] Connecting...")
  314.         cl[0].connect((gethostbyname(ans), PORT))    # connect to server
  315.         (line, data) = read(cl[0], data)    # receive a message
  316.         if line[0] == '-':    # or the public key
  317.             half = RSA.importKey(line)    # import it
  318.             write(cl[0], half.encrypt(key, 32)[0])    # send that hash (symmetric key), encrypted
  319.             (line, data) = read(cl, data)    # hello
  320.             log.write("[i] Handshake successful.")
  321.             sys.stdout.write(line)
  322.             Watcher(cl, data).start()
  323.             HeartBeat(cl).start()
  324.             while True:
  325.                 msg = raw_input()
  326.                 write(cl, msg + '\n')
  327.                 if msg == "/quit":
  328.                     break
  329.         else:
  330.             sys.stdout.write(line)
  331.     except (error, gaierror) as msg:
  332.         raise Exception(msg)    # timed out or invalid server
  333.     finally:
  334.         try:
  335.             cl[0].shutdown(SHUT_RDWR)
  336.             cl[0].close()
  337.         except:
  338.             pass
  339.  
  340.  
  341. def server():
  342.     """Listen to MAXC clients, each in a separate thread."""
  343.     global clients, full    # extends the visibility domain
  344.     log.write("\n{0} {1}:{2} started as {3}.".format(time.ctime(), HOST, PORT, "server"))
  345.     full = RSA.generate(BITS)    # full key (private + public)
  346.     log.write("[i] Asymmetric key generated.")
  347.     clients = dict()    # with ip = [sock, nick, busy, [ignore], encryption]
  348.     sv = socket(AF_INET, SOCK_STREAM)    # create the socket object
  349.     sv.bind((HOST, PORT))    # open a port to local address
  350.     sv.listen(5)    # queue not-accepted (can be 1)
  351.     log.write("[i] Listening...")
  352.     while True:
  353.         try:
  354.             conn = sv.accept()    # (sock, ip)
  355.             if clients.has_key(conn[1][0]):
  356.                 write(conn[0], "You again ?\n")
  357.                 continue
  358.             if len(clients) >= MAXC:
  359.                 write(conn[0], "Too many, please reconnect later.\n")
  360.                 log.write("[i] Maximum number of clients reached.")
  361.                 continue
  362.             idnr = 1
  363.             ids = set()
  364.             for cl in clients.items():
  365.                 if clients.has_key(cl[0]) and cl[1][1][:len(NAME)] == NAME and cl[1][1][len(NAME):].isdigit():
  366.                     ids.add(int(cl[1][1][len(NAME):]))
  367.             while idnr in ids:
  368.                 idnr += 1
  369.             clients[conn[1][0]] = [conn[0], NAME + str(idnr), False, set(), None]    # add client
  370.             log.write("[i] Client connected as {0}.".format(NAME + str(idnr)))
  371.             Listener(conn[1][0]).start()
  372.         except error:
  373.             pass    # here timeout is normal
  374.  
  375.  
  376. def main():
  377.     """Main function, not executed when module is imported."""
  378.     global log    # extends the visibility domain
  379.     try:
  380.         log = Logger(FILE)
  381.         setdefaulttimeout(TOUT)    # non blocking
  382.         ans = int(input("1. Client\n2. Server\n"))
  383.         if ans == 1:
  384.             client()
  385.         else:
  386.             server()
  387.     except Exception as msg:
  388.         log.write("[x] Error: {0}.".format(msg))
  389.     except:
  390.         log.write("[!] Forcibly closed.")
  391.     finally:
  392.         log.close()
  393.  
  394.  
  395. if __name__ == "__main__":
  396.     main()
Add Comment
Please, Sign In to add comment