mirror of
https://github.com//cppla/ServerStatus
synced 2026-08-06 11:03:57 +08:00
248 lines
6.7 KiB
Go
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
|
|
}
|