一种通过魔改Python中http库源代码的方式使得flask在端口穿透时支持Proxy Protocol

一种通过魔改Python中http库源代码的方式使得flask在端口穿透时支持Proxy Protocol

阅读提示

本文共21,413字,阅读大约需42分钟。

引言

在去年使用frpc端口穿透后远程调试Python flask的一个项目时,我意外地发现在启用了proxy_protocol的端口穿透服务中,从端口穿透地址发送的HTTP请求会出现请求失败的问题,问题复现步骤如下:

第一步:配置端口穿透

在端口穿透客户端部分,笔者使用的是从github下载的frp_0.29.0_darwin_amd64,其中,配置文件frpc.ini是这样设置的:

[common]
server_addr=
server_port=7000
tcp_mux=true
protocol=tcp
user=
token=
dns_server=114.114.114.114
 
[proxy_card]
privilege_mode=true
type=tcp
local_ip=127.0.0.1
local_port=1919
remote_port=19054
use_encryption=false
use_compression=false
proxy_protocol_version = v1

注:此处的server_addrtokenuser均被隐去

第二步:编写测试用flask代码

此处笔者简单写了一个Python代码片段,其作用是在用户请求路径/时,以JSON格式返回请求者的IP、x-forwarded-for头和x-real-ip头。

from flask import *
app = Flask(__name__)

@app.route("/")
def root():
    return {
        'remote_addr': request.remote_addr,
        'x-forwarded-for': request.headers.get('x-forwarded-for'),
        'x-real-ip': request.headers.get('x-real-ip'),
    }
if __name__ == '__main__':
    app.run(host="0.0.0.0", port=1919)

第三步:启动代码和端口穿透服务,并测试

笔者使用的是macOS系统,在启动frpc时只需在终端中切换至frpc所在根目录,并输入如下指令即可:

./frpc -c frpc.ini

同时,在Pycharm中配置并运行Python代码,并通过浏览器访问端口穿透地址。

问题截屏和对比

通过内网IP访问时

内网访问时.png
可见,后端准确识别了内网IP地址,且没有报错,Headers中不包含相应请求头信息。

通过端口穿透服务链接访问时

端口穿透服务访问时.png
浏览器端返回ERR_INVALID_HTTP_RESPONSE,同时后端报错

127.0.0.1 - - [19/Mar/2022 02:33:20] code 400, message Bad request version ('19054')
127.0.0.1 - - [19/Mar/2022 02:33:20] "PROXY TCP4 101.94.253.172 10.0.8.2 64134 19054" HTTPStatus.BAD_REQUEST -

后端报错截图.png

值得注意的是,刚才展示的是当proxy_protocol_version被设置为v1时的报错信息,在其被设置成v2时,浏览器会返回ERR_EMPTY_RESPONSE错误,且后端没有相应的消息记录

问题分析

笔者通过分析flask中app.run()方法的源代码,发现其原理是使用werkzeug.serving库中run_simple函数创建简单HTTP服务器,该函数相对应的代码如下(函数说明部分因篇幅原因删去):

