Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/connector.py: 17%

676 statements  

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

1import asyncio 

2import dataclasses 

3import functools 

4import logging 

5import random 

6import sys 

7import traceback 

8import warnings 

9from collections import defaultdict, deque 

10from contextlib import suppress 

11from http import HTTPStatus 

12from http.cookies import SimpleCookie 

13from itertools import cycle, islice 

14from time import monotonic 

15from types import TracebackType 

16from typing import ( # noqa 

17 TYPE_CHECKING, 

18 Any, 

19 Awaitable, 

20 Callable, 

21 DefaultDict, 

22 Dict, 

23 Iterator, 

24 List, 

25 Literal, 

26 Optional, 

27 Set, 

28 Tuple, 

29 Type, 

30 Union, 

31 cast, 

32) 

33 

34from . import hdrs, helpers 

35from .abc import AbstractResolver 

36from .client_exceptions import ( 

37 ClientConnectionError, 

38 ClientConnectorCertificateError, 

39 ClientConnectorError, 

40 ClientConnectorSSLError, 

41 ClientHttpProxyError, 

42 ClientProxyConnectionError, 

43 ServerFingerprintMismatch, 

44 UnixClientConnectorError, 

45 cert_errors, 

46 ssl_errors, 

47) 

48from .client_proto import ResponseHandler 

49from .client_reqrep import SSL_ALLOWED_TYPES, ClientRequest, Fingerprint 

50from .helpers import _SENTINEL, ceil_timeout, is_ip_address, sentinel, set_result 

51from .locks import EventResultOrError 

52from .resolver import DefaultResolver 

53 

54try: 

55 import ssl 

56 

57 SSLContext = ssl.SSLContext 

58except ImportError: # pragma: no cover 

59 ssl = None # type: ignore[assignment] 

60 SSLContext = object # type: ignore[misc,assignment] 

61 

62 

63__all__ = ("BaseConnector", "TCPConnector", "UnixConnector", "NamedPipeConnector") 

64 

65 

66if TYPE_CHECKING: # pragma: no cover 

67 from .client import ClientTimeout 

68 from .client_reqrep import ConnectionKey 

69 from .tracing import Trace 

70 

71 

72class Connection: 

73 _source_traceback = None 

74 _transport = None 

75 

76 def __init__( 

77 self, 

78 connector: "BaseConnector", 

79 key: "ConnectionKey", 

80 protocol: ResponseHandler, 

81 loop: asyncio.AbstractEventLoop, 

82 ) -> None: 

83 self._key = key 

84 self._connector = connector 

85 self._loop = loop 

86 self._protocol: Optional[ResponseHandler] = protocol 

87 self._callbacks: List[Callable[[], None]] = [] 

88 

89 if loop.get_debug(): 

90 self._source_traceback = traceback.extract_stack(sys._getframe(1)) 

91 

92 def __repr__(self) -> str: 

93 return f"Connection<{self._key}>" 

94 

95 def __del__(self, _warnings: Any = warnings) -> None: 

96 if self._protocol is not None: 

97 _warnings.warn( 

98 f"Unclosed connection {self!r}", ResourceWarning, source=self 

99 ) 

100 if self._loop.is_closed(): 

101 return 

102 

103 self._connector._release(self._key, self._protocol, should_close=True) 

104 

105 context = {"client_connection": self, "message": "Unclosed connection"} 

106 if self._source_traceback is not None: 

107 context["source_traceback"] = self._source_traceback 

108 self._loop.call_exception_handler(context) 

109 

110 @property 

111 def transport(self) -> Optional[asyncio.Transport]: 

112 if self._protocol is None: 

113 return None 

114 return self._protocol.transport 

115 

116 @property 

117 def protocol(self) -> Optional[ResponseHandler]: 

118 return self._protocol 

119 

120 def add_callback(self, callback: Callable[[], None]) -> None: 

121 if callback is not None: 

122 self._callbacks.append(callback) 

123 

124 def _notify_release(self) -> None: 

125 callbacks, self._callbacks = self._callbacks[:], [] 

126 

127 for cb in callbacks: 

128 with suppress(Exception): 

129 cb() 

130 

131 def close(self) -> None: 

132 self._notify_release() 

133 

134 if self._protocol is not None: 

135 self._connector._release(self._key, self._protocol, should_close=True) 

136 self._protocol = None 

137 

138 def release(self) -> None: 

139 self._notify_release() 

140 

141 if self._protocol is not None: 

142 self._connector._release( 

143 self._key, self._protocol, should_close=self._protocol.should_close 

144 ) 

145 self._protocol = None 

146 

147 @property 

148 def closed(self) -> bool: 

149 return self._protocol is None or not self._protocol.is_connected() 

150 

151 

152class _TransportPlaceholder: 

153 """placeholder for BaseConnector.connect function""" 

154 

155 def __init__(self, loop: asyncio.AbstractEventLoop) -> None: 

156 fut = loop.create_future() 

157 fut.set_result(None) 

158 self.closed: asyncio.Future[Optional[Exception]] = fut 

159 

160 def close(self) -> None: 

161 pass 

162 

163 

164class BaseConnector: 

165 """Base connector class. 

166 

167 keepalive_timeout - (optional) Keep-alive timeout. 

168 force_close - Set to True to force close and do reconnect 

169 after each request (and between redirects). 

170 limit - The total number of simultaneous connections. 

171 limit_per_host - Number of simultaneous connections to one host. 

172 enable_cleanup_closed - Enables clean-up closed ssl transports. 

173 Disabled by default. 

174 timeout_ceil_threshold - Trigger ceiling of timeout values when 

175 it's above timeout_ceil_threshold. 

176 loop - Optional event loop. 

177 """ 

178 

179 _closed = True # prevent AttributeError in __del__ if ctor was failed 

180 _source_traceback = None 

181 

182 # abort transport after 2 seconds (cleanup broken connections) 

183 _cleanup_closed_period = 2.0 

184 

185 def __init__( 

186 self, 

187 *, 

188 keepalive_timeout: Union[_SENTINEL, None, float] = sentinel, 

189 force_close: bool = False, 

190 limit: int = 100, 

191 limit_per_host: int = 0, 

192 enable_cleanup_closed: bool = False, 

193 timeout_ceil_threshold: float = 5, 

194 ) -> None: 

