feat: initial commit
This commit is contained in:
1 parent
353f62e257
commit
c6852c6f1b
62 files changed
+5911
-2
No files matched your search
@@ -0,0 +1,127 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
||||
)
|
||||
|
||||
// Long-poll hold window. Must stay below nginx proxy_read_timeout (120s).
|
||||
const pollHold = 25 * time.Second
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
// POST /api/agent/enroll — exchange the shared enroll secret for a per-agent id+token.
|
||||
func (s *Server) handleEnroll(w http.ResponseWriter, r *http.Request) {
|
||||
secret := r.Header.Get("X-Enroll-Secret")
|
||||
if !constantTimeEqual(secret, s.db.EnrollSecret()) {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var req proto.EnrollRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&req); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
id, token, err := s.enroll(req.Name, req.Hostname, req.OS, req.Arch, req.Version)
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, proto.EnrollResponse{AgentID: id, Token: token})
|
||||
}
|
||||
|
||||
// GET /api/agent/poll — control channel. Held until a job is queued or timeout.
|
||||
func (s *Server) handlePoll(w http.ResponseWriter, r *http.Request) {
|
||||
a := s.authenticateAgent(r)
|
||||
if a == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// The agent sends its version occasionally; record it when present.
|
||||
if v := r.Header.Get("X-Agent-Version"); v != "" && v != a.Version {
|
||||
_ = s.db.TouchAgentVersion(a.ID, v)
|
||||
} else {
|
||||
_ = s.db.TouchAgent(a.ID)
|
||||
}
|
||||
writeJSON(w, s.hub.WaitControl(a.ID, pollHold))
|
||||
}
|
||||
|
||||
// GET /api/agent/task/{id} — the agent fetches the task to run.
|
||||
func (s *Server) handleTaskFetch(w http.ResponseWriter, r *http.Request) {
|
||||
if s.authenticateAgent(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
env, ok := s.hub.GetTask(r.PathValue("id"))
|
||||
if !ok {
|
||||
http.Error(w, "no task", http.StatusGone)
|
||||
return
|
||||
}
|
||||
writeJSON(w, env)
|
||||
}
|
||||
|
||||
// POST /api/agent/task/{id}/result — the agent returns the task result.
|
||||
func (s *Server) handleTaskResult(w http.ResponseWriter, r *http.Request) {
|
||||
if s.authenticateAgent(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
var res proto.TaskResult
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<20)).Decode(&res); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.hub.CompleteTask(r.PathValue("id"), res)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GET /api/agent/term/{sid}/in — downstream (panel -> agent) long-poll.
|
||||
func (s *Server) handleTermIn(w http.ResponseWriter, r *http.Request) {
|
||||
if s.authenticateAgent(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
sess := s.hub.Session(r.PathValue("sid"))
|
||||
if sess == nil {
|
||||
http.Error(w, "no session", http.StatusGone)
|
||||
return
|
||||
}
|
||||
frames, alive := sess.PollIn(pollHold)
|
||||
if !alive {
|
||||
http.Error(w, "closed", http.StatusGone)
|
||||
return
|
||||
}
|
||||
writeJSON(w, proto.InBatch{Frames: frames})
|
||||
}
|
||||
|
||||
// POST /api/agent/term/{sid}/out — upstream (agent PTY -> panel).
|
||||
func (s *Server) handleTermOut(w http.ResponseWriter, r *http.Request) {
|
||||
if s.authenticateAgent(r) == nil {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
sess := s.hub.Session(r.PathValue("sid"))
|
||||
if sess == nil {
|
||||
http.Error(w, "no session", http.StatusGone)
|
||||
return
|
||||
}
|
||||
var msg proto.OutMessage
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&msg); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !sess.PushOut(msg) {
|
||||
http.Error(w, "closed", http.StatusGone)
|
||||
return
|
||||
}
|
||||
if msg.Exit {
|
||||
s.hub.CloseSession(sess.ID)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
Reference in new issue
Block a user