github.com/m-lab/locate@v0.17.6/locate.go (about) 1 package main 2 3 import ( 4 "context" 5 "flag" 6 "log" 7 "net/http" 8 "time" 9 10 secretmanager "cloud.google.com/go/secretmanager/apiv1" 11 "github.com/gomodule/redigo/redis" 12 "github.com/justinas/alice" 13 promet "github.com/prometheus/client_golang/prometheus" 14 "github.com/prometheus/client_golang/prometheus/promhttp" 15 "gopkg.in/square/go-jose.v2/jwt" 16 17 "github.com/m-lab/access/controller" 18 "github.com/m-lab/access/token" 19 "github.com/m-lab/go/content" 20 "github.com/m-lab/go/flagx" 21 "github.com/m-lab/go/httpx" 22 "github.com/m-lab/go/memoryless" 23 "github.com/m-lab/go/prometheusx" 24 "github.com/m-lab/go/rtx" 25 v2 "github.com/m-lab/locate/api/v2" 26 "github.com/m-lab/locate/clientgeo" 27 "github.com/m-lab/locate/handler" 28 "github.com/m-lab/locate/heartbeat" 29 "github.com/m-lab/locate/limits" 30 "github.com/m-lab/locate/memorystore" 31 "github.com/m-lab/locate/metrics" 32 "github.com/m-lab/locate/prometheus" 33 "github.com/m-lab/locate/secrets" 34 "github.com/m-lab/locate/static" 35 ) 36 37 var ( 38 listenPort string 39 project string 40 platform string 41 locatorAE bool 42 locatorMM bool 43 legacyServer string 44 signerSecretName string 45 maxmind = flagx.URL{} 46 verifySecretName string 47 redisAddr string 48 promUserSecretName string 49 promPassSecretName string 50 promURL string 51 limitsPath string 52 keySource = flagx.Enum{ 53 Options: []string{"secretmanager", "local"}, 54 Value: "secretmanager", 55 } 56 57 rateLimitRedisAddr string 58 rateLimitPrefix string 59 rateLimitIPUAInterval time.Duration 60 rateLimitIPUAMax int 61 rateLimitIPInterval time.Duration 62 rateLimitIPMax int 63 64 earlyExitClients flagx.StringArray 65 ) 66 67 func init() { 68 // PORT and GOOGLE_CLOUD_PROJECT are part of the default App Engine environment. 69 flag.StringVar(&listenPort, "port", "8080", "AppEngine port environment variable") 70 flag.StringVar(&project, "google-cloud-project", "", "AppEngine project environment variable") 71 flag.StringVar(&platform, "platform-project", "", "GCP project for platform machine names") 72 flag.StringVar(&signerSecretName, "signer-secret-name", "locate-service-signer-key", "Name of secret for locate signer key in Secret Manager") 73 flag.StringVar(&verifySecretName, "verify-secret-name", "locate-monitoring-service-verify-key", "Name of secret for monitoring verifier key in Secret Manager") 74 flag.StringVar(&redisAddr, "redis-address", "", "Primary endpoint for Redis instance") 75 flag.StringVar(&promUserSecretName, "prometheus-username-secret-name", "prometheus-support-build-prom-auth-user", 76 "Name of secret for Prometheus username") 77 flag.StringVar(&promPassSecretName, "prometheus-password-secret-name", "prometheus-support-build-prom-auth-pass", 78 "Name of secret for Prometheus password") 79 flag.StringVar(&promURL, "prometheus-url", "", "Base URL to query prometheus") 80 flag.BoolVar(&locatorAE, "locator-appengine", true, "Use the AppEngine clientgeo locator") 81 flag.BoolVar(&locatorMM, "locator-maxmind", false, "Use the MaxMind clientgeo locator") 82 flag.Var(&maxmind, "maxmind-url", "When -locator-maxmind is true, the tar URL of MaxMind IP database. May be: gs://bucket/file or file:./relativepath/file") 83 flag.Var(&keySource, "key-source", "Where to load signer and verifier keys") 84 flag.StringVar(&limitsPath, "limits-path", "/go/src/github.com/m-lab/locate/limits/config.yaml", "Path to the limits config file") 85 flag.DurationVar(&rateLimitIPUAInterval, "rate-limit-interval", time.Hour, "Time window for IP+UA rate limiting") 86 flag.IntVar(&rateLimitIPUAMax, "rate-limit-max", 40, "Max number of events in the time window for IP+UA rate limiting") 87 flag.DurationVar(&rateLimitIPInterval, "rate-limit-ip-interval", time.Hour, 88 "Time window for IP-only rate limiting") 89 flag.IntVar(&rateLimitIPMax, "rate-limit-ip-max", 120, 90 "Max number of events in the time window for IP-only rate limiting") 91 flag.StringVar(&rateLimitPrefix, "rate-limit-prefix", "locate:ratelimit", "Prefix for Redis keys for IP+UA rate limiting") 92 flag.StringVar(&rateLimitRedisAddr, "rate-limit-redis-address", "", "Primary endpoint for Redis instance for rate limiting") 93 flag.Var(&earlyExitClients, "early-exit-clients", "Client names that should receive early_exit parameter (can be specified multiple times)") 94 // Enable logging with line numbers to trace error locations. 95 log.SetFlags(log.LUTC | log.Llongfile) 96 } 97 98 var mainCtx, mainCancel = context.WithCancel(context.Background()) 99 100 type loader interface { 101 LoadSigner(ctx context.Context, name string) (*token.Signer, error) 102 LoadVerifier(ctx context.Context, name string) (*token.Verifier, error) 103 LoadPrometheus(ctx context.Context, user, pass string) (*prometheus.Credentials, error) 104 } 105 106 func main() { 107 flag.Parse() 108 rtx.Must(flagx.ArgsFromEnv(flag.CommandLine), "Could not parse env args") 109 defer mainCancel() 110 111 prom := prometheusx.MustServeMetrics() 112 defer prom.Close() 113 114 // Create the Secret Manager client 115 var cfg loader 116 117 switch keySource.Value { 118 case "secretmanager": 119 client, err := secretmanager.NewClient(mainCtx) 120 rtx.Must(err, "Failed to create Secret Manager client") 121 cfg = secrets.NewConfig(project, client) 122 defer client.Close() 123 case "local": 124 cfg = secrets.NewLocalConfig() 125 } 126 127 // SIGNER - load the signer key. 128 signer, err := cfg.LoadSigner(mainCtx, signerSecretName) 129 rtx.Must(err, "Failed to load signer key") 130 131 locators := clientgeo.MultiLocator{clientgeo.NewUserLocator()} 132 if locatorAE { 133 aeLocator := clientgeo.NewAppEngineLocator() 134 locators = append(locators, aeLocator) 135 } 136 if locatorMM { 137 mm, err := content.FromURL(mainCtx, maxmind.URL) 138 rtx.Must(err, "failed to load maxmindurl: %s", maxmind.URL) 139 mmLocator := clientgeo.NewMaxmindLocator(mainCtx, mm) 140 locators = append(locators, mmLocator) 141 } 142 143 pool := redis.Pool{ 144 Dial: func() (redis.Conn, error) { 145 return redis.Dial("tcp", redisAddr) 146 }, 147 } 148 memorystore := memorystore.NewClient[v2.HeartbeatMessage](&pool) 149 tracker := heartbeat.NewHeartbeatStatusTracker(memorystore) 150 defer tracker.StopImport() 151 srvLocatorV2 := heartbeat.NewServerLocator(tracker) 152 153 // Rate limiter Redis pool. 154 rateLimitPool := redis.Pool{ 155 Dial: func() (redis.Conn, error) { 156 return redis.Dial("tcp", rateLimitRedisAddr) 157 }, 158 } 159 rateLimitConfig := limits.RateLimitConfig{ 160 IPConfig: limits.LimitConfig{ 161 Interval: rateLimitIPInterval, 162 MaxEvents: rateLimitIPMax, 163 }, 164 IPUAConfig: limits.LimitConfig{ 165 Interval: rateLimitIPUAInterval, 166 MaxEvents: rateLimitIPUAMax, 167 }, 168 KeyPrefix: rateLimitPrefix, 169 } 170 ipLimiter := limits.NewRateLimiter(&rateLimitPool, rateLimitConfig) 171 172 creds, err := cfg.LoadPrometheus(mainCtx, promUserSecretName, promPassSecretName) 173 rtx.Must(err, "failed to load Prometheus credentials") 174 promClient, err := prometheus.NewClient(creds, promURL) 175 rtx.Must(err, "failed to create Prometheus client") 176 177 lmts, err := limits.ParseConfig(limitsPath) 178 rtx.Must(err, "failed to parse limits config") 179 c := handler.NewClient(project, signer, srvLocatorV2, locators, promClient, 180 lmts, ipLimiter, earlyExitClients) 181 182 go func() { 183 // Check and reload db at least once a day. 184 reloadConfig := memoryless.Config{ 185 Min: time.Hour, 186 Max: 24 * time.Hour, 187 Expected: 6 * time.Hour, 188 } 189 tick, err := memoryless.NewTicker(mainCtx, reloadConfig) 190 rtx.Must(err, "Could not create ticker for reloading") 191 for range tick.C { 192 locators.Reload(mainCtx) 193 } 194 }() 195 196 // MONITORING VERIFIER - for access tokens provided by monitoring. 197 // The `verifier` returned by cfg.LoadVerifier() is a single object, but may 198 // possibly itself contain multiple verification keys. The sequence for 199 // getting here is something like: flag --verify-secret-name -> var 200 // verifySecretName -> fetch all enabled secrets associated with name from 201 // the Google Secret Manager -> pass a slice of JWT keys (secrets) to 202 // token.NewVerifier(), which results in the `verifier` value assigned 203 // below. 204 verifier, err := cfg.LoadVerifier(mainCtx, verifySecretName) 205 rtx.Must(err, "Failed to create verifier") 206 exp := jwt.Expected{ 207 Issuer: static.IssuerMonitoring, 208 Audience: jwt.Audience{static.AudienceLocate}, 209 } 210 tc, err := controller.NewTokenController(verifier, true, exp) 211 rtx.Must(err, "Failed to create token controller") 212 monitoringChain := alice.New(tc.Limit).Then(http.HandlerFunc(c.Monitoring)) 213 214 // TODO: add verifier for optional access tokens to support NextRequest. 215 216 mux := http.NewServeMux() 217 // PLATFORM APIs 218 // Services report their health to the heartbeat service. 219 mux.HandleFunc("/v2/platform/heartbeat", promhttp.InstrumentHandlerDuration( 220 metrics.RequestHandlerDuration.MustCurryWith(promet.Labels{"path": "/v2/platform/heartbeat"}), 221 http.HandlerFunc(c.Heartbeat))) 222 // Collect Prometheus health signals. 223 mux.HandleFunc("/v2/platform/prometheus", promhttp.InstrumentHandlerDuration( 224 metrics.RequestHandlerDuration.MustCurryWith(promet.Labels{"path": "/v2/platform/prometheus"}), 225 http.HandlerFunc(c.Prometheus))) 226 // End to end monitoring requests access tokens for specific targets. 227 mux.Handle("/v2/platform/monitoring/", promhttp.InstrumentHandlerDuration( 228 metrics.RequestHandlerDuration.MustCurryWith(promet.Labels{"path": "/v2/platform/monitoring/"}), 229 monitoringChain)) 230 231 // USER APIs 232 // Clients request access tokens for specific services. 233 mux.HandleFunc("/v2/nearest/", promhttp.InstrumentHandlerDuration( 234 metrics.RequestHandlerDuration.MustCurryWith(promet.Labels{"path": "/v2/nearest/"}), 235 http.HandlerFunc(c.Nearest))) 236 // REQUIRED: API keys parameters required for priority requests. 237 mux.HandleFunc("/v2/priority/nearest/", promhttp.InstrumentHandlerDuration( 238 metrics.RequestHandlerDuration.MustCurryWith(promet.Labels{"path": "/v2/priority/nearest/"}), 239 http.HandlerFunc(c.Nearest))) 240 241 // Liveness and Readiness checks to support deployments. 242 mux.HandleFunc("/v2/live", c.Live) 243 mux.HandleFunc("/v2/ready", c.Ready) 244 245 // Return list of all heartbeat registrations 246 mux.HandleFunc("/v2/siteinfo/registrations", c.Registrations) 247 248 srv := &http.Server{ 249 Addr: ":" + listenPort, 250 Handler: mux, 251 } 252 log.Println("Listening for INSECURE access requests on " + listenPort) 253 rtx.Must(httpx.ListenAndServeAsync(srv), "Could not start server") 254 defer srv.Close() 255 <-mainCtx.Done() 256 }