Программирование сокетов на python очень удобно для пользователя по сравнению с c. Программисту не нужно беспокоиться о мельчайших деталях, касающихся сокетов. В python у пользователя больше шансов сосредоточиться на прикладном уровне, а не на сетевом. В этом уроке мы будем разрабатывать простой многопоточный прокси-сервер, способный обрабатывать HTTP-трафик. Это было бы в основном основано на основных идеях программирования сокетов. Если вы не уверены в основах, я бы рекомендовал вам освежить их в памяти перед прохождением этого урока.
Это наивная реализация прокси-сервера. Мы будем постепенно превращать его в весьма полезный сервер в следующих учебных пособиях.
Для начала мы бы выполнили этот процесс в 3 простых шага
1. Создание входящего сокета
Мы создаем сокет ServerSocket в методе __init__ класса сервера. Это создает сокет для входящих подключений. Затем мы привязываем сокет, а затем ждем подключения клиентов.
def __init__(self, config):
# Shutdown on Ctrl+C
signal.signal(signal.SIGINT, self.shutdown)
# Create a TCP socket
self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Re-use the socket
self.serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# bind the socket to a public host, and a port
self.serverSocket.bind((config['HOST_NAME'], config['BIND_PORT']))
self.serverSocket.listen(10) # become a server socket
self.__clients = {}
2. Примите клиента и обработайте
Это самый простой и в то же время самый важный из всех шагов. Мы ждем запроса клиента на подключение, и как только соединение будет выполнено успешно, мы отправляем запрос в отдельном потоке, делая себя доступными для следующего запроса. Это позволяет нам обрабатывать несколько запросов одновременно, что многократно повышает производительность сервера.
while True: # Establish the connection (clientSocket, client_address) = self.serverSocket.accept()
d = threading.Thread(name=self._getClientName(client_address),
target = self.proxy_thread, args=(clientSocket, client_address)) d.setDaemon(True) d.start()
3. Перенаправление трафика
Основная особенность прокси — сервера состоит в том, чтобы выступать в качестве промежуточного звена между источником и получателем. Здесь мы будем извлекать данные из источника, а затем передавать их клиенту.
- Сначала мы извлекаем URL-адрес из полученных данных запроса.
# get the request from browser request = conn.recv(config['MAX_REQUEST_LEN'])
# parse the first line first_line = request.split('\n')[0] # get url url = first_line.split(' ')[1]
- Затем мы находим адрес назначения запроса. Адрес представляет собой кортеж из (destination_ip_address, destination_port_no). Мы будем получать данные с этого адреса.
http_pos = url.find("://")
# find pos of :// if (http_pos==-1): temp = url else: temp = url[(http_pos+3):]
# get the rest of url port_pos = temp.find(":")
# find the port pos (if any)
# find end of web server webserver_pos = temp.find("/") if webserver_pos == -1: webserver_pos = len(temp) webserver = "" port = -1 if (port_pos==-1 or webserver_pos < port_pos):
# default port port = 80 webserver = temp[:webserver_pos] else:
# specific port port = int((temp[(port_pos+1):])[:webserver_pos-port_pos-1]) webserver = temp[:port_pos]
- Теперь мы устанавливаем новое соединение с целевым сервером (или удаленным сервером), а затем отправляем копию исходного запроса на сервер. Затем сервер ответит ответом. Все ответные сообщения используют общий формат сообщений RFC 822.
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(config['CONNECTION_TIMEOUT']) s.connect((webserver, port)) s.sendall(request)
- Затем мы перенаправляем ответ сервера клиенту. соединение-это исходное соединение с клиентом. Ответ может быть больше, чем MAX_REQUEST_LEN, который мы получаем в одном вызове, поэтому нулевой ответ отмечает конец ответа.
while 1: # receive data from web server data = s.recv(config['MAX_REQUEST_LEN']) if (len(data) > 0): conn.send(data) # send to browser/client else: break
Затем мы соответствующим образом закрываем подключения к серверу и выполняем обработку ошибок, чтобы убедиться, что сервер работает должным образом.
Как протестировать сервер?
1. Запустите сервер на терминале. Продолжайте работать и переключитесь на свой любимый браузер.
2. Перейдите в настройки прокси-сервера вашего браузера и измените прокси-сервер на «локальный хост», а порт на «12345».
3. Теперь откройте любой веб-сайт HTTP (не HTTPS), например. programbox.ru и волла !! вы должны иметь доступ к содержимому в браузере.
Как только сервер запущен, мы можем отслеживать запросы, поступающие к клиенту. Мы можем использовать эти данные для мониторинга контента, который собирается, или мы можем разработать статистику на основе контента.
Мы даже можем ограничить доступ к веб-сайту или внести в черный список IP-адрес. Мы будем иметь дело с большим количеством таких функций в следующих учебных пособиях.
Что дальше?
В следующих учебных пособиях мы добавим следующие функции на наш прокси-сервер.
– Домены, внесенные в Черный список
– Мониторинг контента
– Ведение журнала
– HTTP веб-сервер + прокси-сервер
- Добавьте черный список доменов. Например. google.com, facebook.com. Создайте список доменов BLACKLIST_DOMAINS в нашей конфигурации. На данный момент просто игнорируйте/отбрасывайте запросы, полученные для доменов, внесенных в черный список. (В идеале мы должны ответить запрещенным ответом.)
# Check if the host:port is blacklisted
for i in range(0, len(config['BLACKLIST_DOMAINS'])):
if config['BLACKLIST_DOMAINS'][i] in url:
conn.close()
return
- Чтобы добавить блокировку хоста: Скажем, вам может потребоваться разрешить подключения из определенной подсети или подключения для конкретного человека. Чтобы добавить это, создайте список всех разрешенных хостов. Поскольку хосты также могут быть подсетью, добавьте регулярное выражение для сопоставления IP-адресов, в частности адресов IPV4. “ Адреса IPv4 канонически представлены в десятичной системе счисления, которая состоит из четырех десятичных чисел, каждое из которых находится в диапазоне от 0 до 255, разделенных точками, например, 172.16.254.1. Каждая часть представляет группу из 8 бит (октет) адреса”.
- Использование регулярного выражения для сопоставления правильных IP-адресов:
- Создайте новый метод _ishostAllowed в классе сервера и используйте модуль fnmatch для сопоставления регулярных выражений. Повторите все регулярные выражения и разрешите запрос, если он соответствует какому-либо из них. Если адрес клиента не найден как часть какого-либо регулярного выражения, отправьте ЗАПРЕЩЕННЫЙ ответ. Опять же, пока пропустите эту часть создания ответа.
def _ishostAllowed(self, host):
""" Check if host is allowed to access
the content """
for wildcard in config['HOST_ALLOWED']:
if fnmatch.fnmatch(host, wildcard):
return True
return False
Регулярное выражение соответствия хоста по умолчанию будет»*’, чтобы соответствовать всем хостам. Хотя также можно использовать регулярное выражение вида ‘192.168.*’. Сервер в настоящее время обрабатывает запросы, но не показывает никаких сообщений, поэтому мы не знаем о состоянии сервера. Его сообщения должны быть зарегистрированы на консоли. Для этой цели используйте модуль ведения журнала , так как он потокобезопасен. (сервер многопоточен, если вы помните.)
Импортируйте модуль и настройте его начальную конфигурацию.
logging.basicConfig(level = logging.DEBUG,
format = '[%(CurrentTime)-10s] (%(ThreadName)-10s) %(message)s',)
- Создайте отдельный метод, который регистрирует каждое сообщение : Передайте его в качестве аргумента с дополнительными данными, такими как имя потока и текущее время, чтобы отслеживать журналы. Также создайте функцию, которая раскрашивает журналы так, чтобы они красиво выглядели на STDOUT.
Чтобы достичь этого, добавьте логическое значение в конфигурацию, COLORED_LOGGING и создайте новую функцию, которая окрашивает каждую переданную ей msg на основе уровня LOG_LOG_.
def log(self, log_level, client, msg): """ Log the messages to appropriate place """ LoggerDict = { 'CurrentTime' : strftime("%a, %d %b %Y %X", localtime()), 'ThreadName' : threading.currentThread().getName() } if client == -1: # Main Thread formatedMSG = msg else: # Child threads or Request Threads formatedMSG = '{0}:{1} {2}'.format(client[0], client[1], msg) logging.debug('%s', utils.colorizeLog(config['COLORED_LOGGING'], log_level, formatedMSG), extra=LoggerDict)
- Создайте новый модуль, ColorizePython.py: Он содержит класс pycolors, который поддерживает список цветовых кодов. Разделите это на другой модуль, чтобы сделать код модульным и соответствовать стандартам PEP8.
# ColorizePython.py class pycolors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' # End color BOLD = '\033[1m' UNDERLINE = '\033[4m'
Модуль:
import ColorizePython
Метод:
def colorizeLog(shouldColorize, log_level, msg): ## Higher is the log_level in the log() ## argument, the lower is its priority. colorize_log = { "NORMAL": ColorizePython.pycolors.ENDC, "WARNING": ColorizePython.pycolors.WARNING, "SUCCESS": ColorizePython.pycolors.OKGREEN, "FAIL": ColorizePython.pycolors.FAIL, "RESET": ColorizePython.pycolors.ENDC } if shouldColorize.lower() == "true": if log_level in colorize_log: return colorize_log[str(log_level)] + msg + colorize_log['RESET'] return colorize_log["NORMAL"] + msg + colorize_log["RESET"] return msg
- Поскольку colorizeLog не является функцией класса сервера, он создается как отдельный модуль с именем utils.py, в котором хранятся все утилиты, облегчающие понимание кода, и помещающие этот метод туда. Добавляйте соответствующие сообщения журнала везде, где это необходимо, особенно при изменении состояния сервера.
- Измените метод завершения работы на сервере, чтобы завершить все запущенные потоки перед выходом из приложения. многопоточность.перечисление() перебирает все запущенные потоки, поэтому нам не нужно вести их список. Поведение модуля потоковой передачи неожиданно, когда мы пытаемся завершить main_thread. В официальной документации также говорится об этом:
“join() вызывает ошибку времени выполнения, если предпринята попытка присоединиться к текущему потоку, так как это приведет к взаимоблокировке. Также является ошибкой присоединиться() к потоку до его запуска, и попытка сделать это вызывает то же исключение”.
Так что пропустите его соответствующим образом. Вот код для того же самого.
def shutdown(self, signum, frame): """ Handle the exiting server. Clean all traces """ self.log("WARNING", -1, 'Shutting down gracefully...') main_thread = threading.currentThread() # Wait for all clients to exit for t in threading.enumerate(): if t is main_thread: continue self.log("FAIL", -1, 'joining ' + t.getName()) t.join() self.serverSocket.close() sys.exit(0)