Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/gunicorn/util.py: 30%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

396 statements  

1# 

2# This file is part of gunicorn released under the MIT license. 

3# See the NOTICE for more information. 

4import ast 

5import email.utils 

6import errno 

7import fcntl 

8import html 

9import importlib 

10import inspect 

11import io 

12import logging 

13import os 

14import pwd 

15import random 

16import re 

17import socket 

18import sys 

19import textwrap 

20import time 

21import traceback 

22import warnings 

23 

24try: 

25 import importlib.metadata as importlib_metadata 

26except (ModuleNotFoundError, ImportError): 

27 import importlib_metadata 

28 

29from gunicorn.errors import AppImportError 

30from gunicorn.workers import SUPPORTED_WORKERS 

31import urllib.parse 

32 

33REDIRECT_TO = getattr(os, 'devnull', '/dev/null') 

34 

35# Server and Date aren't technically hop-by-hop 

36# headers, but they are in the purview of the 

37# origin server which the WSGI spec says we should 

38# act like. So we drop them and add our own. 

39# 

40# In the future, concatenation server header values 

41# might be better, but nothing else does it and 

42# dropping them is easier. 

43hop_headers = set(""" 

44 connection keep-alive proxy-authenticate proxy-authorization 

45 te trailers transfer-encoding upgrade 

46 server date 

47 """.split()) 

48 

49# setproctitle causes segfaults on macOS due to fork() safety issues 

50# https://github.com/benoitc/gunicorn/issues/3021 

51if sys.platform == "darwin": 

52 def _setproctitle(title): 

53 pass 

54else: 

55 try: 

56 from setproctitle import setproctitle, getproctitle 

57 

58 # Force early initialization before any os.environ modifications 

59 # (e.g. removing LISTEN_FDS in systemd socket activation) 

60 # https://github.com/benoitc/gunicorn/issues/3430 

61 getproctitle() 

62 

63 def _setproctitle(title): 

64 setproctitle("gunicorn: %s" % title) 

65 except ImportError: 

66 def _setproctitle(title): 

67 pass 

68 

69 

70def load_entry_point(distribution, group, name): 

71 dist_obj = importlib_metadata.distribution(distribution) 

72 eps = [ep for ep in dist_obj.entry_points 

73 if ep.group == group and ep.name == name] 

74 if not eps: 

75 raise ImportError("Entry point %r not found" % ((group, name),)) 

76 return eps[0].load() 

77 

78 

79def load_class(uri, default="gunicorn.workers.sync.SyncWorker", 

80 section="gunicorn.workers"): 

81 if inspect.isclass(uri): 

82 return uri 

83 if uri.startswith("egg:"): 

84 # uses entry points 

85 entry_str = uri.split("egg:")[1] 

86 try: 

87 dist, name = entry_str.rsplit("#", 1) 

88 except ValueError: 

89 dist = entry_str 

90 name = default 

91 

92 try: 

93 return load_entry_point(dist, section, name) 

94 except Exception: 

95 exc = traceback.format_exc() 

96 msg = "class uri %r invalid or not found: \n\n[%s]" 

97 raise RuntimeError(msg % (uri, exc)) 

98 else: 

99 components = uri.split('.') 

100 if len(components) == 1: 

101 while True: 

102 if uri.startswith("#"): 

103 uri = uri[1:] 

104 

105 if uri in SUPPORTED_WORKERS: 

106 components = SUPPORTED_WORKERS[uri].split(".") 

107 break 

108 

109 try: 

110 return load_entry_point( 

111 "gunicorn", section, uri 

112 ) 

113 except Exception: 

114 exc = traceback.format_exc() 

115 msg = "class uri %r invalid or not found: \n\n[%s]" 

116 raise RuntimeError(msg % (uri, exc)) 

117 

118 klass = components.pop(-1) 

119 

120 try: 

121 mod = importlib.import_module('.'.join(components)) 

122 except Exception: 

123 exc = traceback.format_exc() 

124 msg = "class uri %r invalid or not found: \n\n[%s]" 

125 raise RuntimeError(msg % (uri, exc)) 

126 return getattr(mod, klass) 

127 

128 

129positionals = ( 

130 inspect.Parameter.POSITIONAL_ONLY, 

131 inspect.Parameter.POSITIONAL_OR_KEYWORD, 

132) 

133 

134 

135def get_arity(f): 

136 sig = inspect.signature(f) 

137 arity = 0 

138 

139 for param in sig.parameters.values(): 

