def populate_request_info(self) -> None: """ set request-specific instance attributes according the current incoming request :return: None """ self.request_id = uuid.uuid1() logging.debug( f"{self.request_id} - retrieving request from {self.address}") headers_string = str(self.incoming, 'iso-8859-1').split('\r\n\r\n', 1)[0] self.header_length = len(headers_string) if self.header_length > MAX_HEADER_SIZE: self.error = 413 self.outgoing = get_response( 413, f"(header size must not exceed {MAX_HEADER_SIZE} bytes)") self.host = None else: self.method, self.headers = get_header_info_from_string( headers_string) if self.method not in METHODS: self.error = 400 self.outgoing = get_response( 400, f"(unsupported method \"{self.method}\")") self.host = self.headers[HOST].split(':', 1)[0] \ if HOST in self.headers and self.headers[HOST] else None
def forward_and_get_response(self, data: Any, head: Any, request_id: Optional[uuid.uuid1] = None ) -> Tuple[Optional[int], Any]: """ forward the request receive to one of the member servers of the balancer and return the response :param data: the request to forward (head + body or only body) :param head: the head of the request to forward if not included in the data parameter, otherwise it is zero length :param request_id: optional request id :return: a tuple containing an optional error code and the response (or error message) to send back; if dry run mode is active (self.dry_run == True) 200 OK response is returned """ error, response = None, None host_index = self.get_host_index(request_id) if self.dry_run: logging.error(f"{request_id} - 200 OK (dry run)") response = get_response(200, "(dry-run)") else: host = self.hosts[host_index] with request_time.labels(server=host[0], port=host[1]).time(): error, response = self.send_request_and_get_response( host, data, head, request_id) return error, response
def process_complete_request(self) -> None: """ forward the request to the balancer with id matching the self.host instance attribute and populate the outgoing buffer with response; if a correct balancer cannot be determined, a error is recorded :return: None """ logging.debug( f"{self.request_id} - processing complete request; head: " f"{self.head} - data: {self.incoming} ") if self.host and self.host in self.balancers: balancer = self.balancers[self.host] self.error, self.outgoing = balancer.forward_and_get_response( self.incoming, self.head, self.request_id) else: # Invalid or non-existent host, send error 400 (see link below) # https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.23 self.error, self.outgoing = 400, get_response(400) if self.host: logging.warning(f"{self.request_id} - host {self.host} " f"found in headers from {self.address} does " "not match any load balancer") else: logging.warning(f"{self.request_id} - could not find Host " f"field in headers from {self.address}")
def ready_to_reply(self) -> bool: """ check whether a response is ready to be sent back to the client :return: True if the current request is erroneous or not supported, the incoming request does not have a body, or it has body that was fully retrieved; False otherwise """ logging.debug(f"receiving {self.incoming} from {self.address} with " f"content-length {self.content_length}") if self.content_length is None: self.populate_request_info() if self.error or CONTENT_LENGTH_HEADER not in self.headers: return True elif TRANSFER_ENCODING_HEADER is self.headers: self.error = 400 self.outgoing = get_response( 400, f"(method {self.method} should" f" have a {CONTENT_LENGTH_HEADER} " f"header; " f"{TRANSFER_ENCODING_HEADER} is " "not supported)") self.host = None else: self.content_length = int(self.headers[CONTENT_LENGTH_HEADER]) logging.debug(f"received {self.incoming} from {self.address} " f"with content-length {self.content_length}") if self.received_so_far < self.header_length + self.content_length + 4: if EXPECT_HEADER in self.headers \ and self.headers[EXPECT_HEADER] == '100-continue' \ and not self.head: reply_continue = get_response(100) logging.debug(f"sending {reply_continue} to {self.address}") self.sock.send(reply_continue) self.head = self.incoming self.incoming = b'' return False else: return True
def reply(self) -> None: """ if no error is detected so far the request is processed and a reply to the client (with a response from a member server or an error message) with the content of the outgoing buffer. If the whole buffer is not fully sent, it is truncated so the remaining will be sent the next time the process_connection (above method) is called from the proxy.py event loop. :return: None """ if self.error is None: self.process_complete_request() # since outgoing buffer is begin sent, we do not need data from # incoming or head buffer anymore self.incoming = b'' self.head = b'' if self.outgoing and len(self.outgoing) > 0: logging.debug(f"sending {self.outgoing} (length " f"{len(self.outgoing)}) to {self.address}") else: logging.error( f"outgoing buffer supposed to be send to {self.address} " f"is null or zero-length: {self.outgoing} -> " f"502 Bad Gateway (empty response)") self.error, self.outgoing = 502, get_response(502) sent = self.sock.send(self.outgoing) self.outgoing = self.outgoing[sent:] if self.error: logging.info(f"closing connection to {self.address} because of " f"error {self.error}") self.close() self.record_status() self.reset_request_info()
def test_get_response(): assert get_response(100) == responses['100'].encode() assert get_response(200) == responses['200'].encode() assert get_response(200, 'THIS_IS_A_TEST') == responses['200_msg'].encode() assert get_response(400) == responses['400'].encode() assert get_response(400, 'THIS_IS_A_TEST') == responses['400_msg'].encode() assert get_response(413) == responses['413'].encode() assert get_response(413, 'THIS_IS_A_TEST') == responses['413_msg'].encode() assert get_response(500) == responses['500'].encode() assert get_response(500, 'THIS_IS_A_TEST') == responses['500_msg'].encode() assert get_response(502) == responses['502'].encode() assert get_response(502, 'THIS_IS_A_TEST') == responses['502_msg'].encode() assert get_response(504) == responses['504'].encode() assert get_response(504, 'THIS_IS_A_TEST') == responses['504_msg'].encode() # unknown status code defaults to 500 assert get_response(0) == responses['500'].encode() assert get_response(0, 'THIS_IS_A_TEST') == responses['500_msg'].encode()
def send_request_and_get_response(host: Tuple[str, int], data: Any, head: Any, request_id: Optional[uuid.uuid1] = None) -> Tuple[Optional[int], Any]: """ send a request to given host (specified with ip address + port) and retrieve the response :param host: ip address + port of socket for sending the request :param data: the request to forward (head + body or only body) :param head: the head of the request to forward if not included in the data parameter, otherwise it is zero length :param request_id: optional request id :return: a tuple containing an optional error code and the response (or error message) to send back """ error, response = None, None sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # order number of exchange on the socket with the server (use for SLx) # order is None for head and 1 for the first payload packet; if head # is with the data payload, we start at 0; this way packet with order # number 1 is the first one being sent without head data order = None logging.debug( f"{request_id} - processing request; head: {head} - data: {data} ") try: sock.connect(host) if head: logging.debug(f"{request_id} sending {head} to {host}") with socket_latency.labels(order=order, server=host[0], port=host[1]).time(): sock.send(head) response = sock.recv(BUFFER_SIZE) order = 1 logging.debug(f"{request_id} - response received from " f"{host}: {response}") if response != get_response(CONTINUE): sock.close() return error, response logging.debug(f"{request_id} sending {data} to {host}") if not order: order = 0 with socket_latency.labels(order=order, server=host[0], port=host[1]).time(): sock.sendall(data + "\r\n\r\n".encode()) response = sock.recv(BUFFER_SIZE) headers_string = str( response, 'iso-8859-1').split('\r\n\r\n', 1)[0] _, headers = get_header_info_from_string(headers_string) length = len(headers_string) + int(headers[CONTENT_LENGTH_HEADER]) + 4 logging.debug(f"{request_id} - response received from {host}: " f"{response} - length: {len(response)}/{length}") while len(response) < length: order += 1 with socket_latency.labels(order=order, server=host[0], port=host[1]).time(): incoming = sock.recv(BUFFER_SIZE) response += incoming logging.debug(f"{request_id} - response received from {host}: " f"{response} - length: {len(response)}/{length}") except socket.timeout: logging.error(f"{request_id} - 504 Gateway Timeout") error, response = 504, get_response(504) except OSError: logging.error(f"{request_id} - 502 Bad Gateway") error, response = 502, get_response(502) finally: sock.close() return error, response