Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# created by virtualenv automatically
env/
.idea/
Pipfile
.pytest_cache
.coverage
5 changes: 5 additions & 0 deletions enums/client_messages_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import enum


class ClientMessagesResponse(enum.Enum):
incorrect_url_or_port = 'Incorrect URL-address or port'
9 changes: 9 additions & 0 deletions enums/requests_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import enum


class RequestsNames(enum.Enum):
get = 'get'
post = 'post'
patch = 'patch'
delete = 'delete'
put = 'put'
10 changes: 10 additions & 0 deletions enums/server_validating_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import enum


class ServerValidatingMessages(enum.Enum):
incorrect_url = 'You did not write URL-address'
incorrect_type_request = 'You did not write type of request'
incorrect_format_cookie = 'Incorrect format cookie: type of object which takes cookie must be dictionary'
incorrect_format_headers = 'Incorrect format headers: type of object which takes headers must be dictionary'
incorrect_body_type = 'Incorrect format of body: type of object which takes body is string'
is_validated = 'All parameters is valid'
69 changes: 52 additions & 17 deletions client/http_client.py → http_client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import socket

from enums.client_messages_response import ClientMessagesResponse


class HttpClient:
def __init__(self, settings: dict):
self.__settings = settings
self._settings = settings

self.__HOST = self.__settings.get("url")
self.__PORT = int(self.__settings.get("port"))
self._HOST = self._settings.get("url")
self._PORT = self._settings.get("port")

def get_data(self) -> str:
"""
Expand All @@ -17,13 +19,17 @@ def get_data(self) -> str:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
timeout = 10

if self.__settings.get("timeout") is not None:
timeout = self.__settings.get("timeout")
if self._settings.get("timeout") is not None:
timeout = self._settings.get("timeout")

s.settimeout(timeout)
s.connect((self.__HOST, self.__PORT))

request = self.create_http_request(self.__settings).encode()
try:
s.connect((self._HOST, int(self._PORT)))
except (ValueError, socket.gaierror):
return str(ClientMessagesResponse.incorrect_url_or_port.value)

request = self.create_http_request(self._settings).encode()

try:
s.sendall(request)
Expand All @@ -33,29 +39,58 @@ def get_data(self) -> str:
response = b""

try:
while True:
data = s.recv(4096)

if not data:
break

response += data
response = self.receive_data(s)
except socket.timeout:
pass
except Exception as e:
print('log: ' + str(e))

close_request = self.create_http_close_request(self.__settings).encode()
close_request = self.create_http_close_request(self._settings)

try:
s.sendall(close_request)
self.send_data(conn=s, data=close_request)
except Exception as e:
print('log: ' + str(e))

response = response.decode()

return response

@staticmethod
def receive_data(conn: socket) -> bytes:
response = b""

while True:
data = conn.recv(4096)

if not data:
break

response += data

return response

@staticmethod
def send_data(conn: socket, data: str) -> int:
"""
Отправляет данные клиенту
:param conn: подключение, по которому нужно отправить эти данные
:param data: отправляемые данные
:return: количество отправленных байт
"""
data_to_send = data.encode('utf-8')
bytes_sent = 0

while bytes_sent < len(data_to_send):
sent = conn.send(data_to_send[bytes_sent:])

if sent == 0:
break

bytes_sent += sent

return bytes_sent

@staticmethod
def get_headers(settings: dict) -> str:
"""
Expand All @@ -75,7 +110,7 @@ def get_headers(settings: dict) -> str:
cookies.append(f'{key}={value};')

if len(cookies) != 0:
cookies = 'Cookie: ' + ' '.join(cookies)
cookies = 'Cookie: ' + ' '.join(cookies)
cookies = cookies[:-1]
cookies += '\r\n'
headers.append(cookies)
Expand Down
232 changes: 232 additions & 0 deletions http_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import re
import socket
import json

from enums.client_messages_response import ClientMessagesResponse
from enums.requests_names import RequestsNames
from enums.server_validating_messages import ServerValidatingMessages
from http_client import HttpClient


class HttpServer:
"""
HTTP-сервер, который умеет принимать запросы от клиентов и, используя HttpClient возвращает нужные запросы
"""

def __init__(self):
self.__HOST = '127.0.0.1'
self.__PORT = 8080

self._server = None

self._working = None

def start(self):
"""
Запускает сервер
"""

try:
self._create_server()
except OSError as e:
self.stop()
print(e)

if self._server is None:
return

self._server.listen()

self._start_server()

while self._working:
if self._server is None:
break

conn, addr = self._server.accept()

print(f'Client connected by addr: {addr}')

with conn:
data = conn.recv(8192).decode()

origin = None

if data.startswith('OPTIONS / HTTP/1.1'):
origin = re.search(r'(?<=Origin: )(.+?)(?=\r\n)', data).group(0)

