cn-py-serv/server/http_handler.py
Иван Солнцев 346f01c9d9
Add for file iterator check access to file
- Rename function fileiter to file_iterator;

Fix:
 - BrokenPipeError for thread;
 - Log strings;
2024-10-09 14:31:57 +03:00

264 lines
8.6 KiB
Python

#
# CatNet Python Server
#
# Copyright (C) 2023-2024 John Solntsev
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
import logging
import config
from .common import MAX_REQUEST_LINE_SIZE, HandlerType, HTTPMethod
from .file_read import file_iterator
from .response import Response
log = logging.getLogger(__name__)
class HTTPHandler:
def __init__(self, conn, addr):
self.conn = conn
self.addr = addr
self.read_fd = conn.makefile("rb")
self.write_fd = conn.makefile("wb")
self.http_method = ""
self.http_address = ""
self.http_get_request = {}
self.http_headers = {}
self.data = b""
try:
self.__handle_connection()
except BrokenPipeError:
log.info("Conenction pipe broken, close thread")
def __write_data(self, response):
for chunk in response:
self.write_fd.write(chunk)
self.write_fd.flush()
def __read_data(self, size=MAX_REQUEST_LINE_SIZE+1):
return self.read_fd.readline(size)
def __close(self):
self.write_fd.close()
self.read_fd.close()
self.conn.close()
def __handle_connection(self):
"""
Chain of handle first line of HTTP request
"""
raw = self.__read_data()
if len(raw) > MAX_REQUEST_LINE_SIZE:
log.debug("Request line too long")
r = Response(status_code=413, data=b"Request line too long")
self.__write_data(r)
self.__close()
return
# FIXME data maybe isn't UTF-8 or just binary
req_line = raw.decode().rstrip("\r\n")
req_line_splited = req_line.split(" ")
if len(req_line_splited) != 3:
if len(req_line_splited) > 3:
log.debug("Request head too long")
else:
log.debug("Request head too small")
r = Response(status_code=400, data=b"Bad request")
self.__write_data(r)
self.__close()
return
if req_line_splited[2].startswith("HTTP/"):
if (req_line_splited[2] == "HTTP/1.0" or
req_line_splited[2] == "HTTP/1.1"):
http_method = req_line_splited[0]
else:
log.debug("Unsupported HTTP version")
r = Response(status_code=505, data=b"HTTP Version Not Supported")
self.__write_data(r)
self.__close()
return
else:
log.debug("Protocol isn't HTTP version 1.0 or 1.1")
r = Response(status_code=400, data=b"Bad request")
self.__write_data(r)
self.__close()
return
if http_method == "GET":
self.http_method = HTTPMethod.GET
elif http_method == "POST":
self.http_method = HTTPMethod.POST
elif http_method == "PUT":
self.http_method = HTTPMethod.PUT
elif http_method == "HEAD":
self.http_method = HTTPMethod.HEAD
elif http_method == "DELETE":
self.http_method = HTTPMethod.DELETE
elif http_method == "OPTIONS":
self.http_method = HTTPMethod.OPTIONS
elif http_method == "TRACE":
self.http_method = HTTPMethod.TRACE
elif http_method == "PATCH":
self.http_method = HTTPMethod.PATCH
else:
log.debug("Method not allowed")
r = Response(status_code=405, data=b"Method not allowed")
self.__write_data(r)
self.__close()
return
if (self.http_method == HTTPMethod.GET and
req_line_splited[1].find("?") >= 0):
addr, get_req = req_line_splited[1].split("?", 1)
self.http_address = addr
for param in get_req.split("&"):
if len(param) == 0:
continue
elif param.find("=") >= 0:
name, val = param.split("=", 1)
else:
name, val = (param, "")
self.http_get_request.update({name: val})
else:
self.http_address = req_line_splited[1]
self.http_get_request = {}
self.__http_header_handle()
def __http_header_handle(self):
"""
Chain of handle headers in HTTP request
"""
while True:
raw = self.__read_data()
if len(raw) > MAX_REQUEST_LINE_SIZE:
log.debug("Request header line too long")
self.__close()
return
if raw == b"":
log.debug("Client not send data, close connection")
self.__close()
return
if raw == b"\r\n" or raw == b"\n":
self.__http_data_handle()
break
else:
decoded_data = raw.decode("UTF-8").rstrip("\r\n")
decoded_data_split = decoded_data.split(":", 1)
self.http_headers.update(
{
decoded_data_split[0]: decoded_data_split[1].strip(" ")
})
def __http_data_handle(self):
"""
Chain of receive data partitions HTTP request
"""
if "Content-Length" in self.http_headers:
# TODO: if content-length biggest of MAX_REQUEST_LINE_SIZE - partition receive
log.debug("Request has contain body, try to read")
log.debug("Check Content-Length value type:")
if self.http_headers["Content-Length"].isdigit():
log.debug(" Pass")
bytes_to_receive = int(self.http_headers["Content-Length"])
else:
log.error(" Content-Length is not integer")
self.__close()
return
log.debug("Want to receive %s bytes from client", bytes_to_receive)
self.data = self.read_fd.read(bytes_to_receive)
log.debug("Request method: %s", self.http_method)
log.debug("Requested address: %s", self.http_address)
log.debug("Request headers: %s", self.http_headers)
log.debug("Request in GET after ?: %s", self.http_get_request)
log.debug("Data: %s", self.data)
self.__send_form_data()
def __send_form_data(self):
"""
Chain of handle, pack & send data to client
"""
found = False
for path in config.urls:
if self.http_address == path.address:
if self.http_method in path.methods:
found = True
if path.handler_type == HandlerType.STATIC_FILE:
log.debug("Address associated with static file on path %s", path.link)
r = Response(data=file_iterator(path.link))
self.__write_data(r)
elif path.handler_type == HandlerType.FUNCTION:
log.debug("Address associated with function")
func = path.link
if func.__code__.co_argcount == 0:
func_return = func()
elif func.__code__.co_argcount == 1:
func_return = func(self.http_headers)
elif func.__code__.co_argcount == 2:
func_return = func(self.http_headers, self.http_get_request)
elif func.__code__.co_argcount == 3:
func_return = func(self.http_headers, self.http_get_request, self.data)
self.__write_data(func_return)
else:
log.warning("Address configured on server, but type not allowed URL type in URLs list")
r = Response(status_code=404,
data=b"Address configured on server, but type not allowed URL type in URLs list")
self.__write_data(r)
if not found:
r = Response(status_code=404, data=b"Not found!")
self.__write_data(r)
self.__handle_connection()