flask/app.py

    def run(
        self,
        host: t.Optional[str] = None,
        port: t.Optional[int] = None,
        debug: t.Optional[bool] = None,
        load_dotenv: bool = True,
        **options: t.Any,
    ) -> None:
        # Change this into a no-op if the server is invoked from the
        # command line. Have a look at cli.py for more information.
        if os.environ.get("FLASK_RUN_FROM_CLI") == "true":
            from .debughelpers import explain_ignored_app_run

            explain_ignored_app_run()
            return

        if get_load_dotenv(load_dotenv):
            cli.load_dotenv()

            # if set, let env vars override previous values
            if "FLASK_ENV" in os.environ:
                self.env = get_env()
                self.debug = get_debug_flag()
            elif "FLASK_DEBUG" in os.environ:
                self.debug = get_debug_flag()

        # debug passed to method overrides all other sources
        if debug is not None:
            self.debug = bool(debug)

        server_name = self.config.get("SERVER_NAME")
        sn_host = sn_port = None

        if server_name:
            sn_host, _, sn_port = server_name.partition(":")

        if not host:
            if sn_host:
                host = sn_host
            else:
                host = "127.0.0.1"

        if port or port == 0:
            port = int(port)
        elif sn_port:
            port = int(sn_port)
        else:
            port = 5000

        options.setdefault("use_reloader", self.debug)
        options.setdefault("use_debugger", self.debug)
        options.setdefault("threaded", True)

        cli.show_server_banner(self.env, self.debug, self.name, False)

        from werkzeug.serving import run_simple

        try:
            run_simple(t.cast(str, host), port, self, **options)
        finally:
            # reset the first request information if the development server
            # reset normally.  This makes it possible to restart the server
            # without reloader and that stuff from an interactive shell.
            self._got_first_request = False

继续溯源run_simple()函数,可得知其调用make_server()函数,该函数的定义如下:

def make_server(
    host: str,
    port: int,
    app: "WSGIApplication",
    threaded: bool = False,
    processes: int = 1,
    request_handler: t.Optional[t.Type[WSGIRequestHandler]] = None,
    passthrough_errors: bool = False,
    ssl_context: t.Optional[_TSSLContextArg] = None,
    fd: t.Optional[int] = None,
) -> BaseWSGIServer:

从中,可知默认情况下,处理请求的类为WSGIRequestHandler。对该类分析,得知其为BaseHTTPRequestHandler的子类,这下便豁然开朗。笔者随即便开始着手对于BaseHTTPRequestHandler类的分析。

.../http/server.py

class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
# 此处省略代码....
    def parse_request(self):
        """Parse a request (internal).

        The request should be stored in self.raw_requestline; the results
        are in self.command, self.path, self.request_version and
        self.headers.

        Return True for success, False for failure; on failure, any relevant
        error response has already been sent back.

        """
        self.command = None  # set in case of error on the first line
        self.request_version = version = self.default_request_version
        self.close_connection = True
        requestline = str(self.raw_requestline, 'iso-8859-1')
        requestline = requestline.rstrip('\r\n')
        self.requestline = requestline
        words = requestline.split()
        if len(words) == 0:
            return False

        if len(words) >= 3:  # Enough to determine protocol version
            version = words[-1]
            try:
                if not version.startswith('HTTP/'):
                    raise ValueError
                base_version_number = version.split('/', 1)[1]
                version_number = base_version_number.split(".")
                # RFC 2145 section 3.1 says there can be only one "." and
                #   - major and minor numbers MUST be treated as
                #      separate integers;
                #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
                #      turn is lower than HTTP/12.3;
                #   - Leading zeros MUST be ignored by recipients.
                if len(version_number) != 2:
                    raise ValueError
                version_number = int(version_number[0]), int(version_number[1])
            except (ValueError, IndexError):
                self.send_error(
                    HTTPStatus.BAD_REQUEST,
                    "Bad request version (%r)" % version)
                return False
            if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1":
                self.close_connection = False
            if version_number >= (2, 0):
                self.send_error(
                    HTTPStatus.HTTP_VERSION_NOT_SUPPORTED,
                    "Invalid HTTP version (%s)" % base_version_number)
                return False
            self.request_version = version

        if not 2 <= len(words) <= 3:
            self.send_error(
                HTTPStatus.BAD_REQUEST,
                "Bad request syntax (%r)" % requestline)
            return False
        command, path = words[:2]
        if len(words) == 2:
            self.close_connection = True
            if command != 'GET':
                self.send_error(
                    HTTPStatus.BAD_REQUEST,
                    "Bad HTTP/0.9 request type (%r)" % command)
                return False
        self.command, self.path = command, path

        # Examine the headers and look for a Connection directive.
        try:
            self.headers = http.client.parse_headers(self.rfile,
                                                     _class=self.MessageClass)
        except http.client.LineTooLong as err:
            self.send_error(
                HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
                "Line too long",
                str(err))
            return False
        except http.client.HTTPException as err:
            self.send_error(
                HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
                "Too many headers",
                str(err)
            )
            return False

        conntype = self.headers.get('Connection', "")
        if conntype.lower() == 'close':
            self.close_connection = True
        elif (conntype.lower() == 'keep-alive' and
              self.protocol_version >= "HTTP/1.1"):
            self.close_connection = False
        # Examine the headers and look for an Expect directive
        expect = self.headers.get('Expect', "")
        if (expect.lower() == "100-continue" and
                self.protocol_version >= "HTTP/1.1" and
                self.request_version >= "HTTP/1.1"):
            if not self.handle_expect_100():
                return False
        return True

# 此处省略代码....
    def handle_one_request(self):
        """Handle a single HTTP request.

        You normally don't need to override this method; see the class
        __doc__ string for information on how to handle specific HTTP
        commands such as GET and POST.

        """
        try:
            self.raw_requestline = self.rfile.readline(65537)
            if len(self.raw_requestline) > 65536:
                self.requestline = ''
                self.request_version = ''
                self.command = ''
                self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
                return
            if not self.raw_requestline:
                self.close_connection = True
                return
            if not self.parse_request():
                # An error code has been sent, just exit
                return
            mname = 'do_' + self.command
            if not hasattr(self, mname):
                self.send_error(
                    HTTPStatus.NOT_IMPLEMENTED,
                    "Unsupported method (%r)" % self.command)
                return
            method = getattr(self, mname)
            method()
            self.wfile.flush() #actually send the response if not already done.
        except socket.timeout as e:
            #a read or a write timed out.  Discard this connection
            self.log_error("Request timed out: %r", e)
            self.close_connection = True
            return

在代码中,parse_request()函数负责解析第一行的HTTP请求数据,此处笔者为了找出问题所在,在parse_request()函数中添加了print(requestline)代码,用于将正常请求和端口穿透请求之间对比。很快便发现了问题:

  • 正常请求时,requestline的值为:
GET / HTTP/1.1
  • 端口穿透后的请求中,requestline的值为:
PROXY TCP4 101.94.253.172 10.0.8.2 50982 19054

显然,问题的原因正出在HTTP请求前被多加上去的那一段请求内容。
笔者根据查阅Proxy Protocol官方定义文档时,看到了对该段内容的准确描述:

2.1. Human-readable header format (Version 1)

This is the format specified in version 1 of the protocol. It consists in one
line of US-ASCII text matching exactly the following block, sent immediately
and at once upon the connection establishment and prepended before any data
flowing from the sender to the receiver :

  - a string identifying the protocol : "PROXY" ( \x50 \x52 \x4F \x58 \x59 )
    Seeing this string indicates that this is version 1 of the protocol.

  - exactly one space : " " ( \x20 )

  - a string indicating the proxied INET protocol and family. As of version 1,
    only "TCP4" ( \x54 \x43 \x50 \x34 ) for TCP over IPv4, and "TCP6"
    ( \x54 \x43 \x50 \x36 ) for TCP over IPv6 are allowed. Other, unsupported,
    or unknown protocols must be reported with the name "UNKNOWN" ( \x55 \x4E
    \x4B \x4E \x4F \x57 \x4E ). For "UNKNOWN", the rest of the line before the
    CRLF may be omitted by the sender, and the receiver must ignore anything
    presented before the CRLF is found. Note that an earlier version of this
    specification suggested to use this when sending health checks, but this
    causes issues with servers that reject the "UNKNOWN" keyword. Thus is it
    now recommended not to send "UNKNOWN" when the connection is expected to
    be accepted, but only when it is not possible to correctly fill the PROXY
    line.

  - exactly one space : " " ( \x20 )

  - the layer 3 source address in its canonical format. IPv4 addresses must be
    indicated as a series of exactly 4 integers in the range [0..255] inclusive
    written in decimal representation separated by exactly one dot between each
    other. Heading zeroes are not permitted in front of numbers in order to
    avoid any possible confusion with octal numbers. IPv6 addresses must be
    indicated as series of 4 hexadecimal digits (upper or lower case) delimited
    by colons between each other, with the acceptance of one double colon
    sequence to replace the largest acceptable range of consecutive zeroes. The
    total number of decoded bits must exactly be 128. The advertised protocol
    family dictates what format to use.

  - exactly one space : " " ( \x20 )

  - the layer 3 destination address in its canonical format. It is the same
    format as the layer 3 source address and matches the same family.

  - exactly one space : " " ( \x20 )

  - the TCP source port represented as a decimal integer in the range
    [0..65535] inclusive. Heading zeroes are not permitted in front of numbers
    in order to avoid any possible confusion with octal numbers.

  - exactly one space : " " ( \x20 )

  - the TCP destination port represented as a decimal integer in the range
    [0..65535] inclusive. Heading zeroes are not permitted in front of numbers
    in order to avoid any possible confusion with octal numbers.

  - the CRLF sequence ( \x0D \x0A )


