259 lines
6.1 KiB
Go
259 lines
6.1 KiB
Go
package server
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
|
)
|
|
|
|
// Hub is the in-memory broker between agents (HTTP long-polling) and the panel.
|
|
// It carries three kinds of work to an agent through its single control poll:
|
|
// terminal sessions, port-forward streams, and one-shot tasks (file manager).
|
|
// Nothing here is persisted.
|
|
type Hub struct {
|
|
mu sync.Mutex
|
|
|
|
// jobs holds control messages queued for an agent but not yet delivered via
|
|
// its control poll.
|
|
jobs map[string][]proto.ControlMessage
|
|
// ctrlWaiters wakes a blocked control poll the moment a job is enqueued.
|
|
ctrlWaiters map[string]chan struct{}
|
|
|
|
sessions map[string]*Session
|
|
tasks map[string]*taskEntry
|
|
}
|
|
|
|
// taskEntry is a pending one-shot task awaiting the agent's result.
|
|
type taskEntry struct {
|
|
env proto.TaskEnvelope
|
|
result chan proto.TaskResult
|
|
}
|
|
|
|
// Session is one live byte stream between the panel and an agent (a PTY terminal
|
|
// or a raw TCP forward — the hub treats both identically).
|
|
type Session struct {
|
|
ID string
|
|
AgentID string
|
|
|
|
mu sync.Mutex
|
|
inBuf []proto.InFrame
|
|
inSignal chan struct{} // size-1; pulsed when inBuf gains data
|
|
|
|
out chan proto.OutMessage // agent -> panel
|
|
|
|
closed chan struct{}
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
func NewHub() *Hub {
|
|
return &Hub{
|
|
jobs: map[string][]proto.ControlMessage{},
|
|
ctrlWaiters: map[string]chan struct{}{},
|
|
sessions: map[string]*Session{},
|
|
tasks: map[string]*taskEntry{},
|
|
}
|
|
}
|
|
|
|
// enqueueJob appends a control message for an agent and wakes its poll.
|
|
func (h *Hub) enqueueJob(agentID string, msg proto.ControlMessage) {
|
|
h.mu.Lock()
|
|
h.jobs[agentID] = append(h.jobs[agentID], msg)
|
|
if w := h.ctrlWaiters[agentID]; w != nil {
|
|
select {
|
|
case w <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
func (h *Hub) newSession(agentID string) *Session {
|
|
s := &Session{
|
|
ID: randToken()[:16],
|
|
AgentID: agentID,
|
|
inSignal: make(chan struct{}, 1),
|
|
out: make(chan proto.OutMessage, 1024),
|
|
closed: make(chan struct{}),
|
|
}
|
|
h.mu.Lock()
|
|
h.sessions[s.ID] = s
|
|
h.mu.Unlock()
|
|
return s
|
|
}
|
|
|
|
// CreateSession registers a terminal session and queues it for the agent.
|
|
func (h *Hub) CreateSession(agentID string) *Session {
|
|
s := h.newSession(agentID)
|
|
h.enqueueJob(agentID, proto.ControlMessage{Type: proto.CtrlOpenTerminal, SessionID: s.ID})
|
|
return s
|
|
}
|
|
|
|
// CreateForward registers a port-forward stream (agent dials 127.0.0.1:port).
|
|
func (h *Hub) CreateForward(agentID string, port int) *Session {
|
|
s := h.newSession(agentID)
|
|
h.enqueueJob(agentID, proto.ControlMessage{Type: proto.CtrlOpenForward, SessionID: s.ID, Port: port})
|
|
return s
|
|
}
|
|
|
|
// WaitControl blocks until a job is queued for agentID or timeout elapses.
|
|
func (h *Hub) WaitControl(agentID string, timeout time.Duration) proto.ControlMessage {
|
|
deadline := time.After(timeout)
|
|
for {
|
|
h.mu.Lock()
|
|
if q := h.jobs[agentID]; len(q) > 0 {
|
|
msg := q[0]
|
|
h.jobs[agentID] = q[1:]
|
|
h.mu.Unlock()
|
|
return msg
|
|
}
|
|
w := h.ctrlWaiters[agentID]
|
|
if w == nil {
|
|
w = make(chan struct{}, 1)
|
|
h.ctrlWaiters[agentID] = w
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
select {
|
|
case <-w:
|
|
case <-deadline:
|
|
return proto.ControlMessage{Type: proto.CtrlIdle}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Hub) Session(id string) *Session {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
return h.sessions[id]
|
|
}
|
|
|
|
func (h *Hub) CloseSession(id string) {
|
|
h.mu.Lock()
|
|
s := h.sessions[id]
|
|
delete(h.sessions, id)
|
|
h.mu.Unlock()
|
|
if s != nil {
|
|
s.close()
|
|
}
|
|
}
|
|
|
|
// --- one-shot tasks ---
|
|
|
|
// CreateTask queues a task and returns its id plus a channel for the result.
|
|
func (h *Hub) CreateTask(agentID, typ string, args []byte) (string, chan proto.TaskResult) {
|
|
id := randToken()[:16]
|
|
entry := &taskEntry{
|
|
env: proto.TaskEnvelope{Type: typ, Args: args},
|
|
result: make(chan proto.TaskResult, 1),
|
|
}
|
|
h.mu.Lock()
|
|
h.tasks[id] = entry
|
|
h.mu.Unlock()
|
|
h.enqueueJob(agentID, proto.ControlMessage{Type: proto.CtrlTask, TaskID: id})
|
|
return id, entry.result
|
|
}
|
|
|
|
// GetTask returns the envelope the agent should execute.
|
|
func (h *Hub) GetTask(id string) (proto.TaskEnvelope, bool) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
e := h.tasks[id]
|
|
if e == nil {
|
|
return proto.TaskEnvelope{}, false
|
|
}
|
|
return e.env, true
|
|
}
|
|
|
|
// CompleteTask delivers a result to the waiting panel handler.
|
|
func (h *Hub) CompleteTask(id string, res proto.TaskResult) {
|
|
h.mu.Lock()
|
|
e := h.tasks[id]
|
|
h.mu.Unlock()
|
|
if e != nil {
|
|
select {
|
|
case e.result <- res:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// DiscardTask drops a task from the registry.
|
|
func (h *Hub) DiscardTask(id string) {
|
|
h.mu.Lock()
|
|
delete(h.tasks, id)
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
func (s *Session) close() { s.closeOnce.Do(func() { close(s.closed) }) }
|
|
|
|
// Done returns a channel closed when the session ends.
|
|
func (s *Session) Done() <-chan struct{} { return s.closed }
|
|
|
|
// PushIn appends a downstream frame (panel -> agent) and wakes the agent poll.
|
|
func (s *Session) PushIn(f proto.InFrame) {
|
|
s.mu.Lock()
|
|
s.inBuf = append(s.inBuf, f)
|
|
s.mu.Unlock()
|
|
select {
|
|
case s.inSignal <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// PollIn drains queued downstream frames, blocking up to timeout for the first.
|
|
func (s *Session) PollIn(timeout time.Duration) ([]proto.InFrame, bool) {
|
|
deadline := time.After(timeout)
|
|
for {
|
|
s.mu.Lock()
|
|
if len(s.inBuf) > 0 {
|
|
b := s.inBuf
|
|
s.inBuf = nil
|
|
s.mu.Unlock()
|
|
return b, true
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
select {
|
|
case <-s.inSignal:
|
|
case <-deadline:
|
|
return nil, true
|
|
case <-s.closed:
|
|
return nil, false
|
|
}
|
|
}
|
|
}
|
|
|
|
// PushOut delivers agent output to the panel side. Returns false if closed.
|
|
func (s *Session) PushOut(m proto.OutMessage) bool {
|
|
select {
|
|
case s.out <- m:
|
|
return true
|
|
case <-s.closed:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (s *Session) Out() <-chan proto.OutMessage { return s.out }
|
|
|
|
// PollOut drains queued upstream messages, blocking up to timeout for the first.
|
|
func (s *Session) PollOut(timeout time.Duration) ([]proto.OutMessage, bool) {
|
|
var batch []proto.OutMessage
|
|
select {
|
|
case m := <-s.out:
|
|
batch = append(batch, m)
|
|
case <-time.After(timeout):
|
|
return nil, true
|
|
case <-s.closed:
|
|
return nil, false
|
|
}
|
|
for {
|
|
select {
|
|
case m := <-s.out:
|
|
batch = append(batch, m)
|
|
default:
|
|
return batch, true
|
|
}
|
|
}
|
|
}
|