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