The maximum line lengths the receiver must support including the CRLF are :
  - TCP/IPv4 :
      "PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n"
    => 5 + 1 + 4 + 1 + 15 + 1 + 15 + 1 + 5 + 1 + 5 + 2 = 56 chars

  - TCP/IPv6 :
      "PROXY TCP6 ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n"
    => 5 + 1 + 4 + 1 + 39 + 1 + 39 + 1 + 5 + 1 + 5 + 2 = 104 chars

  - unknown connection (short form) :
      "PROXY UNKNOWN\r\n"
    => 5 + 1 + 7 + 2 = 15 chars

  - worst case (optional fields set to 0xff) :
      "PROXY UNKNOWN ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n"
    => 5 + 1 + 7 + 1 + 39 + 1 + 39 + 1 + 5 + 1 + 5 + 2 = 107 chars

So a 108-byte buffer is always enough to store all the line and a trailing zero
for string processing.

The receiver must wait for the CRLF sequence before starting to decode the
addresses in order to ensure they are complete and properly parsed. If the CRLF
sequence is not found in the first 107 characters, the receiver should declare
the line invalid. A receiver may reject an incomplete line which does not
contain the CRLF sequence in the first atomic read operation. The receiver must
not tolerate a single CR or LF character to end the line when a complete CRLF
sequence is expected.

Any sequence which does not exactly match the protocol must be discarded and
cause the receiver to abort the connection. It is recommended to abort the
connection as soon as possible so that the sender gets a chance to notice the
anomaly and log it.

If the announced transport protocol is "UNKNOWN", then the receiver knows that
the sender speaks the correct PROXY protocol with the appropriate version, and
SHOULD accept the connection and use the real connection's parameters as if
there were no PROXY protocol header on the wire. However, senders SHOULD not
use the "UNKNOWN" protocol when they are the initiators of outgoing connections
because some receivers may reject them. When a load balancing proxy has to send
health checks to a server, it SHOULD build a valid PROXY line which it will
fill with a getsockname()/getpeername() pair indicating the addresses used. It
is important to understand that doing so is not appropriate when some source
address translation is performed between the sender and the receiver.

An example of such a line before an HTTP request would look like this (CR
marked as "\r" and LF marked as "\n") :

    PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n
    GET / HTTP/1.1\r\n
    Host: 192.168.0.11\r\n
    \r\n

For the sender, the header line is easy to put into the output buffers once the
connection is established. Note that since the line is always shorter than an
MSS, the sender is guaranteed to always be able to emit it at once and should
not even bother handling partial sends. For the receiver, once the header is
parsed, it is easy to skip it from the input buffers. Please consult section 9
for implementation suggestions.

