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