Yet another python Chat client

  • Tutorial
Приветствую, хабраюзер.
Уже была статья про чат-клиент на питоне на хабре. Данная статья и сподвигла написать свой велосипед в академических целях, но повторять чужой код не интересно, поставим задачу поинтереснее: Jabber(Асимитричное шифрование RSA)+PyQt.
Если интересно добро пожаловать под кат.

Конечно, не только это, а например и то, что чаты в соцсетях будут прослушиваться, и просто повысить свой скилл в написании программ на питоне.
Писался данный код под Debian, python 2.7, Qt 4.7, поэтому описывать буду для него, на других системах не проверялось.

Приступим


Определимся с форматом сообщений.
1. Запрос ключа
#getkey «Если вы видите это сообщение, значит необходимо поставить утилиту ...»
2. Посылку ключа
#sendkey 234234234
3. Сообщение
#mesg 123123123
4. Пересылка последнего сообщения (не реализовано)
#getlastmesg
Я решил, что #<что-то> неплохой выбор для обозначения команд, к тому же все сообщения проходят шифрование и сообщение вида #<что либо> будет отправлено корректно. Думаю, что можно было обойтись и без этого, просто хотелось красивее.

Начнем с простого, а именно с жаббир части.


Писать свой движок для жаббер-клиента интересно, но сейчас движемся на результат, поэтому возьмем уже готовый модуль xmpppy. Установим его командой
sudo easy_install xmpppy.
Можно, конечно, использовать сразу же данную библиотеку, но я думаю, лучше использовать нашу обертку, и вынести данный функционал в отдельный файл, который в будущем будет проще рефакторить, если возникнет такая надобность. Для работы данной библиотеки необходимо следующее: наш jid, наш пароль и колбек для пришедших сообещений.
jabber.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import xmpp,sys

#Данный фаил сожердит обертку для xmpp 
class sjabber:
	def __init__(self,xmpp_jid,xmpp_pwd):
		self.xmpp_jid = xmpp_jid
		self.xmpp_pwd = xmpp_pwd
		self.jid = xmpp.protocol.JID(xmpp_jid)
		self.client = xmpp.Client(self.jid.getDomain(),debug=[])
	def connect(self):
		con=self.client.connect()
		if not con:
			print 'could not connect!'
			sys.exit()
		print 'connected with',con
		auth = self.client.auth(self.jid.getNode(),str(self.xmpp_pwd),resource='xmpppy')
		if not auth:
			print 'could not authenticate!'
			sys.exit()
		print 'authenticated using',auth
		#Говорим серверу что мы онлайн! 
		self.client.sendInitPresence(1)
	def Process(self):
		a = self.client.Process(1)
	def send(self,to,mess):
		id = self.client.send(xmpp.protocol.Message(to,mess))
		print 'sent message with id',id
	def disconnect(self):
		self.client.sendInitPresence(1)
		self.client.disconnect()
	
	def userList(self):
		return self.client.getRoster().keys()

	def StepOn(self):
		try:
			self.Process()
		except:
			return 0
		return 1
	def setCB(self, CB):
		self.CB = CB
		self.client.RegisterHandler('message',self.messageCB)
	def messageCB(self,conn,mess):
		if ( mess.getBody() == None ):
			return
		self.CB(self,mess)



Данный код при создании инициализирует объекты для подключения. По команде connect подключается к серверу. Также имеет обертку для отправки сообщений. И по сути является только декоратором для кода библиотеки. Наличие большого количества уже готовых библиотек, на мой взгляд, является большим плюсом для питона и позволяет писать код в достаточно сжатые сроки.

Прикручиваем шифрование.


В качестве алгоритма шифрования я решил взять RSA,  просто потому что он мне нравится. К тому же он асимметричный, т.е. мы можем каждую сессию генерировать новые пары ключей и распространять только публичную часть. Таким образом, вместо сообщений третье лицо увидит только кучу HEX вместо сообщений.
Модуль шифрования я сделал отдельным по тем же самым причинам.

rsa_decor.py
# -*- coding: utf-8 -*-
import rsa

