summaryrefslogtreecommitdiff
path: root/day7
diff options
context:
space:
mode:
Diffstat (limited to 'day7')
-rw-r--r--day7/__init__.py0
-rw-r--r--day7/backend.py180
-rw-r--r--day7/config.py2
-rw-r--r--day7/db.py21
-rw-r--r--day7/db/config.dbbin0 -> 16384 bytes
-rw-r--r--day7/db/cookies.dbbin0 -> 16384 bytes
-rw-r--r--day7/http_handler.py137
-rw-r--r--day7/templater.py5
-rw-r--r--day7/templates/form.html26
-rw-r--r--day7/templates/index.html7
-rw-r--r--day7/utils.py91
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
new file mode 100644
index 0000000..5c7d9e2
--- /dev/null
+++ b/day7/db/config.db
Binary files differ
diff --git a/day7/db/cookies.db b/day7/db/cookies.db
new file mode 100644
index 0000000..649e4a6
--- /dev/null
+++ b/day7/db/cookies.db
Binary files differ
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()