Python socket module ssl

Пишем SSL туннель на python

Возникла задача: есть приложение под Windows, которое делает HTTPS-запросы к серверу и получает ответы. После обновления сервера приложение перестало работать. Выяснилось, что на сервере изменилась версия SSL (перешли с SSLv3 на TLSv1), а наше приложение умеет работать только по SSLv3. Приложение никто не поддерживает уже давно и менять, перекомпилировать, тестировать не хотелось. Решено было сделать прослойку между приложением и сервером, которая будет транслировать SSLv3 в TLSv1 и наоборот. Я поискал какой-нибудь прокси в интернете, но сходу не нашел (плохо искал). Решил сделать прокси на питоне. Я не профессионал в питоне, но мне показалось что для этой задачи он хорошо подходит, и интересно параллельно по изучать питон на примере реальной задачи.

Начало

Итак, устанавливаем питон 3.4. Пишем скрипт, я для этого использовал блокнот. Для ssl-сокетов понадобится модуль ssl. Для, собственно, сокетов socket.

Создаем сокет, слушающий клиента, т.к. это будет SSL-сервер, то придется создать для него само-подписанный сертификат, который он будет предоставлять клиенту. Для создания сертификата, я использовал утилиту openssl. Скачал утилиту отсюда indy.fulgan.com/SSL. Для создания сертификата потребуется конфиг для утилиты, пример можно взять здесь web.mit.edu/crypto/openssl.cnf. Кладем конфиг в папку на компе и устанавливаем путь к нему (далее все действия в командной строке):

set OPENSSL_CONF=путь_к_файлу\openssl.cnf 
openssl genrsa -des3 -out server.key 1024 

Попутно будет предложено ввести пароль к ключу и подтверждение пароля, вводим. Создаем запрос на сертификат

openssl req -new -key server.key -out server.csr 

При генерации запроса нам нужно будет ввести пароль ключа и заполнить информацию о компании, городе, стране и т.д. Заполняем. Для того, чтобы можно было использовать ключ без пароля, копируем его и распароливаем

copy server.key server.key.org openssl rsa -in server.key.org -out server.key 
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt 

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

sock = ssl.wrap_socket(socket.socket(), 'server.key', 'server.crt', True) sock.bind( ('localhost', 43433) ) sock.listen(10) 
conn, addr = sock.accept() data = conn.recv(1024) 

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

serv = ssl.wrap_socket(socket.socket()) serv.connect( ('server_url', 443) ) serv.send(data) 
data = serv.recv(1024) conn.send(data) 

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

Подключим модуль logging, настроим конфигурацию логирования и добавим логирование в интересные места

import logging logging.basicConfig(filename = "proxy.log", level = logging.DEBUG, format = "%(asctime)s - %(message)s") logging.info("Ждем входящее соединение"); conn, addr = sock.accept() logging.info("Получаем запрос") data = conn.recv(1024) logging.info(data) logging.info("Отправляем запрос на сервер") serv.send(data) logging.info("Получаем ответ сервера") data = serv.recv(1024) logging.info(data) logging.info("Отдаем ответ клиенту") client.send(resp) 

Оказалось что данные клиент передает по блокам, т.е. мы прочитали не полный запрос. Потом выяснится, что сервер также отдает ответ по блокам. Усовершенствуем наш код чтобы читать запрос и ответ по блокам. Для этого создаем буфер, в который будем складывать весь запрос, устанавливаем сокету таймаут 0.1 с, который он будет ждать данные от входящего соединения и в цикле читаем и складываем в буфер данные. Если данных не будет, то получим исключение и выйдем из цикла

logging.info("Получаем запрос") data = conn.recv(1024) req = b'' conn.settimeout(0.1) while data: req += data try: data = conn.recv(1024) except socket.error: break logging.info(req) 
logging.info("Получаем ответ сервера") resp = b'' serv.settimeout(1) data = serv.recv(1024) while data: resp += data try: data = serv.recv(1024) except socket.error: break logging.info(resp) 
logging.info("Отправляем запрос на сервер") serv.send(req) logging.info("Отдаем ответ клиенту") client.send(resp) 

Запускаем. Теперь работает, однако приходится запускать скрипт при каждом запросе к серверу, что не очень удобно.

Обработка нескольких запросов

Усовершенствуем скрипт, после обработки запроса будем снова слушать сокет

