80 lines
2.4 KiB
Go
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)
|
|
if admin := s.currentAdmin(r); admin != nil {
|
|
s.db.Audit(admin.ID, agentID, "open_terminal")
|
|
}
|
|
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)
|
|
}
|