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

399 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# Characters that make a path a glob pattern rather than a literal path. 

36# glob.has_magic() does the same thing but is absent from glob.__all__. 

37GLOB_MAGIC_RE = re.compile(r'[*?[]') 

38 

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

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

41# origin server which the WSGI spec says we should 

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

43# 

44# In the future, concatenation server header values 

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

46# dropping them is easier. 

47hop_headers = set(""" 

48 connection keep-alive proxy-authenticate proxy-authorization 

49 te trailers transfer-encoding upgrade 

50 server date 

51 """.split()) 

52 

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

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

55if sys.platform == "darwin": 

56 def _setproctitle(title): 

57 pass 

58else: 

59 try: 

60 from setproctitle import setproctitle, getproctitle 

61 

62 # Force early initialization before any os.environ modifications 

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

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

65 getproctitle() 

66 

67 def _setproctitle(title): 

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

69 except ImportError: 

70 def _setproctitle(title): 

71 pass 

72 

73 

74def is_glob_pattern(value): 

75 """Return True if the string should be treated as a glob pattern.""" 

76 return GLOB_MAGIC_RE.search(value) is not None 

77 

78 

79def load_entry_point(distribution, group, name): 

80 dist_obj = importlib_metadata.distribution(distribution) 

81 eps = [ep for ep in dist_obj.entry_points 

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

83 if not eps: 

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

85 return eps[0].load() 

86 

87 

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

89 section="gunicorn.workers"): 

90 if inspect.isclass(uri): 

91 return uri 

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

93 # uses entry points 

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

95 try: 

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

97 except ValueError: 

98 dist = entry_str 

99 name = default 

100 

101 try: 

102 return load_entry_point(dist, section, name) 

103 except Exception: 

104 exc = traceback.format_exc() 

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

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

107 else: 

108 components = uri.split('.') 

109 if len(components) == 1: 

110 while True: 

111 if uri.startswith("#"): 

112 uri = uri[1:] 

113 

114 if uri in SUPPORTED_WORKERS: 

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

116 break 

117 

118 try: 

119 return load_entry_point( 

120 "gunicorn", section, uri 

121 ) 

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 

127 klass = components.pop(-1) 

128 

129 try: 

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

131 except Exception: 

132 exc = traceback.format_exc() 

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

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

135 return getattr(mod, klass) 

136 

137 

138positionals = ( 

139 inspect.Parameter.POSITIONAL_ONLY, 

140 inspect.Parameter.POSITIONAL_OR_KEYWORD, 

141) 

142 

143 

144def get_arity(f): 

145 sig = inspect.signature(f) 

146 arity = 0 

147 

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

149 if param.kind in positionals: 

150 arity += 1 

151 

152 return arity 

153 

154 

155def get_username(uid): 

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

157 return pwd.getpwuid(uid).pw_name 

158 

159 

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

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

162 

163 if gid: 

164 if uid: 

165 try: 

166 username = get_username(uid) 

167 except KeyError: 

168 initgroups = False 

169 

170 if initgroups: 

171 os.initgroups(username, gid) 

172 elif gid != os.getgid(): 

173 os.setgid(gid) 

174 

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

176 os.setuid(uid) 

177 

178 

179def chown(path, uid, gid): 

180 os.chown(path, uid, gid) 

181 

182 

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

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

185 # Perform the operation 

186 func(pathname) 

187 # Now setup the wait loop 

188 if waitall: 

189 dirname = pathname 

190 else: 

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

192 dirname = dirname or '.' 

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

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

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

196 # anyway. 

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

198 # required when contention occurs. 

199 timeout = 0.001 

200 while timeout < 1.0: 

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

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

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

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

205 # Windows API FindFirstFile. 

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

207 # dealing with files that are pending deletion. 

208 L = os.listdir(dirname) 

209 if not L if waitall else name in L: 

210 return 

211 # Increase the timeout and try again 

212 time.sleep(timeout) 

213 timeout *= 2 

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

215 RuntimeWarning, stacklevel=4) 

216 

217 def _unlink(filename): 

218 _waitfor(os.unlink, filename) 

219else: 

220 _unlink = os.unlink 

221 

222 

223def unlink(filename): 

224 try: 

225 _unlink(filename) 

226 except OSError as error: 

227 # The filename need not exist. 

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

229 raise 

230 

231 

232def is_ipv6(addr): 

233 try: 

234 socket.inet_pton(socket.AF_INET6, addr) 

235 except OSError: # not a valid address 

236 return False 

