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

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

366 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 

49try: 

50 from setproctitle import setproctitle 

51 

52 def _setproctitle(title): 

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

54except ImportError: 

55 def _setproctitle(title): 

56 pass 

57 

58 

59def load_entry_point(distribution, group, name): 

60 dist_obj = importlib_metadata.distribution(distribution) 

61 eps = [ep for ep in dist_obj.entry_points 

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

63 if not eps: 

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

65 return eps[0].load() 

66 

67 

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

69 section="gunicorn.workers"): 

70 if inspect.isclass(uri): 

71 return uri 

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

73 # uses entry points 

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

75 try: 

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

77 except ValueError: 

78 dist = entry_str 

79 name = default 

80 

81 try: 

82 return load_entry_point(dist, section, name) 

83 except Exception: 

84 exc = traceback.format_exc() 

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

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

87 else: 

88 components = uri.split('.') 

89 if len(components) == 1: 

90 while True: 

91 if uri.startswith("#"): 

92 uri = uri[1:] 

93 

94 if uri in SUPPORTED_WORKERS: 

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

96 break 

97 

98 try: 

99 return load_entry_point( 

100 "gunicorn", section, uri 

101 ) 

102 except Exception: 

103 exc = traceback.format_exc() 

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

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

106 

107 klass = components.pop(-1) 

108 

109 try: 

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

111 except Exception: 

112 exc = traceback.format_exc() 

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

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

115 return getattr(mod, klass) 

116 

117 

118positionals = ( 

119 inspect.Parameter.POSITIONAL_ONLY, 

120 inspect.Parameter.POSITIONAL_OR_KEYWORD, 

121) 

122 

123 

124def get_arity(f): 

125 sig = inspect.signature(f) 

126 arity = 0 

127 

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

129 if param.kind in positionals: 

130 arity += 1 

131 

132 return arity 

133 

134 

135def get_username(uid): 

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

137 return pwd.getpwuid(uid).pw_name 

138 

139 

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

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

142 

143 if gid: 

144 if uid: 

145 try: 

146 username = get_username(uid) 

147 except KeyError: 

148 initgroups = False 

149 

150 if initgroups: 

151 os.initgroups(username, gid) 

152 elif gid != os.getgid(): 

153 os.setgid(gid) 

154 

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

156 os.setuid(uid) 

157 

158 

159def chown(path, uid, gid): 

160 os.chown(path, uid, gid) 

161 

162 

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

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

165 # Perform the operation 

166 func(pathname) 

167 # Now setup the wait loop 

168 if waitall: 

169 dirname = pathname 

170 else: 

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

172 dirname = dirname or '.' 

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

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

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

176 # anyway. 

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

178 # required when contention occurs. 

179 timeout = 0.001 

180 while timeout < 1.0: 

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

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

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

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

185 # Windows API FindFirstFile. 

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

187 # dealing with files that are pending deletion. 

188 L = os.listdir(dirname) 

189 if not L if waitall else name in L: 

190 return 

191 # Increase the timeout and try again 

192 time.sleep(timeout) 

193 timeout *= 2 

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

195 RuntimeWarning, stacklevel=4) 

196 

197 def _unlink(filename): 

198 _waitfor(os.unlink, filename) 

199else: 

200 _unlink = os.unlink 

201 

202 

203def unlink(filename): 

204 try: 

205 _unlink(filename) 

206 except OSError as error: 

207 # The filename need not exist. 

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

209 raise 

210 

211 

212def is_ipv6(addr): 

213 try: 

214 socket.inet_pton(socket.AF_INET6, addr) 

215 except OSError: # not a valid address 

216 return False 

217 except ValueError: # ipv6 not supported on this platform 

218 return False 

219 return True 

220 

221 

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

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

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

225 

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

227 fd = netloc[5:] 

228 try: 

229 return int(fd) 

230 except ValueError: 

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

232 

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

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

235 host, port = netloc, default_port 

236 

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

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

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

240 elif ':' in netloc: 

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

242 elif netloc == "": 

