# # 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 . # 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()