Coverage for /pythoncovmergedfiles/medio/medio/usr/lib/python3.9/http/server.py: 14%

590 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:05 +0000

1"""HTTP server classes. 

2 

3Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see 

4SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST, 

5and CGIHTTPRequestHandler for CGI scripts. 

6 

7It does, however, optionally implement HTTP/1.1 persistent connections, 

8as of version 0.3. 

9 

10Notes on CGIHTTPRequestHandler 

11------------------------------ 

12 

13This class implements GET and POST requests to cgi-bin scripts. 

14 

15If the os.fork() function is not present (e.g. on Windows), 

16subprocess.Popen() is used as a fallback, with slightly altered semantics. 

17 

18In all cases, the implementation is intentionally naive -- all 

19requests are executed synchronously. 

20 

21SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL 

22-- it may execute arbitrary Python code or external programs. 

23 

24Note that status code 200 is sent prior to execution of a CGI script, so 

25scripts cannot send other status codes such as 302 (redirect). 

26 

27XXX To do: 

28 

29- log requests even later (to capture byte count) 

30- log user-agent header and other interesting goodies 

31- send error log to separate file 

32""" 

33 

34 

35# See also: 

36# 

37# HTTP Working Group T. Berners-Lee 

38# INTERNET-DRAFT R. T. Fielding 

39# <draft-ietf-http-v10-spec-00.txt> H. Frystyk Nielsen 

40# Expires September 8, 1995 March 8, 1995 

41# 

42# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt 

43# 

44# and 

45# 

46# Network Working Group R. Fielding 

47# Request for Comments: 2616 et al 

48# Obsoletes: 2068 June 1999 

49# Category: Standards Track 

50# 

51# URL: http://www.faqs.org/rfcs/rfc2616.html 

52 

53# Log files 

54# --------- 

55# 

56# Here's a quote from the NCSA httpd docs about log file format. 

57# 

58# | The logfile format is as follows. Each line consists of: 

59# | 

60# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb 

61# | 

62# | host: Either the DNS name or the IP number of the remote client 

63# | rfc931: Any information returned by identd for this person, 

64# | - otherwise. 

65# | authuser: If user sent a userid for authentication, the user name, 

66# | - otherwise. 

67# | DD: Day 

68# | Mon: Month (calendar name) 

69# | YYYY: Year 

70# | hh: hour (24-hour format, the machine's timezone) 

71# | mm: minutes 

72# | ss: seconds 

73# | request: The first line of the HTTP request as sent by the client. 

74# | ddd: the status code returned by the server, - if not available. 

75# | bbbb: the total number of bytes sent, 

76# | *not including the HTTP/1.0 header*, - if not available 

77# | 

78# | You can determine the name of the file accessed through request. 

79# 

80# (Actually, the latter is only true if you know the server configuration 

81# at the time the request was made!) 

82 

83__version__ = "0.6" 

84 

85__all__ = [ 

86 "HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler", 

87 "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler", 

88] 

89 

90import copy 

91import datetime 

92import email.utils 

93import html 

94import http.client 

95import io 

96import mimetypes 

97import os 

98import posixpath 

99import select 

100import shutil 

101import socket # For gethostbyaddr() 

102import socketserver 

103import sys 

104import time 

105import urllib.parse 

106import contextlib 

107from functools import partial 

108 

109from http import HTTPStatus 

110 

111 

112# Default error message template 

113DEFAULT_ERROR_MESSAGE = """\ 

114<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 

115 "http://www.w3.org/TR/html4/strict.dtd"> 

116<html> 

117 <head> 

118 <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> 

119 <title>Error response</title> 

120 </head> 

121 <body> 

122 <h1>Error response</h1> 

123 <p>Error code: %(code)d</p> 

124 <p>Message: %(message)s.</p> 

125 <p>Error code explanation: %(code)s - %(explain)s.</p> 

126 </body> 

127</html> 

128""" 

129 

130DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8" 

131 

132class HTTPServer(socketserver.TCPServer): 

133 

134 allow_reuse_address = 1 # Seems to make sense in testing environment 

135 

136 def server_bind(self): 

137 """Override server_bind to store the server name.""" 

138 socketserver.TCPServer.server_bind(self) 

139 host, port = self.server_address[:2] 

140 self.server_name = socket.getfqdn(host) 

141 self.server_port = port 

142 

143 

144class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): 

145 daemon_threads = True 

146 

147 

148class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): 

149 