237 except ValueError: # ipv6 not supported on this platform 

238 return False 

239 return True 

240 

241 

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

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

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

245 

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

247 fd = netloc[5:] 

248 try: 

249 return int(fd) 

250 except ValueError: 

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

252 

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

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

255 host, port = netloc, default_port 

256 

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

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

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

260 elif ':' in netloc: 

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

262 elif netloc == "": 

263 host, port = "0.0.0.0", default_port 

264 

265 try: 

266 port = int(port) 

267 except ValueError: 

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

269 

270 return host.lower(), port 

271 

272 

273def close_on_exec(fd): 

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

275 flags |= fcntl.FD_CLOEXEC 

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

277 

278 

279def set_non_blocking(fd): 

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

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

282 

283 

284def close(sock): 

285 try: 

286 sock.close() 

287 except OSError: 

288 pass 

289 

290 

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

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

293 

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

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

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

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

298 """ 

299 try: 

300 try: 

301 sock.shutdown(socket.SHUT_WR) 

302 except OSError: 

303 return 

304 deadline = time.monotonic() + timeout 

305 drained = 0 

306 while drained < max_drain: 

307 remaining = deadline - time.monotonic() 

308 if remaining <= 0: 

309 break 

310 try: 

311 sock.settimeout(remaining) 

312 data = sock.recv(4096) 

313 except (socket.timeout, OSError): 

314 break 

315 if not data: 

316 break 

317 drained += len(data) 

318 finally: 

319 try: 

320 sock.close() 

321 except OSError: 

322 pass 

323 

324 

325try: 

326 from os import closerange 

327except ImportError: 

328 def closerange(fd_low, fd_high): 

329 # Iterate through and close all file descriptors. 

330 for fd in range(fd_low, fd_high): 

331 try: 

332 os.close(fd) 

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

334 pass 

335 

336 

337def write_chunk(sock, data): 

338 if isinstance(data, str): 

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

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

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

342 sock.sendall(chunk) 

343 

344 

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

346 if chunked: 

347 return write_chunk(sock, data) 

348 sock.sendall(data) 

349 

350 

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

352 timeout = sock.gettimeout() 

353 if timeout != 0.0: 

354 try: 

355 sock.setblocking(0) 

356 return write(sock, data, chunked) 

357 finally: 

358 sock.setblocking(1) 

359 else: 

360 return write(sock, data, chunked) 

361 

362 

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

364 html_error = textwrap.dedent("""\ 

365 <html> 

366 <head> 

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

368 </head> 

369 <body> 

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

371 %(mesg)s 

372 </body> 

373 </html> 

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

375 