response = self.create_option_response(origin)

conn.sendall(response.encode())

data = conn.recv(8190).decode()

body = re.search(r'(?<=\r\n\r\n)(.+)', data).group(0)
settings = json.loads(body)

settings_is_validated = HttpServer.validate_params(settings)

if not settings_is_validated[0]:
response = HttpServer.create_bad_request_response(
origin,
settings_is_validated[1]
)

HttpServer.send_data(conn, response)
else:
client = HttpClient(settings)
client_data = client.get_data()

if client_data == ClientMessagesResponse.incorrect_url_or_port.value:
response = HttpServer.create_bad_request_response(origin, client_data)
HttpServer.send_data(conn, response)
else:
data = client_data

if (
settings.get('request').lower() == RequestsNames.get.value
and
len(settings.get('get_form')) != 0
):
data = self._take_data(settings, client_data)

response = HttpServer.create_ok_request_response(data, origin)
HttpServer.send_data(conn, response)

print(f'Client with addr {addr} was disconnected')

def _stop_server(self):
self._working = False

def _start_server(self):
self._working = True

def stop(self):
self._stop_server()

@staticmethod
def _take_data(settings: dict, client_data: str):
data = client_data

if 'application/json' in client_data:
matches = re.finditer(settings.get('get_form'), client_data, re.IGNORECASE)
data = [match.start() for match in matches]

return data

def _create_server(self):
self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._server.bind((self.__HOST, self.__PORT))

def stop(self):
self._server.close()
self._server = None

@staticmethod
def send_data(conn: socket, response: str) -> int:
"""
Отправляет данные на сервер
:param conn: подключение, по которому нужно отправить эти данные
:param response: отправляемые данные
:return: количество отправленных байт
"""
data_to_send = response.encode('utf-8')
bytes_sent = 0

while bytes_sent < len(data_to_send):
sent = conn.send(data_to_send[bytes_sent:])

bytes_sent += sent

return bytes_sent

@staticmethod
def validate_params(body: dict) -> tuple:
"""
Проверяет являются ли параметры запроса валидными
:return: возвращает tuple, где первый параметр - это true или false: результат валидности,
а второй сообщение описывающее результат валидации
"""

if body.get('url') is None:
return False, ServerValidatingMessages.incorrect_url.value

if body.get('request') is None:
return False, ServerValidatingMessages.incorrect_type_request.value

if not isinstance(body.get('cookie'), dict):
return False, ServerValidatingMessages.incorrect_format_cookie.value

if not isinstance(body.get('headers'), dict):
return False, ServerValidatingMessages.incorrect_format_headers.value

if not isinstance(body.get('body'), str):
return False, ServerValidatingMessages.incorrect_body_type.value

return True, ServerValidatingMessages.is_validated.value

@staticmethod
def create_option_response(origin: str):
"""
Создает ответ на OPTION запрос клиента
:param origin: значение заголовка Origin в Option запросе клиента
:return: возвращает готовый ответ для клиента в формате протокола HTTP
"""

return f"HTTP/1.1 200 OK\r\n" \
f"Access-Control-Allow-Origin: {origin}\r\n" \
f"Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n" \
f"Access-Control-Allow-Headers: Content-Type\r\n" \
f"Connection: keep-alive\r\n" \
f"Content-Length: 0\r\n" \
f"\r\n"

@staticmethod
def create_response(code: int, code_message: str, data: str, origin: str,):
"""
Создает ответ на пользовательский запрос
:param code: код ответа
:param code_message: код ответа
:param data: данные в теле ответа (должны быть в json формате)
:param origin: значение заголовка Origin в Option запросе клиента
:return: ответ
"""

response = f'HTTP/1.1 {code} {code_message}\r\n' \
f'Content-Type: application/json\r\n' \
f'Content-Length: {len(data.encode())}\r\n' \
f'Connection: keep-alive\r\n' \
f'Access-Control-Allow-Origin: {origin}' \
f'\r\n\r\n{data}'

return response

@staticmethod
def create_ok_request_response(data: str, origin: str):
"""
Создает ответ на запрос с кодом успеха
:param data: данные, которые будут помещены в тело запроса
:param origin: значение заголовка Origin в Option запросе клиента
:return: возвращает готовый ответ для клиента в формате протокола HTTP
"""

body = dict()

body['data'] = data
body['code'] = 200

return HttpServer.create_response(body['code'], 'OK', json.dumps(body), origin)

@staticmethod
def create_bad_request_response(origin: str, message: str):
"""
Создает ответ с кодом плохого запроса (bad request)
:return:
"""

body = dict()

body['message'] = message
body['code'] = 400

return HttpServer.create_response(body['code'], 'BAD REQUEST', json.dumps(body), origin)
Loading