summaryrefslogtreecommitdiff
path: root/day7
diff options
context:
space:
mode:
Diffstat (limited to 'day7')
-rw-r--r--day7/backend.py62
-rw-r--r--day7/db/config.dbbin16384 -> 16384 bytes
-rw-r--r--day7/db/cookies.dbbin16384 -> 16384 bytes
-rw-r--r--day7/http_handler.py88
-rw-r--r--day7/utils.py48
5 files changed, 147 insertions, 51 deletions
diff --git a/day7/backend.py b/day7/backend.py
index 99a62bd..e49bfe1 100644
--- a/day7/backend.py
+++ b/day7/backend.py
@@ -1,7 +1,7 @@
import re
from templater import render_template
-from utils import parse_cookies, add_headers, SUCCESS, BAD_REQUEST, NOT_FOUND, HTTP_METHODS
+from utils import parse_cookies, add_text_headers, SUCCESS, BAD_REQUEST, NOT_FOUND, HTTP_METHODS
from config import TEXT_TEMPLATE_NAME
import db
@@ -17,13 +17,17 @@ def route(url_format, methods=None):
def wrapper(func):
def inner(url, query, *args, **kwargs):
pattern = re.compile(url_format)
- match = re.match(pattern, url)
+ match = re.fullmatch(pattern, url)
if match is None or len(match.groups()) != pattern.groups:
return BAD_REQUEST, '400 BAD REQUEST'
return func(query, *match.groups(), *args, **kwargs)
+ # Добавляем вызываемую функцию в дерево роутера.
+ # Благодаря этому указывать паттерн url и метод нужно указывать
+ # только в инициализаторе декоратора, а функция run сама разберется
+ # при каких условиях нужно вызвать конкретную функцию
_router_tree[url_format] = _router_tree.get(url_format, {})
for method in methods:
_router_tree[url_format][method] = inner
@@ -33,18 +37,22 @@ def route(url_format, methods=None):
return wrapper
-def run(method, url: str, cookies: dict, query):
+def run(request):
res = NOT_FOUND, NOT_FOUND
- for key, value in cookies.items():
+
+ for key, value in request['cookies'].items():
db.set_cookie(key, value)
+ method, url = request['method'], request['url']
for url_pattern in _router_tree:
if re.fullmatch(url_pattern, url) and method in _router_tree[url_pattern]:
- res = _router_tree[url_pattern][method](url, query)
+ res = _router_tree[url_pattern][method](url, request['query'])
+ break
- return add_headers(*res)
+ return add_text_headers(*res)
+# По заданию любой метод, кроме GET и POST должны быть запрещены на сервере.
@route('/.*', list(set(HTTP_METHODS) ^ {'GET', 'POST'}))
def fallback_wrong_method(query, *args):
return NOT_FOUND, 'This method is not allowed'
@@ -55,10 +63,6 @@ def index_get(query, *args):
return SUCCESS, render_template('form', color=get_color())
-@route('/', ['POST'])
-def index_post(query, *args):
- return SUCCESS, str(query)
-
# Хотелось попробовать сделать что-то высокоуровневое с декораторами.
# Если в этом коде есть какие-то серьезные проблемы, то скажите сразу.
@route(r'/div/(\d+)/to/(\d+)/?')
@@ -178,6 +182,44 @@ def set_cookie_post(query, *args):
)
+@route(r'/short_log/(\d{1})/?')
+def short_log_get(query, *args):
+ color = get_color()
+
+ if args[0] == '1':
+ db.set_config_entry('short_log', 1)
+ return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Опция short_log включена')
+
+ elif args[0] == '0':
+ db.set_config_entry('short_log', 0)
+ return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Опция short_log выключена')
+
+ else:
+ return BAD_REQUEST, render_template(
+ TEXT_TEMPLATE_NAME, color=color,
+ text='Опция short_log не может принимать такое значение'
+ )
+
+
+@route(r'/short_log/?', ['POST'])
+def short_log_post(query, *args):
+ color = get_color()
+
+ if 'short_log' not in query:
+ db.set_config_entry('short_log', 0)
+ return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Опция short_log выключена')
+
+ elif query['short_log'] == '1':
+ db.set_config_entry('short_log', 1)
+ return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Опция short_log включена')
+
+ else:
+ return BAD_REQUEST, render_template(
+ TEXT_TEMPLATE_NAME, color=color,
+ text='Опция short_log не может принимать такое значение'
+ )
+
+
def get_color():
color = db.get_cookie('bg_color', 'white')
if color not in ['green', 'white']:
diff --git a/day7/db/config.db b/day7/db/config.db
index 5c7d9e2..3728bc0 100644
--- a/day7/db/config.db
+++ b/day7/db/config.db
Binary files differ
diff --git a/day7/db/cookies.db b/day7/db/cookies.db
index 649e4a6..89b0f61 100644
--- a/day7/db/cookies.db
+++ b/day7/db/cookies.db
Binary files differ
diff --git a/day7/http_handler.py b/day7/http_handler.py
index 4712afe..8fd27bc 100644
--- a/day7/http_handler.py
+++ b/day7/http_handler.py
@@ -1,25 +1,61 @@
-from socket import socket, AF_INET, SOCK_STREAM, SHUT_RDWR
+from socket import socket, AF_INET, SOCK_STREAM, SHUT_RDWR, SOL_SOCKET, SO_REUSEADDR
from time import strftime, gmtime
+import logging
from backend import run
-from utils import validate_url, parse_cookies, parse_query, parse_headers, add_headers, NOT_FOUND, BAD_REQUEST
+from utils import (
+ parse_cookies, parse_query, parse_headers,
+ validate_url, add_text_headers, format_cookies,
+ NOT_FOUND, BAD_REQUEST
+)
from templater import render_template
from config import *
import db
+logging.basicConfig(filename='log.log', level=logging.INFO)
+
+
def log_requests(func):
def wrapper(*args, **kwargs):
address = kwargs['address'] if 'address' in kwargs else args[0]
- method = kwargs['method'] if 'method' in kwargs else args[1]
- url = kwargs['url'] if 'url' in kwargs else args[2]
- http_ver = kwargs['http_ver'] if 'http_ver' in kwargs else args[3]
+ request = kwargs['request'] if 'request' in kwargs else args[1]
response = func(*args, **kwargs)
status = response.split('\n').pop(0).split()[1]
- log_line = f'{address[0]}: {strftime("[%d %b %Y %H:%M:%S]", gmtime())} "{method} {url} {http_ver}" Status: {status}'
+ if not db.get_config_entry('short_log', False):
+ # Формируем строку заголовков для вывода
+ header_pairs = []
+ for header, value in request['headers'].items():
+ if header == 'Cookie':
+ value = format_cookies(value)
+ header_pairs.append(f'{header}: {value}')
+
+ # В логе желательно не использовать переводы строк, когда они не предусмотрены, поэтому
+ # \r\n заменяем на <CRLF>, чтобы мы могли понимать где в строке должны были находиться переводы строк
+ headers_line = '<CRLF>'.join(header_pairs)
+
+ # Формируем строку тела запроса для вывода
+ query_pairs = []
+ for key, value in request['query'].items():
+ query_pairs.append(f'{key}={value}')
+
+ query_line = '&'.join(query_pairs)
+
+ to_logger = f'Headers: {headers_line} | Query: {query_line}'
+ else:
+ to_logger = f'Cookies: {format_cookies(request["cookies"])} '
+
+ log_line = (
+ f'{address[0]}: {strftime("[%d %b %Y %H:%M:%S]", gmtime())} '
+ f'"{request["method"]} {request["url"]} {request["http_ver"]}"'
+ f' | {to_logger} | '
+ f'Status: {status}'
+ )
+
print(log_line)
+ logging.info(log_line)
return response
@@ -27,21 +63,16 @@ def log_requests(func):
@log_requests
-def process_request(address, method, url, http_ver, headers: dict, query):
- if validate_url(url):
- try:
- cookies = parse_cookies(headers['Cookie'])
- except (ValueError, KeyError):
- cookies = {}
-
- response = run(method, url, cookies, query)
+def process_request(address, request: dict):
+ if validate_url(request['url']):
+ response = run(request)
else:
- response = add_headers(NOT_FOUND, '404 NOT FOUND')
+ response = add_text_headers(NOT_FOUND, '404 NOT FOUND')
return response
-def get_request(connection):
+def parse_request(connection):
buffer = b''
request = b''
while not request.endswith(b'\r\n\r\n'):
@@ -55,6 +86,12 @@ def get_request(connection):
buffer += data[1]
method, url, http_ver, headers = parse_headers(request.strip().decode())
+
+ try:
+ cookies = parse_cookies(headers['Cookie'])
+ except (ValueError, KeyError):
+ cookies = {}
+
if 'Content-Type' in headers and headers['Content-Type'] == 'application/x-www-form-urlencoded':
length = int(headers['Content-Length']) - len(buffer)
while length > 0:
@@ -69,7 +106,11 @@ def get_request(connection):
query = parse_query(buffer.strip().decode()) if buffer else {}
- return method, url, http_ver, headers, query
+ return {
+ 'method': method, 'url': url,
+ 'http_ver': http_ver, 'headers': headers,
+ 'query': query, 'cookies': cookies
+ }
def get_color():
@@ -81,9 +122,9 @@ def get_color():
def handle_connection(connection, address):
try:
- request = get_request(connection)
+ request = parse_request(connection)
except ValueError as e:
- response = add_headers(BAD_REQUEST, render_template(
+ response = add_text_headers(BAD_REQUEST, render_template(
TEXT_TEMPLATE_NAME, color=get_color(),
text=BAD_REQUEST + ('<br>' + str(e) if db.get_config_entry('show_errors') else '')
))
@@ -93,14 +134,13 @@ def handle_connection(connection, address):
if request is None:
return
- method, url, http_ver, headers, query = request
-
- response = process_request(address, method, url, http_ver, headers, query)
+ response = process_request(address, request)
connection.sendall(response.encode())
def main(host, port):
sock = socket(AF_INET, SOCK_STREAM)
+ sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(5)
print(f'* Server started on http://{host}:{port}/')
@@ -123,6 +163,8 @@ def main(host, port):
connection.shutdown(SHUT_RDWR)
connection.close()
except UnboundLocalError:
+ # Если переменная connection еще не создана, то нам не нужно закрывать
+ # соединение, а значит можно игнорировать это исключение.
pass
sock.shutdown(SHUT_RDWR)
@@ -133,5 +175,5 @@ def main(host, port):
if __name__ == '__main__':
- HOST, PORT = ADDR = '0.0.0.0', 4001
+ HOST, PORT = ADDR = '0.0.0.0', 8888
main(HOST, PORT)
diff --git a/day7/utils.py b/day7/utils.py
index dfd2b5f..c58c014 100644
--- a/day7/utils.py
+++ b/day7/utils.py
@@ -1,23 +1,19 @@
import re
from time import strftime, gmtime
-
-_URI_RESERVED = {
- '21': '!', '23': '#', '24': '$', '26': '&',
- '27': '\'', '28': '(', '29': ')', '2A': '*',
- '2B': '+', '2C': ',', '2F': '/', '3A': ':',
- '3B': ';', '3D': '=', '3F': '?', '40': '@',
- '5B': '[', '5D': ']'
-}
+from string import ascii_letters
BAD_REQUEST = 'HTTP/1.1 400 Bad Request'
NOT_FOUND = 'HTTP/1.1 404 Not Found'
SUCCESS = 'HTTP/1.1 200 OK'
METHOD_NOT_ALLOWED = 'HTTP/1.1 405 Method Not Allowed'
-URL_REGEX_PATTERN = re.compile(r'/((.*?/?)+)?')
+
HTTP_METHODS = ['GET', 'POST', 'OPTIONS', 'HEAD', 'PUT', 'PATCH', 'DELETE', 'TRACE', 'CONNECT']
+URL_REGEX_PATTERN = re.compile(r'/((.*?/?)+)?')
+FIRST_LINE_PATTERN = re.compile(rf'{"(" + "|".join(HTTP_METHODS) + ")"} {URL_REGEX_PATTERN.pattern} HTTP/1\.[01]')
+
-def add_headers(status, html: str):
+def add_text_headers(status, html: str):
return '\r\n'.join([
status,
f'Date: {strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime())}',
@@ -33,9 +29,7 @@ def validate_url(url):
def validate_first_line(line):
- methods_groups = f"({'|'.join(HTTP_METHODS)})"
- first_line_pattern = re.compile(rf'{methods_groups} {URL_REGEX_PATTERN.pattern} HTTP/1\.[01]')
- return bool(first_line_pattern.fullmatch(line))
+ return bool(FIRST_LINE_PATTERN.fullmatch(line))
def parse_cookies(cookies_line: str):
@@ -51,6 +45,14 @@ def parse_cookies(cookies_line: str):
return d
+def format_cookies(cookies: dict):
+ pairs = []
+ for key, value in cookies:
+ pairs.append(f'{key}={value}')
+
+ return ';'.join(pairs)
+
+
def parse_headers(request_line: str):
request = request_line.split('\r\n')
@@ -98,12 +100,8 @@ def url_decoder(url_line: str):
while i < len(url_line):
if url_line[i] == '%':
hex_value = url_line[i + 1: i + 3]
- if hex_value in _URI_RESERVED:
- integer = ord(_URI_RESERVED[hex_value])
- else:
- integer = int(hex_value, 16)
+ encoded += bytes([int(hex_value, 16)])
- encoded += bytes([integer])
i += 3
continue
else:
@@ -111,3 +109,17 @@ def url_decoder(url_line: str):
i += 1
return encoded.decode()
+
+
+def url_encoder(line: str):
+ s = ''
+ for char in line:
+ if char == ' ':
+ s += '+'
+ elif char not in ascii_letters:
+ for byte in char.encode():
+ s += '%' + hex(byte)[2:].upper()
+ else:
+ s += char
+
+ return s