195 if force_close: 

196 if keepalive_timeout is not None and keepalive_timeout is not sentinel: 

197 raise ValueError( 

198 "keepalive_timeout cannot " "be set if force_close is True" 

199 ) 

200 else: 

201 if keepalive_timeout is sentinel: 

202 keepalive_timeout = 15.0 

203 

204 self._timeout_ceil_threshold = timeout_ceil_threshold 

205 

206 loop = asyncio.get_running_loop() 

207 

208 self._closed = False 

209 if loop.get_debug(): 

210 self._source_traceback = traceback.extract_stack(sys._getframe(1)) 

211 

212 self._conns: Dict[ConnectionKey, List[Tuple[ResponseHandler, float]]] = {} 

213 self._limit = limit 

214 self._limit_per_host = limit_per_host 

215 self._acquired: Set[ResponseHandler] = set() 

216 self._acquired_per_host: DefaultDict[ 

217 ConnectionKey, Set[ResponseHandler] 

218 ] = defaultdict(set) 

219 self._keepalive_timeout = cast(float, keepalive_timeout) 

220 self._force_close = force_close 

221 

222 # {host_key: FIFO list of waiters} 

223 self._waiters = defaultdict(deque) # type: ignore[var-annotated] 

224 

225 self._loop = loop 

226 self._factory = functools.partial(ResponseHandler, loop=loop) 

227 

228 self.cookies: SimpleCookie[str] = SimpleCookie() 

229 

230 # start keep-alive connection cleanup task 

231 self._cleanup_handle: Optional[asyncio.TimerHandle] = None 

232 

233 # start cleanup closed transports task 

234 self._cleanup_closed_handle: Optional[asyncio.TimerHandle] = None 

235 self._cleanup_closed_disabled = not enable_cleanup_closed 

236 self._cleanup_closed_transports: List[Optional[asyncio.Transport]] = [] 

237 self._cleanup_closed() 

238 

239 def __del__(self, _warnings: Any = warnings) -> None: 

240 if self._closed: 

241 return 

242 if not self._conns: 

243 return 

244 

245 conns = [repr(c) for c in self._conns.values()] 

246 

247 self._close_immediately() 

248 

249 _warnings.warn(f"Unclosed connector {self!r}", ResourceWarning, source=self) 

250 context = { 

251 "connector": self, 

252 "connections": conns, 

253 "message": "Unclosed connector", 

254 } 

255 if self._source_traceback is not None: 

256 context["source_traceback"] = self._source_traceback 

257 self._loop.call_exception_handler(context) 

258 

259 async def __aenter__(self) -> "BaseConnector": 

260 return self 

261 

262 async def __aexit__( 

263 self, 

264 exc_type: Optional[Type[BaseException]] = None, 

265 exc_value: Optional[BaseException] = None, 

266 exc_traceback: Optional[TracebackType] = None, 

267 ) -> None: 

268 await self.close() 

269 

270 @property 

271 def force_close(self) -> bool: 

272 """Ultimately close connection on releasing if True.""" 

273 return self._force_close 

274 

275 @property 

276 def limit(self) -> int: 

277 """The total number for simultaneous connections. 

278 

279 If limit is 0 the connector has no limit. 

280 The default limit size is 100. 

281 """ 

282 return self._limit 

283 

284 @property 

285 def limit_per_host(self) -> int: 

286 """The limit for simultaneous connections to the same endpoint. 

287 

288 Endpoints are the same if they are have equal 

289 (host, port, is_ssl) triple. 

290 """ 

291 return self._limit_per_host 

292 

293 def _cleanup(self) -> None: 

294 """Cleanup unused transports.""" 

295 if self._cleanup_handle: 

296 self._cleanup_handle.cancel() 

297 # _cleanup_handle should be unset, otherwise _release() will not 

298 # recreate it ever! 

299 self._cleanup_handle = None 

300 

301 now = self._loop.time() 

302 timeout = self._keepalive_timeout 

303 

304 if self._conns: 

305 connections = {} 

306 deadline = now - timeout 

307 for key, conns in self._conns.items(): 

308 alive = [] 

309 for proto, use_time in conns: 

310 if proto.is_connected(): 

311 if use_time - deadline < 0: 

312 transport = proto.transport 

313 proto.close() 

314 if key.is_ssl and not self._cleanup_closed_disabled: 

315 self._cleanup_closed_transports.append(transport) 

316 else: 

317 alive.append((proto, use_time)) 

318 else: 

319 transport = proto.transport 

320 proto.close() 

321 if key.is_ssl and not self._cleanup_closed_disabled: 

322 self._cleanup_closed_transports.append(transport) 

323 

324 if alive: 

325 connections[key] = alive 

326 

327 self._conns = connections 

328 

329 if self._conns: 

330 self._cleanup_handle = helpers.weakref_handle( 

331 self, 

332 "_cleanup", 

333 timeout, 

334 self._loop, 

335 timeout_ceil_threshold=self._timeout_ceil_threshold, 

336 ) 

337 

338 def _drop_acquired_per_host( 

339 self, key: "ConnectionKey", val: ResponseHandler 

340 ) -> None: 

341 acquired_per_host = self._acquired_per_host 

342 if key not in acquired_per_host: 

343 return 

344 conns = acquired_per_host[key] 

345 conns.remove(val) 

346 if not conns: 

347 del self._acquired_per_host[key] 

348 

349 def _cleanup_closed(self) -> None: 

350 """Double confirmation for transport close. 

351 

352 Some broken ssl servers may leave socket open without proper close. 

353 """ 

354 if self._cleanup_closed_handle: 

355 self._cleanup_closed_handle.cancel() 

356 

357 for transport in self._cleanup_closed_transports: 

358 if transport is not None: 

359 transport.abort() 

360 

361 self._cleanup_closed_transports = [] 

362 

363 if not self._cleanup_closed_disabled: 

364 self._cleanup_closed_handle = helpers.weakref_handle( 

365 self, 

366 "_cleanup_closed", 

367 self._cleanup_closed_period, 

368 self._loop, 

369 timeout_ceil_threshold=self._timeout_ceil_threshold, 

370 ) 

