-
Notifications
You must be signed in to change notification settings - Fork 141
Expand file tree
/
Copy pathweb-server.py
More file actions
457 lines (413 loc) · 15.7 KB
/
web-server.py
File metadata and controls
457 lines (413 loc) · 15.7 KB
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# Author : XueWeiHan
# E-mail : 595666367@qq.com
# Date : 2022-03-29 16:56
# Desc : 迷你 Web 服务器
import sys
import socket
import selectors
import datetime
import time
import html
# 默认错误信息 HTML 模版
DEFAULT_ERROR_MESSAGE = """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: %(code)d</p>
<p>Message: %(message)s.</p>
<p>Error code explanation: %(code)s - %(explain)s.</p>
</body>
</html>
"""
# 处理连接进行数据通信
class HTTPServer(object):
def __init__(self, server_address, RequestHandlerClass):
self.server_address = server_address
self.RequestHandlerClass = RequestHandlerClass
self.request_queue_size = 5
self.__shutdown_request = False
# 创建 TCP Socket
self.socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
try:
# 绑定 socket 和端口
self.server_bind()
# 开始监听端口
self.server_activate()
except:
# 关闭 socket
self.server_close()
raise
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定端口
self.socket.bind(self.server_address)
def server_activate(self):
# 监听端口
self.socket.listen(self.request_queue_size)
def server_close(self):
# 关闭socket
self.socket.close()
def fileno(self):
"""
返回 socket 文件号
用于 select 监控文件句柄状态
"""
return self.socket.fileno()
def serve_forever(self, poll_interval=0.5):
"""
服务器启动入口
"""
with selectors.SelectSelector() as selector:
# 基于 select 轮询获取已准备好的可读句柄
selector.register(self, selectors.EVENT_READ)
while True:
ready = selector.select(poll_interval)
if ready:
# 有准备好的可读文件句柄,则与客户端的链接建立完毕
# 可以进行下一步
self._handle_request_noblock()
def _handle_request_noblock(self):
"""
处理请求
"""
try:
# 接收来自客户端的请求:request 对象
request, client_address = self.get_request()
except socket.error:
return
try:
# 开始处理请求
self.process_request(request, client_address)
except:
self.handle_error(client_address)
self.shutdown_request(request)
def get_request(self):
# 接收请求
return self.socket.accept()
def process_request(self, request, client_address):
# 处理请求
self.finish_request(request, client_address)
self.shutdown_request(request)
def finish_request(self, request, client_address):
# 调用处理请求的类
self.RequestHandlerClass(request, client_address, self)
def shutdown_request(self, request):
try:
# 后续不允许发送和接收
request.shutdown(socket.SHUT_WR)
except socket.error:
pass # some platforms may raise ENOTCONN here
# 结束请求
request.close()
def handle_error(self, client_address):
print('-'*40, file=sys.stderr)
print('Exception occurred during processing of request from',
client_address, file=sys.stderr)
import traceback
traceback.print_exc()
print('-'*40, file=sys.stderr)
# 处理请求
class HTTPRequestHandler(object):
responses = {
100: ('Continue', 'Request received, please continue'),
101: ('Switching Protocols',
'Switching to new protocol; obey Upgrade header'),
200: ('OK', 'Request fulfilled, document follows'),
201: ('Created', 'Document created, URL follows'),
202: ('Accepted',
'Request accepted, processing continues off-line'),
203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
204: ('No Content', 'Request fulfilled, nothing follows'),
205: ('Reset Content', 'Clear input form for further input.'),
206: ('Partial Content', 'Partial content follows.'),
300: ('Multiple Choices',
'Object has several resources -- see URI list'),
301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
302: ('Found', 'Object moved temporarily -- see URI list'),
303: ('See Other', 'Object moved -- see Method and URL list'),
304: ('Not Modified',
'Document has not changed since given time'),
305: ('Use Proxy',
'You must use proxy specified in Location to access this '
'resource.'),
307: ('Temporary Redirect',
'Object moved temporarily -- see URI list'),
400: ('Bad Request',
'Bad request syntax or unsupported method'),
401: ('Unauthorized',
'No permission -- see authorization schemes'),
402: ('Payment Required',
'No payment -- see charging schemes'),
403: ('Forbidden',
'Request forbidden -- authorization will not help'),
404: ('Not Found', 'Nothing matches the given URI'),
405: ('Method Not Allowed',
'Specified method is invalid for this resource.'),
406: ('Not Acceptable', 'URI not available in preferred format.'),
407: ('Proxy Authentication Required', 'You must authenticate with '
'this proxy before proceeding.'),
408: ('Request Timeout', 'Request timed out; try again later.'),
409: ('Conflict', 'Request conflict.'),
410: ('Gone',
'URI no longer exists and has been permanently removed.'),
411: ('Length Required', 'Client must specify Content-Length.'),
412: ('Precondition Failed', 'Precondition in headers is false.'),
413: ('Request Entity Too Large', 'Entity is too large.'),
414: ('Request-URI Too Long', 'URI is too long.'),
415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
416: ('Requested Range Not Satisfiable',
'Cannot satisfy request range.'),
417: ('Expectation Failed',
'Expect condition could not be satisfied.'),
500: ('Internal Server Error', 'Server got itself in trouble'),
501: ('Not Implemented',
'Server does not support this operation'),
502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
503: ('Service Unavailable',
'The server cannot process the request due to a high load'),
504: ('Gateway Timeout',
'The gateway server did not receive a timely response'),
505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
}
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.protocol_version = "HTTP/1.0"
self.setup()
try:
self.handle()
finally:
self.finish()
def setup(self):
self.connection = self.request
# 初始化请求和响应的文件句柄
self.rfile = self.connection.makefile('rb', -1)
self.wfile = self.connection.makefile('wb', 0)
def handle(self):
try:
# 设置请求体限制:65536
self.raw_requestline = self.rfile.readline(65537)
# 如果超过限制则返回 414 HTTP code
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(414)
return
# 解析 HTTP 请求,并把值通过 self 属性传递
if not self.parse_request():
return
# 具体处理请求的方法 do_方法,比如:do_get、do_post
mname = ('do_' + self.command).lower()
if not hasattr(self, mname):
self.send_error(501, "Unsupported method (%r)" % self.command)
return
method = getattr(self, mname)
# 对应到具体的处理 HTTP method 的方法
method()
# 返回响应
self.wfile.flush()
except socket.timeout as e:
self.log_error("Request timed out: %r", e)
return
def finish(self):
if not self.wfile.closed:
try:
self.wfile.flush()
except socket.error:
pass
# 关闭请求和响应的句柄
self.wfile.close()
self.rfile.close()
def parse_request(self):
"""
解析 HTTP 请求
"""
self.command = None # set in case of error on the first line
self.request_version = version = "HTTP/1.0"
# 开始解析 HTTP 请求,数据格式如下:
"""
{HTTP method} {PATH} {HTTP version}\r\n
{header field name}:{field value}\r\n
...
\r\n
{request body}
"""
# 解析请求头
requestline = str(self.raw_requestline, 'iso-8859-1')
requestline = requestline.rstrip('\r\n')
self.requestline = requestline
words = requestline.split()
if len(words) == 3:
# HTTP method, PATH, HTTP version
command, path, version = words
if version[:5] != 'HTTP/':
self.send_error(400, "Bad request version (%r)" % version)
return False
# 检查 HTTP version 正确性
try:
base_version_number = version.split('/', 1)[1]
version_number = base_version_number.split(".")
if len(version_number) != 2:
raise ValueError
version_number = int(version_number[0]), int(version_number[1])
if version_number >= (2, 0):
self.send_error(
505, "Invalid HTTP Version (%s)" % base_version_number)
return False
except (ValueError, IndexError):
self.send_error(400, "Bad request version (%r)" % version)
return False
elif len(words) == 2:
# 如果没有 HTTP version 则使用默认版本即:HTTP/1.0
command, path = words
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
# 解析 header,仅做解析
self.headers = self.parse_headers()
return True
def parse_headers(self):
headers = {}
while True:
line = self.rfile.readline()
if line in (b'\r\n', b'\n', b''):
break
line_str = str(line, 'utf-8')
key, value = line_str.split(': ')
headers[key] = value.strip()
return headers
def log_message(self, format, *args):
log_data_time_string = datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")
sys.stderr.write("%s - - [%s] %s\n" %
(self.client_address[0],
log_data_time_string,
format%args))
def log_request(self, code='-', size='-'):
self.log_message('"%s" %s %s',
self.requestline, str(code), str(size))
def log_error(self, format, *args):
self.log_message(format, *args)
def send_error(self, code, message=None):
"""
返回异常响应 code+message
"""
try:
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)
self.send_response(code, message)
content = None
# 状态码大于 200,并且不是 204、205、304 为异常
if code > 200 and code not in (204, 205, 304):
content = (DEFAULT_ERROR_MESSAGE % {
'code': code,
'message': html.escape(message),
'explain': explain
})
self.send_header("Content-Type", "text/html;charset=utf-8")
self.end_headers()
body = content.encode('UTF-8', 'replace')
if self.command != 'HEAD' and content:
# 返回响应
self.wfile.write(body)
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 version} {status code} {status phrase}\r\n
{header field name}:{field value}\r\n
...
\r\n
{response body}
"""
# 写响应头
self.wfile.write(("%s %d %s\r\n" %
(self.protocol_version, code, message)).encode(
'latin-1', 'strict'))
self.send_header('Server', "HG/Python " + sys.version.split()[0])
# 写响应 header
self.send_header('Date', self.date_time_string())
def send_header(self, keyword, value):
self.wfile.write(("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict'))
def end_headers(self):
# header 结束的标识符
self.wfile.write(b"\r\n")
@staticmethod
def date_time_string(timestamp=None):
weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
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" % (
weekdayname[wd],
day, monthname[month],
year, hh, mm, ss)
return s
# 请求具体的处理方式
class RequestHandler(HTTPRequestHandler):
def handle_index(self):
page = '''
<html>
<body>
<p>你好, HG Web Server!</p>
</body>
</html>
'''
self.send_response(200) # status code
# 试试删除这行
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(page)))
self.end_headers()
self.wfile.write(page.encode('utf-8'))
def handle_favicon(self):
page = '''
<html>
<body>
<p>这里还未开发</p>
</body>
</html>
'''
self.send_response(200) # status code
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(page)))
self.end_headers()
self.wfile.write(page.encode('utf-8'))
# 处理 GET 请求
def do_get(self):
# 根据 path 对应到具体的处理方法
if self.path == '/':
self.handle_index()
elif self.path.startswith('/favicon'):
self.handle_favicon()
else:
self.send_error(404)
if __name__ == '__main__':
server = HTTPServer(('', 8080), RequestHandler)
# 启动服务
server.serve_forever()