All checks were successful
goreleaser / goreleaser (push) Successful in 1m29s
232 lines
6.1 KiB
Go
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))
|
|
}
|