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:
Hyungi Ahn
2026-03-19 13:53:55 +09:00
parent dc08d29509
commit c2257d3a86
2709 changed files with 619549 additions and 10 deletions

View File

@@ -0,0 +1,113 @@
"""
the Wassima library is a simple library.
It aims to provide a pythonic way to retrieve root CAs from your system without any difficulties or hazmat.
"""
from __future__ import annotations
import ssl
from functools import lru_cache
from threading import RLock
from ._os import (
root_der_certificates as _root_der_certificates,
)
from ._os._embed import root_der_certificates as fallback_der_certificates
from ._version import VERSION, __version__
# Mozilla TLS recommendations for ciphers
# General-purpose servers with a variety of clients, recommended for almost all systems.
MOZ_INTERMEDIATE_CIPHERS: str = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305" # noqa: E501
#: Contain user custom CAs
_MANUALLY_REGISTERED_CA: list[bytes] = []
#: Lock for shared register-ca
_USER_APPEND_CA_LOCK = RLock()
@lru_cache()
def root_der_certificates() -> list[bytes]:
with _USER_APPEND_CA_LOCK:
certificates = _root_der_certificates()
if not certificates:
certificates = fallback_der_certificates()
certificates.extend(_MANUALLY_REGISTERED_CA)
return certificates
@lru_cache()
def root_pem_certificates() -> list[str]:
"""
Retrieve a list of root certificate from your operating system trust store.
They will be PEM encoded.
"""
pem_certs = []
for bin_cert in root_der_certificates():
pem_certs.append(ssl.DER_cert_to_PEM_cert(bin_cert))
return pem_certs
def generate_ca_bundle() -> str:
"""
Generate an aggregated CA bundle that originate from your system trust store.
Simply put, concatenated root PEM certificate.
"""
return "\n\n".join(root_pem_certificates())
def register_ca(pem_or_der_certificate: bytes | str) -> None:
"""
You may register your own CA certificate in addition to your system trust store.
"""
with _USER_APPEND_CA_LOCK:
if isinstance(pem_or_der_certificate, str):
pem_or_der_certificate = ssl.PEM_cert_to_DER_cert(pem_or_der_certificate)
if pem_or_der_certificate not in _MANUALLY_REGISTERED_CA:
_MANUALLY_REGISTERED_CA.append(pem_or_der_certificate)
root_pem_certificates.cache_clear()
root_der_certificates.cache_clear()
def create_default_ssl_context() -> ssl.SSLContext:
"""
Instantiate a native SSLContext (client purposes) that ships with your system root CAs.
In addition to that, assign it the default OpenSSL ciphers suite and set
TLS 1.2 as the minimum supported version. Also disable commonName check and enforce
hostname altName verification. The Mozilla Recommended Cipher Suite is used instead of system default.
"""
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.load_verify_locations(cadata=generate_ca_bundle())
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.set_ciphers(MOZ_INTERMEDIATE_CIPHERS)
ctx.verify_mode = ssl.CERT_REQUIRED
try:
ctx.hostname_checks_common_name = False
except AttributeError:
pass
try:
ctx.check_hostname = True
except AttributeError: # Defensive: very old 3.7 branch
pass
return ctx
__all__ = (
"root_der_certificates",
"root_pem_certificates",
"generate_ca_bundle",
"create_default_ssl_context",
"register_ca",
"__version__",
"VERSION",
)

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
import platform
import sys
# Platform detection
IS_WINDOWS = sys.platform == "win32"
IS_MACOS = sys.platform == "darwin"
IS_LINUX = sys.platform.startswith("linux")
IS_BSD = sys.platform.startswith(("freebsd", "openbsd", "netbsd"))
# macOS version detection
MACOS_VERSION: tuple[int, ...] | None = None
if IS_MACOS:
version_str = platform.mac_ver()[0]
MACOS_VERSION = tuple(map(int, version_str.split(".")))
if IS_WINDOWS:
from ._windows import root_der_certificates
elif IS_MACOS and MACOS_VERSION >= (10, 15): # type: ignore[operator]
from ._macos import root_der_certificates
elif IS_LINUX or IS_BSD:
from ._linux import root_der_certificates
else:
from ._embed import root_der_certificates
__all__ = ("root_der_certificates",)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
import os
from pathlib import Path
from ssl import PEM_cert_to_DER_cert
# source: http://gagravarr.org/writing/openssl-certs/others.shtml
BUNDLE_TRUST_STORE_DIRECTORIES: list[str] = [
"/var/ssl",
"/usr/share/ssl",
"/usr/local/ssl",
"/usr/local/openssl",
"/usr/local/etc/openssl",
"/usr/local/share/certs",
"/usr/lib/ssl",
"/usr/ssl",
"/etc/openssl",
"/etc/pki/ca-trust/extracted/pem",
"/etc/pki/tls",
"/etc/ssl",
"/etc/certs",
"/opt/etc/ssl",
"/system/etc/security/cacerts",
"/boot/system/data/ssl",
]
KNOWN_TRUST_STORE_EXTENSIONS: list[str] = [
"pem",
"crt",
]
BANNED_KEYWORD_NOT_TLS: set[str] = {
"email",
"objsign",
"trust",
"timestamp",
"codesign",
"ocsp",
"untrusted",
}
def root_der_certificates() -> list[bytes]:
certificates: list[bytes] = []
for directory in BUNDLE_TRUST_STORE_DIRECTORIES:
if not os.path.exists(directory):
continue
# Use rglob to recursively search all files in directory and subdirectories
for filepath in Path(directory).rglob("*"):
try:
if not filepath.is_file(): # Skip directories
continue
extension = filepath.suffix.lstrip(".").lower()
if extension not in KNOWN_TRUST_STORE_EXTENSIONS and extension.isdigit() is False:
continue
if any(kw in str(filepath).lower() for kw in BANNED_KEYWORD_NOT_TLS):
continue
with open(filepath, encoding="utf-8") as f:
bundle = f.read()
if not bundle.strip(): # Skip empty files
continue # Defensive:
line_ending = "\n" if "-----END CERTIFICATE-----\r\n" not in bundle else "\r\n"
boundary = "-----END CERTIFICATE-----" + line_ending
for chunk in bundle.split(boundary):
if chunk:
start_marker = chunk.find("-----BEGIN CERTIFICATE-----" + line_ending)
if start_marker == -1:
break # Defensive: file that aren't PEM encoded in target directories(...)
pem_reconstructed = "".join([chunk[start_marker:], boundary])
try:
der_certificate = PEM_cert_to_DER_cert(pem_reconstructed)
except ValueError: # Defensive: malformed cert/base64?
continue
if der_certificate not in certificates:
certificates.append(der_certificate)
except (OSError, UnicodeDecodeError): # Defensive: Skip files we can't read
# OSError -> e.g. PermissionError
# UnicodeDecodeError -> DER ASN.1 encoded
continue
return certificates