150 """HTTP request handler base class. 

151 

152 The following explanation of HTTP serves to guide you through the 

153 code as well as to expose any misunderstandings I may have about 

154 HTTP (so you don't need to read the code to figure out I'm wrong 

155 :-). 

156 

157 HTTP (HyperText Transfer Protocol) is an extensible protocol on 

158 top of a reliable stream transport (e.g. TCP/IP). The protocol 

159 recognizes three parts to a request: 

160 

161 1. One line identifying the request type and path 

162 2. An optional set of RFC-822-style headers 

163 3. An optional data part 

164 

165 The headers and data are separated by a blank line. 

166 

167 The first line of the request has the form 

168 

169 <command> <path> <version> 

170 

171 where <command> is a (case-sensitive) keyword such as GET or POST, 

172 <path> is a string containing path information for the request, 

173 and <version> should be the string "HTTP/1.0" or "HTTP/1.1". 

174 <path> is encoded using the URL encoding scheme (using %xx to signify 

175 the ASCII character with hex code xx). 

176 

177 The specification specifies that lines are separated by CRLF but 

178 for compatibility with the widest range of clients recommends 

179 servers also handle LF. Similarly, whitespace in the request line 

180 is treated sensibly (allowing multiple spaces between components 

181 and allowing trailing whitespace). 

182 

183 Similarly, for output, lines ought to be separated by CRLF pairs 

184 but most clients grok LF characters just fine. 

185 

186 If the first line of the request has the form 

187 

188 <command> <path> 

189 

190 (i.e. <version> is left out) then this is assumed to be an HTTP 

191 0.9 request; this form has no optional headers and data part and 

192 the reply consists of just the data. 

193 

194 The reply form of the HTTP 1.x protocol again has three parts: 

195 

196 1. One line giving the response code 

197 2. An optional set of RFC-822-style headers 

198 3. The data 

199 

200 Again, the headers and data are separated by a blank line. 

201 

202 The response code line has the form 

203 

204 <version> <responsecode> <responsestring> 

205 

206 where <version> is the protocol version ("HTTP/1.0" or "HTTP/1.1"), 

207 <responsecode> is a 3-digit response code indicating success or 

208 failure of the request, and <responsestring> is an optional 

209 human-readable string explaining what the response code means. 

210 

211 This server parses the request and the headers, and then calls a 

212 function specific to the request type (<command>). Specifically, 

213 a request SPAM will be handled by a method do_SPAM(). If no 

214 such method exists the server sends an error response to the 

215 client. If it exists, it is called with no arguments: 

216 

217 do_SPAM() 

218 

219 Note that the request name is case sensitive (i.e. SPAM and spam 

220 are different requests). 

221 

222 The various request details are stored in instance variables: 

223 

224 - client_address is the client IP address in the form (host, 

225 port); 

226 

227 - command, path and version are the broken-down request line; 

228 

229 - headers is an instance of email.message.Message (or a derived 

230 class) containing the header information; 

231 

232 - rfile is a file object open for reading positioned at the 

233 start of the optional input data part; 

234 

235 - wfile is a file object open for writing. 

236 

237 IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING! 

238 

239 The first thing to be written must be the response line. Then 

240 follow 0 or more header lines, then a blank line, and then the 

241 actual data (if any). The meaning of the header lines depends on 

242 the command executed by the server; in most cases, when data is 

243 returned, there should be at least one header line of the form 

244 

245 Content-type: <type>/<subtype> 

246 

247 where <type> and <subtype> should be registered MIME types, 

248 e.g. "text/html" or "text/plain". 

249 

250 """ 

251 

252 # The Python system version, truncated to its first component. 

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

254 

255 # The server software version. You may want to override this. 

256 # The format is multiple whitespace-separated strings, 

257 # where each string is of the form name[/version]. 

258 server_version = "BaseHTTP/" + __version__ 

259 

260 error_message_format = DEFAULT_ERROR_MESSAGE 

261 error_content_type = DEFAULT_ERROR_CONTENT_TYPE 

262 

263 # The default request version. This only affects responses up until 

264 # the point where the request line is parsed, so it mainly decides what 

265 # the client gets back when sending a malformed request line. 

266 # Most web servers default to HTTP 0.9, i.e. don't send a status line. 

267 default_request_version = "HTTP/0.9" 

268 

269 def parse_request(self): 

270 """Parse a request (internal). 

271 

272 The request should be stored in self.raw_requestline; the results 

273 are in self.command, self.path, self.request_version and 

274 self.headers. 

275 

276 Return True for success, False for failure; on failure, any relevant 

277 error response has already been sent back. 

278 

279 """ 

280 self.command = None # set in case of error on the first line 

281 self.request_version = version = self.default_request_version 

282 self.close_connection = True 

283 requestline = str(self.raw_requestline, 'iso-8859-1') 

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

285 self.requestline = requestline 

286 words = requestline.split() 

287 if len(words) == 0: 

288 return False 

289 

290 if len(words) >= 3: # Enough to determine protocol version 

291 version = words[-1] 

292 try: 

293 if not version.startswith('HTTP/'): 

294 raise ValueError 

295 base_version_number = version.split('/', 1)[1] 

296 version_number = base_version_number.split(".") 

297 # RFC 2145 section 3.1 says there can be only one "." and 

298 # - major and minor numbers MUST be treated as 

299 # separate integers; 

300 # - HTTP/2.4 is a lower version than HTTP/2.13, which in 

301 # turn is lower than HTTP/12.3; 

302 # - Leading zeros MUST be ignored by recipients. 

303 if len(version_number) != 2: 

304 raise ValueError 

305 version_number = int(version_number[0]), int(version_number[1]) 

306 except (ValueError, IndexError): 

307 self.send_error( 

308 HTTPStatus.BAD_REQUEST, 

309 "Bad request version (%r)" % version) 

310 return False 

311 if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": 

312 self.close_connection = False 

313 if version_number >= (2, 0): 

314 self.send_error( 

315 HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, 

316 "Invalid HTTP version (%s)" % base_version_number) 

317 return False 

318 self.request_version = version 

319 

320 if not 2 <= len(words) <= 3: 

321 self.send_error( 

322 HTTPStatus.BAD_REQUEST, 

323 "Bad request syntax (%r)" % requestline) 

324 return False 

325 command, path = words[:2] 

326 if len(words) == 2: 

327 self.close_connection = True 

328 if command != 'GET': 

329 self.send_error( 

330 HTTPStatus.BAD_REQUEST, 

331 "Bad HTTP/0.9 request type (%r)" % command) 

332 return False 

333 self.command, self.path = command, path 

334 

335 # Examine the headers and look for a Connection directive. 

336 try: 

337 self.headers = http.client.parse_headers(self.rfile, 

338 _class=self.MessageClass) 

339 except http.client.LineTooLong as err: 

340 self.send_error( 

341 HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, 

342 "Line too long", 

343 str(err)) 

344 return False 

345 except http.client.HTTPException as err: 

