Files
DragonCoreSSH-NewWEB/managed_servers.go
2026-05-11 14:39:55 -03:00

606 lines
18 KiB
Go

package main
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type ManagedServer struct {
ID int
Name string
BaseURL string
AdminUsername string
AdminKey string
EnableSSH bool
EnableXray bool
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
}
type ManagedServerDTO struct {
ID string `json:"id"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
AdminUsername string `json:"admin_username,omitempty"`
EnableSSH bool `json:"enable_ssh"`
EnableXray bool `json:"enable_xray"`
IsActive bool `json:"is_active"`
IsLocal bool `json:"is_local"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
type ManagedServerPayload struct {
ID string `json:"id"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
AdminUsername string `json:"admin_username"`
AdminKey string `json:"admin_key"`
EnableSSH bool `json:"enable_ssh"`
EnableXray bool `json:"enable_xray"`
IsActive bool `json:"is_active"`
}
func (s *Store) EnsureManagedServersSchema(ctx context.Context) error {
_, err := s.db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS managed_servers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
base_url TEXT NOT NULL UNIQUE,
admin_username TEXT NOT NULL DEFAULT 'admin',
admin_key TEXT NOT NULL DEFAULT '',
enable_ssh BOOLEAN NOT NULL DEFAULT TRUE,
enable_xray BOOLEAN NOT NULL DEFAULT TRUE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`)
return err
}
func (s *Store) ListManagedServers(ctx context.Context) ([]*ManagedServer, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, name, base_url, admin_username, admin_key, enable_ssh, enable_xray, is_active, created_at, updated_at
FROM managed_servers ORDER BY id ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*ManagedServer
for rows.Next() {
ms := &ManagedServer{}
if err := rows.Scan(&ms.ID, &ms.Name, &ms.BaseURL, &ms.AdminUsername, &ms.AdminKey, &ms.EnableSSH, &ms.EnableXray, &ms.IsActive, &ms.CreatedAt, &ms.UpdatedAt); err != nil {
return nil, err
}
out = append(out, ms)
}
return out, rows.Err()
}
func (s *Store) GetManagedServer(ctx context.Context, id int) (*ManagedServer, error) {
ms := &ManagedServer{}
err := s.db.QueryRowContext(ctx, `
SELECT id, name, base_url, admin_username, admin_key, enable_ssh, enable_xray, is_active, created_at, updated_at
FROM managed_servers WHERE id=$1`, id).
Scan(&ms.ID, &ms.Name, &ms.BaseURL, &ms.AdminUsername, &ms.AdminKey, &ms.EnableSSH, &ms.EnableXray, &ms.IsActive, &ms.CreatedAt, &ms.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return ms, nil
}
func (s *Store) UpsertManagedServer(ctx context.Context, p ManagedServerPayload) (*ManagedServer, error) {
name := strings.TrimSpace(p.Name)
baseURL := normalizeManagedServerBaseURL(p.BaseURL)
adminUsername := strings.TrimSpace(p.AdminUsername)
if adminUsername == "" {
adminUsername = "admin"
}
if name == "" {
return nil, fmt.Errorf("server name required")
}
if baseURL == "" {
return nil, fmt.Errorf("base url required")
}
if p.ID != "" && p.ID != "local" {
id, err := strconv.Atoi(p.ID)
if err != nil || id <= 0 {
return nil, fmt.Errorf("invalid server id")
}
if strings.TrimSpace(p.AdminKey) == "" {
_, err = s.db.ExecContext(ctx, `
UPDATE managed_servers
SET name=$2, base_url=$3, admin_username=$4, enable_ssh=$5, enable_xray=$6, is_active=$7, updated_at=NOW()
WHERE id=$1`, id, name, baseURL, adminUsername, p.EnableSSH, p.EnableXray, p.IsActive)
} else {
_, err = s.db.ExecContext(ctx, `
UPDATE managed_servers
SET name=$2, base_url=$3, admin_username=$4, admin_key=$5, enable_ssh=$6, enable_xray=$7, is_active=$8, updated_at=NOW()
WHERE id=$1`, id, name, baseURL, adminUsername, p.AdminKey, p.EnableSSH, p.EnableXray, p.IsActive)
}
if err != nil {
return nil, err
}
return s.GetManagedServer(ctx, id)
}
if strings.TrimSpace(p.AdminKey) == "" {
return nil, fmt.Errorf("admin key/password required")
}
var id int
err := s.db.QueryRowContext(ctx, `
INSERT INTO managed_servers (name, base_url, admin_username, admin_key, enable_ssh, enable_xray, is_active)
VALUES ($1,$2,$3,$4,$5,$6,$7)
ON CONFLICT (base_url) DO UPDATE SET
name=EXCLUDED.name,
admin_username=EXCLUDED.admin_username,
admin_key=EXCLUDED.admin_key,
enable_ssh=EXCLUDED.enable_ssh,
enable_xray=EXCLUDED.enable_xray,
is_active=EXCLUDED.is_active,
updated_at=NOW()
RETURNING id`, name, baseURL, adminUsername, p.AdminKey, p.EnableSSH, p.EnableXray, p.IsActive).Scan(&id)
if err != nil {
return nil, err
}
return s.GetManagedServer(ctx, id)
}
func (s *Store) DeleteManagedServer(ctx context.Context, id int) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM managed_servers WHERE id=$1`, id)
return err
}
func managedServerToDTO(ms *ManagedServer) ManagedServerDTO {
return ManagedServerDTO{
ID: strconv.Itoa(ms.ID),
Name: ms.Name,
BaseURL: ms.BaseURL,
AdminUsername: ms.AdminUsername,
EnableSSH: ms.EnableSSH,
EnableXray: ms.EnableXray,
IsActive: ms.IsActive,
CreatedAt: ms.CreatedAt,
UpdatedAt: ms.UpdatedAt,
}
}
func localManagedServerDTO() ManagedServerDTO {
cfg := getGlobalCfg()
xrayEnabled := cfg != nil && cfg.Xray != nil && cfg.Xray.Enabled
return ManagedServerDTO{
ID: "local",
Name: "Master node",
BaseURL: "local",
EnableSSH: true,
EnableXray: xrayEnabled,
IsActive: true,
IsLocal: true,
}
}
func normalizeManagedServerBaseURL(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
raw = "http://" + raw
}
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
return ""
}
u.Path = strings.TrimRight(u.Path, "/")
u.RawQuery = ""
u.Fragment = ""
return strings.TrimRight(u.String(), "/")
}
func requestedServerID(r *http.Request) string {
id := strings.TrimSpace(r.URL.Query().Get("server_id"))
if id == "" {
id = strings.TrimSpace(r.URL.Query().Get("server"))
}
return id
}
func managedServerFromID(ctx context.Context, store *Store, id string) (*ManagedServer, bool, error) {
id = strings.TrimSpace(id)
if id == "" || id == "local" || id == "0" {
return nil, false, nil
}
if store == nil {
return nil, false, fmt.Errorf("database not configured")
}
n, err := strconv.Atoi(id)
if err != nil || n <= 0 {
return nil, false, fmt.Errorf("invalid server id")
}
ms, err := store.GetManagedServer(ctx, n)
if err != nil {
return nil, false, err
}
if ms == nil {
return nil, false, fmt.Errorf("server not found")
}
if !ms.IsActive {
return nil, false, fmt.Errorf("server is disabled")
}
return ms, true, nil
}
func remoteLoginToken(ctx context.Context, ms *ManagedServer) (string, error) {
body, _ := json.Marshal(map[string]string{"username": ms.AdminUsername, "password": ms.AdminKey})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, ms.BaseURL+"/api/auth/login", bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
data, _ := io.ReadAll(io.LimitReader(resp.Body, 128*1024))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("remote login failed: %s", strings.TrimSpace(string(data)))
}
var out struct {
Token string `json:"token"`
}
if err := json.Unmarshal(data, &out); err != nil || out.Token == "" {
return "", fmt.Errorf("remote login returned no token")
}
return out.Token, nil
}
func proxyManagedServer(ctx context.Context, ms *ManagedServer, method, path string, body []byte, contentType string) (int, []byte, string, error) {
token, err := remoteLoginToken(ctx, ms)
if err != nil {
return 0, nil, "", err
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
req, err := http.NewRequestWithContext(ctx, method, ms.BaseURL+path, bytes.NewReader(body))
if err != nil {
return 0, nil, "", err
}
if contentType == "" {
contentType = "application/json"
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-Session-Token", token)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return 0, nil, "", err
}
defer resp.Body.Close()
data, _ := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024))
return resp.StatusCode, data, resp.Header.Get("Content-Type"), nil
}
func handleManagedProxyOrLocal(store *Store, local http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if proxyManagedServerFromRequest(w, r, store, "", nil, "") {
return
}
local(w, r)
}
}
func writeProxyResponse(w http.ResponseWriter, status int, body []byte, contentType string) {
if contentType != "" {
w.Header().Set("Content-Type", contentType)
}
if status == 0 {
status = http.StatusBadGateway
}
w.WriteHeader(status)
if len(body) > 0 {
_, _ = w.Write(body)
}
}
func proxyManagedServerFromRequest(w http.ResponseWriter, r *http.Request, store *Store, remotePath string, body []byte, filterOwner string) bool {
ms, remote, err := managedServerFromID(r.Context(), store, requestedServerID(r))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return true
}
if !remote {
return false
}
if remotePath == "" {
remotePath = r.URL.Path
if r.URL.RawQuery != "" {
q := r.URL.Query()
q.Del("server_id")
q.Del("server")
if enc := q.Encode(); enc != "" {
remotePath += "?" + enc
}
}
}
if body == nil && r.Body != nil && r.Method != http.MethodGet {
body, _ = io.ReadAll(io.LimitReader(r.Body, 2*1024*1024))
}
status, data, ct, err := proxyManagedServer(r.Context(), ms, r.Method, remotePath, body, r.Header.Get("Content-Type"))
if err != nil {
http.Error(w, "remote server error: "+err.Error(), http.StatusBadGateway)
return true
}
if status >= 200 && status < 300 && filterOwner != "" && strings.Contains(ct, "json") {
if filtered, ok := filterRemoteOwnerJSON(remotePath, data, filterOwner); ok {
data = filtered
}
}
writeProxyResponse(w, status, data, ct)
return true
}
func filterRemoteOwnerJSON(path string, data []byte, owner string) ([]byte, bool) {
if owner == "" || len(data) == 0 {
return data, false
}
if strings.HasPrefix(path, "/api/users") {
var rows []map[string]interface{}
if err := json.Unmarshal(data, &rows); err != nil {
return data, false
}
out := rows[:0]
for _, row := range rows {
if strings.TrimSpace(fmt.Sprint(row["owner_username"])) == owner {
out = append(out, row)
}
}
filtered, _ := json.Marshal(out)
return filtered, true
}
if strings.HasPrefix(path, "/api/xray/inbounds") {
var inbounds []map[string]interface{}
if err := json.Unmarshal(data, &inbounds); err != nil {
return data, false
}
for _, ib := range inbounds {
clients, _ := ib["clients"].([]interface{})
filtered := make([]interface{}, 0, len(clients))
for _, c := range clients {
m, _ := c.(map[string]interface{})
if strings.TrimSpace(fmt.Sprint(m["owner_username"])) == owner {
filtered = append(filtered, c)
}
}
ib["clients"] = filtered
}
filtered, _ := json.Marshal(inbounds)
return filtered, true
}
return data, false
}
func handleServers(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if store == nil {
http.Error(w, "database not configured", http.StatusServiceUnavailable)
return
}
sess := sessionFromCtx(r.Context())
switch r.Method {
case http.MethodGet:
rows, err := store.ListManagedServers(r.Context())
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
out := []ManagedServerDTO{localManagedServerDTO()}
for _, ms := range rows {
if sess != nil && sess.Role == RoleReseller && !ms.IsActive {
continue
}
dto := managedServerToDTO(ms)
if sess != nil && sess.Role == RoleReseller {
dto.AdminUsername = ""
}
out = append(out, dto)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
case http.MethodPost:
if sess == nil || sess.Role != RoleSuperAdmin {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
var p ManagedServerPayload
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
ms, err := store.UpsertManagedServer(r.Context(), p)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(managedServerToDTO(ms))
case http.MethodDelete:
if sess == nil || sess.Role != RoleSuperAdmin {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
idStr := strings.TrimSpace(r.URL.Query().Get("id"))
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
http.Error(w, "invalid server id", http.StatusBadRequest)
return
}
if err := store.DeleteManagedServer(r.Context(), id); err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
}
func handleServerTest(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if store == nil {
http.Error(w, "database not configured", http.StatusServiceUnavailable)
return
}
var p ManagedServerPayload
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
ms := &ManagedServer{Name: p.Name, BaseURL: normalizeManagedServerBaseURL(p.BaseURL), AdminUsername: strings.TrimSpace(p.AdminUsername), AdminKey: p.AdminKey, EnableSSH: p.EnableSSH, EnableXray: p.EnableXray, IsActive: true}
if p.ID != "" && p.ID != "local" && (ms.BaseURL == "" || ms.AdminKey == "") {
id, _ := strconv.Atoi(p.ID)
if id > 0 {
stored, err := store.GetManagedServer(r.Context(), id)
if err == nil && stored != nil {
if ms.BaseURL == "" {
ms.BaseURL = stored.BaseURL
}
if ms.AdminUsername == "" {
ms.AdminUsername = stored.AdminUsername
}
if ms.AdminKey == "" {
ms.AdminKey = stored.AdminKey
}
}
}
}
if ms.AdminUsername == "" {
ms.AdminUsername = "admin"
}
if ms.BaseURL == "" || ms.AdminKey == "" {
http.Error(w, "base url and admin key/password required", http.StatusBadRequest)
return
}
token, err := remoteLoginToken(r.Context(), ms)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
_ = token
status, data, _, err := proxyManagedServer(r.Context(), ms, http.MethodGet, "/api/auth/me", nil, "application/json")
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
if status < 200 || status >= 300 {
http.Error(w, strings.TrimSpace(string(data)), http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "remote login ok"})
}
}
func handleManagedServerConfig(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := requestedServerID(r)
if id == "" || id == "local" || id == "0" {
handleServerConfig(w, r)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
body := []byte(nil)
if r.Method == http.MethodPost {
var err error
body, err = io.ReadAll(io.LimitReader(r.Body, 512*1024))
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
}
ms, remote, err := managedServerFromID(r.Context(), store, id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !remote {
handleServerConfig(w, r)
return
}
status, data, ct, err := proxyManagedServer(r.Context(), ms, r.Method, "/api/server/config", body, "application/json")
if err != nil {
log.Printf("managed server config proxy %s: %v", ms.BaseURL, err)
http.Error(w, "remote server error: "+err.Error(), http.StatusBadGateway)
return
}
writeProxyResponse(w, status, data, ct)
}
}
func remoteSSHUserOwned(ctx context.Context, ms *ManagedServer, username, owner string) bool {
if owner == "" || username == "" {
return false
}
status, data, _, err := proxyManagedServer(ctx, ms, http.MethodGet, "/api/users", nil, "application/json")
if err != nil || status < 200 || status >= 300 {
return false
}
var rows []map[string]interface{}
if err := json.Unmarshal(data, &rows); err != nil {
return false
}
for _, row := range rows {
if fmt.Sprint(row["username"]) == username && fmt.Sprint(row["owner_username"]) == owner {
return true
}
}
return false
}
func remoteXrayClientOwned(ctx context.Context, ms *ManagedServer, uuid, owner string) bool {
if owner == "" || uuid == "" {
return false
}
status, data, _, err := proxyManagedServer(ctx, ms, http.MethodGet, "/api/xray/inbounds", nil, "application/json")
if err != nil || status < 200 || status >= 300 {
return false
}
var inbounds []map[string]interface{}
if err := json.Unmarshal(data, &inbounds); err != nil {
return false
}
for _, ib := range inbounds {
clients, _ := ib["clients"].([]interface{})
for _, c := range clients {
m, _ := c.(map[string]interface{})
if fmt.Sprint(m["id"]) == uuid && fmt.Sprint(m["owner_username"]) == owner {
return true
}
}
}
return false
}