140 if param.kind in positionals: 

141 arity += 1 

142 

143 return arity 

144 

145 

146def get_username(uid): 

147 """ get the username for a user id""" 

148 return pwd.getpwuid(uid).pw_name 

149 

150 

151def set_owner_process(uid, gid, initgroups=False): 

152 """ set user and group of workers processes """ 

153 

154 if gid: 

155 if uid: 

156 try: 

157 username = get_username(uid) 

158 except KeyError: 

159 initgroups = False 

160 

161 if initgroups: 

162 os.initgroups(username, gid) 

163 elif gid != os.getgid(): 

164 os.setgid(gid) 

165 

166 if uid and uid != os.getuid(): 

167 os.setuid(uid) 

168 

169 

170def chown(path, uid, gid): 

171 os.chown(path, uid, gid) 

172 

173 

174if sys.platform.startswith("win"): 

175 def _waitfor(func, pathname, waitall=False): 

176 # Perform the operation 

177 func(pathname) 

178 # Now setup the wait loop 

179 if waitall: 

180 dirname = pathname 

181 else: 

182 dirname, name = os.path.split(pathname) 

183 dirname = dirname or '.' 

184 # Check for `pathname` to be removed from the filesystem. 

185 # The exponential backoff of the timeout amounts to a total 

186 # of ~1 second after which the deletion is probably an error 

187 # anyway. 

188 # Testing on a i7@4.3GHz shows that usually only 1 iteration is 

189 # required when contention occurs. 

190 timeout = 0.001 

191 while timeout < 1.0: 

192 # Note we are only testing for the existence of the file(s) in 

193 # the contents of the directory regardless of any security or 

194 # access rights. If we have made it this far, we have sufficient 

195 # permissions to do that much using Python's equivalent of the 

196 # Windows API FindFirstFile. 

197 # Other Windows APIs can fail or give incorrect results when 

198 # dealing with files that are pending deletion. 

199 L = os.listdir(dirname) 

200 if not L if waitall else name in L: 

201 return 

202 # Increase the timeout and try again 

203 time.sleep(timeout) 

204 timeout *= 2 

205 warnings.warn('tests may fail, delete still pending for ' + pathname, 

206 RuntimeWarning, stacklevel=4) 

207 

208 def _unlink(filename): 

209 _waitfor(os.unlink, filename) 

210else: 

211 _unlink = os.unlink 

212 

213 

214def unlink(filename): 

215 try: 

216 _unlink(filename) 

217 except OSError as error: 

218 # The filename need not exist. 

219 if error.errno not in (errno.ENOENT, errno.ENOTDIR): 

220 raise 

221 

222 

223def is_ipv6(addr): 

224 try: 

225 socket.inet_pton(socket.AF_INET6, addr) 

226 except OSError: # not a valid address 

227 return False 

228 except ValueError: # ipv6 not supported on this platform 

229 return False 

230 return True 

231 

232 

233def parse_address(netloc, default_port='8000'): 

234 if re.match(r'unix:(//)?', netloc): 

235 return re.split(r'unix:(//)?', netloc)[-1] 

236 

237 if netloc.startswith("fd://"): 

238 fd = netloc[5:] 

239 try: 

240 return int(fd) 

241 except ValueError: 

242 raise RuntimeError("%r is not a valid file descriptor." % fd) from None 

243 

244 if netloc.startswith("tcp://"): 

245 netloc = netloc.split("tcp://")[1] 

246 host, port = netloc, default_port 

247 

248 if '[' in netloc and ']' in netloc: 

249 host = netloc.split(']')[0][1:] 

250 port = (netloc.split(']:') + [default_port])[1] 

251 elif ':' in netloc: 

252 host, port = (netloc.split(':') + [default_port])[:2] 

253 elif netloc == "": 

254 host, port = "0.0.0.0", default_port 

255 

256 try: 

257 port = int(port) 

258 except ValueError: 

259 raise RuntimeError("%r is not a valid port number." % port) 

260 

261 return host.lower(), port 

262 

263 

264def close_on_exec(fd): 

265 flags = fcntl.fcntl(fd, fcntl.F_GETFD) 

266 flags |= fcntl.FD_CLOEXEC 

267 fcntl.fcntl(fd, fcntl.F_SETFD, flags) 

268 

269 

270def set_non_blocking(fd): 

271 flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK 

272 fcntl.fcntl(fd, fcntl.F_SETFL, flags) 

273 

