1+ #!/usr/bin/env python3
2+
3+ import sys
4+ import json
5+ import argparse
6+ from pathlib import Path
7+ import re
8+ from ipaddress import ip_address
9+
10+ core_scripts_dir = Path (__file__ ).resolve ().parents [1 ]
11+ if str (core_scripts_dir ) not in sys .path :
12+ sys .path .append (str (core_scripts_dir ))
13+
14+ try :
15+ from paths import NODES_JSON_PATH
16+ except ImportError :
17+ NODES_JSON_PATH = Path ("/etc/hysteria/nodes.json" )
18+
19+
20+ def is_valid_ip_or_domain (value : str ) -> bool :
21+ """Check if the value is a valid IP address or domain name."""
22+ if not value or not value .strip ():
23+ return False
24+ value = value .strip ()
25+ try :
26+ ip_address (value )
27+ return True
28+ except ValueError :
29+ domain_regex = re .compile (
30+ r'^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$' ,
31+ re .IGNORECASE
32+ )
33+ return re .match (domain_regex , value ) is not None
34+
35+ def read_nodes ():
36+ if not NODES_JSON_PATH .exists ():
37+ return []
38+ try :
39+ with NODES_JSON_PATH .open ("r" ) as f :
40+ content = f .read ()
41+ if not content :
42+ return []
43+ return json .loads (content )
44+ except (json .JSONDecodeError , IOError , OSError ) as e :
45+ sys .exit (f"Error reading or parsing { NODES_JSON_PATH } : { e } " )
46+
47+ def write_nodes (nodes ):
48+ try :
49+ NODES_JSON_PATH .parent .mkdir (parents = True , exist_ok = True )
50+ with NODES_JSON_PATH .open ("w" ) as f :
51+ json .dump (nodes , f , indent = 4 )
52+ except (IOError , OSError ) as e :
53+ sys .exit (f"Error writing to { NODES_JSON_PATH } : { e } " )
54+
55+ def add_node (name : str , ip : str ):
56+ if not is_valid_ip_or_domain (ip ):
57+ print (f"Error: '{ ip } ' is not a valid IP address or domain name." , file = sys .stderr )
58+ sys .exit (1 )
59+
60+ nodes = read_nodes ()
61+ if any (node ['name' ] == name for node in nodes ):
62+ print (f"Error: A node with the name '{ name } ' already exists." , file = sys .stderr )
63+ sys .exit (1 )
64+ if any (node ['ip' ] == ip for node in nodes ):
65+ print (f"Error: A node with the IP/domain '{ ip } ' already exists." , file = sys .stderr )
66+ sys .exit (1 )
67+
68+ nodes .append ({"name" : name , "ip" : ip })
69+ write_nodes (nodes )
70+ print (f"Successfully added node '{ name } ' with IP/domain '{ ip } '." )
71+
72+ def delete_node (name : str ):
73+ nodes = read_nodes ()
74+ original_count = len (nodes )
75+ nodes = [node for node in nodes if node ['name' ] != name ]
76+
77+ if len (nodes ) == original_count :
78+ print (f"Error: No node with the name '{ name } ' found." , file = sys .stderr )
79+ sys .exit (1 )
80+
81+ write_nodes (nodes )
82+ print (f"Successfully deleted node '{ name } '." )
83+
84+ def list_nodes ():
85+ nodes = read_nodes ()
86+ if not nodes :
87+ print ("No nodes configured." )
88+ return
89+
90+ print (f"{ 'Name' :<30} { 'IP Address / Domain' } " )
91+ print (f"{ '-' * 30 } { '-' * 25 } " )
92+ for node in sorted (nodes , key = lambda x : x ['name' ]):
93+ print (f"{ node ['name' ]:<30} { node ['ip' ]} " )
94+
95+ def main ():
96+ parser = argparse .ArgumentParser (description = "Manage external node configurations." )
97+ subparsers = parser .add_subparsers (dest = 'command' , required = True )
98+
99+ add_parser = subparsers .add_parser ('add' , help = 'Add a new node.' )
100+ add_parser .add_argument ('--name' , type = str , required = True , help = 'The unique name of the node.' )
101+ add_parser .add_argument ('--ip' , type = str , required = True , help = 'The IP address or domain of the node.' )
102+
103+ delete_parser = subparsers .add_parser ('delete' , help = 'Delete a node by name.' )
104+ delete_parser .add_argument ('--name' , type = str , required = True , help = 'The name of the node to delete.' )
105+
106+ subparsers .add_parser ('list' , help = 'List all configured nodes.' )
107+
108+ args = parser .parse_args ()
109+
110+ if args .command == 'add' :
111+ add_node (args .name , args .ip )
112+ elif args .command == 'delete' :
113+ delete_node (args .name )
114+ elif args .command == 'list' :
115+ list_nodes ()
116+
117+ if __name__ == "__main__" :
118+ main ()
0 commit comments