January Star
  • Home
  • Categories
  • Tags
  • Archives

BaseHTTPServer标准库源码学习

Contents

  • 简介
  • HTTP 各版本协议简介
    • HTTP/0.9
    • HTTP/1.0
    • HTTP/1.1
  • 默认值
    • DEFAULT_ERROR_MESSAGE
    • DEFAULT_ERROR_CONTENT_TYPE
  • _quote_html
  • HTTPServer
    • allow_reuse_address
  • BaseHTTPRequestHandler
    • URL 长度限制
    • 打印日志的相关方法
    • responses

简介

该模块不实现任何具体的 HTTP 请求(比如 GET、POST、HEAD... ...),

如果想了解深入了解,可以参见 SimpleHTTPServer 模块。

该模块只是实现了一个基本框架,让用户只需要关心如何处理具体的 HTTP 请求,并且部分实现了 HTTP/1.1 的长连接功能。

HTTP 各版本协议简介

HTTP 的消息格式定义基本如下:

  1. 起始行,必选,定义请求方法、请求路径等等
  2. 首部,可选,定义该消息的元数据
  3. 主体,可选,消息附带的数据

消息头部与消息体使用 \r\n 来进行分割。

HTTP/0.9

HTTP 的 1991 原型版本称为 HTTP/0.9 ,它的初衷是为了获取简单的 HTML。

该协议有很多严重的设计缺陷,只适用于与老客户端进行交互,并且它只支持 GET 方法。

它的请求消息就一行:

<command> <path>

同时它的响应消息不包含消息头部,只包含响应数据。

HTTP/1.0

该版本是第一个得到广泛应用的版本。

它添加了版本号、各种 HTTP 首部、一些额外的 HTTP 方法,以及对多媒体对象的处理。

请求消息格式:

<command> <path> <version>

HTTP 请求首部

HTTP 请求主体

响应消息格式:

<version> <responsecode> <responsestring>

HTTP 响应首部

HTTP 响应主体

HTTP/1.1

该版本重点关注的是校正 HTTP 设计中的结构性缺陷。明确语义,引入重要的性能优化措施,并删除一些不好的特性。

比如本模块里部分支持 HTTP/1.1 定义的长连接功能。

默认值

这两个没啥好解释的。

DEFAULT_ERROR_MESSAGE

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DEFAULT_ERROR_MESSAGE = """\
<head>
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code %(code)d.
<p>Message: %(message)s.
<p>Error code explanation: %(code)s = %(explain)s.
</body>
"""

DEFAULT_ERROR_CONTENT_TYPE

DEFAULT_ERROR_CONTENT_TYPE = "text/html"

_quote_html

1
2
def _quote_html(html):
    return html.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

对 URL 进行简单的转码,用来防止 XSS 。

详情参见: Issue1100201

该 Bug 简单来说就是利用上面的 DEFAULT_ERROR_MESSAGE 会将 URL 也包含在内的特点,

构造一个包含可执行代码 ( 比如 JS 代码 ) 的 URL,这样就会导致 HTTPServer 返回的错误页面中的代码会被浏览器执行。

PS: cgi.escape 已经提供了类似的方法 , 估计当时写该模块时 cgi 模块还未加入 , 或者作者想减少模块之间的依赖 , 没必要只要使用一个简单的函数而导入一个新的模块 .

HTTPServer

HTTPServer 的代码量很少,原因很简单,HTTP 协议基本可分为两个方面:

  1. 规定了 TCP 通信的格式与内容
  2. 规定了如何使用 TCP 连接

它本质上就是一个 TCPServer ,主要看它是如何进行请求处理的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class HTTPServer(SocketServer.TCPServer):

    allow_reuse_address = 1

    def server_bind(self):
        # 重载 `server_bind` 是为了获取 `server_name` 和 `server_port`
        SocketServer.TCPServer.server_bind(self)
        host, port = self.socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port

allow_reuse_address

一般来说,一个端口释放后会等待两分钟之后才能再被使用,allow_reuse_address 是让端口释放后立即就可以被再次使用。

如果想了解有关详情,请 Google SO_REUSEADDR

BaseHTTPRequestHandler

该类就是进行具体 HTTP 请求(GET、POST... ...)的类。

如果用户想处理 HTTP 请求(假定 HTTP 方法为 SPAM),只需要定义 do_SPAM 方法即可。

Note

此处的 SPAM 是大小写敏感的。

只需要按如下格式编写代码即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MyHTTPRequestHandler(BaseHTTPRequestHandler):
    HELLO_WORLD = '''\
<html>
<body>
<p>Hello, web!</p>
</body>
</html>
'''
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(len(self.HELLO_WORLD))
        self.end_headers()
        self.wfile.write(self.HELLO_WORLD)

