Merge pull request #255 from ReturnFI/auth

Auth & Bulk User Creation
This commit is contained in:
Whispering Wind
2025-08-24 23:22:46 +03:30
committed by GitHub
9 changed files with 365 additions and 41 deletions

View File

@ -1,19 +1,18 @@
# [1.16.0] - 2025-08-19
# [1.17.0] - 2025-08-24
#### ✨ New Features
* 📊 **Dashboard Redesign**
#### ⚡ Authentication
* Modernized UI with detailed server stats
* 🖥️ **Server API Enhancements**
* 🚀 **Implemented Go HTTP Auth Server** for **maximum performance**
* ⚡ Removed old command-based auth system
* Added uptime and traffic-since-reboot metrics
* ⚡ **System Monitor Optimization**
#### 👥 User Management
* Improved performance with async I/O
* Accurate traffic tracking since reboot
* ✨ **Bulk User Creation** added across:
#### 🐛 Fixes
* 🔧 Correctly count **actual device connections** instead of unique users
* 🔥 Fixed subscription blocked page to display the right user data
* 🖥️ **Frontend UI**
* 📡 **API Endpoint**
* 💻 **CLI Command**
* 📜 **Automation Script**
* 🔍 New **Online User Filter & Sort** on the Users page
* 🐛 Fixed: underscores now supported in usernames

View File

@ -13,8 +13,10 @@
}
},
"auth": {
"type": "command",
"command": "/etc/hysteria/core/scripts/hysteria2/user.sh"
"type": "http",
"http": {
"url": "http://127.0.0.1:28262/auth"
}
},
"quic": {
"initStreamReceiveWindow": 8388608,

View File

@ -0,0 +1,139 @@
package main
import (
"crypto/subtle"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
)
const (
listenAddr = "127.0.0.1:28262"
usersFile = "/etc/hysteria/users.json"
cacheTTL = 5 * time.Second
)
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"`
}
type httpAuthRequest struct {
Addr string `json:"addr"`
Auth string `json:"auth"`
Tx uint64 `json:"tx"`
}
type httpAuthResponse struct {
OK bool `json:"ok"`
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()
}
func authHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req httpAuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
username, password, ok := strings.Cut(req.Auth, ":")
if !ok {
json.NewEncoder(w).Encode(httpAuthResponse{OK: false})
return
}
cacheMutex.RLock()
user, ok := userCache[username]
cacheMutex.RUnlock()
if !ok {
json.NewEncoder(w).Encode(httpAuthResponse{OK: false})
return
}
if user.Blocked {
json.NewEncoder(w).Encode(httpAuthResponse{OK: false})
return
}
if subtle.ConstantTimeCompare([]byte(user.Password), []byte(password)) != 1 {
time.Sleep(5 * time.Second) // Slow down brute-force attacks
json.NewEncoder(w).Encode(httpAuthResponse{OK: false})
return
}
if user.UnlimitedUser {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(httpAuthResponse{OK: true, ID: username})
return
}
if user.ExpirationDays > 0 {
creationDate, err := time.Parse("2006-01-02", user.AccountCreationDate)
if err == nil && time.Now().After(creationDate.AddDate(0, 0, user.ExpirationDays)) {
json.NewEncoder(w).Encode(httpAuthResponse{OK: false})
return
}
}
if user.MaxDownloadBytes > 0 && (user.DownloadBytes+user.UploadBytes) >= user.MaxDownloadBytes {
json.NewEncoder(w).Encode(httpAuthResponse{OK: false})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(httpAuthResponse{OK: true, ID: username})
}
func main() {
log.SetOutput(io.Discard)
loadUsersToCache()
ticker := time.NewTicker(cacheTTL)
go func() {
for range ticker.C {
loadUsersToCache()
}
}()
http.HandleFunc("/auth", authHandler)
if err := http.ListenAndServe(listenAddr, nil); err != nil {
log.SetOutput(os.Stderr)
log.Fatalf("Failed to start server: %v", err)
}
}

View File

