From 3ca2e6649e4cf9a3f0dd6a76594b5b8884d29f62 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 24 Aug 2025 19:56:07 +0330 Subject: [PATCH] feat(auth): Migrate from command to high-performance HTTP --- core/scripts/auth/user_auth.go | 147 +++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 core/scripts/auth/user_auth.go diff --git a/core/scripts/auth/user_auth.go b/core/scripts/auth/user_auth.go new file mode 100644 index 0000000..4a4f85c --- /dev/null +++ b/core/scripts/auth/user_auth.go @@ -0,0 +1,147 @@ +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 = 15 * 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() + + // 1. Check existence + if !ok { + json.NewEncoder(w).Encode(httpAuthResponse{OK: false}) + return + } + + // 2. Check if blocked + if user.Blocked { + json.NewEncoder(w).Encode(httpAuthResponse{OK: false}) + return + } + + // 3. Check password (constant time) + 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 + } + + // 4. Check if unlimited (if so, grant access) + if user.UnlimitedUser { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(httpAuthResponse{OK: true, ID: username}) + return + } + + // 5. Check expiration + 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 + } + } + + // 6. Check traffic limit + if user.MaxDownloadBytes > 0 && (user.DownloadBytes+user.UploadBytes) >= user.MaxDownloadBytes { + json.NewEncoder(w).Encode(httpAuthResponse{OK: false}) + return + } + + // All checks passed + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(httpAuthResponse{OK: true, ID: username}) +} + +func main() { + log.SetOutput(io.Discard) // Disable logging for max performance + loadUsersToCache() + + ticker := time.NewTicker(cacheTTL) + go func() { + for range ticker.C { + loadUsersToCache() + } + }() + + http.HandleFunc("/auth", authHandler) + if err := http.ListenAndServe(listenAddr, nil); err != nil { + // If we can't start, log to stderr so systemd can see it + log.SetOutput(os.Stderr) + log.Fatalf("Failed to start server: %v", err) + } +}