View File

@@ -0,0 +1,142 @@
from __future__ import annotations
import ctypes
from ctypes import POINTER, byref, c_int32, c_uint32, c_void_p
# Load frameworks
_core = ctypes.CDLL("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")
_sec = ctypes.CDLL("/System/Library/Frameworks/Security.framework/Security")
# Type aliases
CFTypeRef = c_void_p
CFArrayRef = c_void_p
CFDataRef = c_void_p
CFDictionaryRef = c_void_p
OSStatus = c_int32
# CoreFoundation function prototypes
_CFDictionaryCreate = _core.CFDictionaryCreate
_CFDictionaryCreate.argtypes = [c_void_p, POINTER(c_void_p), POINTER(c_void_p), c_uint32, c_void_p, c_void_p]
_CFDictionaryCreate.restype = CFDictionaryRef
_CFArrayGetCount = _core.CFArrayGetCount
_CFArrayGetCount.argtypes = [CFArrayRef]
_CFArrayGetCount.restype = c_uint32
_CFArrayGetValueAtIndex = _core.CFArrayGetValueAtIndex
_CFArrayGetValueAtIndex.argtypes = [CFArrayRef, c_uint32]
_CFArrayGetValueAtIndex.restype = CFTypeRef
_CFDataGetLength = _core.CFDataGetLength
_CFDataGetLength.argtypes = [CFDataRef]
_CFDataGetLength.restype = c_uint32
_CFDataGetBytePtr = _core.CFDataGetBytePtr
_CFDataGetBytePtr.argtypes = [CFDataRef]
_CFDataGetBytePtr.restype = ctypes.POINTER(ctypes.c_ubyte)
_CFRelease = _core.CFRelease
_CFRelease.argtypes = [c_void_p]
_CFRelease.restype = None
# Security function prototypes
_SecItemCopyMatching = _sec.SecItemCopyMatching
_SecItemCopyMatching.argtypes = [CFDictionaryRef, POINTER(CFTypeRef)]
_SecItemCopyMatching.restype = OSStatus
_SecCertificateCopyData = _sec.SecCertificateCopyData
_SecCertificateCopyData.argtypes = [CFTypeRef]
_SecCertificateCopyData.restype = CFDataRef
_SecTrustSettingsCopyCertificates = _sec.SecTrustSettingsCopyCertificates
_SecTrustSettingsCopyCertificates.argtypes = [c_int32, POINTER(CFArrayRef)]
_SecTrustSettingsCopyCertificates.restype = OSStatus
# CF callbacks & boolean constants
_kCFTypeDictKeyCallBacks = c_void_p.in_dll(_core, "kCFTypeDictionaryKeyCallBacks")
_kCFTypeDictValueCallBacks = c_void_p.in_dll(_core, "kCFTypeDictionaryValueCallBacks")
_kCFBooleanTrue = c_void_p.in_dll(_core, "kCFBooleanTrue")
_kSecBooleanTrue = _kCFBooleanTrue
# SecItem constants
_kSecClass = c_void_p.in_dll(_sec, "kSecClass")
_kSecClassCertificate = c_void_p.in_dll(_sec, "kSecClassCertificate")
_kSecMatchLimit = c_void_p.in_dll(_sec, "kSecMatchLimit")
_kSecMatchLimitAll = c_void_p.in_dll(_sec, "kSecMatchLimitAll")
_kSecMatchTrustedOnly = c_void_p.in_dll(_sec, "kSecMatchTrustedOnly")
_kSecReturnRef = c_void_p.in_dll(_sec, "kSecReturnRef")
# Helper: build a CFDictionary for SecItem queries
def _make_query(keys: list[c_void_p], values: list[c_void_p]) -> CFDictionaryRef:
count = len(keys)
KeyArr = (c_void_p * count)(*keys)
ValArr = (c_void_p * count)(*values)
return _CFDictionaryCreate( # type: ignore[no-any-return]
None,
KeyArr,
ValArr,
count,
_kCFTypeDictKeyCallBacks,
_kCFTypeDictValueCallBacks,
)
# Helper: perform SecItemCopyMatching and return CFTypeRef list
def _query_refs(query: CFDictionaryRef) -> list[CFTypeRef]:
result = CFTypeRef()
status = _SecItemCopyMatching(query, byref(result))
if status != 0:
raise OSError(f"SecItemCopyMatching failed with status={status}") # Defensive: OOM?
array_ref = CFArrayRef(result.value)
count = _CFArrayGetCount(array_ref)
items = [_CFArrayGetValueAtIndex(array_ref, i) for i in range(count)]
# Note: No CFRelease() calls to avoid premature deallocation
return items
# Convert CFDataRef to Python bytes
def _data_to_bytes(data_ref: c_void_p) -> bytes:
length = _CFDataGetLength(data_ref)
ptr = _CFDataGetBytePtr(data_ref)
data = bytes(ctypes.string_at(ptr, length))
_CFRelease(data_ref)
return data
# Public: retrieve DER-encoded trusted certificates
def root_der_certificates() -> list[bytes]:
"""
Returns a list of DER-encoded certificates trusted for TLS server auth,
covering system roots, admin, user trust settings, and personal CAs.
"""
certificates: list[bytes] = []
# 1) System/user/admin trust settings
for domain in (0, 1, 2):
cert_array = CFArrayRef()
status = _SecTrustSettingsCopyCertificates(domain, byref(cert_array))
if status == 0:
count = _CFArrayGetCount(cert_array)
for i in range(count):
cert_ref = _CFArrayGetValueAtIndex(cert_array, i)
certificates.append(_data_to_bytes(_SecCertificateCopyData(cert_ref)))
# 2) Personal CA certificates from keychain marked trusted
query = _make_query(
keys=[_kSecClass, _kSecMatchLimit, _kSecMatchTrustedOnly, _kSecReturnRef],
values=[_kSecClassCertificate, _kSecMatchLimitAll, _kSecBooleanTrue, _kSecReturnRef],
)
try:
cert_refs = _query_refs(query)
for c in cert_refs:
certificates.append(_data_to_bytes(_SecCertificateCopyData(c)))
except OSError: # Defensive: OOM?
pass
return certificates

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from ssl import enum_certificates # type: ignore[attr-defined]
# ROOT: Highest level of trust. Trust anchors. Self-Signed.
# MY: User installed/custom trust anchors. Self-Signed.
# CA: Intermediates CA. Not trusted directly, not self-signed.
WINDOWS_STORES: list[str] = [
"ROOT",
"MY",
"CA",
]
SERVER_AUTH_OID: str = "1.3.6.1.5.5.7.3.1"
def root_der_certificates() -> list[bytes]:
certificates = []
for system_store in WINDOWS_STORES:
try:
for cert_bytes, encoding_type, trust in enum_certificates(system_store):
if not trust:
continue # Defensive: edge case, rare one.
# if not True, then, we MUST LOOK for SERVER_AUTH oid EKU
if not isinstance(trust, bool) and SERVER_AUTH_OID not in trust:
continue
# Check it's in X.509 ASN.1 format and is trusted
if (
encoding_type == "x509_asn" # X.509 ASN.1 data
):
certificates.append(cert_bytes)
except PermissionError: # Defensive: we can't cover that scenario in CI.
continue
return certificates
__all__ = ("root_der_certificates",)

View File

@@ -0,0 +1,4 @@
from __future__ import annotations
__version__ = "2.0.5"
VERSION = __version__.split(".")