// Package server serves RouteWatch's web pages and JSON API: // - the home page, the status figures and the health check // - the AS, prefix and prefix length pages, and the JSON answers behind them // - the IP lookups // - the AS name search // // Every route it shows comes from the routing table in memory. AS names, // WHOIS details and the times a prefix and an AS were first seen come from // the database. package server import ( "context" "errors" "fmt" "net" "net/http" "net/netip" "strconv" "time" "git.eeqj.de/sneak/routewatch/internal/blocklist" "git.eeqj.de/sneak/routewatch/internal/config" "git.eeqj.de/sneak/routewatch/internal/database" "git.eeqj.de/sneak/routewatch/internal/logger" "git.eeqj.de/sneak/routewatch/internal/routetable" "git.eeqj.de/sneak/routewatch/internal/streamer" "github.com/go-chi/chi/v5" ) // ASNFetcherStats contains WHOIS fetcher statistics. type ASNFetcherStats struct { SuccessesLastHour int ErrorsLastHour int CurrentInterval time.Duration ConsecutiveFails int } // ASNFetcher is an interface for queuing ASN WHOIS lookups. type ASNFetcher interface { QueueImmediate(asn int) GetStats() ASNFetcherStats } // Blocklists tells which public blocklists list an address, from copies it // keeps in memory, as *blocklist.Blocklists does. type Blocklists interface { Lookup(addr netip.Addr) []blocklist.Listing ShortestInterval() time.Duration } // dnsResolver looks up the addresses of host names and the names of addresses // in DNS, as *net.Resolver does. type dnsResolver interface { LookupHost(ctx context.Context, host string) ([]string, error) LookupAddr(ctx context.Context, addr string) ([]string, error) } // Server provides HTTP endpoints for status monitoring type Server struct { router *chi.Mux db database.Store table *routetable.Table streamer *streamer.Streamer logger *logger.Logger port int srv *http.Server asnFetcher ASNFetcher blocklists Blocklists // trustedProxies are the reverse proxies whose X-Forwarded-For header // RealIPMiddleware believes trustedProxies []netip.Prefix // resolver makes the IP lookup's DNS lookups: net.DefaultResolver, or a // fake in tests resolver dnsResolver } // New creates a new HTTP server that reads routes from table, the routing // table that the prefix handler keeps current. func New( db database.Store, table *routetable.Table, streamer *streamer.Streamer, logger *logger.Logger, cfg *config.Config, ) *Server { s := &Server{ db: db, table: table, streamer: streamer, logger: logger, port: cfg.Port, trustedProxies: cfg.TrustedProxies, resolver: net.DefaultResolver, } s.setupRoutes() return s } // Start listens on the configured port and serves HTTP in the background. // It returns an error if it cannot listen, for example because the port is // already in use. ctx bounds only opening the port, not serving. func (s *Server) Start(ctx context.Context) error { const ( network = "tcp" readHeaderTimeout = 40 * time.Second readTimeout = 60 * time.Second writeTimeout = 60 * time.Second idleTimeout = 120 * time.Second ) s.srv = &http.Server{ Addr: ":" + strconv.Itoa(s.port), Handler: s.router, ReadHeaderTimeout: readHeaderTimeout, ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, } s.logger.Info("Starting HTTP server", "port", s.port, "addr", s.srv.Addr) var listenConfig net.ListenConfig listener, err := listenConfig.Listen(ctx, network, s.srv.Addr) if err != nil { return fmt.Errorf("starting HTTP server: %w", err) } s.logger.Info("HTTP server listening", "addr", listener.Addr().String()) go func() { err := s.srv.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { s.logger.Error("HTTP server error", "error", err) } }() return nil } // Stop stops the HTTP server. It waits, until ctx ends, for the requests in // progress to finish, and then closes the connections of those still running // and returns ctx's error. func (s *Server) Stop(ctx context.Context) error { if s.srv == nil { return nil } s.logger.Info("Stopping HTTP server") err := s.srv.Shutdown(ctx) if err != nil { return errors.Join(err, s.srv.Close()) } return nil } // SetASNFetcher sets the ASN WHOIS fetcher for on-demand lookups. Call it // before Start: the handlers read the fetcher without a lock. func (s *Server) SetASNFetcher(fetcher ASNFetcher) { s.asnFetcher = fetcher } // SetBlocklists sets the blocklists the IP lookups report. Call it before // Start: the handlers read the blocklists without a lock. func (s *Server) SetBlocklists(blocklists Blocklists) { s.blocklists = blocklists }