# coding:utf-8 # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2026 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT """Account-side client for the bun delivery socket. Runs at the account's uid, inside its cage. It says one thing -- "deliver the modules for this application" -- and reports the verdict. It cannot say who it is: the kernel tells the daemon that over SO_PEERCRED, which is why nothing here is worth forging. Exit codes are the contract with npm_wrapper: 0 delivered 1 declined, for a reason the daemon named (printed to stderr) 2 the daemon is unreachable 3 administrator policy selects classic npm (disabled or excluded) Only 3 selects classic npm, and only for explicit administrator policy. Nothing retries after an operational or delivery failure: a silent fallback is what let two delivery defects sit behind healthy-looking numbers during this feature's development (see *No fallback* in the design spec). """ from __future__ import absolute_import import json import os import re import socket import struct import sys SOCKET_PATH = '/opt/cl-bun/run/delivery.sock' # Long enough for a cold cache to fetch a large tree, short enough that a # wedged daemon does not hold a terminal indefinitely. # A client can wait in the bounded daemon queue before the Bun install and # account-side rebuild begin; leave margin for socket and hand-over work too. TIMEOUT = 2100 _MAJOR = re.compile(r'^[0-9]{1,3}$') def shell_major(environ=os.environ): """The venv the shell is in, from CL_VIRTUAL_ENV, or None. npm_wrapper sources the venv's own `activate` before any phase runs, so this names the venv the account is actually working in -- which a version change may have left beside a newer one the selector records. The root half has to deliver where the user half resolved and linked, so the request carries it. Anything but a small number is left unsaid and the daemon falls back to the record.""" venv = environ.get('CL_VIRTUAL_ENV', '') name = os.path.basename(venv) if venv and '/' in venv and _MAJOR.match(name) and 1 <= int(name) <= 999: return int(name) return None def request(command, app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None, lock_fd=None): """Returns the daemon's verdict as a dict, or raises OSError.""" sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(timeout) try: sock.connect(path) request = {'cmd': command, 'app_root': app_root} if major is not None: request['major'] = major payload = json.dumps(request) message = (payload + '\n').encode('utf-8') if lock_fd is None: sock.sendall(message) else: sent = sock.sendmsg([message], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, struct.pack('i', lock_fd))]) if sent <= 0: raise OSError('could not send delivery lock descriptor') if sent < len(message): sock.sendall(message[sent:]) chunks = [] while True: chunk = sock.recv(65536) if not chunk: break chunks.append(chunk) if b'\n' in chunk: break finally: sock.close() raw = b''.join(chunks).split(b'\n', 1)[0] if not raw: raise OSError('empty reply from delivery service') return json.loads(raw.decode('utf-8')) def request_delivery(app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None, lock_fd=None): return request('deliver', app_root, path, timeout, major, lock_fd) def request_policy(app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None, lock_fd=None): return request('policy', app_root, path, timeout, major, lock_fd) def request_unshare(app_root, path=SOCKET_PATH, timeout=TIMEOUT, major=None, lock_fd=None): return request('unshare', app_root, path, timeout, major, lock_fd) def main(argv=None): argv = sys.argv[1:] if argv is None else argv if len(argv) == 1: command, app_root = 'deliver', argv[0] elif len(argv) == 2 and argv[0] in ('policy', 'unshare'): command, app_root = argv else: sys.stderr.write( 'usage: cloudlinux-bun-client [policy|unshare] \n') return 2 try: raw_lock_fd = os.environ.get('CL_BUN_LOCK_FD') lock_fd = int(raw_lock_fd) if raw_lock_fd and raw_lock_fd.isdigit() else None request_fn = { 'deliver': request_delivery, 'policy': request_policy, 'unshare': request_unshare, }[command] verdict = request_fn(app_root, major=shell_major(), lock_fd=lock_fd) except (OSError, ValueError) as exc: sys.stderr.write('bun delivery unavailable: %s\n' % exc) return 2 if verdict.get('status') == 'delivered': detail = verdict.get('detail') or '' if detail: sys.stderr.write('delivered %s\n' % detail) return 0 if verdict.get('status') == 'unshared': return 0 if command == 'policy' and verdict.get('status') == 'eligible': return 0 if (command != 'unshare' and verdict.get('reason') in ('disabled', 'excluded')): # These two are explicit administrator choices of classic npm. The # wrapper distinguishes them from every actual delivery failure. return 3 sys.stderr.write('bun delivery declined: %s (%s)\n' % (verdict.get('reason', '?'), verdict.get('detail', ''))) return 1 if __name__ == '__main__': sys.exit(main())