346 self.send_error( 

347 HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, 

348 "Too many headers", 

349 str(err) 

350 ) 

351 return False 

352 

353 conntype = self.headers.get('Connection', "") 

354 if conntype.lower() == 'close': 

355 self.close_connection = True 

356 elif (conntype.lower() == 'keep-alive' and 

357 self.protocol_version >= "HTTP/1.1"): 

358 self.close_connection = False 

359 # Examine the headers and look for an Expect directive 

360 expect = self.headers.get('Expect', "") 

361 if (expect.lower() == "100-continue" and 

362 self.protocol_version >= "HTTP/1.1" and 

363 self.request_version >= "HTTP/1.1"): 

364 if not self.handle_expect_100(): 

365 return False 

366 return True 

367 

368 def handle_expect_100(self): 

369 """Decide what to do with an "Expect: 100-continue" header. 

370 

371 If the client is expecting a 100 Continue response, we must 

372 respond with either a 100 Continue or a final response before 

373 waiting for the request body. The default is to always respond 

374 with a 100 Continue. You can behave differently (for example, 

375 reject unauthorized requests) by overriding this method. 

376 

377 This method should either return True (possibly after sending 

378 a 100 Continue response) or send an error response and return 

379 False. 

380 

381 """ 

382 self.send_response_only(HTTPStatus.CONTINUE) 

383 self.end_headers() 

384 return True 

385 

386 def handle_one_request(self): 

387 """Handle a single HTTP request. 

388 

389 You normally don't need to override this method; see the class 

390 __doc__ string for information on how to handle specific HTTP 

391 commands such as GET and POST. 

392 

393 """ 

394 try: 

395 self.raw_requestline = self.rfile.readline(65537) 

396 if len(self.raw_requestline) > 65536: 

397 self.requestline = '' 

398 self.request_version = '' 

399 self.command = '' 

400 self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG) 

401 return 

402 if not self.raw_requestline: 

403 self.close_connection = True 

404 return 

405 if not self.parse_request(): 

406 # An error code has been sent, just exit 

407 return 

408 mname = 'do_' + self.command 

409 if not hasattr(self, mname): 

410 self.send_error( 

411 HTTPStatus.NOT_IMPLEMENTED, 

412 "Unsupported method (%r)" % self.command) 

413 return 

414 method = getattr(self, mname) 

415 method() 

416 self.wfile.flush() #actually send the response if not already done. 

417 except socket.timeout as e: 

418 #a read or a write timed out. Discard this connection 

419 self.log_error("Request timed out: %r", e) 

420 self.close_connection = True 

421 return 

422 

423 def handle(self): 

424 """Handle multiple requests if necessary.""" 

425 self.close_connection = True 

426 

427 self.handle_one_request() 

428 while not self.close_connection: 

429 self.handle_one_request() 

430 

431 def send_error(self, code, message=None, explain=None): 

432 """Send and log an error reply. 

433 

434 Arguments are 

435 * code: an HTTP error code 

436 3 digits 

437 * message: a simple optional 1 line reason phrase. 

438 *( HTAB / SP / VCHAR / %x80-FF ) 

439 defaults to short entry matching the response code 

440 * explain: a detailed message defaults to the long entry 

441 matching the response code. 

442 

443 This sends an error response (so it must be called before any 

444 output has been generated), logs the error, and finally sends 

445 a piece of HTML explaining the error to the user. 

446 

447 """ 

448 

449 try: 

450 shortmsg, longmsg = self.responses[code] 

451 except KeyError: 

452 shortmsg, longmsg = '???', '???' 

453 if message is None: 

454 message = shortmsg 

455 if explain is None: 

456 explain = longmsg 

457 self.log_error("code %d, message %s", code, message) 

458 self.send_response(code, message) 

459 self.send_header('Connection', 'close') 

460 

461 # Message body is omitted for cases described in: 

462 # - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified) 

463 # - RFC7231: 6.3.6. 205(Reset Content) 

464 body = None 

465 if (code >= 200 and 

466 code not in (HTTPStatus.NO_CONTENT, 

467 HTTPStatus.RESET_CONTENT, 

468 HTTPStatus.NOT_MODIFIED)): 

469 # HTML encode to prevent Cross Site Scripting attacks 

470 # (see bug #1100201) 

471 content = (self.error_message_format % { 

472 'code': code, 

473 'message': html.escape(message, quote=False), 

474 'explain': html.escape(explain, quote=False) 

475 }) 

476 body = content.encode('UTF-8', 'replace') 

477 self.send_header("Content-Type", self.error_content_type) 

478 self.send_header('Content-Length', str(len(body))) 

479 self.end_headers() 

480 

481 if self.command != 'HEAD' and body: 

482 self.wfile.write(body) 

483 

484 def send_response(self, code, message=None): 

485 """Add the response header to the headers buffer and log the 

486 response code. 

487 

488 Also send two standard headers with the server software 

489 version and the current date. 

490 

491 """ 

492 self.log_request(code) 

493 self.send_response_only(code, message) 

494 self.send_header('Server', self.version_string()) 

495 self.send_header('Date', self.date_time_string()) 

496 

497 def send_response_only(self, code, message=None): 

498 """Send the response header only.""" 

499 if self.request_version != 'HTTP/0.9': 

500 if message is None: 

501 if code in self.responses: 

502 message = self.responses[code][0] 

503 else: 

504 message = '' 

505 if not hasattr(self, '_headers_buffer'): 

506 self._headers_buffer = [] 

507 self._headers_buffer.append(("%s %d %s\r\n" % 

508 (self.protocol_version, code, message)).encode( 

509 'latin-1', 'strict')) 

510 

511 def send_header(self, keyword, value): 

512 """Send a MIME header to the headers buffer.""" 