@ -0,0 +1,74 @@
import json
import os
import asyncio
from datetime import datetime, timedelta
from aiohttp import web
import aiofiles
from init_paths import *
from paths import *
users_data = {}
users_lock = asyncio.Lock()
async def load_users(app):
global users_data
async with users_lock:
if os.path.exists(USERS_FILE):
try:
async with aiofiles.open(USERS_FILE, 'r') as f:
content = await f.read()
users_data = json.loads(content)
except (IOError, json.JSONDecodeError):
users_data = {}
else:
users_data = {}
app['users_data'] = users_data
async def authenticate(request):
global users_data
try:
data = await request.json()
auth_str = data.get("auth")
if not auth_str:
return web.json_response({"ok": False, "msg": "Auth field missing"}, status=400)
username, password = auth_str.split(":", 1)
except (json.JSONDecodeError, ValueError, TypeError):
return web.json_response({"ok": False, "msg": "Invalid request format"}, status=400)
async with users_lock:
user = users_data.get(username)
if not user:
return web.json_response({"ok": False, "msg": "User not found"}, status=401)
if user.get("blocked", False):
return web.json_response({"ok": False, "msg": "User is blocked"}, status=401)
if user.get("password") != password:
return web.json_response({"ok": False, "msg": "Invalid password"}, status=401)
expiration_days = user.get("expiration_days", 0)
if expiration_days > 0:
creation_date_str = user.get("account_creation_date")
if creation_date_str:
creation_date = datetime.strptime(creation_date_str, "%Y-%m-%d")
expiration_date = creation_date + timedelta(days=expiration_days)
if datetime.now() >= expiration_date:
return web.json_response({"ok": False, "msg": "Account expired"}, status=401)
max_bytes = user.get("max_download_bytes", 0)
if max_bytes > 0:
current_up = user.get("upload_bytes", 0)
current_down = user.get("download_bytes", 0)
if (current_up + current_down) >= max_bytes:
return web.json_response({"ok": False, "msg": "Data limit exceeded"}, status=401)
return web.json_response({"ok": True, "id": username})
app = web.Application()
app.router.add_post("/auth", authenticate)
app.on_startup.append(load_users)
if __name__ == "__main__":
web.run_app(app, host="127.0.0.1", port=28262)

View File

