Contents
简介
该模块不实现任何具体的 HTTP 请求(比如 GET、POST、HEAD... ...),
如果想了解深入了解,可以参见 SimpleHTTPServer 模块。
该模块只是实现了一个基本框架,让用户只需要关心如何处理具体的 HTTP 请求,并且部分实现了 HTTP/1.1 的长连接功能。
HTTP 各版本协议简介
HTTP 的消息格式定义基本如下:
- 起始行,必选,定义请求方法、请求路径等等
- 首部,可选,定义该消息的元数据
- 主体,可选,消息附带的数据
消息头部与消息体使用 \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 响应主体
默认值
这两个没啥好解释的。
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("&", "&").replace("<", "<").replace(">", ">")
|
对 URL 进行简单的转码,用来防止 XSS 。
详情参见: Issue1100201
该 Bug 简单来说就是利用上面的 DEFAULT_ERROR_MESSAGE 会将 URL 也包含在内的特点,
构造一个包含可执行代码 ( 比如 JS 代码 ) 的 URL,这样就会导致 HTTPServer 返回的错误页面中的代码会被浏览器执行。
PS: cgi.escape 已经提供了类似的方法 , 估计当时写该模块时 cgi 模块还未加入 , 或者作者想减少模块之间的依赖 , 没必要只要使用一个简单的函数而导入一个新的模块 .
HTTPServer
HTTPServer 的代码量很少,原因很简单,HTTP 协议基本可分为两个方面:
- 规定了 TCP 通信的格式与内容
- 规定了如何使用 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
|