Sanitized public release v1.0.3
Source-Tag: v1.0.3 Manifest-SHA256: b7b09bf48f9e092ed8ad82f99c0319d6c3837d0a1549bd0cc4a374276c0f3896
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/python3
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
SOCKET_PATH = "{{NEOGUARD_CONTROL_SOCKET_PATH}}"
|
||||
BASE = "{{DROPS_ORIGIN}}"
|
||||
KEY_PATH = Path("{{NEOGUARD_KEY_PATH}}")
|
||||
CLIENTS = set()
|
||||
CLIENTS_LOCK = threading.Lock()
|
||||
LAST_EVENT = 0
|
||||
|
||||
|
||||
def signed_request(path, value=None):
|
||||
body = b"" if value is None else json.dumps(value, separators=(",", ":")).encode()
|
||||
method = "GET" if value is None else "POST"
|
||||
for attempt in range(3):
|
||||
timestamp = int(time.time())
|
||||
nonce = secrets.token_hex(16)
|
||||
canonical = "%d\n%s\n%s\n%s\n%s" % (
|
||||
timestamp, nonce, method, path, hashlib.sha256(body).hexdigest())
|
||||
signature = hmac.new(KEY_PATH.read_bytes(), canonical.encode(), hashlib.sha256).hexdigest()
|
||||
request = urllib.request.Request(BASE + path, data=None if value is None else body, method=method, headers={
|
||||
"Content-Type": "application/json", "X-Neo-Timestamp": str(timestamp),
|
||||
"X-Neo-Nonce": nonce, "X-Neo-Signature": signature,
|
||||
})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code not in (502, 503, 504) or attempt == 2:
|
||||
raise
|
||||
time.sleep(0.25 * (attempt + 1))
|
||||
|
||||
|
||||
def broadcast(message):
|
||||
payload = (message.replace("\x00", "") + "\n").encode()
|
||||
dead = []
|
||||
with CLIENTS_LOCK:
|
||||
for client in CLIENTS:
|
||||
try:
|
||||
client.sendall(payload)
|
||||
except OSError:
|
||||
dead.append(client)
|
||||
for client in dead:
|
||||
CLIENTS.discard(client)
|
||||
client.close()
|
||||
|
||||
|
||||
def attempt_payload(fields):
|
||||
if len(fields) == 3:
|
||||
address, detail = fields[1], fields[2]
|
||||
port = None
|
||||
elif len(fields) == 4:
|
||||
address, port_text, detail = fields[1], fields[2], fields[3]
|
||||
if not port_text.isdecimal() or not 1 <= int(port_text) <= 65535:
|
||||
raise ValueError("invalid destination port")
|
||||
port = int(port_text)
|
||||
else:
|
||||
raise ValueError("invalid attempt event")
|
||||
payload = {"type": "attempt", "ip": address, "source": "relay", "detail": detail}
|
||||
if port is not None:
|
||||
payload["destination_port"] = port
|
||||
return payload
|
||||
|
||||
|
||||
def handle_client(client):
|
||||
buffer = b""
|
||||
try:
|
||||
client.sendall(b"STATUS\tNeoGuard agent connected\n")
|
||||
while True:
|
||||
data = client.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
buffer += data
|
||||
while b"\n" in buffer:
|
||||
line, buffer = buffer.split(b"\n", 1)
|
||||
fields = line.decode("utf-8", "replace").split("\t", 3)
|
||||
try:
|
||||
if fields[0] == "ATTEMPT":
|
||||
signed_request("/api/v1/neoguard/event", attempt_payload(fields))
|
||||
elif fields[0] == "COMMAND" and len(fields) >= 2:
|
||||
result = signed_request("/api/v1/neoguard/event", {"type":"command","command":fields[1]})
|
||||
client.sendall(("RESPONSE\t" + result.get("response", "No response") + "\n").encode())
|
||||
except Exception as error:
|
||||
client.sendall(("ERROR\t" + str(error)[:300] + "\n").encode())
|
||||
finally:
|
||||
with CLIENTS_LOCK:
|
||||
CLIENTS.discard(client)
|
||||
client.close()
|
||||
|
||||
|
||||
def socket_server():
|
||||
try:
|
||||
os.unlink(SOCKET_PATH)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(SOCKET_PATH)
|
||||
os.chmod(SOCKET_PATH, 0o660)
|
||||
server.listen(4)
|
||||
while True:
|
||||
client, _ = server.accept()
|
||||
with CLIENTS_LOCK:
|
||||
CLIENTS.add(client)
|
||||
threading.Thread(target=handle_client, args=(client,), daemon=True).start()
|
||||
|
||||
|
||||
def poll_events():
|
||||
global LAST_EVENT
|
||||
while True:
|
||||
try:
|
||||
result = signed_request("/api/v1/neoguard/poll?after=%d" % LAST_EVENT)
|
||||
for event in result.get("events", []):
|
||||
LAST_EVENT = max(LAST_EVENT, int(event["id"]))
|
||||
broadcast("EVENT\t%s\t%s" % (event["level"], event["message"]))
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
threading.Thread(target=poll_events, daemon=True).start()
|
||||
socket_server()
|
||||
Reference in New Issue
Block a user