513 if self.request_version != 'HTTP/0.9': 

514 if not hasattr(self, '_headers_buffer'): 

515 self._headers_buffer = [] 

516 self._headers_buffer.append( 

517 ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) 

518 

519 if keyword.lower() == 'connection': 

520 if value.lower() == 'close': 

521 self.close_connection = True 

522 elif value.lower() == 'keep-alive': 

523 self.close_connection = False 

524 

525 def end_headers(self): 

526 """Send the blank line ending the MIME headers.""" 

527 if self.request_version != 'HTTP/0.9': 

528 self._headers_buffer.append(b"\r\n") 

529 self.flush_headers() 

530 

531 def flush_headers(self): 

532 if hasattr(self, '_headers_buffer'): 

533 self.wfile.write(b"".join(self._headers_buffer)) 

534 self._headers_buffer = [] 

535 

536 def log_request(self, code='-', size='-'): 

537 """Log an accepted request. 

538 

539 This is called by send_response(). 

540 

541 """ 

542 if isinstance(code, HTTPStatus): 

543 code = code.value 

544 self.log_message('"%s" %s %s', 

545 self.requestline, str(code), str(size)) 

546 

547 def log_error(self, format, *args): 

548 """Log an error. 

549 

550 This is called when a request cannot be fulfilled. By 

551 default it passes the message on to log_message(). 

552 

553 Arguments are the same as for log_message(). 

554 

555 XXX This should go to the separate error log. 

556 

557 """ 

558 

559 self.log_message(format, *args) 

560 

561 def log_message(self, format, *args): 

562 """Log an arbitrary message. 

563 

564 This is used by all other logging functions. Override 

565 it if you have specific logging wishes. 

566 

567 The first argument, FORMAT, is a format string for the 

568 message to be logged. If the format string contains 

569 any % escapes requiring parameters, they should be 

570 specified as subsequent arguments (it's just like 

571 printf!). 

572 

573 The client ip and current date/time are prefixed to 

574 every message. 

575 

576 """ 

577 

578 sys.stderr.write("%s - - [%s] %s\n" % 

579 (self.address_string(), 

580 self.log_date_time_string(), 

581 format%args)) 

582 

583 def version_string(self): 

584 """Return the server software version string.""" 

585 return self.server_version + ' ' + self.sys_version 

586 

587 def date_time_string(self, timestamp=None): 

588 """Return the current date and time formatted for a message header.""" 

589 if timestamp is None: 

590 timestamp = time.time() 

591 return email.utils.formatdate(timestamp, usegmt=True) 

592 

593 def log_date_time_string(self): 

594 """Return the current time formatted for logging.""" 

595 now = time.time() 

596 year, month, day, hh, mm, ss, x, y, z = time.localtime(now) 

597 s = "%02d/%3s/%04d %02d:%02d:%02d" % ( 

598 day, self.monthname[month], year, hh, mm, ss) 

599 return s 

600 

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

602 

603 monthname = [None, 

604 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 

605 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 

606 

607 def address_string(self): 

608 """Return the client address.""" 

609 

610 return self.client_address[0] 

611 

612 # Essentially static class variables 

613 

614 # The version of the HTTP protocol we support. 

615 # Set this to HTTP/1.1 to enable automatic keepalive 

616 protocol_version = "HTTP/1.0" 

617 

618 # MessageClass used to parse headers 

619 MessageClass = http.client.HTTPMessage 

620 

621 # hack to maintain backwards compatibility 

622 responses = { 

623 v: (v.phrase, v.description) 

624 for v in HTTPStatus.__members__.values() 

625 } 

626 

627 

628class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): 

629 

630 """Simple HTTP request handler with GET and HEAD commands. 

631 

632 This serves files from the current directory and any of its 

633 subdirectories. The MIME type for files is determined by 

634 calling the .guess_type() method. 

635 

636 The GET and HEAD requests are identical except that the HEAD 

637 request omits the actual contents of the file. 

638 

639 """ 

640 

641 server_version = "SimpleHTTP/" + __version__ 

642 extensions_map = _encodings_map_default = { 

643 '.gz': 'application/gzip', 

644 '.Z': 'application/octet-stream', 

645 '.bz2': 'application/x-bzip2', 

646 '.xz': 'application/x-xz', 

647 } 

648 

649 def __init__(self, *args, directory=None, **kwargs): 

650 if directory is None: 

651 directory = os.getcwd() 

652 self.directory = os.fspath(directory) 

653 super().__init__(*args, **kwargs) 

654 

655 def do_GET(self): 

656 """Serve a GET request.""" 

657 f = self.send_head() 

658 if f: 

659 try: 

660 self.copyfile(f, self.wfile) 

661 finally: 

662 f.close() 

663 

664 def do_HEAD(self): 

665 """Serve a HEAD request.""" 

666 f = self.send_head() 

667 if f: 

668 f.close() 

669 

670 def send_head(self): 

671 """Common code for GET and HEAD commands. 

672 

673 This sends the response code and MIME headers. 

674 

675 Return value is either a file object (which has to be copied 

676 to the outputfile by the caller unless the command was HEAD, 

677 and must be closed by the caller under all circumstances), or 

678 None, in which case the caller has nothing further to do. 

679 

680 """ 

681 path = self.translate_path(self.path) 

682 f = None 

683 if os.path.isdir(path): 

684 parts = urllib.parse.urlsplit(self.path) 

685 if not parts.path.endswith('/'): 

686 # redirect browser - doing basically what apache does 

687 self.send_response(HTTPStatus.MOVED_PERMANENTLY) 

688 new_parts = (parts[0], parts[1], parts[2] + '/', 

689 parts[3], parts[4]) 

690 new_url = urllib.parse.urlunsplit(new_parts) 

