Files
lpra/internal/server/handlers_panel_term.go
T
2026-10-13 19:00:50 +03:00

80 lines
2.4 KiB
Go

package server
import (
"encoding/json"
"log"
"net/http"
"github.com/x-grk/longpolling-remote-access/internal/proto"
)
// The browser terminal talks to the server over plain HTTP long-polling (the
// same transport the agent uses), so it works through any reverse proxy without
// WebSocket upgrade support.
// POST /api/panel/term/{agentID}/open - create a terminal session.
func (s *Server) handlePanelOpen(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("agentID")
agent, err := s.db.GetAgent(agentID)
if err != nil || agent == nil {
log.Printf("panel open: agent %q not found (err=%v)", agentID, err)
http.NotFound(w, r)
return
}
sess := s.hub.CreateSession(agentID, agent.LoginUser)
if admin := s.currentAdmin(r); admin != nil {
s.db.Audit(admin.ID, agentID, "open_terminal:"+agent.TerminalUser())
}
writeJSON(w, map[string]string{"session_id": sess.ID})
}
// GET /api/panel/term/{sid}/out - long-poll for PTY output.
func (s *Server) handlePanelOut(w http.ResponseWriter, r *http.Request) {
sess := s.hub.Session(r.PathValue("sid"))
if sess == nil {
writeJSON(w, map[string]any{"closed": true})
return
}
msgs, alive := sess.PollOut(pollHold)
if !alive {
writeJSON(w, map[string]any{"closed": true})
return
}
writeJSON(w, map[string]any{"frames": msgs})
}
// panelInMsg is what the browser posts for keystrokes/resize.
type panelInMsg struct {
T string `json:"t"` // "i" input, "r" resize
D string `json:"d,omitempty"`
Cols uint16 `json:"cols,omitempty"`
Rows uint16 `json:"rows,omitempty"`
}
// POST /api/panel/term/{sid}/in - send keystrokes/resize to the agent PTY.
func (s *Server) handlePanelIn(w http.ResponseWriter, r *http.Request) {
sess := s.hub.Session(r.PathValue("sid"))
if sess == nil {
http.Error(w, "closed", http.StatusGone)
return
}
var m panelInMsg
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&m); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
switch m.T {
case "i":
sess.PushIn(proto.InFrame{Type: proto.FrameStdin, Data: m.D})
case "r":
sess.PushIn(proto.InFrame{Type: proto.FrameResize, Cols: m.Cols, Rows: m.Rows})
}
w.WriteHeader(http.StatusNoContent)
}
// POST /api/panel/term/{sid}/close - tear the session down.
func (s *Server) handlePanelClose(w http.ResponseWriter, r *http.Request) {
s.hub.CloseSession(r.PathValue("sid"))
w.WriteHeader(http.StatusNoContent)
}