243 host, port = "0.0.0.0", default_port 

244 

245 try: 

246 port = int(port) 

247 except ValueError: 

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

249 

250 return host.lower(), port 

251 

252 

253def close_on_exec(fd): 

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

255 flags |= fcntl.FD_CLOEXEC 

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

257 

258 

259def set_non_blocking(fd): 

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

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

262 

263 

264def close(sock): 

265 try: 

266 sock.close() 

267 except OSError: 

268 pass 

269 

270 

271try: 

272 from os import closerange 

273except ImportError: 

274 def closerange(fd_low, fd_high): 

275 # Iterate through and close all file descriptors. 

276 for fd in range(fd_low, fd_high): 

277 try: 

278 os.close(fd) 

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

280 pass 

281 

282 

283def write_chunk(sock, data): 

284 if isinstance(data, str): 

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

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

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

288 sock.sendall(chunk) 

289 

290 

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

292 if chunked: 

293 return write_chunk(sock, data) 

294 sock.sendall(data) 

295 

296 

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

298 timeout = sock.gettimeout() 

299 if timeout != 0.0: 

300 try: 

301 sock.setblocking(0) 

302 return write(sock, data, chunked) 

303 finally: 

304 sock.setblocking(1) 

305 else: 

306 return write(sock, data, chunked) 

307 

308 

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

