Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
    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}")
Esempio n. 4
0
    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
Esempio n. 5
0
    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()
Esempio n. 6
0
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()
Esempio n. 7
0
    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