diff --git a/changelog b/changelog index 3173ad3..15d2ae0 100644 --- a/changelog +++ b/changelog @@ -1,31 +1,12 @@ -### ๐Ÿ›ก๏ธ **\[1.18.1] โ€“ Security Patch & Hysteria Settings Tab** +### ๐Ÿš€ **\[2.0.0] โ€“ Major Release: MongoDB Migration** -*Released: 2025-08-30* +*Released: 2025-09-10* -#### ๐Ÿ”’ Security +#### ๐Ÿ’พ Core Update -* ๐Ÿ›ก๏ธ **Open Redirect Fix**: - Removed `next_url` parameter from login flow to prevent **open redirect vulnerability**. - - Special thanks to [**@HEXER365**](https://github.com/HEXER365) for responsible disclosure ๐Ÿ™ +* ๐Ÿ“ฆ **User management migrated from `users.json` โ†’ MongoDB** +* โšก Improved **scalability, performance, and reliability** for large deployments -#### โœจ Features +#### โš ๏ธ Breaking Change -* โš™๏ธ **New Hysteria Settings tab** in Web Panel (with **geo update support**) -* ๐ŸŽจ Redesigned **Login Page** for better UI/UX - -#### ๐Ÿ› ๏ธ Fixes - -* ๐Ÿ“ฆ Backup `extra.json` during upgrades -* ๐Ÿ“ฑ Improved responsive design across web panel -* ๐Ÿงฉ Relaxed conflict check in user viewmodel -* ๐Ÿงน Removed `/dev/null` redirects for cleaner logging - -#### ๐Ÿ“ฆ Chore & Dependencies - -* โฌ†๏ธ Updated dependencies: - - * `typing-extensions` โ†’ 4.15.0 - * `starlette` โ†’ 0.47.3 - * `requests` โ†’ 2.32.5 -* ๐Ÿงน Removed **deprecated `user.sh`** script (legacy auth) +* Previous JSON-based `users.json` file is no longer used diff --git a/core/cli_api.py b/core/cli_api.py index 07e30cb..e73117b 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -28,7 +28,7 @@ class Command(Enum): GET_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'get_user.py') ADD_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'add_user.py') BULK_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'bulk_users.py') - EDIT_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'edit_user.sh') + EDIT_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'edit_user.py') RESET_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'reset_user.py') REMOVE_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'remove_user.py') SHOW_USER_URI = os.path.join(SCRIPT_DIR, 'hysteria2', 'show_user_uri.py') @@ -307,43 +307,40 @@ def bulk_user_add(traffic_gb: float, expiration_days: int, count: int, prefix: s def edit_user(username: str, new_username: str | None, new_traffic_limit: int | None, new_expiration_days: int | None, renew_password: bool, renew_creation_date: bool, blocked: bool | None, unlimited_ip: bool | None): ''' - Edits an existing user's details. + Edits an existing user's details by calling the new edit_user.py script with named flags. ''' if not username: raise InvalidInputError('Error: username is required') - if new_traffic_limit is not None and new_traffic_limit < 0: - raise InvalidInputError('Error: traffic limit must be a non-negative number.') - if new_expiration_days is not None and new_expiration_days < 0: - raise InvalidInputError('Error: expiration days must be a non-negative number.') + command_args = ['python3', Command.EDIT_USER.value, username] - password = generate_password() if renew_password else '' - creation_date = datetime.now().strftime('%Y-%m-%d') if renew_creation_date else '' + if new_username: + command_args.extend(['--new-username', new_username]) - blocked_str = '' - if blocked is True: - blocked_str = 'true' - elif blocked is False: - blocked_str = 'false' + if new_traffic_limit is not None: + if new_traffic_limit < 0: + raise InvalidInputError('Error: traffic limit must be a non-negative number.') + command_args.extend(['--traffic-gb', str(new_traffic_limit)]) - unlimited_str = '' - if unlimited_ip is True: - unlimited_str = 'true' - elif unlimited_ip is False: - unlimited_str = 'false' + if new_expiration_days is not None: + if new_expiration_days < 0: + raise InvalidInputError('Error: expiration days must be a non-negative number.') + command_args.extend(['--expiration-days', str(new_expiration_days)]) + + if renew_password: + password = generate_password() + command_args.extend(['--password', password]) + + if renew_creation_date: + creation_date = datetime.now().strftime('%Y-%m-%d') + command_args.extend(['--creation-date', creation_date]) + + if blocked is not None: + command_args.extend(['--blocked', 'true' if blocked else 'false']) + + if unlimited_ip is not None: + command_args.extend(['--unlimited', 'true' if unlimited_ip else 'false']) - command_args = [ - 'bash', - Command.EDIT_USER.value, - username, - new_username or '', - str(new_traffic_limit) if new_traffic_limit is not None else '', - str(new_expiration_days) if new_expiration_days is not None else '', - password, - creation_date, - blocked_str, - unlimited_str - ] run_cmd(command_args) diff --git a/core/scripts/auth/user_auth.go b/core/scripts/auth/user_auth.go index 85ffed4..90f4654 100644 --- a/core/scripts/auth/user_auth.go +++ b/core/scripts/auth/user_auth.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/subtle" "encoding/json" "io" @@ -8,25 +9,30 @@ import ( "net/http" "os" "strings" - "sync" "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) const ( - listenAddr = "127.0.0.1:28262" - usersFile = "/etc/hysteria/users.json" - cacheTTL = 5 * time.Second + listenAddr = "127.0.0.1:28262" + mongoURI = "mongodb://localhost:27017" + dbName = "blitz_panel" + collectionName = "users" ) type User struct { - Password string `json:"password"` - MaxDownloadBytes int64 `json:"max_download_bytes"` - ExpirationDays int `json:"expiration_days"` - AccountCreationDate string `json:"account_creation_date"` - Blocked bool `json:"blocked"` - UploadBytes int64 `json:"upload_bytes"` - DownloadBytes int64 `json:"download_bytes"` - UnlimitedUser bool `json:"unlimited_user"` + ID string `bson:"_id"` + Password string `bson:"password"` + MaxDownloadBytes int64 `bson:"max_download_bytes"` + ExpirationDays int `bson:"expiration_days"` + AccountCreationDate string `bson:"account_creation_date"` + Blocked bool `bson:"blocked"` + UploadBytes int64 `bson:"upload_bytes"` + DownloadBytes int64 `bson:"download_bytes"` + UnlimitedUser bool `bson:"unlimited_user"` } type httpAuthRequest struct { @@ -40,24 +46,7 @@ type httpAuthResponse struct { ID string `json:"id"` } -var ( - userCache map[string]User - cacheMutex = &sync.RWMutex{} -) - -func loadUsersToCache() { - data, err := os.ReadFile(usersFile) - if err != nil { - return - } - var users map[string]User - if err := json.Unmarshal(data, &users); err != nil { - return - } - cacheMutex.Lock() - userCache = users - cacheMutex.Unlock() -} +var userCollection *mongo.Collection func authHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -77,11 +66,12 @@ func authHandler(w http.ResponseWriter, r *http.Request) { return } - cacheMutex.RLock() - user, ok := userCache[username] - cacheMutex.RUnlock() + var user User + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() - if !ok { + err := userCollection.FindOne(ctx, bson.M{"_id": username}).Decode(&user) + if err != nil { json.NewEncoder(w).Encode(httpAuthResponse{OK: false}) return } @@ -92,7 +82,7 @@ func authHandler(w http.ResponseWriter, r *http.Request) { } if subtle.ConstantTimeCompare([]byte(user.Password), []byte(password)) != 1 { - time.Sleep(5 * time.Second) // Slow down brute-force attacks + time.Sleep(5 * time.Second) json.NewEncoder(w).Encode(httpAuthResponse{OK: false}) return } @@ -122,18 +112,26 @@ func authHandler(w http.ResponseWriter, r *http.Request) { func main() { log.SetOutput(io.Discard) - loadUsersToCache() - ticker := time.NewTicker(cacheTTL) - go func() { - for range ticker.C { - loadUsersToCache() - } - }() + clientOptions := options.Client().ApplyURI(mongoURI) + client, err := mongo.Connect(context.TODO(), clientOptions) + if err != nil { + log.SetOutput(os.Stderr) + log.Fatalf("Failed to connect to MongoDB: %v", err) + } + + err = client.Ping(context.TODO(), nil) + if err != nil { + log.SetOutput(os.Stderr) + log.Fatalf("Failed to ping MongoDB: %v", err) + } + + userCollection = client.Database(dbName).Collection(collectionName) http.HandleFunc("/auth", authHandler) + log.SetOutput(os.Stderr) + log.Printf("Auth server starting on %s", listenAddr) if err := http.ListenAndServe(listenAddr, nil); err != nil { - log.SetOutput(os.Stderr) log.Fatalf("Failed to start server: %v", err) } -} +} \ No newline at end of file diff --git a/core/scripts/db/database.py b/core/scripts/db/database.py new file mode 100644 index 0000000..30bb403 --- /dev/null +++ b/core/scripts/db/database.py @@ -0,0 +1,41 @@ +import pymongo +from bson.objectid import ObjectId + +class Database: + def __init__(self, db_name="blitz_panel", collection_name="users"): + try: + self.client = pymongo.MongoClient("mongodb://localhost:27017/") + self.db = self.client[db_name] + self.collection = self.db[collection_name] + self.client.server_info() + except pymongo.errors.ConnectionFailure as e: + print(f"Could not connect to MongoDB: {e}") + raise + + def add_user(self, user_data): + username = user_data.pop('username', None) + if not username: + raise ValueError("Username is required") + + if self.collection.find_one({"_id": username.lower()}): + return None + + user_data['_id'] = username.lower() + return self.collection.insert_one(user_data) + + def get_user(self, username): + return self.collection.find_one({"_id": username.lower()}) + + def get_all_users(self): + return list(self.collection.find({})) + + def update_user(self, username, updates): + return self.collection.update_one({"_id": username.lower()}, {"$set": updates}) + + def delete_user(self, username): + return self.collection.delete_one({"_id": username.lower()}) + +try: + db = Database() +except pymongo.errors.ConnectionFailure: + db = None \ No newline at end of file diff --git a/core/scripts/db/migrate_users.py b/core/scripts/db/migrate_users.py new file mode 100644 index 0000000..7771aaa --- /dev/null +++ b/core/scripts/db/migrate_users.py @@ -0,0 +1,70 @@ +import json +from pathlib import Path +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from db.database import db + +def migrate(): + users_json_path = Path("/etc/hysteria/users.json") + + if not users_json_path.exists(): + print("users.json not found, no migration needed.") + return + + if db is None: + print("Error: Database connection failed. Cannot perform migration.", file=sys.stderr) + sys.exit(1) + + try: + with users_json_path.open('r') as f: + users_data = json.load(f) + except (json.JSONDecodeError, IOError) as e: + print(f"Error reading or parsing users.json: {e}", file=sys.stderr) + sys.exit(1) + + print(f"Found {len(users_data)} users in users.json to migrate.") + migrated_count = 0 + + for username, data in users_data.items(): + try: + user_doc = { + "_id": username.lower(), + "password": data.get("password"), + "max_download_bytes": data.get("max_download_bytes", 0), + "expiration_days": data.get("expiration_days", 0), + "account_creation_date": data.get("account_creation_date"), + "blocked": data.get("blocked", False), + "unlimited_user": data.get("unlimited_user", False), + "status": data.get("status", "Offline"), + "upload_bytes": data.get("upload_bytes", 0), + "download_bytes": data.get("download_bytes", 0), + } + + if user_doc["password"] is None: + print(f"Warning: User '{username}' has no password, skipping.", file=sys.stderr) + continue + + db.collection.update_one( + {'_id': user_doc['_id']}, + {'$set': user_doc}, + upsert=True + ) + migrated_count += 1 + print(f" - Migrated user: {username}") + + except Exception as e: + print(f"Error migrating user '{username}': {e}", file=sys.stderr) + + print(f"Migration complete. {migrated_count} users successfully migrated to MongoDB.") + + try: + migrated_file_path = users_json_path.with_name("users.json.migrated") + users_json_path.rename(migrated_file_path) + print(f"Renamed old user file to: {migrated_file_path}") + except OSError as e: + print(f"Warning: Could not rename users.json: {e}", file=sys.stderr) + +if __name__ == "__main__": + migrate() \ No newline at end of file diff --git a/core/scripts/hysteria2/add_user.py b/core/scripts/hysteria2/add_user.py index d9853e7..3a3fd6a 100644 --- a/core/scripts/hysteria2/add_user.py +++ b/core/scripts/hysteria2/add_user.py @@ -1,33 +1,22 @@ #!/usr/bin/env python3 -import json +import init_paths import sys import os import subprocess import re from datetime import datetime -from init_paths import * -from paths import * +from db.database import db def add_user(username, traffic_gb, expiration_days, password=None, creation_date=None, unlimited_user=False): - """ - Adds a new user to the USERS_FILE. - - Args: - username (str): The username to add. - traffic_gb (str): The traffic limit in GB. - expiration_days (str): The number of days until the account expires. - password (str, optional): The user's password. If None, a random one is generated. - creation_date (str, optional): The account creation date in YYYY-MM-DD format. Defaults to None. - unlimited_user (bool, optional): If True, user is exempt from IP limits. Defaults to False. - - Returns: - int: 0 on success, 1 on failure. - """ if not username or not traffic_gb or not expiration_days: print(f"Usage: {sys.argv[0]} [password] [creation_date] [unlimited_user (true/false)]") return 1 + if db is None: + print("Error: Database connection failed. Please ensure MongoDB is running and configured.") + return 1 + try: traffic_bytes = int(float(traffic_gb) * 1073741824) expiration_days = int(expiration_days) @@ -48,61 +37,45 @@ def add_user(username, traffic_gb, expiration_days, password=None, creation_date print("Error: Failed to generate password. Please install 'pwgen' or ensure /proc access.") return 1 - if creation_date: - if not re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", creation_date): - print("Invalid date format. Expected YYYY-MM-DD.") - return 1 - try: - datetime.strptime(creation_date, "%Y-%m-%d") - except ValueError: - print("Invalid date. Please provide a valid date in YYYY-MM-DD format.") - return 1 - else: - creation_date = None - if not re.match(r"^[a-zA-Z0-9_]+$", username): print("Error: Username can only contain letters, numbers, and underscores.") return 1 - if not os.path.isfile(USERS_FILE): - try: - with open(USERS_FILE, 'w') as f: - json.dump({}, f) - except IOError: - print(f"Error: Could not create {USERS_FILE}.") + try: + if db.get_user(username_lower): + print("User already exists.") return 1 - try: - with open(USERS_FILE, 'r+') as f: + user_data = { + "username": username_lower, + "password": password, + "max_download_bytes": traffic_bytes, + "expiration_days": expiration_days, + "blocked": False, + "unlimited_user": unlimited_user + } + + if creation_date: + if not re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", creation_date): + print("Invalid date format. Expected YYYY-MM-DD.") + return 1 try: - users_data = json.load(f) - except json.JSONDecodeError: - print(f"Error: {USERS_FILE} contains invalid JSON.") + datetime.strptime(creation_date, "%Y-%m-%d") + user_data["account_creation_date"] = creation_date + except ValueError: + print("Invalid date. Please provide a valid date in YYYY-MM-DD format.") return 1 - for existing_username in users_data: - if existing_username.lower() == username_lower: - print("User already exists.") - return 1 + result = db.add_user(user_data) + if result: + print(f"User {username} added successfully.") + return 0 + else: + print(f"Error: Failed to add user {username}.") + return 1 - users_data[username_lower] = { - "password": password, - "max_download_bytes": traffic_bytes, - "expiration_days": expiration_days, - "account_creation_date": creation_date, - "blocked": False, - "unlimited_user": unlimited_user - } - - f.seek(0) - json.dump(users_data, f, indent=4) - f.truncate() - - print(f"User {username} added successfully.") - return 0 - - except IOError: - print(f"Error: Could not write to {USERS_FILE}.") + except Exception as e: + print(f"An error occurred: {e}") return 1 if __name__ == "__main__": diff --git a/core/scripts/hysteria2/backup.py b/core/scripts/hysteria2/backup.py index 01dd60f..fa419bb 100644 --- a/core/scripts/hysteria2/backup.py +++ b/core/scripts/hysteria2/backup.py @@ -1,27 +1,69 @@ #!/usr/bin/env python3 import zipfile +import subprocess +import shutil from pathlib import Path from datetime import datetime -backup_dir = Path("/opt/hysbackup") -backup_file = backup_dir / f"hysteria_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" +# --- Configuration --- +DB_NAME = "blitz_panel" +BACKUP_ROOT_DIR = Path("/opt/hysbackup") +TIMESTAMP = datetime.now().strftime('%Y%m%d_%H%M%S') +BACKUP_FILENAME = BACKUP_ROOT_DIR / f"hysteria_backup_{TIMESTAMP}.zip" +TEMP_DUMP_DIR = BACKUP_ROOT_DIR / f"mongodump_{TIMESTAMP}" -files_to_backup = [ +FILES_TO_BACKUP = [ Path("/etc/hysteria/ca.key"), Path("/etc/hysteria/ca.crt"), - Path("/etc/hysteria/users.json"), Path("/etc/hysteria/config.json"), Path("/etc/hysteria/.configs.env"), ] -backup_dir.mkdir(parents=True, exist_ok=True) +def create_backup(): + """Dumps the MongoDB database and zips it with config files.""" + try: + BACKUP_ROOT_DIR.mkdir(parents=True, exist_ok=True) + TEMP_DUMP_DIR.mkdir(parents=True) -try: - with zipfile.ZipFile(backup_file, 'w') as zipf: - for file_path in files_to_backup: - if file_path.exists(): - zipf.write(file_path, arcname=file_path.name) - print("Backup successfully created") -except Exception as e: - print("Backup failed!", str(e)) + print(f"Dumping database '{DB_NAME}'...") + mongodump_cmd = [ + "mongodump", + f"--db={DB_NAME}", + f"--out={TEMP_DUMP_DIR}" + ] + subprocess.run(mongodump_cmd, check=True, capture_output=True) + print("Database dump successful.") + + print(f"Creating backup archive: {BACKUP_FILENAME}") + with zipfile.ZipFile(BACKUP_FILENAME, 'w', zipfile.ZIP_DEFLATED) as zipf: + for file_path in FILES_TO_BACKUP: + if file_path.exists() and file_path.is_file(): + zipf.write(file_path, arcname=file_path.name) + print(f" - Added {file_path.name}") + else: + print(f" - Warning: Skipping missing file {file_path}") + + dump_content_path = TEMP_DUMP_DIR / DB_NAME + if dump_content_path.exists(): + for file_path in dump_content_path.rglob('*'): + arcname = file_path.relative_to(TEMP_DUMP_DIR) + zipf.write(file_path, arcname=arcname) + print(f" - Added database dump for '{DB_NAME}'") + + print("\nBackup successfully created.") + + except FileNotFoundError: + print("\nBackup failed! 'mongodump' command not found. Is MongoDB installed and in your PATH?") + except subprocess.CalledProcessError as e: + print("\nBackup failed! Error during mongodump.") + print(f" - Stderr: {e.stderr.decode().strip()}") + except Exception as e: + print(f"\nBackup failed! An unexpected error occurred: {e}") + finally: + if TEMP_DUMP_DIR.exists(): + shutil.rmtree(TEMP_DUMP_DIR) + print("Temporary dump directory cleaned up.") + +if __name__ == "__main__": + create_backup() \ No newline at end of file diff --git a/core/scripts/hysteria2/bulk_users.py b/core/scripts/hysteria2/bulk_users.py index 1fdd23f..45051ff 100644 --- a/core/scripts/hysteria2/bulk_users.py +++ b/core/scripts/hysteria2/bulk_users.py @@ -1,97 +1,82 @@ #!/usr/bin/env python3 -import json +import init_paths import sys import os import subprocess import argparse import re -from datetime import datetime -from init_paths import * -from paths import * +from db.database import db def add_bulk_users(traffic_gb, expiration_days, count, prefix, start_number, unlimited_user): + if db is None: + print("Error: Database connection failed. Please ensure MongoDB is running.") + return 1 + try: traffic_bytes = int(float(traffic_gb) * 1073741824) except ValueError: print("Error: Traffic limit must be a numeric value.") return 1 - if not os.path.isfile(USERS_FILE): - try: - with open(USERS_FILE, 'w') as f: - json.dump({}, f) - except IOError: - print(f"Error: Could not create {USERS_FILE}.") + potential_usernames = [] + for i in range(count): + username = f"{prefix}{start_number + i}" + if not re.match(r"^[a-zA-Z0-9_]+$", username): + print(f"Error: Generated username '{username}' contains invalid characters. Aborting.") return 1 + potential_usernames.append(username.lower()) try: - with open(USERS_FILE, 'r+') as f: - try: - users_data = json.load(f) - except json.JSONDecodeError: - print(f"Error: {USERS_FILE} contains invalid JSON.") - return 1 + existing_docs = db.collection.find({"_id": {"$in": potential_usernames}}, {"_id": 1}) + existing_users_set = {doc['_id'] for doc in existing_docs} + except Exception as e: + print(f"Error querying database for existing users: {e}") + return 1 + + new_usernames = [u for u in potential_usernames if u not in existing_users_set] + new_users_count = len(new_usernames) - existing_users_lower = {u.lower() for u in users_data} - new_users_to_add = {} - creation_date = None - - try: - password_process = subprocess.run(['pwgen', '-s', '32', str(count)], capture_output=True, text=True, check=True) - passwords = password_process.stdout.strip().split('\n') - except (FileNotFoundError, subprocess.CalledProcessError): - print("Warning: 'pwgen' not found or failed. Falling back to UUID for password generation.") - passwords = [subprocess.check_output(['cat', '/proc/sys/kernel/random/uuid'], text=True).strip() for _ in range(count)] - - if len(passwords) < count: - print("Error: Could not generate enough passwords.") - return 1 - - for i in range(count): - username = f"{prefix}{start_number + i}" - username_lower = username.lower() - - if not re.match(r"^[a-zA-Z0-9_]+$", username_lower): - print(f"Error: Generated username '{username}' contains invalid characters. Use only letters, numbers, and underscores.") - continue - - if username_lower in existing_users_lower or username_lower in new_users_to_add: - print(f"Warning: User '{username}' already exists. Skipping.") - continue - - new_users_to_add[username_lower] = { - "password": passwords[i], - "max_download_bytes": traffic_bytes, - "expiration_days": expiration_days, - "account_creation_date": creation_date, - "blocked": False, - "unlimited_user": unlimited_user - } - # print(f"Preparing to add user: {username}") - - if not new_users_to_add: - print("No new users to add.") - return 0 - - users_data.update(new_users_to_add) - - f.seek(0) - json.dump(users_data, f, indent=4) - f.truncate() - - print(f"\nSuccessfully added {len(new_users_to_add)} users.") + if new_users_count == 0: + print("No new users to add. All generated usernames already exist.") return 0 - except IOError: - print(f"Error: Could not read or write to {USERS_FILE}.") + if count > new_users_count: + print(f"Warning: {count - new_users_count} user(s) already exist. Skipping them.") + + try: + password_process = subprocess.run(['pwgen', '-s', '32', str(new_users_count)], capture_output=True, text=True, check=True) + passwords = password_process.stdout.strip().split('\n') + except (FileNotFoundError, subprocess.CalledProcessError): + print("Warning: 'pwgen' not found or failed. Falling back to UUID for password generation.") + passwords = [subprocess.check_output(['cat', '/proc/sys/kernel/random/uuid'], text=True).strip() for _ in range(new_users_count)] + + if len(passwords) < new_users_count: + print("Error: Could not generate enough passwords.") return 1 + + users_to_insert = [] + for i, username in enumerate(new_usernames): + user_doc = { + "_id": username, + "password": passwords[i], + "max_download_bytes": traffic_bytes, + "expiration_days": expiration_days, + "blocked": False, + "unlimited_user": unlimited_user + } + users_to_insert.append(user_doc) + + try: + db.collection.insert_many(users_to_insert, ordered=False) + print(f"\nSuccessfully added {len(users_to_insert)} new users.") + return 0 except Exception as e: - print(f"An unexpected error occurred: {e}") + print(f"An unexpected error occurred during database insert: {e}") return 1 if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Add bulk users to Hysteria2.") + parser = argparse.ArgumentParser(description="Add bulk users to Hysteria2 via database.") parser.add_argument("-t", "--traffic-gb", dest="traffic_gb", type=float, required=True, help="Traffic limit for each user in GB.") parser.add_argument("-e", "--expiration-days", dest="expiration_days", type=int, required=True, help="Expiration duration for each user in days.") parser.add_argument("-c", "--count", type=int, required=True, help="Number of users to create.") diff --git a/core/scripts/hysteria2/edit_user.py b/core/scripts/hysteria2/edit_user.py new file mode 100644 index 0000000..1573913 --- /dev/null +++ b/core/scripts/hysteria2/edit_user.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +import init_paths +import sys +import os +import argparse +import re +from datetime import datetime +from db.database import db + +def edit_user(username, new_username=None, new_password=None, traffic_gb=None, expiration_days=None, creation_date=None, blocked=None, unlimited_user=None): + if db is None: + print("Error: Database connection failed.", file=sys.stderr) + return 1 + + username_lower = username.lower() + try: + user_data = db.get_user(username_lower) + if not user_data: + print(f"Error: User '{username}' not found.", file=sys.stderr) + return 1 + except Exception as e: + print(f"Error fetching user data: {e}", file=sys.stderr) + return 1 + + updates = {} + + if new_password: + updates['password'] = new_password + + if traffic_gb is not None: + updates['max_download_bytes'] = int(float(traffic_gb) * 1073741824) + + if expiration_days is not None: + updates['expiration_days'] = int(expiration_days) + + if creation_date is not None: + if creation_date.lower() == 'null': + updates['account_creation_date'] = None + else: + updates['account_creation_date'] = creation_date + + if blocked is not None: + updates['blocked'] = blocked + + if unlimited_user is not None: + updates['unlimited_user'] = unlimited_user + + try: + if updates: + db.update_user(username_lower, updates) + print(f"User '{username}' attributes updated successfully.") + + if new_username and new_username.lower() != username_lower: + new_username_lower = new_username.lower() + if db.get_user(new_username_lower): + print(f"Error: Target username '{new_username}' already exists.", file=sys.stderr) + return 1 + + updated_user_data = db.get_user(username_lower) + + updated_user_data.pop('_id') + updated_user_data['_id'] = new_username_lower + + db.collection.insert_one(updated_user_data) + db.delete_user(username_lower) + print(f"User '{username}' successfully renamed to '{new_username}'.") + + elif not updates and not (new_username and new_username.lower() != username_lower): + print("No changes specified.") + + except Exception as e: + print(f"An error occurred during update: {e}", file=sys.stderr) + return 1 + + return 0 + + +def str_to_bool(val): + if val.lower() in ('true', 'y', '1'): + return True + elif val.lower() in ('false', 'n', '0'): + return False + raise argparse.ArgumentTypeError('Boolean value expected (true/false, y/n, 1/0).') + +def validate_date(date_str): + if date_str.lower() == 'null': + return date_str + try: + datetime.strptime(date_str, "%Y-%m-%d") + return date_str + except ValueError: + raise argparse.ArgumentTypeError("Invalid date format. Expected YYYY-MM-DD or 'null'.") + +def validate_username(username): + if not re.match(r"^[a-zA-Z0-9_]+$", username): + raise argparse.ArgumentTypeError("Username can only contain letters, numbers, and underscores.") + return username + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Edit a Hysteria2 user's details in the database.") + parser.add_argument("username", type=validate_username, help="The current username of the user to edit.") + parser.add_argument("--new-username", dest="new_username", type=validate_username, help="New username for the user.") + parser.add_argument("--password", dest="new_password", help="New password for the user.") + parser.add_argument("--traffic-gb", dest="traffic_gb", type=float, help="New traffic limit in GB (e.g., 50). Use 0 for unlimited.") + parser.add_argument("--expiration-days", dest="expiration_days", type=int, help="New expiration in days from creation date (e.g., 30). Use 0 for unlimited.") + parser.add_argument("--creation-date", dest="creation_date", type=validate_date, help="New creation date in YYYY-MM-DD format, or 'null' to reset to On-hold.") + parser.add_argument("--blocked", type=str_to_bool, help="Set blocked status (true/false).") + parser.add_argument("--unlimited", dest="unlimited_user", type=str_to_bool, help="Set unlimited user status for IP limits (true/false).") + + args = parser.parse_args() + + sys.exit(edit_user( + username=args.username, + new_username=args.new_username, + new_password=args.new_password, + traffic_gb=args.traffic_gb, + expiration_days=args.expiration_days, + creation_date=args.creation_date, + blocked=args.blocked, + unlimited_user=args.unlimited_user + )) \ No newline at end of file diff --git a/core/scripts/hysteria2/edit_user.sh b/core/scripts/hysteria2/edit_user.sh deleted file mode 100644 index c006bfd..0000000 --- a/core/scripts/hysteria2/edit_user.sh +++ /dev/null @@ -1,268 +0,0 @@ -#!/bin/bash - -source /etc/hysteria/core/scripts/utils.sh -source /etc/hysteria/core/scripts/path.sh - -readonly GB_TO_BYTES=$((1024 * 1024 * 1024)) - -validate_username() { - local username=$1 - if [ -z "$username" ]; then - return 0 - fi - if ! [[ "$username" =~ ^[a-zA-Z0-9]+$ ]]; then - echo "Username can only contain letters and numbers." - return 1 - fi - return 0 -} - - -validate_traffic_limit() { - local traffic_limit=$1 - if [ -z "$traffic_limit" ]; then - return 0 - fi - if ! [[ "$traffic_limit" =~ ^[0-9]+$ ]]; then - echo "Error: Traffic limit must be a valid non-negative number (use 0 for unlimited)." - return 1 - fi - return 0 -} - -validate_expiration_days() { - local expiration_days=$1 - if [ -z "$expiration_days" ]; then - return 0 - fi - if ! [[ "$expiration_days" =~ ^[0-9]+$ ]]; then - echo "Error: Expiration days must be a valid non-negative number (use 0 for unlimited)." - return 1 - fi - return 0 -} - -validate_date() { - local date_str=$1 - if [ -z "$date_str" ]; then - return 0 - fi - if [ "$date_str" == "null" ]; then - return 0 - fi - if ! [[ "$date_str" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then - echo "Invalid date format. Expected YYYY-MM-DD." - return 1 - elif ! date -d "$date_str" >/dev/null 2>&1; then - echo "Invalid date. Please provide a valid date in YYYY-MM-DD format." - return 1 - fi - return 0 -} - -validate_blocked_status() { - local status=$1 - if [ -z "$status" ]; then - return 0 - fi - if [ "$status" != "true" ] && [ "$status" != "false" ]; then - echo "Blocked status must be 'true' or 'false'." - return 1 - fi - return 0 -} - -validate_unlimited_status() { - local status=$1 - if [ -z "$status" ]; then - return 0 - fi - if [ "$status" != "true" ] && [ "$status" != "false" ]; then - echo "Unlimited status must be 'true' or 'false'." - return 1 - fi - return 0 -} - - -convert_boolean_status() { - local status=$1 - case "$status" in - y|Y) echo "true" ;; - n|N) echo "false" ;; - true|false) echo "$status" ;; - *) echo "false" ;; - esac -} - -get_user_info() { - local username=$1 - python3 $CLI_PATH get-user -u "$username" -} - -update_user_info() { - local old_username=$1 - local new_username=$2 - local new_password=$3 - local new_max_download_bytes=$4 - local new_expiration_days=$5 - local new_account_creation_date=$6 - local new_blocked=$7 - local new_unlimited=$8 - - if [ ! -f "$USERS_FILE" ]; then - echo -e "${red}Error:${NC} File '$USERS_FILE' not found." - return 1 - fi - - - user_exists=$(jq -e --arg username "$old_username" '.[$username]' "$USERS_FILE" >/dev/null 2>&1 ) - - if [ $? -ne 0 ]; then - echo -e "${red}Error:${NC} User '$old_username' not found." - return 1 - fi - - existing_user_data=$(jq --arg username "$old_username" '.[$username]' "$USERS_FILE") - upload_bytes=$(echo "$existing_user_data" | jq -r '.upload_bytes // 0') - download_bytes=$(echo "$existing_user_data" | jq -r '.download_bytes // 0') - status=$(echo "$existing_user_data" | jq -r '.status // "Offline"') - - - echo "Updating user:" - echo "Username: $new_username" - echo "Password: ${new_password:-(not changed)}" - echo "Max Download Bytes: ${new_max_download_bytes:-(not changed)}" - echo "Expiration Days: ${new_expiration_days:-(not changed)}" - echo "Creation Date: ${new_account_creation_date:-(not changed)}" - echo "Blocked: $new_blocked" - echo "Unlimited IP: $new_unlimited" - - - jq \ - --arg old_username "$old_username" \ - --arg new_username "$new_username" \ - --arg password "${new_password:-null}" \ - --argjson max_download_bytes "${new_max_download_bytes:-null}" \ - --argjson expiration_days "${new_expiration_days:-null}" \ - --arg account_creation_date "${new_account_creation_date:-null}" \ - --argjson blocked "$new_blocked" \ - --argjson unlimited "$new_unlimited" \ - --argjson upload_bytes "$upload_bytes" \ - --argjson download_bytes "$download_bytes" \ - --arg status "$status" \ - ' - .[$new_username] = .[$old_username] | - (if $old_username != $new_username then del(.[$old_username]) else . end) | - .[$new_username] |= ( - .password = ($password // .password) | - .max_download_bytes = ($max_download_bytes // .max_download_bytes) | - .expiration_days = ($expiration_days // .expiration_days) | - .account_creation_date = (if $account_creation_date == "null" then null else $account_creation_date end) | - .blocked = $blocked | - .unlimited_user = $unlimited | - .status = $status - ) | - (if .[$new_username].status != "On-hold" then - .[$new_username] |= ( - .upload_bytes = $upload_bytes | - .download_bytes = $download_bytes - ) - else . end - ) - ' "$USERS_FILE" > tmp.$$.json && mv tmp.$$.json "$USERS_FILE" - - if [ $? -ne 0 ]; then - echo "Failed to update user '$old_username' in '$USERS_FILE'." - return 1 - fi - - return 0 -} - - -edit_user() { - local username=$1 - local new_username=$2 - local new_traffic_limit=$3 - local new_expiration_days=$4 - local new_password=$5 - local new_creation_date=$6 - local new_blocked=$7 - local new_unlimited=$8 - - - local user_info=$(get_user_info "$username") - - if [ $? -ne 0 ] || [ -z "$user_info" ]; then - echo "User '$username' not found." - return 1 - fi - - local password=$(echo "$user_info" | jq -r '.password') - local traffic_limit=$(echo "$user_info" | jq -r '.max_download_bytes') - local expiration_days=$(echo "$user_info" | jq -r '.expiration_days') - local creation_date=$(echo "$user_info" | jq -r '.account_creation_date') - local blocked=$(echo "$user_info" | jq -r '.blocked') - local unlimited_user=$(echo "$user_info" | jq -r '.unlimited_user // false') - - if ! validate_username "$new_username"; then - echo "Invalid username: $new_username" - return 1 - fi - - if ! validate_traffic_limit "$new_traffic_limit"; then - echo "Invalid traffic limit: $new_traffic_limit" - return 1 - fi - - - if ! validate_expiration_days "$new_expiration_days"; then - echo "Invalid expiration days: $new_expiration_days" - return 1 - fi - - - if ! validate_date "$new_creation_date"; then - echo "Invalid creation date: $new_creation_date" - return 1 - fi - - if ! validate_blocked_status "$new_blocked"; then - echo "Invalid blocked status: $new_blocked" - return 1 - fi - - if ! validate_unlimited_status "$new_unlimited"; then - echo "Invalid unlimited status: $new_unlimited" - return 1 - fi - - - new_username=${new_username:-$username} - new_password=${new_password:-$password} - - - if [ -n "$new_traffic_limit" ]; then - new_traffic_limit=$((new_traffic_limit * GB_TO_BYTES)) - else - new_traffic_limit=$traffic_limit - fi - - new_expiration_days=${new_expiration_days:-$expiration_days} - new_creation_date=${new_creation_date:-$creation_date} - new_blocked=$(convert_boolean_status "${new_blocked:-$blocked}") - new_unlimited=$(convert_boolean_status "${new_unlimited:-$unlimited_user}") - - - if ! update_user_info "$username" "$new_username" "$new_password" "$new_traffic_limit" "$new_expiration_days" "$new_creation_date" "$new_blocked" "$new_unlimited"; then - return 1 - fi - - echo "User updated successfully." - return 0 - -} - - -edit_user "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" \ No newline at end of file diff --git a/core/scripts/hysteria2/get_user.py b/core/scripts/hysteria2/get_user.py index 1273675..9a56aed 100644 --- a/core/scripts/hysteria2/get_user.py +++ b/core/scripts/hysteria2/get_user.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 +import init_paths import json import sys import os import getopt -from init_paths import * -from paths import * +from db.database import db def get_user_info(username): """ - Retrieves and prints information for a specific user from the USERS_FILE. + Retrieves and prints information for a specific user from the database. Args: username (str): The username to look up. @@ -17,30 +17,20 @@ def get_user_info(username): Returns: int: 0 on success, 1 on failure. """ - if not os.path.isfile(USERS_FILE): - print(f"users.json file not found at {USERS_FILE}!") + if db is None: + print("Error: Database connection failed. Please ensure MongoDB is running.") return 1 - + try: - with open(USERS_FILE, 'r') as f: - users_data = json.load(f) - except json.JSONDecodeError: - print(f"Error: {USERS_FILE} contains invalid JSON.") - return 1 - - if username in users_data: - user_info = users_data[username] - print(json.dumps(user_info, indent=4)) # Print with indentation for readability - # upload_bytes = user_info.get('upload_bytes', "No upload data available") - # download_bytes = user_info.get('download_bytes', "No download data available") - # status = user_info.get('status', "Status unavailable") - # You can choose to print these individually as well, if needed - # print(f"Upload Bytes: {upload_bytes}") - # print(f"Download Bytes: {download_bytes}") - # print(f"Status: {status}") - return 0 - else: - print(f"User '{username}' not found in {USERS_FILE}.") + user_info = db.get_user(username) + if user_info: + print(json.dumps(user_info, indent=4)) + return 0 + else: + print(f"User '{username}' not found in the database.") + return 1 + except Exception as e: + print(f"An error occurred while fetching user data: {e}") return 1 if __name__ == "__main__": @@ -54,7 +44,7 @@ if __name__ == "__main__": for opt, arg in opts: if opt in ("-u", "--username"): - username = arg + username = arg.lower() if not username: print(f"Usage: {sys.argv[0]} -u ") diff --git a/core/scripts/hysteria2/kick.py b/core/scripts/hysteria2/kick.py index ade55f7..031c3f8 100644 --- a/core/scripts/hysteria2/kick.py +++ b/core/scripts/hysteria2/kick.py @@ -1,18 +1,17 @@ #!/usr/bin/env python3 +import init_paths import os import sys import json -import time import fcntl -import shutil import datetime -from concurrent.futures import ThreadPoolExecutor -from init_paths import * -from paths import * -from hysteria2_api import Hysteria2Client - import logging +from concurrent.futures import ThreadPoolExecutor +from db.database import db +from hysteria2_api import Hysteria2Client +from paths import CONFIG_FILE + logging.basicConfig( stream=sys.stdout, level=logging.INFO, @@ -22,8 +21,8 @@ logging.basicConfig( logger = logging.getLogger() LOCKFILE = "/tmp/kick.lock" -BACKUP_FILE = f"{USERS_FILE}.bak" MAX_WORKERS = 8 +API_BASE_URL = 'http://127.0.0.1:25413' def acquire_lock(): try: @@ -34,144 +33,104 @@ def acquire_lock(): logger.warning("Another instance is already running. Exiting.") sys.exit(1) -def kick_users(usernames, secret): +def get_secret(): try: - client = Hysteria2Client( - base_url="http://127.0.0.1:25413", - secret=secret - ) - - client.kick_clients(usernames) - logger.info(f"Successfully kicked {len(usernames)} users: {', '.join(usernames)}") - return True - except Exception as e: - logger.error(f"Error kicking users: {str(e)}") - return False + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + return config.get('trafficStats', {}).get('secret') + except (json.JSONDecodeError, FileNotFoundError): + return None -def process_user(username, user_data, config_secret, users_data): - blocked = user_data.get('blocked', False) - - if blocked: - logger.info(f"Skipping {username} as they are already blocked.") - return None - - max_download_bytes = user_data.get('max_download_bytes', 0) - expiration_days = user_data.get('expiration_days', 0) - account_creation_date = user_data.get('account_creation_date') - current_download_bytes = user_data.get('download_bytes', 0) - current_upload_bytes = user_data.get('upload_bytes', 0) - - total_bytes = current_download_bytes + current_upload_bytes - - if not account_creation_date: - logger.info(f"Skipping {username} due to missing account creation date.") - return None - +def kick_users_api(usernames, secret): try: - current_date = datetime.datetime.now().timestamp() - creation_date = datetime.datetime.fromisoformat(account_creation_date.replace('Z', '+00:00')) - expiration_date = (creation_date + datetime.timedelta(days=expiration_days)).timestamp() - - should_block = False - - if max_download_bytes > 0 and total_bytes >= 0 and expiration_days > 0: - if total_bytes >= max_download_bytes or current_date >= expiration_date: - should_block = True - - if should_block: - logger.info(f"Setting blocked=True for user {username}") - users_data[username]['blocked'] = True - return username - else: - logger.info(f"Skipping {username} due to invalid or missing data.") - return None - + client = Hysteria2Client(base_url=API_BASE_URL, secret=secret) + client.kick_clients(usernames) + logger.info(f"Successfully sent kick command for users: {', '.join(usernames)}") except Exception as e: - logger.error(f"Error processing user {username}: {str(e)}") + logger.error(f"Error kicking users via API: {e}") + +def process_user(user_doc): + username = user_doc.get('_id') + + if not username or user_doc.get('blocked', False): return None + + account_creation_date = user_doc.get('account_creation_date') + if not account_creation_date: + return None + + should_block = False + + try: + expiration_days = user_doc.get('expiration_days', 0) + if expiration_days > 0: + creation_date = datetime.datetime.strptime(account_creation_date, "%Y-%m-%d") + expiration_date = creation_date + datetime.timedelta(days=expiration_days) + if datetime.datetime.now() >= expiration_date: + should_block = True + logger.info(f"User {username} is expired.") + + if not should_block: + max_download_bytes = user_doc.get('max_download_bytes', 0) + if max_download_bytes > 0: + total_bytes = user_doc.get('download_bytes', 0) + user_doc.get('upload_bytes', 0) + if total_bytes >= max_download_bytes: + should_block = True + logger.info(f"User {username} has exceeded their traffic limit.") + + if should_block: + return username + + except (ValueError, TypeError) as e: + logger.error(f"Error processing user {username} due to invalid data: {e}") return None def main(): lock_file = acquire_lock() - try: - shutil.copy2(USERS_FILE, BACKUP_FILE) - logger.info(f"Created backup of users file at {BACKUP_FILE}") - - try: - with open(CONFIG_FILE, 'r') as f: - config = json.load(f) - secret = config.get('trafficStats', {}).get('secret', '') - if not secret: - logger.error("No secret found in config file") - sys.exit(1) - except Exception as e: - logger.error(f"Failed to load config file: {str(e)}") - shutil.copy2(BACKUP_FILE, USERS_FILE) + if db is None: + logger.error("Database connection failed. Exiting.") + sys.exit(1) + + secret = get_secret() + if not secret: + logger.error(f"Could not find secret in {CONFIG_FILE}. Exiting.") sys.exit(1) - try: - with open(USERS_FILE, 'r') as f: - users_data = json.load(f) - logger.info(f"Loaded data for {len(users_data)} users") - except json.JSONDecodeError: - logger.error("Invalid users.json. Restoring backup.") - shutil.copy2(BACKUP_FILE, USERS_FILE) - sys.exit(1) - except Exception as e: - logger.error(f"Failed to load users file: {str(e)}") - shutil.copy2(BACKUP_FILE, USERS_FILE) - sys.exit(1) + all_users = db.get_all_users() + logger.info(f"Loaded {len(all_users)} users from the database for processing.") - users_to_kick = [] - logger.info(f"Processing {len(users_data)} users in parallel with {MAX_WORKERS} workers") + users_to_block = [] with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: - future_to_user = { - executor.submit(process_user, username, user_data, secret, users_data): username - for username, user_data in users_data.items() - } - + future_to_user = {executor.submit(process_user, user_doc): user_doc for user_doc in all_users} for future in future_to_user: - username = future.result() - if username: - users_to_kick.append(username) - logger.info(f"User {username} added to kick list") + result = future.result() + if result: + users_to_block.append(result) - if users_to_kick: - logger.info(f"Saving changes to users file for {len(users_to_kick)} blocked users") - for retry in range(3): - try: - with open(USERS_FILE, 'w') as f: - json.dump(users_data, f, indent=2) - break - except Exception as e: - logger.error(f"Failed to save users file (attempt {retry+1}): {str(e)}") - time.sleep(1) - if retry == 2: - raise + if not users_to_block: + logger.info("No users to block or kick.") + return + + logger.info(f"Found {len(users_to_block)} users to block: {', '.join(users_to_block)}") - if users_to_kick: - logger.info(f"Kicking {len(users_to_kick)} users") - batch_size = 50 - for i in range(0, len(users_to_kick), batch_size): - batch = users_to_kick[i:i+batch_size] - logger.info(f"Processing batch of {len(batch)} users") - kick_users(batch, secret) - for username in batch: - logger.info(f"Blocked and kicked user {username}") - else: - logger.info("No users to kick") + for username in users_to_block: + db.update_user(username, {'blocked': True}) + logger.info("Successfully updated user statuses to 'blocked' in the database.") + + batch_size = 50 + for i in range(0, len(users_to_block), batch_size): + batch = users_to_block[i:i + batch_size] + kick_users_api(batch, secret) except Exception as e: - logger.error(f"An error occurred: {str(e)}") - logger.info("Restoring users file from backup") - shutil.copy2(BACKUP_FILE, USERS_FILE) + logger.error(f"An unexpected error occurred in main execution: {e}", exc_info=True) sys.exit(1) finally: fcntl.flock(lock_file, fcntl.LOCK_UN) lock_file.close() - logger.info("Script completed") + logger.info("Script finished.") if __name__ == "__main__": diff --git a/core/scripts/hysteria2/list_users.py b/core/scripts/hysteria2/list_users.py index a1ce151..2d1e45a 100644 --- a/core/scripts/hysteria2/list_users.py +++ b/core/scripts/hysteria2/list_users.py @@ -1,14 +1,12 @@ +#!/usr/bin/env python3 + +import init_paths import sys import json from pathlib import Path - -try: - from hysteria2_api import Hysteria2Client -except ImportError: - sys.exit("Error: hysteria2_api library not found. Please install it.") - -sys.path.append(str(Path(__file__).parent.parent)) -from paths import USERS_FILE, CONFIG_FILE, API_BASE_URL +from hysteria2_api import Hysteria2Client +from db.database import db +from paths import CONFIG_FILE, API_BASE_URL def get_secret() -> str | None: if not CONFIG_FILE.exists(): @@ -20,34 +18,45 @@ def get_secret() -> str | None: except (json.JSONDecodeError, IOError): return None -def get_users() -> dict: - if not USERS_FILE.exists(): - return {} +def get_users_from_db() -> list: + if db is None: + print("Error: Database connection failed.", file=sys.stderr) + return [] try: - with USERS_FILE.open('r') as f: - return json.load(f) - except (json.JSONDecodeError, IOError): - return {} + users = db.get_all_users() + for user in users: + user['username'] = user.pop('_id') + return users + except Exception as e: + print(f"Error retrieving users from database: {e}", file=sys.stderr) + return [] def main(): - users_dict = get_users() + users_list = get_users_from_db() + if not users_list: + print(json.dumps([], indent=2)) + return + secret = get_secret() - if secret and users_dict: + if secret: try: client = Hysteria2Client(base_url=API_BASE_URL, secret=secret) online_clients = client.get_online_clients() - + + users_dict = {user['username']: user for user in users_list} for username, status in online_clients.items(): if status.is_online and username in users_dict: users_dict[username]['online_count'] = status.connections - except Exception: + + users_list = list(users_dict.values()) + + except Exception as e: + print(f"Warning: Could not connect to Hysteria2 API to get online status. {e}", file=sys.stderr) pass - users_list = [ - {**user_data, 'username': username, 'online_count': user_data.get('online_count', 0)} - for username, user_data in users_dict.items() - ] + for user in users_list: + user.setdefault('online_count', 0) print(json.dumps(users_list, indent=2)) diff --git a/core/scripts/hysteria2/list_users.sh b/core/scripts/hysteria2/list_users.sh deleted file mode 100644 index ca76d18..0000000 --- a/core/scripts/hysteria2/list_users.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -source /etc/hysteria/core/scripts/path.sh - - -cat "$USERS_FILE" \ No newline at end of file diff --git a/core/scripts/hysteria2/remove_user.py b/core/scripts/hysteria2/remove_user.py index 0663eb4..d5a32a3 100644 --- a/core/scripts/hysteria2/remove_user.py +++ b/core/scripts/hysteria2/remove_user.py @@ -1,46 +1,33 @@ #!/usr/bin/env python3 -import json +import init_paths import sys import os -import asyncio -from init_paths import * -from paths import * +from db.database import db -def sync_remove_user(username): - if not os.path.isfile(USERS_FILE): - return 1, f"Error: Config file {USERS_FILE} not found." +def remove_user(username): + if db is None: + return 1, "Error: Database connection failed. Please ensure MongoDB is running." try: - with open(USERS_FILE, 'r') as f: - try: - users_data = json.load(f) - except json.JSONDecodeError: - return 1, f"Error: {USERS_FILE} contains invalid JSON." - - if username in users_data: - del users_data[username] - with open(USERS_FILE, 'w') as f: - json.dump(users_data, f, indent=4) + result = db.delete_user(username) + if result.deleted_count > 0: return 0, f"User {username} removed successfully." else: return 1, f"Error: User {username} not found." except Exception as e: - return 1, f"Error: {str(e)}" + return 1, f"An error occurred while removing the user: {e}" -async def remove_user(username): - return await asyncio.to_thread(sync_remove_user, username) - -async def main(): +def main(): if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} ") sys.exit(1) - username = sys.argv[1] - exit_code, message = await remove_user(username) + username = sys.argv[1].lower() + exit_code, message = remove_user(username) print(message) sys.exit(exit_code) if __name__ == "__main__": - asyncio.run(main()) + main() \ No newline at end of file diff --git a/core/scripts/hysteria2/reset_user.py b/core/scripts/hysteria2/reset_user.py index 98598eb..91516af 100644 --- a/core/scripts/hysteria2/reset_user.py +++ b/core/scripts/hysteria2/reset_user.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 -import json +import init_paths import sys import os from datetime import date -from init_paths import * -from paths import * +from db.database import db def reset_user(username): """ - Resets the data usage, status, and creation date of a user in the USERS_FILE. + Resets the data usage, status, and creation date of a user in the database. Args: username (str): The username to reset. @@ -17,35 +16,34 @@ def reset_user(username): Returns: int: 0 on success, 1 on failure. """ - if not os.path.isfile(USERS_FILE): - print(f"Error: File '{USERS_FILE}' not found.") + if db is None: + print("Error: Database connection failed. Please ensure MongoDB is running.") return 1 try: - with open(USERS_FILE, 'r') as f: - users_data = json.load(f) - except json.JSONDecodeError: - print(f"Error: {USERS_FILE} contains invalid JSON.") - return 1 + user = db.get_user(username) + if not user: + print(f"Error: User '{username}' not found in the database.") + return 1 - if username not in users_data: - print(f"Error: User '{username}' not found in '{USERS_FILE}'.") - return 1 + updates = { + 'upload_bytes': 0, + 'download_bytes': 0, + 'status': 'Offline', + 'account_creation_date': date.today().strftime("%Y-%m-%d"), + 'blocked': False + } - today = date.today().strftime("%Y-%m-%d") - users_data[username]['upload_bytes'] = 0 - users_data[username]['download_bytes'] = 0 - users_data[username]['status'] = "Offline" - users_data[username]['account_creation_date'] = today - users_data[username]['blocked'] = False + result = db.update_user(username, updates) + if result.modified_count > 0: + print(f"User '{username}' has been reset successfully.") + return 0 + else: + print(f"User '{username}' data was already in a reset state. No changes made.") + return 0 - try: - with open(USERS_FILE, 'w') as f: - json.dump(users_data, f, indent=4) - print(f"User '{username}' has been reset successfully.") - return 0 - except IOError: - print(f"Error: Failed to reset user '{username}' in '{USERS_FILE}'.") + except Exception as e: + print(f"An error occurred while resetting the user: {e}") return 1 if __name__ == "__main__": @@ -53,6 +51,6 @@ if __name__ == "__main__": print(f"Usage: {sys.argv[0]} ") sys.exit(1) - username_to_reset = sys.argv[1] + username_to_reset = sys.argv[1].lower() exit_code = reset_user(username_to_reset) sys.exit(exit_code) \ No newline at end of file diff --git a/core/scripts/hysteria2/restore.py b/core/scripts/hysteria2/restore.py index a8a85b7..b9292e6 100644 --- a/core/scripts/hysteria2/restore.py +++ b/core/scripts/hysteria2/restore.py @@ -7,158 +7,120 @@ import shutil import zipfile import tempfile import subprocess -import datetime from pathlib import Path -from init_paths import * -from paths import * -def run_command(command, capture_output=True, check=False): - """Run a shell command and return its output""" - result = subprocess.run( - command, - shell=True, - capture_output=capture_output, - text=True, - check=check - ) - if capture_output: - return result.returncode, result.stdout.strip() - return result.returncode, None +DB_NAME = "blitz_panel" +HYSTERIA_CONFIG_DIR = Path("/etc/hysteria") +CLI_PATH = Path("/etc/hysteria/core/cli.py") + +def run_command(command, check=False): + try: + return subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + check=check + ) + except subprocess.CalledProcessError as e: + print(f"Error executing command: '{e.cmd}'", file=sys.stderr) + print(f"Stderr: {e.stderr}", file=sys.stderr) + raise def main(): if len(sys.argv) < 2: - print("Error: Backup file path is required.") + print("Error: Backup file path is required.", file=sys.stderr) return 1 - backup_zip_file = sys.argv[1] + backup_zip_file = Path(sys.argv[1]) - if not os.path.isfile(backup_zip_file): - print(f"Error: Backup file not found: {backup_zip_file}") + if not backup_zip_file.is_file(): + print(f"Error: Backup file not found: {backup_zip_file}", file=sys.stderr) return 1 - if not backup_zip_file.lower().endswith('.zip'): - print("Error: Backup file must be a .zip file.") + if backup_zip_file.suffix.lower() != '.zip': + print("Error: Backup file must be a .zip file.", file=sys.stderr) return 1 - timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - restore_dir = f"/tmp/hysteria_restore_{timestamp}" - target_dir = "/etc/hysteria" - try: - os.makedirs(restore_dir, exist_ok=True) - - try: - with zipfile.ZipFile(backup_zip_file) as zf: - zf.testzip() - zf.extractall(restore_dir) - except zipfile.BadZipFile: - print("Error: Invalid ZIP file.") - return 1 - except Exception as e: - print(f"Error: Could not extract the ZIP file: {e}") - return 1 - - expected_files = [ - "ca.key", - "ca.crt", - "users.json", - "config.json", - ".configs.env" - ] - - for file in expected_files: - file_path = os.path.join(restore_dir, file) - if not os.path.isfile(file_path): - print(f"Error: Required file '{file}' is missing from the backup.") - return 1 - - existing_backup_dir = f"/opt/hysbackup/restore_pre_backup_{timestamp}" - os.makedirs(existing_backup_dir, exist_ok=True) - - for file in expected_files: - source_file = os.path.join(target_dir, file) - dest_file = os.path.join(existing_backup_dir, file) - - if os.path.isfile(source_file): - try: - shutil.copy2(source_file, dest_file) - except Exception as e: - print(f"Error creating backup file before restore from '{source_file}': {e}") - return 1 - - for file in expected_files: - source_file = os.path.join(restore_dir, file) - dest_file = os.path.join(target_dir, file) - + with tempfile.TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + print(f"Extracting backup to temporary directory: {temp_dir}") + try: - shutil.copy2(source_file, dest_file) - except Exception as e: - print(f"Error: replace Configuration Files '{file}': {e}") - shutil.rmtree(existing_backup_dir, ignore_errors=True) + with zipfile.ZipFile(backup_zip_file) as zf: + zf.extractall(temp_dir) + except zipfile.BadZipFile: + print("Error: Invalid or corrupt ZIP file.", file=sys.stderr) + return 1 + + dump_dir = temp_dir / DB_NAME + if not dump_dir.is_dir(): + print("Error: Backup is in an old format or is missing the database dump.", file=sys.stderr) + print("Please use a backup created with the new MongoDB-aware script.", file=sys.stderr) return 1 - - config_file = os.path.join(target_dir, "config.json") - - if os.path.isfile(config_file): - print("Checking and adjusting config.json based on system state...") - ret_code, networkdef = run_command("ip route | grep '^default' | awk '{print $5}'") - networkdef = networkdef.strip() - - if networkdef: - with open(config_file, 'r') as f: - config = json.load(f) - - for outbound in config.get('outbounds', []): - if outbound.get('name') == 'v4' and 'direct' in outbound: - current_v4_device = outbound['direct'].get('bindDevice', '') - - if current_v4_device != networkdef: - print(f"Updating v4 outbound bindDevice from '{current_v4_device}' to '{networkdef}'...") - outbound['direct']['bindDevice'] = networkdef - - with open(config_file, 'w') as f: - json.dump(config, f, indent=2) - - ret_code, _ = run_command("systemctl is-active --quiet wg-quick@wgcf.service", capture_output=False) - - if ret_code != 0: - print("wgcf service is NOT active. Removing warps outbound and any ACL rules...") - - with open(config_file, 'r') as f: - config = json.load(f) - - config['outbounds'] = [outbound for outbound in config.get('outbounds', []) - if outbound.get('name') != 'warps'] - - if 'acl' in config and 'inline' in config['acl']: - config['acl']['inline'] = [rule for rule in config['acl']['inline'] - if not rule.startswith('warps(')] - - with open(config_file, 'w') as f: - json.dump(config, f, indent=2) - - run_command("chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt", - capture_output=False) - run_command("chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt", - capture_output=False) - - ret_code, _ = run_command(f"python3 {CLI_PATH} restart-hysteria2", capture_output=False) - - if ret_code != 0: - print("Error: Restart service failed.") - return 1 - - print("Hysteria configuration restored and updated successfully.") - return 0 - - except Exception as e: - print(f"An unexpected error occurred: {e}") + print("Restoring MongoDB database... (This will drop the current user data)") + run_command(f"mongorestore --db={DB_NAME} --drop --dir='{dump_dir}'", check=True) + print("Database restored successfully.") + + files_to_copy = ["config.json", ".configs.env", "ca.key", "ca.crt"] + print("Restoring configuration files...") + for filename in files_to_copy: + src = temp_dir / filename + if src.exists(): + shutil.copy2(src, HYSTERIA_CONFIG_DIR / filename) + print(f" - Restored {filename}") + + adjust_config_file() + + print("Setting permissions...") + run_command(f"chown hysteria:hysteria {HYSTERIA_CONFIG_DIR / 'ca.key'} {HYSTERIA_CONFIG_DIR / 'ca.crt'}") + run_command(f"chmod 640 {HYSTERIA_CONFIG_DIR / 'ca.key'} {HYSTERIA_CONFIG_DIR / 'ca.crt'}") + + print("Restarting Hysteria service...") + run_command(f"python3 {CLI_PATH} restart-hysteria2", check=True) + + print("\nRestore completed successfully.") + return 0 + + except subprocess.CalledProcessError: + print("\nRestore failed due to a command execution error.", file=sys.stderr) return 1 - finally: - shutil.rmtree(restore_dir, ignore_errors=True) - if 'existing_backup_dir' in locals(): - shutil.rmtree(existing_backup_dir, ignore_errors=True) + except Exception as e: + print(f"\nAn unexpected error occurred during restore: {e}", file=sys.stderr) + return 1 + +def adjust_config_file(): + config_file = HYSTERIA_CONFIG_DIR / "config.json" + if not config_file.exists(): + return + + print("Adjusting config.json based on current system state...") + try: + with open(config_file, 'r') as f: + config = json.load(f) + + result = run_command("ip route | grep '^default' | awk '{print $5}'") + network_device = result.stdout.strip() + if network_device: + for outbound in config.get('outbounds', []): + if outbound.get('name') == 'v4' and 'direct' in outbound: + outbound['direct']['bindDevice'] = network_device + + result = run_command("systemctl is-active --quiet wg-quick@wgcf.service") + if result.returncode != 0: + print(" - WARP service is inactive, removing related configuration.") + config['outbounds'] = [o for o in config.get('outbounds', []) if o.get('name') != 'warps'] + if 'acl' in config and 'inline' in config['acl']: + config['acl']['inline'] = [r for r in config['acl']['inline'] if not r.startswith('warps(')] + + with open(config_file, 'w') as f: + json.dump(config, f, indent=2) + + except Exception as e: + print(f"Warning: Could not adjust config.json. {e}", file=sys.stderr) + if __name__ == "__main__": sys.exit(main()) \ No newline at end of file diff --git a/core/scripts/hysteria2/show_user_uri.py b/core/scripts/hysteria2/show_user_uri.py index d841996..8fd9661 100644 --- a/core/scripts/hysteria2/show_user_uri.py +++ b/core/scripts/hysteria2/show_user_uri.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import init_paths import os import sys import json @@ -9,11 +10,10 @@ import re import qrcode from io import StringIO from typing import Tuple, Optional, Dict, List, Any -from init_paths import * +from db.database import db from paths import * def load_env_file(env_file: str) -> Dict[str, str]: - """Load environment variables from a file into a dictionary.""" env_vars = {} if os.path.exists(env_file): with open(env_file, 'r') as f: @@ -25,7 +25,6 @@ def load_env_file(env_file: str) -> Dict[str, str]: return env_vars def load_nodes() -> List[Dict[str, str]]: - """Load external node information from the nodes JSON file.""" if NODES_JSON_PATH.exists(): try: with NODES_JSON_PATH.open("r") as f: @@ -37,11 +36,9 @@ def load_nodes() -> List[Dict[str, str]]: return [] def load_hysteria2_env() -> Dict[str, str]: - """Load Hysteria2 environment variables.""" return load_env_file(CONFIG_ENV) def load_hysteria2_ips() -> Tuple[str, str, str]: - """Load Hysteria2 IPv4 and IPv6 addresses from environment.""" env_vars = load_hysteria2_env() ip4 = env_vars.get('IP4', 'None') ip6 = env_vars.get('IP6', 'None') @@ -49,14 +46,12 @@ def load_hysteria2_ips() -> Tuple[str, str, str]: return ip4, ip6, sni def get_singbox_domain_and_port() -> Tuple[str, str]: - """Get domain and port from SingBox config.""" env_vars = load_env_file(SINGBOX_ENV) domain = env_vars.get('HYSTERIA_DOMAIN', '') port = env_vars.get('HYSTERIA_PORT', '') return domain, port def get_normalsub_domain_and_port() -> Tuple[str, str, str]: - """Get domain, port, and subpath from Normal-SUB config.""" env_vars = load_env_file(NORMALSUB_ENV) domain = env_vars.get('HYSTERIA_DOMAIN', '') port = env_vars.get('HYSTERIA_PORT', '') @@ -64,7 +59,6 @@ def get_normalsub_domain_and_port() -> Tuple[str, str, str]: return domain, port, subpath def is_service_active(service_name: str) -> bool: - """Check if a systemd service is active.""" try: result = subprocess.run( ['systemctl', 'is-active', '--quiet', service_name], @@ -77,28 +71,23 @@ def is_service_active(service_name: str) -> bool: def generate_uri(username: str, auth_password: str, ip: str, port: str, obfs_password: str, sha256: str, sni: str, ip_version: int, insecure: bool, fragment_tag: str) -> str: - """Generate Hysteria2 URI for the given parameters.""" - uri_base = f"hy2://{username}%3A{auth_password}@{ip}:{port}" - - if ip_version == 6 and re.match(r'^[0-9a-fA-F:]+$', ip): - uri_base = f"hy2://{username}%3A{auth_password}@[{ip}]:{port}" + ip_part = f"[{ip}]" if ip_version == 6 and ':' in ip else ip + uri_base = f"hy2://{username}:{auth_password}@{ip_part}:{port}" params = [] - if obfs_password: params.append(f"obfs=salamander&obfs-password={obfs_password}") - if sha256: params.append(f"pinSHA256={sha256}") + if sni: + params.append(f"sni={sni}") - insecure_value = "1" if insecure else "0" - params.append(f"insecure={insecure_value}&sni={sni}") + params.append(f"insecure={'1' if insecure else '0'}") - params_str = "&".join(params) - return f"{uri_base}?{params_str}#{fragment_tag}" + query_string = "&".join(params) + return f"{uri_base}?{query_string}#{fragment_tag}" def generate_qr_code(uri: str) -> List[str]: - """Generate terminal-friendly ASCII QR code using pure Python.""" try: qr = qrcode.QRCode( version=1, @@ -116,18 +105,15 @@ def generate_qr_code(uri: str) -> List[str]: return [f"Error generating QR code: {str(e)}"] def center_text(text: str, width: int) -> str: - """Center text in the given width.""" return text.center(width) def get_terminal_width() -> int: - """Get terminal width.""" try: return os.get_terminal_size().columns except (AttributeError, OSError): return 80 def display_uri_and_qr(uri: str, label: str, args: argparse.Namespace, terminal_width: int): - """Helper function to print URI and its QR code.""" if not uri: return @@ -140,27 +126,28 @@ def display_uri_and_qr(uri: str, label: str, args: argparse.Namespace, terminal_ print(center_text(line, terminal_width)) def show_uri(args: argparse.Namespace) -> None: - """Show URI and optional QR codes for the given username and nodes.""" - if not os.path.exists(USERS_FILE): - print(f"\033[0;31mError:\033[0m Config file {USERS_FILE} not found.") + if db is None: + print("\033[0;31mError:\033[0m Database connection failed.") return - + if not is_service_active("hysteria-server.service"): print("\033[0;31mError:\033[0m Hysteria2 is not active.") return - with open(CONFIG_FILE, 'r') as f: - config = json.load(f) - - with open(USERS_FILE, 'r') as f: - users = json.load(f) - - if args.username not in users: - print("Invalid username. Please try again.") + try: + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"\033[0;31mError:\033[0m Could not load config file {CONFIG_FILE}. Details: {e}") + return + + user_doc = db.get_user(args.username) + if not user_doc: + print(f"\033[0;31mError:\033[0m User '{args.username}' not found in the database.") return - auth_password = users[args.username]["password"] - port = config["listen"].split(":")[1] if ":" in config["listen"] else config["listen"] + auth_password = user_doc["password"] + port = config["listen"].split(":")[-1] sha256 = config.get("tls", {}).get("pinSHA256", "") obfs_password = config.get("obfs", {}).get("salamander", {}).get("password", "") insecure = config.get("tls", {}).get("insecure", True) @@ -205,7 +192,6 @@ def show_uri(args: argparse.Namespace) -> None: print(f"\nNormal-SUB Sublink:\nhttps://{domain}:{port}/{subpath}/sub/normal/{auth_password}#Hysteria2\n") def main(): - """Main function to parse arguments and show URIs.""" parser = argparse.ArgumentParser(description="Hysteria2 URI Generator") parser.add_argument("-u", "--username", help="Username to generate URI for") parser.add_argument("-qr", "--qrcode", action="store_true", help="Generate QR code") diff --git a/core/scripts/hysteria2/uninstall.py b/core/scripts/hysteria2/uninstall.py index 5ea2767..0576628 100644 --- a/core/scripts/hysteria2/uninstall.py +++ b/core/scripts/hysteria2/uninstall.py @@ -1,11 +1,16 @@ -#!/usr/bin/env python3 - import os import subprocess import sys +from pathlib import Path + +try: + import pymongo +except ImportError: + pymongo = None SERVICES = [ "hysteria-server.service", + "hysteria-auth.service", "hysteria-webpanel.service", "hysteria-caddy.service", "hysteria-telegram-bot.service", @@ -15,81 +20,99 @@ SERVICES = [ "hysteria-scheduler.service", ] +DB_NAME = "blitz_panel" + def run_command(command, error_message): - """Runs a command and prints an error message if it fails.""" try: subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - return 0 + return True except subprocess.CalledProcessError: - print(error_message) - return 1 + print(f"Warning: {error_message}") + return False except FileNotFoundError: - print(f"Error: Command not found: {command[0]}") - return 1 + print(f"Warning: Command not found: {command[0]}") + return False + +def drop_mongodb_database(): + if not pymongo: + print("Warning: pymongo library not found. Skipping database cleanup.") + return + + print(f"Attempting to drop MongoDB database: '{DB_NAME}'...") + try: + client = pymongo.MongoClient("mongodb://localhost:27017/", serverSelectionTimeoutMS=5000) + client.server_info() + client.drop_database(DB_NAME) + print(f"Database '{DB_NAME}' dropped successfully.") + except pymongo.errors.ConnectionFailure: + print("Warning: Could not connect to MongoDB. Skipping database cleanup.") + except Exception as e: + print(f"An error occurred during database cleanup: {e}") def uninstall_hysteria(): - """Uninstalls Hysteria2.""" - print("Uninstalling Hysteria2...") + print("Uninstalling Hysteria2 and all related components...") - print("Running uninstallation script...") - run_command(["bash", "-c", "curl -fsSL https://get.hy2.sh/ | bash -- --remove"], "Error running the official uninstallation script.") + print("\nStep 1: Stopping and disabling all Hysteria services...") + for service in SERVICES: + run_command(["systemctl", "stop", service], f"Failed to stop {service}.") + run_command(["systemctl", "disable", service], f"Failed to disable {service}.") - print("Removing WARP") + print("\nStep 2: Removing systemd service files...") + systemd_path = Path("/etc/systemd/system") + for service in SERVICES: + service_file = systemd_path / service + if service_file.exists(): + run_command(["rm", "-f", str(service_file)], f"Failed to remove {service_file}") + + print("Reloading systemd daemon...") + run_command(["systemctl", "daemon-reload"], "Failed to reload systemd daemon.") + + print("\nStep 3: Removing Hysteria binaries...") + run_command(["bash", "-c", "curl -fsSL https://get.hy2.sh/ | bash -- --remove"], "Failed to run the official Hysteria uninstallation script.") + + print("\nStep 4: Cleaning MongoDB database...") + drop_mongodb_database() + + print("\nStep 5: Removing related components (WARP)...") cli_path = "/etc/hysteria/core/cli.py" if os.path.exists(cli_path): - run_command([sys.executable, cli_path, "uninstall-warp"], "Error during WARP removal.") + run_command([sys.executable, cli_path, "uninstall-warp"], "Failed during WARP removal.") else: print("Skipping WARP removal (CLI path not found)") - print("Removing Hysteria folder...") - run_command(["rm", "-rf", "/etc/hysteria"], "Error removing the Hysteria folder.") + print("\nStep 6: Removing all Hysteria panel files...") + run_command(["rm", "-rf", "/etc/hysteria"], "Failed to remove the /etc/hysteria folder.") - print("Deleting hysteria user...") - run_command(["userdel", "-r", "hysteria"], "Error deleting the hysteria user.") + print("\nStep 7: Deleting the 'hysteria' user...") + run_command(["userdel", "-r", "hysteria"], "Failed to delete the 'hysteria' user.") - print("Stop/Disabling Hysteria Services...") - for service in SERVICES + ["hysteria-server@*.service"]: - print(f"Stopping and disabling {service}...") - run_command(["systemctl", "stop", service], f"Error stopping {service}.") - run_command(["systemctl", "disable", service], f"Error disabling {service}.") - - print("Removing systemd service files...") - for service in SERVICES + ["hysteria-server@*.service"]: - print(f"Removing service file: {service}") - run_command(["rm", "-f", f"/etc/systemd/system/{service}", f"/etc/systemd/system/multi-user.target.wants/{service}"], f"Error removing service files for {service}.") - - print("Reloading systemd daemon...") - run_command(["systemctl", "daemon-reload"], "Error reloading systemd daemon.") - - print("Removing cron jobs...") + print("\nStep 8: Removing cron jobs...") try: - crontab_list = subprocess.run(["crontab", "-l"], capture_output=True, text=True, check=False) - if "hysteria" in crontab_list.stdout: - new_crontab = "\n".join(line for line in crontab_list.stdout.splitlines() if "hysteria" not in line) - process = subprocess.run(["crontab", "-"], input=new_crontab.encode(), check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except FileNotFoundError: - print("Warning: crontab command not found.") - except subprocess.CalledProcessError: - print("Warning: Could not access crontab.") + crontab_list_proc = subprocess.run(["crontab", "-l"], capture_output=True, text=True, check=False) + if "hysteria" in crontab_list_proc.stdout: + new_crontab = "\n".join(line for line in crontab_list_proc.stdout.splitlines() if "hysteria" not in line) + subprocess.run(["crontab", "-"], input=new_crontab.encode(), check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + print("Hysteria cron jobs removed.") + except (FileNotFoundError, subprocess.CalledProcessError): + print("Warning: Could not access or modify crontab.") - print("Removing alias 'hys2' from .bashrc...") + print("\nStep 9: Removing 'hys2' alias from .bashrc...") bashrc_path = os.path.expanduser("~/.bashrc") if os.path.exists(bashrc_path): try: - with open(bashrc_path, 'r') as f: - lines = f.readlines() - with open(bashrc_path, 'w') as f: - for line in lines: - if 'alias hys2=' not in line: - f.write(line) + with open(bashrc_path, 'r') as f_in: + lines = [line for line in f_in if 'alias hys2=' not in line] + with open(bashrc_path, 'w') as f_out: + f_out.writelines(lines) + print("Alias 'hys2' removed from .bashrc.") except IOError: print(f"Warning: Could not access or modify {bashrc_path}.") - else: - print(f"Warning: {bashrc_path} not found.") - - print("Hysteria2 uninstalled!") - print("Rebooting server...") - run_command(["reboot"], "Error initiating reboot.") - + + print("\nUninstallation of Hysteria2 panel is complete.") + print("It is recommended to reboot the server to ensure all changes take effect.") + if __name__ == "__main__": + if os.geteuid() != 0: + print("Error: This script must be run as root.") + sys.exit(1) uninstall_hysteria() \ No newline at end of file diff --git a/core/scripts/hysteria2/wrapper_uri.py b/core/scripts/hysteria2/wrapper_uri.py index 57a0d4a..8d1b71e 100644 --- a/core/scripts/hysteria2/wrapper_uri.py +++ b/core/scripts/hysteria2/wrapper_uri.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 +import init_paths import os import sys import json import argparse from functools import lru_cache from typing import Dict, List, Any - -from init_paths import * +from db.database import db from paths import * @lru_cache(maxsize=None) @@ -42,10 +42,12 @@ def generate_uri(username: str, auth_password: str, ip: str, port: str, def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: config = load_json_file(CONFIG_FILE) - all_users = load_json_file(USERS_FILE) - - if not config or not all_users: - print("Error: Could not load Hysteria2 configuration or user files.", file=sys.stderr) + if not config: + print("Error: Could not load Hysteria2 configuration file.", file=sys.stderr) + sys.exit(1) + + if db is None: + print("Error: Database connection failed.", file=sys.stderr) sys.exit(1) nodes = load_json_file(NODES_JSON_PATH) or [] @@ -73,7 +75,7 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: results = [] for username in target_usernames: - user_data = all_users.get(username) + user_data = db.get_user(username) if not user_data or "password" not in user_data: results.append({"username": username, "error": "User not found or password not set"}) continue @@ -104,17 +106,20 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: def main(): parser = argparse.ArgumentParser(description="Efficiently generate Hysteria2 URIs for multiple users.") parser.add_argument('usernames', nargs='*', help="A list of usernames to process.") - parser.add_argument('--all', action='store_true', help="Process all users from users.json.") + parser.add_argument('--all', action='store_true', help="Process all users from the database.") args = parser.parse_args() target_usernames = args.usernames if args.all: - all_users = load_json_file(USERS_FILE) - if all_users: - target_usernames = list(all_users.keys()) - else: - print("Error: Could not load users.json to process all users.", file=sys.stderr) + if db is None: + print("Error: Database connection failed.", file=sys.stderr) + sys.exit(1) + try: + all_users_docs = db.get_all_users() + target_usernames = [user['_id'] for user in all_users_docs] + except Exception as e: + print(f"Error retrieving all users from database: {e}", file=sys.stderr) sys.exit(1) if not target_usernames: diff --git a/core/scripts/normalsub/normalsub.py b/core/scripts/normalsub/normalsub.py index fb407f5..425bb87 100644 --- a/core/scripts/normalsub/normalsub.py +++ b/core/scripts/normalsub/normalsub.py @@ -5,7 +5,8 @@ import re import time import shlex import base64 -from typing import Dict, List, Optional, Tuple, Any, Union +import sys +from typing import Dict, List, Optional, Tuple, Any from dataclasses import dataclass, field from io import BytesIO @@ -16,6 +17,9 @@ from dotenv import load_dotenv import qrcode from jinja2 import Environment, FileSystemLoader +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +from db.database import db + load_dotenv() @@ -28,7 +32,6 @@ class AppConfig: sni_file: str singbox_template_path: str hysteria_cli_path: str - users_json_path: str nodes_json_path: str extra_config_path: str rate_limit: int @@ -172,9 +175,8 @@ class Utils: class HysteriaCLI: - def __init__(self, cli_path: str, users_json_path: str): + def __init__(self, cli_path: str): self.cli_path = cli_path - self.users_json_path = users_json_path def _run_command(self, args: List[str]) -> str: try: @@ -192,61 +194,29 @@ class HysteriaCLI: print(f"Hysteria CLI error: {e}") raise - def get_user_details_from_json(self, username: str) -> Optional[Dict[str, Any]]: - try: - with open(self.users_json_path, 'r') as f: - users_data = json.load(f) - return users_data.get(username) - except (FileNotFoundError, json.JSONDecodeError) as e: - print(f"Error reading user details from {self.users_json_path}: {e}") - return None - except Exception as e: - print(f"An unexpected error occurred while reading users file: {e}") - return None - def get_username_by_password(self, password_token: str) -> Optional[str]: - try: - with open(self.users_json_path, 'r') as f: - users_data = json.load(f) - for username, details in users_data.items(): - if details.get('password') == password_token: - return username - return None - except FileNotFoundError: - print(f"Error: Users file not found at {self.users_json_path}") - return None - except json.JSONDecodeError: - print(f"Error: Could not decode JSON from {self.users_json_path}") - return None - except Exception as e: - print(f"An unexpected error occurred while reading users file: {e}") + if not db: return None + user_doc = db.collection.find_one({"password": password_token}, {"_id": 1}) + return user_doc['_id'] if user_doc else None def get_user_info(self, username: str) -> Optional[UserInfo]: - raw_info_str = self._run_command(['get-user', '-u', username]) - if raw_info_str is None: + if not db: return None - - user_details = self.get_user_details_from_json(username) - if not user_details or 'password' not in user_details: - print(f"Warning: Password for user '{username}' could not be fetched from {self.users_json_path}. Cannot create UserInfo.") - return None - - try: - raw_info = json.loads(raw_info_str) - return UserInfo( - username=username, - password=user_details['password'], - upload_bytes=raw_info.get('upload_bytes', 0), - download_bytes=raw_info.get('download_bytes', 0), - max_download_bytes=raw_info.get('max_download_bytes', 0), - account_creation_date=raw_info.get('account_creation_date', ''), - expiration_days=raw_info.get('expiration_days', 0), - blocked=user_details.get('blocked', False) - ) - except json.JSONDecodeError as e: - print(f"JSONDecodeError: {e}, Raw output: {raw_info_str}") + user_doc = db.get_user(username) + if not user_doc: return None + + return UserInfo( + username=user_doc.get('_id'), + password=user_doc.get('password'), + upload_bytes=user_doc.get('upload_bytes', 0), + download_bytes=user_doc.get('download_bytes', 0), + max_download_bytes=user_doc.get('max_download_bytes', 0), + account_creation_date=user_doc.get('account_creation_date', ''), + expiration_days=user_doc.get('expiration_days', 0), + blocked=user_doc.get('blocked', False) + ) def get_all_uris(self, username: str) -> List[str]: output = self._run_command(['show-user-uri', '-u', username, '-a']) @@ -446,7 +416,7 @@ class HysteriaServer: def __init__(self): self.config = self._load_config() self.rate_limiter = RateLimiter(self.config.rate_limit, self.config.rate_limit_window) - self.hysteria_cli = HysteriaCLI(self.config.hysteria_cli_path, self.config.users_json_path) + self.hysteria_cli = HysteriaCLI(self.config.hysteria_cli_path) self.singbox_generator = SingboxConfigGenerator(self.hysteria_cli, self.config.sni) self.singbox_generator.set_template_path(self.config.singbox_template_path) self.subscription_manager = SubscriptionManager(self.hysteria_cli, self.config) @@ -478,7 +448,6 @@ class HysteriaServer: sni_file = '/etc/hysteria/.configs.env' singbox_template_path = '/etc/hysteria/core/scripts/normalsub/singbox.json' hysteria_cli_path = '/etc/hysteria/core/cli.py' - users_json_path = os.getenv('HYSTERIA_USERS_JSON_PATH', '/etc/hysteria/users.json') nodes_json_path = '/etc/hysteria/nodes.json' extra_config_path = '/etc/hysteria/extra.json' rate_limit = 100 @@ -492,7 +461,6 @@ class HysteriaServer: sni_file=sni_file, singbox_template_path=singbox_template_path, hysteria_cli_path=hysteria_cli_path, - users_json_path=users_json_path, nodes_json_path=nodes_json_path, extra_config_path=extra_config_path, rate_limit=rate_limit, rate_limit_window=rate_limit_window, @@ -681,4 +649,4 @@ class HysteriaServer: if __name__ == '__main__': server = HysteriaServer() - server.run() + server.run() \ No newline at end of file diff --git a/core/scripts/telegrambot/utils/adduser.py b/core/scripts/telegrambot/utils/adduser.py index e3c793c..abacce7 100644 --- a/core/scripts/telegrambot/utils/adduser.py +++ b/core/scripts/telegrambot/utils/adduser.py @@ -48,7 +48,7 @@ def process_add_user_step1(message): try: users_data = json.loads(result) - existing_users = {user_key.lower() for user_key in users_data.keys()} + existing_users = {user['username'].lower() for user in users_data} if username.lower() in existing_users: bot.reply_to(message, f"Username '{escape_markdown(username)}' already exists. Please choose a different username:", reply_markup=create_cancel_markup()) bot.register_next_step_handler(message, process_add_user_step1) @@ -105,7 +105,7 @@ def process_add_user_step3(message, username, traffic_limit): bot.send_chat_action(message.chat.id, 'typing') - uri_info_command = f"python3 {CLI_PATH} show-user-uri -u \"{username}\" -ip 4 -n" + uri_info_command = f"python3 {CLI_PATH} show-user-uri -u \"{username}\" -ip 4 -n -s" uri_info_output = run_cli_command(uri_info_command) direct_uri = None diff --git a/core/scripts/telegrambot/utils/edituser.py b/core/scripts/telegrambot/utils/edituser.py index bc9eed4..584eafb 100644 --- a/core/scripts/telegrambot/utils/edituser.py +++ b/core/scripts/telegrambot/utils/edituser.py @@ -24,25 +24,25 @@ def show_user(message): bot.register_next_step_handler(msg, process_show_user) def process_show_user(message): - username = message.text.strip().lower() + username_input = message.text.strip().lower() bot.send_chat_action(message.chat.id, 'typing') command = f"python3 {CLI_PATH} list-users" result = run_cli_command(command) try: - users = json.loads(result) - existing_users = {user.lower(): user for user in users.keys()} + users_list = json.loads(result) + existing_users = {user['username'].lower(): user['username'] for user in users_list} - if username not in existing_users: + if username_input not in existing_users: bot.reply_to(message, f"Username '{escape_markdown(message.text.strip())}' does not exist. Please enter a valid username.") return - actual_username = existing_users[username] - except json.JSONDecodeError: - bot.reply_to(message, "Error retrieving user list. Please try again later.") + actual_username = existing_users[username_input] + except (json.JSONDecodeError, KeyError): + bot.reply_to(message, "Error retrieving or parsing user list. Please try again later.") return - command = f"python3 {CLI_PATH} get-user -u {actual_username}" + command = f"python3 {CLI_PATH} get-user -u \"{actual_username}\"" user_result = run_cli_command(command) try: @@ -53,7 +53,7 @@ def process_show_user(message): status = user_details.get('status', 'Unknown') if upload_bytes is None or download_bytes is None: - traffic_message = "**Traffic Data:**\nUser not active or no traffic data available." + traffic_message = "*Traffic Data:*\nUser not active or no traffic data available." else: upload_gb = upload_bytes / (1024 ** 3) download_gb = download_bytes / (1024 ** 3) @@ -66,48 +66,45 @@ def process_show_user(message): f"๐ŸŒ Status: {status}" ) except json.JSONDecodeError: - bot.reply_to(message, "Failed to parse JSON data. The command output may be malformed.") + bot.reply_to(message, "Failed to parse user details. The command output may be malformed.") return display_username = escape_markdown(actual_username) formatted_details = ( f"\n๐Ÿ†” Name: {display_username}\n" - f"๐Ÿ“Š Traffic Limit: {user_details['max_download_bytes'] / (1024 ** 3):.2f} GB\n" - f"๐Ÿ“… Days: {user_details['expiration_days']}\n" - f"โณ Creation: {user_details['account_creation_date']}\n" - f"๐Ÿ’ก Blocked: {user_details['blocked']}\n\n" + f"๐Ÿ“Š Traffic Limit: {user_details.get('max_download_bytes', 0) / (1024 ** 3):.2f} GB\n" + f"๐Ÿ“… Days: {user_details.get('expiration_days', 'N/A')}\n" + f"โณ Creation: {user_details.get('account_creation_date', 'N/A')}\n" + f"๐Ÿ’ก Blocked: {user_details.get('blocked', 'N/A')}\n\n" f"{traffic_message}" ) - combined_command = f"python3 {CLI_PATH} show-user-uri -u {actual_username} -ip 4 -s -n" + combined_command = f"python3 {CLI_PATH} show-user-uri -u \"{actual_username}\" -ip 4 -s -n" combined_result = run_cli_command(combined_command) - if "Error" in combined_result or "Invalid" in combined_result: - bot.reply_to(message, combined_result) - return - - result_lines = combined_result.strip().split('\n') - uri_v4 = "" - normal_sub_sublink = "" + normal_sub_link = "" - for line in result_lines: - line = line.strip() - if line.startswith("hy2://"): - uri_v4 = line - elif line.startswith("Normal-SUB Sublink:"): - normal_sub_sublink = result_lines[result_lines.index(line) + 1].strip() + lines = combined_result.strip().split('\n') + for i, line in enumerate(lines): + if line.strip() == "IPv4:": + if i + 1 < len(lines) and lines[i+1].strip().startswith("hy2://"): + uri_v4 = lines[i+1].strip() + elif line.strip() == "Normal-SUB Sublink:": + if i + 1 < len(lines) and (lines[i+1].strip().startswith("http://") or lines[i+1].strip().startswith("https://")): + normal_sub_link = lines[i+1].strip() - if not uri_v4: - bot.reply_to(message, "No valid URI found.") + qr_link = normal_sub_link if normal_sub_link else uri_v4 + if not qr_link: + bot.reply_to(message, "No valid URI or Subscription link found for this user.") return - - qr_v4 = qrcode.make(uri_v4) - bio_v4 = io.BytesIO() - qr_v4.save(bio_v4, 'PNG') - bio_v4.seek(0) - + + qr_img = qrcode.make(qr_link) + bio = io.BytesIO() + qr_img.save(bio, 'PNG') + bio.seek(0) + markup = types.InlineKeyboardMarkup(row_width=3) markup.add(types.InlineKeyboardButton("Reset User", callback_data=f"reset_user:{actual_username}"), types.InlineKeyboardButton("IPv6-URI", callback_data=f"ipv6_uri:{actual_username}")) @@ -118,13 +115,15 @@ def process_show_user(message): markup.add(types.InlineKeyboardButton("Renew Creation Date", callback_data=f"renew_creation:{actual_username}"), types.InlineKeyboardButton("Block User", callback_data=f"block_user:{actual_username}")) - caption = f"{formatted_details}\n\n**IPv4 URI:**\n\n`{uri_v4}`" - if normal_sub_sublink: - caption += f"\n\n**Normal SUB:**\n{normal_sub_sublink}" + caption = formatted_details + if uri_v4: + caption += f"\n\n*IPv4 URI:*\n`{uri_v4}`" + if normal_sub_link: + caption += f"\n\n*Normal SUB:*\n`{normal_sub_link}`" bot.send_photo( message.chat.id, - bio_v4, + bio, caption=caption, reply_markup=markup, parse_mode="Markdown" @@ -145,11 +144,11 @@ def handle_edit_callback(call): msg = bot.send_message(call.message.chat.id, f"Enter new expiration days for {display_username}:") bot.register_next_step_handler(msg, process_edit_expiration, username) elif action == 'renew_password': - command = f"python3 {CLI_PATH} edit-user -u {username} -rp" + command = f"python3 {CLI_PATH} edit-user -u \"{username}\" -rp" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'renew_creation': - command = f"python3 {CLI_PATH} edit-user -u {username} -rc" + command = f"python3 {CLI_PATH} edit-user -u \"{username}\" -rc" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'block_user': @@ -158,11 +157,11 @@ def handle_edit_callback(call): types.InlineKeyboardButton("False", callback_data=f"confirm_block:{username}:false")) bot.send_message(call.message.chat.id, f"Set block status for {display_username}:", reply_markup=markup) elif action == 'reset_user': - command = f"python3 {CLI_PATH} reset-user -u {username}" + command = f"python3 {CLI_PATH} reset-user -u \"{username}\"" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) elif action == 'ipv6_uri': - command = f"python3 {CLI_PATH} show-user-uri -u {username} -ip 6" + command = f"python3 {CLI_PATH} show-user-uri -u \"{username}\" -ip 6" result = run_cli_command(command) if "Error" in result or "Invalid" in result: bot.send_message(call.message.chat.id, result) @@ -184,20 +183,21 @@ def handle_edit_callback(call): @bot.callback_query_handler(func=lambda call: call.data.startswith('confirm_block:')) def handle_block_confirmation(call): _, username, block_status = call.data.split(':') - command = f"python3 {CLI_PATH} edit-user -u {username} {'-b' if block_status == 'true' else ''}" + flag = '-b' if block_status == 'true' else '--unblocked' + command = f"python3 {CLI_PATH} edit-user -u \"{username}\" {flag}" result = run_cli_command(command) bot.send_message(call.message.chat.id, result) def process_edit_username(message, username): new_username = message.text.strip() - command = f"python3 {CLI_PATH} edit-user -u {username} -nu {new_username}" + command = f"python3 {CLI_PATH} edit-user -u \"{username}\" -nu \"{new_username}\"" result = run_cli_command(command) bot.reply_to(message, result) def process_edit_traffic(message, username): try: new_traffic_limit = int(message.text.strip()) - command = f"python3 {CLI_PATH} edit-user -u {username} -nt {new_traffic_limit}" + command = f"python3 {CLI_PATH} edit-user -u \"{username}\" -nt {new_traffic_limit}" result = run_cli_command(command) bot.reply_to(message, result) except ValueError: @@ -206,8 +206,8 @@ def process_edit_traffic(message, username): def process_edit_expiration(message, username): try: new_expiration_days = int(message.text.strip()) - command = f"python3 {CLI_PATH} edit-user -u {username} -ne {new_expiration_days}" + command = f"python3 {CLI_PATH} edit-user -u \"{username}\" -ne {new_expiration_days}" result = run_cli_command(command) bot.reply_to(message, result) except ValueError: - bot.reply_to(message, "Invalid expiration days. Please enter a number.") + bot.reply_to(message, "Invalid expiration days. Please enter a number.") \ No newline at end of file diff --git a/core/scripts/telegrambot/utils/search.py b/core/scripts/telegrambot/utils/search.py index 684c87d..0e729ab 100644 --- a/core/scripts/telegrambot/utils/search.py +++ b/core/scripts/telegrambot/utils/search.py @@ -15,38 +15,42 @@ def handle_inline_query(query): results = [] if query_text == "block": - for username, details in users.items(): - if details.get('blocked', False): + for user in users: + if user.get('blocked', False): + username = user['username'] title = f"{username} (Blocked)" - description = f"Traffic Limit: {details['max_download_bytes'] / (1024 ** 3):.2f} GB, Expiration Days: {details['expiration_days']}" + description = f"Traffic Limit: {user.get('max_download_bytes', 0) / (1024 ** 3):.2f} GB, Expiration Days: {user.get('expiration_days', 'N/A')}" + message_content = ( + f"Name: {username}\n" + f"Traffic limit: {user.get('max_download_bytes', 0) / (1024 ** 3):.2f} GB\n" + f"Days: {user.get('expiration_days', 'N/A')}\n" + f"Account Creation: {user.get('account_creation_date', 'N/A')}\n" + f"Blocked: {user.get('blocked', False)}" + ) results.append(types.InlineQueryResultArticle( id=username, title=title, description=description, - input_message_content=types.InputTextMessageContent( - message_text=f"Name: {username}\n" - f"Traffic limit: {details['max_download_bytes'] / (1024 ** 3):.2f} GB\n" - f"Days: {details['expiration_days']}\n" - f"Account Creation: {details['account_creation_date']}\n" - f"Blocked: {details['blocked']}" - ) + input_message_content=types.InputTextMessageContent(message_text=message_content) )) else: - for username, details in users.items(): + for user in users: + username = user['username'] if query_text in username.lower(): title = f"{username}" - description = f"Traffic Limit: {details['max_download_bytes'] / (1024 ** 3):.2f} GB, Expiration Days: {details['expiration_days']}" + description = f"Traffic Limit: {user.get('max_download_bytes', 0) / (1024 ** 3):.2f} GB, Expiration Days: {user.get('expiration_days', 'N/A')}" + message_content = ( + f"Name: {username}\n" + f"Traffic limit: {user.get('max_download_bytes', 0) / (1024 ** 3):.2f} GB\n" + f"Days: {user.get('expiration_days', 'N/A')}\n" + f"Account Creation: {user.get('account_creation_date', 'N/A')}\n" + f"Blocked: {user.get('blocked', False)}" + ) results.append(types.InlineQueryResultArticle( id=username, title=title, description=description, - input_message_content=types.InputTextMessageContent( - message_text=f"Name: {username}\n" - f"Traffic limit: {details['max_download_bytes'] / (1024 ** 3):.2f} GB\n" - f"Days: {details['expiration_days']}\n" - f"Account Creation: {details['account_creation_date']}\n" - f"Blocked: {details['blocked']}" - ) + input_message_content=types.InputTextMessageContent(message_text=message_content) )) - bot.answer_inline_query(query.id, results, cache_time=0) + bot.answer_inline_query(query.id, results, cache_time=0) \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/config/hysteria.py b/core/scripts/webpanel/routers/api/v1/config/hysteria.py index 7c676df..6044547 100644 --- a/core/scripts/webpanel/routers/api/v1/config/hysteria.py +++ b/core/scripts/webpanel/routers/api/v1/config/hysteria.py @@ -7,6 +7,7 @@ import zipfile import tempfile # from ..schema.config.hysteria import InstallInputBody import os +from pathlib import Path import cli_api router = APIRouter() @@ -113,12 +114,9 @@ async def set_sni_api(sni: str): @router.get('/backup', response_class=FileResponse, summary='Backup Hysteria2 configuration') async def backup_api(): - """ - Backups the Hysteria2 configuration and sends the backup ZIP file. - """ try: cli_api.backup_hysteria2() - backup_dir = "/opt/hysbackup/" # TODO: get this path from .env + backup_dir = "/opt/hysbackup/" if not os.path.isdir(backup_dir): raise HTTPException(status_code=500, detail="Backup directory does not exist.") @@ -129,13 +127,8 @@ async def backup_api(): if latest_backup_file: backup_file_path = os.path.join(backup_dir, latest_backup_file) - - if not backup_file_path.startswith(backup_dir): - raise HTTPException(status_code=400, detail="Invalid backup file path.") - if not os.path.exists(backup_file_path): raise HTTPException(status_code=404, detail="Backup file not found.") - return FileResponse(path=backup_file_path, filename=os.path.basename(backup_file_path), media_type="application/zip") else: raise HTTPException(status_code=500, detail="No backup file found after backup process.") @@ -146,46 +139,43 @@ async def backup_api(): @router.post('/restore', response_model=DetailResponse, summary='Restore Hysteria2 Configuration') async def restore_api(file: UploadFile = File(...)): + temp_path = None try: - - with tempfile.NamedTemporaryFile(delete=False) as temp_file: + with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_file: shutil.copyfileobj(file.file, temp_file) temp_path = temp_file.name if not zipfile.is_zipfile(temp_path): - os.unlink(temp_path) raise HTTPException(status_code=400, detail="Invalid file type. Must be a ZIP file.") - required_files = {"ca.key", "ca.crt", "users.json", "config.json", ".configs.env"} with zipfile.ZipFile(temp_path, 'r') as zip_ref: - zip_contents = set(zip_ref.namelist()) - missing_files = required_files - zip_contents + namelist = zip_ref.namelist() + required_flat_files = {"ca.key", "ca.crt", "config.json", ".configs.env"} + missing_files = required_flat_files - set(namelist) if missing_files: - os.unlink(temp_path) raise HTTPException( status_code=400, - detail=f"Backup file is missing required files: {', '.join(missing_files)}" + detail=f"Backup is missing required configuration files: {', '.join(missing_files)}" ) - dst_dir_path = '/opt/hysbackup/' # TODO: get this path from .env - os.makedirs(dst_dir_path, exist_ok=True) - - dst_path = os.path.join(dst_dir_path, file.filename) # type: ignore - shutil.move(temp_path, dst_path) - cli_api.restore_hysteria2(dst_path) + db_dump_prefix = "blitz_panel/" + if not any(name.startswith(db_dump_prefix) for name in namelist): + raise HTTPException( + status_code=400, + detail=f"Backup file is not a modern backup and is missing the database dump directory: '{db_dump_prefix}'" + ) + + cli_api.restore_hysteria2(temp_path) return DetailResponse(detail='Hysteria2 restored successfully.') except HTTPException as e: raise e except Exception as e: - if 'temp_path' in locals(): - try: - os.unlink(temp_path) - except: - pass raise HTTPException(status_code=500, detail=f'Error: {str(e)}') - + finally: + if temp_path and os.path.exists(temp_path): + os.unlink(temp_path) @router.get('/enable-obfs', response_model=DetailResponse, summary='Enable Hysteria2 obfs') async def enable_obfs(): diff --git a/core/traffic.py b/core/traffic.py index f27244a..2c4d0cf 100644 --- a/core/traffic.py +++ b/core/traffic.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 + import json import os import sys -import time import fcntl -import shutil import datetime -from concurrent.futures import ThreadPoolExecutor from hysteria2_api import Hysteria2Client +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(SCRIPT_DIR, 'scripts')) + +from db.database import db + CONFIG_FILE = '/etc/hysteria/config.json' -USERS_FILE = '/etc/hysteria/users.json' API_BASE_URL = 'http://127.0.0.1:25413' -LOCKFILE = "/tmp/kick.lock" -BACKUP_FILE = f"{USERS_FILE}.bak" -MAX_WORKERS = 8 +LOCKFILE = "/tmp/hysteria_traffic.lock" def acquire_lock(): - """Acquires a lock file to prevent concurrent execution""" try: lock_file = open(LOCKFILE, 'w') fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) @@ -25,92 +24,22 @@ def acquire_lock(): except IOError: sys.exit(1) -def traffic_status(no_gui=False): - """Updates and retrieves traffic statistics for all users.""" - green = '\033[0;32m' - cyan = '\033[0;36m' - NC = '\033[0m' - +def get_secret(): try: - with open(CONFIG_FILE, 'r') as config_file: - config = json.load(config_file) - secret = config.get('trafficStats', {}).get('secret') - except (json.JSONDecodeError, FileNotFoundError) as e: - if not no_gui: - print(f"Error: Failed to read secret from {CONFIG_FILE}. Details: {e}") + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + return config.get('trafficStats', {}).get('secret') + except (json.JSONDecodeError, FileNotFoundError): return None - if not secret: - if not no_gui: - print("Error: Secret not found in config.json") - return None - - client = Hysteria2Client(base_url=API_BASE_URL, secret=secret) - - try: - traffic_stats = client.get_traffic_stats(clear=True) - online_status = client.get_online_clients() - except Exception as e: - if not no_gui: - print(f"Error communicating with Hysteria2 API: {e}") - return None - - users_data = {} - if os.path.exists(USERS_FILE): - try: - with open(USERS_FILE, 'r') as users_file: - users_data = json.load(users_file) - except json.JSONDecodeError: - if not no_gui: - print("Error: Failed to parse existing users data JSON file.") - return None - - for user in users_data: - users_data[user]["status"] = "Offline" - - for user_id, status in online_status.items(): - if user_id in users_data: - users_data[user_id]["status"] = "Online" if status.is_online else "Offline" - else: - users_data[user_id] = { - "upload_bytes": 0, "download_bytes": 0, - "status": "Online" if status.is_online else "Offline" - } - - for user_id, stats in traffic_stats.items(): - if user_id in users_data: - users_data[user_id]["upload_bytes"] = users_data[user_id].get("upload_bytes", 0) + stats.upload_bytes - users_data[user_id]["download_bytes"] = users_data[user_id].get("download_bytes", 0) + stats.download_bytes - else: - online = user_id in online_status and online_status[user_id].is_online - users_data[user_id] = { - "upload_bytes": stats.upload_bytes, "download_bytes": stats.download_bytes, - "status": "Online" if online else "Offline" - } - - today_date = datetime.datetime.now().strftime("%Y-%m-%d") - for username, user_data in users_data.items(): - is_on_hold = not user_data.get("account_creation_date") - - if is_on_hold: - is_online = user_data.get("status") == "Online" - has_traffic = user_data.get("download_bytes", 0) > 0 or user_data.get("upload_bytes", 0) > 0 - - if is_online or has_traffic: - user_data["account_creation_date"] = today_date - else: - user_data["status"] = "On-hold" - - with open(USERS_FILE, 'w') as users_file: - json.dump(users_data, users_file, indent=4) - - if not no_gui: - display_traffic_data(users_data, green, cyan, NC) - - return users_data +def format_bytes(bytes_val): + if bytes_val < 1024: return f"{bytes_val}B" + elif bytes_val < 1048576: return f"{bytes_val / 1024:.2f}KB" + elif bytes_val < 1073741824: return f"{bytes_val / 1048576:.2f}MB" + elif bytes_val < 1099511627776: return f"{bytes_val / 1073741824:.2f}GB" + else: return f"{bytes_val / 1099511627776:.2f}TB" def display_traffic_data(data, green, cyan, NC): - """Displays traffic data in a formatted table""" if not data: print("No traffic data to display.") return @@ -123,113 +52,151 @@ def display_traffic_data(data, green, cyan, NC): for user, entry in data.items(): upload_bytes = entry.get("upload_bytes", 0) download_bytes = entry.get("download_bytes", 0) - status = entry.get("status", "Offline") - + status = entry.get("status", "On-hold") formatted_tx = format_bytes(upload_bytes) formatted_rx = format_bytes(download_bytes) - print(f"{user:<15} {green}{formatted_tx:<15}{NC} {cyan}{formatted_rx:<15}{NC} {status:<10}") print("-------------------------------------------------") -def format_bytes(bytes_val): - """Format bytes as human-readable string""" - if bytes_val < 1024: return f"{bytes_val}B" - elif bytes_val < 1048576: return f"{bytes_val / 1024:.2f}KB" - elif bytes_val < 1073741824: return f"{bytes_val / 1048576:.2f}MB" - elif bytes_val < 1099511627776: return f"{bytes_val / 1073741824:.2f}GB" - else: return f"{bytes_val / 1099511627776:.2f}TB" +def traffic_status(no_gui=False): + green, cyan, NC = '\033[0;32m', '\033[0;36m', '\033[0m' + + if db is None: + if not no_gui: print("Error: Database connection failed.") + return None -def kick_users(usernames, secret): - """Kicks specified users from the server""" + secret = get_secret() + if not secret: + if not no_gui: print(f"Error: Secret not found or failed to read {CONFIG_FILE}.") + return None + + client = Hysteria2Client(base_url=API_BASE_URL, secret=secret) + try: + traffic_stats = client.get_traffic_stats(clear=True) + online_status = client.get_online_clients() + except Exception as e: + if not no_gui: print(f"Error communicating with Hysteria2 API: {e}") + return None + + try: + all_users = db.get_all_users() + initial_users_data = {user['_id']: user for user in all_users} + except Exception as e: + if not no_gui: print(f"Error fetching users from database: {e}") + return None + + today_date = datetime.datetime.now().strftime("%Y-%m-%d") + users_to_update = {} + + for username, user_data in initial_users_data.items(): + updates = {} + is_online = username in online_status and online_status[username].is_online + + if username in traffic_stats: + new_upload = user_data.get('upload_bytes', 0) + traffic_stats[username].upload_bytes + new_download = user_data.get('download_bytes', 0) + traffic_stats[username].download_bytes + if new_upload != user_data.get('upload_bytes'): updates['upload_bytes'] = new_upload + if new_download != user_data.get('download_bytes'): updates['download_bytes'] = new_download + + is_activated = "account_creation_date" in user_data + + if not is_activated: + current_traffic = traffic_stats.get(username) + has_activity = is_online or (current_traffic and (current_traffic.upload_bytes > 0 or current_traffic.download_bytes > 0)) + + if has_activity: + updates["account_creation_date"] = today_date + updates["status"] = "Online" if is_online else "Offline" + else: + if user_data.get("status") != "On-hold": + updates["status"] = "On-hold" + else: + new_status = "Online" if is_online else "Offline" + if user_data.get("status") != new_status: + updates["status"] = new_status + + if updates: + users_to_update[username] = updates + + if users_to_update: + try: + for username, update_data in users_to_update.items(): + db.update_user(username, update_data) + except Exception as e: + if not no_gui: print(f"Error updating database: {e}") + return None + + if not no_gui: + # For display, merge updates into the initial data + for username, updates in users_to_update.items(): + initial_users_data[username].update(updates) + display_traffic_data(initial_users_data, green, cyan, NC) + + return initial_users_data + +def kick_api_call(usernames, secret): try: client = Hysteria2Client(base_url=API_BASE_URL, secret=secret) client.kick_clients(usernames) - return True - except Exception: - return False - -def process_user(username, user_data, users_data): - """Process a single user to check if they should be kicked""" - if user_data.get('blocked', False): return None - - account_creation_date = user_data.get('account_creation_date') - if not account_creation_date: return None - - max_download_bytes = user_data.get('max_download_bytes', 0) - expiration_days = user_data.get('expiration_days', 0) - total_bytes = user_data.get('download_bytes', 0) + user_data.get('upload_bytes', 0) - - should_block = False - try: - if expiration_days > 0: - creation_date = datetime.datetime.strptime(account_creation_date, "%Y-%m-%d") - expiration_date = creation_date + datetime.timedelta(days=expiration_days) - if datetime.datetime.now() >= expiration_date: - should_block = True - - if not should_block and max_download_bytes > 0 and total_bytes >= max_download_bytes: - should_block = True - - if should_block: - users_data[username]['blocked'] = True - return username - except (ValueError, TypeError): - return None - - return None + except Exception as e: + print(f"Failed to kick users via API: {e}", file=sys.stderr) def kick_expired_users(): - """Kicks users who have exceeded their data limits or whose accounts have expired""" - lock_file = acquire_lock() + if db is None: + print("Error: Database connection failed.", file=sys.stderr) + return + + secret = get_secret() + if not secret: + print(f"Error: Secret not found or failed to read {CONFIG_FILE}.", file=sys.stderr) + return - try: - if not os.path.exists(USERS_FILE): return - shutil.copy2(USERS_FILE, BACKUP_FILE) - + all_users = db.get_all_users() + users_to_kick, users_to_block = [], [] + + for user in all_users: + username = user.get('_id') + if not username or user.get('blocked', False) or not user.get('account_creation_date'): + continue + + total_bytes = user.get('download_bytes', 0) + user.get('upload_bytes', 0) + should_block = False try: - with open(CONFIG_FILE, 'r') as f: - config = json.load(f) - secret = config.get('trafficStats', {}).get('secret', '') - if not secret: sys.exit(1) - except Exception: - sys.exit(1) + if user.get('expiration_days', 0) > 0: + creation_date = datetime.datetime.strptime(user['account_creation_date'], "%Y-%m-%d") + if datetime.datetime.now() >= creation_date + datetime.timedelta(days=user['expiration_days']): + should_block = True - with open(USERS_FILE, 'r') as f: - users_data = json.load(f) - - users_to_kick = [] - with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: - futures = [executor.submit(process_user, u, d, users_data) for u, d in users_data.items()] - for future in futures: - result = future.result() - if result: - users_to_kick.append(result) - - if users_to_kick: - with open(USERS_FILE, 'w') as f: - json.dump(users_data, f, indent=4) - - for i in range(0, len(users_to_kick), 50): - batch = users_to_kick[i:i+50] - kick_users(batch, secret) - - except Exception: - if os.path.exists(BACKUP_FILE): - shutil.copy2(BACKUP_FILE, USERS_FILE) - sys.exit(1) - finally: - fcntl.flock(lock_file, fcntl.LOCK_UN) - lock_file.close() + if not should_block and user.get('max_download_bytes', 0) > 0 and total_bytes >= user['max_download_bytes']: + should_block = True + + if should_block: + users_to_kick.append(username) + users_to_block.append(username) + except (ValueError, TypeError): + continue + + if users_to_block: + for username in users_to_block: + db.update_user(username, {'blocked': True}) + + if users_to_kick: + for i in range(0, len(users_to_kick), 50): + kick_api_call(users_to_kick[i:i+50], secret) if __name__ == "__main__": - if len(sys.argv) > 1: - if sys.argv[1] == "kick": - kick_expired_users() - elif sys.argv[1] == "--no-gui": - traffic_status(no_gui=True) - kick_expired_users() + lock_file = acquire_lock() + try: + if len(sys.argv) > 1: + if sys.argv[1] == "kick": + kick_expired_users() + elif sys.argv[1] == "--no-gui": + traffic_status(no_gui=True) + kick_expired_users() + else: + print(f"Usage: python {sys.argv[0]} [kick|--no-gui]") else: - print(f"Unknown argument: {sys.argv[1]}") - print("Usage: python traffic.py [kick|--no-gui]") - else: - traffic_status(no_gui=False) \ No newline at end of file + traffic_status(no_gui=False) + finally: + fcntl.flock(lock_file, fcntl.LOCK_UN) + lock_file.close() \ No newline at end of file diff --git a/install.sh b/install.sh index 3ecbe43..e48f7f3 100644 --- a/install.sh +++ b/install.sh @@ -75,14 +75,57 @@ check_os_version() { fi } +install_mongodb() { + log_info "Installing MongoDB..." + + if command -v mongod &> /dev/null; then + log_success "MongoDB is already installed" + return 0 + fi + + local os_name os_version + os_name=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"') + os_version=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"') + + curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + + if [[ "$os_name" == "ubuntu" ]]; then + if [[ "$os_version" == "24.04" ]]; then + echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list > /dev/null + elif [[ "$os_version" == "22.04" ]]; then + echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list > /dev/null + else + echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list > /dev/null + fi + elif [[ "$os_name" == "debian" && "$os_version" == "12" ]]; then + echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list > /dev/null + else + log_error "Unsupported OS for MongoDB installation: $os_name $os_version" + exit 1 + fi + + apt update -qq + apt install -y -qq mongodb-org + + systemctl enable mongod + systemctl start mongod + + if systemctl is-active --quiet mongod; then + log_success "MongoDB installed and started successfully" + else + log_error "MongoDB installation failed or service not running" + exit 1 + fi +} + install_packages() { - local REQUIRED_PACKAGES=("jq" "curl" "pwgen" "python3" "python3-pip" "python3-venv" "git" "bc" "zip" "cron" "lsof" "golang-go") + local REQUIRED_PACKAGES=("jq" "curl" "pwgen" "python3" "python3-pip" "python3-venv" "git" "bc" "zip" "cron" "lsof" "golang-go" "gnupg" "lsb-release") local MISSING_PACKAGES=() log_info "Checking required packages..." for package in "${REQUIRED_PACKAGES[@]}"; do - if ! command -v "$package" &> /dev/null; then + if ! command -v "$package" &> /dev/null && ! dpkg -l | grep -q "^ii.*$package "; then MISSING_PACKAGES+=("$package") else log_success "Package $package is already installed" @@ -106,6 +149,8 @@ install_packages() { else log_success "All required packages are already installed." fi + + install_mongodb } clone_repository() { diff --git a/requirements.txt b/requirements.txt index 099cf00..be4b892 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ yarl==1.20.1 hysteria2-api==0.1.3 schedule==1.2.2 aiofiles==24.1.0 +pymongo==4.14.1 # webpanel annotated-types==0.7.0 diff --git a/upgrade.sh b/upgrade.sh index 9e08d67..7d0e9b9 100644 --- a/upgrade.sh +++ b/upgrade.sh @@ -11,6 +11,7 @@ REPO_URL="https://github.com/ReturnFI/Blitz" REPO_BRANCH="main" GEOSITE_URL="https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geosite.dat" GEOIP_URL="https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geoip.dat" +MIGRATE_SCRIPT_PATH="$HYSTERIA_INSTALL_DIR/core/scripts/db/migrate_users.py" # ========== Color Setup ========== GREEN=$(tput setaf 2) @@ -24,6 +25,62 @@ success() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [OK] - ${RESET} $1"; warn() { echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] - ${RESET} $1"; } error() { echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] - ${RESET} $1"; } +# ========== Install MongoDB ========== +install_mongodb() { + info "Checking for MongoDB..." + if ! command -v mongod &>/dev/null; then + warn "MongoDB not found. Installing from official repository..." + + local os_name os_version + os_name=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"') + os_version=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"') + + apt-get update + apt-get install -y gnupg curl lsb-release + + curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + + if [[ "$os_name" == "ubuntu" ]]; then + if [[ "$os_version" == "24.04" ]]; then + echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" > /etc/apt/sources.list.d/mongodb-org-8.0.list + elif [[ "$os_version" == "22.04" ]]; then + echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" > /etc/apt/sources.list.d/mongodb-org-8.0.list + else + echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" > /etc/apt/sources.list.d/mongodb-org-8.0.list + fi + elif [[ "$os_name" == "debian" && "$os_version" == "12" ]]; then + echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" > /etc/apt/sources.list.d/mongodb-org-8.0.list + else + error "Unsupported OS for MongoDB installation: $os_name $os_version" + exit 1 + fi + + apt-get update -qq + apt-get install -y mongodb-org + systemctl start mongod + systemctl enable mongod + success "MongoDB installed and started successfully." + else + success "MongoDB is already installed." + fi +} + +# ========== New Function to Migrate Data ========== +migrate_json_to_mongo() { + info "Checking for user data migration..." + if [[ -f "$HYSTERIA_INSTALL_DIR/users.json" ]]; then + info "Found users.json. Proceeding with migration to MongoDB." + if python3 "$MIGRATE_SCRIPT_PATH"; then + success "Data migration completed successfully." + else + error "Data migration script failed. Please check the output above." + exit 1 + fi + else + info "No users.json found. Skipping migration." + fi +} + # ========== Capture Active Services ========== declare -a ACTIVE_SERVICES_BEFORE_UPGRADE=() ALL_SERVICES=( @@ -46,14 +103,15 @@ for SERVICE in "${ALL_SERVICES[@]}"; do fi done +# ========== Install MongoDB Prerequisite ========== +install_mongodb -# ========== New Function to Install Go and Compile Auth Binary ========== +# ========== Install Go and Compile Auth Binary ========== install_go_and_compile_auth() { info "Checking for Go and compiling authentication binary..." if ! command -v go &>/dev/null; then warn "Go is not installed. Attempting to install..." - #apt-get update -qq >/dev/null - apt install golang-go -y + apt-get install -y golang-go success "Go installed successfully." else success "Go is already installed." @@ -158,6 +216,9 @@ pip install --upgrade pip >/dev/null pip install -r requirements.txt >/dev/null success "Python environment ready." +# ========== Data Migration ========== +migrate_json_to_mongo + # ========== Compile Go Binary ========== install_go_and_compile_auth @@ -192,7 +253,6 @@ else done fi - # ========== Final Check ========== if systemctl is-active --quiet hysteria-server.service; then success "๐ŸŽ‰ Upgrade completed successfully!"