while True: logging.info("Ждем входящее соединение"); conn, addr = sock.accept() logging.info("Получаем запрос") data = conn.recv(1024) req = b'' conn.settimeout(0.1) while data: req += data try: data = conn.recv(1024) except socket.error: break logging.info(req) logging.info("Отправляем запрос на сервер") serv.send(req) logging.info("Получаем ответ сервера") resp = b'' serv.settimeout(1) data = serv.recv(1024) while data: resp += data try: data = serv.recv(1024) except socket.error: break logging.info(resp) logging.info("Отдаем ответ клиенту") client.send(resp) 

Это будет работать, однако есть проблема — у нас бесконечный цикл из которого программа не может выйти нормальным образом. Для выхода можно использовать клавиатурное прерывание Ctrl+C и отправим запрос, после этого программа завершится по исключению KeyboardInterrupt.

Чтобы обеспечить более-менее нормальный выход, я решил передавать в сокет STOP, это будет управляющей командой завершения. Напишем обработчик для такой команды. Для этого нам потребуется модифицировать код чтения из клиентского сокета. Получаем первые четыре байта и если они будут STOP, прерываем цикл.

 logging.info("Получаем запрос") data = conn.recv(4) if data == b'STOP': break 

Напишем функцию для остановки нашего прокси. В ней создадим сокет (ssl) и отправим STOP на наш прокси

def stop(): logging.info("Останов"); me = ssl.wrap_socket(socket.socket()) me.connect( ('localhost', 43433) ) me.send(b'STOP') me.close() 

Для запуска команды STOP будем использовать параметр командной строки. Если передали строку stop в командной строке, то будем вызывать нашу функцию stop() (Помещаем этот код и функцию стоп в начало, после установки формата логирования).

if len(sys.argv) > 1: if sys.argv[1] == "stop": stop(); 

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

def run(): # сюда поместим код прокси-сервера описанный выше def stop(): # код приведен выше if len(sys.argv) > 1: if sys.argv[1] == "stop": stop(); else: print("Неизвестная комманда ", sys.argv[1]) else: run() 

Заодно обработали случай с неправильной командой.

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

import subprocess def daemonize(): logging.info("Запуск демона"); subprocess.Popen("py proxy.py", creationflags=0x08000000, close_fds=True) 

Здесь creationflags=0x08000000, установка флага CREATE_NO_WINDOW для процесса. Будем запускать наш сервис в режиме демона если передали start в командной строке

if len(sys.argv) > 1: if sys.argv[1] == "stop": stop(); elif sys.argv[1] == "start": daemonize(); else: print("Неизвестная комманда ", sys.argv[1]) else: run() 

Теперь мы можем запускть наш сервис в режиме демона и останавливать.

Еще маленький штрих, добавим возможность обработки нескольких клиентов, для этого вынесем наш код работы с клиентом в отдельную функцию

def client_run(client, data): req = b'' logging.info("Получаем запрос") client.settimeout(0.1) while data: req += data try: data = client.recv(1024) except socket.error: break logging.info(req) serv = ssl.wrap_socket(socket.socket()) serv.connect( ('server_name', 443) ) logging.info("Отправляем запрос на сервер") serv.send(req) logging.info("Получаем ответ сервера") resp = b'' serv.settimeout(1) data = serv.recv(1024) while data: resp += data try: data = serv.recv(1024) except socket.error: break logging.info(resp) logging.info("Отдаем ответ клиенту") client.send(resp) 

А в главной функции будем запускать client_run в отдельном потоке, т.к. мы устанавливали socket.listen(10), то одновременно у нас может быть до 10 потоков

def run(): logging.info("Старт главного потока"); sock = ssl.wrap_socket(socket.socket(), 'server.key', 'server.crt', True) sock.bind( ('localhost', 43433) ) sock.listen(10) while True: logging.info("Ждем входящее соединение"); conn, addr = sock.accept() data = conn.recv(4) if data == b'STOP': break logging.info("Получен запрос") t = threading.Thread(target = client_run, args = ( conn, data ) ) t.run() logging.info("Остановка") 

Теперь наш прокси-сервис готов.

PS: Позже мне коллега подсказал, что для моей задачи можно использовать stunnel, и я решил поставить его, а скрипт выложить сюда, вдруг кому будет интересно. Конфиг для stunnel такой:

[client-in] sslVersion = SSLv3 accept = 127.0.0.1:43433 connect = 127.0.0.1:8080 [server-out] sslVersion = TLSv1 client = yes accept = 127.0.0.1:8080 connect = server_name:443 

С stunnel также пришлось повозиться, т.к. на сервере были некорректные настройки и не проходила верификация SNI, заработало только с версией 4.36, т.к. там нет такой верификации.

Источник

Читайте также:  Всплывающая подсказка на CSS
Оцените статью