Status page per machine: CPU load, RAM and disk of the client, sampled by the agent only while the page is open and visible (one reading serves every open tab), plus a button that has the agent measure its link to the host: ping, down and up. Online/offline now comes from the control polls the hub holds open, so a dropped agent shows offline at once instead of 45 s later, and the dashboard keeps the status column live. A forward rule can be created "without ufw": the panel opens nothing in ufw for it and so revokes nothing when it is deleted. The checkbox is shown only while the ufw integration is on. Dashboard row actions are icons; a status action is added.
117 lines
3.2 KiB
Go
117 lines
3.2 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
|
)
|
|
|
|
// fsOps maps the URL op segment to a proto task type.
|
|
var fsOps = map[string]string{
|
|
"list": proto.TaskFSList,
|
|
"read": proto.TaskFSRead,
|
|
"write": proto.TaskFSWrite,
|
|
"mkdir": proto.TaskFSMkdir,
|
|
"delete": proto.TaskFSDelete,
|
|
"deltree": proto.TaskFSDelTree,
|
|
"unzip": proto.TaskFSUnzip,
|
|
"zip": proto.TaskFSZip,
|
|
"move": proto.TaskFSMove,
|
|
}
|
|
|
|
// fsTaskTimeout bounds how long we wait for an agent to answer a file op.
|
|
const fsTaskTimeout = 60 * time.Second
|
|
|
|
func (s *Server) handleFilesPage(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
a, err := s.db.GetAgent(id)
|
|
if err != nil || a == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
s.render(w, "files.html", map[string]any{"Agent": a, "EditLimit": proto.FSReadCap})
|
|
}
|
|
|
|
// POST /api/panel/agents/{id}/fs/{op} — run a file-manager operation on the
|
|
// agent and return its result. The request body is the op's JSON args and is
|
|
// forwarded verbatim to the agent.
|
|
func (s *Server) handleFSOp(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
op := r.PathValue("op")
|
|
taskType, ok := fsOps[op]
|
|
if !ok {
|
|
http.Error(w, "unknown op", http.StatusBadRequest)
|
|
return
|
|
}
|
|
a, err := s.db.GetAgent(agentID)
|
|
if err != nil || a == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if !s.online(a) {
|
|
writeJSON(w, proto.TaskResult{Error: "машина офлайн"})
|
|
return
|
|
}
|
|
args, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 64<<20))
|
|
if err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
taskID, resultCh := s.hub.CreateTask(agentID, taskType, args)
|
|
defer s.hub.DiscardTask(taskID)
|
|
|
|
select {
|
|
case res := <-resultCh:
|
|
writeJSON(w, res)
|
|
case <-time.After(fsTaskTimeout):
|
|
writeJSON(w, proto.TaskResult{Error: "таймаут ожидания агента"})
|
|
}
|
|
}
|
|
|
|
// GET /api/panel/agents/{id}/fs/download?path=... — stream a file as a download.
|
|
func (s *Server) handleFSDownload(w http.ResponseWriter, r *http.Request) {
|
|
agentID := r.PathValue("id")
|
|
p := r.URL.Query().Get("path")
|
|
a, err := s.db.GetAgent(agentID)
|
|
if err != nil || a == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if !s.online(a) {
|
|
http.Error(w, "машина офлайн", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
args, _ := json.Marshal(proto.FSPathArgs{Path: p})
|
|
taskID, ch := s.hub.CreateTask(agentID, proto.TaskFSDownload, args)
|
|
defer s.hub.DiscardTask(taskID)
|
|
|
|
select {
|
|
case res := <-ch:
|
|
if !res.OK {
|
|
http.Error(w, res.Error, http.StatusBadGateway)
|
|
return
|
|
}
|
|
var rr proto.FSReadResult
|
|
if err := json.Unmarshal(res.Data, &rr); err != nil {
|
|
http.Error(w, "bad result", http.StatusBadGateway)
|
|
return
|
|
}
|
|
data, _ := base64.StdEncoding.DecodeString(rr.Content)
|
|
name := path.Base(p)
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
|
|
if rr.Truncated {
|
|
w.Header().Set("X-Truncated", "1")
|
|
}
|
|
_, _ = w.Write(data)
|
|
case <-time.After(fsTaskTimeout):
|
|
http.Error(w, "таймаут ожидания агента", http.StatusGatewayTimeout)
|
|
}
|
|
}
|