mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 15:21:57 -06:00
Additional Changes to Code
This commit is contained in:
@@ -24,6 +24,7 @@ import threading
|
|||||||
import contextlib
|
import contextlib
|
||||||
import errno
|
import errno
|
||||||
import re
|
import re
|
||||||
|
from urllib.parse import urlparse
|
||||||
from typing import Any, Dict, Optional, List, Callable, Tuple
|
from typing import Any, Dict, Optional, List, Callable, Tuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -39,6 +40,18 @@ from security import AgentKeyStore
|
|||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
|
|
||||||
|
def _iter_exception_chain(exc: BaseException):
|
||||||
|
seen = set()
|
||||||
|
while exc and exc not in seen:
|
||||||
|
yield exc
|
||||||
|
seen.add(exc)
|
||||||
|
if exc.__cause__ is not None:
|
||||||
|
exc = exc.__cause__
|
||||||
|
elif exc.__context__ is not None:
|
||||||
|
exc = exc.__context__
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
# Centralized logging helpers (Agent)
|
# Centralized logging helpers (Agent)
|
||||||
def _agent_logs_root() -> str:
|
def _agent_logs_root() -> str:
|
||||||
try:
|
try:
|
||||||
@@ -819,6 +832,8 @@ class AgentHttpClient:
|
|||||||
self._auth_lock = threading.RLock()
|
self._auth_lock = threading.RLock()
|
||||||
self._active_installer_code: Optional[str] = None
|
self._active_installer_code: Optional[str] = None
|
||||||
self._cached_ssl_context: Optional[ssl.SSLContext] = None
|
self._cached_ssl_context: Optional[ssl.SSLContext] = None
|
||||||
|
self._socketio_http_session = None
|
||||||
|
self._socketio_session_mode: Optional[Tuple[str, Optional[str]]] = None
|
||||||
self.refresh_base_url()
|
self.refresh_base_url()
|
||||||
self._configure_verify()
|
self._configure_verify()
|
||||||
self._reload_tokens_from_disk()
|
self._reload_tokens_from_disk()
|
||||||
@@ -979,6 +994,7 @@ class AgentHttpClient:
|
|||||||
f"layered_default={bundle_summary['layered_default']}",
|
f"layered_default={bundle_summary['layered_default']}",
|
||||||
fname="agent.log",
|
fname="agent.log",
|
||||||
)
|
)
|
||||||
|
self._apply_socketio_transport(client, context, verify=True, fingerprint=bundle_summary["fingerprint"])
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fall back to boolean verification flags when we either do not
|
# Fall back to boolean verification flags when we either do not
|
||||||
@@ -993,6 +1009,7 @@ class AgentHttpClient:
|
|||||||
_set_attr(http_iface, "ssl_verify", verify_flag)
|
_set_attr(http_iface, "ssl_verify", verify_flag)
|
||||||
_set_attr(http_iface, "verify_ssl", verify_flag)
|
_set_attr(http_iface, "verify_ssl", verify_flag)
|
||||||
_reset_cached_session()
|
_reset_cached_session()
|
||||||
|
self._apply_socketio_transport(client, None if verify_flag else False, verify=verify_flag, fingerprint=None)
|
||||||
_log_agent(
|
_log_agent(
|
||||||
f"SocketIO TLS alignment fallback verify_flag={verify_flag}",
|
f"SocketIO TLS alignment fallback verify_flag={verify_flag}",
|
||||||
fname="agent.log",
|
fname="agent.log",
|
||||||
@@ -1005,32 +1022,155 @@ class AgentHttpClient:
|
|||||||
_log_exception_trace("configure_socketio")
|
_log_exception_trace("configure_socketio")
|
||||||
|
|
||||||
def socketio_ssl_params(self) -> Dict[str, Any]:
|
def socketio_ssl_params(self) -> Dict[str, Any]:
|
||||||
verify = getattr(self.session, "verify", True)
|
# Socket.IO AsyncClient.connect does not accept SSL kwargs; configuration is
|
||||||
if isinstance(verify, str) and os.path.isfile(verify):
|
# handled via the Engine.IO client setup in configure_socketio.
|
||||||
context = self._cached_ssl_context
|
return {}
|
||||||
if context is None:
|
|
||||||
context = self.key_store.build_ssl_context()
|
def _schedule_socketio_session_close(self, session) -> None:
|
||||||
if context is not None:
|
if session is None:
|
||||||
self._cached_ssl_context = context
|
return
|
||||||
if context is not None:
|
try:
|
||||||
return {"ssl_verify": verify}
|
if session.closed:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
loop.create_task(session.close())
|
||||||
|
except RuntimeError:
|
||||||
try:
|
try:
|
||||||
fallback = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
|
asyncio.run(session.close())
|
||||||
fallback.load_verify_locations(cafile=verify)
|
except Exception:
|
||||||
self._cached_ssl_context = fallback
|
pass
|
||||||
return {"ssl_verify": verify}
|
|
||||||
|
def _set_socketio_http_session(
|
||||||
|
self,
|
||||||
|
client: "socketio.AsyncClient",
|
||||||
|
session,
|
||||||
|
mode: Optional[Tuple[str, Optional[str]]],
|
||||||
|
) -> None:
|
||||||
|
engine = getattr(client, "eio", None)
|
||||||
|
if engine is None:
|
||||||
|
return
|
||||||
|
if self._socketio_http_session is not session and self._socketio_http_session is not None:
|
||||||
|
self._schedule_socketio_session_close(self._socketio_http_session)
|
||||||
|
self._socketio_http_session = session
|
||||||
|
self._socketio_session_mode = mode
|
||||||
|
if session is None:
|
||||||
|
engine.http = None
|
||||||
|
engine.external_http = False
|
||||||
|
else:
|
||||||
|
engine.http = session
|
||||||
|
engine.external_http = True
|
||||||
|
|
||||||
|
def _apply_socketio_transport(
|
||||||
|
self,
|
||||||
|
client: "socketio.AsyncClient",
|
||||||
|
ssl_context,
|
||||||
|
*,
|
||||||
|
verify: bool,
|
||||||
|
fingerprint: Optional[str],
|
||||||
|
) -> None:
|
||||||
|
engine = getattr(client, "eio", None)
|
||||||
|
if engine is None:
|
||||||
|
return
|
||||||
|
options = getattr(engine, "websocket_extra_options", {}) or {}
|
||||||
|
options = dict(options)
|
||||||
|
options.pop("ssl", None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import aiohttp # type: ignore
|
||||||
|
except Exception as exc:
|
||||||
|
_log_agent(
|
||||||
|
f"aiohttp unavailable for socket transport configuration: {exc}",
|
||||||
|
fname="agent.error.log",
|
||||||
|
)
|
||||||
|
self._set_socketio_http_session(client, None, None)
|
||||||
|
engine.ssl_verify = verify
|
||||||
|
engine.websocket_extra_options = options
|
||||||
|
return
|
||||||
|
|
||||||
|
if ssl_context is False or not verify:
|
||||||
|
options["ssl"] = False
|
||||||
|
engine.ssl_verify = False
|
||||||
|
try:
|
||||||
|
connector = aiohttp.TCPConnector(ssl=False)
|
||||||
|
session = aiohttp.ClientSession(connector=connector)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._cached_ssl_context = None
|
|
||||||
_log_agent(
|
_log_agent(
|
||||||
f"SocketIO TLS fallback context build failed: {exc}; disabling verification",
|
f"SocketIO TLS disabled but failed to create aiohttp session: {exc}",
|
||||||
fname="agent.error.log",
|
fname="agent.error.log",
|
||||||
)
|
)
|
||||||
return {"ssl_verify": False}
|
self._set_socketio_http_session(client, None, None)
|
||||||
if verify is False:
|
else:
|
||||||
|
self._set_socketio_http_session(client, session, ("noverify", None))
|
||||||
|
elif isinstance(ssl_context, ssl.SSLContext):
|
||||||
|
options.pop("ssl", None)
|
||||||
|
engine.ssl_verify = True
|
||||||
|
try:
|
||||||
|
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
||||||
|
session = aiohttp.ClientSession(connector=connector)
|
||||||
|
except Exception as exc:
|
||||||
|
_log_agent(
|
||||||
|
f"SocketIO TLS session creation failed; falling back to default handling: {exc}",
|
||||||
|
fname="agent.error.log",
|
||||||
|
)
|
||||||
|
self._set_socketio_http_session(client, None, None)
|
||||||
|
else:
|
||||||
|
mode = ("context", fingerprint or "<unknown>")
|
||||||
|
self._set_socketio_http_session(client, session, mode)
|
||||||
|
else:
|
||||||
|
options.pop("ssl", None)
|
||||||
|
engine.ssl_verify = True
|
||||||
|
self._set_socketio_http_session(client, None, None)
|
||||||
|
|
||||||
|
engine.websocket_extra_options = options
|
||||||
|
|
||||||
|
def refresh_pinned_certificate(self) -> bool:
|
||||||
|
url = self.base_url or ""
|
||||||
|
parsed = urlparse(url)
|
||||||
|
host = parsed.hostname or "localhost"
|
||||||
|
port = parsed.port
|
||||||
|
if not port:
|
||||||
|
port = 443 if (parsed.scheme or "").lower() == "https" else 80
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
with socket.create_connection((host, port), timeout=5) as sock:
|
||||||
|
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
|
||||||
|
der = ssock.getpeercert(binary_form=True)
|
||||||
|
except Exception as exc:
|
||||||
|
_log_agent(
|
||||||
|
f"Server certificate probe failed host={host} port={port} error={exc}",
|
||||||
|
fname="agent.error.log",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
pem_text = ssl.DER_cert_to_PEM_cert(der)
|
||||||
|
except Exception as exc:
|
||||||
|
_log_agent(
|
||||||
|
f"Unable to convert probed certificate to PEM: {exc}",
|
||||||
|
fname="agent.error.log",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.key_store.save_server_certificate(pem_text)
|
||||||
self._cached_ssl_context = None
|
self._cached_ssl_context = None
|
||||||
return {"ssl_verify": False}
|
_log_agent(
|
||||||
self._cached_ssl_context = None
|
"Refreshed pinned server certificate after TLS failure",
|
||||||
return {}
|
fname="agent.log",
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
_log_agent(
|
||||||
|
f"Failed to persist refreshed server certificate: {exc}",
|
||||||
|
fname="agent.error.log",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Enrollment & token management
|
# Enrollment & token management
|
||||||
@@ -2851,6 +2991,36 @@ async def connect_loop():
|
|||||||
conn_err = None
|
conn_err = None
|
||||||
if conn_err:
|
if conn_err:
|
||||||
detail = f"{detail}; connection_error={conn_err!r}"
|
detail = f"{detail}; connection_error={conn_err!r}"
|
||||||
|
|
||||||
|
tls_error = False
|
||||||
|
for exc in _iter_exception_chain(e):
|
||||||
|
if isinstance(exc, aiohttp.client_exceptions.ClientConnectorCertificateError):
|
||||||
|
tls_error = True
|
||||||
|
break
|
||||||
|
message = str(exc)
|
||||||
|
if "CERTIFICATE_VERIFY_FAILED" in message.upper():
|
||||||
|
tls_error = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not tls_error and conn_err and isinstance(conn_err, Exception):
|
||||||
|
for exc in _iter_exception_chain(conn_err):
|
||||||
|
if isinstance(exc, aiohttp.client_exceptions.ClientConnectorCertificateError):
|
||||||
|
tls_error = True
|
||||||
|
break
|
||||||
|
message = str(exc)
|
||||||
|
if "CERTIFICATE_VERIFY_FAILED" in message.upper():
|
||||||
|
tls_error = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if tls_error:
|
||||||
|
if client.refresh_pinned_certificate():
|
||||||
|
_log_agent(
|
||||||
|
f"connect_loop attempt={attempt} refreshed server certificate after TLS failure; retrying immediately",
|
||||||
|
fname="agent.log",
|
||||||
|
)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
message = (
|
message = (
|
||||||
f"connect_loop attempt={attempt} server unavailable: {detail}. "
|
f"connect_loop attempt={attempt} server unavailable: {detail}. "
|
||||||
f"Retrying in {retry}s..."
|
f"Retrying in {retry}s..."
|
||||||
|
|||||||
@@ -76,3 +76,31 @@ def ensure_runtime_dir(*parts: str) -> Path:
|
|||||||
path = runtime_path(*parts)
|
path = runtime_path(*parts)
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def certificates_root() -> Path:
|
||||||
|
"""Base directory for persisted certificate material."""
|
||||||
|
|
||||||
|
env = _env_path("BOREALIS_CERT_ROOT")
|
||||||
|
if env:
|
||||||
|
env.mkdir(parents=True, exist_ok=True)
|
||||||
|
return env
|
||||||
|
|
||||||
|
root = project_root() / "Certificates"
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def certificates_path(*parts: str) -> Path:
|
||||||
|
"""Return a path under the certificates root."""
|
||||||
|
|
||||||
|
return certificates_root().joinpath(*parts)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_certificates_dir(*parts: str) -> Path:
|
||||||
|
"""Create (if required) and return a certificates subdirectory."""
|
||||||
|
|
||||||
|
path = certificates_path(*parts)
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|||||||
Reference in New Issue
Block a user