371 

372 async def close(self) -> None: 

373 """Close all opened transports.""" 

374 waiters = self._close_immediately() 

375 if waiters: 

376 results = await asyncio.gather(*waiters, return_exceptions=True) 

377 for res in results: 

378 if isinstance(res, Exception): 

379 err_msg = "Error while closing connector: " + repr(res) 

380 logging.error(err_msg) 

381 

382 def _close_immediately(self) -> List["asyncio.Future[None]"]: 

383 waiters: List["asyncio.Future[None]"] = [] 

384 

385 if self._closed: 

386 return waiters 

387 

388 self._closed = True 

389 

390 try: 

391 if self._loop.is_closed(): 

392 return waiters 

393 

394 # cancel cleanup task 

395 if self._cleanup_handle: 

396 self._cleanup_handle.cancel() 

397 

398 # cancel cleanup close task 

399 if self._cleanup_closed_handle: 

400 self._cleanup_closed_handle.cancel() 

401 

402 for data in self._conns.values(): 

403 for proto, t0 in data: 

404 proto.close() 

405 waiters.append(proto.closed) 

406 

407 for proto in self._acquired: 

408 proto.close() 

409 waiters.append(proto.closed) 

410 

411 # TODO (A.Yushovskiy, 24-May-2019) collect transp. closing futures 

412 for transport in self._cleanup_closed_transports: 

413 if transport is not None: 

414 transport.abort() 

415 

416 return waiters 

417 

418 finally: 

419 self._conns.clear() 

420 self._acquired.clear() 

421 self._waiters.clear() 

422 self._cleanup_handle = None 

423 self._cleanup_closed_transports.clear() 

424 self._cleanup_closed_handle = None 

425 

426 @property 

427 def closed(self) -> bool: 

428 """Is connector closed. 

429 

430 A readonly property. 

431 """ 

432 return self._closed 

433 

434 def _available_connections(self, key: "ConnectionKey") -> int: 

435 """ 

436 Return number of available connections. 

437 

438 The limit, limit_per_host and the connection key are taken into account. 

439 

440 If it returns less than 1 means that there are no connections 

441 available. 

442 """ 

443 if self._limit: 

444 # total calc available connections 

445 available = self._limit - len(self._acquired) 

446 

447 # check limit per host 

448 if ( 

449 self._limit_per_host 

450 and available > 0 

451 and key in self._acquired_per_host 

452 ): 

453 acquired = self._acquired_per_host.get(key) 

454 assert acquired is not None 

455 available = self._limit_per_host - len(acquired) 

456 

457 elif self._limit_per_host and key in self._acquired_per_host: 

458 # check limit per host 

459 acquired = self._acquired_per_host.get(key) 

460 assert acquired is not None 

461 available = self._limit_per_host - len(acquired) 

462 else: 

463 available = 1 

464 

465 return available 

466 

467 async def connect( 

468 self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" 

469 ) -> Connection: 

470 """Get from pool or create new connection.""" 

471 key = req.connection_key 

472 available = self._available_connections(key) 

473 

474 # Wait if there are no available connections or if there are/were 

475 # waiters (i.e. don't steal connection from a waiter about to wake up) 

476 if available <= 0 or key in self._waiters: 

477 fut = self._loop.create_future() 

478 

479 # This connection will now count towards the limit. 

480 self._waiters[key].append(fut) 

481 

482 if traces: 

483 for trace in traces: 

484 await trace.send_connection_queued_start() 

485 

486 try: 

487 await fut 

488 except BaseException as e: 

489 if key in self._waiters: 

490 # remove a waiter even if it was cancelled, normally it's 

491 # removed when it's notified 

492 try: 

493 self._waiters[key].remove(fut) 

494 except ValueError: # fut may no longer be in list 

495 pass 

496 

497 raise e 

498 finally: 

499 if key in self._waiters and not self._waiters[key]: 

500 del self._waiters[key] 

501 

502 if traces: 

503 for trace in traces: 

504 await trace.send_connection_queued_end() 

505 

506 proto = self._get(key) 

507 if proto is None: 

508 placeholder = cast(ResponseHandler, _TransportPlaceholder(self._loop)) 

509 self._acquired.add(placeholder) 

510 self._acquired_per_host[key].add(placeholder) 

511 

512 if traces: 

513 for trace in traces: 

514 await trace.send_connection_create_start() 

515 

516 try: 

517 proto = await self._create_connection(req, traces, timeout) 

518 if self._closed: 

519 proto.close() 

520 raise ClientConnectionError("Connector is closed.") 

521 except BaseException: 

522 if not self._closed: 

523 self._acquired.remove(placeholder) 

524 self._drop_acquired_per_host(key, placeholder) 

525 self._release_waiter() 

526 raise 

527 else: 

528 if not self._closed: 

529 self._acquired.remove(placeholder) 

530 self._drop_acquired_per_host(key, placeholder) 

531 

532 if traces: 

533 for trace in traces: 

534 await trace.send_connection_create_end() 

535 else: 

536 if traces: 

537 # Acquire the connection to prevent race conditions with limits 

538 placeholder = cast(ResponseHandler, _TransportPlaceholder(self._loop)) 

539 self._acquired.add(placeholder) 

540 self._acquired_per_host[key].add(placeholder) 

541 for trace in traces: 

542 await trace.send_connection_reuseconn() 

543 self._acquired.remove(placeholder) 

544 self._drop_acquired_per_host(key, placeholder) 

545 

546 self._acquired.add(proto) 

547 self._acquired_per_host[key].add(proto) 

548 return Connection(self, key, proto, self._loop) 

549 

550 def _get(self, key: "ConnectionKey") -> Optional[ResponseHandler]: 

551 try: 

552 conns = self._conns[key] 

553 except KeyError: 

554 return None 

555 

556 t1 = self._loop.time() 

557 while conns: 

558 proto, t0 = conns.pop() 

559 if proto.is_connected(): 

560 if t1 - t0 > self._keepalive_timeout: 

561 transport = proto.transport 

562 proto.close() 

563 # only for SSL transports 

564 if key.is_ssl and not self._cleanup_closed_disabled: 

