|
Server : LiteSpeed System : Linux barito.iixcp.rumahweb.net 5.14.0-611.49.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Apr 21 16:39:08 EDT 2026 x86_64 User : elvh3918 ( 1528) PHP Version : 8.2.31 Disable Function : mail Directory : /opt/cloudlinux/venv/lib64/python3.11/site-packages/clwpos/ |
#!/opt/cloudlinux/venv/bin/python3 -bb
# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
from __future__ import absolute_import
import json
import logging
import socket
import struct
import time
from typing import Optional
logger = logging.getLogger(__name__)
# uint32_t, big endian
_format = '>I'
# Socket read timeout, seconds
_WPOS_SOCKET_READ_TIMEOUT_SEC = 10
# Maximum allowed message size, bytes
_MAX_MSG_SIZE = 1 * 1024 * 1024
def get_uid_from_socket(sock_object: socket.socket) -> int:
"""
Retrieve credentials from SO_PEERCRED option
:param sock_object: Socket object
:return: uid of user, which connects to this socket.
"""
_format_string = '3I'
creds = sock_object.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize(_format_string))
# creds contains _pid, _uid, _gid - 3 uint32_t numbers
_, _uid, _ = struct.unpack(_format_string, creds)
return _uid
def pack_data_for_socket(data_dict: dict) -> bytes:
"""
Prefix message with a 4-byte length
:param data_dict: Data dict for send
:return: byte array for send to socket
"""
msg_bytes = json.dumps(data_dict).encode('utf-8')
# Output data format:
# 4 bytes unsigned int, big-endian - data_len
# data_len - data_bytes
return struct.pack(_format, len(msg_bytes)) + msg_bytes
def _read_bytes_from_socket_with_timeout(sock_object: socket.socket, num_bytes: int,
timeout_sec: int) -> Optional[bytes]:
"""
Read amount data from socket
:param sock_object: Socket object to read data from
:param num_bytes: Bytes number to read
:param timeout_sec: Read timeout, None - timeout expired, data not received
"""
# Save the caller's timeout so a near-zero per-iteration budget on the
# last recv doesn't leak into a subsequent sendall on the same socket
# (bugbot ref1_cd536b1e on MR !38).
prev_timeout = sock_object.gettimeout()
deadline = time.monotonic() + timeout_sec
msg = bytes()
try:
while len(msg) < num_bytes:
remaining = deadline - time.monotonic()
if remaining <= 0:
return None
sock_object.settimeout(remaining)
chunk = sock_object.recv(num_bytes - len(msg))
if not chunk:
return None
msg += chunk
except socket.timeout:
return None
finally:
sock_object.settimeout(prev_timeout)
return msg
def read_unpack_response_from_socket_daemon(sock_object: socket.socket) -> Optional[dict]:
"""
Read length-prefixed amount of data from socket
:param sock_object: Socket object to read data
:return: Data received from socket dictionary. None - socket data format error
"""
# Socket Input data format:
# 4 bytes unsigned int, big-endian - data_len
# data_len - data_bytes
# Get data length (4 bytes)
raw_msglen = _read_bytes_from_socket_with_timeout(sock_object, 4, _WPOS_SOCKET_READ_TIMEOUT_SEC)
if raw_msglen is None:
return None
msglen = struct.unpack(_format, raw_msglen)[0]
if msglen > _MAX_MSG_SIZE:
return None
msg = _read_bytes_from_socket_with_timeout(sock_object, msglen, _WPOS_SOCKET_READ_TIMEOUT_SEC)
if msg is None:
return None
return json.loads(msg.decode('utf-8'))
def read_unpack_response_from_socket_client(sock_object: socket.socket) -> Optional[dict]:
"""
Read length-prefixed amount of data from socket
:param sock_object: Socket object to read data
:return: Data received from socket dictionary. None - socket data format error
"""
# Socket Input data format:
# 4 bytes unsigned int, big-endian - data_len
# data_len - data_bytes
try:
raw_msglen = sock_object.recv(4)
msglen = struct.unpack(_format, raw_msglen)[0]
if msglen > _MAX_MSG_SIZE:
return None
msg = bytes()
while len(msg) != msglen:
msg += sock_object.recv(4096)
except socket.timeout:
return None
return json.loads(msg.decode('utf-8'))
def send_dict_to_socket_connection_and_close(connection: socket.socket, data_to_send: dict):
"""
Send a dictionary to a socket connection and close it.
This is a fire-and-forget terminal send. It never re-raises socket
I/O errors to the caller — callers in daemon.py and
daemon_subscription_handler.py already treat it that way (no
try/except around the call). Delivery failures are logged at
WARNING level only; the socket is always closed on the way out
(best-effort).
The function does still propagate programming errors (e.g.,
TypeError from a non-serializable dict) so they surface during
development instead of being silently swallowed.
"""
# Defensive: a socket that is already closed (e.g., a prior call in
# the same handler chain failed mid-sendall and our finally closed
# it, then daemon.py's error handler retried with the same socket
# ref) must short-circuit. Calling settimeout on a closed FD would
# raise OSError(EBADF). fileno() returns -1 on closed sockets, and
# itself raises OSError on a fully-detached socket — treat both as
# already-closed.
try:
if connection.fileno() < 0:
return
except OSError:
return
# pack runs outside the OSError swallow zone: a TypeError here
# indicates a non-serializable response dict (programming error).
bytes_to_send = pack_data_for_socket(data_to_send)
try:
# Bound sendall — a peer that connects, sends a valid request,
# and never reads the response must not hold a daemon thread.
connection.settimeout(_WPOS_SOCKET_READ_TIMEOUT_SEC)
connection.sendall(bytes_to_send)
except OSError as exc:
# OSError covers the realistic socket-failure modes:
# - BrokenPipeError, ConnectionResetError, ConnectionAbortedError
# (peer-side disconnects)
# - socket.timeout (== TimeoutError in Py3.10+, subclass of OSError)
# from the explicit send-side timeout
# - OSError(EBADF) from settimeout if the FD races to closed
# Log and continue to close — the response is undeliverable,
# but the daemon thread must move on.
logger.warning('Failed to deliver response to socket peer: %s', exc)
finally:
# close() on an already-closed socket is normally a no-op, but
# in pathological states (double-close after dup, kernel error)
# it can raise. Swallow to keep the never-raises contract.
try:
connection.close()
except OSError:
pass