274 

275def close(sock): 

276 try: 

277 sock.close() 

278 except OSError: 

279 pass 

280 

281 

282def close_graceful(sock, timeout=2.0, max_drain=65536): 

283 """Close a TCP socket following RFC 9112 section 9.6. 

284 

285 Half-closes the write side to send FIN, then lingers on the read side 

286 to drain the kernel recv buffer until the peer closes or a cap is hit, 

287 then fully closes. This avoids the kernel sending RST (truncating the 

288 last response segment) when unread request data remains in the buffer. 

289 """ 

290 try: 

291 try: 

292 sock.shutdown(socket.SHUT_WR) 

293 except OSError: 

294 return 

295 deadline = time.monotonic() + timeout 

296 drained = 0 

297 while drained < max_drain: 

298 remaining = deadline - time.monotonic() 

299 if remaining <= 0: 

300 break 

301 try: 

302 sock.settimeout(remaining) 

303 data = sock.recv(4096) 

304 except (socket.timeout, OSError): 

305 break 

306 if not data: 

307 break 

308 drained += len(data) 

309 finally: 

310 try: 

311 sock.close() 

312 except OSError: 

313 pass 

314 

315 

316try: 

317 from os import closerange 

318except ImportError: 

319 def closerange(fd_low, fd_high): 

320 # Iterate through and close all file descriptors. 

321 for fd in range(fd_low, fd_high): 

322 try: 

323 os.close(fd) 

324 except OSError: # ERROR, fd wasn't open to begin with (ignored) 

325 pass 

326 

327 

328def write_chunk(sock, data): 

329 if isinstance(data, str): 

330 data = data.encode('utf-8') 

331 chunk_size = "%X\r\n" % len(data) 

332 chunk = b"".join([chunk_size.encode('utf-8'), data, b"\r\n"]) 

333 sock.sendall(chunk) 

334 

335 

336def write(sock, data, chunked=False): 

337 if chunked: 

338 return write_chunk(sock, data) 

339 sock.sendall(data) 

340 

341 

342def write_nonblock(sock, data, chunked=False): 

343 timeout = sock.gettimeout() 

344 if timeout != 0.0: 

345 try: 

346 sock.setblocking(0) 

347 return write(sock, data, chunked) 

348 finally: 

349 sock.setblocking(1) 

350 else: 

351 return write(sock, data, chunked) 

352 

353 

354def write_error(sock, status_int, reason, mesg): 

355 html_error = textwrap.dedent("""\ 

356 <html> 

357 <head> 

358 <title>%(reason)s</title> 

359 </head> 

360 <body> 

361 <h1><p>%(reason)s</p></h1> 

362 %(mesg)s 

363 </body> 

364 </html> 

365 """) % {"reason": reason, "mesg": html.escape(mesg)} 

366 

367 http = textwrap.dedent("""\ 

368 HTTP/1.1 %s %s\r 

369 Connection: close\r 

370 Content-Type: text/html\r 

371 Content-Length: %d\r 

372 \r 

373 %s""") % (str(status_int), reason, len(html_error), html_error) 

374 write_nonblock(sock, http.encode('latin1')) 

375 

376 

377def _called_with_wrong_args(f): 

378 """Check whether calling a function raised a ``TypeError`` because 

379 the call failed or because something in the function raised the 

380 error. 

381 

382 :param f: The function that was called. 

383 :return: ``True`` if the call failed. 

384 """ 

385 tb = sys.exc_info()[2] 

386 

387 try: 

388 while tb is not None: 

389 if tb.tb_frame.f_code is f.__code__: 

390 # In the function, it was called successfully. 

391 return False 

392 

393 tb = tb.tb_next 

394 

395 # Didn't reach the function. 

396 return True 

397 finally: 

398 # Delete tb to break a circular reference in Python 2. 

399 # https://docs.python.org/2/library/sys.html#sys.exc_info 

400 del tb 

401 

402 

403def import_app(module): 

404 parts = module.split(":", 1) 

405 if len(parts) == 1: 

406 obj = "application" 

407 else: 

408 module, obj = parts[0], parts[1] 

409 

410 try: 

411 mod = importlib.import_module(module) 

412 except ImportError: 

413 if module.endswith(".py") and os.path.exists(module): 

414 msg = "Failed to find application, did you mean '%s:%s'?" 

415 raise ImportError(msg % (module.rsplit(".", 1)[0], obj)) 

416 raise 

417 