565 self._cleanup_closed_transports.append(transport) 

566 else: 

567 if not conns: 

568 # The very last connection was reclaimed: drop the key 

569 del self._conns[key] 

570 return proto 

571 else: 

572 transport = proto.transport 

573 proto.close() 

574 if key.is_ssl and not self._cleanup_closed_disabled: 

575 self._cleanup_closed_transports.append(transport) 

576 

577 # No more connections: drop the key 

578 del self._conns[key] 

579 return None 

580 

581 def _release_waiter(self) -> None: 

582 """ 

583 Iterates over all waiters until one to be released is found. 

584 

585 The one to be released is not finished and 

586 belongs to a host that has available connections. 

587 """ 

588 if not self._waiters: 

589 return 

590 

591 # Having the dict keys ordered this avoids to iterate 

592 # at the same order at each call. 

593 queues = list(self._waiters.keys()) 

594 random.shuffle(queues) 

595 

596 for key in queues: 

597 if self._available_connections(key) < 1: 

598 continue 

599 

600 waiters = self._waiters[key] 

601 while waiters: 

602 waiter = waiters.popleft() 

603 if not waiter.done(): 

604 waiter.set_result(None) 

605 return 

606 

607 def _release_acquired(self, key: "ConnectionKey", proto: ResponseHandler) -> None: 

608 if self._closed: 

609 # acquired connection is already released on connector closing 

610 return 

611 

612 try: 

613 self._acquired.remove(proto) 

614 self._drop_acquired_per_host(key, proto) 

615 except KeyError: # pragma: no cover 

616 # this may be result of undetermenistic order of objects 

617 # finalization due garbage collection. 

618 pass 

619 else: 

620 self._release_waiter() 

621 

622 def _release( 

623 self, 

624 key: "ConnectionKey", 

625 protocol: ResponseHandler, 

626 *, 

627 should_close: bool = False, 

628 ) -> None: 

629 if self._closed: 

630 # acquired connection is already released on connector closing 

631 return 

632 

633 self._release_acquired(key, protocol) 

634 

635 if self._force_close: 

636 should_close = True 

637 

638 if should_close or protocol.should_close: 

639 transport = protocol.transport 

640 protocol.close() 

641 # TODO: Remove once fixed: https://bugs.python.org/issue39951 

642 # See PR #6321 

643 set_result(protocol.closed, None) 

644 

645 if key.is_ssl and not self._cleanup_closed_disabled: 

646 self._cleanup_closed_transports.append(transport) 

647 else: 

648 conns = self._conns.get(key) 

649 if conns is None: 

650 conns = self._conns[key] = [] 

651 conns.append((protocol, self._loop.time())) 

652 

653 if self._cleanup_handle is None: 

654 self._cleanup_handle = helpers.weakref_handle( 

655 self, 

656 "_cleanup", 

657 self._keepalive_timeout, 

658 self._loop, 

659 timeout_ceil_threshold=self._timeout_ceil_threshold, 

660 ) 

661 

662 async def _create_connection( 

663 self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" 

664 ) -> ResponseHandler: 

665 raise NotImplementedError() 

666 

667 

668class _DNSCacheTable: 

669 def __init__(self, ttl: Optional[float] = None) -> None: 

670 self._addrs_rr: Dict[Tuple[str, int], Tuple[Iterator[Dict[str, Any]], int]] = {} 

671 self._timestamps: Dict[Tuple[str, int], float] = {} 

672 self._ttl = ttl 

673 

674 def __contains__(self, host: object) -> bool: 

675 return host in self._addrs_rr 

676 

677 def add(self, key: Tuple[str, int], addrs: List[Dict[str, Any]]) -> None: 

678 self._addrs_rr[key] = (cycle(addrs), len(addrs)) 

679 

680 if self._ttl is not None: 

681 self._timestamps[key] = monotonic() 

682 

683 def remove(self, key: Tuple[str, int]) -> None: 

684 self._addrs_rr.pop(key, None) 

685 

686 if self._ttl is not None: 

687 self._timestamps.pop(key, None) 

688 

689 def clear(self) -> None: 

690 self._addrs_rr.clear() 

691 self._timestamps.clear() 

692 

693 def next_addrs(self, key: Tuple[str, int]) -> List[Dict[str, Any]]: 

694 loop, length = self._addrs_rr[key] 

695 addrs = list(islice(loop, length)) 

696 # Consume one more element to shift internal state of `cycle` 

697 next(loop) 

698 return addrs 

699 

700 def expired(self, key: Tuple[str, int]) -> bool: 

701 if self._ttl is None: 

702 return False 

703 

704 return self._timestamps[key] + self._ttl < monotonic() 

705 

706 

707class TCPConnector(BaseConnector): 

708 """TCP connector. 

709 

710 verify_ssl - Set to True to check ssl certifications. 

711 fingerprint - Pass the binary sha256 

712 digest of the expected certificate in DER format to verify 

713 that the certificate the server presents matches. See also 

714 https://en.wikipedia.org/wiki/Transport_Layer_Security#Certificate_pinning 

715 resolver - Enable DNS lookups and use this 

716 resolver 

717 use_dns_cache - Use memory cache for DNS lookups. 

718 ttl_dns_cache - Max seconds having cached a DNS entry, None forever. 

719 family - socket address family 

720 local_addr - local tuple of (host, port) to bind socket to 

721 

722 keepalive_timeout - (optional) Keep-alive timeout. 

723 force_close - Set to True to force close and do reconnect 

724 after each request (and between redirects). 

725 limit - The total number of simultaneous connections. 

726 limit_per_host - Number of simultaneous connections to one host. 

727 enable_cleanup_closed - Enables clean-up closed ssl transports. 

728 Disabled by default. 

729 loop - Optional event loop. 

730 """ 

731 

732 def __init__( 

733 self, 

734 *, 

735 use_dns_cache: bool = True, 

736 ttl_dns_cache: Optional[int] = 10, 

737 family: int = 0, 

738 ssl: Union[None, Literal[False], Fingerprint, SSLContext] = None, 

739 local_addr: Optional[Tuple[str, int]] = None, 

740 resolver: Optional[AbstractResolver] = None, 

741 keepalive_timeout: Union[None, float, _SENTINEL] = sentinel, 

742 force_close: bool = False, 

743 limit: int = 100, 

744 limit_per_host: int = 0, 

745 enable_cleanup_closed: bool = False, 

746 timeout_ceil_threshold: float = 5, 

747 ) -> None: 

