Files
panel-vpn/internal/handlers/nodes.go
T

317 lines
9.0 KiB
Go

package handlers
import (
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/orohi/vpn-panel/internal/db"
"github.com/orohi/vpn-panel/internal/models"
"github.com/orohi/vpn-panel/internal/nodeclient"
"github.com/orohi/vpn-panel/internal/nodeinstall"
"github.com/orohi/vpn-panel/internal/secretbox"
)
func (s *Server) nodesList(w http.ResponseWriter, r *http.Request) {
nodes, err := db.ListNodes(s.DB)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
s.render(w, "nodes.html", pageData{
"Title": "Ноды",
"UserName": s.Auth.CurrentName(r),
"Nodes": nodes,
"Active": "nodes",
"Flash": r.URL.Query().Get("ok"),
"Error": r.URL.Query().Get("err"),
})
}
func (s *Server) nodesNewGet(w http.ResponseWriter, r *http.Request) {
s.render(w, "node_new.html", pageData{
"Title": "Добавить ноду",
"UserName": s.Auth.CurrentName(r),
"Active": "nodes",
"Form": map[string]any{
"SSHPort": 22,
"NodePort": 2222,
"SSHUser": "root",
"SSHAuthType": "password",
"InstallMode": "auto",
},
})
}
func (s *Server) nodesCreate(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
name := strings.TrimSpace(r.FormValue("name"))
host := strings.TrimSpace(r.FormValue("host"))
sshUser := strings.TrimSpace(r.FormValue("ssh_user"))
sshAuth := r.FormValue("ssh_auth_type")
sshSecret := r.FormValue("ssh_secret")
installMode := r.FormValue("install_mode")
if installMode != "manual" {
installMode = "auto"
}
if sshAuth != "key" {
sshAuth = "password"
}
sshPort, _ := strconv.Atoi(r.FormValue("ssh_port"))
if sshPort <= 0 {
sshPort = 22
}
nodePort, _ := strconv.Atoi(r.FormValue("node_port"))
if nodePort <= 0 {
nodePort = 2222
}
formErr := func(msg string) {
s.render(w, "node_new.html", pageData{
"Title": "Добавить ноду",
"UserName": s.Auth.CurrentName(r),
"Active": "nodes",
"Error": msg,
"Form": map[string]any{
"Name": name, "Host": host, "SSHUser": sshUser,
"SSHPort": sshPort, "NodePort": nodePort,
"SSHAuthType": sshAuth, "InstallMode": installMode,
},
})
}
if name == "" || host == "" {
formErr("Укажите имя и хост ноды")
return
}
if installMode == "auto" && strings.TrimSpace(sshSecret) == "" {
formErr("Для автоустановки нужен SSH пароль или ключ")
return
}
token, err := secretbox.RandomToken(32)
if err != nil {
http.Error(w, "token error", http.StatusInternalServerError)
return
}
enc, err := secretbox.Encrypt(s.SecretKey, sshSecret)
if err != nil {
http.Error(w, "encrypt error", http.StatusInternalServerError)
return
}
n := &models.Node{
ID: uuid.New(),
Name: name,
Host: host,
SSHPort: sshPort,
SSHUser: sshUser,
SSHAuthType: sshAuth,
SSHSecretEnc: enc,
NodePort: nodePort,
SecretKey: token,
Status: models.NodeStatusPending,
InstallMode: installMode,
}
if err := db.CreateNode(s.DB, n); err != nil {
log.Printf("create node: %v", err)
formErr("Не удалось сохранить ноду")
return
}
if installMode == "auto" {
go s.runAutoInstall(n.ID)
http.Redirect(w, r, "/admin/nodes/"+n.ID.String()+"?ok=install_started", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/nodes/"+n.ID.String()+"?ok=created", http.StatusSeeOther)
}
func (s *Server) nodeView(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
n, err := db.GetNode(s.DB, id)
if err != nil || n == nil {
http.NotFound(w, r)
return
}
nodeProtocols, err := db.ListNodeProtocols(s.DB, id)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
profiles, _ := db.ListProfiles(s.DB)
nodeInbounds, _ := db.ListNodeInbounds(s.DB, id)
s.render(w, "node_view.html", pageData{
"Title": n.Name,
"UserName": s.Auth.CurrentName(r),
"Active": "nodes",
"Node": n,
"NodeProtocols": nodeProtocols,
"NodeInbounds": nodeInbounds,
"Profiles": profiles,
"Compose": nodeinstall.ComposeYAML(n),
"ManualScript": nodeinstall.ManualInstallScript(n),
"Flash": r.URL.Query().Get("ok"),
"Error": r.URL.Query().Get("err"),
})
}
func (s *Server) nodeInstall(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
n, err := db.GetNode(s.DB, id)
if err != nil || n == nil {
http.NotFound(w, r)
return
}
if n.Status == models.NodeStatusInstalling {
http.Redirect(w, r, "/admin/nodes/"+id.String()+"?err=already_installing", http.StatusSeeOther)
return
}
go s.runAutoInstall(id)
http.Redirect(w, r, "/admin/nodes/"+id.String()+"?ok=install_started", http.StatusSeeOther)
}
func (s *Server) nodeCheck(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
n, err := db.GetNode(s.DB, id)
if err != nil || n == nil {
http.NotFound(w, r)
return
}
hr, err := nodeclient.Health(n)
if err != nil {
_ = db.UpdateNodeStatus(s.DB, id, models.NodeStatusOffline, err.Error())
http.Redirect(w, r, "/admin/nodes/"+id.String()+"?err=offline", http.StatusSeeOther)
return
}
_ = db.MarkNodeSeen(s.DB, id, hr.Version, models.NodeStatusOnline)
// Panel is source of truth — push desired protocols to the agent.
if err := s.syncNodeConfig(n); err != nil {
log.Printf("push config to node %s: %v", id, err)
}
http.Redirect(w, r, "/admin/nodes/"+id.String()+"?ok=online", http.StatusSeeOther)
}
func (s *Server) nodeDelete(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
if err := db.DeleteNode(s.DB, id); err != nil {
http.Redirect(w, r, "/admin/nodes?err=delete", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/nodes?ok=deleted", http.StatusSeeOther)
}
func (s *Server) nodeFetchLogs(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
n, err := db.GetNode(s.DB, id)
if err != nil || n == nil {
http.NotFound(w, r)
return
}
var chunks []string
stamp := time.Now().UTC().Format("2006-01-02 15:04:05 UTC")
chunks = append(chunks, "=== fetched at "+stamp+" ===")
if agentLogs, err := nodeclient.FetchLogs(n, 300); err == nil {
chunks = append(chunks, "---- agent API /api/v1/logs ----", agentLogs)
} else {
chunks = append(chunks, "---- agent API ----", "unavailable: "+err.Error())
}
secret, err := secretbox.Decrypt(s.SecretKey, n.SSHSecretEnc)
if err == nil && secret != "" {
if dockerLogs, err := nodeinstall.FetchDockerLogs(n, secret, 300); err == nil {
chunks = append(chunks, "---- docker / SSH ----", dockerLogs)
} else {
chunks = append(chunks, "---- docker / SSH ----", "unavailable: "+err.Error())
}
} else {
chunks = append(chunks, "---- docker / SSH ----", "(no SSH credentials stored — only agent API logs)")
}
full := strings.Join(chunks, "\n")
if err := db.SetNodeRuntimeLog(s.DB, id, full); err != nil {
log.Printf("save runtime log: %v", err)
http.Redirect(w, r, "/admin/nodes/"+id.String()+"?err=logs_save", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/admin/nodes/"+id.String()+"#logs", http.StatusSeeOther)
}
func (s *Server) runAutoInstall(id uuid.UUID) {
n, err := db.GetNode(s.DB, id)
if err != nil || n == nil {
return
}
_ = db.UpdateNodeStatus(s.DB, id, models.NodeStatusInstalling, "")
_ = db.ClearNodeInstallLog(s.DB, id)
logFn := func(line string) {
_ = db.AppendNodeInstallLog(s.DB, id, time.Now().UTC().Format("15:04:05")+" "+line)
}
secret, err := secretbox.Decrypt(s.SecretKey, n.SSHSecretEnc)
if err != nil {
_ = db.UpdateNodeStatus(s.DB, id, models.NodeStatusError, "decrypt ssh secret: "+err.Error())
return
}
if secret == "" {
_ = db.UpdateNodeStatus(s.DB, id, models.NodeStatusError, "SSH credentials empty")
return
}
bin, err := nodeinstall.LoadAgentBinary()
if err != nil {
_ = db.UpdateNodeStatus(s.DB, id, models.NodeStatusError, err.Error())
logFn(err.Error())
return
}
if err := nodeinstall.Provision(n, secret, bin, logFn); err != nil {
_ = db.UpdateNodeStatus(s.DB, id, models.NodeStatusError, err.Error())
logFn("ERROR: " + err.Error())
return
}
// wait briefly then probe health
time.Sleep(3 * time.Second)
if hr, err := nodeclient.Health(n); err == nil {
_ = db.MarkNodeSeen(s.DB, id, hr.Version, models.NodeStatusOnline)
logFn("health OK, node online")
return
}
_ = db.UpdateNodeStatus(s.DB, id, models.NodeStatusOffline, "agent started but health check failed (check firewall NODE_PORT)")
logFn("agent up, waiting for health on NODE_PORT — open firewall from panel IP")
}