From 8b126b460e1dec53a47dca1258ce2cd824989955 Mon Sep 17 00:00:00 2001 From: Brian Bockelman Date: Tue, 4 Jul 2023 18:10:38 -0500 Subject: [PATCH 1/4] First pass at translating director code to go This has director code which compiles but does not populate the origin/cache information nor does it do GeoIP yet. --- director/cache_ads.go | 86 +++++++++++++++++++++++++++++++++++++ director/redirect.go | 98 +++++++++++++++++++++++++++++++++++++++++++ director/sort.go | 8 ++++ 3 files changed, 192 insertions(+) create mode 100644 director/cache_ads.go create mode 100644 director/redirect.go create mode 100644 director/sort.go diff --git a/director/cache_ads.go b/director/cache_ads.go new file mode 100644 index 000000000..7294d89dc --- /dev/null +++ b/director/cache_ads.go @@ -0,0 +1,86 @@ +package director + +import ( + "net/url" + "path" + "strings" + "sync" + "time" + + "github.com/jellydator/ttlcache/v3" +) + +type ( + NamespaceAd struct { + RequireToken bool + Path string + Issuer url.URL + MaxScopeDepth uint + Strategy StrategyType + BasePath string + VaultServer string + } + + ServerAd struct { + Name string + URL url.URL + Type ServerType + Latitude float64 + Longitude float64 + } + + ServerType string + StrategyType string +) + +const ( + CacheType ServerType = "Cache" + OriginType ServerType = "Origin" +) + +const ( + OAuthStrategy StrategyType = "OAuth2" + VaultStrategy StrategyType = "Vault" +) + + +var ( + serverAds = ttlcache.New[ServerAd, []NamespaceAd](ttlcache.WithTTL[ServerAd, []NamespaceAd](15 * time.Minute)) + serverAdMutex = sync.RWMutex{} +) + +func matchesPrefix(reqPath string, namespaceAds []NamespaceAd) *NamespaceAd { + for _, namespace := range namespaceAds { + serverPath := namespace.Path + if serverPath == reqPath { + return &namespace + } + serverPath += "/" + if strings.HasPrefix(reqPath, serverPath) { + return &namespace + } + } + return nil +} + +func GetCacheAdsForPath(reqPath string) (originNamespace NamespaceAd, ads []ServerAd) { + serverAdMutex.RLock() + defer serverAdMutex.Unlock() + reqPath = path.Clean(reqPath) + for _, item := range serverAds.Items() { + if item == nil { + continue + } + serverAd := item.Key() + if serverAd.Type == OriginType { + ns := matchesPrefix(reqPath, item.Value()) + if ns != nil { + originNamespace = *ns + } + continue + } else if serverAd.Type == CacheType && matchesPrefix(reqPath, item.Value()) != nil{ + ads = append(ads, serverAd) + } + } + return +} diff --git a/director/redirect.go b/director/redirect.go new file mode 100644 index 000000000..61966b2a4 --- /dev/null +++ b/director/redirect.go @@ -0,0 +1,98 @@ +package director + +import ( + "fmt" + "net/netip" + "net/url" + "path" + "github.com/gin-gonic/gin" +) + +func getRedirectURL(reqPath string, serverURL url.URL) (redirectURL url.URL) { + reqPath = path.Clean("/" + reqPath) + redirectURL.Scheme = "https" + redirectURL.Host = serverURL.Host + redirectURL.Path = path.Clean(serverURL.Path + reqPath) + return +} + +func RedirectToCache(ginCtx *gin.Context) { + reqPath := path.Clean("/" + ginCtx.Request.URL.Path) + ip_addr_list := ginCtx.Request.Header["X-Real-Ip"] + var ipAddr netip.Addr + if len(ip_addr_list) == 0 { + var err error + ipAddr, err = netip.ParseAddr(ginCtx.RemoteIP()) + if err != nil { + ginCtx.String(500, "Failed to parse IP address: %s", err.Error()) + return + } + } else { + var err error + ipAddr, err = netip.ParseAddr(ip_addr_list[0]) + if err != nil { + ginCtx.String(500, "Failed to parse X-Real-Ip header: %s", err.Error()) + return + } + } + namespaceAd, ads := GetCacheAdsForPath(reqPath) + if len(ads) == 0 { + ginCtx.String(404, "No cache found for path") + return + } + if namespaceAd.Path == "" { + ginCtx.String(404, "No origin found for path") + return + } + + ads, err := SortCaches(ipAddr, ads) + if err != nil { + ginCtx.String(500, "Failed to determine server ordering") + return + } + + redirectURL := getRedirectURL(reqPath, ads[0].URL) + + linkHeader := "" + first := true + for idx, ad := range ads { + if first { + first = false + } else { + linkHeader += ", " + } + linkHeader += fmt.Sprintf(`<%s>; rel="duplicate"; prio=%s`, getRedirectURL(reqPath, ad.URL), idx + 1) + } + ginCtx.Writer.Header()["Link"] = []string{linkHeader} + + if namespaceAd.Issuer.Host != "" { + ginCtx.Writer.Header()["X-Pelican-Authorization"] = []string{"issuer=" + namespaceAd.Issuer.String()} + + tokenGen := "" + first := true + hdrVals := []string{namespaceAd.Issuer.String(), string(namespaceAd.MaxScopeDepth), string(namespaceAd.Strategy), + namespaceAd.BasePath, namespaceAd.VaultServer} + for idx, hdrKey := range []string{"issuer", "max-scope-depth", "strategy", "base-path", "vault-server"} { + hdrVal := hdrVals[idx] + if hdrVal == "" { + continue + } + if !first { + tokenGen += ", " + } + first = false + tokenGen += hdrKey + "=" + hdrVal + } + if tokenGen != "" { + ginCtx.Writer.Header()["X-Pelican-Token-Generation"] = []string{tokenGen} + } + ginCtx.Writer.Header()["X-Pelican-Namespace"] = []string{fmt.Sprintf("namespace=%s, require-token=%v", + namespaceAd.Path, namespaceAd.RequireToken)} + } + + ginCtx.Redirect(307, redirectURL.String()) +} + +func RegisterDirector(router *gin.RouterGroup) { + router.GET("/", RedirectToCache) +} diff --git a/director/sort.go b/director/sort.go new file mode 100644 index 000000000..e4b34e7b1 --- /dev/null +++ b/director/sort.go @@ -0,0 +1,8 @@ +package director + +import "net/netip" + +// TODO: Actually invoke GeoIP sorting +func SortCaches(_ netip.Addr, ads []ServerAd) ([]ServerAd, error) { + return ads, nil +} From 089260788d8a2188a01bd03e704dcaa819e1da8d Mon Sep 17 00:00:00 2001 From: Brian Bockelman Date: Tue, 4 Jul 2023 22:37:21 -0500 Subject: [PATCH 2/4] Add support for geosorting within the director. --- cmd/origin_serve.go | 28 ++++--- director/geosort.go | 40 ++++++++++ director/sort.go | 189 +++++++++++++++++++++++++++++++++++++++++++- go.mod | 4 +- go.sum | 7 +- 5 files changed, 253 insertions(+), 15 deletions(-) create mode 100644 director/geosort.go diff --git a/cmd/origin_serve.go b/cmd/origin_serve.go index 38a82f992..74c8fdc4c 100644 --- a/cmd/origin_serve.go +++ b/cmd/origin_serve.go @@ -91,6 +91,8 @@ func init() { viper.SetDefault("TLSCertificate", "/etc/pelican/certificates/tls.crt") viper.SetDefault("TLSKey", "/etc/pelican/certificates/tls.key") viper.SetDefault("XrootdRun", "/run/pelican/xrootd") + viper.SetDefault("GeoIPLocation", "/run/pelican/geoip/GeoIP2.mmdb") + viper.SetDefault("MaxMindKeyFile", "/run/pelican/maxmind/maxmind.key") viper.SetDefault("RobotsTxtFile", "/etc/pelican/robots.txt") viper.SetDefault("ScitokensConfig", "/etc/pelican/xrootd/scitokens.cfg") viper.SetDefault("Authfile", "/etc/pelican/xrootd/authfile") @@ -109,20 +111,28 @@ func init() { viper.SetDefault("Authfile", filepath.Join(configBase, "xrootd", "authfile")) viper.SetDefault("MacaroonsKeyFile", filepath.Join(configBase, "macaroons-secret")) viper.SetDefault("IssuerKey", filepath.Join(configBase, "issuer.jwk")) + viper.SetDefault("MaxMindKeyFile", filepath.Join(configBase, "maxmind.key") + var runtimeDir string if userRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); userRuntimeDir != "" { - runtimeDir := filepath.Join(userRuntimeDir, "pelican") - err := os.MkdirAll(runtimeDir, 0750) - if err != nil { - cobra.CheckErr(err) - } - viper.SetDefault("XrootdRun", runtimeDir) + runtimeDir = filepath.Join(userRuntimeDir, "pelican") } else { - dir, err := os.MkdirTemp("", "pelican-xrootd-*") + runtimeDir, err = os.MkdirTemp("", "pelican-xrootd-*") + cobra.CheckErr(err) + cleanupDirOnShutdown(runtimeDir) + } + xrootdRuntimeDir := filepath.Join(runtimeDir, "xrootd") + if err = os.MkdirAll(xrootdRuntimeDir, 0750); err != nil { cobra.CheckErr(err) - viper.SetDefault("XrootdRun", dir) - cleanupDirOnShutdown(dir) } + viper.SetDefault("XrootdRun", xrootdRuntimeDir) + + geoipRuntimeDir := filepath.Join(runtimeDir, "geoip") + if err = os.MkdirAll(geoipRuntimeDir, 0750); err != nil { + cobra.CheckErr(err) + } + viper.SetDefault("GeoIPLocation", filepath.Join(geoipRuntimeDir, "GeoIP2.mmdb")) + viper.SetDefault("XrootdMultiuser", false) } viper.SetDefault("TLSCertFile", "/etc/pki/tls/cert.pem") diff --git a/director/geosort.go b/director/geosort.go new file mode 100644 index 000000000..832b0343c --- /dev/null +++ b/director/geosort.go @@ -0,0 +1,40 @@ +package director + +import ( + "math" +) + +// Mathematical function, not implementation, came from +// http://www.johndcook.com/python_longitude_latitude.html +func distanceOnSphere(lat1 float64, long1 float64, lat2 float64, long2 float64) float64 { + + if (lat1 == lat2) && (long1 == long2) { + return 0.0 + } + + // Convert latitude and longitude to + // spherical coordinates in radians. + degrees_to_radians := math.Pi/180.0 + + // phi = 90 - latitude + phi1 := (90.0 - lat1)*degrees_to_radians + phi2 := (90.0 - lat2)*degrees_to_radians + + // theta = longitude + theta1 := long1*degrees_to_radians + theta2 := long2*degrees_to_radians + + // Compute spherical distance from spherical coordinates. + + // For two locations in spherical coordinates + // (1, theta, phi) and (1, theta, phi) + // cosine( arc length ) = + // sin phi sin phi' cos(theta-theta') + cos phi cos phi' + // distance = rho * arc length + + cos := (math.Sin(phi1) * math.Sin(phi2) * math.Cos(theta1 - theta2) + + math.Cos(phi1) * math.Cos(phi2)) + arc := math.Acos( cos ) + + return arc +} diff --git a/director/sort.go b/director/sort.go index e4b34e7b1..38ee9d54e 100644 --- a/director/sort.go +++ b/director/sort.go @@ -1,8 +1,189 @@ package director -import "net/netip" +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "math/rand" + "net" + "net/http" + "net/netip" + "os" + "path" + "path/filepath" + "sort" + "strings" + "sync/atomic" + "time" -// TODO: Actually invoke GeoIP sorting -func SortCaches(_ netip.Addr, ads []ServerAd) ([]ServerAd, error) { - return ads, nil + "github.com/oschwald/geoip2-golang" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +const ( + maxMindURL string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz" +) + +var ( + maxMindReader atomic.Pointer[geoip2.Reader] +) + +type ( + SwapMap struct { + Distance float64 + Index int + } + + SwapMaps []SwapMap +) + +func (me SwapMaps) Len() int { + return len(me) +} + +func (me SwapMaps) Less(left, right int) bool { + return me[left].Distance < me[right].Distance +} + +func (me SwapMaps) Swap(left, right int) { + me[left], me[right] = me[right], me[left] +} + +func GetLatLong(addr netip.Addr) (lat float64, long float64, err error) { + ip := net.IP(addr.AsSlice()) + reader := maxMindReader.Load() + if reader == nil { + err = errors.New("No GeoIP database is available") + return + } + record, err := reader.City(ip) + if err != nil { + return + } + lat = record.Location.Latitude + long = record.Location.Longitude + return +} + + +func SortCaches(addr netip.Addr, ads []ServerAd) ([]ServerAd, error) { + distances := make(SwapMaps, len(ads)) + lat, long, err := GetLatLong(addr) + isInvalid := err != nil + for idx, ad := range ads { + if isInvalid || (ad.Latitude == 0 && ad.Longitude == 0) { + // Unable to compute distances for this server; just do random distances. + // Note that valid distances are between 0 and 1, hence (1 + random) is always + // going to be sorted after valid distances. + distances[idx] = SwapMap{1 + rand.Float64(), idx} + } else { + distances[idx] = SwapMap{distanceOnSphere(lat, long, ad.Latitude, ad.Longitude), + idx} + } + } + sort.Sort(distances) + resultAds := make([]ServerAd, len(ads)) + for idx, distance := range distances { + resultAds[distance.Index] = ads[idx] + } + return resultAds, nil +} + +func DownloadDB(localFile string) error { + keyFile := viper.GetString("MaxMindKeyFile") + if keyFile == "" { + return errors.New("No MaxMind license key found in MaxMindKeyFile config parameter") + } + contents, err := os.ReadFile(keyFile) + if err != nil { + return err + } + licenseKey := strings.TrimSpace(string(contents)) + url := fmt.Sprintf(maxMindURL, licenseKey) + localDir := filepath.Dir(localFile) + fileHandle, err := os.CreateTemp(localDir, filepath.Base(localFile) + ".tmp") + if err != nil { + return err + } + defer fileHandle.Close() + resp, err := http.Get(url) + if err != nil { + os.Remove(fileHandle.Name()) + return err + } + defer resp.Body.Close() + + gz, err := gzip.NewReader(resp.Body) + if err != nil { + return err + } + tr := tar.NewReader(gz) + foundDB := false + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } else if err != nil { + return err + } + baseName := path.Base(hdr.Name) + if baseName != "GeoLite2-City.mmdb" { + continue + } + if _, err = io.Copy(fileHandle, resp.Body); err != nil { + os.Remove(fileHandle.Name()) + return err + } + foundDB = true + break + } + if !foundDB { + return errors.New("GeoIP database not found in downloaded resource") + } + os.Rename(fileHandle.Name(), localFile) + return nil +} + +func PeriodicReload() { + for { + time.Sleep(time.Hour * 24) + localFile := viper.GetString("GeoIPLocation") + if err := DownloadDB(localFile); err != nil { + log.Warningln("Failed to download GeoIP database:", err) + } else { + localReader, err := geoip2.Open(localFile) + if err != nil { + log.Warningln("Failed to re-open GeoIP database:", err) + } else { + maxMindReader.Store(localReader) + } + } + } +} + +func InitializeDB() { + go PeriodicReload() + localFile := viper.GetString("GeoIPLocation") + localReader, err := geoip2.Open(localFile) + if err != nil { + log.Warningln("Local GeoIP database file not present; will attempt a download.", err) + err = DownloadDB(localFile) + if err != nil { + log.Errorln("Failed to download GeoIP database! Will not be available", err) + return + } + localReader, err = geoip2.Open(localFile) + if err != nil { + log.Errorln("Failed to reopen GeoIP database! Will not be available", err) + return + } + } + maxMindReader.Store(localReader) +} + +func init() { + InitializeDB() } diff --git a/go.mod b/go.mod index ea5717346..0f3edb01e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/jellydator/ttlcache/v3 v3.0.1 github.com/jsipprell/keyctl v1.0.4-0.20211208153515-36ca02672b6c github.com/lestrrat-go/jwx v1.2.26 + github.com/oschwald/geoip2-golang v1.9.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.11.1 github.com/sirupsen/logrus v1.8.1 @@ -60,6 +61,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oschwald/maxminddb-golang v1.11.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -75,7 +77,7 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/go.sum b/go.sum index 57cc8c7fe..f55e033e7 100644 --- a/go.sum +++ b/go.sum @@ -242,6 +242,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc= +github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y= +github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0= +github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -499,8 +503,9 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From 3fecd214dc1973de9fa43a08c7c8ba160826e9f1 Mon Sep 17 00:00:00 2001 From: Brian Bockelman Date: Wed, 5 Jul 2023 07:52:10 -0500 Subject: [PATCH 3/4] First pass at implementing periodic query from topology. --- director/advertise.go | 122 ++++++++++++++++++++++++++++++++++++++++++ director/cache_ads.go | 40 ++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 director/advertise.go diff --git a/director/advertise.go b/director/advertise.go new file mode 100644 index 000000000..bf8ac1b68 --- /dev/null +++ b/director/advertise.go @@ -0,0 +1,122 @@ +package director + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +type ( + Cache struct { + AuthEndpoint string `json:"auth_endpoint"` + Endpoint string `json:"endpoint"` + Resource string `json:"resource"` + } + + CredentialGeneration struct { + BasePath string `json:"base_path"` + Issuer string `json:"issuer"` + MaxScopeDepth int `json:"max_scope_depth"` + Strategy string `json:"strategy"` + VaultIssuer string `json:"vault_issuer"` + VaultServer string `json:"vault_server"` + } + + Namespace struct { + Caches []Cache `json:"caches"` + CredentialGeneration CredentialGeneration `json:"credential_generation"` + DirlistHost string `json:"dirlisthost"` + Path string `json:"path"` + ReadHTTPS bool `json:"readhttps"` + UseTokenOnRead bool `json:"usetokenonread"` + WritebackHost string `json:"writebackhost"` + } + + NamespaceJSON struct { + Caches []Cache `json:"caches"` + Namespaces []Namespace `json:"namespaces"` + } +) + +func AdvertiseOSDF() error { + namespaceURL := viper.GetString("NamespaceURL") + if namespaceURL == "" { + return errors.New("NamespaceURL configuration option not set") + } + + req, err := http.NewRequest("GET", namespaceURL, nil) + if err != nil { + return err + } + + req.Header.Set("Accept", "application/json") + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + var namespaces NamespaceJSON + if err = json.Unmarshal(respBytes, &namespaces); err != nil { + return err + } + + cacheAdMap := make(map[ServerAd][]NamespaceAd) + + for _, ns := range namespaces.Namespaces { + originAd := ServerAd{} + originNameStr := ns.WritebackHost + originURL, err := url.Parse(originNameStr) + if err != nil { + return err + } + originAd.Name = originURL.Host + originAd.URL = *originURL + originAd.Type = OriginType + + originNS := NamespaceAd{} + originNS.RequireToken = ns.UseTokenOnRead + originNS.Path = ns.Path + issuerURL, err := url.Parse(ns.CredentialGeneration.Issuer) + if err != nil { + return err + } + originNS.Issuer = *issuerURL + originNS.MaxScopeDepth = uint(ns.CredentialGeneration.MaxScopeDepth) + originNS.Strategy = StrategyType(ns.CredentialGeneration.Strategy) + originNS.BasePath = ns.CredentialGeneration.BasePath + originNS.VaultServer = ns.CredentialGeneration.VaultServer + + RecordAd(originAd, &[]NamespaceAd{originNS}) + + for _, cache := range ns.Caches { + cacheAd := ServerAd{} + cacheAd.Type = CacheType + cacheAd.Name = cache.Resource + cacheURL, err := url.Parse(cache.AuthEndpoint) + if err != nil { + log.Warningf("Namespace JSON returned cache %s with invalid URL %s", + cache.Resource, cache.AuthEndpoint) + } + cacheAd.URL = *cacheURL + cacheNS := NamespaceAd{} + cacheNS.Path = ns.Path + cacheNS.RequireToken = ns.UseTokenOnRead + cacheAdMap[cacheAd] = append(cacheAdMap[cacheAd], cacheNS) + } + } + return nil +} diff --git a/director/cache_ads.go b/director/cache_ads.go index 7294d89dc..8efc39e14 100644 --- a/director/cache_ads.go +++ b/director/cache_ads.go @@ -1,6 +1,10 @@ package director import ( + "errors" + "fmt" + "net" + "net/netip" "net/url" "path" "strings" @@ -8,6 +12,7 @@ import ( "time" "github.com/jellydator/ttlcache/v3" + log "github.com/sirupsen/logrus" ) type ( @@ -49,6 +54,41 @@ var ( serverAdMutex = sync.RWMutex{} ) +func RecordAd(ad ServerAd, namespaceAds *[]NamespaceAd) { + if err := UpdateLatLong(&ad); err != nil { + log.Debugln("Failed to lookup GeoIP coordinates for host", ad.URL.Host) + } + serverAdMutex.Lock() + defer serverAdMutex.Unlock() + + serverAds.Set(ad, *namespaceAds, ttlcache.DefaultTTL) +} + +func UpdateLatLong(ad *ServerAd) error { + if ad == nil { + return errors.New("Cannot provide a nil ad to UpdateLatLong") + } + hostname := strings.Split(ad.URL.Host, ":")[0] + ip, err := net.LookupIP(hostname) + if err != nil { + return err + } + if len(ip) == 0 { + return fmt.Errorf("Unable to find an IP address for hostname %s", hostname) + } + addr, ok := netip.AddrFromSlice(ip[0]) + if !ok { + return errors.New("Failed to create address object from IP") + } + lat, long, err := GetLatLong(addr) + if err != nil { + return err + } + ad.Latitude = lat + ad.Longitude = long + return nil +} + func matchesPrefix(reqPath string, namespaceAds []NamespaceAd) *NamespaceAd { for _, namespace := range namespaceAds { serverPath := namespace.Path From 62a0dd3aeea04ab54860875f59ab0429ebdc9da3 Mon Sep 17 00:00:00 2001 From: Brian Bockelman Date: Wed, 5 Jul 2023 08:00:38 -0500 Subject: [PATCH 4/4] Cleanup build and lint errors. --- cmd/origin_serve.go | 2 +- director/redirect.go | 5 +++-- director/sort.go | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cmd/origin_serve.go b/cmd/origin_serve.go index 74c8fdc4c..6c8f5ce9b 100644 --- a/cmd/origin_serve.go +++ b/cmd/origin_serve.go @@ -111,7 +111,7 @@ func init() { viper.SetDefault("Authfile", filepath.Join(configBase, "xrootd", "authfile")) viper.SetDefault("MacaroonsKeyFile", filepath.Join(configBase, "macaroons-secret")) viper.SetDefault("IssuerKey", filepath.Join(configBase, "issuer.jwk")) - viper.SetDefault("MaxMindKeyFile", filepath.Join(configBase, "maxmind.key") + viper.SetDefault("MaxMindKeyFile", filepath.Join(configBase, "maxmind.key")) var runtimeDir string if userRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); userRuntimeDir != "" { diff --git a/director/redirect.go b/director/redirect.go index 61966b2a4..263fa417e 100644 --- a/director/redirect.go +++ b/director/redirect.go @@ -61,7 +61,8 @@ func RedirectToCache(ginCtx *gin.Context) { } else { linkHeader += ", " } - linkHeader += fmt.Sprintf(`<%s>; rel="duplicate"; prio=%s`, getRedirectURL(reqPath, ad.URL), idx + 1) + redirectURL := getRedirectURL(reqPath, ad.URL) + linkHeader += fmt.Sprintf(`<%s>; rel="duplicate"; prio=%d`, redirectURL.String(), idx + 1) } ginCtx.Writer.Header()["Link"] = []string{linkHeader} @@ -70,7 +71,7 @@ func RedirectToCache(ginCtx *gin.Context) { tokenGen := "" first := true - hdrVals := []string{namespaceAd.Issuer.String(), string(namespaceAd.MaxScopeDepth), string(namespaceAd.Strategy), + hdrVals := []string{namespaceAd.Issuer.String(), fmt.Sprint(namespaceAd.MaxScopeDepth), string(namespaceAd.Strategy), namespaceAd.BasePath, namespaceAd.VaultServer} for idx, hdrKey := range []string{"issuer", "max-scope-depth", "strategy", "base-path", "vault-server"} { hdrVal := hdrVals[idx] diff --git a/director/sort.go b/director/sort.go index 38ee9d54e..27c6892ba 100644 --- a/director/sort.go +++ b/director/sort.go @@ -143,7 +143,9 @@ func DownloadDB(localFile string) error { if !foundDB { return errors.New("GeoIP database not found in downloaded resource") } - os.Rename(fileHandle.Name(), localFile) + if err = os.Rename(fileHandle.Name(), localFile); err != nil { + return err + } return nil }