diff --git a/tools/esp-cmd b/tools/esp-cmd index 17c37d4..757686c 100755 --- a/tools/esp-cmd +++ b/tools/esp-cmd @@ -3,8 +3,9 @@ import socket import sys +import time -from esp_ctl.auth import sign_command +from esp_ctl.auth import get_secret, sign_command DEFAULT_PORT = 5501 TIMEOUT = 2.0 @@ -37,14 +38,35 @@ def resolve(host): sys.exit(1) +def get_uptime(ip): + """Query device uptime_s for HMAC timestamp (unauthenticated).""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(TIMEOUT) + try: + sock.sendto(b"STATUS", (ip, DEFAULT_PORT)) + data, _ = sock.recvfrom(1500) + for part in data.decode().split(): + if part.startswith("uptime_s="): + return int(part.split("=", 1)[1]) + except (socket.timeout, OSError, ValueError): + pass + finally: + sock.close() + return 0 + + def main(): if len(sys.argv) < 3 or sys.argv[1] in ("-h", "--help"): print(USAGE) sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2) host = sys.argv[1] - cmd = sign_command(" ".join(sys.argv[2:]).strip()) ip = resolve(host) + secret = get_secret() + uptime = get_uptime(ip) if secret else 0 + if secret and uptime: + time.sleep(0.06) # avoid firmware rate limiter (50ms) + cmd = sign_command(" ".join(sys.argv[2:]).strip(), uptime, secret) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(TIMEOUT) diff --git a/tools/esp-fleet b/tools/esp-fleet index ba681e5..691559a 100755 --- a/tools/esp-fleet +++ b/tools/esp-fleet @@ -10,7 +10,7 @@ import sys import threading import time -from esp_ctl.auth import sign_command +from esp_ctl.auth import get_secret, sign_command DEFAULT_PORT = 5501 DEFAULT_HTTP_PORT = 8070 @@ -53,15 +53,37 @@ Examples: esp-fleet ota --parallel /path/to/firmware.bin""" +def _get_uptime(ip, timeout=TIMEOUT): + """Query device uptime_s for HMAC timestamp (unauthenticated).""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + try: + sock.sendto(b"STATUS", (ip, DEFAULT_PORT)) + data, _ = sock.recvfrom(1500) + for part in data.decode().split(): + if part.startswith("uptime_s="): + return int(part.split("=", 1)[1]) + except (socket.timeout, OSError, ValueError): + pass + finally: + sock.close() + return 0 + + def query(name, host, cmd): """Send command to one sensor, return (name, reply_or_error).""" - cmd = sign_command(cmd) try: info = socket.getaddrinfo(host, DEFAULT_PORT, socket.AF_INET, socket.SOCK_DGRAM) ip = info[0][4][0] except socket.gaierror: return (name, f"ERR: cannot resolve {host}") + secret = get_secret() + uptime = _get_uptime(ip) if secret else 0 + if secret and uptime: + time.sleep(0.06) # avoid firmware rate limiter (50ms) + cmd = sign_command(cmd, uptime, secret) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(TIMEOUT) try: @@ -87,7 +109,11 @@ def _resolve(host): def _udp_cmd(ip, cmd, timeout=TIMEOUT): """Send signed UDP command and return reply string.""" - cmd = sign_command(cmd) + secret = get_secret() + uptime = _get_uptime(ip, timeout) if secret else 0 + if secret and uptime: + time.sleep(0.06) # avoid firmware rate limiter (50ms) + cmd = sign_command(cmd, uptime, secret) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(timeout) try: diff --git a/tools/esp-ota b/tools/esp-ota index f908395..0fd6379 100755 --- a/tools/esp-ota +++ b/tools/esp-ota @@ -9,7 +9,7 @@ import sys import threading import time -from esp_ctl.auth import sign_command +from esp_ctl.auth import get_secret, sign_command DEFAULT_CMD_PORT = 5501 DEFAULT_HTTP_PORT = 8070 @@ -32,9 +32,30 @@ def resolve(host: str) -> str: sys.exit(1) +def get_uptime(ip: str, timeout: float = TIMEOUT) -> int: + """Query device uptime_s for HMAC timestamp (unauthenticated).""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + try: + sock.sendto(b"STATUS", (ip, DEFAULT_CMD_PORT)) + data, _ = sock.recvfrom(1500) + for part in data.decode().split(): + if part.startswith("uptime_s="): + return int(part.split("=", 1)[1]) + except (socket.timeout, OSError, ValueError): + pass + finally: + sock.close() + return 0 + + def udp_cmd(ip: str, cmd: str, timeout: float = TIMEOUT) -> str: """Send UDP command and return reply.""" - cmd = sign_command(cmd) + secret = get_secret() + uptime = get_uptime(ip, timeout) if secret else 0 + if secret and uptime: + time.sleep(0.06) # avoid firmware rate limiter (50ms) + cmd = sign_command(cmd, uptime, secret) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(timeout) try: