diff --git a/check.go b/check.go index a9e587d..17414a5 100644 --- a/check.go +++ b/check.go @@ -15,19 +15,27 @@ import ( ) var ( - ErrHttpsRedirect = errors.New("unexpected forced https redirect") - ErrHttpRedirect = errors.New("unexpected redirect to insecure url") - ErrCertExpired = errors.New("certificate is expired") + // ErrHTTPSRedirect is an error thrown when the webserver returns + // an https redirect for an http url. + ErrHTTPSRedirect = errors.New("unexpected forced https redirect") + + // ErrHTTPRedirect is an error thrown when the webserver returns + // a redirect to a non-https url from an https request. + ErrHTTPRedirect = errors.New("unexpected redirect to insecure url") + + // ErrCertExpired is a fatal error thrown when the webserver's + // certificate is expired. + ErrCertExpired = errors.New("certificate is expired") ) -func (r *Redirector) checkHttp(scheme string) ServerCheck { +func (r *Redirector) checkHTTP(scheme string) ServerCheck { return func(server *Server, logFields log.Fields) (bool, error) { - return r.checkHttpScheme(server, scheme, logFields) + return r.checkHTTPScheme(server, scheme, logFields) } } -// checkHttp checks a URL for validity, and checks redirects -func (r *Redirector) checkHttpScheme(server *Server, scheme string, logFields log.Fields) (bool, error) { +// checkHTTPScheme checks a URL for validity, and checks redirects +func (r *Redirector) checkHTTPScheme(server *Server, scheme string, logFields log.Fields) (bool, error) { u := &url.URL{ Scheme: scheme, Host: server.Host, @@ -82,7 +90,7 @@ func (r *Redirector) checkHttpScheme(server *Server, scheme string, logFields lo } func (r *Redirector) checkProtocol(server *Server, scheme string) { - res, err := r.checkHttpScheme(server, scheme, log.Fields{}) + res, err := r.checkHTTPScheme(server, scheme, log.Fields{}) if !res || err != nil { return @@ -95,16 +103,16 @@ func (r *Redirector) checkProtocol(server *Server, scheme string) { // checkRedirect parses a location header response and checks the scheme func (r *Redirector) checkRedirect(originatingScheme, locationHeader string) (bool, error) { - newUrl, err := url.Parse(locationHeader) + newURL, err := url.Parse(locationHeader) if err != nil { return false, err } - if newUrl.Scheme == "https" { - return false, ErrHttpsRedirect - } else if originatingScheme == "https" && newUrl.Scheme == "http" { - return false, ErrHttpRedirect + if newURL.Scheme == "https" { + return false, ErrHTTPSRedirect + } else if originatingScheme == "https" && newURL.Scheme == "http" { + return false, ErrHTTPRedirect } return true, nil diff --git a/check_test.go b/check_test.go index 2a5bed0..efdbfd0 100644 --- a/check_test.go +++ b/check_test.go @@ -92,7 +92,7 @@ var _ = Describe("Check suite", func() { w.WriteHeader(http.StatusOK) } - res, err := r.checkHttpScheme(server, "http", log.Fields{}) + res, err := r.checkHTTPScheme(server, "http", log.Fields{}) Expect(res).To(BeTrue()) Expect(err).To(BeNil()) @@ -146,11 +146,11 @@ var _ = Describe("Check suite", func() { logFields := log.Fields{} - res, err := r.checkHttpScheme(server, "https", logFields) + res, err := r.checkHTTPScheme(server, "https", logFields) Expect(logFields["url"]).ToNot(BeEmpty()) Expect(logFields["url"]).ToNot(Equal(httpServer.URL)) - Expect(err).To(Equal(ErrHttpRedirect)) + Expect(err).To(Equal(ErrHTTPRedirect)) Expect(res).To(BeFalse()) }) }) diff --git a/config.go b/config.go index bd3e10d..589d53f 100644 --- a/config.go +++ b/config.go @@ -17,17 +17,39 @@ import ( "time" ) +// Config represents our application's configuration. type Config struct { - BindAddress string `mapstructure:"bind"` - GeoDBPath string `mapstructure:"geodb"` - ASNDBPath string `mapstructure:"asndb"` - MapFile string `mapstructure:"dl_map"` - CacheSize int `mapstructure:"cacheSize"` - TopChoices int `mapstructure:"topChoices"` - ReloadToken string `mapstructure:"reloadToken"` - ServerList []ServerConfig `mapstructure:"servers"` - ReloadFunc func() - RootCAs *x509.CertPool + // BindAddress is the address to bind our webserver to. + BindAddress string `mapstructure:"bind"` + + // GeoDBPath is the path to the MaxMind GeoLite2 City DB. + GeoDBPath string `mapstructure:"geodb"` + + // ASNDBPath is the path to the GeoLite2 ASN DB. + ASNDBPath string `mapstructure:"asndb"` + + // MapFile is a file used to map download urls via redirect. + MapFile string `mapstructure:"dl_map"` + + // CacheSize is the number of items to keep in the LRU cache. + CacheSize int `mapstructure:"cacheSize"` + + // TopChoices is the number of servers to use in a rotation. + // With the default being 3, the top 3 servers will be rotated based on weight. + TopChoices int `mapstructure:"topChoices"` + + // ReloadToken is a secret token used for web-based reload. + ReloadToken string `mapstructure:"reloadToken"` + + // ServerList is a list of ServerConfig structs, which gets parsed into servers. + ServerList []ServerConfig `mapstructure:"servers"` + + // ReloadFunc is called when a reload is done via http api. + ReloadFunc func() + + // RootCAs is a list of CA certificates, which we parse from Mozilla directly. + RootCAs *x509.CertPool + checkClient *http.Client } @@ -51,8 +73,11 @@ func (c *Config) SetRootCAs(cas *x509.CertPool) { } } +// ASNList is a list of Autonomous System Numbers (in int) +// It can be used to include or exclude specific ASNs from requests. type ASNList []uint +// Contains checks if an ASN is in the list. func (a ASNList) Contains(value uint) bool { for _, val := range a { if value == val { @@ -63,8 +88,10 @@ func (a ASNList) Contains(value uint) bool { return false } +// ProtocolList is a list of supported protocols. type ProtocolList []string +// Contains checks if the specified protocol exists in the list. func (p ProtocolList) Contains(value string) bool { for _, val := range p { if value == val { @@ -75,10 +102,12 @@ func (p ProtocolList) Contains(value string) bool { return false } +// Append adds a supported protocol to the list. func (p ProtocolList) Append(value string) ProtocolList { return append(p, value) } +// Remove a specific protocol from the list. func (p ProtocolList) Remove(value string) ProtocolList { index := -1 @@ -97,6 +126,7 @@ func (p ProtocolList) Remove(value string) ProtocolList { return p[:len(p)-1] } +// ReloadConfig is called to reload the server's configuration. func (r *Redirector) ReloadConfig() error { log.Info("Loading configuration...") diff --git a/mirrors.go b/mirrors.go index 462166a..ced5b55 100644 --- a/mirrors.go +++ b/mirrors.go @@ -1,6 +1,7 @@ package redirector import ( + // embed is a blank import for Go's embedding, used for image files. _ "embed" "encoding/json" "github.com/go-chi/chi/v5" diff --git a/redirector.go b/redirector.go index b4a946b..ed0c2b6 100644 --- a/redirector.go +++ b/redirector.go @@ -25,6 +25,7 @@ var ( }) ) +// Redirector is our application instance. type Redirector struct { config *Config db *maxminddb.Reader @@ -39,6 +40,8 @@ type Redirector struct { checkClient *http.Client } +// LocationLookup is a specific GeoIP lookup on the maxminddb side, +// used for finding the closest servers. type LocationLookup struct { Location struct { Latitude float64 `maxminddb:"latitude"` @@ -46,7 +49,8 @@ type LocationLookup struct { } `maxminddb:"location"` } -// City represents a MaxmindDB city +// City represents a MaxmindDB city, used only when loading servers, +// or returning a GeoIP response. type City struct { Continent struct { Code string `maxminddb:"code" json:"code"` @@ -76,6 +80,8 @@ type ASN struct { AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` } +// ServerConfig is a configuration struct holding basic server configuration. +// This is used for initial loading of server information before parsing into Server. type ServerConfig struct { Server string `mapstructure:"server" yaml:"server"` Latitude float64 `mapstructure:"latitude" yaml:"latitude"` @@ -92,13 +98,15 @@ func New(config *Config) *Redirector { } r.checks = []ServerCheck{ - r.checkHttp("http"), + r.checkHTTP("http"), r.checkTLS, } return r } +// Start registers the routes, loggers, and server checks, +// then returns the http.Handler. func (r *Redirector) Start() http.Handler { if err := r.ReloadConfig(); err != nil { log.WithError(err).Fatalln("Unable to load configuration") diff --git a/servers.go b/servers.go index c990f74..f4c5a9c 100644 --- a/servers.go +++ b/servers.go @@ -27,6 +27,7 @@ type Server struct { LastChange time.Time `json:"lastChange"` } +// ServerCheck is a check function which can return information about a status. type ServerCheck func(server *Server, logFields log.Fields) (bool, error) // checkStatus runs all status checks against a server @@ -70,8 +71,12 @@ func (server *Server) checkStatus(checks []ServerCheck) { } } +// ServerList is a wrapper for a Server slice. +// It implements features like server checks. type ServerList []*Server +// checkLoop is a loop function which checks server statuses +// every 60 seconds. func (s ServerList) checkLoop(checks []ServerCheck) { t := time.NewTicker(60 * time.Second) diff --git a/util.go b/util.go index d52af81..7c6fb21 100644 --- a/util.go +++ b/util.go @@ -4,6 +4,7 @@ import "math/rand" var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +// RandomSequence is an insecure, but "good enough" random generator. func RandomSequence(n int) string { b := make([]rune, n) for i := range b { diff --git a/util/certificates.go b/util/certificates.go index a1c76fd..c8af7e3 100644 --- a/util/certificates.go +++ b/util/certificates.go @@ -11,6 +11,7 @@ const ( defaultDownloadURL = "https://github.com/mozilla/gecko-dev/blob/master/security/nss/lib/ckfw/builtins/certdata.txt?raw=true" ) +// LoadCACerts loads the certdata from Mozilla and parses it into a CertPool. func LoadCACerts() (*x509.CertPool, error) { res, err := http.Get(defaultDownloadURL)