Skip to content

Commit

Permalink
⚡ overall performance improvement by reducing redundant iterations fr…
Browse files Browse the repository at this point in the history
…om requests era (#121)

also avoid parsing x509 certificate many times for the same remote peer
  • Loading branch information
Ousret authored May 16, 2024
1 parent 6940d55 commit 41eab91
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 10 deletions.
7 changes: 7 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release History
===============

3.6.4 (2024-05-16)
------------------

**Changed**
- Avoid parsing X509 peer certificate in the certificate revocation check process over and over again.
- Avoid iterating over header items redundantly or needlessly.

3.6.3 (2024-05-06)
------------------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description = "Niquests is a simple, yet elegant, HTTP library. It is a drop-in
readme = "README.md"
license-files = { paths = ["LICENSE"] }
license = "Apache-2.0"
keywords = ["requests", "http/2", "http/3", "QUIC", "http", "https", "http client", "http/1.1", "ocsp", "revocation", "tls", "multiplexed", "dns-over-quic", "doq", "dns-over-tls", "dot", "dns-over-https", "doh", "dnssec"]
keywords = ["requests", "http2", "http3", "QUIC", "http", "https", "http client", "http/1.1", "ocsp", "revocation", "tls", "multiplexed", "dns-over-quic", "doq", "dns-over-tls", "dot", "dns-over-https", "doh", "dnssec"]
authors = [
{name = "Kenneth Reitz", email = "me@kennethreitz.org"}
]
Expand Down
4 changes: 2 additions & 2 deletions src/niquests/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
__url__: str = "https://niquests.readthedocs.io"

__version__: str
__version__ = "3.6.3"
__version__ = "3.6.4"

__build__: int = 0x030603
__build__: int = 0x030604
__author__: str = "Kenneth Reitz"
__author_email__: str = "me@kennethreitz.org"
__license__: str = "Apache-2.0"
Expand Down
8 changes: 6 additions & 2 deletions src/niquests/extensions/_async_ocsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@
async_recv_tls_and_decrypt,
async_send_tls,
)
from ._ocsp import _str_fingerprint_of, readable_revocation_reason
from ._ocsp import (
_str_fingerprint_of,
readable_revocation_reason,
_parse_x509_der_cached,
)


async def _ask_nicely_for_issuer(
Expand Down Expand Up @@ -320,7 +324,7 @@ async def verify(
if not endpoints:
return

peer_certificate = Certificate(conn_info.certificate_der)
peer_certificate = _parse_x509_der_cached(conn_info.certificate_der)

async with _SharedRevocationStatusCache.lock(peer_certificate):
# this feature, by default, is reserved for a reasonable usage.
Expand Down
8 changes: 7 additions & 1 deletion src/niquests/extensions/_ocsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from hashlib import sha256
from random import randint
from statistics import mean
from functools import lru_cache

from qh3._hazmat import (
OCSPRequest,
Expand Down Expand Up @@ -52,6 +53,11 @@
)


@lru_cache(maxsize=64)
def _parse_x509_der_cached(der: bytes) -> Certificate:
return Certificate(der)


def _str_fingerprint_of(certificate: Certificate) -> str:
return ":".join(
[format(i, "02x") for i in sha256(certificate.public_bytes()).digest()]
Expand Down Expand Up @@ -327,7 +333,7 @@ def verify(
if _SharedRevocationStatusCache.hold:
return

peer_certificate = Certificate(conn_info.certificate_der)
peer_certificate = _parse_x509_der_cached(conn_info.certificate_der)
cached_response = _SharedRevocationStatusCache.check(peer_certificate)

if cached_response is not None:
Expand Down
15 changes: 12 additions & 3 deletions src/niquests/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,26 @@ def merge_setting(
return session_setting

# Bypass if not a dictionary (e.g. verify)
if not (
if isinstance(session_setting, bool) or not (
isinstance(session_setting, Mapping) and isinstance(request_setting, Mapping)
):
return request_setting

merged_setting = dict_class(to_key_val_list(session_setting))
if hasattr(session_setting, "copy"):
merged_setting = (
session_setting.copy()
if session_setting.__class__ is dict_class
else dict_class(session_setting.copy())
)
else:
merged_setting = dict_class(to_key_val_list(session_setting))

merged_setting.update(to_key_val_list(request_setting))

# Remove keys that are set to None. Extract keys first to avoid altering
# the dictionary during iteration.
none_keys = [k for (k, v) in merged_setting.items() if v is None]
none_keys = [k for k in merged_setting if merged_setting[k] is None]

for key in none_keys:
del merged_setting[key]

Expand Down
6 changes: 5 additions & 1 deletion src/niquests/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ def lower_items(self) -> typing.Iterator[tuple[bytes | str, bytes | str]]:
"""Like iteritems(), but with all lowercase keys."""
return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())

def items(self):
for k in self._store:
yield self._store[k]

def __eq__(self, other) -> bool:
if isinstance(other, Mapping):
other = CaseInsensitiveDict(other)
Expand All @@ -136,7 +140,7 @@ def __eq__(self, other) -> bool:

# Copy is required
def copy(self) -> CaseInsensitiveDict:
return CaseInsensitiveDict(self._store.values())
return CaseInsensitiveDict(self)

def __repr__(self) -> str:
return str(dict(self.items()))
Expand Down

0 comments on commit 41eab91

Please sign in to comment.