748 super().__init__( 

749 keepalive_timeout=keepalive_timeout, 

750 force_close=force_close, 

751 limit=limit, 

752 limit_per_host=limit_per_host, 

753 enable_cleanup_closed=enable_cleanup_closed, 

754 timeout_ceil_threshold=timeout_ceil_threshold, 

755 ) 

756 

757 if not isinstance(ssl, SSL_ALLOWED_TYPES): 

758 raise TypeError( 

759 "ssl should be SSLContext, bool, Fingerprint, " 

760 "or None, got {!r} instead.".format(ssl) 

761 ) 

762 self._ssl = ssl 

763 if resolver is None: 

764 resolver = DefaultResolver() 

765 self._resolver: AbstractResolver = resolver 

766 

767 self._use_dns_cache = use_dns_cache 

768 self._cached_hosts = _DNSCacheTable(ttl=ttl_dns_cache) 

769 self._throttle_dns_events: Dict[Tuple[str, int], EventResultOrError] = {} 

770 self._family = family 

771 self._local_addr = local_addr 

772 

773 def _close_immediately(self) -> List["asyncio.Future[None]"]: 

774 for ev in self._throttle_dns_events.values(): 

775 ev.cancel() 

776 return super()._close_immediately() 

777 

778 @property 

779 def family(self) -> int: 

780 """Socket family like AF_INET.""" 

781 return self._family 

782 

783 @property 

784 def use_dns_cache(self) -> bool: 

785 """True if local DNS caching is enabled.""" 

786 return self._use_dns_cache 

787 

788 def clear_dns_cache( 

789 self, host: Optional[str] = None, port: Optional[int] = None 

790 ) -> None: 

791 """Remove specified host/port or clear all dns local cache.""" 

792 if host is not None and port is not None: 

793 self._cached_hosts.remove((host, port)) 

794 elif host is not None or port is not None: 

795 raise ValueError("either both host and port " "or none of them are allowed") 

796 else: 

797 self._cached_hosts.clear() 

798 

799 async def _resolve_host( 

800 self, host: str, port: int, traces: Optional[List["Trace"]] = None 

801 ) -> List[Dict[str, Any]]: 

802 if is_ip_address(host): 

803 return [ 

804 { 

805 "hostname": host, 

806 "host": host, 

807 "port": port, 

808 "family": self._family, 

809 "proto": 0, 

810 "flags": 0, 

811 } 

812 ] 

813 

814 if not self._use_dns_cache: 

815 if traces: 

816 for trace in traces: 

817 await trace.send_dns_resolvehost_start(host) 

818 

819 res = await self._resolver.resolve(host, port, family=self._family) 

820 

821 if traces: 

822 for trace in traces: 

823 await trace.send_dns_resolvehost_end(host) 

824 

825 return res 

826 

827 key = (host, port) 

828 

829 if (key in self._cached_hosts) and (not self._cached_hosts.expired(key)): 

830 # get result early, before any await (#4014) 

831 result = self._cached_hosts.next_addrs(key) 

832 

833 if traces: 

834 for trace in traces: 

835 await trace.send_dns_cache_hit(host) 

836 return result 

837 

838 if key in self._throttle_dns_events: 

839 # get event early, before any await (#4014) 

840 event = self._throttle_dns_events[key] 

841 if traces: 

842 for trace in traces: 

843 await trace.send_dns_cache_hit(host) 

844 await event.wait() 

845 else: 

846 # update dict early, before any await (#4014) 

847 self._throttle_dns_events[key] = EventResultOrError(self._loop) 

848 if traces: 

849 for trace in traces: 

850 await trace.send_dns_cache_miss(host) 

851 try: 

852 if traces: 

853 for trace in traces: 

854 await trace.send_dns_resolvehost_start(host) 

855 

856 addrs = await self._resolver.resolve(host, port, family=self._family) 

857 if traces: 

858 for trace in traces: 

859 await trace.send_dns_resolvehost_end(host) 

860 

861 self._cached_hosts.add(key, addrs) 

862 self._throttle_dns_events[key].set() 

863 except BaseException as e: 

864 # any DNS exception, independently of the implementation 

865 # is set for the waiters to raise the same exception. 

866 self._throttle_dns_events[key].set(exc=e) 

867 raise 

868 finally: 

869 self._throttle_dns_events.pop(key) 

870 

871 return self._cached_hosts.next_addrs(key) 

872 

873 async def _create_connection( 

874 self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" 

875 ) -> ResponseHandler: 

876 """Create connection. 

877 

878 Has same keyword arguments as BaseEventLoop.create_connection. 

879 """ 

880 if req.proxy: 

881 _, proto = await self._create_proxy_connection(req, traces, timeout) 

882 else: 

883 _, proto = await self._create_direct_connection(req, traces, timeout) 

884 

885 return proto 

886 

887 @staticmethod 

888 @functools.lru_cache(None) 

889 def _make_ssl_context(verified: bool) -> SSLContext: 

890 if verified: 

891 return ssl.create_default_context() 

892 else: 

893 sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) 

894 sslcontext.options |= ssl.OP_NO_SSLv2 

895 sslcontext.options |= ssl.OP_NO_SSLv3 

896 sslcontext.check_hostname = False 

897 sslcontext.verify_mode = ssl.CERT_NONE 

898 try: 

899 sslcontext.options |= ssl.OP_NO_COMPRESSION 

900 except AttributeError as attr_err: 

901 warnings.warn( 

902 "{!s}: The Python interpreter is compiled " 

903 "against OpenSSL < 1.0.0. Ref: " 

904 "https://docs.python.org/3/library/ssl.html" 

905 "#ssl.OP_NO_COMPRESSION".format(attr_err), 

906 ) 

907 sslcontext.set_default_verify_paths() 

908 return sslcontext 

909 

910 def _get_ssl_context(self, req: ClientRequest) -> Optional[SSLContext]: 