418 # Parse obj as a single expression to determine if it's a valid 

419 # attribute name or function call. 

420 try: 

421 expression = ast.parse(obj, mode="eval").body 

422 except SyntaxError: 

423 raise AppImportError( 

424 "Failed to parse %r as an attribute name or function call." % obj 

425 ) 

426 

427 if isinstance(expression, ast.Name): 

428 name = expression.id 

429 args = kwargs = None 

430 elif isinstance(expression, ast.Call): 

431 # Ensure the function name is an attribute name only. 

432 if not isinstance(expression.func, ast.Name): 

433 raise AppImportError("Function reference must be a simple name: %r" % obj) 

434 

435 name = expression.func.id 

436 

437 # Parse the positional and keyword arguments as literals. 

438 try: 

439 args = [ast.literal_eval(arg) for arg in expression.args] 

440 kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expression.keywords} 

441 except ValueError: 

442 # literal_eval gives cryptic error messages, show a generic 

443 # message with the full expression instead. 

444 raise AppImportError( 

445 "Failed to parse arguments as literal values: %r" % obj 

446 ) 

447 else: 

448 raise AppImportError( 

449 "Failed to parse %r as an attribute name or function call." % obj 

450 ) 

451 

452 is_debug = logging.root.level == logging.DEBUG 

453 try: 

454 app = getattr(mod, name) 

455 except AttributeError: 

456 if is_debug: 

457 traceback.print_exception(*sys.exc_info()) 

458 raise AppImportError("Failed to find attribute %r in %r." % (name, module)) 

459 

460 # If the expression was a function call, call the retrieved object 

461 # to get the real application. 

462 if args is not None: 

463 try: 

464 app = app(*args, **kwargs) 

465 except TypeError as e: 

466 # If the TypeError was due to bad arguments to the factory 

467 # function, show Python's nice error message without a 

468 # traceback. 

469 if _called_with_wrong_args(app): 

470 raise AppImportError( 

471 "".join(traceback.format_exception_only(TypeError, e)).strip() 

472 ) 

473 

474 # Otherwise it was raised from within the function, show the 

475 # full traceback. 

476 raise 

477 

478 if app is None: 

479 raise AppImportError("Failed to find application object: %r" % obj) 

480 

481 if not callable(app): 

482 raise AppImportError("Application object must be callable.") 

483 return app 

484 

485 

486def getcwd(): 

487 # get current path, try to use PWD env first 

488 try: 

489 a = os.stat(os.environ['PWD']) 

490 b = os.stat(os.getcwd()) 

491 if a.st_ino == b.st_ino and a.st_dev == b.st_dev: 

492 cwd = os.environ['PWD'] 

493 else: 

494 cwd = os.getcwd() 

495 except Exception: 

496 cwd = os.getcwd() 

497 return cwd 

498 

499 

500def http_date(timestamp=None): 

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

502 if timestamp is None: 

503 timestamp = time.time() 

504 s = email.utils.formatdate(timestamp, localtime=False, usegmt=True) 

505 return s 

506 

507 

508def is_hoppish(header): 

509 return header.lower().strip() in hop_headers 

510 

511 

512def daemonize(enable_stdio_inheritance=False): 

513 """\ 

514 Standard daemonization of a process. 

515 http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7 

516 """ 

517 if 'GUNICORN_FD' not in os.environ: 

518 if os.fork(): 

519 os._exit(0) 

520 os.setsid() 

521 

522 if os.fork(): 

523 os._exit(0) 

524 

525 os.umask(0o22) 

526 

527 # In both the following any file descriptors above stdin 

528 # stdout and stderr are left untouched. The inheritance 

529 # option simply allows one to have output go to a file 

530 # specified by way of shell redirection when not wanting 

531 # to use --error-log option. 

532 

533 if not enable_stdio_inheritance: 

534 # Remap all of stdin, stdout and stderr on to 

535 # /dev/null. The expectation is that users have 

536 # specified the --error-log option. 

537 

538 closerange(0, 3) 

539 

540 fd_null = os.open(REDIRECT_TO, os.O_RDWR) 

541 # PEP 446, make fd for /dev/null inheritable 

542 os.set_inheritable(fd_null, True) 

543 

544 # expect fd_null to be always 0 here, but in-case not ... 

545 if fd_null != 0: 

546 os.dup2(fd_null, 0) 

547 

548 os.dup2(fd_null, 1) 

549 os.dup2(fd_null, 2) 

550 

551 else: 

