Initial server source import
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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')
|
||||
Reference in New Issue
Block a user