376 http = textwrap.dedent("""\ 

377 HTTP/1.1 %s %s\r 

378 Connection: close\r 

379 Content-Type: text/html\r 

380 Content-Length: %d\r 

381 \r 

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

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

384 

385 

386def _called_with_wrong_args(f): 

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

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

389 error. 

390 

391 :param f: The function that was called. 

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

393 """ 

394 tb = sys.exc_info()[2] 

395 

396 try: 

397 while tb is not None: 

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

399 # In the function, it was called successfully. 

400 return False 

401 

402 tb = tb.tb_next 

403 

404 # Didn't reach the function. 

405 return True 

406 finally: 

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

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

409 del tb 

410 

411 

412def import_app(module): 

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

414 if len(parts) == 1: 

415 obj = "application" 

416 else: 

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

418 

419 try: 

420 mod = importlib.import_module(module) 

421 except ImportError: 

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

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

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

425 raise 

426 

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

428 # attribute name or function call. 

429 try: 

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

431 except SyntaxError: 

432 raise AppImportError( 

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

434 ) 

435 

436 if isinstance(expression, ast.Name): 

437 name = expression.id 

438 args = kwargs = None 

439 elif isinstance(expression, ast.Call): 

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

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

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

443 

444 name = expression.func.id 

445 

446 # Parse the positional and keyword arguments as literals. 

447 try: 

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

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

450 except ValueError: 

451 # literal_eval gives cryptic error messages, show a generic 

452 # message with the full expression instead. 

453 raise AppImportError( 

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

455 ) 

456 else: 

457 raise AppImportError( 

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

459 ) 

460 

461 is_debug = logging.root.level == logging.DEBUG 

462 try: 

463 app = getattr(mod, name) 

464 except AttributeError: 

465 if is_debug: 

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

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

468 

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

470 # to get the real application. 

471 if args is not None: 

472 try: 

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

474 except TypeError as e: 

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

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

477 # traceback. 

478 if _called_with_wrong_args(app): 

479 raise AppImportError( 

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

481 ) 

482 

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

484 # full traceback. 

485 raise 

486 

487 if app is None: 

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

489 

490 if not callable(app): 

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

492 return app 

493 

494 

495def getcwd(): 

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

497 try: 

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

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

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

501 cwd = os.environ['PWD'] 

502 else: 

503 cwd = os.getcwd() 

504 except Exception: 

505 cwd = os.getcwd() 

506 return cwd 

507 

508 

509def http_date(timestamp=None): 

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

511 if timestamp is None: 

512 timestamp = time.time() 

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

514 return s 

515 

516 

517def is_hoppish(header): 

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

519 

520 

521def daemonize(enable_stdio_inheritance=False): 

522 """\ 

523 Standard daemonization of a process. 

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

525 """ 

526 if 'GUNICORN_FD' not in os.environ: 

527 if os.fork(): 

528 os._exit(0) 

529 os.setsid() 

530 

531 if os.fork(): 

532 os._exit(0) 

533 

534 os.umask(0o22) 

535 

536 # In both the following any file descriptors above stdin 

537 # stdout and stderr are left untouched. The inheritance 

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

539 # specified by way of shell redirection when not wanting 

540 # to use --error-log option. 

541 

542 if not enable_stdio_inheritance: 

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

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

545 # specified the --error-log option. 

546 

547 closerange(0, 3) 

548 

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

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

551 os.set_inheritable(fd_null, True) 

552 

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

554 if fd_null != 0: 

555 os.dup2(fd_null, 0) 

556 

557 os.dup2(fd_null, 1) 

558 os.dup2(fd_null, 2) 

559 

560 else: 

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

562 

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

564 # never expect to need to read interactive input. 

565 

566 if fd_null != 0: 

567 os.close(0) 

568 os.dup2(fd_null, 0) 

569 

570 # If stdout and stderr are still connected to 

571 # their original file descriptors we check to see 

572 # if they are associated with terminal devices. 

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

574 # are still detached from any controlling terminal 

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

576 # 

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

578 # original file descriptors, then all bets are 

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

580 # they were. 

581 # 

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

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

584 # as expected. 

585 # 

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

587 # file specified through shell redirection will 

588 # only be used up until the log file specified 

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

590 # and stderr at the file descriptor level, then 

591 # anything using stdout or stderr, including having 

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

593 

594 def redirect(stream, fd_expect): 

595 try: 

596 fd = stream.fileno() 

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

598 os.close(fd) 

599 os.dup2(fd_null, fd) 

600 except AttributeError: 

601 pass 

602 

603 redirect(sys.stdout, 1) 

604 redirect(sys.stderr, 2) 

605 

606 

607def seed(): 

608 try: 

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

610 except NotImplementedError: 

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

612 

613 

614def check_is_writable(path): 

615 try: 

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

617 f.close() 

618 except OSError as e: 

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

620 

621 

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

623 """Converts a string argument to a byte string""" 

624 if isinstance(value, bytes): 

625 return value 

626 if not isinstance(value, str): 

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

628 

629 return value.encode(encoding) 

630 

631 

632def has_fileno(obj): 

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

634 return False 

635 

636 # check BytesIO case and maybe others 

637 try: 

638 obj.fileno() 

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

640 return False 

641 

642 return True 

643 

644 

645def warn(msg): 

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

647 

648 lines = msg.splitlines() 

649 for i, line in enumerate(lines): 

650 if i == 0: 

651 line = "WARNING: %s" % line 

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

653 

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

655 sys.stderr.flush() 

656 

657 

658def make_fail_app(msg): 

659 msg = to_bytestring(msg) 

660 

661 def app(environ, start_response): 

662 start_response("500 Internal Server Error", [ 

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

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

665 ]) 

666 return [msg] 

667 

668 return app 

669 

670 

671def split_request_uri(uri): 

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

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

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

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

676 # We use temporary dot prefix to workaround this behaviour 

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

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

679 

680 return urllib.parse.urlsplit(uri) 

681 

682 

683# From six.reraise 

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

685 try: 

686 if value is None: 

687 value = tp() 

688 if value.__traceback__ is not tb: 

689 raise value.with_traceback(tb) 

690 raise value 

691 finally: 

692 value = None 

693 tb = None 

694 

695 

696def bytes_to_str(b): 

697 if isinstance(b, str): 

698 return b 

699 return str(b, 'latin1') 

700 

701 

702def unquote_to_wsgi_str(string): 

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