|
51 | 51 | ClientSSLError as ClientSSLError,
|
52 | 52 | ContentTypeError as ContentTypeError,
|
53 | 53 | InvalidURL as InvalidURL,
|
| 54 | + InvalidUrlClientError as InvalidUrlClientError, |
| 55 | + InvalidUrlRedirectClientError as InvalidUrlRedirectClientError, |
| 56 | + NonHttpUrlClientError as NonHttpUrlClientError, |
| 57 | + NonHttpUrlRedirectClientError as NonHttpUrlRedirectClientError, |
| 58 | + RedirectClientError as RedirectClientError, |
54 | 59 | ServerConnectionError as ServerConnectionError,
|
55 | 60 | ServerDisconnectedError as ServerDisconnectedError,
|
56 | 61 | ServerFingerprintMismatch as ServerFingerprintMismatch,
|
|
106 | 111 | "ClientSSLError",
|
107 | 112 | "ContentTypeError",
|
108 | 113 | "InvalidURL",
|
| 114 | + "InvalidUrlClientError", |
| 115 | + "RedirectClientError", |
| 116 | + "NonHttpUrlClientError", |
| 117 | + "InvalidUrlRedirectClientError", |
| 118 | + "NonHttpUrlRedirectClientError", |
109 | 119 | "ServerConnectionError",
|
110 | 120 | "ServerDisconnectedError",
|
111 | 121 | "ServerFingerprintMismatch",
|
@@ -162,6 +172,10 @@ class ClientTimeout:
|
162 | 172 | # 5 Minute default read timeout
|
163 | 173 | DEFAULT_TIMEOUT: Final[ClientTimeout] = ClientTimeout(total=5 * 60)
|
164 | 174 |
|
| 175 | +# https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2 |
| 176 | +IDEMPOTENT_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "TRACE", "PUT", "DELETE"}) |
| 177 | +HTTP_SCHEMA_SET = frozenset({"http", "https", ""}) |
| 178 | + |
165 | 179 | _RetType = TypeVar("_RetType")
|
166 | 180 | _CharsetResolver = Callable[[ClientResponse, bytes], str]
|
167 | 181 |
|
@@ -448,7 +462,10 @@ async def _request(
|
448 | 462 | try:
|
449 | 463 | url = self._build_url(str_or_url)
|
450 | 464 | except ValueError as e:
|
451 |
| - raise InvalidURL(str_or_url) from e |
| 465 | + raise InvalidUrlClientError(str_or_url) from e |
| 466 | + |
| 467 | + if url.scheme not in HTTP_SCHEMA_SET: |
| 468 | + raise NonHttpUrlClientError(url) |
452 | 469 |
|
453 | 470 | skip_headers = set(self._skip_auto_headers)
|
454 | 471 | if skip_auto_headers is not None:
|
@@ -504,6 +521,15 @@ async def _request(
|
504 | 521 | with timer:
|
505 | 522 | while True:
|
506 | 523 | url, auth_from_url = strip_auth_from_url(url)
|
| 524 | + if not url.raw_host: |
| 525 | + # NOTE: Bail early, otherwise, causes `InvalidURL` through |
| 526 | + # NOTE: `self._request_class()` below. |
| 527 | + err_exc_cls = ( |
| 528 | + InvalidUrlRedirectClientError |
| 529 | + if redirects |
| 530 | + else InvalidUrlClientError |
| 531 | + ) |
| 532 | + raise err_exc_cls(url) |
507 | 533 | if auth and auth_from_url:
|
508 | 534 | raise ValueError(
|
509 | 535 | "Cannot combine AUTH argument with "
|
@@ -656,25 +682,44 @@ async def _request(
|
656 | 682 | resp.release()
|
657 | 683 |
|
658 | 684 | try:
|
659 |
| - parsed_url = URL( |
| 685 | + parsed_redirect_url = URL( |
660 | 686 | r_url, encoded=not self._requote_redirect_url
|
661 | 687 | )
|
662 |
| - |
663 | 688 | except ValueError as e:
|
664 |
| - raise InvalidURL(r_url) from e |
| 689 | + raise InvalidUrlRedirectClientError( |
| 690 | + r_url, |
| 691 | + "Server attempted redirecting to a location that does not look like a URL", |
| 692 | + ) from e |
665 | 693 |
|
666 |
| - scheme = parsed_url.scheme |
667 |
| - if scheme not in ("http", "https", ""): |
| 694 | + scheme = parsed_redirect_url.scheme |
| 695 | + if scheme not in HTTP_SCHEMA_SET: |
668 | 696 | resp.close()
|
669 |
| - raise ValueError("Can redirect only to http or https") |
| 697 | + raise NonHttpUrlRedirectClientError(r_url) |
670 | 698 | elif not scheme:
|
671 |
| - parsed_url = url.join(parsed_url) |
| 699 | + parsed_redirect_url = url.join(parsed_redirect_url) |
672 | 700 |
|
673 |
| - if url.origin() != parsed_url.origin(): |
| 701 | + is_same_host_https_redirect = ( |
| 702 | + url.host == parsed_redirect_url.host |
| 703 | + and parsed_redirect_url.scheme == "https" |
| 704 | + and url.scheme == "http" |
| 705 | + ) |
| 706 | + |
| 707 | + try: |
| 708 | + redirect_origin = parsed_redirect_url.origin() |
| 709 | + except ValueError as origin_val_err: |
| 710 | + raise InvalidUrlRedirectClientError( |
| 711 | + parsed_redirect_url, |
| 712 | + "Invalid redirect URL origin", |
| 713 | + ) from origin_val_err |
| 714 | + |
| 715 | + if ( |
| 716 | + url.origin() != redirect_origin |
| 717 | + and not is_same_host_https_redirect |
| 718 | + ): |
674 | 719 | auth = None
|
675 | 720 | headers.pop(hdrs.AUTHORIZATION, None)
|
676 | 721 |
|
677 |
| - url = parsed_url |
| 722 | + url = parsed_redirect_url |
678 | 723 | params = {}
|
679 | 724 | resp.release()
|
680 | 725 | continue
|
|
0 commit comments