Files
2026-07-10 15:35:10 +08:00

248 lines
6.7 KiB
Go

package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"strings"
"time"
)
type AgentServer struct {
app *App
}
func NewAgentServer(app *App) *AgentServer {
return &AgentServer{app: app}
}
func (s *AgentServer) ListenAndServe() error {
listener, err := net.Listen("tcp", s.app.opts.AgentAddr)
if err != nil {
return err
}
s.app.logger.Printf("agent TCP listening on %s", listener.Addr())
return s.Serve(listener)
}
func (s *AgentServer) Serve(listener net.Listener) error {
defer listener.Close()
s.app.agentRunning.Store(true)
defer s.app.agentRunning.Store(false)
go func() {
<-s.app.ctx.Done()
_ = listener.Close()
}()
for {
conn, err := listener.Accept()
if err != nil {
if s.app.ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
go s.handleConnection(conn)
}
}
func (s *AgentServer) handleConnection(conn net.Conn) {
defer conn.Close()
if tcpConn, ok := conn.(*net.TCPConn); ok {
_ = tcpConn.SetNoDelay(true)
_ = tcpConn.SetKeepAlive(true)
_ = tcpConn.SetKeepAlivePeriod(10 * time.Second)
}
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
if _, err := io.WriteString(conn, "Authentication required:\n"); err != nil {
return
}
reader := bufio.NewReaderSize(conn, 64*1024)
credentials, err := reader.ReadString('\n')
if err != nil {
return
}
credentials = strings.TrimSpace(credentials)
parts := strings.SplitN(credentials, ":", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
_, _ = io.WriteString(conn, "Wrong username and/or password.\n")
return
}
family := remoteFamily(conn.RemoteAddr())
connectionID, monitors, apiErr := s.app.connectAgent(parts[0], parts[1], conn, family)
if apiErr != nil {
_, _ = io.WriteString(conn, apiErr.Message+"\n")
return
}
username := parts[0]
defer s.app.disconnectAgent(username, conn, connectionID)
if _, err := io.WriteString(conn, "Authentication successful. Access granted.\n"); err != nil {
return
}
// Existing Python agents use recv() instead of a line reader during the
// handshake. Keep the auth and metadata packets separate for compatibility.
time.Sleep(20 * time.Millisecond)
var metadata strings.Builder
fmt.Fprintf(&metadata, "You are connecting via: IPv%d\n", family)
for index, monitor := range monitors {
payload := map[string]any{"name": monitor.Name, "host": monitor.Host, "interval": monitor.Interval, "type": monitor.Type, "monitor": index}
data, _ := json.Marshal(payload)
metadata.Write(data)
metadata.WriteByte('\n')
}
if _, err := io.WriteString(conn, metadata.String()); err != nil {
return
}
_ = conn.SetDeadline(time.Now().Add(20 * time.Second))
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 4096), maxRequestBody)
for scanner.Scan() {
_ = conn.SetDeadline(time.Now().Add(20 * time.Second))
line := strings.TrimSpace(scanner.Text())
switch {
case strings.HasPrefix(line, "update"):
body := strings.TrimSpace(strings.TrimPrefix(line, "update"))
var update AgentStats
if err := json.Unmarshal([]byte(body), &update); err != nil {
if s.app.agentPong(username, connectionID) {
_, _ = io.WriteString(conn, "1\n")
}
continue
}
if !s.app.updateAgent(username, connectionID, update) {
return
}
if s.app.agentPong(username, connectionID) {
_, _ = io.WriteString(conn, "0\n")
}
case strings.HasPrefix(line, "pong"):
value := strings.TrimSpace(strings.TrimPrefix(line, "pong"))
s.app.setAgentPong(username, connectionID, value == "1" || strings.EqualFold(value, "on"))
default:
if s.app.agentPong(username, connectionID) {
_, _ = io.WriteString(conn, "1\n")
}
}
}
}
func remoteFamily(address net.Addr) int {
host, _, err := net.SplitHostPort(address.String())
if err == nil {
if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
return 6
}
}
return 4
}
func (a *App) connectAgent(username, password string, conn net.Conn, family int) (uint64, []MonitorConfig, *APIError) {
a.configMu.RLock()
defer a.configMu.RUnlock()
var config *ServerConfig
for index := range a.runtime.Servers {
if a.runtime.Servers[index].Username == username {
server := a.runtime.Servers[index]
config = &server
break
}
}
if config == nil || config.Password != password {
return 0, nil, &APIError{Status: 401, Message: "Wrong username and/or password."}
}
if config.Disabled {
return 0, nil, &APIError{Status: 403, Message: "Server is disabled."}
}
a.nodeMu.Lock()
defer a.nodeMu.Unlock()
node := a.nodes[username]
if node == nil {
return 0, nil, &APIError{Status: 404, Message: "Server is not configured."}
}
if node.Connected {
return 0, nil, &APIError{Status: 409, Message: "Only one connection per user allowed."}
}
id := a.connectionID.Add(1)
node.Connected = true
node.Connection = conn
node.ConnectionID = id
node.Family = family
node.HasUpdate = false
node.Pong = false
node.Online4 = family == 4
node.Online6 = family == 6
a.wakeStatsWriter()
return id, append([]MonitorConfig(nil), a.runtime.Monitors...), nil
}
func (a *App) disconnectAgent(username string, conn net.Conn, connectionID uint64) {
a.nodeMu.Lock()
node := a.nodes[username]
if node == nil || node.ConnectionID != connectionID || node.Connection != conn {
a.nodeMu.Unlock()
return
}
node.Connected = false
node.Connection = nil
node.Online4 = false
node.Online6 = false
node.HasUpdate = false
node.Pong = false
a.nodeMu.Unlock()
a.wakeStatsWriter()
time.AfterFunc(25*time.Second, func() {
if a.ctx.Err() != nil {
return
}
a.nodeMu.RLock()
current := a.nodes[username]
stillOffline := current != nil && !current.Connected && current.ConnectionID == connectionID
a.nodeMu.RUnlock()
if stillOffline {
a.evaluateWatchdogs(username, true)
}
})
}
func (a *App) updateAgent(username string, connectionID uint64, update AgentStats) bool {
a.nodeMu.Lock()
node := a.nodes[username]
if node == nil || !node.Connected || node.ConnectionID != connectionID {
a.nodeMu.Unlock()
return false
}
if update.Online4 != nil {
node.Online4 = *update.Online4
}
if update.Online6 != nil {
node.Online6 = *update.Online6
}
node.Stats = update
node.HasUpdate = true
node.LastUpdate = time.Now()
a.nodeMu.Unlock()
a.wakeStatsWriter()
a.evaluateWatchdogs(username, false)
return true
}
func (a *App) setAgentPong(username string, connectionID uint64, enabled bool) {
a.nodeMu.Lock()
defer a.nodeMu.Unlock()
if node := a.nodes[username]; node != nil && node.ConnectionID == connectionID {
node.Pong = enabled
}
}
func (a *App) agentPong(username string, connectionID uint64) bool {
a.nodeMu.RLock()
defer a.nodeMu.RUnlock()
node := a.nodes[username]
return node != nil && node.ConnectionID == connectionID && node.Pong
}