Как стать автором
Обновить

Yet another python Chat client

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

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

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

Приветствуется конструктивная критика кода.
Теги:
Хабы:
Всего голосов 27: ↑21 и ↓6+15
Комментарии3

Публикации

Истории

Работа

Python разработчик
102 вакансии
Data Scientist
64 вакансии

Ближайшие события

28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань