Merge pull request #267 from ReturnFI/beta
Migrate User Management to MongoDB
This commit is contained in:
33
changelog
33
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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
core/scripts/db/database.py
Normal file
41
core/scripts/db/database.py
Normal file
@ -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
|
||||
70
core/scripts/db/migrate_users.py
Normal file
70
core/scripts/db/migrate_users.py
Normal file
@ -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()
|
||||
@ -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]} <username> <traffic_limit_GB> <expiration_days> [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__":
|
||||
|
||||
@ -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()
|
||||
@ -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.")
|
||||
|
||||
123
core/scripts/hysteria2/edit_user.py
Normal file
123
core/scripts/hysteria2/edit_user.py
Normal file
@ -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
|
||||
))
|
||||
@ -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"
|
||||
@ -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 <username>")
|
||||
|
||||
@ -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__":
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source /etc/hysteria/core/scripts/path.sh
|
||||
|
||||
|
||||
cat "$USERS_FILE"
|
||||
@ -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]} <username>")
|
||||
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()
|
||||
@ -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]} <username>")
|
||||
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)
|
||||
@ -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())
|
||||
@ -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")
|
||||
|
||||
@ -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()
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
@ -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
|
||||
|
||||
@ -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.")
|
||||
@ -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)
|
||||
@ -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():
|
||||
|
||||
331
core/traffic.py
331
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)
|
||||
traffic_status(no_gui=False)
|
||||
finally:
|
||||
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
||||
lock_file.close()
|
||||
49
install.sh
49
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() {
|
||||
|
||||
@ -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
|
||||
|
||||
68
upgrade.sh
68
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!"
|
||||
|
||||
Reference in New Issue
Block a user