snowflake-connector-python 4.4.0__cp314-cp314-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snowflake/connector/__init__.py +118 -0
- snowflake/connector/_query_context_cache.py +276 -0
- snowflake/connector/_sql_util.py +48 -0
- snowflake/connector/_utils.py +353 -0
- snowflake/connector/aio/README.md +88 -0
- snowflake/connector/aio/__init__.py +163 -0
- snowflake/connector/aio/_azure_storage_client.py +227 -0
- snowflake/connector/aio/_bind_upload_agent.py +69 -0
- snowflake/connector/aio/_connection.py +1216 -0
- snowflake/connector/aio/_cursor.py +1402 -0
- snowflake/connector/aio/_description.py +5 -0
- snowflake/connector/aio/_direct_file_operation_utils.py +88 -0
- snowflake/connector/aio/_file_transfer_agent.py +316 -0
- snowflake/connector/aio/_gcs_storage_client.py +360 -0
- snowflake/connector/aio/_network.py +860 -0
- snowflake/connector/aio/_ocsp_asn1crypto.py +51 -0
- snowflake/connector/aio/_ocsp_snowflake.py +608 -0
- snowflake/connector/aio/_pandas_tools.py +561 -0
- snowflake/connector/aio/_result_batch.py +442 -0
- snowflake/connector/aio/_result_set.py +309 -0
- snowflake/connector/aio/_s3_storage_client.py +460 -0
- snowflake/connector/aio/_session_manager.py +588 -0
- snowflake/connector/aio/_storage_client.py +331 -0
- snowflake/connector/aio/_telemetry.py +97 -0
- snowflake/connector/aio/_time_util.py +57 -0
- snowflake/connector/aio/_wif_util.py +324 -0
- snowflake/connector/aio/auth/__init__.py +52 -0
- snowflake/connector/aio/auth/_auth.py +406 -0
- snowflake/connector/aio/auth/_by_plugin.py +131 -0
- snowflake/connector/aio/auth/_default.py +28 -0
- snowflake/connector/aio/auth/_idtoken.py +54 -0
- snowflake/connector/aio/auth/_keypair.py +60 -0
- snowflake/connector/aio/auth/_no_auth.py +31 -0
- snowflake/connector/aio/auth/_oauth.py +27 -0
- snowflake/connector/aio/auth/_oauth_code.py +117 -0
- snowflake/connector/aio/auth/_oauth_credentials.py +100 -0
- snowflake/connector/aio/auth/_okta.py +247 -0
- snowflake/connector/aio/auth/_pat.py +27 -0
- snowflake/connector/aio/auth/_usrpwdmfa.py +30 -0
- snowflake/connector/aio/auth/_webbrowser.py +405 -0
- snowflake/connector/aio/auth/_workload_identity.py +60 -0
- snowflake/connector/arrow_context.py +211 -0
- snowflake/connector/auth/__init__.py +53 -0
- snowflake/connector/auth/_auth.py +669 -0
- snowflake/connector/auth/_http_server.py +257 -0
- snowflake/connector/auth/_oauth_base.py +472 -0
- snowflake/connector/auth/by_plugin.py +219 -0
- snowflake/connector/auth/default.py +36 -0
- snowflake/connector/auth/idtoken.py +72 -0
- snowflake/connector/auth/keypair.py +225 -0
- snowflake/connector/auth/no_auth.py +39 -0
- snowflake/connector/auth/oauth.py +49 -0
- snowflake/connector/auth/oauth_code.py +490 -0
- snowflake/connector/auth/oauth_credentials.py +71 -0
- snowflake/connector/auth/okta.py +339 -0
- snowflake/connector/auth/pat.py +40 -0
- snowflake/connector/auth/usrpwdmfa.py +69 -0
- snowflake/connector/auth/webbrowser.py +514 -0
- snowflake/connector/auth/workload_identity.py +110 -0
- snowflake/connector/azure_storage_client.py +278 -0
- snowflake/connector/backoff_policies.py +141 -0
- snowflake/connector/bind_upload_agent.py +89 -0
- snowflake/connector/cache.py +696 -0
- snowflake/connector/compat.py +127 -0
- snowflake/connector/config_manager.py +553 -0
- snowflake/connector/connection.py +2671 -0
- snowflake/connector/connection_diagnostic.py +793 -0
- snowflake/connector/constants.py +460 -0
- snowflake/connector/converter.py +809 -0
- snowflake/connector/converter_issue23517.py +87 -0
- snowflake/connector/converter_null.py +14 -0
- snowflake/connector/converter_snowsql.py +205 -0
- snowflake/connector/crl.py +919 -0
- snowflake/connector/crl_cache.py +734 -0
- snowflake/connector/cursor.py +2099 -0
- snowflake/connector/dbapi.py +53 -0
- snowflake/connector/description.py +21 -0
- snowflake/connector/direct_file_operation_utils.py +88 -0
- snowflake/connector/encryption_util.py +220 -0
- snowflake/connector/errorcode.py +94 -0
- snowflake/connector/errors.py +666 -0
- snowflake/connector/externals_utils/__init__.py +0 -0
- snowflake/connector/externals_utils/externals_setup.py +30 -0
- snowflake/connector/feature.py +4 -0
- snowflake/connector/file_compression_type.py +118 -0
- snowflake/connector/file_lock.py +72 -0
- snowflake/connector/file_transfer_agent.py +1244 -0
- snowflake/connector/file_util.py +153 -0
- snowflake/connector/gcs_storage_client.py +476 -0
- snowflake/connector/gzip_decoder.py +85 -0
- snowflake/connector/interval_util.py +26 -0
- snowflake/connector/local_storage_client.py +90 -0
- snowflake/connector/log_configuration.py +62 -0
- snowflake/connector/logging_utils/__init__.py +0 -0
- snowflake/connector/logging_utils/filters.py +72 -0
- snowflake/connector/minicore/__init__.py +0 -0
- snowflake/connector/minicore/aix_ppc64/libsf_mini_core.a +0 -0
- snowflake/connector/minicore/aix_ppc64/libsf_mini_core_static.a +0 -0
- snowflake/connector/minicore/linux_aarch64_glibc/libsf_mini_core.so +0 -0
- snowflake/connector/minicore/linux_aarch64_musl/libsf_mini_core.so +0 -0
- snowflake/connector/minicore/linux_x86_64_glibc/libsf_mini_core.so +0 -0
- snowflake/connector/minicore/linux_x86_64_musl/libsf_mini_core.so +0 -0
- snowflake/connector/minicore/macos_aarch64/libsf_mini_core.dylib +0 -0
- snowflake/connector/minicore/macos_x86_64/libsf_mini_core.dylib +0 -0
- snowflake/connector/minicore/windows_x86_64/sf_mini_core.dll +0 -0
- snowflake/connector/minicore/windows_x86_64/sf_mini_core_static.lib +0 -0
- snowflake/connector/nanoarrow_arrow_iterator.cp314-win_amd64.pyd +0 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.cpp +60 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.hpp +29 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.cpp +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.hpp +26 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.cpp +21 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.hpp +23 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.cpp +570 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.hpp +98 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.cpp +125 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.hpp +115 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.cpp +1140 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.hpp +138 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.cpp +47 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.hpp +46 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.cpp +83 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.hpp +35 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.cpp +97 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.hpp +72 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.cpp +73 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.hpp +28 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.cpp +30 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.hpp +35 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IColumnConverter.hpp +17 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.cpp +23 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.hpp +45 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.cpp +75 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.hpp +57 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/LICENSE.txt +209 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.cpp +75 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.hpp +30 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.cpp +46 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.hpp +29 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.cpp +8 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.hpp +95 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.cpp +57 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.hpp +36 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp +34 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.hpp +48 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.cpp +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.hpp +26 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.cpp +36 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.hpp +31 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.cpp +346 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.hpp +145 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/macros.hpp +14 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.cpp +67 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.hpp +68 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_accessors.h +101 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_alloc.h +127 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_assert.h +45 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_builder.h +1908 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_emitter.h +215 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_endian.h +125 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_epilogue.h +7 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_flatbuffers.h +55 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_identifier.h +148 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_iov.h +31 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_prologue.h +8 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_refmap.h +144 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_rtconfig.h +162 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_types.h +97 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_verifier.h +239 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/flatcc_portable.h +14 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/paligned_alloc.h +210 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pattributes.h +84 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic.h +84 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_pop.h +20 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_push.h +51 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian.h +206 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian_detect.h +118 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinline.h +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinttypes.h +52 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable.h +2 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable_basic.h +25 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstatic_assert.h +67 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdalign.h +162 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdint.h +898 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/punaligned.h +190 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pversion.h +6 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pwarnings.h +52 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc.c +3204 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.c +3217 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.h +3618 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.hpp +379 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_arrow_iterator.pyx +262 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.c +512 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.h +350 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.c +33273 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.h +397 -0
- snowflake/connector/nanoarrow_cpp/Logging/logging.cpp +100 -0
- snowflake/connector/nanoarrow_cpp/Logging/logging.hpp +49 -0
- snowflake/connector/network.py +1227 -0
- snowflake/connector/ocsp_asn1crypto.py +447 -0
- snowflake/connector/ocsp_snowflake.py +1939 -0
- snowflake/connector/options.py +179 -0
- snowflake/connector/os_details.py +88 -0
- snowflake/connector/pandas_tools.py +734 -0
- snowflake/connector/platform_detection.py +555 -0
- snowflake/connector/proxy.py +28 -0
- snowflake/connector/py.typed +0 -0
- snowflake/connector/result_batch.py +917 -0
- snowflake/connector/result_set.py +340 -0
- snowflake/connector/s3_storage_client.py +614 -0
- snowflake/connector/secret_detector.py +181 -0
- snowflake/connector/session_manager.py +650 -0
- snowflake/connector/sf_dirs.py +64 -0
- snowflake/connector/sfbinaryformat.py +35 -0
- snowflake/connector/sfdatetime.py +345 -0
- snowflake/connector/snow_logging.py +144 -0
- snowflake/connector/sqlstate.py +9 -0
- snowflake/connector/ssd_internal_keys.py +30 -0
- snowflake/connector/ssl_wrap_socket.py +281 -0
- snowflake/connector/storage_client.py +486 -0
- snowflake/connector/telemetry.py +254 -0
- snowflake/connector/telemetry_oob.py +544 -0
- snowflake/connector/test_util.py +30 -0
- snowflake/connector/time_util.py +159 -0
- snowflake/connector/token_cache.py +457 -0
- snowflake/connector/tool/__init__.py +0 -0
- snowflake/connector/tool/dump_certs.py +57 -0
- snowflake/connector/tool/dump_ocsp_response.py +139 -0
- snowflake/connector/tool/dump_ocsp_response_cache.py +194 -0
- snowflake/connector/tool/probe_connection.py +69 -0
- snowflake/connector/url_util.py +55 -0
- snowflake/connector/util_text.py +320 -0
- snowflake/connector/vendored/__init__.py +3 -0
- snowflake/connector/vendored/requests/LICENSE +175 -0
- snowflake/connector/vendored/requests/__init__.py +184 -0
- snowflake/connector/vendored/requests/__version__.py +14 -0
- snowflake/connector/vendored/requests/_internal_utils.py +50 -0
- snowflake/connector/vendored/requests/adapters.py +696 -0
- snowflake/connector/vendored/requests/api.py +157 -0
- snowflake/connector/vendored/requests/auth.py +314 -0
- snowflake/connector/vendored/requests/certs.py +17 -0
- snowflake/connector/vendored/requests/compat.py +106 -0
- snowflake/connector/vendored/requests/cookies.py +561 -0
- snowflake/connector/vendored/requests/exceptions.py +151 -0
- snowflake/connector/vendored/requests/help.py +134 -0
- snowflake/connector/vendored/requests/hooks.py +33 -0
- snowflake/connector/vendored/requests/models.py +1039 -0
- snowflake/connector/vendored/requests/packages.py +23 -0
- snowflake/connector/vendored/requests/sessions.py +831 -0
- snowflake/connector/vendored/requests/status_codes.py +128 -0
- snowflake/connector/vendored/requests/structures.py +99 -0
- snowflake/connector/vendored/requests/utils.py +1086 -0
- snowflake/connector/vendored/urllib3/LICENSE.txt +21 -0
- snowflake/connector/vendored/urllib3/__init__.py +211 -0
- snowflake/connector/vendored/urllib3/_base_connection.py +165 -0
- snowflake/connector/vendored/urllib3/_collections.py +487 -0
- snowflake/connector/vendored/urllib3/_request_methods.py +278 -0
- snowflake/connector/vendored/urllib3/_version.py +34 -0
- snowflake/connector/vendored/urllib3/connection.py +1108 -0
- snowflake/connector/vendored/urllib3/connectionpool.py +1178 -0
- snowflake/connector/vendored/urllib3/contrib/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/contrib/emscripten/__init__.py +17 -0
- snowflake/connector/vendored/urllib3/contrib/emscripten/connection.py +260 -0
- snowflake/connector/vendored/urllib3/contrib/emscripten/emscripten_fetch_worker.js +110 -0
- snowflake/connector/vendored/urllib3/contrib/emscripten/fetch.py +726 -0
- snowflake/connector/vendored/urllib3/contrib/emscripten/request.py +22 -0
- snowflake/connector/vendored/urllib3/contrib/emscripten/response.py +277 -0
- snowflake/connector/vendored/urllib3/contrib/pyopenssl.py +564 -0
- snowflake/connector/vendored/urllib3/contrib/socks.py +228 -0
- snowflake/connector/vendored/urllib3/exceptions.py +335 -0
- snowflake/connector/vendored/urllib3/fields.py +341 -0
- snowflake/connector/vendored/urllib3/filepost.py +89 -0
- snowflake/connector/vendored/urllib3/http2/__init__.py +53 -0
- snowflake/connector/vendored/urllib3/http2/connection.py +356 -0
- snowflake/connector/vendored/urllib3/http2/probe.py +87 -0
- snowflake/connector/vendored/urllib3/poolmanager.py +651 -0
- snowflake/connector/vendored/urllib3/py.typed +2 -0
- snowflake/connector/vendored/urllib3/response.py +1480 -0
- snowflake/connector/vendored/urllib3/util/__init__.py +42 -0
- snowflake/connector/vendored/urllib3/util/connection.py +137 -0
- snowflake/connector/vendored/urllib3/util/proxy.py +43 -0
- snowflake/connector/vendored/urllib3/util/request.py +263 -0
- snowflake/connector/vendored/urllib3/util/response.py +101 -0
- snowflake/connector/vendored/urllib3/util/retry.py +549 -0
- snowflake/connector/vendored/urllib3/util/ssl_.py +527 -0
- snowflake/connector/vendored/urllib3/util/ssl_match_hostname.py +159 -0
- snowflake/connector/vendored/urllib3/util/ssltransport.py +271 -0
- snowflake/connector/vendored/urllib3/util/timeout.py +275 -0
- snowflake/connector/vendored/urllib3/util/url.py +469 -0
- snowflake/connector/vendored/urllib3/util/util.py +42 -0
- snowflake/connector/vendored/urllib3/util/wait.py +124 -0
- snowflake/connector/version.py +3 -0
- snowflake/connector/wif_util.py +434 -0
- snowflake_connector_python-4.4.0.dist-info/DELVEWHEEL +2 -0
- snowflake_connector_python-4.4.0.dist-info/METADATA +1591 -0
- snowflake_connector_python-4.4.0.dist-info/RECORD +302 -0
- snowflake_connector_python-4.4.0.dist-info/WHEEL +5 -0
- snowflake_connector_python-4.4.0.dist-info/entry_points.txt +4 -0
- snowflake_connector_python-4.4.0.dist-info/licenses/LICENSE.txt +202 -0
- snowflake_connector_python-4.4.0.dist-info/licenses/NOTICE +8 -0
- snowflake_connector_python-4.4.0.dist-info/top_level.txt +1 -0
- snowflake_connector_python.libs/msvcp140-0f885b509a685d2bbfa652fed26b5fb3.dll +0 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import atexit
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import stat
|
|
10
|
+
import threading
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timedelta, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from cryptography import x509
|
|
17
|
+
from cryptography.hazmat.backends import default_backend
|
|
18
|
+
from cryptography.hazmat.primitives import serialization
|
|
19
|
+
from filelock import BaseFileLock, FileLock
|
|
20
|
+
|
|
21
|
+
from .compat import IS_WINDOWS
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CRLCacheEntry:
|
|
28
|
+
"""Cache entry containing a CRL and its download timestamp."""
|
|
29
|
+
|
|
30
|
+
crl: x509.CertificateRevocationList
|
|
31
|
+
download_time: datetime
|
|
32
|
+
|
|
33
|
+
def _next_update(self) -> datetime | None:
|
|
34
|
+
"""A compatibility wrapper around crl.next_update."""
|
|
35
|
+
return getattr(self.crl, "next_update_utc", None) or getattr(
|
|
36
|
+
self.crl, "next_update", None
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def is_crl_expired_by(self, ts: datetime) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Check if the CRL has expired.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
ts: Time to check against
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if the CRL has expired, False otherwise
|
|
48
|
+
"""
|
|
49
|
+
next_update = self._next_update()
|
|
50
|
+
return next_update is not None and next_update < ts
|
|
51
|
+
|
|
52
|
+
def is_evicted_by(self, ts: datetime, cache_validity_time: timedelta) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Check if the cache entry should be evicted based on cache validity time.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
ts: Current time to check against
|
|
58
|
+
cache_validity_time: How long cache entries remain valid
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if the entry should be evicted, False otherwise
|
|
62
|
+
"""
|
|
63
|
+
expiry_time = self.download_time + cache_validity_time
|
|
64
|
+
return expiry_time < ts
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CRLCache(ABC):
|
|
68
|
+
"""
|
|
69
|
+
Abstract base class for CRL caches.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def get(self, crl_url: str) -> CRLCacheEntry | None:
|
|
74
|
+
"""
|
|
75
|
+
Get a CRL cache entry by URL.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
crl_url: The CRL URL
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The cache entry if found, None otherwise
|
|
82
|
+
"""
|
|
83
|
+
raise NotImplementedError()
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def put(self, crl_url: str, entry: CRLCacheEntry) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Store a CRL cache entry.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
crl_url: The CRL URL
|
|
92
|
+
entry: The cache entry to store
|
|
93
|
+
"""
|
|
94
|
+
raise NotImplementedError()
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def cleanup(self) -> None:
|
|
98
|
+
"""Remove expired and evicted entries from the cache."""
|
|
99
|
+
raise NotImplementedError()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class NoopCRLCache(CRLCache):
|
|
103
|
+
"""
|
|
104
|
+
No-operation CRL cache that doesn't store anything.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# Singleton instance
|
|
108
|
+
INSTANCE = None
|
|
109
|
+
|
|
110
|
+
def __new__(cls):
|
|
111
|
+
if cls.INSTANCE is None:
|
|
112
|
+
cls.INSTANCE = super().__new__(cls)
|
|
113
|
+
return cls.INSTANCE
|
|
114
|
+
|
|
115
|
+
def get(self, crl_url: str) -> CRLCacheEntry | None:
|
|
116
|
+
"""Always returns None."""
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def put(self, crl_url: str, entry: CRLCacheEntry) -> None:
|
|
120
|
+
"""Does nothing."""
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
def cleanup(self) -> None:
|
|
124
|
+
"""Does nothing."""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class CRLInMemoryCache(CRLCache):
|
|
129
|
+
"""
|
|
130
|
+
In-memory CRL cache using a thread-safe dictionary.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, cache_validity_time: timedelta):
|
|
134
|
+
"""
|
|
135
|
+
Initialize the in-memory cache.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
cache_validity_time: How long cache entries remain valid
|
|
139
|
+
"""
|
|
140
|
+
self._cache: dict[str, CRLCacheEntry] = {}
|
|
141
|
+
self._cache_validity_time = cache_validity_time
|
|
142
|
+
self._lock = threading.RLock()
|
|
143
|
+
|
|
144
|
+
def get(self, crl_url: str) -> CRLCacheEntry | None:
|
|
145
|
+
"""
|
|
146
|
+
Get a CRL cache entry from memory.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
crl_url: The CRL URL
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The cache entry if found, None otherwise
|
|
153
|
+
"""
|
|
154
|
+
with self._lock:
|
|
155
|
+
entry = self._cache.get(crl_url)
|
|
156
|
+
if entry is not None:
|
|
157
|
+
logger.debug(f"Found CRL in memory cache for {crl_url}")
|
|
158
|
+
return entry
|
|
159
|
+
|
|
160
|
+
def put(self, crl_url: str, entry: CRLCacheEntry) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Store a CRL cache entry in memory.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
crl_url: The CRL URL
|
|
166
|
+
entry: The cache entry to store
|
|
167
|
+
"""
|
|
168
|
+
with self._lock:
|
|
169
|
+
self._cache[crl_url] = entry
|
|
170
|
+
|
|
171
|
+
def cleanup(self) -> None:
|
|
172
|
+
"""Remove expired and evicted entries from memory cache."""
|
|
173
|
+
now = datetime.now(timezone.utc)
|
|
174
|
+
logger.debug(f"Cleaning up in-memory CRL cache at {now}")
|
|
175
|
+
|
|
176
|
+
with self._lock:
|
|
177
|
+
urls_to_remove = []
|
|
178
|
+
|
|
179
|
+
for url, entry in self._cache.items():
|
|
180
|
+
expired = entry.is_crl_expired_by(now)
|
|
181
|
+
evicted = entry.is_evicted_by(now, self._cache_validity_time)
|
|
182
|
+
|
|
183
|
+
if expired or evicted:
|
|
184
|
+
logger.debug(
|
|
185
|
+
f"Removing in-memory CRL cache entry for {url}: "
|
|
186
|
+
f"expired={expired}, evicted={evicted}"
|
|
187
|
+
)
|
|
188
|
+
urls_to_remove.append(url)
|
|
189
|
+
|
|
190
|
+
for url in urls_to_remove:
|
|
191
|
+
del self._cache[url]
|
|
192
|
+
|
|
193
|
+
removed_count = len(urls_to_remove)
|
|
194
|
+
if removed_count > 0:
|
|
195
|
+
logger.debug(
|
|
196
|
+
f"Removed {removed_count} expired/evicted entries from in-memory CRL cache"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CRLFileCache(CRLCache):
|
|
201
|
+
"""
|
|
202
|
+
File-based CRL cache that persists CRLs to disk.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(
|
|
206
|
+
self,
|
|
207
|
+
cache_dir: Path | None = None,
|
|
208
|
+
removal_delay: timedelta | None = None,
|
|
209
|
+
unsafe_skip_file_permissions_check: bool = False,
|
|
210
|
+
):
|
|
211
|
+
"""
|
|
212
|
+
Initialize the file cache.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
cache_dir: Directory to store cached CRLs
|
|
216
|
+
removal_delay: How long to wait before removing expired files
|
|
217
|
+
unsafe_skip_file_permissions_check: Skip file permission validation for security
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
OSError: If cache directory cannot be created
|
|
221
|
+
"""
|
|
222
|
+
self._cache_file_lock_timeout = 5.0
|
|
223
|
+
self._cache_dir = cache_dir or _get_default_crl_cache_path()
|
|
224
|
+
self._removal_delay = removal_delay or timedelta(days=7)
|
|
225
|
+
self._unsafe_skip_file_permissions_check = unsafe_skip_file_permissions_check
|
|
226
|
+
|
|
227
|
+
self._ensure_cache_directory_exists()
|
|
228
|
+
|
|
229
|
+
def _ensure_cache_directory_exists(self) -> None:
|
|
230
|
+
"""Create the cache directory if it doesn't exist with secure permissions."""
|
|
231
|
+
try:
|
|
232
|
+
# Create directory with secure permissions (owner read/write/execute only)
|
|
233
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
234
|
+
|
|
235
|
+
# Verify directory permissions (if it already existed)
|
|
236
|
+
if not self._unsafe_skip_file_permissions_check:
|
|
237
|
+
self._check_permissions(self._cache_dir, "directory", "0o700")
|
|
238
|
+
|
|
239
|
+
logger.debug(f"Cache directory created/verified: {self._cache_dir}")
|
|
240
|
+
except PermissionError:
|
|
241
|
+
# Re-raise permission errors as-is
|
|
242
|
+
raise
|
|
243
|
+
except OSError as e:
|
|
244
|
+
raise OSError(f"Failed to create cache directory {self._cache_dir}: {e}")
|
|
245
|
+
|
|
246
|
+
def _get_crl_file_path(self, crl_url: str) -> Path:
|
|
247
|
+
"""
|
|
248
|
+
Generate a file path for the given CRL URL.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
crl_url: The CRL URL
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Path to the cache file
|
|
255
|
+
"""
|
|
256
|
+
# Create a safe filename from the URL using a hash
|
|
257
|
+
url_hash = hashlib.sha256(crl_url.encode()).hexdigest()
|
|
258
|
+
return self._cache_dir / f"crl_{url_hash}.der"
|
|
259
|
+
|
|
260
|
+
def _get_crl_file_lock(self, crl_cache_file: Path) -> BaseFileLock:
|
|
261
|
+
"""Return a lock instance for the given CRL cache file"""
|
|
262
|
+
return FileLock(
|
|
263
|
+
crl_cache_file.with_suffix(".lock"),
|
|
264
|
+
thread_local=True,
|
|
265
|
+
timeout=self._cache_file_lock_timeout,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def _check_permissions(
|
|
269
|
+
self, path: Path, resource_type: str, expected_perms: str
|
|
270
|
+
) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Check that a CRL cache resource has secure permissions (owner-only access).
|
|
273
|
+
|
|
274
|
+
Note: This check is only performed on Unix-like systems. Windows file
|
|
275
|
+
permissions work differently and are not checked.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
path: Path to the resource (file or directory) to check
|
|
279
|
+
resource_type: Description of the resource type (e.g., "file", "directory")
|
|
280
|
+
expected_perms: Description of expected permissions (e.g., "0o600 or 0o400", "0o700")
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
PermissionError: If resource permissions are too wide
|
|
284
|
+
"""
|
|
285
|
+
# Skip permission checks on Windows as they work differently
|
|
286
|
+
if IS_WINDOWS:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
stat_info = path.stat()
|
|
291
|
+
actual_permissions = stat.S_IMODE(stat_info.st_mode)
|
|
292
|
+
|
|
293
|
+
# Check that resource is accessible only by owner (no group/other permissions)
|
|
294
|
+
if (
|
|
295
|
+
actual_permissions & 0o077 != 0
|
|
296
|
+
): # Check if group or others have any permission
|
|
297
|
+
raise PermissionError(
|
|
298
|
+
f"CRL cache {resource_type} {path} has insecure permissions: {oct(actual_permissions)}. "
|
|
299
|
+
f"{resource_type.capitalize()} must be accessible only by the owner ({expected_perms})."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
except FileNotFoundError:
|
|
303
|
+
# Resource doesn't exist yet, this is fine
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
def get(self, crl_url: str) -> CRLCacheEntry | None:
|
|
307
|
+
"""
|
|
308
|
+
Get a CRL cache entry from disk.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
crl_url: The CRL URL
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
The cache entry if found, None otherwise
|
|
315
|
+
"""
|
|
316
|
+
crl_file_path = self._get_crl_file_path(crl_url)
|
|
317
|
+
with self._get_crl_file_lock(crl_file_path):
|
|
318
|
+
try:
|
|
319
|
+
if crl_file_path.exists():
|
|
320
|
+
logger.debug(f"Found CRL on disk for {crl_file_path}")
|
|
321
|
+
|
|
322
|
+
# Check file permissions before reading
|
|
323
|
+
if not self._unsafe_skip_file_permissions_check:
|
|
324
|
+
self._check_permissions(crl_file_path, "file", "0o600 or 0o400")
|
|
325
|
+
else:
|
|
326
|
+
logger.warning(
|
|
327
|
+
f"Skipping file permissions check for {crl_file_path}"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Get file modification time as download time
|
|
331
|
+
stat_info = crl_file_path.stat()
|
|
332
|
+
download_time = datetime.fromtimestamp(
|
|
333
|
+
stat_info.st_mtime, tz=timezone.utc
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Read and parse the CRL
|
|
337
|
+
with open(crl_file_path, "rb") as f:
|
|
338
|
+
crl_data = f.read()
|
|
339
|
+
|
|
340
|
+
crl = x509.load_der_x509_crl(crl_data, backend=default_backend())
|
|
341
|
+
return CRLCacheEntry(crl, download_time)
|
|
342
|
+
|
|
343
|
+
except PermissionError as e:
|
|
344
|
+
logger.error(
|
|
345
|
+
f"Permission error reading CRL from disk cache for {crl_url}: {e}"
|
|
346
|
+
)
|
|
347
|
+
return None
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.warning(f"Failed to read CRL from disk cache for {crl_url}: {e}")
|
|
350
|
+
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
def put(self, crl_url: str, entry: CRLCacheEntry) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Store a CRL cache entry to disk.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
crl_url: The CRL URL
|
|
359
|
+
entry: The cache entry to store
|
|
360
|
+
"""
|
|
361
|
+
crl_file_path = self._get_crl_file_path(crl_url)
|
|
362
|
+
with self._get_crl_file_lock(crl_file_path):
|
|
363
|
+
try:
|
|
364
|
+
# Serialize the CRL to DER format
|
|
365
|
+
crl_data = entry.crl.public_bytes(serialization.Encoding.DER)
|
|
366
|
+
|
|
367
|
+
# Write to file with secure permissions (owner read/write only)
|
|
368
|
+
# Using os.open with 0o600 ensures the file is created with secure permissions
|
|
369
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
|
370
|
+
if IS_WINDOWS:
|
|
371
|
+
# flag necessary for writing binary data to file on Windows OS
|
|
372
|
+
flags |= os.O_BINARY
|
|
373
|
+
fd = os.open(crl_file_path, flags, 0o600)
|
|
374
|
+
try:
|
|
375
|
+
os.write(fd, crl_data)
|
|
376
|
+
finally:
|
|
377
|
+
os.close(fd)
|
|
378
|
+
|
|
379
|
+
# Set file modification time to download time
|
|
380
|
+
download_timestamp = entry.download_time.timestamp()
|
|
381
|
+
os.utime(crl_file_path, (download_timestamp, download_timestamp))
|
|
382
|
+
|
|
383
|
+
logger.debug(f"Stored CRL to disk cache: {crl_file_path}")
|
|
384
|
+
|
|
385
|
+
except Exception as e:
|
|
386
|
+
logger.warning(f"Failed to write CRL to disk cache for {crl_url}: {e}")
|
|
387
|
+
|
|
388
|
+
def _is_cached_crl_file_for_removal(
|
|
389
|
+
self, crl_cache_file: Path, ts: datetime
|
|
390
|
+
) -> bool:
|
|
391
|
+
"""Check if the given CRL cache file is by its lifetime."""
|
|
392
|
+
try:
|
|
393
|
+
# Get file modification time
|
|
394
|
+
stat_info = crl_cache_file.stat()
|
|
395
|
+
download_time = datetime.fromtimestamp(stat_info.st_mtime, tz=timezone.utc)
|
|
396
|
+
|
|
397
|
+
# Check if file should be removed based on removal delay
|
|
398
|
+
removal_time = download_time + self._removal_delay
|
|
399
|
+
return ts > removal_time
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.warning(f"Error processing cache file {crl_cache_file}: {e}")
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
def cleanup(self) -> None:
|
|
405
|
+
"""Remove expired files from disk cache."""
|
|
406
|
+
now = datetime.now(timezone.utc)
|
|
407
|
+
logger.debug(f"Cleaning up file-based CRL cache at {now}")
|
|
408
|
+
|
|
409
|
+
removed_count = 0
|
|
410
|
+
try:
|
|
411
|
+
for crl_file in self._cache_dir.glob("crl_*.der"):
|
|
412
|
+
# double-checked locking
|
|
413
|
+
if self._is_cached_crl_file_for_removal(crl_file, now):
|
|
414
|
+
with self._get_crl_file_lock(crl_file):
|
|
415
|
+
if self._is_cached_crl_file_for_removal(crl_file, now):
|
|
416
|
+
crl_file.unlink(missing_ok=True)
|
|
417
|
+
removed_count += 1
|
|
418
|
+
logger.debug(f"Removed expired file: {crl_file}")
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.error(f"Error during file cache cleanup: {e}")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class CRLCacheManager:
|
|
424
|
+
"""
|
|
425
|
+
Cache manager that coordinates between in-memory and file-based CRL caches.
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
def __init__(
|
|
429
|
+
self,
|
|
430
|
+
memory_cache: CRLCache,
|
|
431
|
+
file_cache: CRLCache,
|
|
432
|
+
):
|
|
433
|
+
"""
|
|
434
|
+
Initialize the cache manager.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
memory_cache: In-memory cache implementation
|
|
438
|
+
file_cache: File-based cache implementation
|
|
439
|
+
"""
|
|
440
|
+
self._memory_cache = memory_cache
|
|
441
|
+
self._file_cache = file_cache
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def noop(cls) -> CRLCacheManager:
|
|
445
|
+
"""Create noop cache manager."""
|
|
446
|
+
return cls(NoopCRLCache(), NoopCRLCache())
|
|
447
|
+
|
|
448
|
+
def get(self, crl_url: str) -> CRLCacheEntry | None:
|
|
449
|
+
"""
|
|
450
|
+
Get a CRL cache entry, checking memory cache first, then file cache.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
crl_url: The CRL URL
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
The cache entry if found, None otherwise
|
|
457
|
+
"""
|
|
458
|
+
# Check memory cache first
|
|
459
|
+
entry = self._memory_cache.get(crl_url)
|
|
460
|
+
if entry is not None:
|
|
461
|
+
return entry
|
|
462
|
+
|
|
463
|
+
# Check file cache
|
|
464
|
+
entry = self._file_cache.get(crl_url)
|
|
465
|
+
if entry is not None:
|
|
466
|
+
# Promote to memory cache
|
|
467
|
+
self._memory_cache.put(crl_url, entry)
|
|
468
|
+
return entry
|
|
469
|
+
|
|
470
|
+
logger.debug(f"CRL not found in cache for {crl_url}")
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
def put(
|
|
474
|
+
self, crl_url: str, crl: x509.CertificateRevocationList, download_time: datetime
|
|
475
|
+
) -> None:
|
|
476
|
+
"""
|
|
477
|
+
Store a CRL in both memory and file caches.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
crl_url: The CRL URL
|
|
481
|
+
crl: The CRL to store
|
|
482
|
+
download_time: When the CRL was downloaded
|
|
483
|
+
"""
|
|
484
|
+
entry = CRLCacheEntry(crl, download_time)
|
|
485
|
+
self._memory_cache.put(crl_url, entry)
|
|
486
|
+
self._file_cache.put(crl_url, entry)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class CRLCacheFactory:
|
|
490
|
+
"""
|
|
491
|
+
Factory class for creating singleton instances of CRL caches.
|
|
492
|
+
|
|
493
|
+
This factory ensures that only one instance of each cache type exists,
|
|
494
|
+
providing warnings when attempting to create instances with different parameters.
|
|
495
|
+
Also manages background cleanup of existing cache instances.
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
# Singleton instances
|
|
499
|
+
_memory_cache_instance = None
|
|
500
|
+
_file_cache_instance = None
|
|
501
|
+
_instance_lock = threading.RLock()
|
|
502
|
+
|
|
503
|
+
# Cleanup management
|
|
504
|
+
_cleanup_thread: threading.Thread | None = None
|
|
505
|
+
_cleanup_shutdown: threading.Event = threading.Event()
|
|
506
|
+
_cleanup_interval: timedelta | None = None
|
|
507
|
+
_atexit_registered: bool = False
|
|
508
|
+
|
|
509
|
+
@classmethod
|
|
510
|
+
def get_memory_cache(cls, cache_validity_time: timedelta) -> CRLInMemoryCache:
|
|
511
|
+
"""
|
|
512
|
+
Get or create a singleton CRLInMemoryCache instance.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
cache_validity_time: How long cache entries remain valid
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
The singleton CRLInMemoryCache instance
|
|
519
|
+
"""
|
|
520
|
+
with cls._instance_lock:
|
|
521
|
+
if cls._memory_cache_instance is None:
|
|
522
|
+
cls._memory_cache_instance = CRLInMemoryCache(cache_validity_time)
|
|
523
|
+
elif cls._memory_cache_instance._cache_validity_time != cache_validity_time:
|
|
524
|
+
logger.warning(
|
|
525
|
+
f"CRLs in-memory cache has already been initialized with cache validity time of {cls._memory_cache_instance._cache_validity_time}, "
|
|
526
|
+
f"ignoring new cache validity time of {cache_validity_time}"
|
|
527
|
+
)
|
|
528
|
+
return cls._memory_cache_instance
|
|
529
|
+
|
|
530
|
+
@classmethod
|
|
531
|
+
def get_file_cache(
|
|
532
|
+
cls,
|
|
533
|
+
cache_dir: Path | None = None,
|
|
534
|
+
removal_delay: timedelta | None = None,
|
|
535
|
+
unsafe_skip_file_permissions_check: bool = False,
|
|
536
|
+
) -> CRLFileCache:
|
|
537
|
+
"""
|
|
538
|
+
Get or create a singleton CRLFileCache instance.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
cache_dir: Directory to store cached CRLs
|
|
542
|
+
removal_delay: How long to wait before removing expired files
|
|
543
|
+
unsafe_skip_file_permissions_check: Skip file permission validation for security
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
The singleton CRLFileCache instance
|
|
547
|
+
"""
|
|
548
|
+
with cls._instance_lock:
|
|
549
|
+
if cls._file_cache_instance is None:
|
|
550
|
+
cls._file_cache_instance = CRLFileCache(
|
|
551
|
+
cache_dir, removal_delay, unsafe_skip_file_permissions_check
|
|
552
|
+
)
|
|
553
|
+
else:
|
|
554
|
+
# Check if parameters differ from existing instance
|
|
555
|
+
existing_cache_dir = cls._file_cache_instance._cache_dir
|
|
556
|
+
existing_removal_delay = cls._file_cache_instance._removal_delay
|
|
557
|
+
existing_skip_check = (
|
|
558
|
+
cls._file_cache_instance._unsafe_skip_file_permissions_check
|
|
559
|
+
)
|
|
560
|
+
requested_cache_dir = cache_dir or _get_default_crl_cache_path()
|
|
561
|
+
requested_removal_delay = removal_delay or timedelta(days=7)
|
|
562
|
+
|
|
563
|
+
if existing_cache_dir != requested_cache_dir:
|
|
564
|
+
logger.warning(
|
|
565
|
+
f"CRLs file cache has already been initialized with cache directory '{existing_cache_dir}', "
|
|
566
|
+
f"ignoring new cache directory '{requested_cache_dir}'"
|
|
567
|
+
)
|
|
568
|
+
if existing_removal_delay != requested_removal_delay:
|
|
569
|
+
logger.warning(
|
|
570
|
+
f"CRLs file cache has already been initialized with removal delay of {existing_removal_delay}, "
|
|
571
|
+
f"ignoring new removal delay of {requested_removal_delay}"
|
|
572
|
+
)
|
|
573
|
+
if existing_skip_check != unsafe_skip_file_permissions_check:
|
|
574
|
+
logger.warning(
|
|
575
|
+
f"CRLs file cache has already been initialized with unsafe_skip_file_permissions_check={existing_skip_check}, "
|
|
576
|
+
f"ignoring new value {unsafe_skip_file_permissions_check}"
|
|
577
|
+
)
|
|
578
|
+
return cls._file_cache_instance
|
|
579
|
+
|
|
580
|
+
@classmethod
|
|
581
|
+
def start_periodic_cleanup(cls, cleanup_interval: timedelta) -> None:
|
|
582
|
+
"""
|
|
583
|
+
Start the periodic cleanup task for existing cache instances.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
cleanup_interval: How often to run cleanup tasks
|
|
587
|
+
"""
|
|
588
|
+
with cls._instance_lock:
|
|
589
|
+
if cls.is_periodic_cleanup_running():
|
|
590
|
+
logger.debug(
|
|
591
|
+
"Periodic cleanup already running, so it will first be stopped before restarting."
|
|
592
|
+
)
|
|
593
|
+
cls.stop_periodic_cleanup()
|
|
594
|
+
|
|
595
|
+
cls._cleanup_interval = cleanup_interval
|
|
596
|
+
cls._cleanup_thread = threading.Thread(
|
|
597
|
+
target=cls._cleanup_loop,
|
|
598
|
+
name="crl-cache-cleanup",
|
|
599
|
+
daemon=True, # Make it a daemon thread so it doesn't block program exit
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Register atexit handler for graceful shutdown (only once)
|
|
603
|
+
if not cls._atexit_registered:
|
|
604
|
+
atexit.register(cls._atexit_cleanup_handler)
|
|
605
|
+
cls._atexit_registered = True
|
|
606
|
+
|
|
607
|
+
# Start the cleanup thread
|
|
608
|
+
cls._cleanup_thread.start()
|
|
609
|
+
|
|
610
|
+
logger.debug(
|
|
611
|
+
f"Scheduled CRL cache cleanup task to run every {cleanup_interval.total_seconds()} seconds."
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
@classmethod
|
|
615
|
+
def stop_periodic_cleanup(cls) -> None:
|
|
616
|
+
"""Stop the periodic cleanup task."""
|
|
617
|
+
thread_to_join = None
|
|
618
|
+
|
|
619
|
+
with cls._instance_lock:
|
|
620
|
+
if cls._cleanup_thread is None or cls._cleanup_shutdown.is_set():
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
cls._cleanup_shutdown.set()
|
|
624
|
+
thread_to_join = cls._cleanup_thread
|
|
625
|
+
|
|
626
|
+
# Join thread outside of lock to avoid deadlock
|
|
627
|
+
if thread_to_join is not None and thread_to_join.is_alive():
|
|
628
|
+
thread_to_join.join(timeout=5.0)
|
|
629
|
+
|
|
630
|
+
with cls._instance_lock:
|
|
631
|
+
cls._cleanup_shutdown.clear()
|
|
632
|
+
cls._cleanup_thread = None
|
|
633
|
+
cls._cleanup_interval = None
|
|
634
|
+
|
|
635
|
+
@classmethod
|
|
636
|
+
def is_periodic_cleanup_running(cls) -> bool:
|
|
637
|
+
"""Check if periodic cleanup task is running."""
|
|
638
|
+
with cls._instance_lock:
|
|
639
|
+
return cls._cleanup_thread is not None and cls._cleanup_thread.is_alive()
|
|
640
|
+
|
|
641
|
+
@classmethod
|
|
642
|
+
def _cleanup_loop(cls) -> None:
|
|
643
|
+
"""Main cleanup loop that runs periodically."""
|
|
644
|
+
while not cls._cleanup_shutdown.is_set():
|
|
645
|
+
if cls._cleanup_interval is None:
|
|
646
|
+
break
|
|
647
|
+
|
|
648
|
+
logger.debug(
|
|
649
|
+
f"Running periodic CRL cache cleanup with interval {cls._cleanup_interval.total_seconds()} seconds"
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Clean memory cache only if it exists
|
|
653
|
+
if cls._memory_cache_instance is not None:
|
|
654
|
+
try:
|
|
655
|
+
cls._memory_cache_instance.cleanup()
|
|
656
|
+
except Exception as e:
|
|
657
|
+
logger.error(
|
|
658
|
+
f"An error occurred during scheduled CRL memory cache cleanup: {e}"
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# Clean file cache only if it exists
|
|
662
|
+
if cls._file_cache_instance is not None:
|
|
663
|
+
try:
|
|
664
|
+
cls._file_cache_instance.cleanup()
|
|
665
|
+
except Exception as e:
|
|
666
|
+
logger.error(
|
|
667
|
+
f"An error occurred during scheduled CRL disk cache cleanup: {e}"
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
shutdown = cls._cleanup_shutdown.wait(
|
|
671
|
+
timeout=cls._cleanup_interval.total_seconds()
|
|
672
|
+
)
|
|
673
|
+
if shutdown:
|
|
674
|
+
logger.debug(
|
|
675
|
+
"CRL cache cleanup stopped gracefully by a shutdown event."
|
|
676
|
+
)
|
|
677
|
+
break
|
|
678
|
+
|
|
679
|
+
@classmethod
|
|
680
|
+
def _atexit_cleanup_handler(cls) -> None:
|
|
681
|
+
"""
|
|
682
|
+
Atexit handler to ensure graceful shutdown of periodic cleanup on program exit.
|
|
683
|
+
"""
|
|
684
|
+
try:
|
|
685
|
+
cls.stop_periodic_cleanup()
|
|
686
|
+
logger.debug("CRL cache cleanup stopped gracefully on program exit.")
|
|
687
|
+
except Exception as e:
|
|
688
|
+
# Don't raise exceptions in atexit handlers
|
|
689
|
+
logger.error(f"Error stopping CRL cache cleanup on program exit: {e}")
|
|
690
|
+
|
|
691
|
+
@classmethod
|
|
692
|
+
def reset(cls) -> None:
|
|
693
|
+
"""
|
|
694
|
+
Reset the factory, clearing all singleton instances and stopping cleanup.
|
|
695
|
+
This is primarily useful for testing purposes.
|
|
696
|
+
"""
|
|
697
|
+
with cls._instance_lock:
|
|
698
|
+
cls.stop_periodic_cleanup()
|
|
699
|
+
cls._memory_cache_instance = None
|
|
700
|
+
cls._file_cache_instance = None
|
|
701
|
+
cls._atexit_registered = False
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _get_windows_home_path() -> Path:
|
|
705
|
+
try:
|
|
706
|
+
return Path.home()
|
|
707
|
+
except RuntimeError:
|
|
708
|
+
pass
|
|
709
|
+
if "USERPROFILE" in os.environ:
|
|
710
|
+
return Path(os.environ["USERPROFILE"])
|
|
711
|
+
if "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ:
|
|
712
|
+
return Path(os.environ["HOMEDRIVE"]) / os.environ["HOMEPATH"]
|
|
713
|
+
if "LOCALAPPDATA" in os.environ:
|
|
714
|
+
return Path(os.environ["LOCALAPPDATA"]).parent.parent
|
|
715
|
+
if "APPDATA" in os.environ:
|
|
716
|
+
return Path(os.environ["APPDATA"]).parent.parent
|
|
717
|
+
return Path("~")
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _get_default_crl_cache_path() -> Path:
|
|
721
|
+
"""Return the default path to persist cached CRLs."""
|
|
722
|
+
if platform.system() == "Windows":
|
|
723
|
+
return (
|
|
724
|
+
_get_windows_home_path()
|
|
725
|
+
/ "AppData"
|
|
726
|
+
/ "Local"
|
|
727
|
+
/ "Snowflake"
|
|
728
|
+
/ "Caches"
|
|
729
|
+
/ "crls"
|
|
730
|
+
)
|
|
731
|
+
elif platform.system() == "Darwin":
|
|
732
|
+
return Path.home() / "Library" / "Caches" / "Snowflake" / "crls"
|
|
733
|
+
else:
|
|
734
|
+
return Path.home() / ".cache" / "Snowflake" / "crls"
|