310 html_error = textwrap.dedent("""\ 

311 <html> 

312 <head> 

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

314 </head> 

315 <body> 

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

317 %(mesg)s 

318 </body> 

319 </html> 

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

321 

322 http = textwrap.dedent("""\ 

323 HTTP/1.1 %s %s\r 

324 Connection: close\r 

325 Content-Type: text/html\r 

326 Content-Length: %d\r 

327 \r 

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

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

330 

331 

332def _called_with_wrong_args(f): 

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

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

335 error. 

336 

337 :param f: The function that was called. 

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

339 """ 

340 tb = sys.exc_info()[2] 

341 

342 try: 

343 while tb is not None: 

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

345 # In the function, it was called successfully. 

346 return False 

347 

348 tb = tb.tb_next 

349 

350 # Didn't reach the function. 

351 return True 

352 finally: 

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

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

355 del tb 

356 

357 

358def import_app(module): 

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

360 if len(parts) == 1: 

361 obj = "application" 

362 else: 

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

364 

365 try: 

366 mod = importlib.import_module(module) 

367 except ImportError: 

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

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

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

371 raise 

372 

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

374 # attribute name or function call. 

375 try: 

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

377 except SyntaxError: 

378 raise AppImportError( 

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

380 ) 

381 

382 if isinstance(expression, ast.Name): 

383 name = expression.id 

384 args = kwargs = None 

385 elif isinstance(expression, ast.Call): 

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

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

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

389 

390 name = expression.func.id 

391 

392 # Parse the positional and keyword arguments as literals. 

393 try: 

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

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

396 except ValueError: 

397 # literal_eval gives cryptic error messages, show a generic 

398 # message with the full expression instead. 

399 raise AppImportError( 

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

401 ) 

402 else: 

403 raise AppImportError( 

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

405 ) 

406 

407 is_debug = logging.root.level == logging.DEBUG 

408 try: 

409 app = getattr(mod, name) 

410 except AttributeError: 

411 if is_debug: 

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

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

414 

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

416 # to get the real application. 

417 if args is not None: 

418 try: 

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

420 except TypeError as e: 

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

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

423 # traceback. 

424 if _called_with_wrong_args(app): 

425 raise AppImportError( 

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

427 ) 

428 

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

430 # full traceback. 

431 raise 

432 

433 if app is None: 

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

435 

436 if not callable(app): 

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

438 return app 

439 

440 

441def getcwd(): 

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

443 try: 

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

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

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

447 cwd = os.environ['PWD'] 

448 else: 

449 cwd = os.getcwd() 

450 except Exception: 

451 cwd = os.getcwd() 

452 return cwd 

453 

454 

455def http_date(timestamp=None): 

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

457 if timestamp is None: 

458 timestamp = time.time() 

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

460 return s 

461 

462 

463def is_hoppish(header): 

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

465 

466 

467def daemonize(enable_stdio_inheritance=False): 

468 """\ 

469 Standard daemonization of a process. 

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

471 """ 

472 if 'GUNICORN_FD' not in os.environ: 

473 if os.fork(): 

474 os._exit(0) 

475 os.setsid() 

476 

477 if os.fork(): 

478 os._exit(0) 

479 

480 os.umask(0o22) 

481 

482 # In both the following any file descriptors above stdin 

483 # stdout and stderr are left untouched. The inheritance 

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

485 # specified by way of shell redirection when not wanting 

486 # to use --error-log option. 

487 

488 if not enable_stdio_inheritance: 

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

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

491 # specified the --error-log option. 

492 

493 closerange(0, 3) 

494 

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

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

497 os.set_inheritable(fd_null, True) 

498 

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

500 if fd_null != 0: 

501 os.dup2(fd_null, 0) 

502 

503 os.dup2(fd_null, 1) 

504 os.dup2(fd_null, 2) 

505 

506 else: 

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

508 

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

510 # never expect to need to read interactive input. 

511 

512 if fd_null != 0: 

513 os.close(0) 

514 os.dup2(fd_null, 0) 

515 

516 # If stdout and stderr are still connected to 

517 # their original file descriptors we check to see 

518 # if they are associated with terminal devices. 

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

520 # are still detached from any controlling terminal 

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

522 # 

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

524 # original file descriptors, then all bets are 

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

526 # they were. 

527 # 

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

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

530 # as expected. 

531 # 

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

533 # file specified through shell redirection will 

534 # only be used up until the log file specified 

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

536 # and stderr at the file descriptor level, then 

537 # anything using stdout or stderr, including having 

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

539 

540 def redirect(stream, fd_expect): 

541 try: 

542 fd = stream.fileno() 

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

544 os.close(fd) 

545 os.dup2(fd_null, fd) 

546 except AttributeError: 

547 pass 

548 

549 redirect(sys.stdout, 1) 

550 redirect(sys.stderr, 2) 

551 

552 

553def seed(): 

554 try: 

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

556 except NotImplementedError: 

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

558 

559 

560def check_is_writable(path): 

561 try: 

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

563 f.close() 

564 except OSError as e: 

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

566 

567 

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

569 """Converts a string argument to a byte string""" 

570 if isinstance(value, bytes): 

571 return value 

572 if not isinstance(value, str): 

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

574 

575 return value.encode(encoding) 

576 

577 

578def has_fileno(obj): 

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

580 return False 

581 

582 # check BytesIO case and maybe others 

583 try: 

584 obj.fileno() 

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

586 return False 

587 

588 return True 

589 

590 

591def warn(msg): 

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

593 

594 lines = msg.splitlines() 

595 for i, line in enumerate(lines): 

596 if i == 0: 

597 line = "WARNING: %s" % line 

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

599 

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

601 sys.stderr.flush() 

602 

603 

604def make_fail_app(msg): 

605 msg = to_bytestring(msg) 

606 

607 def app(environ, start_response): 

608 start_response("500 Internal Server Error", [ 

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

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

611 ]) 

612 return [msg] 

613 

614 return app 

615 

616 

617def split_request_uri(uri): 

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

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

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

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

622 # We use temporary dot prefix to workaround this behaviour 

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

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

625 

626 return urllib.parse.urlsplit(uri) 

627 

628 

629# From six.reraise 

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

631 try: 

632 if value is None: 

633 value = tp() 

634 if value.__traceback__ is not tb: 

635 raise value.with_traceback(tb) 

636 raise value 

637 finally: 

638 value = None 

639 tb = None 

640 

641 

642def bytes_to_str(b): 

643 if isinstance(b, str): 

644 return b 

645 return str(b, 'latin1') 

646 

647 

648def unquote_to_wsgi_str(string): 

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