691 self.send_header("Location", new_url) 

692 self.end_headers() 

693 return None 

694 for index in "index.html", "index.htm": 

695 index = os.path.join(path, index) 

696 if os.path.exists(index): 

697 path = index 

698 break 

699 else: 

700 return self.list_directory(path) 

701 ctype = self.guess_type(path) 

702 # check for trailing "/" which should return 404. See Issue17324 

703 # The test for this was added in test_httpserver.py 

704 # However, some OS platforms accept a trailingSlash as a filename 

705 # See discussion on python-dev and Issue34711 regarding 

706 # parseing and rejection of filenames with a trailing slash 

707 if path.endswith("/"): 

708 self.send_error(HTTPStatus.NOT_FOUND, "File not found") 

709 return None 

710 try: 

711 f = open(path, 'rb') 

712 except OSError: 

713 self.send_error(HTTPStatus.NOT_FOUND, "File not found") 

714 return None 

715 

716 try: 

717 fs = os.fstat(f.fileno()) 

718 # Use browser cache if possible 

719 if ("If-Modified-Since" in self.headers 

720 and "If-None-Match" not in self.headers): 

721 # compare If-Modified-Since and time of last file modification 

722 try: 

723 ims = email.utils.parsedate_to_datetime( 

724 self.headers["If-Modified-Since"]) 

725 except (TypeError, IndexError, OverflowError, ValueError): 

726 # ignore ill-formed values 

727 pass 

728 else: 

729 if ims.tzinfo is None: 

730 # obsolete format with no timezone, cf. 

731 # https://tools.ietf.org/html/rfc7231#section-7.1.1.1 

732 ims = ims.replace(tzinfo=datetime.timezone.utc) 

733 if ims.tzinfo is datetime.timezone.utc: 

734 # compare to UTC datetime of last modification 

735 last_modif = datetime.datetime.fromtimestamp( 

736 fs.st_mtime, datetime.timezone.utc) 

737 # remove microseconds, like in If-Modified-Since 

738 last_modif = last_modif.replace(microsecond=0) 

739 

740 if last_modif <= ims: 

741 self.send_response(HTTPStatus.NOT_MODIFIED) 

742 self.end_headers() 

743 f.close() 

744 return None 

745 

746 self.send_response(HTTPStatus.OK) 

747 self.send_header("Content-type", ctype) 

748 self.send_header("Content-Length", str(fs[6])) 

749 self.send_header("Last-Modified", 

750 self.date_time_string(fs.st_mtime)) 

751 self.end_headers() 

752 return f 

753 except: 

754 f.close() 

755 raise 

756 

757 def list_directory(self, path): 

758 """Helper to produce a directory listing (absent index.html). 

759 

760 Return value is either a file object, or None (indicating an 

761 error). In either case, the headers are sent, making the 

762 interface the same as for send_head(). 

763 

764 """ 

765 try: 

766 list = os.listdir(path) 

767 except OSError: 

768 self.send_error( 

769 HTTPStatus.NOT_FOUND, 

770 "No permission to list directory") 

771 return None 

772 list.sort(key=lambda a: a.lower()) 

773 r = [] 

774 try: 

775 displaypath = urllib.parse.unquote(self.path, 

776 errors='surrogatepass') 

777 except UnicodeDecodeError: 

778 displaypath = urllib.parse.unquote(path) 

779 displaypath = html.escape(displaypath, quote=False) 

780 enc = sys.getfilesystemencoding() 

781 title = 'Directory listing for %s' % displaypath 

782 r.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" ' 

783 '"http://www.w3.org/TR/html4/strict.dtd">') 

784 r.append('<html>\n<head>') 

785 r.append('<meta http-equiv="Content-Type" ' 

786 'content="text/html; charset=%s">' % enc) 

787 r.append('<title>%s</title>\n</head>' % title) 

788 r.append('<body>\n<h1>%s</h1>' % title) 

789 r.append('<hr>\n<ul>') 

790 for name in list: 

791 fullname = os.path.join(path, name) 

792 displayname = linkname = name 

793 # Append / for directories or @ for symbolic links 

794 if os.path.isdir(fullname): 

795 displayname = name + "/" 

796 linkname = name + "/" 

797 if os.path.islink(fullname): 

798 displayname = name + "@" 

799 # Note: a link to a directory displays with @ and links with / 

800 r.append('<li><a href="%s">%s</a></li>' 

801 % (urllib.parse.quote(linkname, 

802 errors='surrogatepass'), 

803 html.escape(displayname, quote=False))) 

804 r.append('</ul>\n<hr>\n</body>\n</html>\n') 

805 encoded = '\n'.join(r).encode(enc, 'surrogateescape') 

806 f = io.BytesIO() 

807 f.write(encoded) 

808 f.seek(0) 

809 self.send_response(HTTPStatus.OK) 

810 self.send_header("Content-type", "text/html; charset=%s" % enc) 

811 self.send_header("Content-Length", str(len(encoded))) 

812 self.end_headers() 

813 return f 

814 

815 def translate_path(self, path): 

816 """Translate a /-separated PATH to the local filename syntax. 

817 

818 Components that mean special things to the local file system 

819 (e.g. drive or directory names) are ignored. (XXX They should 

820 probably be diagnosed.) 

821 

822 """ 

823 # abandon query parameters 

824 path = path.split('?',1)[0] 

825 path = path.split('#',1)[0] 

826 # Don't forget explicit trailing slash when normalizing. Issue17324 

827 trailing_slash = path.rstrip().endswith('/') 

828 try: 

829 path = urllib.parse.unquote(path, errors='surrogatepass') 

830 except UnicodeDecodeError: 

831 path = urllib.parse.unquote(path) 

832 path = posixpath.normpath(path) 

