client.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. """
  2. :mod:`websockets.legacy.client` defines the WebSocket client APIs.
  3. """
  4. import asyncio
  5. import collections.abc
  6. import functools
  7. import logging
  8. import warnings
  9. from types import TracebackType
  10. from typing import Any, Callable, Generator, List, Optional, Sequence, Tuple, Type, cast
  11. from ..datastructures import Headers, HeadersLike
  12. from ..exceptions import (
  13. InvalidHandshake,
  14. InvalidHeader,
  15. InvalidMessage,
  16. InvalidStatusCode,
  17. NegotiationError,
  18. RedirectHandshake,
  19. SecurityError,
  20. )
  21. from ..extensions.base import ClientExtensionFactory, Extension
  22. from ..extensions.permessage_deflate import enable_client_permessage_deflate
  23. from ..headers import (
  24. build_authorization_basic,
  25. build_extension,
  26. build_subprotocol,
  27. parse_extension,
  28. parse_subprotocol,
  29. )
  30. from ..http import USER_AGENT, build_host
  31. from ..typing import ExtensionHeader, Origin, Subprotocol
  32. from ..uri import WebSocketURI, parse_uri
  33. from .handshake import build_request, check_response
  34. from .http import read_response
  35. from .protocol import WebSocketCommonProtocol
  36. __all__ = ["connect", "unix_connect", "WebSocketClientProtocol"]
  37. logger = logging.getLogger("websockets.server")
  38. class WebSocketClientProtocol(WebSocketCommonProtocol):
  39. """
  40. :class:`~asyncio.Protocol` subclass implementing a WebSocket client.
  41. :class:`WebSocketClientProtocol`:
  42. * performs the opening handshake to establish the connection;
  43. * provides :meth:`recv` and :meth:`send` coroutines for receiving and
  44. sending messages;
  45. * deals with control frames automatically;
  46. * performs the closing handshake to terminate the connection.
  47. :class:`WebSocketClientProtocol` supports asynchronous iteration::
  48. async for message in websocket:
  49. await process(message)
  50. The iterator yields incoming messages. It exits normally when the
  51. connection is closed with the close code 1000 (OK) or 1001 (going away).
  52. It raises a :exc:`~websockets.exceptions.ConnectionClosedError` exception
  53. when the connection is closed with any other code.
  54. Once the connection is open, a `Ping frame`_ is sent every
  55. ``ping_interval`` seconds. This serves as a keepalive. It helps keeping
  56. the connection open, especially in the presence of proxies with short
  57. timeouts on inactive connections. Set ``ping_interval`` to ``None`` to
  58. disable this behavior.
  59. .. _Ping frame: https://tools.ietf.org/html/rfc6455#section-5.5.2
  60. If the corresponding `Pong frame`_ isn't received within ``ping_timeout``
  61. seconds, the connection is considered unusable and is closed with
  62. code 1011. This ensures that the remote endpoint remains responsive. Set
  63. ``ping_timeout`` to ``None`` to disable this behavior.
  64. .. _Pong frame: https://tools.ietf.org/html/rfc6455#section-5.5.3
  65. The ``close_timeout`` parameter defines a maximum wait time for completing
  66. the closing handshake and terminating the TCP connection. For legacy
  67. reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds.
  68. ``close_timeout`` needs to be a parameter of the protocol because
  69. websockets usually calls :meth:`close` implicitly upon exit when
  70. :func:`connect` is used as a context manager.
  71. To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`.
  72. The ``max_size`` parameter enforces the maximum size for incoming messages
  73. in bytes. The default value is 1 MiB. ``None`` disables the limit. If a
  74. message larger than the maximum size is received, :meth:`recv` will
  75. raise :exc:`~websockets.exceptions.ConnectionClosedError` and the
  76. connection will be closed with code 1009.
  77. The ``max_queue`` parameter sets the maximum length of the queue that
  78. holds incoming messages. The default value is ``32``. ``None`` disables
  79. the limit. Messages are added to an in-memory queue when they're received;
  80. then :meth:`recv` pops from that queue. In order to prevent excessive
  81. memory consumption when messages are received faster than they can be
  82. processed, the queue must be bounded. If the queue fills up, the protocol
  83. stops processing incoming data until :meth:`recv` is called. In this
  84. situation, various receive buffers (at least in :mod:`asyncio` and in the
  85. OS) will fill up, then the TCP receive window will shrink, slowing down
  86. transmission to avoid packet loss.
  87. Since Python can use up to 4 bytes of memory to represent a single
  88. character, each connection may use up to ``4 * max_size * max_queue``
  89. bytes of memory to store incoming messages. By default, this is 128 MiB.
  90. You may want to lower the limits, depending on your application's
  91. requirements.
  92. The ``read_limit`` argument sets the high-water limit of the buffer for
  93. incoming bytes. The low-water limit is half the high-water limit. The
  94. default value is 64 KiB, half of asyncio's default (based on the current
  95. implementation of :class:`~asyncio.StreamReader`).
  96. The ``write_limit`` argument sets the high-water limit of the buffer for
  97. outgoing bytes. The low-water limit is a quarter of the high-water limit.
  98. The default value is 64 KiB, equal to asyncio's default (based on the
  99. current implementation of ``FlowControlMixin``).
  100. As soon as the HTTP request and response in the opening handshake are
  101. processed:
  102. * the request path is available in the :attr:`path` attribute;
  103. * the request and response HTTP headers are available in the
  104. :attr:`request_headers` and :attr:`response_headers` attributes,
  105. which are :class:`~websockets.http.Headers` instances.
  106. If a subprotocol was negotiated, it's available in the :attr:`subprotocol`
  107. attribute.
  108. Once the connection is closed, the code is available in the
  109. :attr:`close_code` attribute and the reason in :attr:`close_reason`.
  110. All attributes must be treated as read-only.
  111. """
  112. is_client = True
  113. side = "client"
  114. def __init__(
  115. self,
  116. *,
  117. origin: Optional[Origin] = None,
  118. extensions: Optional[Sequence[ClientExtensionFactory]] = None,
  119. subprotocols: Optional[Sequence[Subprotocol]] = None,
  120. extra_headers: Optional[HeadersLike] = None,
  121. **kwargs: Any,
  122. ) -> None:
  123. self.origin = origin
  124. self.available_extensions = extensions
  125. self.available_subprotocols = subprotocols
  126. self.extra_headers = extra_headers
  127. super().__init__(**kwargs)
  128. def write_http_request(self, path: str, headers: Headers) -> None:
  129. """
  130. Write request line and headers to the HTTP request.
  131. """
  132. self.path = path
  133. self.request_headers = headers
  134. logger.debug("%s > GET %s HTTP/1.1", self.side, path)
  135. logger.debug("%s > %r", self.side, headers)
  136. # Since the path and headers only contain ASCII characters,
  137. # we can keep this simple.
  138. request = f"GET {path} HTTP/1.1\r\n"
  139. request += str(headers)
  140. self.transport.write(request.encode())
  141. async def read_http_response(self) -> Tuple[int, Headers]:
  142. """
  143. Read status line and headers from the HTTP response.
  144. If the response contains a body, it may be read from ``self.reader``
  145. after this coroutine returns.
  146. :raises ~websockets.exceptions.InvalidMessage: if the HTTP message is
  147. malformed or isn't an HTTP/1.1 GET response
  148. """
  149. try:
  150. status_code, reason, headers = await read_response(self.reader)
  151. # Remove this branch when dropping support for Python < 3.8
  152. # because CancelledError no longer inherits Exception.
  153. except asyncio.CancelledError: # pragma: no cover
  154. raise
  155. except Exception as exc:
  156. raise InvalidMessage("did not receive a valid HTTP response") from exc
  157. logger.debug("%s < HTTP/1.1 %d %s", self.side, status_code, reason)
  158. logger.debug("%s < %r", self.side, headers)
  159. self.response_headers = headers
  160. return status_code, self.response_headers
  161. @staticmethod
  162. def process_extensions(
  163. headers: Headers,
  164. available_extensions: Optional[Sequence[ClientExtensionFactory]],
  165. ) -> List[Extension]:
  166. """
  167. Handle the Sec-WebSocket-Extensions HTTP response header.
  168. Check that each extension is supported, as well as its parameters.
  169. Return the list of accepted extensions.
  170. Raise :exc:`~websockets.exceptions.InvalidHandshake` to abort the
  171. connection.
  172. :rfc:`6455` leaves the rules up to the specification of each
  173. :extension.
  174. To provide this level of flexibility, for each extension accepted by
  175. the server, we check for a match with each extension available in the
  176. client configuration. If no match is found, an exception is raised.
  177. If several variants of the same extension are accepted by the server,
  178. it may be configured several times, which won't make sense in general.
  179. Extensions must implement their own requirements. For this purpose,
  180. the list of previously accepted extensions is provided.
  181. Other requirements, for example related to mandatory extensions or the
  182. order of extensions, may be implemented by overriding this method.
  183. """
  184. accepted_extensions: List[Extension] = []
  185. header_values = headers.get_all("Sec-WebSocket-Extensions")
  186. if header_values:
  187. if available_extensions is None:
  188. raise InvalidHandshake("no extensions supported")
  189. parsed_header_values: List[ExtensionHeader] = sum(
  190. [parse_extension(header_value) for header_value in header_values], []
  191. )
  192. for name, response_params in parsed_header_values:
  193. for extension_factory in available_extensions:
  194. # Skip non-matching extensions based on their name.
  195. if extension_factory.name != name:
  196. continue
  197. # Skip non-matching extensions based on their params.
  198. try:
  199. extension = extension_factory.process_response_params(
  200. response_params, accepted_extensions
  201. )
  202. except NegotiationError:
  203. continue
  204. # Add matching extension to the final list.
  205. accepted_extensions.append(extension)
  206. # Break out of the loop once we have a match.
  207. break
  208. # If we didn't break from the loop, no extension in our list
  209. # matched what the server sent. Fail the connection.
  210. else:
  211. raise NegotiationError(
  212. f"Unsupported extension: "
  213. f"name = {name}, params = {response_params}"
  214. )
  215. return accepted_extensions
  216. @staticmethod
  217. def process_subprotocol(
  218. headers: Headers, available_subprotocols: Optional[Sequence[Subprotocol]]
  219. ) -> Optional[Subprotocol]:
  220. """
  221. Handle the Sec-WebSocket-Protocol HTTP response header.
  222. Check that it contains exactly one supported subprotocol.
  223. Return the selected subprotocol.
  224. """
  225. subprotocol: Optional[Subprotocol] = None
  226. header_values = headers.get_all("Sec-WebSocket-Protocol")
  227. if header_values:
  228. if available_subprotocols is None:
  229. raise InvalidHandshake("no subprotocols supported")
  230. parsed_header_values: Sequence[Subprotocol] = sum(
  231. [parse_subprotocol(header_value) for header_value in header_values], []
  232. )
  233. if len(parsed_header_values) > 1:
  234. subprotocols = ", ".join(parsed_header_values)
  235. raise InvalidHandshake(f"multiple subprotocols: {subprotocols}")
  236. subprotocol = parsed_header_values[0]
  237. if subprotocol not in available_subprotocols:
  238. raise NegotiationError(f"unsupported subprotocol: {subprotocol}")
  239. return subprotocol
  240. async def handshake(
  241. self,
  242. wsuri: WebSocketURI,
  243. origin: Optional[Origin] = None,
  244. available_extensions: Optional[Sequence[ClientExtensionFactory]] = None,
  245. available_subprotocols: Optional[Sequence[Subprotocol]] = None,
  246. extra_headers: Optional[HeadersLike] = None,
  247. ) -> None:
  248. """
  249. Perform the client side of the opening handshake.
  250. :param origin: sets the Origin HTTP header
  251. :param available_extensions: list of supported extensions in the order
  252. in which they should be used
  253. :param available_subprotocols: list of supported subprotocols in order
  254. of decreasing preference
  255. :param extra_headers: sets additional HTTP request headers; it must be
  256. a :class:`~websockets.http.Headers` instance, a
  257. :class:`~collections.abc.Mapping`, or an iterable of ``(name,
  258. value)`` pairs
  259. :raises ~websockets.exceptions.InvalidHandshake: if the handshake
  260. fails
  261. """
  262. request_headers = Headers()
  263. request_headers["Host"] = build_host(wsuri.host, wsuri.port, wsuri.secure)
  264. if wsuri.user_info:
  265. request_headers["Authorization"] = build_authorization_basic(
  266. *wsuri.user_info
  267. )
  268. if origin is not None:
  269. request_headers["Origin"] = origin
  270. key = build_request(request_headers)
  271. if available_extensions is not None:
  272. extensions_header = build_extension(
  273. [
  274. (extension_factory.name, extension_factory.get_request_params())
  275. for extension_factory in available_extensions
  276. ]
  277. )
  278. request_headers["Sec-WebSocket-Extensions"] = extensions_header
  279. if available_subprotocols is not None:
  280. protocol_header = build_subprotocol(available_subprotocols)
  281. request_headers["Sec-WebSocket-Protocol"] = protocol_header
  282. if extra_headers is not None:
  283. if isinstance(extra_headers, Headers):
  284. extra_headers = extra_headers.raw_items()
  285. elif isinstance(extra_headers, collections.abc.Mapping):
  286. extra_headers = extra_headers.items()
  287. for name, value in extra_headers:
  288. request_headers[name] = value
  289. request_headers.setdefault("User-Agent", USER_AGENT)
  290. self.write_http_request(wsuri.resource_name, request_headers)
  291. status_code, response_headers = await self.read_http_response()
  292. if status_code in (301, 302, 303, 307, 308):
  293. if "Location" not in response_headers:
  294. raise InvalidHeader("Location")
  295. raise RedirectHandshake(response_headers["Location"])
  296. elif status_code != 101:
  297. raise InvalidStatusCode(status_code)
  298. check_response(response_headers, key)
  299. self.extensions = self.process_extensions(
  300. response_headers, available_extensions
  301. )
  302. self.subprotocol = self.process_subprotocol(
  303. response_headers, available_subprotocols
  304. )
  305. self.connection_open()
  306. class Connect:
  307. """
  308. Connect to the WebSocket server at the given ``uri``.
  309. Awaiting :func:`connect` yields a :class:`WebSocketClientProtocol` which
  310. can then be used to send and receive messages.
  311. :func:`connect` can also be used as a asynchronous context manager::
  312. async with connect(...) as websocket:
  313. ...
  314. In that case, the connection is closed when exiting the context.
  315. :func:`connect` is a wrapper around the event loop's
  316. :meth:`~asyncio.loop.create_connection` method. Unknown keyword arguments
  317. are passed to :meth:`~asyncio.loop.create_connection`.
  318. For example, you can set the ``ssl`` keyword argument to a
  319. :class:`~ssl.SSLContext` to enforce some TLS settings. When connecting to
  320. a ``wss://`` URI, if this argument isn't provided explicitly,
  321. :func:`ssl.create_default_context` is called to create a context.
  322. You can connect to a different host and port from those found in ``uri``
  323. by setting ``host`` and ``port`` keyword arguments. This only changes the
  324. destination of the TCP connection. The host name from ``uri`` is still
  325. used in the TLS handshake for secure connections and in the ``Host`` HTTP
  326. header.
  327. ``create_protocol`` defaults to :class:`WebSocketClientProtocol`. It may
  328. be replaced by a wrapper or a subclass to customize the protocol that
  329. manages the connection.
  330. The behavior of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
  331. ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit`` is
  332. described in :class:`WebSocketClientProtocol`.
  333. :func:`connect` also accepts the following optional arguments:
  334. * ``compression`` is a shortcut to configure compression extensions;
  335. by default it enables the "permessage-deflate" extension; set it to
  336. ``None`` to disable compression.
  337. * ``origin`` sets the Origin HTTP header.
  338. * ``extensions`` is a list of supported extensions in order of
  339. decreasing preference.
  340. * ``subprotocols`` is a list of supported subprotocols in order of
  341. decreasing preference.
  342. * ``extra_headers`` sets additional HTTP request headers; it can be a
  343. :class:`~websockets.http.Headers` instance, a
  344. :class:`~collections.abc.Mapping`, or an iterable of ``(name, value)``
  345. pairs.
  346. :raises ~websockets.uri.InvalidURI: if ``uri`` is invalid
  347. :raises ~websockets.handshake.InvalidHandshake: if the opening handshake
  348. fails
  349. """
  350. MAX_REDIRECTS_ALLOWED = 10
  351. def __init__(
  352. self,
  353. uri: str,
  354. *,
  355. create_protocol: Optional[Callable[[Any], WebSocketClientProtocol]] = None,
  356. ping_interval: Optional[float] = 20,
  357. ping_timeout: Optional[float] = 20,
  358. close_timeout: Optional[float] = None,
  359. max_size: Optional[int] = 2 ** 20,
  360. max_queue: Optional[int] = 2 ** 5,
  361. read_limit: int = 2 ** 16,
  362. write_limit: int = 2 ** 16,
  363. loop: Optional[asyncio.AbstractEventLoop] = None,
  364. compression: Optional[str] = "deflate",
  365. origin: Optional[Origin] = None,
  366. extensions: Optional[Sequence[ClientExtensionFactory]] = None,
  367. subprotocols: Optional[Sequence[Subprotocol]] = None,
  368. extra_headers: Optional[HeadersLike] = None,
  369. **kwargs: Any,
  370. ) -> None:
  371. # Backwards compatibility: close_timeout used to be called timeout.
  372. timeout: Optional[float] = kwargs.pop("timeout", None)
  373. if timeout is None:
  374. timeout = 10
  375. else:
  376. warnings.warn("rename timeout to close_timeout", DeprecationWarning)
  377. # If both are specified, timeout is ignored.
  378. if close_timeout is None:
  379. close_timeout = timeout
  380. # Backwards compatibility: create_protocol used to be called klass.
  381. klass: Optional[Type[WebSocketClientProtocol]] = kwargs.pop("klass", None)
  382. if klass is None:
  383. klass = WebSocketClientProtocol
  384. else:
  385. warnings.warn("rename klass to create_protocol", DeprecationWarning)
  386. # If both are specified, klass is ignored.
  387. if create_protocol is None:
  388. create_protocol = klass
  389. # Backwards compatibility: recv() used to return None on closed connections
  390. legacy_recv: bool = kwargs.pop("legacy_recv", False)
  391. if loop is None:
  392. loop = asyncio.get_event_loop()
  393. wsuri = parse_uri(uri)
  394. if wsuri.secure:
  395. kwargs.setdefault("ssl", True)
  396. elif kwargs.get("ssl") is not None:
  397. raise ValueError(
  398. "connect() received a ssl argument for a ws:// URI, "
  399. "use a wss:// URI to enable TLS"
  400. )
  401. if compression == "deflate":
  402. extensions = enable_client_permessage_deflate(extensions)
  403. elif compression is not None:
  404. raise ValueError(f"unsupported compression: {compression}")
  405. factory = functools.partial(
  406. create_protocol,
  407. ping_interval=ping_interval,
  408. ping_timeout=ping_timeout,
  409. close_timeout=close_timeout,
  410. max_size=max_size,
  411. max_queue=max_queue,
  412. read_limit=read_limit,
  413. write_limit=write_limit,
  414. loop=loop,
  415. host=wsuri.host,
  416. port=wsuri.port,
  417. secure=wsuri.secure,
  418. legacy_recv=legacy_recv,
  419. origin=origin,
  420. extensions=extensions,
  421. subprotocols=subprotocols,
  422. extra_headers=extra_headers,
  423. )
  424. if kwargs.pop("unix", False):
  425. path: Optional[str] = kwargs.pop("path", None)
  426. create_connection = functools.partial(
  427. loop.create_unix_connection, factory, path, **kwargs
  428. )
  429. else:
  430. host: Optional[str]
  431. port: Optional[int]
  432. if kwargs.get("sock") is None:
  433. host, port = wsuri.host, wsuri.port
  434. else:
  435. # If sock is given, host and port shouldn't be specified.
  436. host, port = None, None
  437. # If host and port are given, override values from the URI.
  438. host = kwargs.pop("host", host)
  439. port = kwargs.pop("port", port)
  440. create_connection = functools.partial(
  441. loop.create_connection, factory, host, port, **kwargs
  442. )
  443. # This is a coroutine function.
  444. self._create_connection = create_connection
  445. self._wsuri = wsuri
  446. def handle_redirect(self, uri: str) -> None:
  447. # Update the state of this instance to connect to a new URI.
  448. old_wsuri = self._wsuri
  449. new_wsuri = parse_uri(uri)
  450. # Forbid TLS downgrade.
  451. if old_wsuri.secure and not new_wsuri.secure:
  452. raise SecurityError("redirect from WSS to WS")
  453. same_origin = (
  454. old_wsuri.host == new_wsuri.host and old_wsuri.port == new_wsuri.port
  455. )
  456. # Rewrite the host and port arguments for cross-origin redirects.
  457. # This preserves connection overrides with the host and port
  458. # arguments if the redirect points to the same host and port.
  459. if not same_origin:
  460. # Replace the host and port argument passed to the protocol factory.
  461. factory = self._create_connection.args[0]
  462. factory = functools.partial(
  463. factory.func,
  464. *factory.args,
  465. **dict(factory.keywords, host=new_wsuri.host, port=new_wsuri.port),
  466. )
  467. # Replace the host and port argument passed to create_connection.
  468. self._create_connection = functools.partial(
  469. self._create_connection.func,
  470. *(factory, new_wsuri.host, new_wsuri.port),
  471. **self._create_connection.keywords,
  472. )
  473. # Set the new WebSocket URI. This suffices for same-origin redirects.
  474. self._wsuri = new_wsuri
  475. # async with connect(...)
  476. async def __aenter__(self) -> WebSocketClientProtocol:
  477. return await self
  478. async def __aexit__(
  479. self,
  480. exc_type: Optional[Type[BaseException]],
  481. exc_value: Optional[BaseException],
  482. traceback: Optional[TracebackType],
  483. ) -> None:
  484. await self.protocol.close()
  485. # await connect(...)
  486. def __await__(self) -> Generator[Any, None, WebSocketClientProtocol]:
  487. # Create a suitable iterator by calling __await__ on a coroutine.
  488. return self.__await_impl__().__await__()
  489. async def __await_impl__(self) -> WebSocketClientProtocol:
  490. for redirects in range(self.MAX_REDIRECTS_ALLOWED):
  491. transport, protocol = await self._create_connection()
  492. # https://github.com/python/typeshed/pull/2756
  493. transport = cast(asyncio.Transport, transport)
  494. protocol = cast(WebSocketClientProtocol, protocol)
  495. try:
  496. try:
  497. await protocol.handshake(
  498. self._wsuri,
  499. origin=protocol.origin,
  500. available_extensions=protocol.available_extensions,
  501. available_subprotocols=protocol.available_subprotocols,
  502. extra_headers=protocol.extra_headers,
  503. )
  504. except Exception:
  505. protocol.fail_connection()
  506. await protocol.wait_closed()
  507. raise
  508. else:
  509. self.protocol = protocol
  510. return protocol
  511. except RedirectHandshake as exc:
  512. self.handle_redirect(exc.uri)
  513. else:
  514. raise SecurityError("too many redirects")
  515. # yield from connect(...)
  516. __iter__ = __await__
  517. connect = Connect
  518. def unix_connect(
  519. path: Optional[str], uri: str = "ws://localhost/", **kwargs: Any
  520. ) -> Connect:
  521. """
  522. Similar to :func:`connect`, but for connecting to a Unix socket.
  523. This function calls the event loop's
  524. :meth:`~asyncio.loop.create_unix_connection` method.
  525. It is only available on Unix.
  526. It's mainly useful for debugging servers listening on Unix sockets.
  527. :param path: file system path to the Unix socket
  528. :param uri: WebSocket URI
  529. """
  530. return connect(uri=uri, path=path, unix=True, **kwargs)