From 7f8d38bcbde018590cccf532f1d96bd6c2d62e44 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 25 May 2019 18:12:29 +0400 Subject: =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=B2=20=D0=BD=D0=B5=D0=BA=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D1=8B=D1=85=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B9.=20=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F.?= =?UTF-8?q?=20=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=B8=D1=80=D0=BE=D0=B2=D1=89=D0=B8=D0=BA=20?= =?UTF-8?q?url.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- day7/backend.py | 62 ++++++++++++++++++++++++++++++------ day7/db/config.db | Bin 16384 -> 16384 bytes day7/db/cookies.db | Bin 16384 -> 16384 bytes day7/http_handler.py | 88 +++++++++++++++++++++++++++++++++++++-------------- day7/utils.py | 48 +++++++++++++++++----------- 5 files changed, 147 insertions(+), 51 deletions(-) (limited to 'day7') 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 Binary files a/day7/db/config.db and b/day7/db/config.db differ diff --git a/day7/db/cookies.db b/day7/db/cookies.db index 649e4a6..89b0f61 100644 Binary files a/day7/db/cookies.db and b/day7/db/cookies.db 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 заменяем на , чтобы мы могли понимать где в строке должны были находиться переводы строк + headers_line = ''.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 + ('
' + 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 -- cgit v1.2.3