833 words = path.split('/') 

834 words = filter(None, words) 

835 path = self.directory 

836 for word in words: 

837 if os.path.dirname(word) or word in (os.curdir, os.pardir): 

838 # Ignore components that are not a simple file/directory name 

839 continue 

840 path = os.path.join(path, word) 

841 if trailing_slash: 

842 path += '/' 

843 return path 

844 

845 def copyfile(self, source, outputfile): 

846 """Copy all data between two file objects. 

847 

848 The SOURCE argument is a file object open for reading 

849 (or anything with a read() method) and the DESTINATION 

850 argument is a file object open for writing (or 

851 anything with a write() method). 

852 

853 The only reason for overriding this would be to change 

854 the block size or perhaps to replace newlines by CRLF 

855 -- note however that this the default server uses this 

856 to copy binary data as well. 

857 

858 """ 

859 shutil.copyfileobj(source, outputfile) 

860 

861 def guess_type(self, path): 

862 """Guess the type of a file. 

863 

864 Argument is a PATH (a filename). 

865 

866 Return value is a string of the form type/subtype, 

867 usable for a MIME Content-type header. 

868 

869 The default implementation looks the file's extension 

870 up in the table self.extensions_map, using application/octet-stream 

871 as a default; however it would be permissible (if 

872 slow) to look inside the data to make a better guess. 

873 

874 """ 

875 base, ext = posixpath.splitext(path) 

876 if ext in self.extensions_map: 

877 return self.extensions_map[ext] 

878 ext = ext.lower() 

879 if ext in self.extensions_map: 

880 return self.extensions_map[ext] 

881 guess, _ = mimetypes.guess_type(path) 

882 if guess: 

883 return guess 

884 return 'application/octet-stream' 

885 

886 

887# Utilities for CGIHTTPRequestHandler 

888 

889def _url_collapse_path(path): 

890 """ 

891 Given a URL path, remove extra '/'s and '.' path elements and collapse 

892 any '..' references and returns a collapsed path. 

893 

894 Implements something akin to RFC-2396 5.2 step 6 to parse relative paths. 

895 The utility of this function is limited to is_cgi method and helps 

896 preventing some security attacks. 

897 

898 Returns: The reconstituted URL, which will always start with a '/'. 

899 

900 Raises: IndexError if too many '..' occur within the path. 

901 

902 """ 

903 # Query component should not be involved. 

904 path, _, query = path.partition('?') 

905 path = urllib.parse.unquote(path) 

906 

907 # Similar to os.path.split(os.path.normpath(path)) but specific to URL 

908 # path semantics rather than local operating system semantics. 

909 path_parts = path.split('/') 

910 head_parts = [] 

911 for part in path_parts[:-1]: 

912 if part == '..': 

913 head_parts.pop() # IndexError if more '..' than prior parts 

914 elif part and part != '.': 

915 head_parts.append( part ) 

916 if path_parts: 

917 tail_part = path_parts.pop() 

918 if tail_part: 

919 if tail_part == '..': 

920 head_parts.pop() 

921 tail_part = '' 

922 elif tail_part == '.': 

923 tail_part = '' 

924 else: 

925 tail_part = '' 

926 

927 if query: 

928 tail_part = '?'.join((tail_part, query)) 

929 

930 splitpath = ('/' + '/'.join(head_parts), tail_part) 

931 collapsed_path = "/".join(splitpath) 

932 

933 return collapsed_path 

934 

935 

936 

937nobody = None 

938 

939def nobody_uid(): 

940 """Internal routine to get nobody's uid""" 

941 global nobody 

942 if nobody: 

943 return nobody 

944 try: 

945 import pwd 

946 except ImportError: 

947 return -1 

948 try: 

949 nobody = pwd.getpwnam('nobody')[2] 

950 except KeyError: 

951 nobody = 1 + max(x[2] for x in pwd.getpwall()) 

952 return nobody 

953 

954 

955def executable(path): 

956 """Test for executable file.""" 

957 return os.access(path, os.X_OK) 

958 

959 

960class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): 

961 

962 """Complete HTTP server with GET, HEAD and POST commands. 

963 

964 GET and HEAD also support running CGI scripts. 

965 

966 The POST command is *only* implemented for CGI scripts. 

967 

968 """ 

969 

970 # Determine platform specifics 

971 have_fork = hasattr(os, 'fork') 

972 

973 # Make rfile unbuffered -- we need to read one line and then pass 

974 # the rest to a subprocess, so we can't use buffered input. 

975 rbufsize = 0 

976 

977 def do_POST(self): 

978 """Serve a POST request. 

979 

980 This is only implemented for CGI scripts. 

981 

982 """ 

983 

984 if self.is_cgi(): 

985 self.run_cgi() 

986 else: 

987 self.send_error( 

988 HTTPStatus.NOT_IMPLEMENTED, 

989 "Can only POST to CGI scripts") 

990 

991 def send_head(self): 

992 """Version of send_head that support CGI scripts""" 

993 if self.is_cgi(): 

994 return self.run_cgi() 

995 else: 

996 return SimpleHTTPRequestHandler.send_head(self) 

997 

998 def is_cgi(self): 

999 """Test whether self.path corresponds to a CGI script. 

1000 

1001 Returns True and updates the cgi_info attribute to the tuple 

1002 (dir, rest) if self.path requires running a CGI script. 

1003 Returns False otherwise. 

1004 

1005 If any exception is raised, the caller should assume that 

1006 self.path was rejected as invalid and act accordingly. 

1007 

1008 The default implementation tests whether the normalized url 

1009 path begins with one of the strings in self.cgi_directories 

1010 (and the next character is a '/' or the end of the string). 

1011 

1012 """ 

1013 collapsed_path = _url_collapse_path(self.path) 

