diff --git a/CHANGELOG.md b/CHANGELOG.md index f18adec8ad..c4b6f8aae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,19 @@ This project adheres to [Semantic Versioning](https://semver.org/). - Added support for specifying the local port with `--local-port`. ([#1456](https://github.com/httpie/cli/issues/1456)) ([#1531](https://github.com/httpie/cli/pull/1531)) - Added support for forcing either IPv4 or IPv6 to reach the remote HTTP server with `-6` or `-4`. ([#94](https://github.com/httpie/cli/issues/94)) ([#1531](https://github.com/httpie/cli/pull/1531)) - Removed support for pyopenssl. ([#1531](https://github.com/httpie/cli/pull/1531)) +- Removed support for dead SSL protocols < TLS 1.0 (e.g. sslv3) as per pyopenssl removal. ([#1531](https://github.com/httpie/cli/pull/1531)) - Dropped dependency on `requests_toolbelt` in favor of directly including `MultipartEncoder` into HTTPie due to its direct dependency to requests. ([#1531](https://github.com/httpie/cli/pull/1531)) - Dropped dependency on `multidict` in favor of implementing an internal one due to often missing pre-built wheels. ([#1522](https://github.com/httpie/cli/issues/1522)) ([#1531](https://github.com/httpie/cli/pull/1531)) - Fixed the case when multiple headers where concatenated in the response output. ([#1413](https://github.com/httpie/cli/issues/1413)) ([#1531](https://github.com/httpie/cli/pull/1531)) - Fixed an edge case where HTTPie could be lead to believe data was passed in stdin, thus sending a POST by default. ([#1551](https://github.com/httpie/cli/issues/1551)) ([#1531](https://github.com/httpie/cli/pull/1531)) + This fix has the particularity to consider 0 byte long stdin buffer as absent stdin. Empty stdin buffer will be ignored. - Slightly improved performance while downloading by setting chunk size to `-1` to retrieve packets as they arrive. ([#1531](https://github.com/httpie/cli/pull/1531)) - Added support for using the system trust store to retrieve root CAs for verifying TLS certificates. ([#1531](https://github.com/httpie/cli/pull/1531)) +- Removed support for keeping the original casing of HTTP headers. This come from an outer constraint by newer protocols, namely HTTP/2+ that normalize header keys by default. + From the HTTPie user perspective, they are "prettified" on the output by default. e.g. "x-hello-world" is displayed as "X-Hello-World". + +The plugins are expected to work without any changes. The only caveat would be that certain plugin explicitly require `requests`. +Future contributions may be made in order to relax the constraints where applicable. ## [3.2.2](https://github.com/httpie/cli/compare/3.2.1...3.2.2) (2022-05-19) diff --git a/httpie/client.py b/httpie/client.py index c05ee746f3..2191cf1291 100644 --- a/httpie/client.py +++ b/httpie/client.py @@ -8,21 +8,10 @@ from urllib.parse import urlparse, urlunparse import niquests -# to understand why this is required -# see https://niquests.readthedocs.io/en/latest/community/faq.html#what-is-urllib3-future -from niquests._compat import HAS_LEGACY_URLLIB3 - -if not HAS_LEGACY_URLLIB3: - # noinspection PyPackageRequirements - import urllib3 - from urllib3.util import SKIP_HEADER, SKIPPABLE_HEADERS, parse_url -else: - # noinspection PyPackageRequirements - import urllib3_future as urllib3 - from urllib3_future.util import SKIP_HEADER, SKIPPABLE_HEADERS, parse_url from . import __version__ from .adapters import HTTPieHTTPAdapter +from .compat import urllib3, SKIP_HEADER, SKIPPABLE_HEADERS, parse_url, Timeout from .cli.constants import HTTP_OPTIONS from .cli.dicts import HTTPHeadersDict from .cli.nested_json import unwrap_top_level_list_if_needed @@ -99,9 +88,20 @@ def collect_messages( source_address=source_address, ) + parsed_url = parse_url(args.url) + if args.disable_http3 is False and args.force_http3 is True: - url = parse_url(args.url) - requests_session.quic_cache_layer[(url.host, url.port or 443)] = (url.host, url.port or 443) + requests_session.quic_cache_layer[(parsed_url.host, parsed_url.port or 443)] = (parsed_url.host, parsed_url.port or 443) + # well, this one is tricky. If we allow HTTP/3, and remote host was marked as QUIC capable + # but is not anymore, we may face an indefinite hang if timeout isn't set. This could surprise some user. + elif ( + args.disable_http3 is False + and requests_session.quic_cache_layer.get((parsed_url.host, parsed_url.port or 443)) is not None + and send_kwargs["timeout"] is None + ): + # we only set the connect timeout, the rest is still indefinite. + send_kwargs["timeout"] = Timeout(connect=3) + setattr(args, "_failsafe_http3", True) if httpie_session: httpie_session.update_headers(request_kwargs['headers']) diff --git a/httpie/compat.py b/httpie/compat.py index fcf167ca7d..7afc490279 100644 --- a/httpie/compat.py +++ b/httpie/compat.py @@ -4,6 +4,23 @@ from httpie.cookies import HTTPieCookiePolicy from http import cookiejar # noqa +from niquests._compat import HAS_LEGACY_URLLIB3 + +# to understand why this is required +# see https://niquests.readthedocs.io/en/latest/community/faq.html#what-is-urllib3-future +# short story, urllib3 (import/top-level import) may be the legacy one https://github.com/urllib3/urllib3 +# instead of urllib3-future https://github.com/jawah/urllib3.future used by Niquests +# or only the secondary entry point could be available (e.g. urllib3_future on some distro without urllib3) +if not HAS_LEGACY_URLLIB3: + # noinspection PyPackageRequirements + import urllib3 # noqa: F401 + from urllib3.util import SKIP_HEADER, SKIPPABLE_HEADERS, parse_url, Timeout # noqa: F401 + from urllib3.fields import RequestField # noqa: F401 +else: + # noinspection PyPackageRequirements + import urllib3_future as urllib3 # noqa: F401 + from urllib3_future.util import SKIP_HEADER, SKIPPABLE_HEADERS, parse_url, Timeout # noqa: F401 + from urllib3_future.fields import RequestField # noqa: F401 # Request does not carry the original policy attached to the # cookie jar, so until it is resolved we change the global cookie diff --git a/httpie/core.py b/httpie/core.py index 070046a67e..2cece5b3c7 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -26,6 +26,7 @@ from .utils import unwrap_context from .internal.update_warnings import check_updates from .internal.daemon_runner import is_daemon_mode, run_daemon_task +from .ssl_ import QuicCapabilityCache # noinspection PyDefaultArgument @@ -114,7 +115,14 @@ def handle_generic_error(e, annotation=None): exit_status = ExitStatus.ERROR except niquests.Timeout: exit_status = ExitStatus.ERROR_TIMEOUT - env.log_error(f'Request timed out ({parsed_args.timeout}s).') + # this detects if we tried to connect with HTTP/3 when the remote isn't compatible anymore. + if hasattr(parsed_args, "_failsafe_http3"): + env.log_error( + f'Unable to connect. Was the remote specified HTTP/3 compatible but is not anymore? ' + f'Remove "{QuicCapabilityCache.__file__}" to clear it out. Or set --disable-http3 flag.' + ) + else: + env.log_error(f'Request timed out ({parsed_args.timeout}s).') except niquests.TooManyRedirects: exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS env.log_error( diff --git a/httpie/internal/encoder.py b/httpie/internal/encoder.py index 4af687fe79..a6867223df 100644 --- a/httpie/internal/encoder.py +++ b/httpie/internal/encoder.py @@ -21,14 +21,7 @@ import os from uuid import uuid4 -# to understand why this is required -# see https://niquests.readthedocs.io/en/latest/community/faq.html#what-is-urllib3-future -from niquests._compat import HAS_LEGACY_URLLIB3 - -if HAS_LEGACY_URLLIB3: - from urllib3_future.fields import RequestField -else: - from urllib3.fields import RequestField +from ..compat import RequestField class MultipartEncoder(object): diff --git a/httpie/models.py b/httpie/models.py index ea3f00b40d..76ccc8c748 100644 --- a/httpie/models.py +++ b/httpie/models.py @@ -2,17 +2,6 @@ import niquests -# to understand why this is required -# see https://niquests.readthedocs.io/en/latest/community/faq.html#what-is-urllib3-future -from niquests._compat import HAS_LEGACY_URLLIB3 - -if not HAS_LEGACY_URLLIB3: - from urllib3 import ConnectionInfo - from urllib3.util import SKIP_HEADER, SKIPPABLE_HEADERS -else: - from urllib3_future import ConnectionInfo - from urllib3_future.util import SKIP_HEADER, SKIPPABLE_HEADERS - from kiss_headers.utils import prettify_header_name from enum import Enum, auto @@ -26,7 +15,7 @@ OUT_RESP_HEAD, OUT_RESP_META ) -from .compat import cached_property +from .compat import urllib3, SKIP_HEADER, SKIPPABLE_HEADERS, cached_property from .utils import split_cookies, parse_content_type_header ELAPSED_TIME_LABEL = 'Elapsed time' @@ -152,7 +141,7 @@ def iter_lines(self, chunk_size): @property def metadata(self) -> str: - conn_info: ConnectionInfo = self._orig.conn_info + conn_info: urllib3.ConnectionInfo = self._orig.conn_info metadatum = f"Connected to: {conn_info.destination_address[0]} port {conn_info.destination_address[1]}\n" diff --git a/httpie/ssl_.py b/httpie/ssl_.py index 9fe02abd1b..292f03602c 100644 --- a/httpie/ssl_.py +++ b/httpie/ssl_.py @@ -1,4 +1,5 @@ import ssl +import typing from typing import NamedTuple, Optional, Tuple, MutableMapping import json import os.path @@ -37,16 +38,21 @@ class QuicCapabilityCache( See https://urllib3future.readthedocs.io/en/latest/advanced-usage.html#remembering-http-3-over-quic-support for the implementation guide.""" + __file__ = os.path.join(DEFAULT_CONFIG_DIR, "quic.json") + def __init__(self): self._cache = {} if not os.path.exists(DEFAULT_CONFIG_DIR): makedirs(DEFAULT_CONFIG_DIR, exist_ok=True) - if os.path.exists(os.path.join(DEFAULT_CONFIG_DIR, "quic.json")): - with open(os.path.join(DEFAULT_CONFIG_DIR, "quic.json"), "r") as fp: - self._cache = json.load(fp) + if os.path.exists(QuicCapabilityCache.__file__): + with open(QuicCapabilityCache.__file__, "r") as fp: + try: + self._cache = json.load(fp) + except json.JSONDecodeError: # if the file is corrupted (invalid json) then, ignore it. + pass def save(self): - with open(os.path.join(DEFAULT_CONFIG_DIR, "quic.json"), "w+") as fp: + with open(QuicCapabilityCache.__file__, "w+") as fp: json.dump(self._cache, fp) def __contains__(self, item: Tuple[str, int]): @@ -82,8 +88,11 @@ class HTTPieCertificate(NamedTuple): key_file: Optional[str] = None key_password: Optional[str] = None - def to_raw_cert(self): - """Synthesize a requests-compatible (2-item tuple of cert and key file) + def to_raw_cert(self) -> typing.Union[ + typing.Tuple[typing.Optional[str], typing.Optional[str], typing.Optional[str]], # with password + typing.Tuple[typing.Optional[str], typing.Optional[str]] # without password + ]: + """Synthesize a niquests-compatible (2(or 3)-item tuple of cert, key file and optionally password) object from HTTPie's internal representation of a certificate.""" if self.key_password: # Niquests support 3-tuple repr in addition to the 2-tuple repr diff --git a/setup.cfg b/setup.cfg index fa95a47458..64aa59d373 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,8 +19,15 @@ markers = requires_external_processes filterwarnings = default + # due to urllib3.future no longer needing http.client! nothing to be concerned about. ignore:Passing msg=\.\. is deprecated:DeprecationWarning + # this only concern the test suite / local test server with a self signed certificate. ignore:Unverified HTTPS request is being made to host:urllib3.exceptions.InsecureRequestWarning + # the constant themselves are deprecated in the ssl module, we want to silent them in the test suite until we + # change the concerned code. Python 3.13 may remove them, so we'll need to think about it soon. + ignore:ssl\.PROTOCOL_(TLSv1|TLSv1_1|TLSv1_2) is deprecated:DeprecationWarning + ignore:ssl\.TLSVersion\.(TLSv1|TLSv1_1|TLSv1_2) is deprecated:DeprecationWarning + [metadata] name = httpie