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

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

372 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 

282try: 

283 from os import closerange 

284except ImportError: 

285 def closerange(fd_low, fd_high): 

286 # Iterate through and close all file descriptors. 

287 for fd in range(fd_low, fd_high): 

288 try: 

289 os.close(fd) 

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

291 pass 

292 

293 

294def write_chunk(sock, data): 

295 if isinstance(data, str): 

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

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

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

299 sock.sendall(chunk) 

300 

301 

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

303 if chunked: 

304 return write_chunk(sock, data) 

305 sock.sendall(data) 

306 

307 

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

309 timeout = sock.gettimeout() 

310 if timeout != 0.0: 

311 try: 

312 sock.setblocking(0) 

313 return write(sock, data, chunked) 

314 finally: 

315 sock.setblocking(1) 

316 else: 

317 return write(sock, data, chunked) 

318 

319 

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

321 html_error = textwrap.dedent("""\ 

322 <html> 

323 <head> 

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

325 </head> 

326 <body> 

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

328 %(mesg)s 

329 </body> 

330 </html> 

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

332 

333 http = textwrap.dedent("""\ 

334 HTTP/1.1 %s %s\r 

335 Connection: close\r 

336 Content-Type: text/html\r 

337 Content-Length: %d\r 

338 \r 

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

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

341 

342 

343def _called_with_wrong_args(f): 

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

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

346 error. 

347 

348 :param f: The function that was called. 

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

350 """ 

351 tb = sys.exc_info()[2] 

352 

353 try: 

354 while tb is not None: 

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

356 # In the function, it was called successfully. 

357 return False 

358 

359 tb = tb.tb_next 

360 

361 # Didn't reach the function. 

362 return True 

363 finally: 

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

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

366 del tb 

367 

368 

369def import_app(module): 

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

371 if len(parts) == 1: 

372 obj = "application" 

373 else: 

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

375 

376 try: 

377 mod = importlib.import_module(module) 

378 except ImportError: 

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

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

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

382 raise 

383 

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

385 # attribute name or function call. 

386 try: 

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

388 except SyntaxError: 

389 raise AppImportError( 

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

391 ) 

392 

393 if isinstance(expression, ast.Name): 

394 name = expression.id 

395 args = kwargs = None 

396 elif isinstance(expression, ast.Call): 

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

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

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

400 

401 name = expression.func.id 

402 

403 # Parse the positional and keyword arguments as literals. 

404 try: 

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

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

407 except ValueError: 

408 # literal_eval gives cryptic error messages, show a generic 

409 # message with the full expression instead. 

410 raise AppImportError( 

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

412 ) 

413 else: 

414 raise AppImportError( 

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

416 ) 

417 

418 is_debug = logging.root.level == logging.DEBUG 

419 try: 

420 app = getattr(mod, name) 

421 except AttributeError: 

422 if is_debug: 

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

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

425 

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

427 # to get the real application. 

428 if args is not None: 

429 try: 

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

431 except TypeError as e: 

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

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

434 # traceback. 

435 if _called_with_wrong_args(app): 

436 raise AppImportError( 

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

438 ) 

439 

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

441 # full traceback. 

442 raise 

443 

444 if app is None: 

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

446 

447 if not callable(app): 

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

449 return app 

450 

451 

452def getcwd(): 

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

454 try: 

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

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

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

458 cwd = os.environ['PWD'] 

459 else: 

460 cwd = os.getcwd() 

461 except Exception: 

462 cwd = os.getcwd() 

463 return cwd 

464 

465 

466def http_date(timestamp=None): 

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

468 if timestamp is None: 

469 timestamp = time.time() 

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

471 return s 

472 

473 

474def is_hoppish(header): 

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

476 

477 

478def daemonize(enable_stdio_inheritance=False): 

479 """\ 

480 Standard daemonization of a process. 

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

482 """ 

483 if 'GUNICORN_FD' not in os.environ: 

484 if os.fork(): 

485 os._exit(0) 

486 os.setsid() 

487 

488 if os.fork(): 

489 os._exit(0) 

490 

491 os.umask(0o22) 

492 

493 # In both the following any file descriptors above stdin 

494 # stdout and stderr are left untouched. The inheritance 

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

496 # specified by way of shell redirection when not wanting 

497 # to use --error-log option. 

498 

499 if not enable_stdio_inheritance: 

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

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

502 # specified the --error-log option. 

503 

504 closerange(0, 3) 

505 

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

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

508 os.set_inheritable(fd_null, True) 

509 

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

511 if fd_null != 0: 

512 os.dup2(fd_null, 0) 

513 

514 os.dup2(fd_null, 1) 

515 os.dup2(fd_null, 2) 

516 

517 else: 

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

519 

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

521 # never expect to need to read interactive input. 

522 

523 if fd_null != 0: 

524 os.close(0) 

525 os.dup2(fd_null, 0) 

526 

527 # If stdout and stderr are still connected to 

528 # their original file descriptors we check to see 

529 # if they are associated with terminal devices. 

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

531 # are still detached from any controlling terminal 

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

533 # 

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

535 # original file descriptors, then all bets are 

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

537 # they were. 

538 # 

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

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

541 # as expected. 

542 # 

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

544 # file specified through shell redirection will 

545 # only be used up until the log file specified 

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

547 # and stderr at the file descriptor level, then 

548 # anything using stdout or stderr, including having 

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

550 

551 def redirect(stream, fd_expect): 

552 try: 

553 fd = stream.fileno() 

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

555 os.close(fd) 

556 os.dup2(fd_null, fd) 

557 except AttributeError: 

558 pass 

559 

560 redirect(sys.stdout, 1) 

561 redirect(sys.stderr, 2) 

562 

563 

564def seed(): 

565 try: 

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

567 except NotImplementedError: 

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

569 

570 

571def check_is_writable(path): 

572 try: 

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

574 f.close() 

575 except OSError as e: 

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

577 

578 

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

580 """Converts a string argument to a byte string""" 

581 if isinstance(value, bytes): 

582 return value 

583 if not isinstance(value, str): 

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

585 

586 return value.encode(encoding) 

587 

588 

589def has_fileno(obj): 

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

591 return False 

592 

593 # check BytesIO case and maybe others 

594 try: 

595 obj.fileno() 

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

597 return False 

598 

599 return True 

600 

601 

602def warn(msg): 

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

604 

605 lines = msg.splitlines() 

606 for i, line in enumerate(lines): 

607 if i == 0: 

608 line = "WARNING: %s" % line 

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

610 

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

612 sys.stderr.flush() 

613 

614 

615def make_fail_app(msg): 

616 msg = to_bytestring(msg) 

617 

618 def app(environ, start_response): 

619 start_response("500 Internal Server Error", [ 

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

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

622 ]) 

623 return [msg] 

624 

625 return app 

626 

627 

628def split_request_uri(uri): 

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

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

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

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

633 # We use temporary dot prefix to workaround this behaviour 

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

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

636 

637 return urllib.parse.urlsplit(uri) 

638 

639 

640# From six.reraise 

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

642 try: 

643 if value is None: 

644 value = tp() 

645 if value.__traceback__ is not tb: 

646 raise value.with_traceback(tb) 

647 raise value 

648 finally: 

649 value = None 

650 tb = None 

651 

652 

653def bytes_to_str(b): 

654 if isinstance(b, str): 

655 return b 

656 return str(b, 'latin1') 

657 

658 

659def unquote_to_wsgi_str(string): 

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