Last active 1 month ago

gistfile1.txt Raw
1#!/usr/bin/env python3
2"""
3wgctl JSON helper — called by shell functions to read/write JSON files.
4Usage: json_helper.py <command> <file> [key] [value]
5"""
6
7import sys
8import json
9import os
10
11DATETIME_FMT = os.environ.get('WGCTL_DATETIME_FMT', '%Y-%m-%d %H:%M')
12
13def get(file, key):
14 try:
15 with open(file) as f:
16 data = json.load(f)
17 val = data.get(key, [])
18 if isinstance(val, bool):
19 print(str(val).lower()) # true/false not True/False
20 elif isinstance(val, list):
21 if val:
22 print('\n'.join(str(v) for v in val))
23 else:
24 if val:
25 print(val)
26 except:
27 sys.exit(0)
28
29def set_key(file, key, value):
30 try:
31 data = {}
32 if os.path.exists(file):
33 with open(file) as f:
34 data = json.load(f)
35 # Try to parse as JSON value first (for arrays/bools)
36 try:
37 data[key] = json.loads(value)
38 except:
39 data[key] = value
40 with open(file, 'w') as f:
41 json.dump(data, f, indent=2)
42 except Exception as e:
43 print(f"Error: {e}", file=sys.stderr)
44 sys.exit(1)
45
46def delete_key(file, key):
47 try:
48 with open(file) as f:
49 data = json.load(f)
50 data.pop(key, None)
51 with open(file, 'w') as f:
52 json.dump(data, f, indent=2)
53 except Exception as e:
54 print(f"Error: {e}", file=sys.stderr)
55 sys.exit(1)
56
57def append(file, key, value):
58 try:
59 data = {}
60 if os.path.exists(file):
61 with open(file) as f:
62 data = json.load(f)
63 if key not in data:
64 data[key] = []
65 if value not in data[key]:
66 data[key].append(value)
67 with open(file, 'w') as f:
68 json.dump(data, f, indent=2)
69 except Exception as e:
70 print(f"Error: {e}", file=sys.stderr)
71 sys.exit(1)
72
73def remove_value(file, key, value):
74 try:
75 with open(file) as f:
76 data = json.load(f)
77 if key in data and value in data[key]:
78 data[key].remove(value)
79 with open(file, 'w') as f:
80 json.dump(data, f, indent=2)
81 except Exception as e:
82 print(f"Error: {e}", file=sys.stderr)
83 sys.exit(1)
84
85def cat(file):
86 try:
87 with open(file) as f:
88 data = json.load(f)
89 print(json.dumps(data, indent=2))
90 except Exception as e:
91 sys.exit(1)
92
93def has_key(file, key):
94 try:
95 with open(file) as f:
96 data = json.load(f)
97 sys.exit(0 if key in data else 1)
98 except:
99 sys.exit(1)
100
101def filter_values(file, key, value):
102 """Remove all entries where value matches"""
103 try:
104 with open(file) as f:
105 data = json.load(f)
106 data = {k: v for k, v in data.items() if v != value}
107 with open(file, 'w') as f:
108 json.dump(data, f, indent=2)
109 except Exception as e:
110 print(f"Error: {e}", file=sys.stderr)
111 sys.exit(1)
112
113def last_event(file, key, field, client):
114 """Get last event field for a client"""
115 try:
116 last = None
117 with open(file) as f:
118 for line in f:
119 try:
120 e = json.loads(line.strip())
121 if e.get(key) == client:
122 last = e
123 except:
124 pass
125 if last:
126 print(last.get(field, ''))
127 except:
128 pass
129
130def events_for(file, ip, limit):
131 """Format events for a given IP"""
132 try:
133 from datetime import datetime
134 events = []
135 with open(file) as f:
136 for line in f:
137 try:
138 e = json.loads(line.strip())
139 if e.get('ip') == ip:
140 events.append(e)
141 except:
142 pass
143 for e in events[-int(limit):]:
144 ts = e.get('timestamp', '')
145 try:
146 dt = datetime.fromisoformat(ts)
147 ts = dt.strftime(DATETIME_FMT)
148 except:
149 pass
150 endpoint = e.get('endpoint', '—')
151 client = e.get('client', '—')
152 event = e.get('event', '—')
153 print(f' {ts} {client:<20} {endpoint:<20} {event}')
154 except:
155 pass
156
157def fw_events(file, filter_ip, filter_type, clients_dir, limit):
158 """Format firewall drop events"""
159 import glob
160
161 proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
162
163 # Build ip->name map
164 ip_to_name = {}
165 for conf in glob.glob(f"{clients_dir}/*.conf"):
166 name = os.path.basename(conf).replace('.conf', '')
167 try:
168 with open(conf) as f:
169 for line in f:
170 if line.startswith('Address'):
171 ip = line.split('=')[1].strip().split('/')[0]
172 ip_to_name[ip] = name
173 except:
174 pass
175
176 events = []
177 last_seen = {} # (src, dst, port, proto) -> last timestamp
178
179 try:
180 with open(file) as f:
181 for line in f:
182 try:
183 e = json.loads(line.strip())
184 src = e.get('src_ip', '')
185 if not src:
186 continue
187 if filter_ip and src != filter_ip:
188 continue
189
190 # Dedup key
191 dst = e.get('dest_ip', '')
192 port = e.get('dest_port', '')
193 proto = e.get('ip.protocol', 0)
194 key = (src, dst, port, proto)
195
196 ts_str = e.get('timestamp', '')
197 try:
198 from datetime import datetime
199 ts = datetime.fromisoformat(ts_str).timestamp()
200 except:
201 ts = 0
202
203 # Protocol-aware dedup window
204 dedup_windows = {1: 5, 6: 30, 17: 10} # icmp=5s, tcp=30s, udp=10s
205 window = dedup_windows.get(proto, 10)
206
207 # Skip if same event within protocol window
208 if key in last_seen and (ts - last_seen[key]) < window:
209 continue
210 last_seen[key] = ts
211
212 events.append(e)
213 except:
214 pass
215 except:
216 pass
217
218 for e in events[-int(limit):]:
219 ts = e.get('timestamp', '')
220 try:
221 from datetime import datetime
222 dt = datetime.fromisoformat(ts)
223 ts = dt.strftime(DATETIME_FMT)
224 except:
225 pass
226 src = e.get('src_ip', '—')
227 dst = e.get('dest_ip', '—')
228 port = e.get('dest_port', '')
229 proto_num = e.get('ip.protocol', 0)
230 proto = proto_map.get(proto_num, str(proto_num))
231 dst_str = f"{dst}:{port}" if port else dst
232 client = ip_to_name.get(src, src)
233
234 if filter_type and not client.startswith(filter_type + '-'):
235 continue
236
237 print(f"{ts}|{client}|{dst_str}|{proto}")
238
239def wg_events(file, filter_client, filter_type, limit):
240 """Format WireGuard events from events.log"""
241 events = []
242 try:
243 with open(file) as f:
244 for line in f:
245 try:
246 e = json.loads(line.strip())
247 client = e.get('client', '')
248 if not client:
249 continue
250 if filter_client and client != filter_client:
251 continue
252 if filter_type and not client.startswith(filter_type + '-'):
253 continue
254 events.append(e)
255 except:
256 pass
257 except:
258 pass
259
260 for e in events[-int(limit):]:
261 ts = e.get('timestamp', '')
262 try:
263 from datetime import datetime
264 dt = datetime.fromisoformat(ts)
265 ts = dt.strftime(DATETIME_FMT)
266 except:
267 pass
268 client = e.get('client', '—')
269 endpoint = e.get('endpoint', '—')
270 event = e.get('event', '—')
271 print(f"{ts}|{client}|{endpoint}|{event}")
272
273def format_fw_event(line, clients_dir):
274 """Format a single fw_event line"""
275 import glob
276 proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
277
278 # Build ip->name map
279 ip_to_name = {}
280 for conf in glob.glob(f"{clients_dir}/*.conf"):
281 name = os.path.basename(conf).replace('.conf', '')
282 try:
283 with open(conf) as f:
284 for l in f:
285 if l.startswith('Address'):
286 ip = l.split('=')[1].strip().split('/')[0]
287 ip_to_name[ip] = name
288 except:
289 pass
290
291 try:
292 e = json.loads(line.strip())
293 src = e.get('src_ip', '')
294 if not src:
295 return None
296 ts = e.get('timestamp', '')
297 try:
298 from datetime import datetime
299 dt = datetime.fromisoformat(ts)
300 ts = dt.strftime(DATETIME_FMT)
301 except:
302 pass
303 dst = e.get('dest_ip', '—')
304 port = e.get('dest_port', '')
305 proto_num = e.get('ip.protocol', 0)
306 proto = proto_map.get(proto_num, str(proto_num))
307 dst_str = f"{dst}:{port}" if port else dst
308 client = ip_to_name.get(src, src)
309 return f"{ts}|{client}|{dst_str}|{proto}"
310 except:
311 return None
312
313def format_wg_event(line):
314 """Format a single wg_event line"""
315 try:
316 e = json.loads(line.strip())
317 client = e.get('client', '')
318 if not client:
319 return None
320 ts = e.get('timestamp', '')
321 try:
322 from datetime import datetime
323 dt = datetime.fromisoformat(ts)
324 ts = dt.strftime(DATETIME_FMT)
325 except:
326 pass
327 endpoint = e.get('endpoint', '—')
328 event = e.get('event', '—')
329 return f"{ts}|{client}|{endpoint}|{event}|wg"
330 except:
331 return None
332
333def remove_events(file, identifier):
334 """Remove all events for a client/ip from a JSONL file"""
335 try:
336 lines = []
337 with open(file) as f:
338 for line in f:
339 try:
340 e = json.loads(line.strip())
341 if e.get('client') == identifier or e.get('src_ip') == identifier:
342 continue
343 lines.append(line)
344 except:
345 lines.append(line)
346 with open(file, 'w') as f:
347 f.writelines(lines)
348 except Exception as e:
349 print(f"Error: {e}", file=sys.stderr)
350 sys.exit(1)
351
352def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_peers=""):
353 """Follow both log files and output formatted events"""
354 import glob, time, select
355
356 proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
357 peer_filter = set(filter_peers.split(',')) if filter_peers else set()
358 import sys
359 print(f"DEBUG follow_logs: peer_filter={peer_filter}", file=sys.stderr, flush=True)
360
361 # Build ip->name map
362 ip_to_name = {}
363 for conf in glob.glob(f"{clients_dir}/*.conf"):
364 name = os.path.basename(conf).replace('.conf', '')
365 try:
366 with open(conf) as f:
367 for l in f:
368 if l.startswith('Address'):
369 ip = l.split('=')[1].strip().split('/')[0]
370 ip_to_name[ip] = name
371 except:
372 pass
373
374 # Open files and seek to end
375 files = {}
376 for label, path in [('fw', fw_file), ('wg', wg_file)]:
377 if path and os.path.exists(path):
378 f = open(path)
379 f.seek(0, 2) # seek to end
380 files[label] = f
381
382 dedup = {}
383
384 try:
385 while True:
386 for label, f in files.items():
387 line = f.readline()
388 if not line:
389 continue
390 try:
391 e = json.loads(line.strip())
392 except:
393 continue
394
395 if label == 'fw':
396 src = e.get('src_ip', '')
397 if not src:
398 continue
399 if filter_ip and src != filter_ip:
400 continue
401
402 # Filter by peer names if specified
403 if peer_filter:
404 client_name = ip_to_name.get(src, '')
405 if client_name not in peer_filter:
406 continue
407 dst = e.get('dest_ip', '—')
408 port = e.get('dest_port', '')
409 proto_num = e.get('ip.protocol', 0)
410 proto = proto_map.get(proto_num, str(proto_num))
411
412 # Dedup
413 key = (src, dst, port, proto_num)
414 windows = {1: 5, 6: 30, 17: 10}
415 window = windows.get(proto_num, 10)
416 now = time.time()
417 if key in dedup and (now - dedup[key]) < window:
418 continue
419 dedup[key] = now
420
421 client = ip_to_name.get(src, src)
422 if filter_type and not client.startswith(filter_type + '-'):
423 continue
424 dst_str = f"{dst}:{port}" if port else dst
425 ts = e.get('timestamp', '')[:16].replace('T', ' ')
426 print(f"fw|{ts}|{client}|{dst_str}|{proto}", flush=True)
427
428 elif label == 'wg':
429 client = e.get('client', '')
430 if not client:
431 continue
432 if filter_ip:
433 ip = ip_to_name.get(filter_ip, '')
434 if client != ip and client != filter_ip:
435 continue
436
437 if peer_filter and client not in peer_filter:
438 continue
439 if filter_type and not client.startswith(filter_type + '-'):
440 continue
441 ts = e.get('timestamp', '')[:16].replace('T', ' ')
442 endpoint = e.get('endpoint', '—')
443 event = e.get('event', '—')
444 print(f"wg|{ts}|{client}|{endpoint}|{event}", flush=True)
445
446 time.sleep(0.1)
447 except KeyboardInterrupt:
448 pass
449
450def count(file, key):
451 try:
452 with open(file) as f:
453 data = json.load(f)
454 val = data.get(key, [])
455 print(len(val) if isinstance(val, list) else 0)
456 except:
457 print(0)
458
459def audit_fw_counts(clients_dir):
460 """Return peer_name:fw_count pairs from iptables and client configs"""
461 import glob, subprocess
462
463 # Get iptables output once
464 try:
465 result = subprocess.run(
466 ['iptables', '-L', 'FORWARD', '-n'],
467 capture_output=True, text=True
468 )
469 fw_output = result.stdout
470 except:
471 fw_output = ""
472
473 # Build ip->name and count rules
474 for conf in glob.glob(f"{clients_dir}/*.conf"):
475 name = os.path.basename(conf).replace('.conf', '')
476 try:
477 with open(conf) as f:
478 for line in f:
479 if line.startswith('Address'):
480 ip = line.split('=')[1].strip().split('/')[0]
481 count = fw_output.count(ip)
482 print(f"{name}:{count}")
483 break
484 except:
485 pass
486
487def peer_group_map(groups_dir):
488 """Return peer:group pairs for all groups"""
489 import glob
490 try:
491 for group_file in glob.glob(f"{groups_dir}/*.group"):
492 try:
493 with open(group_file) as f:
494 g = json.load(f)
495 name = g.get('name', '')
496 for peer in g.get('peers', []):
497 if peer:
498 print(f"{peer}:{name}")
499 except:
500 pass
501 except:
502 pass
503
504def peer_groups(groups_dir, peer_name):
505 """Find all groups containing a peer"""
506 import glob
507 try:
508 for group_file in glob.glob(f"{groups_dir}/*.group"):
509 try:
510 with open(group_file) as f:
511 g = json.load(f)
512 if peer_name in g.get('peers', []):
513 print(g.get('name', ''))
514 except:
515 pass
516 except:
517 pass
518
519def peer_data(clients_dir, meta_dir, events_log):
520 import glob
521
522 meta = {}
523 for f in glob.glob(f"{meta_dir}/*.meta"):
524 name = os.path.basename(f).replace('.meta', '')
525 try:
526 with open(f) as mf:
527 meta[name] = json.load(mf)
528 except:
529 meta[name] = {}
530
531 last_events = {}
532 try:
533 with open(events_log) as f:
534 for line in f:
535 try:
536 e = json.loads(line.strip())
537 client = e.get('client', '')
538 if client:
539 last_events[client] = e
540 except:
541 pass
542 except:
543 pass
544
545 for conf in sorted(glob.glob(f"{clients_dir}/*.conf")):
546 name = os.path.basename(conf).replace('.conf', '')
547 ip = ''
548 try:
549 with open(conf) as f:
550 for line in f:
551 if line.startswith('Address'):
552 ip = line.split('=')[1].strip().split('/')[0]
553 break
554 except:
555 pass
556
557 m = meta.get(name, {})
558 rule = m.get('rule', '')
559 subtype = m.get('subtype', '')
560
561 last_event = last_events.get(name, {})
562 last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
563 last_evt = last_event.get('event', '') # fixed: was last_event
564
565 print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}")
566
567def iso_to_ts(iso_str):
568 """Convert ISO timestamp to unix timestamp"""
569 try:
570 from datetime import datetime, timezone
571 dt = datetime.fromisoformat(iso_str)
572 if dt.tzinfo is None:
573 dt = dt.replace(tzinfo=timezone.utc)
574 print(int(dt.timestamp()))
575 except:
576 print(0)
577
578def rule_list_data(rules_dir, meta_dir):
579 """Return all rule data for list display in one call"""
580 import glob
581
582 # Count peers per rule from meta files
583 rule_peer_counts = {}
584 for f in glob.glob(f"{meta_dir}/*.meta"):
585 try:
586 with open(f) as mf:
587 meta = json.load(mf)
588 rule = meta.get('rule', '')
589 if rule:
590 rule_peer_counts[rule] = rule_peer_counts.get(rule, 0) + 1
591 except:
592 pass
593
594 # Read each rule file
595 for rule_file in sorted(glob.glob(f"{rules_dir}/*.rule")):
596 try:
597 with open(rule_file) as f:
598 r = json.load(f)
599 name = r.get('name', '')
600 desc = r.get('desc', '')
601 n_allows = len(r.get('allow_ips', [])) + len(r.get('allow_ports', []))
602 n_blocks = len(r.get('block_ips', [])) + len(r.get('block_ports', []))
603 peer_count = rule_peer_counts.get(name, 0)
604 print(f"{name}|{desc}|{n_allows}|{n_blocks}|{peer_count}")
605 except:
606 pass
607
608def group_list_data(groups_dir, blocks_dir):
609 """Return group summary data in one call"""
610 import glob
611
612 # Get all block files
613 blocked_peers = set()
614 for f in glob.glob(f"{blocks_dir}/*.block"):
615 name = os.path.basename(f).replace('.block', '')
616 blocked_peers.add(name)
617
618 for group_file in sorted(glob.glob(f"{groups_dir}/*.group")):
619 try:
620 with open(group_file) as f:
621 g = json.load(f)
622 name = g.get('name', '')
623 desc = g.get('desc', '')
624 peers = [p for p in g.get('peers', []) if p]
625 total = len(peers)
626 blocked = sum(1 for p in peers if p in blocked_peers)
627 print(f"{name}|{desc}|{total}|{blocked}")
628 except:
629 pass
630
631def fmt_datetime(iso_str, fmt):
632 """Format ISO timestamp with given strftime format"""
633 try:
634 from datetime import datetime
635 dt = datetime.fromisoformat(iso_str)
636 print(dt.strftime(fmt))
637 except:
638 print(iso_str)
639
640def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips, block_ports):
641 rule = {
642 'name': name,
643 'desc': desc,
644 'dns_redirect': dns_redirect == 'true',
645 'allow_ips': [x for x in allow_ips.split(',') if x] if allow_ips else [],
646 'block_ips': [x for x in block_ips.split(',') if x] if block_ips else [],
647 'block_ports': [x for x in block_ports.split(',') if x] if block_ports else [],
648 'allow_ports': []
649 }
650 with open(file, 'w') as f:
651 json.dump(rule, f, indent=2)
652
653def cleanup_config(config_file):
654 """Normalize blank lines in WireGuard config"""
655 import re
656 try:
657 with open(config_file) as f:
658 config = f.read()
659 config = re.sub(r'\n{3,}', '\n\n', config)
660 config = config.rstrip('\n') + '\n'
661 with open(config_file, 'w') as f:
662 f.write(config)
663 except Exception as e:
664 print(f"Error: {e}", file=sys.stderr)
665 sys.exit(1)
666
667def remove_peer_block(config_file, name):
668 """Remove a peer block from WireGuard config by name"""
669 import re
670 try:
671 with open(config_file) as f:
672 config = f.read()
673 pattern = r'\n\[Peer\]\n# ' + re.escape(name) + r'\n[^\n]+\n[^\n]+\n'
674 result = re.sub(pattern, '\n', config)
675 with open(config_file, 'w') as f:
676 f.write(result)
677 except Exception as e:
678 print(f"Error: {e}", file=sys.stderr)
679 sys.exit(1)
680
681def create_group(file, name, desc):
682 """Create a new group JSON file"""
683 try:
684 group = {'name': name, 'desc': desc, 'peers': []}
685 with open(file, 'w') as f:
686 json.dump(group, f, indent=2)
687 except Exception as e:
688 print(f"Error: {e}", file=sys.stderr)
689 sys.exit(1)
690
691def parse_event(line):
692 """Parse a single JSON event line"""
693 try:
694 e = json.loads(line)
695 print(f"{e.get('timestamp','')}|{e.get('client','')}|{e.get('endpoint','')}|{e.get('event','')}")
696 except:
697 pass
698
699def parse_fw_event(line):
700 """Parse a single fw_events.log JSON line"""
701 try:
702 e = json.loads(line)
703 ts = e.get('timestamp', '')
704 src = e.get('src_ip', '')
705 dst = e.get('dest_ip', '')
706 port = e.get('dest_port', '')
707 proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
708 proto_num = e.get('ip.protocol', 0)
709 proto = proto_map.get(proto_num, str(proto_num))
710 print(f"{ts}|{src}|{dst}|{port}|{proto}")
711 except:
712 pass
713
714def peer_transfer(wg_interface):
715 """Get transfer bytes per peer from wg show"""
716 import subprocess
717 try:
718 result = subprocess.run(
719 ['wg', 'show', wg_interface, 'transfer'],
720 capture_output=True, text=True
721 )
722 for line in result.stdout.strip().split('\n'):
723 if not line:
724 continue
725 parts = line.split('\t')
726 if len(parts) == 3:
727 pubkey, rx, tx = parts
728 total = int(rx) + int(tx)
729 if total == 0:
730 level = 'none'
731 elif total < 1_000_000:
732 level = 'low'
733 elif total < 10_000_000:
734 level = 'medium'
735 elif total < 100_000_000:
736 level = 'high'
737 else:
738 level = 'very high'
739 print(f"{pubkey}|{rx}|{tx}|{level}")
740 except:
741 pass
742
743commands = {
744 'get': lambda args: get(args[0], args[1]),
745 'set': lambda args: set_key(args[0], args[1], args[2]),
746 'delete': lambda args: delete_key(args[0], args[1]),
747 'append': lambda args: append(args[0], args[1], args[2]),
748 'remove': lambda args: remove_value(args[0], args[1], args[2]),
749 'cat': lambda args: cat(args[0]),
750 'has_key': lambda args: has_key(args[0], args[1]),
751 'filter_values': lambda args: filter_values(args[0], args[1], args[2]),
752 'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]),
753 'events_for': lambda args: events_for(args[0], args[1], args[2]),
754 'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]),
755 'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]),
756 'format_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]),
757 'format_wg_event': lambda args: format_wg_event(sys.stdin.read()),
758 'remove_events': lambda args: remove_events(args[0], args[1]),
759 'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4], args[5]),
760 'count': lambda args: count(args[0], args[1]),
761 'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
762 'peer_group_map': lambda args: peer_group_map(args[0]),
763 'peer_groups': lambda args: peer_groups(args[0], args[1]),
764 'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
765 'iso_to_ts': lambda args: iso_to_ts(args[0]),
766 'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
767 'group_list_data': lambda args: group_list_data(args[0], args[1]),
768 'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]),
769 'create_rule': lambda args: create_rule(args[0], args[1], args[2], args[3], args[4], args[5], args[6]),
770 'cleanup_config': lambda args: cleanup_config(args[0]),
771 'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
772 'create_group': lambda args: create_group(args[0], args[1], args[2]),
773 'parse_event': lambda args: parse_event(args[0]),
774 'parse_fw_event': lambda args: parse_fw_event(args[0]),
775 'peer_transfer': lambda args: peer_transfer(args[0]),
776}
777
778if __name__ == '__main__':
779 if len(sys.argv) < 2:
780 print("Usage: json_helper.py <command> <file> [key] [value]", file=sys.stderr)
781 sys.exit(1)
782
783 cmd = sys.argv[1]
784 args = sys.argv[2:]
785
786 if cmd not in commands:
787 print(f"Unknown command: {cmd}", file=sys.stderr)
788 sys.exit(1)
789
790 commands[cmd](args)