diff options
| author | Andrew <saintruler@gmail.com> | 2019-05-24 16:29:03 +0400 |
|---|---|---|
| committer | Andrew <saintruler@gmail.com> | 2019-05-24 16:29:03 +0400 |
| commit | 0f0e815ad1b775ff93699b695f290c562c57962f (patch) | |
| tree | 2d41e2d3b3ede86b6fb18a176b201920d6681dfa | |
| parent | 402d0d2b9ebd76b8e99eddceeb85c4a66030b51b (diff) | |
Модули для обработки http-запросов, базовый обработчик url-путей. Обертка для работы с shelve базами данных, html-шаблонизатор.
| -rw-r--r-- | day7/__init__.py | 0 | ||||
| -rw-r--r-- | day7/backend.py | 180 | ||||
| -rw-r--r-- | day7/config.py | 2 | ||||
| -rw-r--r-- | day7/db.py | 21 | ||||
| -rw-r--r-- | day7/db/config.db | bin | 0 -> 16384 bytes | |||
| -rw-r--r-- | day7/db/cookies.db | bin | 0 -> 16384 bytes | |||
| -rw-r--r-- | day7/http_handler.py | 137 | ||||
| -rw-r--r-- | day7/templater.py | 5 | ||||
| -rw-r--r-- | day7/templates/form.html | 26 | ||||
| -rw-r--r-- | day7/templates/index.html | 7 | ||||
| -rw-r--r-- | day7/utils.py | 91 |
11 files changed, 469 insertions, 0 deletions
diff --git a/day7/__init__.py b/day7/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/day7/__init__.py diff --git a/day7/backend.py b/day7/backend.py new file mode 100644 index 0000000..9f9b118 --- /dev/null +++ b/day7/backend.py @@ -0,0 +1,180 @@ +import re + +from templater import render_template +from utils import parse_cookies, add_headers, SUCCESS, BAD_REQUEST, NOT_FOUND +from config import TEXT_TEMPLATE_NAME +import db + + +_router_tree = {} + + +# url_format - регулярное выражение +def route(url_format, methods=None): + if methods is None: + methods = ['GET'] + + def wrapper(func): + def inner(url, query, *args, **kwargs): + pattern = re.compile(url_format) + match = re.match(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) + + _router_tree[url_format] = _router_tree.get(url_format, {}) + for method in methods: + _router_tree[url_format][method] = inner + + return inner + + return wrapper + + +def run(method, url: str, cookies: dict, query): + res = NOT_FOUND, NOT_FOUND + 'KAVO' + for key, value in cookies.items(): + db.set_cookie(key, value) + + 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) + + return add_headers(*res) + + +@route('/') +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+)/?') +def divide_get(query, *args): + color = get_color() + + try: + text = str(int(args[0]) / int(args[1])) + return SUCCESS, render_template(TEXT_TEMPLATE_NAME, text=text, color=color) + + except ZeroDivisionError as e: + return BAD_REQUEST, render_template( + TEXT_TEMPLATE_NAME, color=color, + text=BAD_REQUEST + ('<br>' + str(e) if db.get_config_entry('show_errors') else '') + ) + + +@route(r'/div/?', ['POST']) +def divide_post(query, *args): + color = get_color() + + try: + text = str(int(query['numerator']) / int(query['denominator'])) + return SUCCESS, render_template(TEXT_TEMPLATE_NAME, text=text, color=color) + + except KeyError: + field = 'числитель' if 'numerator' not in query else 'знаменатель' + return BAD_REQUEST, render_template( + TEXT_TEMPLATE_NAME, color=color, + text=BAD_REQUEST + ('<br>' + f'Указан неверный {field}') + ) + + except (ValueError, ZeroDivisionError) as e: + return BAD_REQUEST, render_template( + TEXT_TEMPLATE_NAME, color=color, + text=BAD_REQUEST + ('<br>' + str(e) if db.get_config_entry('show_errors') else '') + ) + + +@route(r'/show_errors/(\d{1})/?') +def show_errors_get(query, *args): + color = get_color() + + if args[0] == '1': + db.set_config_entry('show_errors', 1) + return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Опция show_errors включена') + + elif args[0] == '0': + db.set_config_entry('show_errors', 0) + return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Опция show_errors выключена') + + else: + return BAD_REQUEST, render_template( + TEXT_TEMPLATE_NAME, color=color, + text='Опция show_errors не может принимать такое значение' + ) + + +@route(r'/show_errors/?', ['POST']) +def show_errors_post(query, *args): + color = get_color() + + if 'show_errors' not in query: + db.set_config_entry('show_errors', 0) + return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Опция show_errors выключена') + + elif query['show_errors'] == '1': + db.set_config_entry('show_errors', 1) + return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Опция show_errors включена') + + else: + return BAD_REQUEST, render_template( + TEXT_TEMPLATE_NAME, color=color, + text='Опция show_errors не может принимать такое значение' + ) + + +@route(r'/set_cookie/=/(.*)/?') +def set_cookie_get(query, *args): + cookie_line = args[0] + color = get_color() + try: + cookies = parse_cookies(cookie_line) + for key, value in cookies.items(): + db.set_cookie(key, value) + + return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Cookie-файл обновлен') + + except ValueError as e: + return BAD_REQUEST, render_template( + TEXT_TEMPLATE_NAME, color=color, + text=BAD_REQUEST + ('<br>' + str(e) if db.get_config_entry('show_errors') else '') + ) + + +@route(r'/set_cookie/=/?', ['POST']) +def set_cookie_post(query, *args): + color = get_color() + try: + cookie_line = query['cookie_line'] + cookies = parse_cookies(cookie_line) + for key, value in cookies.items(): + db.set_cookie(key, value) + + return SUCCESS, render_template(TEXT_TEMPLATE_NAME, color=color, text='Cookie-файл обновлен') + + except KeyError: + return BAD_REQUEST, render_template( + TEXT_TEMPLATE_NAME, color=color, + text=BAD_REQUEST + ('<br>' + f'Не указана строка с cookie') + ) + + except ValueError as e: + return BAD_REQUEST, render_template( + TEXT_TEMPLATE_NAME, color=color, + text=BAD_REQUEST + ('<br>' + str(e) if db.get_config_entry('show_errors') else '') + ) + + +def get_color(): + color = db.get_cookie('bg_color', 'white') + if color not in ['green', 'white']: + color = 'white' + return color diff --git a/day7/config.py b/day7/config.py new file mode 100644 index 0000000..b5d596a --- /dev/null +++ b/day7/config.py @@ -0,0 +1,2 @@ +CHUNK = 1024 +TEXT_TEMPLATE_NAME = 'index' diff --git a/day7/db.py b/day7/db.py new file mode 100644 index 0000000..11d2a45 --- /dev/null +++ b/day7/db.py @@ -0,0 +1,21 @@ +import shelve + + +def set_cookie(key, value): + _db[key] = value + + +def get_cookie(key, default=None): + return _db.get(key, default) + + +def set_config_entry(key, value): + _config[key] = value + + +def get_config_entry(key, default=None): + return _config.get(key, default) + + +_db = shelve.open('db/cookies.db') +_config = shelve.open('db/config.db') diff --git a/day7/db/config.db b/day7/db/config.db Binary files differnew file mode 100644 index 0000000..5c7d9e2 --- /dev/null +++ b/day7/db/config.db diff --git a/day7/db/cookies.db b/day7/db/cookies.db Binary files differnew file mode 100644 index 0000000..649e4a6 --- /dev/null +++ b/day7/db/cookies.db diff --git a/day7/http_handler.py b/day7/http_handler.py new file mode 100644 index 0000000..e5a551b --- /dev/null +++ b/day7/http_handler.py @@ -0,0 +1,137 @@ +from socket import socket, AF_INET, SOCK_STREAM, SHUT_RDWR +from time import strftime, gmtime + +from backend import run +from utils import validate_url, parse_cookies, parse_query, parse_headers, add_headers, NOT_FOUND, BAD_REQUEST +from templater import render_template +from config import * +import db + + +def log_func(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] + + 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}' + print(log_line) + + return response + + return wrapper + + +@log_func +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) + else: + response = add_headers(NOT_FOUND, '404 NOT FOUND') + + return response + + +def get_request(connection): + buffer = b'' + request = b'' + while not request.endswith(b'\r\n\r\n'): + data = connection.recv(1024).split(b'\r\n\r\n') + if len(data) == 1: + if not data[0]: + return None + request += data[0] + else: + request += data[0] + b'\r\n\r\n' + buffer += data[1] + + method, url, http_ver, headers = parse_headers(request.strip().decode()) + if 'Content-Type' in headers and headers['Content-Type'] == 'application/x-www-form-urlencoded': + length = int(headers['Content-Length']) - len(buffer) + while length > 0: + if length < CHUNK: + data = connection.recv(length) + length = 0 + else: + data = connection.recv(CHUNK) + length -= CHUNK + + buffer += data[0] + + query = parse_query(buffer.strip().decode()) if buffer else {} + + return method, url, http_ver, headers, query + + +def get_color(): + color = db.get_cookie('bg_color', 'white') + if color not in ['green', 'white']: + color = 'white' + return color + + +def handle_connection(connection, address): + try: + request = get_request(connection) + except ValueError as e: + response = add_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 '') + )) + connection.sendall(response.encode()) + return + + if request is None: + return + + method, url, http_ver, headers, query = request + + response = process_request(address, method, url, http_ver, headers, query) + connection.sendall(response.encode()) + + +def main(host, port): + sock = socket(AF_INET, SOCK_STREAM) + sock.bind((host, port)) + sock.listen(5) + print(f'* Server started on http://{host}:{port}/') + print('* Listening to new connections...') + + while True: + try: + connection, address = sock.accept() + + handle_connection(connection, address) + + connection.shutdown(SHUT_RDWR) + connection.close() + del connection + + except KeyboardInterrupt: + print('Stopping server...') + + try: + connection.shutdown(SHUT_RDWR) + connection.close() + except UnboundLocalError: + pass + + sock.shutdown(SHUT_RDWR) + sock.close() + + print('Server stopped') + break + + +if __name__ == '__main__': + HOST, PORT = ADDR = '0.0.0.0', 4002 + main(HOST, PORT) diff --git a/day7/templater.py b/day7/templater.py new file mode 100644 index 0000000..92fb87f --- /dev/null +++ b/day7/templater.py @@ -0,0 +1,5 @@ +def render_template(template_name, **kwargs): + with open(f'templates/{template_name}.html') as f: + data = f.read() + + return data.format(**kwargs) diff --git a/day7/templates/form.html b/day7/templates/form.html new file mode 100644 index 0000000..e1edb96 --- /dev/null +++ b/day7/templates/form.html @@ -0,0 +1,26 @@ +<html style="background-color: {color};"> + <body> + <form action="/show_errors" method="POST"> + <input type="checkbox" name="show_errors" value="1">Show errors<br/> + <input type="submit" value="Send"><br/> + </form> + + <form action="/div" method="POST"> + Разделить + <input type="text" name="numerator"> на + <input type="text" name="denominator"><br/> + <input type="submit" value="Send"><br/> + </form> + + <form action="/set_cookie/=" method="POST"> + Изменить Cookies + <input type="text" name="cookie_line"><br/> + <input type="submit" value="Send"><br/> + </form> + + <form action="/" method="POST"> + <input type="checkbox" name="short_log" value="1">Short log<br/> + <input type="submit" value="Send"><br/> + </form> + </body> +</html> diff --git a/day7/templates/index.html b/day7/templates/index.html new file mode 100644 index 0000000..b5fae3a --- /dev/null +++ b/day7/templates/index.html @@ -0,0 +1,7 @@ +<html style="background-color: {color};"> + +<body> +{text} +</body> + +</html>
\ No newline at end of file diff --git a/day7/utils.py b/day7/utils.py new file mode 100644 index 0000000..0aa0709 --- /dev/null +++ b/day7/utils.py @@ -0,0 +1,91 @@ +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': ']' +} + +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' + + +def add_headers(status, html: str): + return '\r\n'.join([ + status, + f'Date: {strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime())}', + 'Server: BrandNewServer', + 'Content-Type: text/html; charset=utf-8', + 'Connection: keep-alive', + '', html + ]) + + +def validate_url(url): + url_pattern = re.compile(r'/((.*?/?)+)?') + return bool(url_pattern.fullmatch(url)) + + +def parse_cookies(cookies_line: str): + cookies_line = cookies_line.strip().strip(';') + pairs = cookies_line.split(';') + d = {} + for pair in pairs: + try: + key, value = pair.split('=', maxsplit=1) + d[key] = value + except ValueError: + raise ValueError('Wrong format of cookies') + + return d + + +def parse_headers(request_line: str): + request = request_line.split('\r\n') + method, url, http_ver = request.pop(0).split() + headers = {} + for line in request: + field, value = line.split(': ', maxsplit=1) + headers[field] = value + + return method, url, http_ver, headers + + +def parse_query(query_line: str): + pairs = query_line.strip().split('&') + d = {} + for pair in pairs: + try: + key, value = pair.split('=', maxsplit=1) + d[url_decoder(key)] = url_decoder(value) + except ValueError: + raise ValueError('Wrong format of query') + + return d + + +def url_decoder(url_line: str): + url_line = url_line.replace('+', ' ') + encoded = b'' + i = 0 + 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([integer]) + i += 3 + continue + else: + encoded += bytes([ord(url_line[i])]) + i += 1 + + return encoded.decode() |