911 """Logic to get the correct SSL context 

912 

913 0. if req.ssl is false, return None 

914 

915 1. if ssl_context is specified in req, use it 

916 2. if _ssl_context is specified in self, use it 

917 3. otherwise: 

918 1. if verify_ssl is not specified in req, use self.ssl_context 

919 (will generate a default context according to self.verify_ssl) 

920 2. if verify_ssl is True in req, generate a default SSL context 

921 3. if verify_ssl is False in req, generate a SSL context that 

922 won't verify 

923 """ 

924 if req.is_ssl(): 

925 if ssl is None: # pragma: no cover 

926 raise RuntimeError("SSL is not supported.") 

927 sslcontext = req.ssl 

928 if isinstance(sslcontext, ssl.SSLContext): 

929 return sslcontext 

930 if sslcontext is not None: 

931 # not verified or fingerprinted 

932 return self._make_ssl_context(False) 

933 sslcontext = self._ssl 

934 if isinstance(sslcontext, ssl.SSLContext): 

935 return sslcontext 

936 if sslcontext is not None: 

937 # not verified or fingerprinted 

938 return self._make_ssl_context(False) 

939 return self._make_ssl_context(True) 

940 else: 

941 return None 

942 

943 def _get_fingerprint(self, req: ClientRequest) -> Optional["Fingerprint"]: 

944 ret = req.ssl 

945 if isinstance(ret, Fingerprint): 

946 return ret 

947 ret = self._ssl 

948 if isinstance(ret, Fingerprint): 

949 return ret 

950 return None 

951 

952 async def _wrap_create_connection( 

953 self, 

954 *args: Any, 

955 req: ClientRequest, 

956 timeout: "ClientTimeout", 

957 client_error: Type[Exception] = ClientConnectorError, 

958 **kwargs: Any, 

959 ) -> Tuple[asyncio.Transport, ResponseHandler]: 

960 try: 

961 async with ceil_timeout( 

962 timeout.sock_connect, ceil_threshold=timeout.ceil_threshold 

963 ): 

964 return await self._loop.create_connection(*args, **kwargs) 

965 except cert_errors as exc: 

966 raise ClientConnectorCertificateError(req.connection_key, exc) from exc 

967 except ssl_errors as exc: 

968 raise ClientConnectorSSLError(req.connection_key, exc) from exc 

969 except OSError as exc: 

970 if exc.errno is None and isinstance(exc, asyncio.TimeoutError): 

971 raise 

972 raise client_error(req.connection_key, exc) from exc 

973 

974 def _warn_about_tls_in_tls( 

975 self, 

976 underlying_transport: asyncio.Transport, 

977 req: ClientRequest, 

978 ) -> None: 

979 """Issue a warning if the requested URL has HTTPS scheme.""" 

980 if req.request_info.url.scheme != "https": 

981 return 

982 

983 asyncio_supports_tls_in_tls = getattr( 

984 underlying_transport, 

985 "_start_tls_compatible", 

986 False, 

987 ) 

988 

989 if asyncio_supports_tls_in_tls: 

990 return 

991 

992 warnings.warn( 

993 "An HTTPS request is being sent through an HTTPS proxy. " 

994 "This support for TLS in TLS is known to be disabled " 

995 "in the stdlib asyncio. This is why you'll probably see " 

996 "an error in the log below.\n\n" 

997 "It is possible to enable it via monkeypatching. " 

998 "For more details, see:\n" 

999 "* https://bugs.python.org/issue37179\n" 

1000 "* https://github.com/python/cpython/pull/28073\n\n" 

1001 "You can temporarily patch this as follows:\n" 

1002 "* https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support\n" 

1003 "* https://github.com/aio-libs/aiohttp/discussions/6044\n", 

1004 RuntimeWarning, 

1005 source=self, 

1006 # Why `4`? At least 3 of the calls in the stack originate 

1007 # from the methods in this class. 

1008 stacklevel=3, 

1009 ) 

1010 

1011 async def _start_tls_connection( 

1012 self, 

1013 underlying_transport: asyncio.Transport, 

1014 req: ClientRequest, 

1015 timeout: "ClientTimeout", 

1016 client_error: Type[Exception] = ClientConnectorError, 

1017 ) -> Tuple[asyncio.BaseTransport, ResponseHandler]: 

1018 """Wrap the raw TCP transport with TLS.""" 

1019 tls_proto = self._factory() # Create a brand new proto for TLS 

1020 

1021 # Safety of the `cast()` call here is based on the fact that 

1022 # internally `_get_ssl_context()` only returns `None` when 

1023 # `req.is_ssl()` evaluates to `False` which is never gonna happen 

1024 # in this code path. Of course, it's rather fragile 

1025 # maintainability-wise but this is to be solved separately. 

1026 sslcontext = cast(ssl.SSLContext, self._get_ssl_context(req)) 

1027 

1028 try: 

1029 async with ceil_timeout( 

1030 timeout.sock_connect, ceil_threshold=timeout.ceil_threshold 

1031 ): 

1032 try: 

1033 tls_transport = await self._loop.start_tls( 

1034 underlying_transport, 

1035 tls_proto, 

1036 sslcontext, 

1037 server_hostname=req.server_hostname or req.host, 

1038 ssl_handshake_timeout=timeout.total, 

1039 ) 

1040 except BaseException: 

1041 # We need to close the underlying transport since 

1042 # `start_tls()` probably failed before it had a 

1043 # chance to do this: 

1044 underlying_transport.close() 

1045 raise 

1046 except cert_errors as exc: 

1047 raise ClientConnectorCertificateError(req.connection_key, exc) from exc 

1048 except ssl_errors as exc: 

1049 raise ClientConnectorSSLError(req.connection_key, exc) from exc 

1050 except OSError as exc: 

1051 if exc.errno is None and isinstance(exc, asyncio.TimeoutError): 

1052 raise 

1053 raise client_error(req.connection_key, exc) from exc 

1054 except TypeError as type_err: 

1055 # Example cause looks like this: 

1056 # TypeError: transport <asyncio.sslproto._SSLProtocolTransport 

1057 # object at 0x7f760615e460> is not supported by start_tls() 

1058 