@ -5,6 +5,29 @@ source /etc/hysteria/core/scripts/utils.sh
source /etc/hysteria/core/scripts/scheduler.sh
define_colors
compile_auth_binary() {
echo "Compiling authentication binary..."
local auth_dir="/etc/hysteria/core/scripts/auth"
if [ -f "$auth_dir/user_auth.go" ]; then
(
cd "$auth_dir" || exit 1
go mod init hysteria-auth >/dev/null 2>&1
go mod tidy >/dev/null 2>&1
if go build -o user_auth .; then
chmod +x user_auth
echo "Authentication binary compiled successfully."
else
echo -e "${red}Error:${NC} Failed to compile the authentication binary."
exit 1
fi
)
else
echo -e "${red}Error:${NC} Go source file not found at $auth_dir/user_auth.go"
exit 1
fi
}
install_hysteria() {
local port=$1
@ -13,6 +36,8 @@ install_hysteria() {
mkdir -p /etc/hysteria && cd /etc/hysteria/
compile_auth_binary
echo "Generating CA key and certificate..."
openssl ecparam -genkey -name prime256v1 -out ca.key >/dev/null 2>&1
openssl req -new -x509 -days 36500 -key ca.key -out ca.crt -subj "/CN=$sni" >/dev/null 2>&1
@ -80,9 +105,20 @@ install_hysteria() {
exit 1
fi
chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh
chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py
if ! check_auth_server_service; then
echo "Setting up Hysteria auth server..."
setup_hysteria_auth_server
fi
if systemctl is-active --quiet hysteria-auth.service; then
echo -e "${cyan}Hysteria auth server${NC} has been successfully started."
else
echo -e "${red}Error:${NC} hysteria-auth.service is not active."
exit 1
fi
if ! check_scheduler_service; then
setup_hysteria_scheduler
fi

View File

@ -27,10 +27,7 @@ EOF
systemctl daemon-reload
systemctl enable hysteria-scheduler.service
systemctl start hysteria-scheduler.service
# wait 2
(crontab -l | grep -v "hysteria2_venv.*traffic-status" | grep -v "hysteria2_venv.*backup-hysteria") | crontab -
# return 0
}
check_scheduler_service() {
@ -40,3 +37,38 @@ check_scheduler_service() {
return 1
fi
}
setup_hysteria_auth_server() {
# chmod +x /etc/hysteria/core/scripts/auth/user_auth
cat > /etc/systemd/system/hysteria-auth.service << 'EOF'
[Unit]
Description=Hysteria Auth Server
After=network.target
[Service]
Type=simple
User=root
ExecStart=/etc/hysteria/core/scripts/auth/user_auth
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=hysteria-Auth
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable hysteria-auth.service
systemctl start hysteria-auth.service
}
check_auth_server_service() {
if systemctl is-active --quiet hysteria-auth.service; then
return 0
else
return 1
fi
}

View File

@ -3,6 +3,7 @@
declare -a services=(
"hysteria-server.service"
"hysteria-scheduler.service"
"hysteria-auth.service"
"hysteria-webpanel.service"
"hysteria-caddy.service"
"hysteria-telegram-bot.service"
@ -22,8 +23,6 @@ for service in "${services[@]}"; do
fi
done
# Remove trailing comma and close JSON properly
status_json="${status_json%,}}"
# Format output as valid JSON
echo "$status_json" | jq -M .

View File

@ -76,7 +76,7 @@ check_os_version() {
}
install_packages() {
local REQUIRED_PACKAGES=("jq" "curl" "pwgen" "python3" "python3-pip" "python3-venv" "git" "bc" "zip" "cron" "lsof")
local REQUIRED_PACKAGES=("jq" "curl" "pwgen" "python3" "python3-pip" "python3-venv" "git" "bc" "zip" "cron" "lsof" "golang-go")
local MISSING_PACKAGES=()
log_info "Checking required packages..."

View File

@ -6,6 +6,7 @@ trap 'echo -e "\n❌ An error occurred. Aborting."; exit 1' ERR
# ========== Variables ==========
HYSTERIA_INSTALL_DIR="/etc/hysteria"
HYSTERIA_VENV_DIR="$HYSTERIA_INSTALL_DIR/hysteria2_venv"
AUTH_BINARY_DIR="$HYSTERIA_INSTALL_DIR/core/scripts/auth"
REPO_URL="https://github.com/ReturnFI/Blitz"
REPO_BRANCH="main"
GEOSITE_URL="https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geosite.dat"
@ -23,6 +24,37 @@ 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"; }
# ========== New Function to 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 -y >/dev/null
apt-get install -y golang-go >/dev/null
success "Go installed successfully."
else
success "Go is already installed."
fi
if [[ -f "$AUTH_BINARY_DIR/user_auth.go" ]]; then
info "Found auth binary source. Compiling..."
(
cd "$AUTH_BINARY_DIR"
go mod init hysteria_auth >/dev/null 2>&1
go mod tidy >/dev/null 2>&1
if go build -o user_auth .; then
chmod +x user_auth
success "Authentication binary compiled successfully."
else
error "Failed to compile the authentication binary."
exit 1
fi
)
else
warn "Authentication binary source not found. Skipping compilation."
fi
}
# ========== Backup Files ==========
cd /root
TEMP_DIR=$(mktemp -d)
@ -34,7 +66,6 @@ FILES=(
"$HYSTERIA_INSTALL_DIR/.configs.env"
"$HYSTERIA_INSTALL_DIR/nodes.json"
"$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot/.env"
# "$HYSTERIA_INSTALL_DIR/core/scripts/singbox/.env"
"$HYSTERIA_INSTALL_DIR/core/scripts/normalsub/.env"
"$HYSTERIA_INSTALL_DIR/core/scripts/normalsub/Caddyfile.normalsub"
"$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/.env"
@ -77,15 +108,21 @@ for FILE in "${FILES[@]}"; do
fi
done
# ========== Update Configuration ==========
info "Updating Hysteria configuration for HTTP authentication..."
auth_block='{"type": "http", "http": {"url": "http://127.0.0.1:28262/auth"}}'
if [[ -f "$HYSTERIA_INSTALL_DIR/config.json" ]]; then
jq --argjson auth_block "$auth_block" '.auth = $auth_block' "$HYSTERIA_INSTALL_DIR/config.json" > "$HYSTERIA_INSTALL_DIR/config.json.tmp" && mv "$HYSTERIA_INSTALL_DIR/config.json.tmp" "$HYSTERIA_INSTALL_DIR/config.json"
success "config.json updated to use auth server."
else
warn "config.json not found after restore. Skipping auth update."
fi
# ========== Permissions ==========
info "Setting ownership and permissions..."
chown hysteria:hysteria "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt"
chmod 640 "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt"
# chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/singbox"
chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot"
chmod +x "$HYSTERIA_INSTALL_DIR/core/scripts/hysteria2/user.sh"
chmod +x "$HYSTERIA_INSTALL_DIR/core/scripts/hysteria2/kick.py"
# ========== Virtual Environment ==========
@ -97,26 +134,32 @@ pip install --upgrade pip >/dev/null
pip install -r requirements.txt >/dev/null
success "Python environment ready."
# ========== Scheduler ==========
info "Ensuring scheduler is set..."
# ========== Compile Go Binary ==========
install_go_and_compile_auth
# ========== Systemd Services ==========
info "Ensuring systemd services are configured..."
if source "$HYSTERIA_INSTALL_DIR/core/scripts/scheduler.sh"; then
if ! check_auth_server_service; then
setup_hysteria_auth_server && success "Auth server service configured." || warn "Auth server setup failed."
else
success "Auth server service already configured."
fi
if ! check_scheduler_service; then
if setup_hysteria_scheduler; then
success "Scheduler service configured."
setup_hysteria_scheduler && success "Scheduler service configured." || warn "Scheduler setup failed."
else
warn "Scheduler setup failed, but continuing upgrade..."
success "Scheduler service already set."
fi
else
success "Scheduler already set."
fi
else
warn "Failed to source scheduler.sh, continuing without scheduler setup..."
warn "Failed to source scheduler.sh, continuing without service setup..."
fi
# ========== Restart Services ==========
SERVICES=(
hysteria-caddy.service
hysteria-server.service
hysteria-auth.service
hysteria-scheduler.service
hysteria-telegram-bot.service
hysteria-normal-sub.service
@ -130,7 +173,7 @@ for SERVICE in "${SERVICES[@]}"; do
if systemctl status "$SERVICE" &>/dev/null; then
systemctl restart "$SERVICE" && success "$SERVICE restarted." || warn "$SERVICE failed to restart."
else
warn "$SERVICE not found or not installed. Skipping..."
warn "$SERVICE not found. Skipping..."
fi
done