552 fd_null = os.open(REDIRECT_TO, os.O_RDWR) 

553 

554 # Always redirect stdin to /dev/null as we would 

555 # never expect to need to read interactive input. 

556 

557 if fd_null != 0: 

558 os.close(0) 

559 os.dup2(fd_null, 0) 

560 

561 # If stdout and stderr are still connected to 

562 # their original file descriptors we check to see 

563 # if they are associated with terminal devices. 

564 # When they are we map them to /dev/null so that 

565 # are still detached from any controlling terminal 

566 # properly. If not we preserve them as they are. 

567 # 

568 # If stdin and stdout were not hooked up to the 

569 # original file descriptors, then all bets are 

570 # off and all we can really do is leave them as 

571 # they were. 

572 # 

573 # This will allow 'gunicorn ... > output.log 2>&1' 

574 # to work with stdout/stderr going to the file 

575 # as expected. 

576 # 

577 # Note that if using --error-log option, the log 

578 # file specified through shell redirection will 

579 # only be used up until the log file specified 

580 # by the option takes over. As it replaces stdout 

581 # and stderr at the file descriptor level, then 

582 # anything using stdout or stderr, including having 

583 # cached a reference to them, will still work. 

584 

585 def redirect(stream, fd_expect): 

586 try: 

587 fd = stream.fileno() 

588 if fd == fd_expect and stream.isatty(): 

589 os.close(fd) 

590 os.dup2(fd_null, fd) 

591 except AttributeError: 

592 pass 

593 

594 redirect(sys.stdout, 1) 

595 redirect(sys.stderr, 2) 

596 

597 

598def seed(): 

599 try: 

600 random.seed(os.urandom(64)) 

601 except NotImplementedError: 

602 random.seed('%s.%s' % (time.time(), os.getpid())) 

603 

604 

605def check_is_writable(path): 

606 try: 

607 with open(path, 'a') as f: 

608 f.close() 

609 except OSError as e: 

610 raise RuntimeError("Error: '%s' isn't writable [%r]" % (path, e)) 

611 

612 

613def to_bytestring(value, encoding="utf8"): 

614 """Converts a string argument to a byte string""" 

615 if isinstance(value, bytes): 

616 return value 

617 if not isinstance(value, str): 

618 raise TypeError('%r is not a string' % value) 

619 

620 return value.encode(encoding) 

621 

622 

623def has_fileno(obj): 

624 if not hasattr(obj, "fileno"): 

625 return False 

626 

627 # check BytesIO case and maybe others 

628 try: 

629 obj.fileno() 

630 except (AttributeError, OSError, io.UnsupportedOperation): 

631 return False 

632 

633 return True 

634 

635 

636def warn(msg): 

637 print("!!!", file=sys.stderr) 

638 

639 lines = msg.splitlines() 

640 for i, line in enumerate(lines): 

641 if i == 0: 

642 line = "WARNING: %s" % line 

643 print("!!! %s" % line, file=sys.stderr) 

644 

645 print("!!!\n", file=sys.stderr) 

646 sys.stderr.flush() 

647 

648 

649def make_fail_app(msg): 

650 msg = to_bytestring(msg) 

651 

652 def app(environ, start_response): 

653 start_response("500 Internal Server Error", [ 

654 ("Content-Type", "text/plain"), 

655 ("Content-Length", str(len(msg))) 

656 ]) 

657 return [msg] 

658 

659 return app 

660 

661 

662def split_request_uri(uri): 

663 if uri.startswith("//"): 

664 # When the path starts with //, urlsplit considers it as a 

665 # relative uri while the RFC says we should consider it as abs_path 

666 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 

667 # We use temporary dot prefix to workaround this behaviour 

668 parts = urllib.parse.urlsplit("." + uri) 

669 return parts._replace(path=parts.path[1:]) 

670 

671 return urllib.parse.urlsplit(uri) 

672 

673 

674# From six.reraise 

675def reraise(tp, value, tb=None): 

676 try: 

677 if value is None: 

678 value = tp() 

679 if value.__traceback__ is not tb: 

680 raise value.with_traceback(tb) 

681 raise value 

682 finally: 

683 value = None 

684 tb = None 

685 

686 

687def bytes_to_str(b): 

688 if isinstance(b, str): 

689 return b 

690 return str(b, 'latin1') 

691 

692 

693def unquote_to_wsgi_str(string): 

694 return urllib.parse.unquote_to_bytes(string).decode('latin-1')