package server import ( "encoding/json" "io" "net/http" "strings" "github.com/x-grk/longpolling-remote-access/internal/proto" ) // Files on an SSH host through a machine, for the panel's file panes: the // machine's agent keeps the SSH connection (open gives its id) and runs the // operations there. Downloads and uploads go in the same pieces as a // machine's own files. // POST /api/panel/agents/{id}/sshfs/open - {"ssh": {...}} → {conn, home}. func (s *Server) handleSSHFSOpen(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") var in struct { SSH *proto.SSHTarget `json:"ssh"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&in); err != nil || in.SSH == nil { writeJSON(w, proto.TaskResult{Error: "не указан хост"}) return } if err := s.fillSaved(s.currentAdmin(r).ID, in.SSH); err != nil { writeJSON(w, proto.TaskResult{Error: err.Error()}) return } t := in.SSH t.Host, t.User = strings.TrimSpace(t.Host), strings.TrimSpace(t.User) if t.Port == 0 { t.Port = 22 } if t.Host == "" || t.User == "" || t.Port < 1 || t.Port > 65535 { writeJSON(w, proto.TaskResult{Error: "нужны хост, порт и пользователь"}) return } if !s.fsAgent(w, agentID, true) { return } res, err := s.waitTask(r.Context(), agentID, proto.TaskSSHFSOpen, proto.SSHFSArgs{SSH: t}, fsTaskTimeout) if err != nil { res = proto.TaskResult{Error: err.Error()} } if unknownOp(res) { res = proto.TaskResult{Error: "агент не умеет файлы по SSH - обновите его"} } if res.OK { if admin := s.currentAdmin(r); admin != nil { s.db.Audit(admin.ID, agentID, "sshfs.open:"+t.User+"@"+t.Host) } } writeJSON(w, res) } // sshfsChanges are the operations that change files, for the audit log. var sshfsChanges = map[string]bool{"write": true, "mkdir": true, "delete": true, "deltree": true, "move": true, "copy": true, "chmod": true, "zip": true, "unzip": true} // POST /api/panel/agents/{id}/sshfs/{conn}/{op} - an operation on the host; // the body is in the shape of the machine's own fs.{op}. "close" drops the // connection. func (s *Server) handleSSHFSOp(w http.ResponseWriter, r *http.Request) { agentID, conn, op := r.PathValue("id"), r.PathValue("conn"), r.PathValue("op") body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 64<<20)) if err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if !s.fsAgent(w, agentID, true) { return } task, args := proto.TaskSSHFSOp, proto.SSHFSArgs{Conn: conn, Op: op, Args: json.RawMessage(body)} if op == "close" { task, args = proto.TaskSSHFSClose, proto.SSHFSArgs{Conn: conn} } if len(args.Args) == 0 { args.Args = json.RawMessage("{}") } if sshfsChanges[op] { var d struct{ Path, Src, Dst string } _ = json.Unmarshal(body, &d) if admin := s.currentAdmin(r); admin != nil { s.db.Audit(admin.ID, agentID, "sshfs."+op+":"+strings.TrimSpace(d.Path+" "+d.Src+" "+d.Dst)) } } res, err := s.waitTask(r.Context(), agentID, task, args, fsTaskTimeout) if err != nil { res = proto.TaskResult{Error: err.Error()} } if unknownOp(res) && op != "close" { res = proto.TaskResult{Error: "агент не умеет файлы по SSH - обновите его"} } writeJSON(w, res) } // GET /api/panel/agents/{id}/sshfs/{conn}/download?path=...[&inline=1] func (s *Server) handleSSHFSDownload(w http.ResponseWriter, r *http.Request) { s.download(w, r, proto.TaskSSHFSSend, r.PathValue("conn")) } // POST /api/panel/agents/{id}/sshfs/{conn}/upload?path=...&size=N func (s *Server) handleSSHFSUpload(w http.ResponseWriter, r *http.Request) { if admin := s.currentAdmin(r); admin != nil { s.db.Audit(admin.ID, r.PathValue("id"), "sshfs.upload:"+r.URL.Query().Get("path")) } s.uploadStart(w, r, proto.TaskSSHFSPut, r.PathValue("conn")) }