diff --git a/example.config.yaml b/example.config.yaml index 8652a7e..9dc1e01 100644 --- a/example.config.yaml +++ b/example.config.yaml @@ -19,6 +19,11 @@ grpc: # gRPC cert: "/path/to/cert" # certificate path (becomes optional if self_signed is true) key: "/path/to/key" # private key path (becomes optional if self_signed is true) +redis: # Redis Cache Settings + enabled: true # Enable or disable cache + ttl: 5m # Cache TTL in seconds + uri: "redis://redis-svc:6379/0?protocol=3" # Redis host URI + web: # WebUI Settings, Most if not all of these are entirely optional enabled: true # Enable or disable web interface grpc_url: "" # URI of the GRCP server; uses current host as viewed by the browser if not set (optional) diff --git a/go.mod b/go.mod index a088ecb..d1363e5 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,17 @@ go 1.22 require ( connectrpc.com/connect v1.16.2 connectrpc.com/grpchealth v1.3.0 - github.com/rs/cors v1.11.0 - golang.org/x/crypto v0.25.0 - golang.org/x/net v0.27.0 + github.com/redis/go-redis/v9 v9.6.1 + github.com/rs/cors v1.11.1 + golang.org/x/crypto v0.26.0 + golang.org/x/net v0.28.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v2 v2.4.0 ) require ( - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index fd2da5c..876e618 100644 --- a/go.sum +++ b/go.sum @@ -2,20 +2,30 @@ connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= connectrpc.com/grpchealth v1.3.0 h1:FA3OIwAvuMokQIXQrY5LbIy8IenftksTP/lG4PbYN+E= connectrpc.com/grpchealth v1.3.0/go.mod h1:3vpqmX25/ir0gVgW6RdnCPPZRcR6HvqtXX5RNPmDXHM= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/protobuf/lookingglass/v0/lookingglass.proto b/protobuf/lookingglass/v0/lookingglass.proto index 006d6c3..4948e43 100644 --- a/protobuf/lookingglass/v0/lookingglass.proto +++ b/protobuf/lookingglass/v0/lookingglass.proto @@ -74,12 +74,16 @@ message GetRoutersResponse { // The next page token. uint32 next_page = 2; + + // Age of Response + google.protobuf.Timestamp timestamp = 3; } // PingRequest is the request message for Ping. message PingRequest { // The ID of the router. int64 router_id = 1; + // The IP address to ping. string target = 2; } @@ -88,12 +92,16 @@ message PingRequest { message PingResponse { // The result of the ping. string result = 1; + + // Age of Response + google.protobuf.Timestamp timestamp = 2; } // TracerouteRequest is the request message for Traceroute. message TracerouteRequest { // The ID of the router. int64 router_id = 1; + // The IP address to traceroute. string target = 2; } @@ -102,6 +110,9 @@ message TracerouteRequest { message TracerouteResponse { // The result of the traceroute. string result = 1; + + // Age of Response + google.protobuf.Timestamp timestamp = 2; } // BGPSummaryRequest is the request message for BGPSummary. @@ -114,12 +125,16 @@ message BGPSummaryRequest { message BGPSummaryResponse { // The BGP summary. string result = 1; + + // Age of Response + google.protobuf.Timestamp timestamp = 2; } // BGPRouteRequest is the request message for BGPRoute. message BGPRouteRequest { // The ID of the router. int64 router_id = 1; + // The IP address to look up. string target = 2; } @@ -128,6 +143,9 @@ message BGPRouteRequest { message BGPRouteResponse { // The BGP route. string result = 1; + + // Age of Response + google.protobuf.Timestamp timestamp = 2; } // BGPCommunityRequest is the request message for BGPCommunity. @@ -142,12 +160,16 @@ message BGPCommunityRequest { message BGPCommunityResponse { // The BGP community. string result = 1; + + // Age of Response + google.protobuf.Timestamp timestamp = 2; } // BGPASPathRequest is the request message for BGPASPath. message BGPASPathRequest { // The ID of the router. int64 router_id = 1; + // The IP address to look up. string pattern = 2; } @@ -156,5 +178,7 @@ message BGPASPathRequest { message BGPASPathResponse { // The BGP AS path. string result = 1; -} + // Age of Response + google.protobuf.Timestamp timestamp = 2; +} diff --git a/protobuf/package-lock.json b/protobuf/package-lock.json index 11842ed..a1f21d9 100644 --- a/protobuf/package-lock.json +++ b/protobuf/package-lock.json @@ -8,14 +8,15 @@ "name": "@as203038/lg-protobuf", "version": "0.0.1", "dependencies": { - "@bufbuild/protobuf": "^2.0.0", + "@bufbuild/protobuf": "^1.10.0", "typescript": "^5.5.4" } }, "node_modules/@bufbuild/protobuf": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.0.0.tgz", - "integrity": "sha512-sw2JhwJyvyL0zlhG61aDzOVryEfJg2PDZFSV7i7IdC7nAE41WuXCru3QWLGiP87At0BMzKOoKO/FqEGoKygGZQ==" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/typescript": { "version": "5.5.4", diff --git a/server/http/grpc/grpc.go b/server/http/grpc/grpc.go index 127e85a..d443b58 100644 --- a/server/http/grpc/grpc.go +++ b/server/http/grpc/grpc.go @@ -5,9 +5,7 @@ import ( "log" "net/http" "time" - "unsafe" - "connectrpc.com/connect" "connectrpc.com/grpchealth" "github.com/AS203038/looking-glass/protobuf/lookingglass/v0/lookingglassconnect" "github.com/AS203038/looking-glass/server/utils" @@ -15,46 +13,8 @@ import ( var Health = grpchealth.NewStaticChecker(lookingglassconnect.LookingGlassServiceName) -func NewLogInterceptor() connect.UnaryInterceptorFunc { - interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { - return connect.UnaryFunc(func( - ctx context.Context, - req connect.AnyRequest, - ) (connect.AnyResponse, error) { - start := time.Now() - - // Call the original handler - r, e := next(ctx, req) - - // Log the request - n := 200 - if e != nil { - n = 500 - } - p := req.Header().Get("X-Forwarded-For") - if p == "" { - p = req.Peer().Addr - } - log.Printf("%s \"%s %s %s\" %d %d \"%s\" \"%s\" %s", - p, - req.HTTPMethod(), - req.Spec().Procedure, - req.Peer().Protocol, - n, - unsafe.Sizeof(req.Any()), - req.Header().Get("Referer"), - req.Header().Get("User-Agent"), - time.Since(start), - ) - return r, e - }) - } - return connect.UnaryInterceptorFunc(interceptor) -} - func Mux(ctx context.Context, mux *http.ServeMux, rts utils.RouterMap) { - interceptors := connect.WithInterceptors(NewLogInterceptor()) - mux.Handle(lookingglassconnect.NewLookingGlassServiceHandler(NewLookingGlassService(ctx, rts), interceptors)) + mux.Handle(lookingglassconnect.NewLookingGlassServiceHandler(NewLookingGlassService(ctx, rts))) mux.Handle(grpchealth.NewHandler(Health)) Health.SetStatus(lookingglassconnect.LookingGlassServiceName, grpchealth.StatusServing) go healthcheck(ctx, rts) @@ -72,12 +32,12 @@ func healthcheck(ctx context.Context, rts utils.RouterMap) { if err := r.Healthcheck(); err == nil { if !o { Health.SetStatus(lookingglassconnect.LookingGlassServiceName+"/"+r.Config.Name, grpchealth.StatusServing) - log.Printf("Router %s is healthy", r.Config.Name) + log.Printf("NOTICE: Router %s is healthy", r.Config.Name) } } else { if o { Health.SetStatus(lookingglassconnect.LookingGlassServiceName+"/"+r.Config.Name, grpchealth.StatusNotServing) - log.Printf("Router %s is unhealthy: %s", r.Config.Name, err) + log.Printf("WARNING: Router %s is unhealthy: %s", r.Config.Name, err) } } } diff --git a/server/http/grpc/service.go b/server/http/grpc/service.go index 268a1b6..5c8fb44 100644 --- a/server/http/grpc/service.go +++ b/server/http/grpc/service.go @@ -5,6 +5,7 @@ import ( "os" "strconv" "strings" + "time" "connectrpc.com/connect" pb "github.com/AS203038/looking-glass/protobuf/lookingglass/v0" @@ -97,8 +98,13 @@ func (s *LookingGlassService) Ping(ctx context.Context, req *connect.Request[pb. if err != nil { return nil, err } + ts := time.Now() return connect.NewResponse(&pb.PingResponse{ Result: strings.Join(ret, "\n"), + Timestamp: ×tamppb.Timestamp{ + Seconds: ts.Unix(), + Nanos: int32(ts.Nanosecond()), + }, }), nil } @@ -116,8 +122,13 @@ func (s *LookingGlassService) Traceroute(ctx context.Context, req *connect.Reque if err != nil { return nil, err } + ts := time.Now() return connect.NewResponse(&pb.TracerouteResponse{ Result: strings.Join(ret, "\n"), + Timestamp: ×tamppb.Timestamp{ + Seconds: ts.Unix(), + Nanos: int32(ts.Nanosecond()), + }, }), nil } @@ -135,8 +146,13 @@ func (s *LookingGlassService) BGPRoute(ctx context.Context, req *connect.Request if err != nil { return nil, err } + ts := time.Now() return connect.NewResponse(&pb.BGPRouteResponse{ Result: strings.Join(ret, "\n"), + Timestamp: ×tamppb.Timestamp{ + Seconds: ts.Unix(), + Nanos: int32(ts.Nanosecond()), + }, }), nil } @@ -151,8 +167,13 @@ func (s *LookingGlassService) BGPCommunity(ctx context.Context, req *connect.Req if err != nil { return nil, err } + ts := time.Now() return connect.NewResponse(&pb.BGPCommunityResponse{ Result: strings.Join(ret, "\n"), + Timestamp: ×tamppb.Timestamp{ + Seconds: ts.Unix(), + Nanos: int32(ts.Nanosecond()), + }, }), nil } @@ -170,7 +191,12 @@ func (s *LookingGlassService) BGPASPath(ctx context.Context, req *connect.Reques if err != nil { return nil, err } + ts := time.Now() return connect.NewResponse(&pb.BGPASPathResponse{ Result: strings.Join(ret, "\n"), + Timestamp: ×tamppb.Timestamp{ + Seconds: ts.Unix(), + Nanos: int32(ts.Nanosecond()), + }, }), nil } diff --git a/server/http/http.go b/server/http/http.go index a9e1bb1..f330968 100644 --- a/server/http/http.go +++ b/server/http/http.go @@ -1,8 +1,13 @@ package http import ( + "bytes" "context" + "crypto/md5" "crypto/tls" + "encoding/json" + "fmt" + "io" "io/fs" "log" "net/http" @@ -11,14 +16,18 @@ import ( "github.com/AS203038/looking-glass/server/http/grpc" "github.com/AS203038/looking-glass/server/http/webui" "github.com/AS203038/looking-glass/server/utils" + "github.com/redis/go-redis/v9" "github.com/rs/cors" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) +var redisClient *redis.Client + type httpwriter struct { http.ResponseWriter Status int + Body []byte } func (w *httpwriter) Header() http.Header { @@ -31,10 +40,68 @@ func (w *httpwriter) WriteHeader(status int) { } func (w *httpwriter) Write(b []byte) (int, error) { + w.Body = append(w.Body, b...) return w.ResponseWriter.Write(b) } -func HTTPHandler(h http.Handler) http.Handler { +type CacheEnrty struct { + Body []byte `json:"body"` + Status int `json:"status"` + Header http.Header `json:"header"` +} + +func cacheHandler(cfg utils.RedisConfig, h http.Handler) http.Handler { + ttl, err := time.ParseDuration(cfg.TTL) + if err != nil { + log.Println("WARNING: Failed to parse TTL:", err, "using default of 60 seconds") + ttl = 60 * time.Second + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bd, _ := io.ReadAll(r.Body) + // reset the body so it can be read again + r.Body = io.NopCloser(bytes.NewReader(bd)) + cacheKey := fmt.Sprintf("%x", md5.Sum([]byte(r.URL.Path+string(bd)))) + cachedResponse, err := redisClient.Get(context.Background(), cacheKey).Result() + if err == nil { + var cacheEntry CacheEnrty + err = json.Unmarshal([]byte(cachedResponse), &cacheEntry) + if err == nil { + w.Header().Set("X-Cache", "HIT") + for k, v := range cacheEntry.Header { + w.Header()[k] = v + } + if cacheEntry.Status > 0 { + w.WriteHeader(cacheEntry.Status) + } + w.Write(cacheEntry.Body) + return + } + } + + // If the response is not cached, execute the handler and cache the response + responseWriter := &httpwriter{ResponseWriter: w} + h.ServeHTTP(responseWriter, r) + + go func() { + cacheEntry := CacheEnrty{ + Body: responseWriter.Body, + Status: responseWriter.Status, + Header: responseWriter.Header(), + } + cachejson, err := json.Marshal(cacheEntry) + if err != nil { + log.Println("ERROR: Failed to marshal cache entry:", err) + return + } + _, err = redisClient.Set(context.Background(), cacheKey, cachejson, ttl).Result() + if err != nil { + log.Println("ERROR: Failed to cache response:", err) + } + }() // Don't wait for the cache to complete + }) +} + +func loggingHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() @@ -54,7 +121,8 @@ func HTTPHandler(h http.Handler) http.Handler { if p == "" { p = r.RemoteAddr } - log.Printf("%s \"%s %s %s\" %d %d \"%s\" \"%s\" %s", + c := wr.Header().Get("X-Cache") + log.Printf("%s \"%s %s %s\" %d %d \"%s\" \"%s\" %s %s", p, r.Method, r.RequestURI, @@ -64,6 +132,7 @@ func HTTPHandler(h http.Handler) http.Handler { r.Referer(), r.UserAgent(), time.Since(start), + c, ) }) } @@ -81,11 +150,11 @@ func ListenAndServe(ctx context.Context, cfg *utils.Config, rts utils.RouterMap, grpc.Mux(ctx, mux, rts) } if cfg.SecurityTxt.Enabled { - mux.Handle("/.well-known/security.txt", HTTPHandler(SecurityTxtInjector(cfg.SecurityTxt))) + mux.Handle("/.well-known/security.txt", SecurityTxtInjector(cfg.SecurityTxt)) } if cfg.Web.Enabled { - mux.Handle("/_app/env.js", HTTPHandler(webui.ConfigInjector(cfg.Web))) - mux.Handle("/", HTTPHandler(http.FileServerFS(webfs))) + mux.Handle("/_app/env.js", webui.ConfigInjector(cfg.Web)) + mux.Handle("/", http.FileServerFS(webfs)) } corsHandler := cors.New(cors.Options{ Debug: false, @@ -114,17 +183,33 @@ func ListenAndServe(ctx context.Context, cfg *utils.Config, rts utils.RouterMap, "Grpc-Message", // Required for gRPC-web }, }) - handler := corsHandler.Handler(mux) + + var handler http.Handler = mux + + if cfg.Redis.Enabled { + // Create a Redis client + opts, err := redis.ParseURL(cfg.Redis.URI) + if err != nil { + log.Println("ERROR: Failed to parse Redis URL:", err) + } else { + log.Println("NOTICE: Connecting to Redis at", cfg.Redis.URI) + redisClient = redis.NewClient(opts) + // Wrap the existing handler with the cache middleware + handler = cacheHandler(cfg.Redis, handler) + } + } + + handler = loggingHandler(corsHandler.Handler(handler)) srv := &http.Server{ Addr: cfg.Grpc.Listen, ErrorLog: log.Default(), } if cfg.Grpc.TLS.Enabled { - log.Printf("Listening on %s with TLS", cfg.Grpc.Listen) + log.Printf("NOTICE: Listening on %s with TLS", cfg.Grpc.Listen) srv.Handler = handler if cfg.Grpc.TLS.SelfSigned { - log.Printf("Using self-signed certificate") + log.Printf("NOTICE: Using self-signed certificate") key, crt, err := utils.GenerateSelfSignedPair() if err != nil { panic(err) @@ -137,11 +222,11 @@ func ListenAndServe(ctx context.Context, cfg *utils.Config, rts utils.RouterMap, } srv.ListenAndServeTLS("", "") } else { - log.Printf("Using certificate %s", cfg.Grpc.TLS.Cert) + log.Printf("NOTICE: Using certificate %s", cfg.Grpc.TLS.Cert) srv.ListenAndServeTLS(cfg.Grpc.TLS.Cert, cfg.Grpc.TLS.Key) } } else { - log.Printf("Listening on %s", cfg.Grpc.Listen) + log.Printf("NOTICE: Listening on %s", cfg.Grpc.Listen) srv.Handler = h2c.NewHandler(handler, &http2.Server{}) srv.ListenAndServe() } diff --git a/server/routers/routers.go b/server/routers/routers.go index d0d7098..1866d7d 100644 --- a/server/routers/routers.go +++ b/server/routers/routers.go @@ -12,10 +12,10 @@ var ( func register(name string, rt utils.Router) bool { if name == "" { - log.Panicln("Router name cannot be empty") + log.Panicln("ERROR: Router name cannot be empty") } if _, ok := _routers[name]; ok { - log.Panicf("Router %s already registered", name) + log.Panicf("WARNING: Router %s already registered", name) } _routers[name] = rt return true @@ -33,7 +33,7 @@ func CreateRouterMap(cfg *utils.Config) utils.RouterMap { for _, v := range cfg.Devices { rt := Get(v.Type) if rt == nil { - log.Printf("Router Type %s not found (%s)\n", v.Type, v.Name) + log.Printf("ERROR: Router Type %s not found (%s)\n", v.Type, v.Name) continue } ri := &utils.RouterInstance{ diff --git a/server/routers/yaml.go b/server/routers/yaml.go index a1fdd97..c3c3e44 100644 --- a/server/routers/yaml.go +++ b/server/routers/yaml.go @@ -60,9 +60,9 @@ func init() { // Load all routers from ROUTER_DIR files, err := os.ReadDir(rd) if err != nil { - log.Panicf("Could not Read directory %s: %+v", rd, err) + log.Panicf("ERROR: Could not Read directory %s: %+v", rd, err) } - log.Printf("Loading routers from %s\n", rd) + log.Printf("NOTICE: Loading routers from %s\n", rd) for _, file := range files { if file.IsDir() || (!strings.HasSuffix(file.Name(), ".yml") && !strings.HasSuffix(file.Name(), ".yaml")) { continue @@ -71,41 +71,41 @@ func init() { y := &Yaml{Path: file.Name()} yamlFile, err := os.ReadFile(rd + "/" + y.Path) if err != nil { - log.Panicf("Could not Read file %s/%s: %+v", rd, y.Path, err) + log.Panicf("ERROR: Could not Read file %s/%s: %+v", rd, y.Path, err) } err = yaml.Unmarshal(yamlFile, &y.Template) if err != nil { - log.Panicf("Could not Unmarshal file %s/%s: %+v", rd, y.Path, err) + log.Panicf("ERROR: Could not Unmarshal file %s/%s: %+v", rd, y.Path, err) } if y.Template.Name == "" { - log.Printf("Router name cannot be empty (%s/%s)", rd, y.Path) + log.Printf("ERROR: Router name cannot be empty (%s/%s)", rd, y.Path) continue } register(y.Template.Name, y) - log.Printf("Router %s (%s/%s) registered\n", y.Template.Name, rd, y.Path) + log.Printf("NOTICE: Router %s (%s/%s) registered\n", y.Template.Name, rd, y.Path) } } // Load all bundled routers files, err := compiledRouters.ReadDir(".") if err != nil { - log.Panicf("Could not Read builtin directory: %+v", err) + log.Panicf("ERROR: Could not Read builtin directory: %+v", err) } for _, file := range files { y := &Yaml{Path: file.Name()} yamlFile, err := compiledRouters.ReadFile(y.Path) if err != nil { - log.Panicf("Could not Read file builtin:%s: %+v", y.Path, err) + log.Panicf("ERROR: Could not Read file builtin:%s: %+v", y.Path, err) } err = yaml.Unmarshal(yamlFile, &y.Template) if err != nil { - log.Panicf("Could not Unmarshal file builtin:%s: %+v", y.Path, err) + log.Panicf("ERROR: Could not Unmarshal file builtin:%s: %+v", y.Path, err) } if _, ok := _routers[y.Template.Name]; !ok { register(y.Template.Name, y) - log.Printf("Router %s (builtin:%s) registered\n", y.Template.Name, y.Path) + log.Printf("NOTICE: Router %s (builtin:%s) registered\n", y.Template.Name, y.Path) } else { - log.Printf("Router %s already registered\n", y.Template.Name) + log.Printf("WARNING: Router %s already registered\n", y.Template.Name) } } } diff --git a/server/server.go b/server/server.go index 8a6dd37..de13973 100644 --- a/server/server.go +++ b/server/server.go @@ -13,9 +13,9 @@ import ( func Start(ctx context.Context, web fs.FS) { cfg, err := utils.ParseConfigYaml("config.yaml") if err != nil { - log.Fatalf("Failed to parse config: %v\n", err) + log.Fatalf("ERROR: Failed to parse config: %v\n", err) } rm := routers.CreateRouterMap(cfg) http.ListenAndServe(ctx, cfg, rm, web) - log.Println("Goodbye, World!") + log.Println("NOTICE: Goodbye, World!") } diff --git a/server/utils/config.go b/server/utils/config.go index 9ee81f3..670e894 100644 --- a/server/utils/config.go +++ b/server/utils/config.go @@ -24,6 +24,7 @@ type Config struct { Grpc GrpcConfig `yaml:"grpc"` Web WebConfig `yaml:"web"` SecurityTxt SecurityTxtConfig `yaml:"security.txt"` + Redis RedisConfig `yaml:"redis"` } type RouterConfig struct { @@ -52,6 +53,12 @@ type TLSConfig struct { SelfSigned bool `yaml:"self_signed"` } +type RedisConfig struct { + Enabled bool `yaml:"enabled"` + URI string `yaml:"uri"` + TTL string `yaml:"ttl"` +} + type WebConfig struct { Enabled bool `yaml:"enabled"` GrpcURL string `yaml:"grpc_url"` diff --git a/server/utils/ssh.go b/server/utils/ssh.go index add916f..c2bd1a8 100644 --- a/server/utils/ssh.go +++ b/server/utils/ssh.go @@ -36,7 +36,6 @@ func SSHExec(router *RouterConfig, cmd []string) ([]string, error) { if err != nil { return nil, errs.ExecFailed } - // log.Println(c) output, err := session.Output(c) if err != nil { return nil, errs.ExecFailed diff --git a/webui/package-lock.json b/webui/package-lock.json index e5eccd4..86e2a89 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -39,7 +39,7 @@ "dev": true, "dependencies": { "@bufbuild/protobuf": "^1.10.0", - "typescript": "^5.5.2" + "typescript": "^5.5.4" } }, "node_modules/@alloc/quick-lru": { diff --git a/webui/src/lib/components/execCommand.svelte b/webui/src/lib/components/execCommand.svelte index 7e5dc09..dad45bd 100644 --- a/webui/src/lib/components/execCommand.svelte +++ b/webui/src/lib/components/execCommand.svelte @@ -7,7 +7,7 @@ export let routers: Pb.Router[]; - let outputs: Record = {}; + let outputs: Record = {}; let fire: boolean = false; let _command: string; let _parameter: string; @@ -19,8 +19,10 @@ _command = command; _parameter = parameter; for (let router of routers) { - outputs[router.id.toString()] = - router.name + " $ " + _command + " '" + _parameter + "'\n"; + outputs[router.id.toString()] = { + result: router.name + " $ " + _command + " '" + _parameter + "'\n", + timestamp: 0, + } switch (command) { case "ping": LookingGlassClient() @@ -29,10 +31,11 @@ target: parameter, }) .then((res) => { - outputs[router.id.toString()] += res.result; + outputs[router.id.toString()].result = res.result; + outputs[router.id.toString()].timestamp = new Date(parseInt(res.timestamp.seconds.toString()) * 1000); }) .catch((err) => { - outputs[router.id.toString()] += err; + outputs[router.id.toString()].result += err; }); break; case "traceroute": @@ -42,10 +45,11 @@ target: parameter, }) .then((res) => { - outputs[router.id.toString()] += res.result; + outputs[router.id.toString()].result = res.result; + outputs[router.id.toString()].timestamp = new Date(parseInt(res.timestamp.seconds.toString()) * 1000); }) .catch((err) => { - outputs[router.id.toString()] += err; + outputs[router.id.toString()].result += err; }); break; case "bgp_route": @@ -55,10 +59,11 @@ target: parameter, }) .then((res) => { - outputs[router.id.toString()] += res.result; + outputs[router.id.toString()].result = res.result; + outputs[router.id.toString()].timestamp = new Date(parseInt(res.timestamp.seconds.toString()) * 1000); }) .catch((err) => { - outputs[router.id.toString()] += err; + outputs[router.id.toString()].result += err; }); break; case "bgp_community": @@ -71,10 +76,11 @@ }, }) .then((res) => { - outputs[router.id.toString()] += res.result; + outputs[router.id.toString()].result = res.result; + outputs[router.id.toString()].timestamp = new Date(parseInt(res.timestamp.seconds.toString()) * 1000); }) .catch((err) => { - outputs[router.id.toString()] += err; + outputs[router.id.toString()].result += err; }); break; case "bgp_aspath_regex": @@ -84,10 +90,11 @@ pattern: parameter, }) .then((res) => { - outputs[router.id.toString()] += res.result; + outputs[router.id.toString()].result = res.result; + outputs[router.id.toString()].timestamp = new Date(parseInt(res.timestamp.seconds.toString()) * 1000); }) .catch((err) => { - outputs[router.id.toString()] += err; + outputs[router.id.toString()].result += err; }); break; default: @@ -117,7 +124,7 @@

Location: {router.location}

- {#if outputs[router.id.toString()] == "" || outputs[router.id.toString()] == router.name + " $ " + _command + " '" + _parameter + "'\n"} + {#if outputs[router.id.toString()]?.result == "" || outputs[router.id.toString()]?.result == router.name + " $ " + _command + " '" + _parameter + "'\n"}
{outputs[ router.id.toString() - ]} + ]?.result} +
+                            Timestamp: {outputs[router.id.toString()]?.timestamp.toISOString()}
+