Files
lg/cmd/backend/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

459 lines
12 KiB
Go

package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"regexp"
"git.mziesel.nl/mans/lg/frontend"
)
type Config struct {
LG LGConfig `json:"lg"`
RateLimit RateLimitConfig `json:"rate_limit"`
Routers map[string]RouterConfig `json:"routers"`
}
type LGConfig struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
SharedSecret string `json:"shared_secret"`
}
type RateLimitConfig struct {
Requests int `json:"requests"`
TimeWindow int `json:"time_window"`
}
type RouterConfig struct {
Code string `json:"code"`
Description string `json:"description"`
IPv4 string `json:"ipv4"`
IPv6 string `json:"ipv6"`
Agent AgentConfig `json:"agent"`
}
type AgentConfig struct {
Host string `json:"host"`
Port int `json:"port"`
SharedSecret string `json:"shared_secret"`
}
type BrowserRouterConfig struct {
Code string `json:"code"`
Description string `json:"description"`
IPv4 string `json:"ipv4"`
IPv6 string `json:"ipv6"`
}
var config Config
var ValidDNSNameRegexp *regexp.Regexp
func init() {
ValidDNSNameRegexp = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`)
}
/*
Functions
*/
func loadConfig() Config {
// Default configuration
config = Config{}
// Determine the config file path
configPath := "/etc/lg-backend/backend.json"
if envPath := os.Getenv("LG_BACKEND_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.LG.SharedSecret == "" {
return true
}
authHeader := r.Header.Get("Authorization")
return authHeader == fmt.Sprintf("Bearer %s", config.LG.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 middlewareChain(h http.HandlerFunc) http.HandlerFunc {
return ensureAuthenticatedMiddleware(h)
}
func isValidTarget(target string) error {
ip := net.ParseIP(target)
isValidHostname := ValidDNSNameRegexp.Match([]byte(target))
if ip == nil && !isValidHostname {
return errors.New("invalid target: not a valid IP or hostname")
}
resolved_ip, err := net.ResolveIPAddr("ip", target)
if err != nil {
return errors.New("failed to resolve hostname")
}
if resolved_ip.IP.IsLoopback() {
return errors.New("loopback addresses are not allowed")
}
if resolved_ip.IP.IsPrivate() {
return errors.New("RFC 1918 and RFC 4193 address space is not allowed")
}
return nil
}
/*
Functions
*/
func getRouters() ([]BrowserRouterConfig, error) {
var routers []BrowserRouterConfig
// ensure no agent credentials get leaked
for _, r := range config.Routers {
router := BrowserRouterConfig{
Code: r.Code,
Description: r.Description,
IPv4: r.IPv4,
IPv6: r.IPv6,
}
routers = append(routers, router)
}
return routers, nil
}
func getRouterByCode(code string) (RouterConfig, error) {
for k, v := range config.Routers {
if k == code {
return v, nil
}
}
return RouterConfig{}, errors.New("not found")
}
func (r *RouterConfig) formatPath(path string) string {
ur := fmt.Sprintf("http://%s:%d%s", r.Agent.Host, r.Agent.Port, path)
return ur
}
func (r *RouterConfig) getStatus() ([]byte, error) {
res, err := http.Get(r.formatPath("/bird/status"))
if err != nil {
return nil, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return b, nil
}
func (r *RouterConfig) getPrefix(input string, all bool) ([]byte, error) {
_, _, err := net.ParseCIDR(input)
if err != nil {
testIp := net.ParseIP(input)
if testIp == nil {
return nil, errors.New("Invalid Prefix")
}
}
fs := ""
if all {
fs = " all "
}
cmd := url.QueryEscape(fmt.Sprintf("show route %s for %s\n", fs, input))
res, err := http.Get(r.formatPath(fmt.Sprintf("/bird/command?c=%s", cmd)))
if err != nil {
return nil, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return b, nil
}
func (r *RouterConfig) getProtocols(name string, all bool) ([]byte, error) {
fs := ""
if all {
fs += " all "
}
if name != "" {
fs += " '" + name + "' "
}
cmd := url.QueryEscape(fmt.Sprintf("show protocols%s", fs))
res, err := http.Get(r.formatPath(fmt.Sprintf("/bird/command?c=%s", cmd)))
if err != nil {
return nil, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return b, nil
}
/*
Handlers
*/
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 routersHandler(w http.ResponseWriter, r *http.Request) {
routers, err := getRouters()
if err != nil {
http.Error(w, fmt.Sprintf("Internal Server Error: %s", err), http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(routers); err != nil {
http.Error(w, fmt.Sprintf("Internal Server Error: failed to encode response: %s", err), http.StatusInternalServerError)
}
}
func routerStatusHandler(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
if code == "" {
http.Error(w, "Bad Request: no router selected", http.StatusNotFound)
return
}
router, err := getRouterByCode(code)
if err != nil {
http.Error(w, "Internal Server Error: failed to lookup router", http.StatusInternalServerError)
log.Printf("ERROR: failed to lookup router: %s\n", err)
return
}
status, err := router.getStatus()
if err != nil {
http.Error(w, "Internal Server Error: failed get router status", http.StatusInternalServerError)
log.Printf("ERROR: failed to get router status: %s\n", err)
return
}
if _, err := w.Write(status); err != nil {
http.Error(w, "Internal Server Error: failed to write data to client", http.StatusInternalServerError)
log.Printf("ERROR: failed to write data to client: %s\n", err)
return
}
}
func routerPrefixHandler(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
input := r.URL.Query().Get("prefix")
all := r.URL.Query().Get("all") == "true"
if code == "" {
http.Error(w, "Bad Request: no router selected", http.StatusBadRequest)
return
}
if input == "" {
http.Error(w, "Bad Request: no prefix supplied", http.StatusBadRequest)
return
}
router, err := getRouterByCode(code)
if err != nil {
http.Error(w, "Internal Server Error: failed to lookup router", http.StatusInternalServerError)
log.Printf("ERROR: failed to lookup router: %s\n", err)
return
}
// TODO: check if in the _all path handler
status, err := router.getPrefix(input, all)
if err != nil {
http.Error(w, "Internal Server Error: failed query router", http.StatusInternalServerError)
log.Printf("ERROR: failed to query router: %s\n", err)
return
}
if _, err := w.Write(status); err != nil {
http.Error(w, "Internal Server Error: failed to write data to client", http.StatusInternalServerError)
log.Printf("ERROR: failed to write data to client: %s\n", err)
return
}
}
func routerProtocolsHandler(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
all := r.URL.Query().Get("all") == "true"
proto := r.URL.Query().Get("proto")
if code == "" {
http.Error(w, "Bad Request: no router selected", http.StatusBadRequest)
return
}
router, err := getRouterByCode(code)
if err != nil {
http.Error(w, "Internal Server Error: failed to lookup router", http.StatusInternalServerError)
log.Printf("ERROR: failed to lookup router: %s\n", err)
return
}
protocols, err := router.getProtocols(proto, all)
if err != nil {
http.Error(w, "Internal Server Error: failed query router", http.StatusInternalServerError)
log.Printf("ERROR: failed to query router: %s\n", err)
return
}
if _, err := w.Write(protocols); err != nil {
http.Error(w, "Internal Server Error: failed to write data to client", http.StatusInternalServerError)
log.Printf("ERROR: failed to write data to client: %s\n", err)
return
}
}
func routerExecuteHandler(w http.ResponseWriter, r *http.Request) {
code := r.PathValue("code")
tool := r.URL.Query().Get("tool")
target := r.URL.Query().Get("target")
if code == "" {
http.Error(w, "Bad Request: no router selected", http.StatusBadRequest)
return
}
router, err := getRouterByCode(code)
if err != nil {
http.Error(w, "Internal Server Error: failed to lookup router", http.StatusInternalServerError)
log.Printf("ERROR: failed to lookup router: %s\n", err)
return
}
err = isValidTarget(target)
if err != nil {
http.Error(w, fmt.Sprintf("Bad Request: %s", err), http.StatusBadRequest)
return
}
path := router.formatPath(fmt.Sprintf("/exec?tool=%s&target=%s", url.QueryEscape(tool), url.QueryEscape(target)))
res, err := http.Get(path)
if err != nil {
http.Error(w, "Internal Server Error: failed to contact router", http.StatusInternalServerError)
log.Printf("ERROR: failed to contact router: %s\n", err)
return
}
scanner := bufio.NewScanner(res.Body)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
fmt.Fprintf(w, "%s\n", scanner.Bytes())
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
// type BrowserRouterConfig struct {
// Code string `json:"code"`
// Description string `json:"description"`
// IPv4 string `json:"ipv4"`
// IPv6 string `json:"ipv6"`
// }
//
type indexTemplateData struct {
Routers []BrowserRouterConfig
Tools []string
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
tpl, err := template.ParseFS(frontend.FrontendTemplateFS, "templates/*.html")
if err != nil {
http.Error(w, fmt.Sprintf("Internal Server Error: failed to load templates: %s", err), http.StatusInternalServerError)
return
}
routers, err := getRouters()
if err != nil {
http.Error(w, fmt.Sprintf("Internal Server Error: failed to load routers: %s", err), http.StatusInternalServerError)
return
}
data := indexTemplateData{
Routers: routers,
Tools: []string{"ping", "ping6", "traceroute", "traceroute6", "mtr", "mtr6", "bgp_prefixes", "bgp_prefixes_detailed", "protocols_raw", "protocols_raw_all"},
}
err = tpl.ExecuteTemplate(w, "index.html", data)
if err != nil {
http.Error(w, fmt.Sprintf("Internal Server Error: failed to write template to client: %s", err), http.StatusInternalServerError)
return
}
}
/*
Main
*/
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)
if err != nil {
log.Fatalf("Error loading templates: %s", err)
}
mux := http.NewServeMux()
mux.HandleFunc("/{$}", indexHandler)
mux.Handle("/static/", http.StripPrefix("/", http.FileServer(http.FS(frontend.FrontendStaticFS))))
mux.HandleFunc("/health", middlewareChain(healthHandler))
mux.HandleFunc("/api/routers", middlewareChain(routersHandler))
mux.HandleFunc("POST /api/{code}/status", middlewareChain(routerStatusHandler))
mux.HandleFunc("POST /api/{code}/prefixes", middlewareChain(routerPrefixHandler))
mux.HandleFunc("POST /api/{code}/protocols", middlewareChain(routerProtocolsHandler))
mux.HandleFunc("POST /api/{code}/execute", middlewareChain(routerExecuteHandler))
log.Printf("starting listner on %s:%d\n", config.LG.Host, config.LG.Port)
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", config.LG.Host, config.LG.Port), mux))
}