import os import socket import stat import struct import time from host_agent_protocol import ( CLIENT_CONNECT_TIMEOUT_SECONDS, EXCHANGE_TIMEOUT_SECONDS, HOST_AGENT_SOCKET_PATH, MAX_RESPONSE_PAYLOAD_BYTES, HostAgentAction, HostAgentProtocolError, HostAgentRequest, HostAgentStatus, decode_response_frame, encode_request_frame, receive_frame, require_eof, send_frame, ) class HostAgentClientError(RuntimeError): def __init__(self, category): self.category = category super().__init__('host operations agent request failed') class HostAgentUnavailableError(HostAgentClientError): pass class HostAgentRejectedError(HostAgentClientError): pass def _fixed_socket_is_safe(): try: details = os.lstat(HOST_AGENT_SOCKET_PATH) except OSError: return False return stat.S_ISSOCK(details.st_mode) and details.st_uid == 0 def _peer_credentials(sock): if not hasattr(socket, 'SO_PEERCRED'): raise HostAgentUnavailableError('peer_credentials_unavailable') try: raw = sock.getsockopt( socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i'), ) pid, uid, gid = struct.unpack('3i', raw) except (OSError, struct.error) as exc: raise HostAgentUnavailableError('peer_credentials_unavailable') from exc # A peer outside the client's PID namespace is reported as PID 0 even # though its UID/GID remain authoritative through SO_PEERCRED. if pid < 0: raise HostAgentUnavailableError('peer_identity_invalid') return pid, uid, gid class HostAgentClient: def __init__(self): pass @staticmethod def is_available(): return _fixed_socket_is_safe() def dispatch( self, *, operation_id, action, active_config_sha256, active_secrets_sha256, candidate_config_sha256, candidate_secrets_sha256, ): try: request = HostAgentRequest( operation_id=operation_id, action=HostAgentAction(action), active_config_sha256=active_config_sha256, active_secrets_sha256=active_secrets_sha256, candidate_config_sha256=candidate_config_sha256, candidate_secrets_sha256=candidate_secrets_sha256, ) frame = encode_request_frame(request) except (HostAgentProtocolError, TypeError, ValueError) as exc: raise HostAgentRejectedError('request_invalid') from exc if not _fixed_socket_is_safe(): raise HostAgentUnavailableError('socket_unavailable') deadline = time.monotonic() + EXCHANGE_TIMEOUT_SECONDS connection = None response = None try: family = getattr(socket, 'AF_UNIX', None) if family is None: raise HostAgentUnavailableError('unix_socket_unavailable') connection = socket.socket(family, socket.SOCK_STREAM) connection.settimeout(min( CLIENT_CONNECT_TIMEOUT_SECONDS, max(0.001, deadline - time.monotonic()), )) connection.connect(HOST_AGENT_SOCKET_PATH) _pid, peer_uid, _gid = _peer_credentials(connection) if peer_uid != 0: raise HostAgentUnavailableError('server_identity_invalid') send_frame(connection, frame, deadline=deadline) connection.shutdown(socket.SHUT_WR) response = decode_response_frame(receive_frame( connection, maximum=MAX_RESPONSE_PAYLOAD_BYTES, deadline=deadline, )) require_eof(connection, deadline=deadline) except HostAgentClientError: raise except HostAgentProtocolError as exc: raise HostAgentUnavailableError('response_invalid') from exc except (OSError, TimeoutError) as exc: raise HostAgentUnavailableError('transport_unavailable') from exc finally: if connection is not None: try: connection.close() except OSError: pass connection = frame = request = family = None if response.operation_id != operation_id: raise HostAgentUnavailableError('response_identity_invalid') if response.status is HostAgentStatus.ACCEPTED: return response if response.status is HostAgentStatus.REJECTED: raise HostAgentRejectedError('request_rejected') raise HostAgentUnavailableError('request_unavailable')