317 lines
9.0 KiB
Go
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")
|
|
}
|