1014 dir_sep = collapsed_path.find('/', 1) 

1015 while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories: 

1016 dir_sep = collapsed_path.find('/', dir_sep+1) 

1017 if dir_sep > 0: 

1018 head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:] 

1019 self.cgi_info = head, tail 

1020 return True 

1021 return False 

1022 

1023 

1024 cgi_directories = ['/cgi-bin', '/htbin'] 

1025 

1026 def is_executable(self, path): 

1027 """Test whether argument path is an executable file.""" 

1028 return executable(path) 

1029 

1030 def is_python(self, path): 

1031 """Test whether argument path is a Python script.""" 

1032 head, tail = os.path.splitext(path) 

1033 return tail.lower() in (".py", ".pyw") 

1034 

1035 def run_cgi(self): 

1036 """Execute a CGI script.""" 

1037 dir, rest = self.cgi_info 

1038 path = dir + '/' + rest 

1039 i = path.find('/', len(dir)+1) 

1040 while i >= 0: 

1041 nextdir = path[:i] 

1042 nextrest = path[i+1:] 

1043 

1044 scriptdir = self.translate_path(nextdir) 

1045 if os.path.isdir(scriptdir): 

1046 dir, rest = nextdir, nextrest 

1047 i = path.find('/', len(dir)+1) 

1048 else: 

1049 break 

1050 

1051 # find an explicit query string, if present. 

1052 rest, _, query = rest.partition('?') 

1053 

1054 # dissect the part after the directory name into a script name & 

1055 # a possible additional path, to be stored in PATH_INFO. 

1056 i = rest.find('/') 

1057 if i >= 0: 

1058 script, rest = rest[:i], rest[i:] 

1059 else: 

1060 script, rest = rest, '' 

1061 

1062 scriptname = dir + '/' + script 

1063 scriptfile = self.translate_path(scriptname) 

1064 if not os.path.exists(scriptfile): 

1065 self.send_error( 

1066 HTTPStatus.NOT_FOUND, 

1067 "No such CGI script (%r)" % scriptname) 

1068 return 

1069 if not os.path.isfile(scriptfile): 

1070 self.send_error( 

1071 HTTPStatus.FORBIDDEN, 

1072 "CGI script is not a plain file (%r)" % scriptname) 

1073 return 

1074 ispy = self.is_python(scriptname) 

1075 if self.have_fork or not ispy: 

1076 if not self.is_executable(scriptfile): 

1077 self.send_error( 

1078 HTTPStatus.FORBIDDEN, 

1079 "CGI script is not executable (%r)" % scriptname) 

1080 return 

1081 

1082 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html 

1083 # XXX Much of the following could be prepared ahead of time! 

1084 env = copy.deepcopy(os.environ) 

1085 env['SERVER_SOFTWARE'] = self.version_string() 

1086 env['SERVER_NAME'] = self.server.server_name 

1087 env['GATEWAY_INTERFACE'] = 'CGI/1.1' 

1088 env['SERVER_PROTOCOL'] = self.protocol_version 

1089 env['SERVER_PORT'] = str(self.server.server_port) 

1090 env['REQUEST_METHOD'] = self.command 

1091 uqrest = urllib.parse.unquote(rest) 

1092 env['PATH_INFO'] = uqrest 

1093 env['PATH_TRANSLATED'] = self.translate_path(uqrest) 

1094 env['SCRIPT_NAME'] = scriptname 

1095 if query: 

1096 env['QUERY_STRING'] = query 

1097 env['REMOTE_ADDR'] = self.client_address[0] 

1098 authorization = self.headers.get("authorization") 

1099 if authorization: 

1100 authorization = authorization.split() 

1101 if len(authorization) == 2: 

1102 import base64, binascii 

1103 env['AUTH_TYPE'] = authorization[0] 

1104 if authorization[0].lower() == "basic": 

1105 try: 

1106 authorization = authorization[1].encode('ascii') 

1107 authorization = base64.decodebytes(authorization).\ 

1108 decode('ascii') 

1109 except (binascii.Error, UnicodeError): 

1110 pass 

1111 else: 

1112 authorization = authorization.split(':') 

1113 if len(authorization) == 2: 

1114 env['REMOTE_USER'] = authorization[0] 

1115 # XXX REMOTE_IDENT 

1116 if self.headers.get('content-type') is None: 

1117 env['CONTENT_TYPE'] = self.headers.get_content_type() 

1118 else: 

1119 env['CONTENT_TYPE'] = self.headers['content-type'] 

1120 length = self.headers.get('content-length') 

1121 if length: 

1122 env['CONTENT_LENGTH'] = length 

1123 referer = self.headers.get('referer') 

1124 if referer: 

1125 env['HTTP_REFERER'] = referer 

1126 accept = self.headers.get_all('accept', ()) 

1127 env['HTTP_ACCEPT'] = ','.join(accept) 

1128 ua = self.headers.get('user-agent') 

1129 if ua: 

1130 env['HTTP_USER_AGENT'] = ua 

1131 co = filter(None, self.headers.get_all('cookie', [])) 

1132 cookie_str = ', '.join(co) 

1133 if cookie_str: 

1134 env['HTTP_COOKIE'] = cookie_str 

1135 # XXX Other HTTP_* headers 

1136 # Since we're setting the env in the parent, provide empty 

1137 # values to override previously set values 

1138 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', 

1139 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'): 

1140 env.setdefault(k, "") 

1141 

1142 self.send_response(HTTPStatus.OK, "Script output follows") 

1143 self.flush_headers() 

1144 

1145 decoded_query = query.replace('+', ' ') 

1146 

1147 if self.have_fork: 

1148 # Unix -- fork as we should 

1149 args = [script] 