也就是说,在使用Proxy Protocol协议的请求中,HTTP请求前都会增加一行用于表示端口穿透信息,其开头固定为PROXY,紧接着的是网际协议(TCP4/TCP6),然后是请求来源IP、请求目标IP、请求来源端口号和请求目标端口号,最后以换行符\r\n结尾。

同样的,对于Proxy Protocol v2,也有相应的请求信息插入到HTTP请求前,篇幅原因此处不加以解读,读者可在文末相关链接中阅读协议原文。

解决方法

至此,问题便相当明显了,其实就是Python内置的http解析不支持Proxy Protocol咯。既然如此,只需要魔改代码,让其支持Proxy Protocol,一切问题便迎刃而解。
在此,笔者对server.py文件中BaseHTTPRequestHandler类进行了一些修改:简单而言,就是解析Proxy Protocol数据(支持v1和v2版本),将请求IP加入http请求头中,并在后续解析中删除Proxy Protocol数据。
核心代码如下:

    def parse_request(self):
        """Parse a request (internal).
        The request should be stored in self.raw_requestline; the results
        are in self.command, self.path, self.request_version and
        self.headers.
        Return True for success, False for failure; on failure, any relevant
        error response has already been sent back.
        """
        self.command = None  # set in case of error on the first line
        self.request_version = version = self.default_request_version
        self.close_connection = True
        requestline = str(self.raw_requestline, 'iso-8859-1')
        # --Proxy Protocol V2--

        if self.request_start:  # First time read the stream
            self.request_start = False
            if self.raw_requestline == b"\r\n":
                # Judge if is proxy_protocol_v2, its signature should be b'\r\n\r\n\x00\r\nQUIT\n'
                if self.rfile.readline(65537) != b"\r\n":
                    return False
                if self.rfile.readline(65537) != b"\x00\r\n":
                    return False
                if self.rfile.readline(65537) != b"QUIT\n":
                    return False
                self.enable_proxy_protocol_v2 = True

        # print("enable_proxy_protocol_v2", self.enable_proxy_protocol_v2)

        if self.enable_proxy_protocol_v2:  # If protocol proxy v2
            requestline = self.rfile.readline(65537)  # Read next line
            # The first 4 Bytes should be the data of IP address
            proxy_info = requestline[0:4]  # Proxy info
            # print(proxy_info)
            # The next byte (the 13th one) is the protocol version and command.
            # The highest four bits contains the version. As of this specification, it must
            # always be sent as \x2 and the receiver must only accept this value.
            proxy_protocol_version_command = proxy_info[0]
            proxy_protocol_version = proxy_protocol_version_command >> 4
            # print('Version:', proxy_protocol_version)
            # The lowest four bits represents the command.
            # \x0-LOCAL \x1-PROXY Other-Drop
            proxy_protocol_command = proxy_protocol_version_command - (proxy_protocol_version << 4)
            # print('Command:', proxy_protocol_command)
            if proxy_protocol_command != 0 and proxy_protocol_command != 1:
                return False

            # The 14th byte contains the transport protocol and address family. The highest 4
            # bits contain the address family, the lowest 4 bits contain the protocol.
            proxy_protocol_address = proxy_info[1]
            proxy_protocol_address_family = proxy_protocol_address >> 4
            # print('Address Family:', proxy_protocol_address_family)
            # 0x1-IPv4 0x2-IPv6
            # Other-Drop because http servers only accept AF_INET/AF_INET6
            if proxy_protocol_address_family != 1 and proxy_protocol_address_family != 2:
                return False
            # The transport protocol is specified in the lowest 4 bits of the 14th byte
            # 0x1-TCP
            # Other-Drop because http servers only accept TCP traffic
            proxy_protocol_address_protocol = proxy_protocol_address - (proxy_protocol_address_family << 4)
            if proxy_protocol_address_protocol != 1:
                return False
            # print('Address Protocol:', proxy_protocol_address_family)

            # The 15th and 16th bytes is the address length in bytes in network endian order.
            proxy_protocol_ip_data_length = (proxy_info[2] << 4) + int(proxy_info[3])
            # print('IP Data Length:', proxy_protocol_ip_data_length)

            # Starting from the 17th byte, addresses are presented in network byte order.
            # The address order is always the same :
            #   - source layer 3 address in network byte order
            #   - destination layer 3 address in network byte order
            #   - source layer 4 address if any, in network byte order (port)
            #   - destination layer 4 address if any, in network byte order (port)

            proxy_protocol_ip_data = requestline[4:proxy_protocol_ip_data_length + 4]
            # If the ip address is sent as \n or \r, it may be splited by function self.rfile.readline()
            readline_data = ' ';  # Just in case the request is invalid which may cause infinite loop
            while len(proxy_protocol_ip_data) < proxy_protocol_ip_data_length and len(readline_data) > 0:
                readline_data = self.rfile.readline(65537)
                requestline += readline_data
                proxy_protocol_ip_data = requestline[4:proxy_protocol_ip_data_length + 4]
            if len(proxy_protocol_ip_data) < proxy_protocol_ip_data_length:  # It's a bad request
                return False
            self.proxy_info = []
            if proxy_protocol_address_family == 1:  # TCP4
                self.proxy_info.append('TCP4')
                proxy_protocol_src_ip = proxy_protocol_ip_data[0:4]
                self.proxy_info.append('%r.%r.%r.%r' %
                                       (proxy_protocol_src_ip[0], proxy_protocol_src_ip[1], proxy_protocol_src_ip[2],
                                        proxy_protocol_src_ip[3]))
                proxy_protocol_dist_ip = proxy_protocol_ip_data[4:8]
                self.proxy_info.append('%r.%r.%r.%r' %
                                       (proxy_protocol_dist_ip[0], proxy_protocol_dist_ip[1], proxy_protocol_dist_ip[2],
                                        proxy_protocol_dist_ip[3]))
                self.proxy_info.append((proxy_protocol_ip_data[8] << 8) + proxy_protocol_ip_data[9])
                self.proxy_info.append((proxy_protocol_ip_data[10] << 8) + proxy_protocol_ip_data[11])
            else:
                self.proxy_info.append('TCP6')

                def bytes_2_ip6(bytes_arr):
                    ip_int = int.from_bytes(bytes_arr, byteorder='big')
                    import ipaddress
                    return str(ipaddress.ip_address(ip_int))

                proxy_protocol_src_ip = proxy_protocol_ip_data[0:16]
                self.proxy_info.append(bytes_2_ip6(proxy_protocol_src_ip))
                proxy_protocol_dist_ip = proxy_protocol_ip_data[16:32]
                self.proxy_info.append(bytes_2_ip6(proxy_protocol_dist_ip))
                self.proxy_info.append((proxy_protocol_ip_data[32] << 8) + proxy_protocol_ip_data[33])
                self.proxy_info.append((proxy_protocol_ip_data[34] << 8) + proxy_protocol_ip_data[35])

            # print(self.proxy_info)
            requestline = str(requestline[4 + proxy_protocol_ip_data_length:], 'iso-8859-1')  # Real request data
        # ------------------

        requestline = requestline.rstrip('\r\n')
        self.requestline = requestline

        # --Proxy Protocol V1--
        words = requestline.split()

        if len(words) == 0:
            return False
        if words[0] == "PROXY":  # If run with proxy_protocol v1
            try:
                self.proxy_info = [words[1], words[2], words[3], words[4], words[5]]
                # Proxy info: [AF, L3_SADDR, L3_DADDR, L4_SADDR, L4_DADDR]
            except:
                self.send_error(
                    HTTPStatus.BAD_REQUEST,
                    "Bad Protocol Data")
                return False
            self.handle_one_request()  # Read next line
            requestline = str(self.raw_requestline, 'iso-8859-1')
            requestline = re.sub("^PROXY[ a-z0-9A-Z.]*\r\n", "",
                                 requestline)  # Delete proxy line to make sure the request is valid
            requestline = requestline.rstrip('\r\n')
            self.requestline = requestline
            words = requestline.split()  # Regenerate the words
        # ------------------

        if len(words) == 0:
            return False

        if len(words) >= 3:  # Enough to determine protocol version
            version = words[-1]
            try:
                if not version.startswith('HTTP/'):
                    raise ValueError
                base_version_number = version.split('/', 1)[1]
                version_number = base_version_number.split(".")
                # RFC 2145 section 3.1 says there can be only one "." and
                #   - major and minor numbers MUST be treated as
                #      separate integers;
                #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
                #      turn is lower than HTTP/12.3;
                #   - Leading zeros MUST be ignored by recipients.
                if len(version_number) != 2:
                    raise ValueError
                version_number = int(version_number[0]), int(version_number[1])
            except (ValueError, IndexError):
                self.send_error(
                    HTTPStatus.BAD_REQUEST,
                    "Bad request version (%r)" % version)
                return False
            if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1":
                self.close_connection = False
            if version_number >= (2, 0):
                self.send_error(
                    HTTPStatus.HTTP_VERSION_NOT_SUPPORTED,
                    "Invalid HTTP version (%s)" % base_version_number)
                return False
            self.request_version = version

        if not 2 <= len(words) <= 3:
            self.send_error(
                HTTPStatus.BAD_REQUEST,
                "Bad request syntax (%r)" % requestline)
            return False
        command, path = words[:2]
        if len(words) == 2:
            self.close_connection = True
            if command != 'GET':
                self.send_error(
                    HTTPStatus.BAD_REQUEST,
                    "Bad HTTP/0.9 request type (%r)" % command)
                return False
        self.command, self.path = command, path

        # Examine the headers and look for a Connection directive.
        try:
            self.headers = http.client.parse_headers(self.rfile,
                                                     _class=self.MessageClass)

            # If Proxy, automatically add headers
            if self.proxy_info != []:
                # print("Through Proxy", requestline, self.proxy_info)
                self.headers.add_header('X-Real-IP', self.proxy_info[1])
                if self.headers.get('X-Forwarded-For'):
                    self.headers.add_header('X-Forwarded-For',
                                            self.headers.get('X-Forwarded-For') + ',' + self.proxy_info[1])
                else:
                    self.headers.add_header('X-Forwarded-For', self.proxy_info[1])
            # ---------

        except http.client.LineTooLong as err:
            self.send_error(
                HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
                "Line too long",
                str(err))
            return False
        except http.client.HTTPException as err:
            self.send_error(
                HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
                "Too many headers",
                str(err)
            )
            return False

        conntype = self.headers.get('Connection', "")
        if conntype.lower() == 'close':
            self.close_connection = True
        elif (conntype.lower() == 'keep-alive' and
              self.protocol_version >= "HTTP/1.1"):
            self.close_connection = False
        # Examine the headers and look for an Expect directive
        expect = self.headers.get('Expect', "")
        if (expect.lower() == "100-continue" and
                self.protocol_version >= "HTTP/1.1" and
                self.request_version >= "HTTP/1.1"):
            if not self.handle_expect_100():
                return False
        return True

完整的魔改后代码已开源至GITHUBserver.py代码见此链接

使用方法
下载server.py,将Python lib中http/server.py替换成下载的文件即可。(以防万一,替换前请先备份)

具体替换地址,不同操作系统和环境,地址均不相同,在笔者的计算机中,其地址为:

/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/http/server.py

希望能对你有些帮助。

问题解决

经过以上操作后,再次访问端口穿透服务的地址,可见问题已被成功解决:
解决后的截屏.png

最后,附上GitHub仓库地址~

https://github.com/vvbbnn00/python_proxy_protocol

如果对你有帮助,不妨点个Star

参考资料

至此,问题解决!完结撒花✿✿ヽ(°▽°)ノ✿

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×