fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099
Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from io import UnsupportedOperation
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import ssl
|
||||
|
||||
from ._ctypes import load_cert_chain as _ctypes_load_cert_chain
|
||||
from ._shm import load_cert_chain as _shm_load_cert_chain
|
||||
|
||||
SUPPORTED_METHODS: list[
|
||||
typing.Callable[
|
||||
[
|
||||
ssl.SSLContext,
|
||||
bytes | str,
|
||||
bytes | str,
|
||||
bytes | str | typing.Callable[[], str | bytes] | None,
|
||||
],
|
||||
None,
|
||||
]
|
||||
] = [
|
||||
_ctypes_load_cert_chain,
|
||||
_shm_load_cert_chain,
|
||||
]
|
||||
|
||||
|
||||
def load_cert_chain(
|
||||
ctx: ssl.SSLContext,
|
||||
certdata: bytes | str,
|
||||
keydata: bytes | str,
|
||||
password: bytes | str | typing.Callable[[], str | bytes] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Unique workaround the known limitation of CPython inability to initialize the mTLS context without files.
|
||||
:raise UnsupportedOperation: If anything goes wrong in the process.
|
||||
"""
|
||||
err = None
|
||||
|
||||
for supported in SUPPORTED_METHODS:
|
||||
try:
|
||||
supported(
|
||||
ctx,
|
||||
certdata,
|
||||
keydata,
|
||||
password,
|
||||
)
|
||||
return
|
||||
except UnsupportedOperation as e:
|
||||
if err is None:
|
||||
err = e
|
||||
|
||||
if err is not None:
|
||||
raise err
|
||||
|
||||
raise UnsupportedOperation("unable to initialize mTLS using in-memory cert and key")
|
||||
|
||||
|
||||
__all__ = ("load_cert_chain",)
|
||||
@@ -0,0 +1,376 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
import typing
|
||||
from io import UnsupportedOperation
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import ssl
|
||||
|
||||
|
||||
class _OpenSSL:
|
||||
"""Access hazardous material from CPython OpenSSL (or compatible SSL) implementation."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
import platform
|
||||
|
||||
if platform.python_implementation() != "CPython":
|
||||
raise UnsupportedOperation("Only CPython is supported")
|
||||
|
||||
import ssl
|
||||
|
||||
self._name = ssl.OPENSSL_VERSION
|
||||
self.ssl = ssl
|
||||
|
||||
# bug seen in Windows + CPython < 3.11
|
||||
# where CPython official API for options
|
||||
# cast OpenSSL get_options to SIGNED long
|
||||
# where we want UNSIGNED long.
|
||||
_ssl_options_signed_long_bug = False
|
||||
|
||||
if not hasattr(ssl, "_ssl"):
|
||||
raise UnsupportedOperation(
|
||||
"Unsupported interpreter due to missing private ssl module"
|
||||
)
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# possible search locations
|
||||
candidates = {
|
||||
os.path.dirname(sys.executable),
|
||||
os.path.join(sys.prefix, "DLLs"),
|
||||
sys.prefix,
|
||||
}
|
||||
|
||||
if hasattr(ssl._ssl, "__file__"):
|
||||
candidates.add(os.path.dirname(ssl._ssl.__file__))
|
||||
|
||||
_ssl_options_signed_long_bug = sys.version_info < (3, 11)
|
||||
|
||||
ssl_potential_match = None
|
||||
crypto_potential_match = None
|
||||
|
||||
for d in candidates:
|
||||
if not os.path.exists(d):
|
||||
continue
|
||||
|
||||
for filename in os.listdir(d):
|
||||
if ssl_potential_match is None:
|
||||
if filename.startswith("libssl") and filename.endswith(".dll"):
|
||||
ssl_potential_match = os.path.join(d, filename)
|
||||
|
||||
if crypto_potential_match is None:
|
||||
if filename.startswith("libcrypto") and filename.endswith(
|
||||
".dll"
|
||||
):
|
||||
crypto_potential_match = os.path.join(d, filename)
|
||||
|
||||
if crypto_potential_match and ssl_potential_match:
|
||||
break
|
||||
|
||||
if not ssl_potential_match or not crypto_potential_match:
|
||||
raise UnsupportedOperation(
|
||||
"Could not locate OpenSSL DLLs next to Python; "
|
||||
"check your /DLLs folder or your PATH."
|
||||
)
|
||||
|
||||
self._ssl = ctypes.CDLL(ssl_potential_match)
|
||||
self._crypto = ctypes.CDLL(crypto_potential_match)
|
||||
else:
|
||||
# that's the most common path
|
||||
# ssl built in module already loaded both crypto and ssl
|
||||
# symbols.
|
||||
if hasattr(ssl._ssl, "__file__"):
|
||||
self._ssl = ctypes.CDLL(ssl._ssl.__file__)
|
||||
else:
|
||||
# _ssl is statically linked into the interpreter
|
||||
# (e.g. python-build-standalone via uv). OpenSSL symbols
|
||||
# are in the main process image; ctypes.CDLL(None) exposes them.
|
||||
# see https://github.com/jawah/urllib3.future/issues/325 for more
|
||||
# details.
|
||||
self._ssl = ctypes.CDLL(None)
|
||||
self._crypto = self._ssl
|
||||
|
||||
# we want to ensure a minimal set of symbols
|
||||
# are present. CPython should have at least:
|
||||
for required_symbol in [
|
||||
"SSL_CTX_use_certificate",
|
||||
"SSL_CTX_check_private_key",
|
||||
"SSL_CTX_use_PrivateKey",
|
||||
]:
|
||||
if not hasattr(self._ssl, required_symbol):
|
||||
raise UnsupportedOperation(
|
||||
f"Python interpreter built against '{self._name}' is unsupported. (libssl) {required_symbol} is not present."
|
||||
)
|
||||
|
||||
for required_symbol in [
|
||||
"BIO_free",
|
||||
"BIO_new_mem_buf",
|
||||
"PEM_read_bio_X509",
|
||||
"PEM_read_bio_PrivateKey",
|
||||
"ERR_get_error",
|
||||
"ERR_error_string",
|
||||
]:
|
||||
if not hasattr(self._crypto, required_symbol):
|
||||
raise UnsupportedOperation(
|
||||
f"Python interpreter built against '{self._name}' is unsupported. (libcrypto) {required_symbol} is not present."
|
||||
)
|
||||
|
||||
# https://docs.openssl.org/3.0/man3/SSL_CTX_use_certificate/
|
||||
self.SSL_CTX_use_certificate = self._ssl.SSL_CTX_use_certificate
|
||||
self.SSL_CTX_use_certificate.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
self.SSL_CTX_use_certificate.restype = ctypes.c_int
|
||||
|
||||
self.SSL_CTX_check_private_key = self._ssl.SSL_CTX_check_private_key
|
||||
self.SSL_CTX_check_private_key.argtypes = [ctypes.c_void_p]
|
||||
self.SSL_CTX_check_private_key.restype = ctypes.c_int
|
||||
|
||||
# https://docs.openssl.org/3.0/man3/BIO_new/
|
||||
self.BIO_free = self._crypto.BIO_free
|
||||
self.BIO_free.argtypes = [ctypes.c_void_p]
|
||||
self.BIO_free.restype = None
|
||||
|
||||
self.BIO_new_mem_buf = self._crypto.BIO_new_mem_buf
|
||||
self.BIO_new_mem_buf.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
self.BIO_new_mem_buf.restype = ctypes.c_void_p
|
||||
|
||||
# https://docs.openssl.org/3.0/man3/PEM_read_bio_PrivateKey/
|
||||
self.PEM_read_bio_X509 = self._crypto.PEM_read_bio_X509
|
||||
self.PEM_read_bio_X509.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_void_p,
|
||||
]
|
||||
self.PEM_read_bio_X509.restype = ctypes.c_void_p
|
||||
|
||||
self.PEM_read_bio_PrivateKey = self._crypto.PEM_read_bio_PrivateKey
|
||||
self.PEM_read_bio_PrivateKey.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_void_p,
|
||||
]
|
||||
self.PEM_read_bio_PrivateKey.restype = ctypes.c_void_p
|
||||
|
||||
# https://docs.openssl.org/3.0/man3/SSL_CTX_use_certificate/
|
||||
self.SSL_CTX_use_PrivateKey = self._ssl.SSL_CTX_use_PrivateKey
|
||||
self.SSL_CTX_use_PrivateKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
self.SSL_CTX_use_PrivateKey.restype = ctypes.c_int
|
||||
|
||||
self.ERR_get_error = self._crypto.ERR_get_error
|
||||
self.ERR_get_error.argtypes = []
|
||||
self.ERR_get_error.restype = ctypes.c_ulong
|
||||
|
||||
self.ERR_error_string = self._crypto.ERR_error_string
|
||||
self.ERR_error_string.argtypes = [ctypes.c_ulong, ctypes.c_char_p]
|
||||
self.ERR_error_string.restype = ctypes.c_char_p
|
||||
|
||||
if hasattr(self._ssl, "SSL_CTX_get_options"):
|
||||
self.SSL_CTX_get_options = self._ssl.SSL_CTX_get_options
|
||||
self.SSL_CTX_get_options.argtypes = [ctypes.c_void_p]
|
||||
self.SSL_CTX_get_options.restype = (
|
||||
ctypes.c_ulong if not _ssl_options_signed_long_bug else ctypes.c_long
|
||||
) # OpenSSL's options are long
|
||||
elif hasattr(self._ssl, "SSL_CTX_ctrl"):
|
||||
# some old build inline SSL_CTX_get_options (mere C define)
|
||||
# define SSL_CTX_get_options(ctx) SSL_CTX_ctrl((ctx),SSL_CTRL_OPTIONS,0,NULL)
|
||||
# define SSL_CTRL_OPTIONS 32
|
||||
|
||||
self.SSL_CTX_ctrl = self._ssl.SSL_CTX_ctrl
|
||||
self.SSL_CTX_ctrl.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_int,
|
||||
ctypes.c_int,
|
||||
ctypes.c_void_p,
|
||||
]
|
||||
self.SSL_CTX_ctrl.restype = (
|
||||
ctypes.c_ulong if not _ssl_options_signed_long_bug else ctypes.c_long
|
||||
)
|
||||
|
||||
self.SSL_CTX_get_options = lambda ctx: self.SSL_CTX_ctrl( # type: ignore[assignment]
|
||||
ctx, 32, 0, None
|
||||
)
|
||||
else:
|
||||
raise UnsupportedOperation()
|
||||
|
||||
def pull_error(self) -> typing.NoReturn:
|
||||
raise self.ssl.SSLError(
|
||||
self.ERR_error_string(
|
||||
self.ERR_get_error(), ctypes.create_string_buffer(256)
|
||||
).decode()
|
||||
)
|
||||
|
||||
|
||||
_IS_GIL_DISABLED = hasattr(sys, "_is_gil_enabled") and sys._is_gil_enabled() is False
|
||||
_IS_LINUX = sys.platform == "linux"
|
||||
_FT_HEAD_ADDITIONAL_OFFSET = 1 if _IS_LINUX else 2
|
||||
|
||||
_head_extra_fields = []
|
||||
|
||||
if sys.flags.debug:
|
||||
# In debug builds (_POSIX_C_SOURCE or Py_DEBUG is defined), PyObject_HEAD
|
||||
# is preceded by _PyObject_HEAD_EXTRA, which typically consists of
|
||||
# two pointers (_ob_next, _ob_prev).
|
||||
_head_extra_fields = [("_ob_next", ctypes.c_void_p), ("_ob_prev", ctypes.c_void_p)]
|
||||
|
||||
|
||||
# Define the PySSLContext C structure using ctypes.
|
||||
# This definition assumes that 'SSL_CTX *ctx' is the first member
|
||||
# immediately following PyObject_HEAD. This has been observed to be
|
||||
# the case in various CPython versions (e.g., 3.7 through 3.14 so far).
|
||||
#
|
||||
# CPython's Modules/_ssl.c (simplified):
|
||||
# typedef struct {
|
||||
# PyObject_HEAD // Expands to _PyObject_HEAD_EXTRA (if debug) + ob_refcnt + ob_type
|
||||
# SSL_CTX *ctx;
|
||||
# // ... other members ...
|
||||
# } PySSLContextObject;
|
||||
#
|
||||
class PySSLContextStruct(ctypes.Structure):
|
||||
_fields_ = (
|
||||
_head_extra_fields # type: ignore[assignment]
|
||||
+ [
|
||||
("ob_refcnt", ctypes.c_ssize_t), # Py_ssize_t ob_refcnt;
|
||||
("ob_type", ctypes.c_void_p), # PyTypeObject *ob_type;
|
||||
]
|
||||
+ (
|
||||
[(f"_ob_ft{i}", ctypes.c_void_p) for i in range(_FT_HEAD_ADDITIONAL_OFFSET)]
|
||||
if _IS_GIL_DISABLED
|
||||
else []
|
||||
)
|
||||
+ [
|
||||
("ssl_ctx", ctypes.c_void_p), # SSL_CTX *ctx; (this is the pointer we want)
|
||||
# If there were other C members between ob_type and ssl_ctx,
|
||||
# they would need to be defined here with their correct types and padding.
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _split_client_cert(data: bytes) -> list[bytes]:
|
||||
line_ending = b"\n" if b"-----\r\n" not in data else b"\r\n"
|
||||
boundary = b"-----END CERTIFICATE-----" + line_ending
|
||||
|
||||
certificates = []
|
||||
|
||||
for chunk in data.split(boundary):
|
||||
if chunk:
|
||||
start_marker = chunk.find(b"-----BEGIN CERTIFICATE-----" + line_ending)
|
||||
if start_marker == -1:
|
||||
break
|
||||
pem_reconstructed = b"".join([chunk[start_marker:], boundary])
|
||||
certificates.append(pem_reconstructed)
|
||||
|
||||
return certificates
|
||||
|
||||
|
||||
def load_cert_chain(
|
||||
ctx: ssl.SSLContext,
|
||||
certdata: bytes | str,
|
||||
keydata: bytes | str,
|
||||
password: bytes | str | typing.Callable[[], str | bytes] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Unique workaround the known limitation of CPython inability to initialize the mTLS context without files.
|
||||
:raise UnsupportedOperation: If anything goes wrong in the process.
|
||||
"""
|
||||
lib = _OpenSSL()
|
||||
|
||||
# Get the memory address of the Python ssl.SSLContext object.
|
||||
# id() returns the address of the PyObject.
|
||||
addr = id(ctx)
|
||||
|
||||
# Cast this memory address to a pointer to our defined PySSLContextStruct.
|
||||
ptr_to_pysslcontext_struct = ctypes.cast(addr, ctypes.POINTER(PySSLContextStruct))
|
||||
|
||||
# Access the 'ssl_ctx' field from the structure. This field holds the
|
||||
# actual SSL_CTX* C pointer value.
|
||||
ssl_ctx_address = ptr_to_pysslcontext_struct.contents.ssl_ctx
|
||||
|
||||
# We want to ensure we got the right pointer address
|
||||
# the safest way to achieve that is to retrieve options
|
||||
# and compare it with the official ctx property.
|
||||
if lib.SSL_CTX_get_options is not None:
|
||||
bypass_options = lib.SSL_CTX_get_options(ssl_ctx_address)
|
||||
expected_options = int(ctx.options)
|
||||
|
||||
if bypass_options != expected_options:
|
||||
raise UnsupportedOperation(
|
||||
f"CPython internal SSL_CTX changed! Cannot pursue safely. Expected = {expected_options:x} Actual = {bypass_options:x}"
|
||||
)
|
||||
|
||||
# normalize inputs
|
||||
if isinstance(certdata, str):
|
||||
certdata = certdata.encode()
|
||||
if isinstance(keydata, str):
|
||||
keydata = keydata.encode()
|
||||
|
||||
client_chain = _split_client_cert(certdata)
|
||||
|
||||
leaf_certificate = client_chain[0]
|
||||
|
||||
# Use a BIO to read the client certificate
|
||||
# only the leaf certificate is supported here.
|
||||
cert_bio = lib.BIO_new_mem_buf(leaf_certificate, len(leaf_certificate))
|
||||
|
||||
if not cert_bio:
|
||||
raise MemoryError("Unable to allocate memory to load the client certificate")
|
||||
|
||||
# Use a BIO to load the key in-memory
|
||||
key_bio = lib.BIO_new_mem_buf(keydata, len(keydata))
|
||||
|
||||
if not key_bio:
|
||||
raise MemoryError("Unable to allocate memory to load the client key")
|
||||
|
||||
# prepare the password
|
||||
if callable(password):
|
||||
password = password()
|
||||
|
||||
if isinstance(password, str):
|
||||
password = password.encode()
|
||||
|
||||
assert password is None or isinstance(password, bytes)
|
||||
|
||||
# the allocated X509 obj MUST NOT be freed by ourselves
|
||||
# OpenSSL internals will free it once not needed.
|
||||
cert = lib.PEM_read_bio_X509(cert_bio, None, None, None)
|
||||
|
||||
# we do own the BIO, once the X509 leaf is instantiated, no need
|
||||
# to keep it afterward.
|
||||
lib.BIO_free(cert_bio)
|
||||
|
||||
if not cert:
|
||||
lib.pull_error()
|
||||
|
||||
pkey = lib.PEM_read_bio_PrivateKey(key_bio, None, None, password)
|
||||
|
||||
lib.BIO_free(key_bio)
|
||||
|
||||
if not pkey:
|
||||
lib.pull_error()
|
||||
|
||||
if lib.SSL_CTX_use_certificate(ssl_ctx_address, cert) != 1:
|
||||
lib.pull_error()
|
||||
|
||||
if lib.SSL_CTX_use_PrivateKey(ssl_ctx_address, pkey) != 1:
|
||||
lib.pull_error()
|
||||
|
||||
if lib.SSL_CTX_check_private_key(ssl_ctx_address) != 1:
|
||||
lib.pull_error()
|
||||
|
||||
# Unfortunately, most of the time
|
||||
# SSL_CTX_add_extra_chain_cert is unavailable
|
||||
# in the final CPython build.
|
||||
# According to OpenSSL latest docs: "The engine
|
||||
# will attempt to build the required chain for the CA store"
|
||||
# It's not going to be used as a trust anchor! (i.e. not self-signed)
|
||||
# "If no chain is specified, the library will try to complete the
|
||||
# chain from the available CA certificates in the trusted
|
||||
# CA storage, see SSL_CTX_load_verify_locations(3)."
|
||||
# see: https://docs.openssl.org/master/man3/SSL_CTX_add_extra_chain_cert/#notes
|
||||
if len(client_chain) > 1:
|
||||
ctx.load_verify_locations(cadata=(b"\n".join(client_chain[1:])).decode())
|
||||
|
||||
|
||||
__all__ = ("load_cert_chain",)
|
||||
@@ -0,0 +1,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import stat
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
from hashlib import sha256
|
||||
from io import UnsupportedOperation
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import ssl
|
||||
|
||||
|
||||
def load_cert_chain(
|
||||
ctx: ssl.SSLContext,
|
||||
certdata: str | bytes,
|
||||
keydata: str | bytes | None = None,
|
||||
password: typing.Callable[[], str | bytes] | str | bytes | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Unique workaround the known limitation of CPython inability to initialize the mTLS context without files.
|
||||
Only supported on Linux, FreeBSD, and OpenBSD.
|
||||
:raise UnsupportedOperation: If anything goes wrong in the process.
|
||||
"""
|
||||
if (
|
||||
sys.platform != "linux"
|
||||
and sys.platform.startswith("freebsd") is False
|
||||
and sys.platform.startswith("openbsd") is False
|
||||
):
|
||||
raise UnsupportedOperation(
|
||||
f"Unable to provide support for in-memory client certificate: Unsupported platform {sys.platform}"
|
||||
)
|
||||
|
||||
unique_name: str = f"{sha256(secrets.token_bytes(32)).hexdigest()}.pem"
|
||||
|
||||
if isinstance(certdata, bytes):
|
||||
certdata = certdata.decode("ascii")
|
||||
|
||||
if keydata is not None:
|
||||
if isinstance(keydata, bytes):
|
||||
keydata = keydata.decode("ascii")
|
||||
|
||||
if hasattr(os, "memfd_create"):
|
||||
fd = os.memfd_create(unique_name, os.MFD_CLOEXEC)
|
||||
else:
|
||||
# this branch patch is for CPython <3.8 and PyPy 3.7+
|
||||
from ctypes import c_int, c_ushort, cdll, create_string_buffer, get_errno, util
|
||||
|
||||
loc = util.find_library("rt") or util.find_library("c")
|
||||
|
||||
if not loc:
|
||||
raise UnsupportedOperation(
|
||||
"Unable to provide support for in-memory client certificate: libc or librt not found."
|
||||
)
|
||||
|
||||
lib = cdll.LoadLibrary(loc)
|
||||
|
||||
_shm_open = lib.shm_open
|
||||
# _shm_unlink = lib.shm_unlink
|
||||
|
||||
buf_name = create_string_buffer(unique_name.encode())
|
||||
|
||||
try:
|
||||
fd = _shm_open(
|
||||
buf_name,
|
||||
c_int(os.O_RDWR | os.O_CREAT),
|
||||
c_ushort(stat.S_IRUSR | stat.S_IWUSR),
|
||||
)
|
||||
except SystemError as e:
|
||||
raise UnsupportedOperation(
|
||||
f"Unable to provide support for in-memory client certificate: {e}"
|
||||
)
|
||||
|
||||
if fd == -1:
|
||||
raise UnsupportedOperation(
|
||||
f"Unable to provide support for in-memory client certificate: {os.strerror(get_errno())}"
|
||||
)
|
||||
|
||||
# Linux 3.17+
|
||||
path = f"/proc/self/fd/{fd}"
|
||||
|
||||
# Alt-path
|
||||
shm_path = f"/dev/shm/{unique_name}"
|
||||
|
||||
if os.path.exists(path) is False:
|
||||
if os.path.exists(shm_path):
|
||||
path = shm_path
|
||||
else:
|
||||
os.fdopen(fd).close()
|
||||
|
||||
raise UnsupportedOperation(
|
||||
"Unable to provide support for in-memory client certificate: no virtual patch available?"
|
||||
)
|
||||
|
||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
with open(path, "w") as fp:
|
||||
fp.write(certdata)
|
||||
|
||||
if keydata:
|
||||
fp.write(keydata)
|
||||
|
||||
path = fp.name
|
||||
|
||||
ctx.load_cert_chain(path, password=password)
|
||||
|
||||
# we shall start cleaning remnants
|
||||
os.fdopen(fd).close()
|
||||
|
||||
if os.path.exists(shm_path):
|
||||
os.unlink(shm_path)
|
||||
|
||||
if os.path.exists(path) or os.path.exists(shm_path):
|
||||
warnings.warn(
|
||||
"In-memory client certificate: The kernel leaked a file descriptor outside of its expected lifetime.",
|
||||
ResourceWarning,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ("load_cert_chain",)
|
||||
Reference in New Issue
Block a user