1059 raise ClientConnectionError( 

1060 "Cannot initialize a TLS-in-TLS connection to host " 

1061 f"{req.host!s}:{req.port:d} through an underlying connection " 

1062 f"to an HTTPS proxy {req.proxy!s} ssl:{req.ssl or 'default'} " 

1063 f"[{type_err!s}]" 

1064 ) from type_err 

1065 else: 

1066 if tls_transport is None: 

1067 msg = "Failed to start TLS (possibly caused by closing transport)" 

1068 raise client_error(req.connection_key, OSError(msg)) 

1069 tls_proto.connection_made( 

1070 tls_transport 

1071 ) # Kick the state machine of the new TLS protocol 

1072 

1073 return tls_transport, tls_proto 

1074 

1075 async def _create_direct_connection( 

1076 self, 

1077 req: ClientRequest, 

1078 traces: List["Trace"], 

1079 timeout: "ClientTimeout", 

1080 *, 

1081 client_error: Type[Exception] = ClientConnectorError, 

1082 ) -> Tuple[asyncio.Transport, ResponseHandler]: 

1083 sslcontext = self._get_ssl_context(req) 

1084 fingerprint = self._get_fingerprint(req) 

1085 

1086 host = req.url.raw_host 

1087 assert host is not None 

1088 # Replace multiple trailing dots with a single one. 

1089 # A trailing dot is only present for fully-qualified domain names. 

1090 # See https://github.com/aio-libs/aiohttp/pull/7364. 

1091 if host.endswith(".."): 

1092 host = host.rstrip(".") + "." 

1093 port = req.port 

1094 assert port is not None 

1095 host_resolved = asyncio.ensure_future( 

1096 self._resolve_host(host, port, traces=traces), loop=self._loop 

1097 ) 

1098 try: 

1099 # Cancelling this lookup should not cancel the underlying lookup 

1100 # or else the cancel event will get broadcast to all the waiters 

1101 # across all connections. 

1102 hosts = await asyncio.shield(host_resolved) 

1103 except asyncio.CancelledError: 

1104 

1105 def drop_exception(fut: "asyncio.Future[List[Dict[str, Any]]]") -> None: 

1106 with suppress(Exception, asyncio.CancelledError): 

1107 fut.result() 

1108 

1109 host_resolved.add_done_callback(drop_exception) 

1110 raise 

1111 except OSError as exc: 

1112 if exc.errno is None and isinstance(exc, asyncio.TimeoutError): 

1113 raise 

1114 # in case of proxy it is not ClientProxyConnectionError 

1115 # it is problem of resolving proxy ip itself 

1116 raise ClientConnectorError(req.connection_key, exc) from exc 

1117 

1118 last_exc: Optional[Exception] = None 

1119 

1120 for hinfo in hosts: 

1121 host = hinfo["host"] 

1122 port = hinfo["port"] 

1123 

1124 # Strip trailing dots, certificates contain FQDN without dots. 

1125 # See https://github.com/aio-libs/aiohttp/issues/3636 

1126 server_hostname = ( 

1127 (req.server_hostname or hinfo["hostname"]).rstrip(".") 

1128 if sslcontext 

1129 else None 

1130 ) 

1131 

1132 try: 

1133 transp, proto = await self._wrap_create_connection( 

1134 self._factory, 

1135 host, 

1136 port, 

1137 timeout=timeout, 

1138 ssl=sslcontext, 

1139 family=hinfo["family"], 

1140 proto=hinfo["proto"], 

1141 flags=hinfo["flags"], 

1142 server_hostname=server_hostname, 

1143 local_addr=self._local_addr, 

1144 req=req, 

1145 client_error=client_error, 

1146 ) 

1147 except ClientConnectorError as exc: 

1148 last_exc = exc 

1149 continue 

1150 

1151 if req.is_ssl() and fingerprint: 

1152 try: 

1153 fingerprint.check(transp) 

1154 except ServerFingerprintMismatch as exc: 

1155 transp.close() 

1156 if not self._cleanup_closed_disabled: 

1157 self._cleanup_closed_transports.append(transp) 

1158 last_exc = exc 

1159 continue 

1160 

1161 return transp, proto 

1162 assert last_exc is not None 

1163 raise last_exc 

1164 

1165 async def _create_proxy_connection( 

1166 self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" 

1167 ) -> Tuple[asyncio.BaseTransport, ResponseHandler]: 

1168 headers: Dict[str, str] = {} 

1169 if req.proxy_headers is not None: 

1170 headers = req.proxy_headers # type: ignore[assignment] 

1171 headers[hdrs.HOST] = req.headers[hdrs.HOST] 

1172 

1173 url = req.proxy 

1174 assert url is not None 

1175 proxy_req = ClientRequest( 

1176 hdrs.METH_GET, 

1177 url, 

1178 headers=headers, 

1179 auth=req.proxy_auth, 

1180 loop=self._loop, 

1181 ssl=req.ssl, 

1182 ) 

1183 

1184 # create connection to proxy server 

1185 transport, proto = await self._create_direct_connection( 

1186 proxy_req, [], timeout, client_error=ClientProxyConnectionError 

1187 ) 

1188 

1189 # Many HTTP proxies has buggy keepalive support. Let's not 

1190 # reuse connection but close it after processing every 

1191 # response. 

1192 proto.force_close() 

1193 

1194 auth = proxy_req.headers.pop(hdrs.AUTHORIZATION, None) 

1195 if auth is not None: 

1196 if not req.is_ssl(): 

1197 req.headers[hdrs.PROXY_AUTHORIZATION] = auth 

1198 else: 

1199 proxy_req.headers[hdrs.PROXY_AUTHORIZATION] = auth 

1200 

1201 if req.is_ssl(): 

1202 self._warn_about_tls_in_tls(transport, req) 

1203 

1204 # For HTTPS requests over HTTP proxy 

1205 # we must notify proxy to tunnel connection 

1206 # so we send CONNECT command: 

1207 # CONNECT www.python.org:443 HTTP/1.1 

1208 # Host: www.python.org 

1209 # 

1210 # next we must do TLS handshake and so on 

1211 # to do this we must wrap raw socket into secure one 

1212 # asyncio handles this perfectly 

1213 proxy_req.method = hdrs.METH_CONNECT 