此外, BaseHTTPRequestHandler 中还有一系列的属性,可以获取以及处理与请求相关的信息。

  • client_address: 客户端 IP
  • command: HTTP 方法
  • path: HTTP 请求路径
  • version: HTTP 版本号
  • headers: HTTP 请求首部,它是 mimetools.Message 实例
  • rfile: 读入请求消息的文件对象
  • wfile: 写入响应消息的文件对象

Warning

如果要返回响应消息,第一行必须要写入响应首行;然后再写入 0 到多个的响应头部;然后再写入一个空行;最后根据需要再写入消息主体。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
class BaseHTTPRequestHandler(SocketServer.StreamRequestHandler):

    sys_version = "Python/" + sys.version.split()[0]

    server_version = "BaseHTTP/" + __version__

    default_request_version = "HTTP/0.9"

    def parse_request(self):
        # 解释一个 HTTP 请求的首行及头部,并将解析结果放到自身的各个属性中 
        self.command = None  # set in case of error on the first line
        self.request_version = version = self.default_request_version
        self.close_connection = 1
        requestline = self.raw_requestline
        requestline = requestline.rstrip('\r\n')
        self.requestline = requestline
        words = requestline.split()
        if len(words) == 3: # 非 HTTP/0.9 版本 
            command, path, version = words
            if version[:5] != 'HTTP/':
                self.send_error(400, "Bad request version (%r)" % version)
                return False
            try:
                # 解释请求消息中的 HTTP 版本号,这里最好也封装起来 
                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(400, "Bad request version (%r)" % version)
                return False
            if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1":
                self.close_connection = 0
            if version_number >= (2, 0):
                self.send_error(505,
                          "Invalid HTTP Version (%s)" % base_version_number)
                return False
        elif len(words) == 2: # 请求首行只有两个字段,说明是 HTTP/0.9
            command, path = words
            self.close_connection = 1
            if command != 'GET': # HTTP/0.9 只支持 GET 方法 
                self.send_error(400,
                                "Bad HTTP/0.9 request type (%r)" % command)
                return False
        elif not words:
            return False
        else:
            self.send_error(400, "Bad request syntax (%r)" % requestline)
            return False
        self.command, self.path, self.request_version = command, path, version

        # Examine the headers and look for a Connection directive
        self.headers = self.MessageClass(self.rfile, 0)

        # 这里就是所谓的部分支持 HTTP/1.1 版本的长连接功能 
        conntype = self.headers.get('Connection', "")
        if conntype.lower() == 'close':
            self.close_connection = 1
        elif (conntype.lower() == 'keep-alive' and
              self.protocol_version >= "HTTP/1.1"):
            self.close_connection = 0
        return True

    def handle_one_request(self):
        """ 处理一次请求 """
        try:
            # 读取请求数据的第一行,不过这里限制了第一行的大小 
            # 由于方法名和版本号长度基本算固定的,所以这里就相当于限制 URL 的长度 
            self.raw_requestline = self.rfile.readline(65537)
            if len(self.raw_requestline) > 65536: # HTTP 请求首行太长了,对应的 HTTP code 码就是 414
                self.requestline = ''
                self.request_version = ''
                self.command = ''
                self.send_error(414)
                return
            # 没有读到任何数据,说明通信异常,该 socket 连接可以直接关闭 
            if not self.raw_requestline:
                self.close_connection = 1
                return
            # 解析请求消息出错 
            if not self.parse_request():
                # An error code has been sent, just exit
                return
            # 以下的几行代码可以封装起来 
            # 根据 HTTP 方法来调用 `do_SPAM` 方法。
            mname = 'do_' + self.command
            if not hasattr(self, mname):
                self.send_error(501, "Unsupported method (%r)" % self.command)
                return
            method = getattr(self, mname)
            method()
            self.wfile.flush()
        except socket.timeout, e:
            self.log_error("Request timed out: %r", e)
            self.close_connection = 1
            return

    def handle(self):
        """ 入口方法,从这里开始整个 HTTP 请求处理及响应 """
        self.close_connection = 1 # 关闭连接标志 

        self.handle_one_request()
        while not self.close_connection:
            self.handle_one_request()

    def send_error(self, code, message=None):
        """ 发送一个错误格式的响应消息 """
        try:
            # 网上有人吐槽 `long` 为 Python 的内置类型,不建议当作变量名 
            short, long = self.responses[code]
        except KeyError:
            short, long = '???', '???'
        if message is None:
            message = short
        explain = long
        self.log_error("code %d, message %s", code, message)
        # 这里使用了 `_quote_html` ,至于为什么使用,上面已有说明。
        content = (self.error_message_format %
                   {'code': code, 'message': _quote_html(message), 'explain': explain})
        self.send_response(code, message)
        self.send_header("Content-Type", self.error_content_type)
        self.send_header('Connection', 'close')
        self.end_headers()
        # 这里的 HTTP code 码都硬编码了,不建议。
        if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
            self.wfile.write(content)

    error_message_format = DEFAULT_ERROR_MESSAGE
    error_content_type = DEFAULT_ERROR_CONTENT_TYPE

    def send_response(self, code, message=None):
        """ 发送响应消息 """
        self.log_request(code)
        if message is None:
            if code in self.responses:
                message = self.responses[code][0]
            else:
                message = ''
        # HTTP/0.9 版本没有响应消息头部定义 
        if self.request_version != 'HTTP/0.9':
            # 响应消息首行 
            self.wfile.write("%s %d %s\r\n" %
                             (self.protocol_version, code, message))
            # print (self.protocol_version, code, message)
        self.send_header('Server', self.version_string())
        self.send_header('Date', self.date_time_string())

    def send_header(self, keyword, value):
        # 写入响应头部,HTTP/0.9 版本没有响应消息头部定义 
        if self.request_version != 'HTTP/0.9':
            self.wfile.write("%s: %s\r\n" % (keyword, value))

        # HTTP/1.1 协议规定 
        # 如果不需要支持长连接,则返回 `close`
        # 反之,返回 `keep-alive`
        if keyword.lower() == 'connection':
            if value.lower() == 'close':
                self.close_connection = 1
            elif value.lower() == 'keep-alive':
                self.close_connection = 0

    def end_headers(self):
        # HTTP 消息头部和消息体之间有一个空行,以 \r\n 分割 
        # HTTP/0.9 版本只要求返回消息体,没有响应消息头部定义 
        if self.request_version != 'HTTP/0.9':
            self.wfile.write("\r\n")

    def log_request(self, code='-', size='-'):
        # 打印请求信息 
        self.log_message('"%s" %s %s',
                         self.requestline, str(code), str(size))

    def log_error(self, format, *args):
        # 打印错误信息,不过这里还是简单调用了 log_message 方法,进行标准输出了 
        # 规范的做法应该进行错误输出 
        self.log_message(format, *args)

    def log_message(self, format, *args):
        # 可重载,详见下面的说明 
        sys.stderr.write("%s - - [%s] %s\n" %
                         (self.client_address[0],
                          self.log_date_time_string(),
                          format%args))

    def version_string(self):
        return self.server_version + ' ' + self.sys_version

    def date_time_string(self, timestamp=None):
        # 当前的日期与时间字符串,用于响应消息头部中的 Date 字段 
        if timestamp is None:
            timestamp = time.time()
        year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
        s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
                self.weekdayname[wd],
                day, self.monthname[month], year,
                hh, mm, ss)
        return s

    def log_date_time_string(self):
        # 当前的时间,用于日志记录 
        now = time.time()
        year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
        s = "%02d/%3s/%04d %02d:%02d:%02d" % (
                day, self.monthname[month], year, hh, mm, ss)
        return s

    weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

    monthname = [None,
                 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

    def address_string(self):
        # 获取客户端地址,用来进行日志记录。
        # 此处可能会有 DNS 解析问题。
        host, port = self.client_address[:2]
        return socket.getfqdn(host)

    # Essentially static class variables

    # The version of the HTTP protocol we support.
    # Set this to HTTP/1.1 to enable automatic keepalive
    protocol_version = "HTTP/1.0"

    # The Message-like class used to parse headers
    MessageClass = mimetools.Message

    responses = {
        100: ('Continue', 'Request received, please continue'),
        ... ...
        200: ('OK', 'Request fulfilled, document follows'),
        ... ...
        300: ('Multiple Choices',
              'Object has several resources -- see URI list'),
        ... ...
        400: ('Bad Request',
              'Bad request syntax or unsupported method'),
        ... ...
        500: ('Internal Server Error', 'Server got itself in trouble'),
        ... ...
        }

URL 长度限制

HTTP 协议规范里面貌似没有对 URL 的长度做出限制,但是各家浏览器以及 Web 服务器在实现时,都对 URL 的长度做了不同程序的限制。

本模块的代码也对 URL 的长度做了限制,大概 65530 左右的样子 ( 该值不确定,因为代码中只对第一行进行了长度判断 )。

打印日志的相关方法

上面的代码中有很多是有关打印日志的,该日志多用于调试。

其实这些方法最好单独整一个类出来存放,因为这方法并不是 BaseHTTPRequestHandler 的主要功能代码。

Warning

由于日志中会获取客户端的主机名等信息,可能会有 DNS 解析过慢的问题。

所以在正式使用时一定要关闭日志的输出功能,比如重载 log_message 方法。

1
2
def log_message(self, format, *args):
    pass

responses

罗列了 RFC 2616 中定义的 HTTP 返回码及其说明。

其格式为 {HTTP 返回码 : ( 简短说明,详细说明 )}

0 Comments

Published

Nov 20, 2014

Category

Python

Tags

  • python 23
  • stdlibs 15

Contact

  • Powered by Pelican. Theme: Elegant by Talha Mansoor