class Crypt:
	def __init__(self):
		#Словарь в котором будут храниться известные нам ключи
		self.keys = dict()
		#Генерируем и сохраняем наши ключи
		(a,b) = self.genKey(1024)
		self.privateKey = b
		self.pubKey = a
	def hasKey(self,id):
		#Проверяем на наличие ключа для контакта
		if self.keys.has_key(id)==False:
			return False
		else:
			return True
	def decodeKey(self,key):
		#Создаем публичный ключи и загружаем переданый по сети вариант
		return rsa.PublicKey(1,1).load_pkcs1(key,format='DER')
	def saveKey(self,id,key): 
		#Сохраняем ключ
		self.keys[id]= key
	def genKey(self,size):
		#Обертка для рса
		return  rsa.newkeys(size, poolsize=8)
	def cryptMesg(self,to,mesg):
		#Шифруем сообщение
		getHex =mesg.encode('utf-8').encode('hex')
		a = rsa.encrypt(getHex, self.keys[to])
		#print len(mesg),len(a)
		return a.encode('hex')
	def decryptMesg(self,mesg):
		#Пытаемся расшифровать сообщение, иначе выдаем ошибку
		try:
			mess = rsa.decrypt(mesg.decode("hex"),self.privateKey)
		except rsa.pkcs1.DecryptionError:
			print "cant decrypt"
			return "#errorDecrypt"
		return mess.decode('hex').decode('utf-8')



Тут тоже все просто и логично. Декорируем нужные нам функции, а также храним все что связанно с ключами (а точнее наши приватные и публичные ключи, словарь с известными нам ключами).

Приступим к главному


Данная часть была написана достаточно быстро, и было решено ваять главный модуль, собственно ядро приложения, которое бы связывало части.

Изначально было решено писать интерфейс на TK. Но получалось плохо, и я вспомнил, что питон умеет неплохо общаться с Qt.
Поэтому доставляем в систему Qt Designer и сам PyQt, на момент написания была версия 4.7 (к сожалению инсталляцию всего этого под Win подсказать не могу,  в линуксе все ставится пакетной системой вашего дистрибутива) установим
sudo apt-get install pyqt4-dev-tools libqt4-core libqt4-dev libqt4-gui python-qt4 qt4-designer
Этого набора пакетов должно хватить.
Поэтому начнем с рисования формы.
Запустим Qt Designer
Создадим форму main_widget.
Организуем следующим образом, центральный виджет
— вертикальный слой.
В нем расположим 2 виджета: горизонтальный слой, в котором будет место для ввода сообщения и кнопка для отправки, сплитеер, в котором будет текстовый браузер для отображения сообщений и лист-виджет, в который мы положим список контактов.
В итоге должно получиться вот так.

Останавливаться на работе QtDesigner не будем, он хорошо описан в документации (у Qt на редкость хорошая документация)
Готовый ui-файл.
Однако этот файл не готов для использования нами, необходимо превратить его в питоновский код, для этого нам необходима утилита pyuic4.
Воспользуемся ей.
pyuic4 main_window.ui -o gui.py
Теперь у нас есть файл с графикой, с шифрованием, с жаббером, осталось все вместе объединить.
Для его объединения напишем класс.
       def __init__(self):
                #Первым делом загрузим настройки
                self.loadConfig()
                #Создадим объект для шифрования
                self.crypt = Crypt()

                #Создадим и подключимся к жабберу
                self.jb = sjabber(self.xmpp_jid,self.xmpp_pwd)
                self.jb.connect()

                #Зададим колбек для приходящих сообщений
                self.jb.setCB(self.messageCB)
               #Создадим Qt-обработчик событий для графики
                self.app = QApplication(sys.argv)

                self.window = QMainWindow()
                self.ui = Ui_MainWindow()
                self.ui.setupUi(self.window)

Тут остановимся подробнее, в Qt существует система сигналов и слотов, для её обработки требуется класс QApplication, а так как графика использует именно их, то добавим его. После чего создадим окно и сами графические элементы (их мы создали выше), после чего разместим их в нашем окне.
               #Подключим сигналы нажатия на кнопку отправить и нажатие энтер
                self.ui.pushButton.clicked.connect(self.sendMsg)
                self.ui.lineEdit.returnPressed.connect(self.sendMsg)

                self.window.show()

                #А теперь заполним юзерлист 
                userList = self.jb.userList()
                for i in userList:
                        self.ui.listWidget.addItem(i)
                #Меняем пользователя для отправки сообщения
                self.ui.listWidget.currentRowChanged.connect(self.setToRow)
                #Выберем по умолчанию первого пользователя
                self.ui.listWidget.setCurrentRow(0)
                #Создадим рабочего который будет "дергать" обработчик жаббера
                self.workThread = WorkThread()
                self.workThread.setWorker(self.jb)
                self.workThread.start()

Данная реализация жаббер-клиента требует постоянного “подергивания” для обработки входящих сообщений, который к тому же блокирует основной поток, поэтому создадим отдельный класс рабочего, который будет жить в отдельном потоке и обслуживать жаббер-клиент. Что характерно, данный класс очень похож на Си++ код для Qt  для работы с потоками.
class WorkThread(QThread):
        def __init__(self):
                QThread.__init__(self)

        def setWorker(self,jb):
                self.jb = jb

        def run(self):
                while self.jb.StepOn(): pass

Собственно на этом наше приложение почти готово, за исключением колбека, обрабатывающего входящие сообщения (ну и немного другой мелочевки).
def messageCB(self,conn,mess)
       def messageCB(self,conn,mess):
                #Данный колбек проверяет регулярное выражение, после чего 
                #Либо работает с ключами, либо шифрует сообщения
                if ( mess.getBody() == None ):
                        return
                msg = mess.getBody()
                patern = re.compile('^#(getkey|sendkey|mesg|getlastmesg) ?(.*)')
                res = patern.search(msg)
                if res:
                        #Проверка
                        a = res.groups()[0]
                        if a == "getkey":
                                self.sendKey(mess.getFrom())
                                if  self.crypt.hasKey(mess.getFrom())!=False:
                                        conn.send(mess.getFrom(),"#getkey")     
                        elif a == "sendkey":
                                if res.groups()[1]!='':
                                        a = self.crypt.decodeKey(res.groups()[1].decode("hex"))
                                        self.crypt.saveKey(mess.getFrom().getStripped(),a)
                        elif a == "mesg":
                                decryptMess = self.crypt.decryptMesg(res.groups()[1])
                                if decryptMess=="#errorDecrypt":
                                        self.sendKey(mess.getFrom())
                                        self.print_("Error decrypt sendKey")
                                else:
                                        self.print_(self.to+"--> "+decryptMess)
                        elif a == "getlastmesg*":
                                print a



В качестве обработчика Я не стал придумывать ничего нового, поэтому сообщение проверяется регулярным выражением, в случае совпадения с оным, осуществляется переход на реакцию, соответствующего типу сообщения.

Ещё один ужас — это отправка сообщений. Дело в том, что стандартный алгоритм RSA может шифровать строки определенной длины, зависящей от размера ключа, что для 1024 байт составляет примерно 52 символа в юникоде, поэтому процедура делит строку на кусочки, которые шифрует и посылает. На мой взгляд, это ужасный костыль, однако моё знание питона не позволило мне сделать красивее.

Весь код вы можете наблюдать на гитхабе.

Собственно результат

Приветствуется конструктивная критика кода.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 3

    +3
    Я долгое время занимаюсь разработкой ПО на Qt с С++. Недавно решил попробовать PyQt. Вы не представляете что для меня эта статья… Это просто какой-то… нет слов.
      +1
      Посмотрите тут еще хорошо про Qt Designer. Там же чат, ну и покликать по блогу, хорошие примеры пользования дизайнером
      +2
      Ну вроде неплохо, но классы и общий стиль кода чудовищный. Попробуйте прочитать pep8 и придерживаться его

      Only users with full accounts can post comments. Log in, please.