1150 if '=' not in decoded_query: 

1151 args.append(decoded_query) 

1152 nobody = nobody_uid() 

1153 self.wfile.flush() # Always flush before forking 

1154 pid = os.fork() 

1155 if pid != 0: 

1156 # Parent 

1157 pid, sts = os.waitpid(pid, 0) 

1158 # throw away additional data [see bug #427345] 

1159 while select.select([self.rfile], [], [], 0)[0]: 

1160 if not self.rfile.read(1): 

1161 break 

1162 exitcode = os.waitstatus_to_exitcode(sts) 

1163 if exitcode: 

1164 self.log_error(f"CGI script exit code {exitcode}") 

1165 return 

1166 # Child 

1167 try: 

1168 try: 

1169 os.setuid(nobody) 

1170 except OSError: 

1171 pass 

1172 os.dup2(self.rfile.fileno(), 0) 

1173 os.dup2(self.wfile.fileno(), 1) 

1174 os.execve(scriptfile, args, env) 

1175 except: 

1176 self.server.handle_error(self.request, self.client_address) 

1177 os._exit(127) 

1178 

1179 else: 

1180 # Non-Unix -- use subprocess 

1181 import subprocess 

1182 cmdline = [scriptfile] 

1183 if self.is_python(scriptfile): 

1184 interp = sys.executable 

1185 if interp.lower().endswith("w.exe"): 

1186 # On Windows, use python.exe, not pythonw.exe 

1187 interp = interp[:-5] + interp[-4:] 

1188 cmdline = [interp, '-u'] + cmdline 

1189 if '=' not in query: 

1190 cmdline.append(query) 

1191 self.log_message("command: %s", subprocess.list2cmdline(cmdline)) 

1192 try: 

1193 nbytes = int(length) 

1194 except (TypeError, ValueError): 

1195 nbytes = 0 

1196 p = subprocess.Popen(cmdline, 

1197 stdin=subprocess.PIPE, 

1198 stdout=subprocess.PIPE, 

1199 stderr=subprocess.PIPE, 

1200 env = env 

1201 ) 

1202 if self.command.lower() == "post" and nbytes > 0: 

1203 data = self.rfile.read(nbytes) 

1204 else: 

1205 data = None 

1206 # throw away additional data [see bug #427345] 

1207 while select.select([self.rfile._sock], [], [], 0)[0]: 

1208 if not self.rfile._sock.recv(1): 

1209 break 

1210 stdout, stderr = p.communicate(data) 

1211 self.wfile.write(stdout) 

1212 if stderr: 

1213 self.log_error('%s', stderr) 

1214 p.stderr.close() 

1215 p.stdout.close() 

1216 status = p.returncode 

1217 if status: 

1218 self.log_error("CGI script exit status %#x", status) 

1219 else: 

1220 self.log_message("CGI script exited OK") 

1221 

1222 

1223def _get_best_family(*address): 

1224 infos = socket.getaddrinfo( 

1225 *address, 

1226 type=socket.SOCK_STREAM, 

1227 flags=socket.AI_PASSIVE, 

1228 ) 

1229 family, type, proto, canonname, sockaddr = next(iter(infos)) 

1230 return family, sockaddr 

1231 

1232 

1233def test(HandlerClass=BaseHTTPRequestHandler, 

1234 ServerClass=ThreadingHTTPServer, 

1235 protocol="HTTP/1.0", port=8000, bind=None): 

1236 """Test the HTTP request handler class. 

1237 

1238 This runs an HTTP server on port 8000 (or the port argument). 

1239 

1240 """ 

1241 ServerClass.address_family, addr = _get_best_family(bind, port) 

1242 

1243 HandlerClass.protocol_version = protocol 

1244 with ServerClass(addr, HandlerClass) as httpd: 

1245 host, port = httpd.socket.getsockname()[:2] 

1246 url_host = f'[{host}]' if ':' in host else host 

1247 print( 

1248 f"Serving HTTP on {host} port {port} " 

1249 f"(http://{url_host}:{port}/) ..." 

1250 ) 

1251 try: 

1252 httpd.serve_forever() 

1253 except KeyboardInterrupt: 

1254 print("\nKeyboard interrupt received, exiting.") 

1255 sys.exit(0) 

1256 

1257if __name__ == '__main__': 

1258 import argparse 

1259 

1260 parser = argparse.ArgumentParser() 

1261 parser.add_argument('--cgi', action='store_true', 

1262 help='Run as CGI Server') 

1263 parser.add_argument('--bind', '-b', metavar='ADDRESS', 

1264 help='Specify alternate bind address ' 

1265 '[default: all interfaces]') 

1266 parser.add_argument('--directory', '-d', default=os.getcwd(), 

1267 help='Specify alternative directory ' 

1268 '[default:current directory]') 

1269 parser.add_argument('port', action='store', 

1270 default=8000, type=int, 

1271 nargs='?', 

1272 help='Specify alternate port [default: 8000]') 

1273 args = parser.parse_args() 

1274 if args.cgi: 

1275 handler_class = CGIHTTPRequestHandler 

1276 else: 

1277 handler_class = partial(SimpleHTTPRequestHandler, 

1278 directory=args.directory) 

1279 

1280 # ensure dual-stack is not disabled; ref #38907 

1281 class DualStackServer(ThreadingHTTPServer): 

1282 def server_bind(self): 

1283 # suppress exception when protocol is IPv4 

1284 with contextlib.suppress(Exception): 

1285 self.socket.setsockopt( 

1286 socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) 

1287 return super().server_bind() 

1288 

1289 test( 

1290 HandlerClass=handler_class, 

1291 ServerClass=DualStackServer, 

1292 port=args.port, 

1293 bind=args.bind, 

1294 )