1214 proxy_req.url = req.url 

1215 key = dataclasses.replace( 

1216 req.connection_key, proxy=None, proxy_auth=None, proxy_headers_hash=None 

1217 ) 

1218 conn = Connection(self, key, proto, self._loop) 

1219 proxy_resp = await proxy_req.send(conn) 

1220 try: 

1221 protocol = conn._protocol 

1222 assert protocol is not None 

1223 

1224 # read_until_eof=True will ensure the connection isn't closed 

1225 # once the response is received and processed allowing 

1226 # START_TLS to work on the connection below. 

1227 protocol.set_response_params( 

1228 read_until_eof=True, 

1229 timeout_ceil_threshold=self._timeout_ceil_threshold, 

1230 ) 

1231 resp = await proxy_resp.start(conn) 

1232 except BaseException: 

1233 proxy_resp.close() 

1234 conn.close() 

1235 raise 

1236 else: 

1237 conn._protocol = None 

1238 conn._transport = None 

1239 try: 

1240 if resp.status != 200: 

1241 message = resp.reason 

1242 if message is None: 

1243 message = HTTPStatus(resp.status).phrase 

1244 raise ClientHttpProxyError( 

1245 proxy_resp.request_info, 

1246 resp.history, 

1247 status=resp.status, 

1248 message=message, 

1249 headers=resp.headers, 

1250 ) 

1251 except BaseException: 

1252 # It shouldn't be closed in `finally` because it's fed to 

1253 # `loop.start_tls()` and the docs say not to touch it after 

1254 # passing there. 

1255 transport.close() 

1256 raise 

1257 

1258 return await self._start_tls_connection( 

1259 # Access the old transport for the last time before it's 

1260 # closed and forgotten forever: 

1261 transport, 

1262 req=req, 

1263 timeout=timeout, 

1264 ) 

1265 finally: 

1266 proxy_resp.close() 

1267 

1268 return transport, proto 

1269 

1270 

1271class UnixConnector(BaseConnector): 

1272 """Unix socket connector. 

1273 

1274 path - Unix socket path. 

1275 keepalive_timeout - (optional) Keep-alive timeout. 

1276 force_close - Set to True to force close and do reconnect 

1277 after each request (and between redirects). 

1278 limit - The total number of simultaneous connections. 

1279 limit_per_host - Number of simultaneous connections to one host. 

1280 loop - Optional event loop. 

1281 """ 

1282 

1283 def __init__( 

1284 self, 

1285 path: str, 

1286 force_close: bool = False, 

1287 keepalive_timeout: Union[_SENTINEL, float, None] = sentinel, 

1288 limit: int = 100, 

1289 limit_per_host: int = 0, 

1290 ) -> None: 

1291 super().__init__( 

1292 force_close=force_close, 

1293 keepalive_timeout=keepalive_timeout, 

1294 limit=limit, 

1295 limit_per_host=limit_per_host, 

1296 ) 

1297 self._path = path 

1298 

1299 @property 

1300 def path(self) -> str: 

1301 """Path to unix socket.""" 

1302 return self._path 

1303 

1304 async def _create_connection( 

1305 self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" 

1306 ) -> ResponseHandler: 

1307 try: 

1308 async with ceil_timeout( 

1309 timeout.sock_connect, ceil_threshold=timeout.ceil_threshold 

1310 ): 

1311 _, proto = await self._loop.create_unix_connection( 

1312 self._factory, self._path 

1313 ) 

1314 except OSError as exc: 

1315 if exc.errno is None and isinstance(exc, asyncio.TimeoutError): 

1316 raise 

1317 raise UnixClientConnectorError(self.path, req.connection_key, exc) from exc 

1318 

1319 return proto 

1320 

1321 

1322class NamedPipeConnector(BaseConnector): 

1323 """Named pipe connector. 

1324 

1325 Only supported by the proactor event loop. 

1326 See also: https://docs.python.org/3/library/asyncio-eventloop.html 

1327 

1328 path - Windows named pipe path. 

1329 keepalive_timeout - (optional) Keep-alive timeout. 

1330 force_close - Set to True to force close and do reconnect 

1331 after each request (and between redirects). 

1332 limit - The total number of simultaneous connections. 

1333 limit_per_host - Number of simultaneous connections to one host. 

1334 loop - Optional event loop. 

1335 """ 

1336 

1337 def __init__( 

1338 self, 

1339 path: str, 

1340 force_close: bool = False, 

1341 keepalive_timeout: Union[_SENTINEL, float, None] = sentinel, 

1342 limit: int = 100, 

1343 limit_per_host: int = 0, 

1344 ) -> None: 

1345 super().__init__( 

1346 force_close=force_close, 

1347 keepalive_timeout=keepalive_timeout, 

1348 limit=limit, 

1349 limit_per_host=limit_per_host, 

1350 ) 

1351 if not isinstance( 

1352 self._loop, asyncio.ProactorEventLoop # type: ignore[attr-defined] 

1353 ): 

1354 raise RuntimeError( 

1355 "Named Pipes only available in proactor " "loop under windows" 

1356 ) 

1357 self._path = path 

1358 

1359 @property 

1360 def path(self) -> str: 

1361 """Path to the named pipe.""" 

1362 return self._path 

1363 

1364 async def _create_connection( 

1365 self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" 

1366 ) -> ResponseHandler: 

1367 try: 

1368 async with ceil_timeout( 

1369 timeout.sock_connect, ceil_threshold=timeout.ceil_threshold 

1370 ): 

1371 _, proto = await self._loop.create_pipe_connection( # type: ignore[attr-defined] 

1372 self._factory, self._path 

1373 ) 

1374 # the drain is required so that the connection_made is called 

1375 # and transport is set otherwise it is not set before the 

1376 # `assert conn.transport is not None` 

1377 # in client.py's _request method 

1378 await asyncio.sleep(0) 

1379 # other option is to manually set transport like 

1380 # `proto.transport = trans` 

1381 except OSError as exc: 

1382 if exc.errno is None and isinstance(exc, asyncio.TimeoutError): 

1383 raise 

1384 raise ClientConnectorError(req.connection_key, exc) from exc 

1385 

1386 return cast(ResponseHandler, proto)