GitHub Action
commited on
Commit
·
dd21a0d
1
Parent(s):
90eafcb
Sync from GitHub with Git LFS
Browse files- agents/peer_sync.py +62 -120
- agents/tools/storage.py +82 -100
agents/peer_sync.py
CHANGED
|
@@ -7,58 +7,27 @@ import threading
|
|
| 7 |
import select
|
| 8 |
import netifaces
|
| 9 |
import re
|
|
|
|
| 10 |
|
| 11 |
from datetime import datetime, timezone as UTC
|
| 12 |
from tools.storage import Storage
|
| 13 |
|
| 14 |
storage = Storage()
|
| 15 |
|
| 16 |
-
# ---------------------------
|
| 17 |
-
# Вспомогательные функции
|
| 18 |
-
# ---------------------------
|
| 19 |
-
def parse_hostport(s: str):
|
| 20 |
-
"""
|
| 21 |
-
Разбирает "IP:port" или "[IPv6]:port" и возвращает (host, port)
|
| 22 |
-
"""
|
| 23 |
-
s = s.strip()
|
| 24 |
-
if s.startswith("["):
|
| 25 |
-
# IPv6 с портом: [addr]:port
|
| 26 |
-
host, _, port = s[1:].partition("]:")
|
| 27 |
-
try:
|
| 28 |
-
port = int(port)
|
| 29 |
-
except:
|
| 30 |
-
port = None
|
| 31 |
-
return host, port
|
| 32 |
-
else:
|
| 33 |
-
# IPv4 или IPv6 без []
|
| 34 |
-
if ":" in s:
|
| 35 |
-
host, port = s.rsplit(":", 1)
|
| 36 |
-
try:
|
| 37 |
-
port = int(port)
|
| 38 |
-
except:
|
| 39 |
-
port = None
|
| 40 |
-
return host, port
|
| 41 |
-
return s, None
|
| 42 |
-
|
| 43 |
-
def is_ipv6(host: str):
|
| 44 |
-
try:
|
| 45 |
-
socket.inet_pton(socket.AF_INET6, host)
|
| 46 |
-
return True
|
| 47 |
-
except OSError:
|
| 48 |
-
return False
|
| 49 |
-
|
| 50 |
# ---------------------------
|
| 51 |
# Конфигурация
|
| 52 |
# ---------------------------
|
| 53 |
my_id = storage.get_config_value("agent_id")
|
| 54 |
agent_name = storage.get_config_value("agent_name", "unknown")
|
| 55 |
local_addresses = storage.get_config_value("local_addresses", [])
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
# Получаем уникальные локальные порты
|
| 58 |
def get_local_ports():
|
| 59 |
ports = set()
|
| 60 |
for addr in local_addresses:
|
| 61 |
-
_, port = parse_hostport(addr.split("://", 1)[1])
|
| 62 |
if port:
|
| 63 |
ports.add(port)
|
| 64 |
return sorted(ports)
|
|
@@ -199,19 +168,17 @@ def udp_discovery():
|
|
| 199 |
time.sleep(DISCOVERY_INTERVAL)
|
| 200 |
|
| 201 |
# ---------------------------
|
| 202 |
-
# TCP Peer Exchange
|
| 203 |
# ---------------------------
|
| 204 |
def tcp_peer_exchange():
|
| 205 |
-
PEER_EXCHANGE_INTERVAL = 20 # для отладки
|
| 206 |
while True:
|
| 207 |
peers = storage.get_known_peers(my_id, limit=50)
|
| 208 |
print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
|
| 209 |
|
| 210 |
for peer in peers:
|
| 211 |
-
if isinstance(peer, dict)
|
| 212 |
-
|
| 213 |
-
else:
|
| 214 |
-
peer_id, addresses_json = peer[0], peer[1]
|
| 215 |
|
| 216 |
if peer_id == my_id:
|
| 217 |
continue
|
|
@@ -222,49 +189,46 @@ def tcp_peer_exchange():
|
|
| 222 |
print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
|
| 223 |
addr_list = []
|
| 224 |
|
| 225 |
-
print(f"[PeerExchange] Peer {peer_id} -> addresses={addr_list}")
|
| 226 |
-
|
| 227 |
for addr in addr_list:
|
| 228 |
norm = storage.normalize_address(addr)
|
| 229 |
if not norm:
|
| 230 |
continue
|
| 231 |
-
|
| 232 |
proto, hostport = norm.split("://", 1)
|
| 233 |
if proto not in ["tcp", "any"]:
|
| 234 |
continue
|
| 235 |
-
|
| 236 |
-
host, port = parse_hostport(hostport)
|
| 237 |
if not host or not port:
|
| 238 |
continue
|
| 239 |
|
| 240 |
print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
|
| 241 |
-
|
| 242 |
try:
|
| 243 |
# IPv6 link-local
|
| 244 |
-
if is_ipv6(host) and host.startswith("fe80:"):
|
| 245 |
-
scope_id =
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
scope_id = socket.if_nametoindex(iface)
|
| 249 |
-
break
|
| 250 |
-
if scope_id is not None:
|
| 251 |
-
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
| 252 |
-
sock.settimeout(3)
|
| 253 |
-
sock.connect((host, port, 0, scope_id))
|
| 254 |
-
else:
|
| 255 |
-
print(f"[PeerExchange] Skipping {host}, no scope_id found")
|
| 256 |
continue
|
|
|
|
|
|
|
|
|
|
| 257 |
else:
|
| 258 |
-
sock = socket.socket(socket.AF_INET6 if is_ipv6(host) else socket.AF_INET,
|
|
|
|
| 259 |
sock.settimeout(3)
|
| 260 |
sock.connect((host, port))
|
| 261 |
|
| 262 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
handshake = {
|
| 264 |
"type": "PEER_EXCHANGE_REQUEST",
|
| 265 |
"id": my_id,
|
| 266 |
"name": agent_name,
|
| 267 |
-
"addresses":
|
| 268 |
}
|
| 269 |
sock.sendall(json.dumps(handshake).encode("utf-8"))
|
| 270 |
|
|
@@ -288,7 +252,7 @@ def tcp_peer_exchange():
|
|
| 288 |
print(f"[PeerExchange] Decode error from {host}:{port} -> {e}")
|
| 289 |
continue
|
| 290 |
|
| 291 |
-
break
|
| 292 |
except Exception as e:
|
| 293 |
print(f"[PeerExchange] Connection to {host}:{port} failed: {e}")
|
| 294 |
continue
|
|
@@ -296,52 +260,41 @@ def tcp_peer_exchange():
|
|
| 296 |
time.sleep(PEER_EXCHANGE_INTERVAL)
|
| 297 |
|
| 298 |
# ---------------------------
|
| 299 |
-
# TCP Listener (
|
| 300 |
# ---------------------------
|
| 301 |
def tcp_listener():
|
| 302 |
listen_sockets = []
|
| 303 |
-
|
| 304 |
-
# Создаём TCP сокеты на всех локальных портах
|
| 305 |
for port in local_ports:
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
# IPv6
|
| 318 |
-
try:
|
| 319 |
-
sock6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
| 320 |
-
sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| 321 |
-
sock6.bind(("::", port))
|
| 322 |
-
sock6.listen(5)
|
| 323 |
-
listen_sockets.append(sock6)
|
| 324 |
-
print(f"[TCP Listener] Listening IPv6 on [::]:{port}")
|
| 325 |
-
except Exception as e:
|
| 326 |
-
print(f"[TCP Listener] IPv6 bind failed on port {port}: {e}")
|
| 327 |
|
| 328 |
while True:
|
| 329 |
if not listen_sockets:
|
| 330 |
time.sleep(1)
|
| 331 |
continue
|
|
|
|
| 332 |
rlist, _, _ = select.select(listen_sockets, [], [], 1)
|
| 333 |
for s in rlist:
|
| 334 |
try:
|
| 335 |
conn, addr = s.accept()
|
| 336 |
-
data = conn.recv(1024)
|
| 337 |
-
|
| 338 |
if not data:
|
| 339 |
conn.close()
|
| 340 |
continue
|
| 341 |
|
| 342 |
try:
|
| 343 |
msg = json.loads(data.decode("utf-8"))
|
| 344 |
-
except Exception:
|
|
|
|
| 345 |
conn.close()
|
| 346 |
continue
|
| 347 |
|
|
@@ -350,48 +303,41 @@ def tcp_listener():
|
|
| 350 |
peer_name = msg.get("name", "unknown")
|
| 351 |
peer_addrs = msg.get("addresses", [])
|
| 352 |
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
peer_id,
|
| 356 |
-
peer_name,
|
| 357 |
-
peer_addrs,
|
| 358 |
-
source="incoming",
|
| 359 |
-
status="online"
|
| 360 |
-
)
|
| 361 |
print(f"[TCP Listener] Handshake from {peer_id} ({addr})")
|
| 362 |
|
| 363 |
-
#
|
|
|
|
|
|
|
| 364 |
peers_list = []
|
| 365 |
for peer in storage.get_known_peers(my_id, limit=50):
|
| 366 |
peer_id = peer["id"]
|
| 367 |
-
addresses_json = peer["addresses"]
|
| 368 |
try:
|
| 369 |
-
addresses = json.loads(
|
| 370 |
except:
|
| 371 |
addresses = []
|
| 372 |
|
| 373 |
-
# Обработка IPv6 link-local: добавить scope_id
|
| 374 |
updated_addresses = []
|
| 375 |
for a in addresses:
|
| 376 |
proto, hostport = a.split("://")
|
| 377 |
-
host, port = parse_hostport(hostport)
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
if scope_id:
|
| 387 |
-
break
|
| 388 |
if scope_id:
|
| 389 |
host = f"{host}%{scope_id}"
|
|
|
|
| 390 |
updated_addresses.append(f"{proto}://{host}:{port}")
|
|
|
|
| 391 |
peers_list.append({"id": peer_id, "addresses": updated_addresses})
|
| 392 |
|
| 393 |
-
|
| 394 |
-
conn.sendall(payload)
|
| 395 |
|
| 396 |
conn.close()
|
| 397 |
except Exception as e:
|
|
@@ -401,13 +347,9 @@ def tcp_listener():
|
|
| 401 |
# Запуск потоков
|
| 402 |
# ---------------------------
|
| 403 |
def start_sync(bootstrap_file="bootstrap.txt"):
|
| 404 |
-
# Загружаем bootstrap-пиров перед запуском discovery/peer exchange
|
| 405 |
load_bootstrap_peers(bootstrap_file)
|
| 406 |
-
|
| 407 |
-
# Печать локальных портов для логов
|
| 408 |
print(f"[PeerSync] Local ports: {local_ports}")
|
| 409 |
|
| 410 |
-
# Запуск потоков
|
| 411 |
threading.Thread(target=udp_discovery, daemon=True).start()
|
| 412 |
threading.Thread(target=tcp_peer_exchange, daemon=True).start()
|
| 413 |
threading.Thread(target=tcp_listener, daemon=True).start()
|
|
|
|
| 7 |
import select
|
| 8 |
import netifaces
|
| 9 |
import re
|
| 10 |
+
import ipaddress
|
| 11 |
|
| 12 |
from datetime import datetime, timezone as UTC
|
| 13 |
from tools.storage import Storage
|
| 14 |
|
| 15 |
storage = Storage()
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# ---------------------------
|
| 18 |
# Конфигурация
|
| 19 |
# ---------------------------
|
| 20 |
my_id = storage.get_config_value("agent_id")
|
| 21 |
agent_name = storage.get_config_value("agent_name", "unknown")
|
| 22 |
local_addresses = storage.get_config_value("local_addresses", [])
|
| 23 |
+
global_addresses = storage.get_config_value("global_addresses", [])
|
| 24 |
+
all_addresses = local_addresses + global_addresses # один раз
|
| 25 |
|
| 26 |
+
# Получаем уникальные локальные порты
|
| 27 |
def get_local_ports():
|
| 28 |
ports = set()
|
| 29 |
for addr in local_addresses:
|
| 30 |
+
_, port = storage.parse_hostport(addr.split("://", 1)[1])
|
| 31 |
if port:
|
| 32 |
ports.add(port)
|
| 33 |
return sorted(ports)
|
|
|
|
| 168 |
time.sleep(DISCOVERY_INTERVAL)
|
| 169 |
|
| 170 |
# ---------------------------
|
| 171 |
+
# TCP Peer Exchange (исходящие)
|
| 172 |
# ---------------------------
|
| 173 |
def tcp_peer_exchange():
|
| 174 |
+
PEER_EXCHANGE_INTERVAL = 20 # для отладки
|
| 175 |
while True:
|
| 176 |
peers = storage.get_known_peers(my_id, limit=50)
|
| 177 |
print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
|
| 178 |
|
| 179 |
for peer in peers:
|
| 180 |
+
peer_id = peer["id"] if isinstance(peer, dict) else peer[0]
|
| 181 |
+
addresses_json = peer["addresses"] if isinstance(peer, dict) else peer[1]
|
|
|
|
|
|
|
| 182 |
|
| 183 |
if peer_id == my_id:
|
| 184 |
continue
|
|
|
|
| 189 |
print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
|
| 190 |
addr_list = []
|
| 191 |
|
|
|
|
|
|
|
| 192 |
for addr in addr_list:
|
| 193 |
norm = storage.normalize_address(addr)
|
| 194 |
if not norm:
|
| 195 |
continue
|
|
|
|
| 196 |
proto, hostport = norm.split("://", 1)
|
| 197 |
if proto not in ["tcp", "any"]:
|
| 198 |
continue
|
| 199 |
+
host, port = storage.parse_hostport(hostport)
|
|
|
|
| 200 |
if not host or not port:
|
| 201 |
continue
|
| 202 |
|
| 203 |
print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
|
|
|
|
| 204 |
try:
|
| 205 |
# IPv6 link-local
|
| 206 |
+
if storage.is_ipv6(host) and host.startswith("fe80:"):
|
| 207 |
+
scope_id = storage.get_ipv6_scope(host)
|
| 208 |
+
if scope_id is None:
|
| 209 |
+
print(f"[PeerExchange] Skipping {host}, no scope_id")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
continue
|
| 211 |
+
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
| 212 |
+
sock.settimeout(3)
|
| 213 |
+
sock.connect((host, port, 0, scope_id))
|
| 214 |
else:
|
| 215 |
+
sock = socket.socket(socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
|
| 216 |
+
socket.SOCK_STREAM)
|
| 217 |
sock.settimeout(3)
|
| 218 |
sock.connect((host, port))
|
| 219 |
|
| 220 |
+
# LAN или Интернет
|
| 221 |
+
if storage.is_private(host):
|
| 222 |
+
send_addresses = all_addresses
|
| 223 |
+
else:
|
| 224 |
+
send_addresses = [a for a in all_addresses
|
| 225 |
+
if is_public(stprage.parse_hostport(a.split("://", 1)[1])[0])]
|
| 226 |
+
|
| 227 |
handshake = {
|
| 228 |
"type": "PEER_EXCHANGE_REQUEST",
|
| 229 |
"id": my_id,
|
| 230 |
"name": agent_name,
|
| 231 |
+
"addresses": send_addresses,
|
| 232 |
}
|
| 233 |
sock.sendall(json.dumps(handshake).encode("utf-8"))
|
| 234 |
|
|
|
|
| 252 |
print(f"[PeerExchange] Decode error from {host}:{port} -> {e}")
|
| 253 |
continue
|
| 254 |
|
| 255 |
+
break
|
| 256 |
except Exception as e:
|
| 257 |
print(f"[PeerExchange] Connection to {host}:{port} failed: {e}")
|
| 258 |
continue
|
|
|
|
| 260 |
time.sleep(PEER_EXCHANGE_INTERVAL)
|
| 261 |
|
| 262 |
# ---------------------------
|
| 263 |
+
# TCP Listener (входящие)
|
| 264 |
# ---------------------------
|
| 265 |
def tcp_listener():
|
| 266 |
listen_sockets = []
|
|
|
|
|
|
|
| 267 |
for port in local_ports:
|
| 268 |
+
for family, addr_str in [(socket.AF_INET, ""), (socket.AF_INET6, "::")]:
|
| 269 |
+
try:
|
| 270 |
+
sock = socket.socket(family, socket.SOCK_STREAM)
|
| 271 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| 272 |
+
sock.bind((addr_str, port))
|
| 273 |
+
sock.listen(5)
|
| 274 |
+
listen_sockets.append(sock)
|
| 275 |
+
proto_str = "IPv6" if family == socket.AF_INET6 else "IPv4"
|
| 276 |
+
print(f"[TCP Listener] Listening {proto_str} on {addr_str}:{port}")
|
| 277 |
+
except Exception as e:
|
| 278 |
+
print(f"[TCP Listener] {proto_str} bind failed on port {port}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
while True:
|
| 281 |
if not listen_sockets:
|
| 282 |
time.sleep(1)
|
| 283 |
continue
|
| 284 |
+
|
| 285 |
rlist, _, _ = select.select(listen_sockets, [], [], 1)
|
| 286 |
for s in rlist:
|
| 287 |
try:
|
| 288 |
conn, addr = s.accept()
|
| 289 |
+
data = conn.recv(64 * 1024)
|
|
|
|
| 290 |
if not data:
|
| 291 |
conn.close()
|
| 292 |
continue
|
| 293 |
|
| 294 |
try:
|
| 295 |
msg = json.loads(data.decode("utf-8"))
|
| 296 |
+
except Exception as e:
|
| 297 |
+
print(f"[TCP Listener] JSON decode error from {addr}: {e}")
|
| 298 |
conn.close()
|
| 299 |
continue
|
| 300 |
|
|
|
|
| 303 |
peer_name = msg.get("name", "unknown")
|
| 304 |
peer_addrs = msg.get("addresses", [])
|
| 305 |
|
| 306 |
+
storage.add_or_update_peer(peer_id, peer_name, peer_addrs,
|
| 307 |
+
source="incoming", status="online")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
print(f"[TCP Listener] Handshake from {peer_id} ({addr})")
|
| 309 |
|
| 310 |
+
# LAN или Интернет
|
| 311 |
+
is_lan = storage.is_private(addr[0])
|
| 312 |
+
|
| 313 |
peers_list = []
|
| 314 |
for peer in storage.get_known_peers(my_id, limit=50):
|
| 315 |
peer_id = peer["id"]
|
|
|
|
| 316 |
try:
|
| 317 |
+
addresses = json.loads(peer["addresses"])
|
| 318 |
except:
|
| 319 |
addresses = []
|
| 320 |
|
|
|
|
| 321 |
updated_addresses = []
|
| 322 |
for a in addresses:
|
| 323 |
proto, hostport = a.split("://")
|
| 324 |
+
host, port = storage.parse_hostport(hostport)
|
| 325 |
+
|
| 326 |
+
# Фильтруем по LAN/Internet
|
| 327 |
+
if not is_lan and not is_public(host):
|
| 328 |
+
continue
|
| 329 |
+
|
| 330 |
+
# IPv6 link-local
|
| 331 |
+
if storage.is_ipv6(host) and host.startswith("fe80:"):
|
| 332 |
+
scope_id = storage.get_ipv6_scope(host)
|
|
|
|
|
|
|
| 333 |
if scope_id:
|
| 334 |
host = f"{host}%{scope_id}"
|
| 335 |
+
|
| 336 |
updated_addresses.append(f"{proto}://{host}:{port}")
|
| 337 |
+
|
| 338 |
peers_list.append({"id": peer_id, "addresses": updated_addresses})
|
| 339 |
|
| 340 |
+
conn.sendall(json.dumps(peers_list).encode("utf-8"))
|
|
|
|
| 341 |
|
| 342 |
conn.close()
|
| 343 |
except Exception as e:
|
|
|
|
| 347 |
# Запуск потоков
|
| 348 |
# ---------------------------
|
| 349 |
def start_sync(bootstrap_file="bootstrap.txt"):
|
|
|
|
| 350 |
load_bootstrap_peers(bootstrap_file)
|
|
|
|
|
|
|
| 351 |
print(f"[PeerSync] Local ports: {local_ports}")
|
| 352 |
|
|
|
|
| 353 |
threading.Thread(target=udp_discovery, daemon=True).start()
|
| 354 |
threading.Thread(target=tcp_peer_exchange, daemon=True).start()
|
| 355 |
threading.Thread(target=tcp_listener, daemon=True).start()
|
agents/tools/storage.py
CHANGED
|
@@ -6,8 +6,11 @@ import os
|
|
| 6 |
import json
|
| 7 |
import uuid
|
| 8 |
import time
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
from datetime import datetime, timedelta, UTC, timezone
|
| 11 |
from werkzeug.security import generate_password_hash, check_password_hash
|
| 12 |
from tools.identity import generate_did
|
| 13 |
from tools.crypto import generate_keypair
|
|
@@ -17,6 +20,7 @@ UTC = timezone.utc
|
|
| 17 |
SCRIPTS_BASE_PATH = "scripts"
|
| 18 |
|
| 19 |
class Storage:
|
|
|
|
| 20 |
def __init__(self, config=None):
|
| 21 |
self.config = config or {}
|
| 22 |
db_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "agent_data.db"))
|
|
@@ -880,6 +884,76 @@ class Storage:
|
|
| 880 |
}
|
| 881 |
return None
|
| 882 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
# Работа с пирам (agent_peers)
|
| 884 |
def add_or_update_peer(
|
| 885 |
self, peer_id, name, addresses,
|
|
@@ -889,18 +963,17 @@ class Storage:
|
|
| 889 |
c = self.conn.cursor()
|
| 890 |
|
| 891 |
# нормализация адресов
|
| 892 |
-
addresses = list({
|
| 893 |
|
| 894 |
-
existing_id = None
|
| 895 |
existing_addresses = []
|
| 896 |
existing_pubkey = None
|
| 897 |
existing_capabilities = {}
|
| 898 |
|
| 899 |
if peer_id:
|
| 900 |
-
c.execute("SELECT
|
| 901 |
row = c.fetchone()
|
| 902 |
if row:
|
| 903 |
-
|
| 904 |
try:
|
| 905 |
existing_addresses = json.loads(db_addresses_json) or []
|
| 906 |
except:
|
|
@@ -910,7 +983,9 @@ class Storage:
|
|
| 910 |
except:
|
| 911 |
existing_capabilities = {}
|
| 912 |
|
| 913 |
-
|
|
|
|
|
|
|
| 914 |
final_pubkey = pubkey or existing_pubkey
|
| 915 |
final_capabilities = capabilities or existing_capabilities
|
| 916 |
|
|
@@ -937,6 +1012,7 @@ class Storage:
|
|
| 937 |
))
|
| 938 |
self.conn.commit()
|
| 939 |
|
|
|
|
| 940 |
def get_online_peers(self, limit=50):
|
| 941 |
c = self.conn.cursor()
|
| 942 |
c.execute("SELECT id, addresses FROM agent_peers WHERE status='online' LIMIT ?", (limit,))
|
|
@@ -947,100 +1023,6 @@ class Storage:
|
|
| 947 |
c.execute("SELECT id, addresses FROM agent_peers WHERE id != ? LIMIT ?", (my_id, limit))
|
| 948 |
return c.fetchall()
|
| 949 |
|
| 950 |
-
# Нормализация адресов
|
| 951 |
-
def normalize_address(self, addr: str) -> str:
|
| 952 |
-
addr = addr.strip()
|
| 953 |
-
if not addr:
|
| 954 |
-
return None
|
| 955 |
-
if "://" not in addr:
|
| 956 |
-
return f"any://{addr}"
|
| 957 |
-
return addr
|
| 958 |
-
|
| 959 |
-
# Bootstrap
|
| 960 |
-
def load_bootstrap(self, bootstrap_file="bootstrap.txt"):
|
| 961 |
-
"""
|
| 962 |
-
Загружает узлы из bootstrap.txt.
|
| 963 |
-
Поддерживаются адреса:
|
| 964 |
-
tcp://host:port
|
| 965 |
-
udp://host:port
|
| 966 |
-
any://host:port
|
| 967 |
-
TCP-узлы проверяются запросом /identity.
|
| 968 |
-
UDP/any регистрируются без проверки (any учитывается как TCP+UDP).
|
| 969 |
-
"""
|
| 970 |
-
import requests
|
| 971 |
-
|
| 972 |
-
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
| 973 |
-
bootstrap_path = os.path.join(base_path, bootstrap_file)
|
| 974 |
-
|
| 975 |
-
if not os.path.exists(bootstrap_path):
|
| 976 |
-
print(f"[Bootstrap] Файл {bootstrap_file} не найден по пути {bootstrap_path}")
|
| 977 |
-
return
|
| 978 |
-
|
| 979 |
-
for line in open(bootstrap_path, encoding="utf-8"):
|
| 980 |
-
line = line.strip()
|
| 981 |
-
if not line or line.startswith("#"):
|
| 982 |
-
continue
|
| 983 |
-
|
| 984 |
-
try:
|
| 985 |
-
addr = self.normalize_address(line)
|
| 986 |
-
if addr is None:
|
| 987 |
-
continue
|
| 988 |
-
|
| 989 |
-
# Разворачиваем any:// на tcp и udp
|
| 990 |
-
addrs_to_register = []
|
| 991 |
-
proto, hostport = addr.split("://")
|
| 992 |
-
if proto == "any":
|
| 993 |
-
addrs_to_register.append(f"tcp://{hostport}")
|
| 994 |
-
addrs_to_register.append(f"udp://{hostport}")
|
| 995 |
-
else:
|
| 996 |
-
addrs_to_register.append(addr)
|
| 997 |
-
|
| 998 |
-
for a in addrs_to_register:
|
| 999 |
-
proto2, hostport2 = a.split("://")
|
| 1000 |
-
|
| 1001 |
-
# TCP → проверяем /identity
|
| 1002 |
-
if proto2 == "tcp":
|
| 1003 |
-
try:
|
| 1004 |
-
url = f"http://{hostport2}/identity"
|
| 1005 |
-
r = requests.get(url, timeout=3)
|
| 1006 |
-
if r.status_code == 200:
|
| 1007 |
-
info = r.json()
|
| 1008 |
-
peer_id = info.get("id")
|
| 1009 |
-
name = info.get("name", "unknown")
|
| 1010 |
-
pubkey = info.get("pubkey")
|
| 1011 |
-
capabilities = info.get("capabilities", {})
|
| 1012 |
-
|
| 1013 |
-
self.add_or_update_peer(
|
| 1014 |
-
peer_id=peer_id,
|
| 1015 |
-
name=name,
|
| 1016 |
-
addresses=[a],
|
| 1017 |
-
source="bootstrap",
|
| 1018 |
-
status="online",
|
| 1019 |
-
pubkey=pubkey,
|
| 1020 |
-
capabilities=capabilities,
|
| 1021 |
-
)
|
| 1022 |
-
print(f"[Bootstrap] Добавлен узел {peer_id} ({a})")
|
| 1023 |
-
else:
|
| 1024 |
-
print(f"[Bootstrap] {a} недоступен (HTTP {r.status_code})")
|
| 1025 |
-
except Exception as e:
|
| 1026 |
-
print(f"[Bootstrap] Ошибка при подключении к {a}: {e}")
|
| 1027 |
-
|
| 1028 |
-
# UDP → просто регистрируем
|
| 1029 |
-
elif proto2 == "udp":
|
| 1030 |
-
peer_id = str(uuid.uuid4())
|
| 1031 |
-
self.add_or_update_peer(
|
| 1032 |
-
peer_id=peer_id,
|
| 1033 |
-
name="unknown",
|
| 1034 |
-
addresses=[a],
|
| 1035 |
-
source="bootstrap",
|
| 1036 |
-
status="unknown"
|
| 1037 |
-
)
|
| 1038 |
-
print(f"[Bootstrap] Добавлен адрес (без проверки): {a}")
|
| 1039 |
-
|
| 1040 |
-
except Exception as e:
|
| 1041 |
-
print(f"[Bootstrap] Ошибка парсинга {line}: {e}")
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
# Утилиты
|
| 1045 |
def close(self):
|
| 1046 |
self.conn.close()
|
|
|
|
| 6 |
import json
|
| 7 |
import uuid
|
| 8 |
import time
|
| 9 |
+
import socket
|
| 10 |
+
import ipaddress
|
| 11 |
+
import netifaces
|
| 12 |
|
| 13 |
+
from datetime import datetime, timedelta, UTC, timezone, timezone as UTC
|
| 14 |
from werkzeug.security import generate_password_hash, check_password_hash
|
| 15 |
from tools.identity import generate_did
|
| 16 |
from tools.crypto import generate_keypair
|
|
|
|
| 20 |
SCRIPTS_BASE_PATH = "scripts"
|
| 21 |
|
| 22 |
class Storage:
|
| 23 |
+
_scope_cache = {}
|
| 24 |
def __init__(self, config=None):
|
| 25 |
self.config = config or {}
|
| 26 |
db_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "agent_data.db"))
|
|
|
|
| 884 |
}
|
| 885 |
return None
|
| 886 |
|
| 887 |
+
# Работа с пирам (agent_peers)
|
| 888 |
+
@staticmethod
|
| 889 |
+
def parse_hostport(s: str):
|
| 890 |
+
"""
|
| 891 |
+
Разбирает "IP:port" или "[IPv6]:port" и возвращает (host, port)
|
| 892 |
+
"""
|
| 893 |
+
s = s.strip()
|
| 894 |
+
if s.startswith("["):
|
| 895 |
+
host, _, port = s[1:].partition("]:")
|
| 896 |
+
try:
|
| 897 |
+
port = int(port)
|
| 898 |
+
except:
|
| 899 |
+
port = None
|
| 900 |
+
return host, port
|
| 901 |
+
else:
|
| 902 |
+
if ":" in s:
|
| 903 |
+
host, port = s.rsplit(":", 1)
|
| 904 |
+
try:
|
| 905 |
+
port = int(port)
|
| 906 |
+
except:
|
| 907 |
+
port = None
|
| 908 |
+
return host, port
|
| 909 |
+
return s, None
|
| 910 |
+
|
| 911 |
+
@staticmethod
|
| 912 |
+
def is_ipv6(host: str):
|
| 913 |
+
try:
|
| 914 |
+
socket.inet_pton(socket.AF_INET6, host)
|
| 915 |
+
return True
|
| 916 |
+
except OSError:
|
| 917 |
+
return False
|
| 918 |
+
|
| 919 |
+
@staticmethod
|
| 920 |
+
def is_private(ip: str) -> bool:
|
| 921 |
+
try:
|
| 922 |
+
return ipaddress.ip_address(ip).is_private
|
| 923 |
+
except ValueError:
|
| 924 |
+
return False
|
| 925 |
+
|
| 926 |
+
@classmethod
|
| 927 |
+
def get_ipv6_scope(cls, host):
|
| 928 |
+
if host in cls._scope_cache:
|
| 929 |
+
return cls._scope_cache[host]
|
| 930 |
+
for iface in netifaces.interfaces():
|
| 931 |
+
iface_addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
|
| 932 |
+
for addr_info in iface_addrs:
|
| 933 |
+
if addr_info.get("addr") == host:
|
| 934 |
+
scope_id = socket.if_nametoindex(iface)
|
| 935 |
+
cls._scope_cache[host] = scope_id
|
| 936 |
+
return scope_id
|
| 937 |
+
return None
|
| 938 |
+
|
| 939 |
+
# Нормализация адресов
|
| 940 |
+
@classmethod
|
| 941 |
+
def normalize_address(cls, addr: str) -> str:
|
| 942 |
+
addr = addr.strip()
|
| 943 |
+
if not addr:
|
| 944 |
+
return None
|
| 945 |
+
if "://" not in addr:
|
| 946 |
+
addr = f"any://{addr}"
|
| 947 |
+
|
| 948 |
+
proto, hostport = addr.split("://", 1)
|
| 949 |
+
host, port = cls.parse_hostport(hostport)
|
| 950 |
+
|
| 951 |
+
# IPv6 без квадратных скобок
|
| 952 |
+
if cls.is_ipv6(host) and not host.startswith("["):
|
| 953 |
+
host = f"[{host}]"
|
| 954 |
+
|
| 955 |
+
return f"{proto}://{host}:{port}" if port else f"{proto}://{host}"
|
| 956 |
+
|
| 957 |
# Работа с пирам (agent_peers)
|
| 958 |
def add_or_update_peer(
|
| 959 |
self, peer_id, name, addresses,
|
|
|
|
| 963 |
c = self.conn.cursor()
|
| 964 |
|
| 965 |
# нормализация адресов
|
| 966 |
+
addresses = list({self.normalize_address(a) for a in (addresses or []) if a and a.strip()})
|
| 967 |
|
|
|
|
| 968 |
existing_addresses = []
|
| 969 |
existing_pubkey = None
|
| 970 |
existing_capabilities = {}
|
| 971 |
|
| 972 |
if peer_id:
|
| 973 |
+
c.execute("SELECT addresses, pubkey, capabilities FROM agent_peers WHERE id=?", (peer_id,))
|
| 974 |
row = c.fetchone()
|
| 975 |
if row:
|
| 976 |
+
db_addresses_json, existing_pubkey, db_caps_json = row
|
| 977 |
try:
|
| 978 |
existing_addresses = json.loads(db_addresses_json) or []
|
| 979 |
except:
|
|
|
|
| 983 |
except:
|
| 984 |
existing_capabilities = {}
|
| 985 |
|
| 986 |
+
# объединяем и нормализуем адреса, чтобы IPv6 всегда были с []
|
| 987 |
+
combined_addresses = list({self.normalize_address(a) for a in (*existing_addresses, *addresses)})
|
| 988 |
+
|
| 989 |
final_pubkey = pubkey or existing_pubkey
|
| 990 |
final_capabilities = capabilities or existing_capabilities
|
| 991 |
|
|
|
|
| 1012 |
))
|
| 1013 |
self.conn.commit()
|
| 1014 |
|
| 1015 |
+
# Получение известных/онлайн пиров
|
| 1016 |
def get_online_peers(self, limit=50):
|
| 1017 |
c = self.conn.cursor()
|
| 1018 |
c.execute("SELECT id, addresses FROM agent_peers WHERE status='online' LIMIT ?", (limit,))
|
|
|
|
| 1023 |
c.execute("SELECT id, addresses FROM agent_peers WHERE id != ? LIMIT ?", (my_id, limit))
|
| 1024 |
return c.fetchall()
|
| 1025 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1026 |
# Утилиты
|
| 1027 |
def close(self):
|
| 1028 |
self.conn.close()
|