Yet another python Chat client

от автора

Приветствую, хабраюзер.
Уже была статья про чат-клиент на питоне на хабре. Данная статья и сподвигла написать свой велосипед в академических целях, но повторять чужой код не интересно, поставим задачу поинтереснее: 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 символа в юникоде, поэтому процедура делит строку на кусочки, которые шифрует и посылает. На мой взгляд, это ужасный костыль, однако моё знание питона не позволило мне сделать красивее.

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

Приветствуется конструктивная критика кода.

ссылка на оригинал статьи http://habrahabr.ru/post/165243/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *