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