package server import ( "context" "html/template" "net/http" "slices" "time" "git.eeqj.de/sneak/routewatch/internal/database" "git.eeqj.de/sneak/routewatch/internal/routetable" "git.eeqj.de/sneak/routewatch/internal/templates" asinfo "git.eeqj.de/sneak/routewatch/pkg/asinfo" ) const ( // homeCardsRefreshInterval is how often the server works out the home // page's cards again. Some of their figures take a walk over the whole // routing table, so no request works them out. homeCardsRefreshInterval = time.Minute // homeCardsDatabaseTimeout is how long the database read behind the // cards of the newest and the oldest AS may take. homeCardsDatabaseTimeout = 4 * time.Second // maxListedEntries is how many entries the bogon and multiple-origin // pages list at most. maxListedEntries = 1000 ) // homeCards is what the home page's cards show. The bogon and multiple-origin // pages list the entries their cards count. The server works it out every // homeCardsRefreshInterval and serves this copy in between. type homeCards struct { routetable.Overview // NewestAS and OldestAS are the ASes the asns table last saw most // recently and longest ago. They are nil when the database did not // answer or holds no AS. NewestAS *database.ASN OldestAS *database.ASN // WellKnownNetworks are the networks of wellKnownASNs, newest update // first. WellKnownNetworks []wellKnownNetwork } // wellKnownNetwork is one network on the home page's card of well-known // networks. type wellKnownNetwork struct { ASN int // Name is its description in the AS list built into RouteWatch. Name string // Prefixes is how many prefixes it originates. Prefixes int // Newest is the time of its newest kept route, or nil when it keeps none. Newest *time.Time } // listPageData is what the bogon and multiple-origin pages are rendered with. type listPageData struct { // CSPNonce is the nonce the page's style block carries. CSPNonce string // Cards is the copy of the home page's cards, or nil before the server // has first worked them out. Cards *homeCards } // The AS numbers of the networks the home page lists as well known. const ( appleASN = 714 googleASN = 15169 amazonASN = 16509 microsoftASN = 8075 metaASN = 32934 cloudflareASN = 13335 akamaiASN = 20940 netflixASN = 2906 dodNetworkInfoCtrASN = 721 cogentASN = 174 hurricaneElectricASN = 6939 level3ASN = 3356 arelionASN = 1299 nttASN = 2914 mitASN = 3 ) // wellKnownASNs returns the AS numbers of the networks the home page lists as // well known: large companies, large transit networks, the US Department of // Defense's Network Information Center and a university. func wellKnownASNs() []int { return []int{ appleASN, googleASN, amazonASN, microsoftASN, metaASN, cloudflareASN, akamaiASN, netflixASN, dodNetworkInfoCtrASN, cogentASN, hurricaneElectricASN, level3ASN, arelionASN, nttASN, mitASN, } } // keepHomeCardsFresh works out the home page's cards now, and again every // interval until ctx ends. func (s *Server) keepHomeCardsFresh(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { s.refreshHomeCards(ctx) select { case <-ctx.Done(): return case <-ticker.C: } } } // refreshHomeCards works out the home page's cards and keeps them for the // pages to serve. When the database does not answer, the failure is logged // and the cards of the newest and the oldest AS stay empty. func (s *Server) refreshHomeCards(ctx context.Context) { cards := &homeCards{ Overview: s.table.Overview(maxListedEntries), WellKnownNetworks: s.wellKnownNetworks(), } ctx, cancel := context.WithTimeout(ctx, homeCardsDatabaseTimeout) defer cancel() newest, oldest, err := s.db.GetLastSeenASes(ctx) if err != nil { s.logger.Warn("Failed to read the newest and the oldest AS", "error", err) } else { cards.NewestAS, cards.OldestAS = newest, oldest } s.cards.Store(cards) } // wellKnownNetworks returns the networks of wellKnownASNs, each with how many // prefixes it originates and the time of its newest kept route. They come // newest first, and those that keep no route last. func (s *Server) wellKnownNetworks() []wellKnownNetwork { asns := wellKnownASNs() networks := make([]wellKnownNetwork, 0, len(asns)) for _, asn := range asns { originated := s.table.OriginatedPrefixes(asn) network := wellKnownNetwork{ASN: asn, Prefixes: len(originated)} if info, ok := asinfo.Get(asn); ok { network.Name = info.Description } for _, p := range originated { if p.Newest != nil && (network.Newest == nil || p.Newest.After(*network.Newest)) { network.Newest = p.Newest } } networks = append(networks, network) } slices.SortStableFunc(networks, func(a, b wellKnownNetwork) int { switch { case a.Newest == nil && b.Newest == nil: return 0 case a.Newest == nil: return 1 case b.Newest == nil: return -1 } return b.Newest.Compare(*a.Newest) }) return networks } // handleBogons returns a handler that serves the page listing the bogon // announcements. func (s *Server) handleBogons() http.HandlerFunc { return s.listPageHandler(templates.BogonsTemplate()) } // handleMultipleOrigins returns a handler that serves the page listing the // prefixes announced by more than one origin AS. func (s *Server) handleMultipleOrigins() http.HandlerFunc { return s.listPageHandler(templates.MultipleOriginsTemplate()) } // listPageHandler returns a handler that renders tmpl, a page that lists // entries the home page's cards count, from the copy of the cards. func (s *Server) listPageHandler(tmpl *template.Template) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") err := tmpl.Execute(w, listPageData{ CSPNonce: cspNonce(r.Context()), Cards: s.cards.Load(), }) if err != nil { s.logger.Error("Failed to render a list page", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } }