Files
lg/cmd/agent/main.go
Mans Ziesel 10f0183839
All checks were successful
goreleaser / goreleaser (push) Successful in 1m29s
add protocols option to lg
2025-09-07 15:44:42 +02:00

232 lines
6.1 KiB
Go

package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"git.mziesel.nl/mans/lg/internal/socket"
)
type LgAgentConfig struct {
Host string `json:"host"`
Port int `json:"port"`
SocketPath string `json:"socket_path"`
SharedSecret string `json:"shared_secret"`
}
var config LgAgentConfig
func loadConfig() LgAgentConfig {
// Default configuration
config = LgAgentConfig{
Host: "localhost",
Port: 3000,
SocketPath: "/var/run/bird/bird.ctl",
SharedSecret: "", // empty = no secret
}
// Determine the config file path
configPath := "/etc/lg-agent/agent.json"
if envPath := os.Getenv("LG_AGENT_CONFIG_PATH"); envPath != "" {
configPath = envPath
}
// Open the configuration file
file, err := os.Open(configPath)
if err != nil {
log.Fatalf("Error opening config file: %v.", err)
return config
}
defer file.Close()
// Decode the JSON configuration
decoder := json.NewDecoder(file)
if err := decoder.Decode(&config); err != nil {
log.Fatalf("Error decoding config file: %v.", err)
}
return config
}
func isAuthenticated(r *http.Request) bool {
if config.SharedSecret == "" {
return true
}
authHeader := r.Header.Get("Authorization")
return authHeader == fmt.Sprintf("Bearer %s", config.SharedSecret)
}
func ensureAuthenticatedMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !isAuthenticated(r) {
http.Error(w, "Unauthorized access", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
}
}
func plainTextMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
next(w, r)
}
}
func middlewareChain(h http.HandlerFunc) http.HandlerFunc {
return plainTextMiddleware(ensureAuthenticatedMiddleware(h))
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("status: ok"))
if err != nil {
log.Println(err)
}
}
func birdStatusHandler(w http.ResponseWriter, r *http.Request) {
s := socket.NewBirdSocket(config.SocketPath, 4092, true)
s.Connect()
defer s.Close()
status, _, err := s.Send("show status")
if err != nil {
http.Error(w, "Internal Server Error: failed to query router status", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte("status: " + string(status)))
if err != nil {
log.Println(err)
}
}
func birdCommandHandler(w http.ResponseWriter, r *http.Request) {
command := r.URL.Query().Get("c")
if command == "" {
http.Error(w, "Bad Request: command parameter is required", http.StatusBadRequest)
return
}
s := socket.NewBirdSocket(config.SocketPath, 4092, true)
s.Connect()
defer s.Close()
log.Printf("[INFO] executing command: \"%s\"", command)
output, _, err := s.Send(command)
if err != nil {
log.Printf("ERROR: failed to query bird: %s\n", err)
http.Error(w, "Internal Server Error: failed to query bird", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(string(output)))
if err != nil {
log.Println(err)
}
}
func createCommand(tool string, target string) (*exec.Cmd, error) {
commands := map[string][]string{
"ping": {"ping", "-4", "-c", "4", "--", target},
"ping6": {"ping", "-6", "-c", "4", "--", target},
"mtr": {"mtr", "--show-ips", "--aslookup", "--report-wide", "-c", "10", "-4", target},
"mtr6": {"mtr", "--show-ips", "--aslookup", "--report-wide", "-c", "10", "-6", target},
"traceroute": {"traceroute", "-4", "--", target},
"traceroute6": {"traceroute", "-6", "--", target},
}
if cmdArgs, exists := commands[tool]; exists {
path, err := exec.LookPath(cmdArgs[0])
if err != nil {
return nil, err
}
cmd := exec.Command(path, cmdArgs[1:]...)
return cmd, nil
}
return nil, errors.New("Command is not allowed")
}
func isValidTarget(target string) error {
addr, err := net.ResolveIPAddr("ip", target)
if err != nil {
return err
}
if addr.IP.IsPrivate() {
return errors.New("RFC 1918 and RFC 4193 address space is not allowed")
}
return nil
}
func executeStreaming(w http.ResponseWriter, r *http.Request) {
tool := r.URL.Query().Get("tool")
target := r.URL.Query().Get("target")
err := isValidTarget(target)
if err != nil {
http.Error(w, fmt.Sprintf("Bad Target: %s", err), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
cmd, err := createCommand(tool, target)
if err != nil {
http.Error(w, "Internal Server Error: failed to create command", http.StatusInternalServerError)
log.Printf("ERROR: Failed to create command: %s\n", err)
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
http.Error(w, "Internal Server Error: failed to open pipe to CMD output", http.StatusInternalServerError)
log.Printf("ERROR: Failed to open pipe to CMD output: %s\n", err)
return
}
cmd.Stderr = cmd.Stdout
err = cmd.Start()
if err != nil {
http.Error(w, "Internal Server Error: failed to start command", http.StatusInternalServerError)
log.Printf("ERROR: Failed to start command: %s\n", err)
return
}
scanner := bufio.NewScanner(stdout)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
fmt.Fprintf(w, "%s\n", scanner.Bytes())
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
func main() {
config := loadConfig()
formattedConfig, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("Error formatting config: %v", err)
}
log.Printf("Loaded configuration: %s\n", formattedConfig)
mux := http.NewServeMux()
mux.HandleFunc("/health", middlewareChain(healthHandler))
mux.HandleFunc("/bird/status", middlewareChain(birdStatusHandler))
mux.HandleFunc("/bird/command", middlewareChain(birdCommandHandler))
mux.HandleFunc("/exec", middlewareChain(executeStreaming))
log.Printf("starting listner on %s:%d\n", config.Host, config.Port)
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", config.Host, config.Port), mux))
}