From f15ff8fd3902a21e141e24313689b8874135e88a Mon Sep 17 00:00:00 2001 From: garmr Date: Tue, 30 Jul 2024 14:17:46 -0700 Subject: [PATCH] add config handler to service, use new config service for geolocation --- config/config.go | 18 ++- config/config_test.go | 200 ++++++++++++++-------------- config/global/global.go | 2 +- config/initializer_test.go | 18 +-- config/proxy/proxy.go | 199 ++++++++++++++++++++++++++++ flashlight.go | 69 +++++----- flashlight_test.go | 8 +- geolookup/geolookup.go | 257 ++++++------------------------------ geolookup/geolookup_test.go | 223 ------------------------------- go.mod | 1 - go.sum | 2 - issue/issue_test.go | 11 +- services/config.go | 198 +++++---------------------- 13 files changed, 434 insertions(+), 772 deletions(-) create mode 100644 config/proxy/proxy.go delete mode 100644 geolookup/geolookup_test.go diff --git a/config/config.go b/config/config.go index eb8bbf3bc..a7ed2a3cd 100644 --- a/config/config.go +++ b/config/config.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "path/filepath" @@ -209,6 +208,7 @@ func embeddedIsNewer(conf *config, opts *options) bool { if opts.embeddedRequired { sentry.CaptureException(log.Errorf("no embedded config for %v", opts.name)) } + return false } @@ -233,23 +233,25 @@ func yamlRoundTrip(o interface{}) interface{} { if o == nil { return nil } + var or interface{} - t := reflect.TypeOf(o) - if t.Kind() == reflect.Ptr { + if t := reflect.TypeOf(o); t.Kind() == reflect.Ptr { or = reflect.New(t.Elem()).Interface() } else { or = reflect.New(t).Interface() } + b, err := yaml.Marshal(o) if err != nil { log.Errorf("Unable to yaml round trip (marshal): %v %v", o, err) return o } - err = yaml.Unmarshal(b, or) - if err != nil { + + if err = yaml.Unmarshal(b, or); err != nil { log.Errorf("Unable to yaml round trip (unmarshal): %v %v", o, err) return o } + return or } @@ -287,12 +289,13 @@ func (conf *config) saved() (interface{}, error) { in = rot13.NewReader(infile) } - bytes, err := ioutil.ReadAll(in) + bytes, err := io.ReadAll(in) if err != nil { err = fmt.Errorf("error reading config from %v: %w", conf.filePath, err) log.Error(err.Error()) return nil, err } + if len(bytes) == 0 { return nil, fmt.Errorf("config exists but is empty at %v", conf.filePath) } @@ -311,6 +314,7 @@ func (conf *config) configFetcher(stopCh chan bool, dispatch func(interface{}), if sleepDuration == noSleep { sleepDuration = defaultSleep() } + select { case <-stopCh: log.Debug("Stopping polling") @@ -370,10 +374,12 @@ func (conf *config) doSaveOne(in []byte) error { if conf.obfuscate { out = rot13.NewWriter(outfile) } + _, err = out.Write(in) if err != nil { return fmt.Errorf("unable to write yaml to file %v: %w", conf.filePath, err) } + log.Debugf("Wrote file at %v", conf.filePath) return nil } diff --git a/config/config_test.go b/config/config_test.go index 4177de1a4..f1ca71788 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,7 +2,6 @@ package config import ( "errors" - "io/ioutil" "net/http" "os" "testing" @@ -10,12 +9,10 @@ import ( "github.com/stretchr/testify/assert" - commonconfig "github.com/getlantern/common/config" "github.com/getlantern/fronted" "github.com/getlantern/golog" "github.com/getlantern/flashlight/v7/common" - "github.com/getlantern/flashlight/v7/embeddedconfig" ) func TestEmptyEmbedded(t *testing.T) { @@ -58,7 +55,7 @@ func TestEmbeddedIsNewer(t *testing.T) { func TestInvalidFile(t *testing.T) { logger := golog.LoggerFor("config-test") - tmpfile, err := ioutil.TempFile("", "invalid-test-file") + tmpfile, err := os.CreateTemp("", "invalid-test-file") if err != nil { logger.Fatal(err) } @@ -106,105 +103,108 @@ func TestObfuscated(t *testing.T) { }) } -// TestSaved tests reading stored proxies from disk -func TestSaved(t *testing.T) { - withTempDir(t, func(inTempDir func(string) string) { - file := inTempDir("proxies.yaml") - proxiesConfig := newProxiesConfig(t) - writeObfuscatedConfig(t, proxiesConfig, file) - - cfg := newConfig(file, &options{ - obfuscate: true, - unmarshaler: newProxiesUnmarshaler(), - }) - - pr, err := cfg.saved() - assert.Nil(t, err) - - proxies := pr.(map[string]*commonconfig.ProxyConfig) - chained := proxies["fallback-104.236.192.114"] - assert.True(t, chained != nil) - assert.Equal(t, "104.236.192.114:443", chained.Addr) - }) -} - -// TestEmbedded tests reading stored proxies from disk -func TestEmbedded(t *testing.T) { - withTempDir(t, func(inTempDir func(string) string) { - file := inTempDir("proxies.yaml") - - cfg := newConfig(file, &options{ - unmarshaler: newProxiesUnmarshaler(), - }) - - _, err := cfg.embedded(embeddedconfig.Proxies) - assert.NotNil(t, err) - }) -} - -func TestPollProxies(t *testing.T) { - withTempDir(t, func(inTempDir func(string) string) { - fronted.ConfigureForTest(t) - - file := inTempDir("proxies.yaml") - proxyConfig := newProxiesConfig(t) - writeObfuscatedConfig(t, proxyConfig, file) - - proxyChan := make(chan interface{}) - cfg := newConfig(file, &options{ - unmarshaler: newProxiesUnmarshaler(), - }) - var fi os.FileInfo - var err error - for i := 1; i <= 400; i++ { - fi, err = os.Stat(file) - if err == nil { - break - } - time.Sleep(200 * time.Millisecond) - } - if !assert.Nil(t, err) { - return - } - - mtime := fi.ModTime() - os.Remove(file) - - proxyConfigURLs, _ := startConfigServer(t, proxyConfig) - fetcher := newHttpFetcher(newTestUserConfig(), &http.Transport{}, proxyConfigURLs) - dispatch := func(cfg interface{}) { - proxyChan <- cfg - } - go cfg.configFetcher(nil, dispatch, fetcher, func() time.Duration { return 1 * time.Hour }, log) - proxies := (<-proxyChan).(map[string]*commonconfig.ProxyConfig) - - assert.True(t, len(proxies) > 0) - for _, val := range proxies { - assert.True(t, val != nil) - assert.True(t, len(val.Addr) > 6) - } - - for i := 1; i <= 400; i++ { - fi, err = os.Stat(file) - if err == nil && fi != nil && fi.ModTime().After(mtime) { - //log.Debugf("Got newer mod time?") - break - } - time.Sleep(50 * time.Millisecond) - } - - fi, err = os.Stat(file) - - assert.NotNil(t, fi) - assert.Nil(t, err, "Got error: %v", err) - - assert.True(t, fi.ModTime().After(mtime)) - }) -} +/* + commented out for now because this is for the old proxy config stuff +*/ + +// // TestSaved tests reading stored proxies from disk +// func TestSaved(t *testing.T) { +// withTempDir(t, func(inTempDir func(string) string) { +// file := inTempDir("proxies.yaml") +// proxiesConfig := newProxiesConfig(t) +// writeObfuscatedConfig(t, proxiesConfig, file) +// +// cfg := newConfig(file, &options{ +// obfuscate: true, +// unmarshaler: newProxiesUnmarshaler(), +// }) +// +// pr, err := cfg.saved() +// assert.Nil(t, err) +// +// proxies := pr.(map[string]*commonconfig.ProxyConfig) +// chained := proxies["fallback-104.236.192.114"] +// assert.True(t, chained != nil) +// assert.Equal(t, "104.236.192.114:443", chained.Addr) +// }) +// } + +// // TestEmbedded tests reading stored proxies from disk +// func TestEmbedded(t *testing.T) { +// withTempDir(t, func(inTempDir func(string) string) { +// file := inTempDir("proxies.yaml") +// +// cfg := newConfig(file, &options{ +// unmarshaler: newProxiesUnmarshaler(), +// }) +// +// _, err := cfg.embedded(embeddedconfig.Proxies) +// assert.NotNil(t, err) +// }) +// } + +// func TestPollProxies(t *testing.T) { +// withTempDir(t, func(inTempDir func(string) string) { +// fronted.ConfigureForTest(t) +// +// file := inTempDir("proxies.yaml") +// proxyConfig := newProxiesConfig(t) +// writeObfuscatedConfig(t, proxyConfig, file) +// +// proxyChan := make(chan interface{}) +// cfg := newConfig(file, &options{ +// unmarshaler: newProxiesUnmarshaler(), +// }) +// var fi os.FileInfo +// var err error +// for i := 1; i <= 400; i++ { +// fi, err = os.Stat(file) +// if err == nil { +// break +// } +// time.Sleep(200 * time.Millisecond) +// } +// if !assert.Nil(t, err) { +// return +// } +// +// mtime := fi.ModTime() +// os.Remove(file) +// +// proxyConfigURLs, _ := startConfigServer(t, proxyConfig) +// fetcher := newHttpFetcher(newTestUserConfig(), &http.Transport{}, proxyConfigURLs) +// dispatch := func(cfg interface{}) { +// proxyChan <- cfg +// } +// go cfg.configFetcher(nil, dispatch, fetcher, func() time.Duration { return 1 * time.Hour }, log) +// proxies := (<-proxyChan).(map[string]*commonconfig.ProxyConfig) +// +// assert.True(t, len(proxies) > 0) +// for _, val := range proxies { +// assert.True(t, val != nil) +// assert.True(t, len(val.Addr) > 6) +// } +// +// for i := 1; i <= 400; i++ { +// fi, err = os.Stat(file) +// if err == nil && fi != nil && fi.ModTime().After(mtime) { +// //log.Debugf("Got newer mod time?") +// break +// } +// time.Sleep(50 * time.Millisecond) +// } +// +// fi, err = os.Stat(file) +// +// assert.NotNil(t, fi) +// assert.Nil(t, err, "Got error: %v", err) +// +// assert.True(t, fi.ModTime().After(mtime)) +// }) +// } // TestProductionGlobal validates certain properties of the live production global config func TestProductionGlobal(t *testing.T) { - testURL := common.GlobalURL // this should always point to the live production configuration (not staging etc) expectedProviders := map[string]bool{ diff --git a/config/global/global.go b/config/global/global.go index b439b1382..18ad8d674 100644 --- a/config/global/global.go +++ b/config/global/global.go @@ -1,5 +1,5 @@ // This package breaks out some global config handling to where it can be used externally without dependence on all flashlight config. -package globalConfig +package globalconfig import ( "errors" diff --git a/config/initializer_test.go b/config/initializer_test.go index 1bb7199fc..07aa65eda 100644 --- a/config/initializer_test.go +++ b/config/initializer_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" - commonconfig "github.com/getlantern/common/config" "github.com/getlantern/eventual" + "github.com/getlantern/flashlight/v7/common" ) @@ -27,18 +27,13 @@ func TestInit(t *testing.T) { // Note these dispatch functions will receive multiple configs -- local ones, // embedded ones, and remote ones. - proxiesDispatch := func(cfg interface{}, src Source) { - proxies := cfg.(map[string]*commonconfig.ProxyConfig) - assert.True(t, len(proxies) > 0) - gotProxies.Set(true) - } globalDispatch := func(cfg interface{}, src Source) { global := cfg.(*Global) assert.True(t, len(global.Client.MasqueradeSets) > 1) gotGlobal.Set(true) } stop := Init( - ".", flags, newTestUserConfig(), proxiesDispatch, nil, globalDispatch, nil, &http.Transport{ + ".", flags, newTestUserConfig(), globalDispatch, nil, &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { // the same token should also be configured on staging // config-server, staging proxies and staging DDF distributions. @@ -59,7 +54,6 @@ func TestInit(t *testing.T) { func TestInitWithURLs(t *testing.T) { withTempDir(t, func(inTempDir func(string) string) { globalConfig := newGlobalConfig(t) - proxiesConfig := newProxiesConfig(t) globalConfig.GlobalConfigPollInterval = 3 * time.Second globalConfig.ProxyConfigPollInterval = 1 * time.Second @@ -75,20 +69,15 @@ func TestInitWithURLs(t *testing.T) { // set up servers to serve global config and count number of requests globalConfigURL, globalReqCount := startConfigServer(t, globalConfig) - // set up servers to serve global config and count number of requests - proxyConfigURL, proxyReqCount := startConfigServer(t, proxiesConfig) - // set up and call InitWithURLs flags := make(map[string]interface{}) flags["staging"] = true - proxiesDispatch := func(interface{}, Source) {} globalDispatch := func(interface{}, Source) {} stop := InitWithURLs( inTempDir("."), flags, newTestUserConfig(), - proxiesDispatch, nil, globalDispatch, nil, - proxyConfigURL, globalConfigURL, &http.Transport{}) + globalConfigURL, &http.Transport{}) defer stop() // sleep some amount @@ -100,7 +89,6 @@ func TestInitWithURLs(t *testing.T) { // test that proxy & config servers were called the correct number of times assert.GreaterOrEqual(t, 3, int(globalReqCount()), "should have fetched global config every %v", globalConfig.GlobalConfigPollInterval) - assert.GreaterOrEqual(t, 7, int(proxyReqCount()), "should have fetched proxy config every %v", globalConfig.ProxyConfigPollInterval) }) } diff --git a/config/proxy/proxy.go b/config/proxy/proxy.go new file mode 100644 index 000000000..8c61316a8 --- /dev/null +++ b/config/proxy/proxy.go @@ -0,0 +1,199 @@ +// Description: This file contains the implementation of the new consolidated proxy/geolocation config. + +package proxyconfig + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" + + "github.com/getlantern/eventual/v2" + "github.com/getlantern/golog" + "github.com/getlantern/lantern-cloud/cmd/api/apipb" + "github.com/getlantern/rot13" + "google.golang.org/protobuf/proto" +) + +const ( + defaultConfigSaveDir = "" + defaultConfigFilename = "proxies.conf" +) + +// aliases for better readability +type ( + ProxyConfig = apipb.ConfigResponse + ProxyConnectConfig = apipb.ProxyConnectConfig +) + +var ( + _config = &config{ + config: eventual.NewValue(), + } + + log = golog.LoggerFor("proxyconfig") +) + +type config struct { + config eventual.Value + + // filePath is where we should save new configs and look for existing saved configs. + filePath string + // obfuscate specifies whether or not to obfuscate the config on disk. + obfuscate bool + + // listeners is a list of functions to call when the config changes. + listeners []func(old, new *ProxyConfig) + mu sync.Mutex +} + +func Init(saveDir string, obfuscate bool) (*config, error) { + if saveDir == "" { + saveDir = defaultConfigSaveDir + _config.filePath = filepath.Join(saveDir, defaultConfigFilename) + } + + _config.obfuscate = obfuscate + + saved, err := readExistingConfig(_config.filePath, obfuscate) + if err != nil { + log.Errorf("Failed to read existing client config: %w", err) + } + + _config.config.Set(saved) + notifyListeners(nil, saved) + return _config, nil +} + +// GetConfig implements services.ConfigHandler +func (c *config) GetConfig() *ProxyConfig { + conf, _ := c.config.Get(eventual.DontWait) + return conf.(*ProxyConfig) +} + +// SetConfig implements services.ConfigHandler +func (c *config) SetConfig(new *ProxyConfig) { + old := c.GetConfig() + c.config.Set(new) + if err := saveConfig(c.filePath, new, c.obfuscate); err != nil { + log.Errorf("Failed to save client config: %w", err) + } + + notifyListeners(old, new) +} + +// GetConfig returns the current client config. An error is returned if the config is not available +// within the given timeout. +func GetConfig(timeout time.Duration) (*ProxyConfig, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + conf, err := _config.config.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + + return conf.(*ProxyConfig), nil +} + +// GetProToken returns the pro token from the current client config. An error is returned if the +// config is not available within the given timeout. +func GetProToken(timeout time.Duration) (string, error) { + config, err := GetConfig(timeout) + if err != nil || config == nil { + return "", err + } + + return config.GetProToken(), nil +} + +// GetProxyConfigs returns the list of proxy configs from the current client config. An error is +// returned if the config is not available within the given timeout. +func GetProxyConfigs(timeout time.Duration) ([]*ProxyConnectConfig, error) { + config, err := GetConfig(timeout) + if err != nil || config == nil { + return nil, err + } + + return config.GetProxy().GetProxies(), nil +} + +// readExistingConfig reads a config from a file at the specified path, filePath, +// deobfuscating it if obfuscate is true. +func readExistingConfig(filePath string, obfuscate bool) (*ProxyConfig, error) { + infile, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("unable to open config file %v for reading: %w", filePath, err) + } + defer infile.Close() + + var in io.Reader = infile + if obfuscate { + in = rot13.NewReader(infile) + } + + bytes, err := io.ReadAll(in) + if err != nil { + return nil, fmt.Errorf("failed to read config from %v: %w", filePath, err) + } + + if len(bytes) == 0 { + return nil, nil // file is empty + } + + conf := &ProxyConfig{} + err = proto.Unmarshal(bytes, conf) + return conf, err +} + +// saveConfig writes conf to a file at the specified path, filePath, obfuscating it if +// obfuscate is true. If the file already exists, it will be overwritten. +func saveConfig(filePath string, conf *ProxyConfig, obfuscate bool) error { + in, err := proto.Marshal(conf) + if err != nil { + return fmt.Errorf("unable to marshal config: %w", err) + } + + outfile, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("unable to open file %v for writing: %w", filePath, err) + } + defer outfile.Close() + + var out io.Writer = outfile + if obfuscate { + out = rot13.NewWriter(outfile) + } + + if _, err = out.Write(in); err != nil { + return fmt.Errorf("unable to write config to file %v: %w", filePath, err) + } + + return nil +} + +// OnConfigChange registers a function to be called on config change. This allows callers to +// respond to each change without having to constantly poll for changes. +func OnConfigChange(fn func(old, new *ProxyConfig)) { + _config.mu.Lock() + if _config.listeners == nil { + _config.listeners = make([]func(old, new *ProxyConfig), 0, 1) + } + + _config.listeners = append(_config.listeners, fn) + _config.mu.Unlock() +} + +func notifyListeners(old, new *ProxyConfig) { + _config.mu.Lock() + listeners := _config.listeners + _config.mu.Unlock() + // TODO: should we clone the configs before passing them to the listeners? + for _, fn := range listeners { + // don't block the config service + go fn(old, new) + } +} diff --git a/flashlight.go b/flashlight.go index 524f0460a..f402463fc 100644 --- a/flashlight.go +++ b/flashlight.go @@ -21,6 +21,7 @@ import ( "github.com/getlantern/flashlight/v7/client" "github.com/getlantern/flashlight/v7/common" "github.com/getlantern/flashlight/v7/config" + proxyconfig "github.com/getlantern/flashlight/v7/config/proxy" "github.com/getlantern/flashlight/v7/domainrouting" "github.com/getlantern/flashlight/v7/email" "github.com/getlantern/flashlight/v7/geolookup" @@ -128,7 +129,7 @@ func (f *Flashlight) EnabledFeatures() map[string]bool { } global := f.global f.mxGlobal.RUnlock() - country := services.GetCountry() + country := geolookup.GetCountry(0) for feature := range global.FeaturesEnabled { if f.calcFeature(global, country, "0.0.1", feature) { featuresEnabled[feature] = true @@ -186,7 +187,7 @@ func (f *Flashlight) FeatureEnabled(feature, applicationVersion string) bool { f.mxGlobal.RLock() global := f.global f.mxGlobal.RUnlock() - return f.calcFeature(global, services.GetCountry(), applicationVersion, feature) + return f.calcFeature(global, geolookup.GetCountry(0), applicationVersion, feature) } func (f *Flashlight) calcFeature(global *config.Global, country, applicationVersion, feature string) bool { @@ -331,7 +332,10 @@ func New( proxyListeners: make([]func(map[string]*commonconfig.ProxyConfig, config.Source), 0), } - fops.InitGlobalContext(appName, appVersion, revisionDate, deviceID, isPro, services.GetCountry) + fops.InitGlobalContext( + appName, appVersion, revisionDate, deviceID, isPro, func() string { + return geolookup.GetCountry(0) + }) f.addProxyListener(func(proxies map[string]*commonconfig.ProxyConfig, src config.Source) { log.Debug("Applying proxy config with proxies") @@ -457,26 +461,51 @@ func (f *Flashlight) StartBackgroundServices() (func(), error) { stopGlobalConfigFetch := f.startGlobalConfigFetch() stopBypass := services.StartBypassService(f.addProxyListener, f.configDir, f.userConfig) + stopConfigService, err := f.startConfigService() + if err != nil { + return func() { + stopMonitor() + stopBypass() + stopGlobalConfigFetch() + }, err + } + + return func() { + stopMonitor() + stopBypass() + stopGlobalConfigFetch() + stopConfigService() + }, nil +} + +func (f *Flashlight) startConfigService() (services.StopFn, error) { + obfuscate, _ := f.flagsAsMap["readableconfig"].(bool) + handler, err := proxyconfig.Init(f.configDir, obfuscate) + if err != nil { + return nil, err + } - services.OnConfigChange(func(old, new *services.ClientConfig) { + fn := func(old, new *proxyconfig.ProxyConfig) { var country string if old != nil { country = old.GetCountry() } + // update the country if it has changed if nc := new.GetCountry(); nc != country { - // Update the country if it has changed log.Debugf("Setting detour country to %v", nc) detour.SetCountry(nc) } proxyMap := f.convertNewProxyConfToOld(new.GetProxy().GetProxies()) f.notifyProxyListeners(proxyMap, config.Fetched) - }) + } + conf, _ := proxyconfig.GetConfig(0) + fn(nil, conf) + + proxyconfig.OnConfigChange(fn) configOpts := &services.ConfigOptions{ - SaveDir: f.configDir, - Obfuscate: true, OriginURL: "", UserConfig: f.userConfig, Sticky: false, @@ -484,36 +513,16 @@ func (f *Flashlight) StartBackgroundServices() (func(), error) { } setConfigFlagOpts(configOpts, f.flagsAsMap) - stopConfigService, err := services.StartConfigService(configOpts) - if err != nil { - return func() { - stopMonitor() - stopBypass() - stopGlobalConfigFetch() - }, err - } - - // TODO: update all code to use the new config format for geolookup - geolookup.EnablePersistence(filepath.Join(f.configDir, "latestgeoinfo.json")) - geolookup.Refresh() - - return func() { - stopMonitor() - stopBypass() - stopGlobalConfigFetch() - stopConfigService() - }, err + return services.StartConfigService(handler, configOpts) } -// setConfigFlagOpts sets the OriginURL, Sticky, and Obfuscate config options based on the -// input flags. +// setConfigFlagOpts sets the OriginURL, Sticky, and Obfuscate config options based on the input flags. func setConfigFlagOpts(opts *services.ConfigOptions, flags map[string]interface{}) { toBool := func(v interface{}) bool { b, _ := v.(bool) return b } - opts.Obfuscate = !toBool(flags["readableconfig"]) opts.Sticky = toBool(flags["stickyconfig"]) if cloudURL, ok := flags["cloudconfig"].(string); ok && cloudURL != "" { opts.OriginURL = cloudURL diff --git a/flashlight_test.go b/flashlight_test.go index 8af6d680d..7dea9cd14 100644 --- a/flashlight_test.go +++ b/flashlight_test.go @@ -3,7 +3,7 @@ package flashlight import ( "crypto/tls" "crypto/x509" - "io/ioutil" + "io" "net" "net/http" "net/url" @@ -37,11 +37,11 @@ const ( // was successful, it also tests to make sure that the outbound request didn't // leak any Lantern or CloudFlare headers. func testRequest(testCase string, t *testing.T, requests chan *http.Request, https bool, certPool *x509.CertPool, expectedStatus int, expectedErr error) { - dir, err := ioutil.TempDir("", "direct_test") + err := os.Mkdir("direct_test", 0700) if !assert.NoError(t, err, "Unable to create temp dir") { return } - defer os.RemoveAll(dir) + defer os.RemoveAll("direct_test") fronted.ConfigureForTest(t) log.Debug("Making request") @@ -82,7 +82,7 @@ func testRequest(testCase string, t *testing.T, requests chan *http.Request, htt t.Errorf("%s: Wrong response status. Expected %d, got %d", testCase, expectedStatus, resp.StatusCode) } else { // Check body - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { t.Errorf("%s: Unable to read response body: %s", testCase, err) } else if string(body) != expectedBody { diff --git a/geolookup/geolookup.go b/geolookup/geolookup.go index ef329eb47..b90c6aac7 100644 --- a/geolookup/geolookup.go +++ b/geolookup/geolookup.go @@ -2,143 +2,76 @@ package geolookup import ( "context" - "encoding/json" - "fmt" - "math" - "net/http" - "os" "sync" "time" "github.com/getlantern/eventual/v2" - geo "github.com/getlantern/geolookup" "github.com/getlantern/golog" - "github.com/getlantern/flashlight/v7/ops" - "github.com/getlantern/flashlight/v7/proxied" + proxyconfig "github.com/getlantern/flashlight/v7/config/proxy" ) var ( log = golog.LoggerFor("flashlight.geolookup") - refreshRequest = make(chan interface{}, 1) - currentGeoInfo = eventual.NewValue() - watchers []chan bool - persistToFile string - mx sync.Mutex - roundTripper http.RoundTripper -) + _country = eventual.NewValue() + _ip = eventual.NewValue() -const ( - maxTimeout = 10 * time.Minute - retryWaitMillis = 100 - maxRetryWait = 30 * time.Second + watchers []chan bool + mx sync.Mutex ) -type GeoInfo struct { - IP string - City *geo.City - FromDisk bool -} - func init() { - SetDefaultRoundTripper() + proxyconfig.OnConfigChange(func(old, new *proxyconfig.ProxyConfig) { + oldCCountry, _ := _country.Get(eventual.DontWait) + oldIP, _ := _ip.Get(eventual.DontWait) + + _country.Set(new.Country) + _ip.Set(new.Ip) + + // if the country or IP has changed, notify watchers + if oldCCountry != new.Country || oldIP != new.Ip { + for _, ch := range watchers { + select { + case ch <- true: + default: + } + } + } + }) } -// GetIP gets the IP. If the IP hasn't been determined yet, waits up to the -// given timeout for an IP to become available. +// GetIP gets the IP. If the IP hasn't been determined yet, waits up to the given timeout for the +// IP to become available. func GetIP(timeout time.Duration) string { - gi, err := GetGeoInfo(timeout) - if err != nil { - log.Debugf("Could not get IP: %v", err) - return "" - } - return gi.IP -} + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() -// GetCountry gets the country. If the country hasn't been determined yet, waits -// up to the given timeout for a country to become available. -func GetCountry(timeout time.Duration) string { - gi, err := GetGeoInfo(timeout) + ip, err := _ip.Get(ctx) if err != nil { - log.Debugf("Could not get country: %v", err) + log.Errorf("Failed to get IP: %w", err) return "" } - return gi.City.Country.IsoCode + + return ip.(string) } -func GetGeoInfo(timeout time.Duration) (*GeoInfo, error) { - // We need to specially handle negative timeouts because some callers may use - // eventual.Forever (aka -1), expecting it to block forever. - if timeout < 0 { - timeout = maxTimeout - } +// GetCountry gets the country. If the country hasn't been determined yet, waits up to the given +// timeout for country to become available. +func GetCountry(timeout time.Duration) string { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - gi, err := currentGeoInfo.Get(ctx) + country, err := _country.Get(ctx) if err != nil { - return nil, fmt.Errorf( - "could not get geoinfo with timeout %v: %w", - timeout, - err, - ) - } - if gi == nil { - return nil, fmt.Errorf("no geo info after %v", timeout) - } - return gi.(*GeoInfo), nil -} - -// EnablePersistence enables persistence of the current geo info to disk at the named file and -// initializes current geo info from that file if necessary. -func EnablePersistence(geoFile string) { - mx.Lock() - defer mx.Unlock() - - // use this file going forward - persistToFile = geoFile - - log.Debugf("Will persist geolocation info to %v", persistToFile) - - // initialize from file if necessary - knownCountry := GetCountry(0) - if knownCountry == "" { - file, err := os.Open(persistToFile) - if err == nil { - log.Debugf("Initializing geolocation info from %v", persistToFile) - dec := json.NewDecoder(file) - gi := &GeoInfo{ - FromDisk: true, - } - decodeErr := dec.Decode(gi) - if decodeErr != nil { - log.Errorf( - "Error initializing geolocation info from %v: %v", - persistToFile, - decodeErr, - ) - return - } - setGeoInfo(gi, false) - } + log.Errorf("Failed to get country: %w", err) + return "" } -} -// Refresh refreshes the geolookup information by calling the remote geolookup -// service. It will keep calling the service until it's able to determine an IP -// and country. -func Refresh() { - select { - case refreshRequest <- true: - log.Debug("Requested refresh") - default: - log.Debug("Refresh already in progress") - } + return country.(string) } -// OnRefresh creates a channel that caller can receive on when new geolocation -// information is got. +// OnRefresh returns a chan that will signal when the goelocation has changed. func OnRefresh() <-chan bool { ch := make(chan bool, 1) mx.Lock() @@ -146,117 +79,3 @@ func OnRefresh() <-chan bool { mx.Unlock() return ch } - -func init() { - go run() -} - -func run() { - for range refreshRequest { - geoInfo := lookup() - - // Check if the IP has changed and if the old IP is simply cached from - // disk. If it is cached, we should still notify anyone looking for - // a new IP because they won't have been notified of the IP on disk, - // as that is loaded very soon on startup. - if !isNew(geoInfo) { - log.Debug("public IP from network did not change - not notifying watchers") - continue - } - log.Debug("Setting new geolocation info") - mx.Lock() - setGeoInfo(geoInfo, true) - mx.Unlock() - } -} - -func isNew(newGeoInfo *GeoInfo) bool { - if newGeoInfo == nil { - return false - } - oldGeoInfo, err := GetGeoInfo(0) - if err != nil { - return true - } - if oldGeoInfo == nil { - return true - } - return oldGeoInfo.IP != newGeoInfo.IP || - oldGeoInfo.FromDisk != newGeoInfo.FromDisk -} - -func setGeoInfo(gi *GeoInfo, persist bool) { - currentGeoInfo.Set(gi) - w := watchers - for _, ch := range w { - select { - case ch <- true: - default: - } - } - if persist && persistToFile != "" { - b, err := json.Marshal(gi) - if err != nil { - log.Errorf( - "Unable to marshal geolocation info to JSON for persisting: %v", - err, - ) - return - } - writeErr := os.WriteFile(persistToFile, b, 0644) - if writeErr != nil { - log.Errorf( - "Error persisting geolocation info to %v: %v", - persistToFile, - err, - ) - } - } -} - -func lookup() *GeoInfo { - consecutiveFailures := 0 - - for { - gi, err := doLookup() - if err != nil { - log.Debugf("Unable to get current location: %s", err) - wait := time.Duration( - math.Pow( - 2, - float64(consecutiveFailures), - )*float64( - retryWaitMillis, - ), - ) * time.Millisecond - if wait > maxRetryWait { - wait = maxRetryWait - } - log.Debugf("Waiting %v before retrying", wait) - time.Sleep(wait) - consecutiveFailures++ - } else { - return gi - } - } -} - -func doLookup() (*GeoInfo, error) { - op := ops.Begin("geolookup") - defer op.End() - city, ip, err := geo.LookupIP("", roundTripper) - - if err != nil { - log.Errorf("Could not lookup IP %v", err) - return nil, op.FailIf(err) - } - return &GeoInfo{ - IP: ip, - City: city, - FromDisk: false}, - nil -} - -func SetDefaultRoundTripper() { - roundTripper = proxied.ParallelPreferChained() -} diff --git a/geolookup/geolookup_test.go b/geolookup/geolookup_test.go deleted file mode 100644 index c67d7015a..000000000 --- a/geolookup/geolookup_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package geolookup - -import ( - "io/ioutil" - "net" - "net/http" - "os" - "testing" - "time" - - "github.com/getlantern/eventual/v2" - "github.com/getlantern/fronted" - "github.com/stretchr/testify/require" -) - -const initialInfo = ` -{ - "City": { - "City": { - "GeoNameID": 4671654, - "Names": { - "de": "Austin", - "en": "Austin", - "es": "Austin", - "fr": "Austin", - "ja": "\u30aa\u30fc\u30b9\u30c6\u30a3\u30f3", - "pt-BR": "Austin", - "ru": "\u041e\u0441\u0442\u0438\u043d" - } - }, - "Continent": { - "Code": "NA", - "GeoNameID": 6255149, - "Names": { - "de": "Nordamerika", - "en": "North America", - "es": "Norteam\u00e9rica", - "fr": "Am\u00e9rique du Nord", - "ja": "\u5317\u30a2\u30e1\u30ea\u30ab", - "pt-BR": "Am\u00e9rica do Norte", - "ru": "\u0421\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0410\u043c\u0435\u0440\u0438\u043a\u0430", - "zh-CN": "\u5317\u7f8e\u6d32" - } - }, - "Country": { - "GeoNameID": 6252001, - "IsoCode": "FM", - "Names": { - "de": "USA", - "en": "United States", - "es": "Estados Unidos", - "fr": "\u00c9tats Unis", - "ja": "\u30a2\u30e1\u30ea\u30ab", - "pt-BR": "EUA", - "ru": "\u0421\u0428\u0410", - "zh-CN": "\u7f8e\u56fd" - } - }, - "Location": { - "Latitude": 30.2095, - "Longitude": -97.7972, - "MetroCode": 635, - "TimeZone": "America/Chicago" - }, - "Postal": { - "Code": "78745" - }, - "RegisteredCountry": { - "GeoNameID": 6252001, - "IsoCode": "US", - "Names": { - "de": "USA", - "en": "United States", - "es": "Estados Unidos", - "fr": "\u00c9tats Unis", - "ja": "\u30a2\u30e1\u30ea\u30ab", - "pt-BR": "EUA", - "ru": "\u0421\u0428\u0410", - "zh-CN": "\u7f8e\u56fd" - } - }, - "RepresentedCountry": { - "GeoNameID": 0, - "IsoCode": "", - "Names": null, - "Type": "" - }, - "Subdivisions": [ - { - "GeoNameID": 4736286, - "IsoCode": "TX", - "Names": { - "en": "Texas", - "es": "Texas", - "fr": "Texas", - "ja": "\u30c6\u30ad\u30b5\u30b9\u5dde", - "ru": "\u0422\u0435\u0445\u0430\u0441", - "zh-CN": "\u5fb7\u514b\u8428\u65af\u5dde" - } - } - ], - "Traits": { - "IsAnonymousProxy": false, - "IsSatelliteProvider": false - } - }, - "IP": "999.999.999.999" -} -` - -func TestGetIP(t *testing.T) { - currentGeoInfo = eventual.NewValue() - roundTripper = &http.Transport{ - Dial: (&net.Dialer{ - Timeout: 10 * time.Second, - }).Dial, - } - ip := GetIP(0) - require.Equal(t, "", ip) - go Refresh() - ip = GetIP(-1) - addr := net.ParseIP(ip) - require.NotNil(t, addr) -} - -func TestGetCountry(t *testing.T) { - currentGeoInfo = eventual.NewValue() - roundTripper = &http.Transport{ - Dial: (&net.Dialer{ - Timeout: 10 * time.Second, - }).Dial, - } - - country := GetCountry(0) - require.Equal(t, "", country) - go Refresh() - country = GetCountry(-1) - require.NotEmpty(t, country) -} - -func TestFronted(t *testing.T) { - currentGeoInfo = eventual.NewValue() - geoFile, err := ioutil.TempFile("", "") - require.NoError(t, err) - defer os.Remove(geoFile.Name()) - - ioutil.WriteFile(geoFile.Name(), []byte(initialInfo), 0644) - - fronted.ConfigureHostAlaisesForTest(t, map[string]string{ - "geo.getiantem.org": "d3u5fqukq7qrhd.cloudfront.net", - }) - - // test persistence - ch := OnRefresh() - EnablePersistence(geoFile.Name()) - country := GetCountry(0) - require.Equal(t, "FM", country, "Should immediately get persisted country") - select { - case <-ch: - // okay - case <-time.After(5 * time.Second): - t.Error("should update watcher after enabling persistence") - } - - // clear initial value to make sure we read value from network - currentGeoInfo.Reset() - Refresh() - country = GetCountry(60 * time.Second) - ip := GetIP(5 * time.Second) - require.Len(t, country, 2, "Bad country '%v' for ip %v", country, ip) - require.NotEqual( - t, - "FM", - country, - "Should have gotten a new country from network (note, this test will fail if run in Micronesia)", - ) - require.True(t, len(ip) >= 7, "Bad IP %s", ip) - - select { - case <-ch: - // okay - case <-time.After(5 * time.Second): - t.Error("should update watcher after network refresh") - } - - // Give persistence time to finish - time.Sleep(1 * time.Second) - b, err := ioutil.ReadFile(geoFile.Name()) - require.NoError(t, err) - require.NotEmpty(t, b) - require.NotEqual( - t, - initialInfo, - string(b), - "persisted geolocation information should have changed", - ) -} - -func TestIsNew(t *testing.T) { - type args struct { - newGeoInfo *GeoInfo - oldGeoInfo *GeoInfo - } - tests := []struct { - name string - args args - want bool - }{ - {"nil new should be not new", args{nil, &GeoInfo{FromDisk: true}}, false}, - {"nil existing should be new", args{&GeoInfo{}, nil}, true}, - {"old from disk should be new", args{&GeoInfo{IP: "1.1.1.1", FromDisk: false}, &GeoInfo{IP: "1.1.1.1", FromDisk: true}}, true}, - {"old not from disk should not be new", args{&GeoInfo{IP: "1.1.1.1", FromDisk: false}, &GeoInfo{IP: "1.1.1.1", FromDisk: false}}, false}, - {"new IP should be new", args{&GeoInfo{IP: "1.1.1.2", FromDisk: false}, &GeoInfo{IP: "1.1.1.1", FromDisk: false}}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - currentGeoInfo.Set(tt.args.oldGeoInfo) - if got := isNew(tt.args.newGeoInfo); got != tt.want { - t.Errorf("isNew() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/go.mod b/go.mod index b7b0e75c2..2cd311968 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,6 @@ require ( github.com/getlantern/eventual v1.0.0 github.com/getlantern/eventual/v2 v2.0.2 github.com/getlantern/fronted v0.0.0-20230601004823-7fec719639d8 - github.com/getlantern/geolookup v0.0.0-20230327091034-aebe73c6eef4 github.com/getlantern/go-socks5 v0.0.0-20171114193258-79d4dd3e2db5 github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 github.com/getlantern/hellosplitter v0.1.1 diff --git a/go.sum b/go.sum index 87f6139f5..60be46f0c 100644 --- a/go.sum +++ b/go.sum @@ -269,8 +269,6 @@ github.com/getlantern/fronted v0.0.0-20230601004823-7fec719639d8 h1:r/Z/SPPIfLXD github.com/getlantern/fronted v0.0.0-20230601004823-7fec719639d8/go.mod h1:HGe2a5LAanMKLb5BZHwDntWt5sme18AqEmJnfBgsDqc= github.com/getlantern/geo v0.0.0-20240108161311-50692a1b69a9 h1:mSg57/+t59Q08AqArlhW+3N1AVPn5ox0dTOYonRps6w= github.com/getlantern/geo v0.0.0-20240108161311-50692a1b69a9/go.mod h1:RjQ0krF8NTCc5xo2Q1995/vZBnYg33h8svn15do7dLg= -github.com/getlantern/geolookup v0.0.0-20230327091034-aebe73c6eef4 h1:Ju9l1RretVWJTNo2vpl/xAW8Dcuiyg5kJC6LRBpCigw= -github.com/getlantern/geolookup v0.0.0-20230327091034-aebe73c6eef4/go.mod h1:4UNvIsawdB8WclVxqYv46Oe1zzWJ8wMhUO+q6tUzATo= github.com/getlantern/go-socks5 v0.0.0-20171114193258-79d4dd3e2db5 h1:RBKofGGMt2k6eGBwX8mky9qunjL+KnAp9JdzXjiRkRw= github.com/getlantern/go-socks5 v0.0.0-20171114193258-79d4dd3e2db5/go.mod h1:kGHRXch95rnGLHjER/GhhFiHvfnqNz7KqWD9kGfATHY= github.com/getlantern/golog v0.0.0-20190809085441-26e09e6dd330/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= diff --git a/issue/issue_test.go b/issue/issue_test.go index 20da6d06b..fb5d7bbfd 100644 --- a/issue/issue_test.go +++ b/issue/issue_test.go @@ -1,7 +1,6 @@ package issue import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -9,9 +8,10 @@ import ( "gopkg.in/yaml.v2" + "github.com/getlantern/fronted" + "github.com/getlantern/flashlight/v7/config" "github.com/getlantern/flashlight/v7/geolookup" - "github.com/getlantern/fronted" ) func TestMain(m *testing.M) { @@ -23,26 +23,27 @@ func TestMain(m *testing.M) { defer os.RemoveAll(tempConfigDir) // Init domain-fronting - global, err := ioutil.ReadFile("../embeddedconfig/global.yaml") + global, err := os.ReadFile("../embeddedconfig/global.yaml") if err != nil { log.Errorf("Unable to load embedded global config: %v", err) os.Exit(1) } + cfg := config.NewGlobal() err = yaml.Unmarshal(global, cfg) if err != nil { log.Errorf("Unable to unmarshal embedded global config: %v", err) os.Exit(1) } + certs, err := cfg.TrustedCACerts() if err != nil { log.Errorf("Unable to read trusted certs: %v", err) } + log.Debug(cfg.Client.FrontedProviders()) fronted.Configure(certs, cfg.Client.FrontedProviders(), config.DefaultFrontedProviderID, filepath.Join(tempConfigDir, "masquerade_cache")) - // Perform initial geolookup with a high timeout so that we don't later timeout when trying to - geolookup.Refresh() geolookup.GetCountry(1 * time.Minute) os.Exit(m.Run()) } diff --git a/services/config.go b/services/config.go index d6f9a0603..2e0400a2c 100644 --- a/services/config.go +++ b/services/config.go @@ -8,16 +8,12 @@ import ( "io" "net/http" "net/url" - "os" - "path/filepath" "strconv" "sync" - "sync/atomic" "time" "github.com/getlantern/detour" "github.com/getlantern/golog" - "github.com/getlantern/rot13" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -46,19 +42,6 @@ type ( // ConfigOptions specifies the options to use for ConfigService. type ConfigOptions struct { - // SaveDir is the directory where we should save new configs and also look - // for existing saved configs. - SaveDir string - filePath string - - // obfuscate specifies whether or not to obfuscate the config on disk. - Obfuscate bool - - // Name specifies the name of the config file both on disk and in the - // embedded config that uses tarfs (the same in the interest of using - // configuration by convention). - Name string - // URL to use for fetching this config. OriginURL string @@ -81,48 +64,53 @@ type ConfigOptions struct { } type configService struct { - opts *ConfigOptions - clientInfo *ClientInfo - clientConfig atomic.Value - lastFetched time.Time + opts *ConfigOptions + clientInfo *ClientInfo + configHandler ConfigHandler + lastFetched time.Time + + logger golog.Logger done chan struct{} running bool - logger golog.Logger + mu sync.Mutex +} - // listeners is a list of functions to call when the config changes. - listeners []func(old, new *ClientConfig) +// ConfigHandler handles updating and retrieving the client config. +type ConfigHandler interface { + // GetConfig returns the current client config. + GetConfig() *ClientConfig + // SetConfig sets the client config to the given config. + SetConfig(new *ClientConfig) } var ( // initialize variable so we don't have to lock mutex and check if it's nil every time someone // calls GetClientConfig - _configService = &configService{clientConfig: atomic.Value{}} - configServiceMu sync.Mutex + _configService = &configService{} ) // StartConfigService starts a new config service with the given options and returns a func to stop // it. It will return an error if opts.OriginURL, opts.Rt, opts.Fetcher, or opts.OnConfig are nil. -func StartConfigService(opts *ConfigOptions) (StopFn, error) { - configServiceMu.Lock() - defer configServiceMu.Unlock() +func StartConfigService(handler ConfigHandler, opts *ConfigOptions) (StopFn, error) { + _configService.mu.Lock() + defer _configService.mu.Unlock() - if _configService != nil && _configService.running { + if _configService.running { return _configService.Stop, nil } switch { + case handler == nil: + return nil, errors.New("ConfigHandler is required") + case opts == nil: + return nil, errors.New("ConfigOptions is required") case opts.RoundTripper == nil: return nil, errors.New("RoundTripper is required") case opts.OriginURL == "": return nil, errors.New("OriginURL is required") } - if opts.SaveDir == "" { - opts.SaveDir = defaultConfigSaveDir - opts.filePath = filepath.Join(opts.SaveDir, defaultConfigFilename) - } - if opts.PollInterval <= 0 { opts.PollInterval = defaultConfigPollInterval } @@ -151,9 +139,8 @@ func StartConfigService(opts *ConfigOptions) (StopFn, error) { _configService.done = make(chan struct{}) _configService.logger = logger - if err := _configService.init(); err != nil { - return nil, err - } + config := handler.GetConfig() + _configService.clientInfo.Country = config.GetCountry() _configService.logger.Debug("Starting config service") _configService.running = true @@ -170,27 +157,6 @@ func StartConfigService(opts *ConfigOptions) (StopFn, error) { return _configService.Stop, nil } -func (cs *configService) init() error { - cs.logger.Debug("Initializing config service") - conf, err := readExistingClientConfig(cs.opts.filePath, cs.opts.Obfuscate) - if conf == nil { - if err != nil { - cs.logger.Errorf("could not read existing config: %v", err) - } - - cs.clientConfig.Store(&ClientConfig{}) - return err - } - - cs.logger.Debugf("loaded saved config at %v", cs.opts.filePath) - - cs.clientInfo.Country = conf.Country - cs.clientConfig.Store(conf) - cs.notifyListeners(nil, conf) - - return nil -} - func (cs *configService) updateClientInfo(conf *ClientConfig) { cs.clientInfo.ProToken = conf.ProToken cs.clientInfo.Country = conf.Country @@ -198,8 +164,8 @@ func (cs *configService) updateClientInfo(conf *ClientConfig) { } func (cs *configService) Stop() { - configServiceMu.Lock() - defer configServiceMu.Unlock() + cs.mu.Lock() + defer cs.mu.Unlock() if !cs.running { return @@ -224,7 +190,7 @@ func (cs *configService) fetchConfig() (int64, error) { cs.lastFetched = time.Now() cs.logger.Debug("Received config") - curConf := GetClientConfig() + curConf := cs.configHandler.GetConfig() if curConf != nil && !configIsNew(curConf, newConf) { op.Set("config_changed", false) cs.logger.Debug("Config is unchanged") @@ -233,16 +199,8 @@ func (cs *configService) fetchConfig() (int64, error) { op.Set("config_changed", true) - err = saveClientConfig(cs.opts.filePath, newConf, cs.opts.Obfuscate) - if err != nil { - cs.logger.Error(err) - } else { - cs.logger.Debugf("Wrote config to %v", cs.opts.filePath) - } - cs.updateClientInfo(newConf) - old := cs.clientConfig.Swap(newConf) - cs.notifyListeners(old.(*ClientConfig), newConf) + cs.configHandler.SetConfig(newConf) return sleep, nil } @@ -282,8 +240,10 @@ func (cs *configService) fetch() (*ClientConfig, int64, error) { return newConf, sleep, err } +// newRequest returns a new ConfigRequest with the current client info, proxy ids, and the last +// time the config func (cs *configService) newRequest() *ConfigRequest { - conf := GetClientConfig() + conf := cs.configHandler.GetConfig() proxies := []*ProxyConnectConfig{} if conf != nil { // not the first request proxies = conf.GetProxy().GetProxies() @@ -305,60 +265,6 @@ func (cs *configService) newRequest() *ConfigRequest { return confReq } -// readExistingClientConfig reads a config from a file at the specified path, filePath, -// deobfuscating it if obfuscate is true. -func readExistingClientConfig(filePath string, obfuscate bool) (*ClientConfig, error) { - infile, err := os.Open(filePath) - if err != nil { - return nil, fmt.Errorf("unable to open config file %v for reading: %w", filePath, err) - } - defer infile.Close() - - var in io.Reader = infile - if obfuscate { - in = rot13.NewReader(infile) - } - - bytes, err := io.ReadAll(in) - if err != nil { - return nil, fmt.Errorf("failed to read config from %v: %w", filePath, err) - } - - if len(bytes) == 0 { - return nil, nil // file is empty - } - - conf := &ClientConfig{} - err = proto.Unmarshal(bytes, conf) - return conf, err -} - -// saveClientConfig writes conf to a file at the specified path, filePath, obfuscating it if -// obfuscate is true. If the file already exists, it will be overwritten. -func saveClientConfig(filePath string, conf *ClientConfig, obfuscate bool) error { - in, err := proto.Marshal(conf) - if err != nil { - return fmt.Errorf("unable to marshal config: %w", err) - } - - outfile, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) - if err != nil { - return fmt.Errorf("unable to open file %v for writing: %w", filePath, err) - } - defer outfile.Close() - - var out io.Writer = outfile - if obfuscate { - out = rot13.NewWriter(outfile) - } - - if _, err = out.Write(in); err != nil { - return fmt.Errorf("unable to write config to file %v: %w", filePath, err) - } - - return nil -} - // configIsNew returns true if country, proToken, or ip in currInfo differ from new or if new has // proxy configs. func configIsNew(cur, new *ClientConfig) bool { @@ -366,43 +272,3 @@ func configIsNew(cur, new *ClientConfig) bool { cur.GetProToken() != new.GetProToken() || len(new.GetProxy().GetProxies()) > 0 } - -// GetClientConfig returns the current client config. -func GetClientConfig() *ClientConfig { - // We don't need to lock the mutex here because we know that the configService var is not nil - return _configService.clientConfig.Load().(*ClientConfig) -} - -// GetCountry returns the country from the current client config. If there is no config, it returns -// the default country. -func GetCountry() string { - conf := GetClientConfig() - if conf == nil { // no config yet - return "" - } - - return conf.GetCountry() -} - -// OnConfigChange registers a function to be called when the config changes. This allows callers to -// respond to changes in the config without having to poll for changes. -func OnConfigChange(fn func(old, new *ClientConfig)) { - configServiceMu.Lock() - if _configService.listeners == nil { - _configService.listeners = make([]func(old, new *ClientConfig), 0, 1) - } - - _configService.listeners = append(_configService.listeners, fn) - configServiceMu.Unlock() -} - -func (cs *configService) notifyListeners(old, new *ClientConfig) { - configServiceMu.Lock() - listeners := cs.listeners - configServiceMu.Unlock() - // TODO: should we clone the configs before passing them to the listeners? - for _, fn := range listeners { - // don't block the config service - go fn(old, new) - } -}