diff --git a/go.mod b/go.mod index 40666d63018d7b021e2808bcf05fd5162300199c..4c8836b331cf220f9d828f64b469e40a5211fbd5 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( ) require ( - 0xacab.org/leap/bitmask-core v0.0.0-20240830161617-121267e4c4eb + 0xacab.org/leap/bitmask-core v0.0.0-20241015185856-0812b9aadf98 github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce github.com/prometheus-community/pro-bing v0.4.0 github.com/rs/zerolog v1.33.0 diff --git a/go.sum b/go.sum index f02a51c3d272a087f369eabed0a5bdf6a0f84a54..14d62c1635e245729a9a770c2b4e84bd78da657f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -0xacab.org/leap/bitmask-core v0.0.0-20240830161617-121267e4c4eb h1:RDLhdZKH4UAwEg8PCFpgqagsPvsx6s+CVrH2Ty6Wn/8= -0xacab.org/leap/bitmask-core v0.0.0-20240830161617-121267e4c4eb/go.mod h1:iGWa13tANV0T5ZPgWskx0wHW/FLRsRmG7UOZLixuC7I= +0xacab.org/leap/bitmask-core v0.0.0-20241015185856-0812b9aadf98 h1:0tsyOms2hcHOe9V1aJ8fhRpw/DxpswZzCisIvulLamk= +0xacab.org/leap/bitmask-core v0.0.0-20241015185856-0812b9aadf98/go.mod h1:VDzbMRiY/bCOu4z1DwpzzqgQyj6i82TpuvuCU6ogl6Q= 0xacab.org/leap/obfsvpn v1.1.0 h1:mRiV4jzmQOeRVdhbUQbNdT/aj3rmFqhL5GsK8hIol2E= 0xacab.org/leap/obfsvpn v1.1.0/go.mod h1:o9Jj+5lBfDO/HjJvO7sjnOjziKtwLnbbxMSXrhwupGA= 0xacab.org/leap/tunnel-telemetry v0.0.0-20240830081933-7328bb50078b h1:GI6SVhECFVdHalARYd5Qt/i5/+M0fjcLtJIEEQVO4Sk= @@ -551,6 +551,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/init.go b/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/init.go index d4bd129523da664c1962ef9f7f19a4c048029589..68041247bd541c95aafe95c308dd3f81973d1c8b 100644 --- a/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/init.go +++ b/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/init.go @@ -16,12 +16,11 @@ import ( "0xacab.org/leap/bitmask-core/pkg/client" "0xacab.org/leap/bitmask-core/pkg/client/provisioning" "0xacab.org/leap/bitmask-core/pkg/introducer" + bitmask_storage "0xacab.org/leap/bitmask-core/pkg/storage" "0xacab.org/leap/tunnel-telemetry/pkg/geolocate" ) type Config struct { - // FallbackCountryCode is an ISO-2 country code. Fallback if geolocation lookup fails. - FallbackCountryCode string // country code used to fetch gateways/bridges. If geolocation lookup fails, // api.config.FallbackCountryCode is used CountryCode string @@ -79,9 +78,10 @@ func NewAPI(cfg *Config) (*API, error) { transportConfig := client.DefaultTransportConfig() var intro *introducer.Introducer + var err error if cfg.Introducer != "" { - intro, err := introducer.NewIntroducerFromURL(cfg.Introducer) + intro, err = introducer.NewIntroducerFromURL(cfg.Introducer) if err != nil { return nil, err } @@ -117,13 +117,15 @@ func NewAPI(cfg *Config) (*API, error) { // In the future, we might want to add a timeout and mark it as unusable if it fails. if intro != nil { client, err := introducer.NewHTTPClientFromIntroducer(intro) - if client != nil { + if err != nil { return nil, err } - log.Info().Msg("Using obfuscated http client") + log.Info(). + Str("type", intro.Type). + Str("addr", intro.Addr). + Bool("UseKCP", intro.KCP). + Msg("Using introducer") api.httpClient = client - // We got an http client configured to use the obfuscated introducer, - // so we'll stop here. return api, nil } @@ -166,28 +168,44 @@ func getSocksProxyClient(proxyString string) (*http.Client, error) { // DoGeolocationLookup will try to fetch a valid country code.This country // code will be stored and sent in any subsequent resource queries to menshen // This method should be called only once, right after initializing the API object. -// The VPN must be turned off when calling this function. -func (api *API) DoGeolocationLookup() (string, error) { +// The VPN must be turned off when calling this function. When the geolocation +// lookup succeeds, the country code is saved on disk for the future, in case +// the geolcation lookup fails. + +func (api *API) DoGeolocationLookup() error { log.Debug().Msg("Doing geolocataion lookup") + + storage, err := bitmask_storage.GetStorage() + if err != nil { + return fmt.Errorf("Could not get storage to load/save country code fallback: %s", err) + } + geo, err := geolocate.FindCurrentHostGeolocationWithSTUN(api.config.STUNServers, api.config.CountryCodeLookupURL) + if err == nil { - // FIXME scrub if we're going to submit logs. - log.Info(). - Str("countryCode", geo.CC). - Msg("Successfully got country code") api.config.CountryCode = geo.CC + storage.SaveFallbackCountryCode(geo.CC) + return nil + } + + log.Warn(). + Err(err). + Str("stunServers", strings.Join(api.config.STUNServers, ",")). + Str("countryCodeLookupURL", api.config.CountryCodeLookupURL). + Msg("Could not get country code using STUN servers. " + + "Trying to use previously fetched country code") + + cc := storage.GetFallbackCountryCode() + if cc == "" { + log.Warn().Msg("No fallback country code was saved. Proceeding without country code") } else { - if api.config.FallbackCountryCode != "" { - log.Warn(). - Err(err). - Str("fallbackCountryCode", api.config.FallbackCountryCode). - Msg("Could not get country code via geolocation lookup. Using fallback country code") - api.config.CountryCode = api.config.FallbackCountryCode - } else { - return "", err - } + log.Info(). + Str("countryCode", cc). + Msg("Using fallback country code") + api.config.CountryCode = cc } - return api.config.CountryCode, nil + + return nil } func (api *API) GetProvider() (*models.ModelsProvider, error) { @@ -243,6 +261,14 @@ type GatewayParams struct { CC string } +type BridgeParams struct { + Location string + Port string + Transport string + CC string + Type string +} + // GetGateways returns a list of gateways (it it's enabled by the menshen // API). It optionally accepts a GatewayParams object where you can set // different filters. @@ -265,6 +291,24 @@ func (api *API) GetGateways(p *GatewayParams) ([]*models.ModelsGateway, error) { return gateways.Payload, err } +func (api *API) GetAllBridges(p *BridgeParams) ([]*models.ModelsBridge, error) { + params := provisioning.NewGetAPI5BridgesParams() + if p != nil { + params.Loc = &p.Location + params.Port = &p.Port + params.Tr = &p.Transport + params.Type = &p.Type + } + if api.httpClient != nil { + params = params.WithHTTPClient(api.httpClient) + } + bridges, err := api.client.Provisioning.GetAPI5Bridges(params) + if err != nil { + return nil, err + } + return bridges.Payload, nil +} + // GetOpenVPNCert returns valid OpenVPN client credentials (certificate and // private key) func (api *API) GetOpenVPNCert() (string, error) { @@ -288,14 +332,9 @@ func (api *API) SerializeConfig(params *GatewayParams) (string, error) { return "", err } - var key string - if strings.Contains(rawCert, rsaBegin) { - key = matchDelimitedString(rawCert, rsaBegin, rsaEnd) - } else { - key = matchDelimitedString(rawCert, keyBegin, keyEnd) - } - + key := matchDelimitedString(rawCert, keyBegin, keyEnd) crt := matchDelimitedString(rawCert, certBegin, certEnd) + ca := matchDelimitedString(rawCert, caBegin, caEnd) gateways, err := api.GetGateways(params) if err != nil { return "", err @@ -305,7 +344,7 @@ func (api *API) SerializeConfig(params *GatewayParams) (string, error) { gw := gateways[0] vars := configVars{ - CA: riseupCA, + CA: ca, Cert: crt, Key: key, IPAddr: gw.IPAddr, diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/serialize.go b/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/serialize.go index f11327894c623869f55a656c5a6a1298a33dc805..8a63878dbb5942a29d7167af659cad35a3e94edf 100644 --- a/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/serialize.go +++ b/vendor/0xacab.org/leap/bitmask-core/pkg/bootstrap/serialize.go @@ -39,12 +39,12 @@ remote {{ .IPAddr }} {{ .Port }} {{ .Transport }} </key>` var ( - rsaBegin = "-----BEGIN RSA PRIVATE KEY-----" - rsaEnd = "-----END RSA PRIVATE KEY-----" - keyBegin = "-----BEGIN PRIVATE KEY-----" - keyEnd = "-----END PRIVATE KEY-----" - certBegin = "-----BEGIN CERTIFICATE-----" - certEnd = "-----END CERTIFICATE-----" + keyBegin = "<key>\n" + keyEnd = "</key>\n" + certBegin = "<cert>\n" + certEnd = "</cert>\n" + caBegin = "<ca>\n" + caEnd = "</ca>\n" ) func matchDelimitedString(str, left, right string) string { @@ -53,49 +53,5 @@ func matchDelimitedString(str, left, right string) string { if len(matches) != 1 { return "" } - return left + matches[0][1] + right + return matches[0][1] } - -// TODO get the CA from the service too -var riseupCA = `-----BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl -dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE -AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw -NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM -Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv -b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m -TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a -7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE -LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY -iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK -5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx -HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58 -m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF -PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q -hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj -shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k -ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu -f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD -VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB -AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v -qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/ -3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ -4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7 -3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch -Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf -Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg -tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF -tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ -UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp -0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIBYjCCAQigAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxMRUFQIFJv -b3QgQ0EwHhcNMjExMTAyMTkwNTM3WhcNMjYxMTAyMTkxMDM3WjAXMRUwEwYDVQQD -EwxMRUFQIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQxOXBGu+gf -pjHzVteGTWL6XnFxtEnKMFpKaJkA/VOHmESzoLsZRQxt88GssxaqC01J17idQiqv -zgNpedmtvFtyo0UwQzAOBgNVHQ8BAf8EBAMCAqQwEgYDVR0TAQH/BAgwBgEB/wIB -ATAdBgNVHQ4EFgQUZdoUlJrCIUNFrpffAq+LQjnwEz4wCgYIKoZIzj0EAwIDSAAw -RQIgfr3w4tnRG+NdI3LsGPlsRktGK20xHTzsB3orB0yC6cICIQCB+/9y8nmSStfN -VUMUyk2hNd7/kC8nL222TTD7VZUtsg== ------END CERTIFICATE-----` diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/httpClient.go b/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/httpClient.go index 877d353f686c65410c1cabf0d5939adeeddb6745..167a2a2abe05972eae35616a41a13cb9ccb18dd2 100644 --- a/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/httpClient.go +++ b/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/httpClient.go @@ -76,8 +76,17 @@ func NewHTTPClientFromIntroducer(introducer *Introducer) (*http.Client, error) { Transport: &CallbackTransport{ OriginalTransport: transport, Callback: func(r *http.Response) { - if err := storage.MaybeUpdateLastUsedForIntroducer(introducer.URL()); err != nil { - log.Error().Err(err).Msg("cannot update introducer") + store, err := storage.GetStorage() + if err != nil { + log.Warn(). + Err(err). + Msg("Could not load DB to update lastUsed timestamp for introducer") + return + } + if err := store.UpdateLastUsedForIntroducer(introducer.URL()); err != nil { + log.Warn(). + Err(err). + Msg("Could not update lastUsed timestamp for introducer") } }, }, diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/introducer.go b/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/introducer.go index 1c0a745f748b0c455c307d468a1dffbc123ca4a6..0a244acf09178f36e1b73a5970ea29721a2c6160 100644 --- a/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/introducer.go +++ b/vendor/0xacab.org/leap/bitmask-core/pkg/introducer/introducer.go @@ -28,7 +28,7 @@ func (i *Introducer) Validate() error { if len(i.Cert) != 70 { return fmt.Errorf("wrong certificate len = %d", len(i.Cert)) } - if len(strings.Split(i.FQDN, ".")) < 2 { + if i.FQDN != "localhost" && len(strings.Split(i.FQDN, ".")) < 2 { return fmt.Errorf("expected a FQDN, got: %s", i.FQDN) } return nil diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/models/models.go b/vendor/0xacab.org/leap/bitmask-core/pkg/models/models.go index 6a5382d15e5221dd08035117a0168b6c49ccc22f..d7073625fd5c12501f78f1d18a873dfc2af81f5a 100644 --- a/vendor/0xacab.org/leap/bitmask-core/pkg/models/models.go +++ b/vendor/0xacab.org/leap/bitmask-core/pkg/models/models.go @@ -9,23 +9,21 @@ import "time" // Introducer keeps metadata about introducers that the user has added to the Bitmask application. Introducers are expected to be transmitted off-band. type Introducer struct { - ID int `storm:"increment"` - Name string `storm:"index,unique"` + FQDN string // URL is the canonical URL. It should be stored after validation and writing in the canonical order, since // we will check for uniqueness. - URL string `storm:"unique"` - CreatedAt time.Time `storm:"index"` + URL string + CreatedAt time.Time LastUsed time.Time } // Bridge is a private bridge. type Bridge struct { - ID int `storm:"increment"` - Name string `storm:"index,unique"` + Name string Location string Type string // Raw is the raw JSON serialization of the bridge. We could also use the menshen model as a nested struct. - Raw string `storm:"unique"` - CreatedAt time.Time `storm:"index"` + Raw string + CreatedAt time.Time LastUsed time.Time } diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/bridge.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/bridge.go deleted file mode 100644 index a3fb7bad019c79de67276f1366aa4e0fae52a5ae..0000000000000000000000000000000000000000 --- a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/bridge.go +++ /dev/null @@ -1,24 +0,0 @@ -package storage - -import ( - "github.com/rs/zerolog/log" - - "0xacab.org/leap/bitmask-core/pkg/models" -) - -func MaybeGetBridgeByName(name string) (models.Bridge, error) { - var b models.Bridge - var err error - - db, err := NewStorageWithDefaultDir() - if err != nil { - log.Fatal().Err(err).Msg("cannot open storage") - } - defer db.Close() - - b, err = db.GetBridgeByName(name) - if err != nil { - return b, err - } - return b, nil -} diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db.go deleted file mode 100644 index fb0bd8a63e6761ba86ebd9b5708cee51587028e4..0000000000000000000000000000000000000000 --- a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db.go +++ /dev/null @@ -1,224 +0,0 @@ -// storage provides an embeddable database that bitmask uses to persist -// a series of values. -// For now, we store the following items in the database: -// - Introducer metadata. -// - Private bridges. -package storage - -import ( - "fmt" - "os" - "path/filepath" - "time" - - "github.com/asdine/storm/v3" - homedir "github.com/mitchellh/go-homedir" - "github.com/rs/zerolog/log" - - "0xacab.org/leap/bitmask-core/pkg/models" -) - -var AppName = "bitmask" - -type Storage struct { - db *storm.DB -} - -// NewStorage initializes a new storage with a given path. `NewStorageWithDefaultDir` should be preferred to initialize a storage, since it will try to pick a default path. -func NewStorage(path string) (*Storage, error) { - fp := filepath.Join(path, "bitmask.db") - db, err := storm.Open(fp) - if err != nil { - return nil, err - } - - // Initialize buckets and indexes - if err := db.Init(&models.Introducer{}); err != nil { - return nil, err - } - if err := db.Init(&models.Bridge{}); err != nil { - return nil, err - } - return &Storage{db: db}, nil -} - -func NewStorageWithDefaultDir() (*Storage, error) { - home, err := homedir.Dir() - if err != nil { - return nil, err - } - - var configPath string - switch _os := os.Getenv("GOOS"); _os { - case "windows": - configPath = filepath.Join(os.Getenv("APPDATA"), AppName) - default: - // This will cover both 'darwin' (macOS) and 'linux' - configPath = filepath.Join(home, ".config", AppName) - } - - err = os.MkdirAll(configPath, 0700) - if err != nil { - return nil, err - } - - return NewStorage(configPath) -} - -// NewIntroducer creates a new Introducer from the name and URL. -func (s *Storage) NewIntroducer(name, url string) error { - item := &models.Introducer{ - Name: name, - URL: url, - } - item.CreatedAt = time.Now() - return s.db.Save(item) -} - -// ListIntroducers returns an array of all the introducers -func (s *Storage) ListIntroducers() ([]models.Introducer, error) { - var items []models.Introducer - err := s.db.AllByIndex("CreatedAt", &items) - return items, err -} - -// GetIntroducerByID will return the Introducer with the given ID, if found, and an error. -func (s *Storage) GetIntroducerByID(id int) (models.Introducer, error) { - var introducer models.Introducer - err := s.db.One("ID", id, &introducer) - return introducer, err -} - -// GetIntroducerByName will return the Introducer with the given Name, if found, and an error. -func (s *Storage) GetIntroducerByName(name string) (models.Introducer, error) { - var introducer models.Introducer - err := s.db.One("Name", name, &introducer) - return introducer, err -} - -// DeleteIntroducer accepts a name and an ID. If you want to delete by name, pass 0 as the ID; -// if you want to delete by ID, pass the empty string as name. -func (s *Storage) DeleteIntroducer(id int, name string) error { - if id == 0 && name == "" { - return fmt.Errorf("need to pass id or name") - } - var introducer models.Introducer - var err error - switch { - case id != 0: - introducer, err = s.GetIntroducerByID(id) - if err != nil { - return err - } - case name != "": - introducer, err = s.GetIntroducerByName(name) - if err != nil { - return err - } - default: - return fmt.Errorf("unhandled case") - } - return s.db.DeleteStruct(&introducer) -} - -// TODO GetIntroducersNeverUsed - useful for prune - -// NewBridge creates a new Bridge from the passed parameters. -func (s *Storage) NewBridge(name, bridgeType, location, raw string) error { - item := &models.Bridge{ - Name: name, - Type: bridgeType, - Location: location, - Raw: raw, - } - item.CreatedAt = time.Now() - return s.db.Save(item) -} - -// ListBridges returns an array of all the bridges. -func (s *Storage) ListBridges() ([]models.Bridge, error) { - var items []models.Bridge - err := s.db.AllByIndex("CreatedAt", &items) - return items, err -} - -// GetBridgeByID will return the Bridge with the given ID, if found, and an error. -func (s *Storage) GetBridgeByID(id int) (models.Bridge, error) { - var bridge models.Bridge - err := s.db.One("ID", id, &bridge) - return bridge, err -} - -// GetBridgeByName will return the Bridge with the given Name, if found, and an error. -func (s *Storage) GetBridgeByName(name string) (models.Bridge, error) { - var bridge models.Bridge - err := s.db.One("Name", name, &bridge) - return bridge, err -} - -// GetBridgesByType will return all Bridges with the given Type, if found, and an error. -func (s *Storage) GetBridgesByType(bridgeType string) ([]models.Bridge, error) { - var bridges []models.Bridge - err := s.db.Find("Type", bridgeType, &bridges) - return bridges, err -} - -// GetBridgesByLocation will return all Bridges with the given Location, if found, and an error. -func (s *Storage) GetBridgesByLocation(location string) ([]models.Bridge, error) { - var bridges []models.Bridge - err := s.db.Find("Location", location, &bridges) - return bridges, err -} - -// DeleteBridge accepts a name and an ID. If you want to delete by name, pass 0 as the ID; -// if you want to delete by ID, pass the empty string as name. -func (s *Storage) DeleteBridge(id int, name string) error { - if id == 0 && name == "" { - return fmt.Errorf("need to pass id or name") - } - var bridge models.Bridge - var err error - switch { - case id != 0: - bridge, err = s.GetBridgeByID(id) - if err != nil { - return err - } - case name != "": - bridge, err = s.GetBridgeByName(name) - if err != nil { - return err - } - default: - return fmt.Errorf("unhandled case") - } - return s.db.DeleteStruct(&bridge) -} - -// Close closes the db connection -func (s *Storage) Close() { - s.db.Close() -} - -// MaybeUpdateLastUsedForIntroducer will attempt to update the LastUsed timestamp for the introducer -// that matches the passed URL. -func MaybeUpdateLastUsedForIntroducer(url string) error { - db, err := NewStorageWithDefaultDir() - if err != nil { - return err - } - defer db.Close() - - var intro models.Introducer - err = db.db.One("URL", url, &intro) - if err != nil { - log.Error().Err(err) - return err - } - intro.LastUsed = time.Now() - err = db.db.Save(&intro) - if err != nil { - return err - } - return nil -} diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db_store.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db_store.go new file mode 100644 index 0000000000000000000000000000000000000000..6794198dd055f364e418228f9174be5af4bccdc3 --- /dev/null +++ b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/db_store.go @@ -0,0 +1,161 @@ +package storage + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/asdine/storm/v3" +) + +type DBStore struct { + Store + db *storm.DB + filepath string +} + +const defaultBucket = "defaultBucket" + +func NewDBStore(path string) (*DBStore, error) { + fp := filepath.Join(path, "bitmask.db") + err := os.MkdirAll(filepath.Dir(fp), 0750) + if err != nil { + return nil, fmt.Errorf("failed to create DB store path: %v", err) + } + db, err := storm.Open(fp) + + if err != nil { + return nil, fmt.Errorf("failed to open storm DB: %v", err) + } + + return &DBStore{db: db, filepath: fp}, nil +} + +func (s *DBStore) GetString(key string) string { + var value string + err := s.db.Get(defaultBucket, key, &value) + if err != nil { + return "" + } + return value +} + +func (s *DBStore) GetStringWithDefault(key string, value string) string { + var result string + err := s.db.Get(defaultBucket, key, &value) + if err != nil { + return value + } + return result +} + +func (s *DBStore) GetBoolean(key string) bool { + var value bool + err := s.db.Get(defaultBucket, key, &value) + if err != nil { + return false + } + return value +} + +func (s *DBStore) GetBooleanWithDefault(key string, value bool) bool { + var result bool + err := s.db.Get(defaultBucket, key, &result) + if err != nil { + return value + } + return result +} + +func (s *DBStore) GetInt(key string) int { + var value int + err := s.db.Get(defaultBucket, key, &value) + if err != nil { + return 0 + } + return value +} +func (s *DBStore) GetIntWithDefault(key string, value int) int { + var result int + err := s.db.Get(defaultBucket, key, &result) + if err != nil { + return value + } + return result +} +func (s *DBStore) GetLong(key string) int64 { + var value int64 + err := s.db.Get(defaultBucket, key, &value) + if err != nil { + return 0 + } + return value +} + +func (s *DBStore) GetLongWithDefault(key string, value int64) int64 { + var result int64 + err := s.db.Get(defaultBucket, key, &result) + if err != nil { + return value + } + return result +} + +func (s *DBStore) GetByteArray(key string) []byte { + var value []byte + err := s.db.Get(defaultBucket, key, &value) + if err != nil { + return nil + } + return value +} + +func (s *DBStore) GetByteArrayWithDefault(key string, value []byte) []byte { + var result []byte + err := s.db.Get(defaultBucket, key, &result) + if err != nil { + return value + } + return result +} + +// Key-Value Setters +func (s *DBStore) SetString(key string, value string) { + _ = s.db.Set(defaultBucket, key, value) +} +func (s *DBStore) SetBoolean(key string, value bool) { + _ = s.db.Set(defaultBucket, key, value) +} + +func (s *DBStore) SetInt(key string, value int) { + _ = s.db.Set(defaultBucket, key, value) +} +func (s *DBStore) SetLong(key string, value int64) { + _ = s.db.Set(defaultBucket, key, value) +} +func (s *DBStore) SetByteArray(key string, value []byte) { + _ = s.db.Set(defaultBucket, key, value) +} + +func (s *DBStore) Contains(key string) (bool, error) { + return s.db.KeyExists(defaultBucket, key) +} +func (s *DBStore) Remove(key string) error { + return s.db.Delete(defaultBucket, key) +} +func (s *DBStore) Clear() error { + return s.db.Drop(defaultBucket) +} + +func (s *DBStore) Close() error { + return s.db.Close() +} + +func (s *DBStore) Open() error { + db, err := storm.Open(s.filepath) + if err != nil { + return fmt.Errorf("failed to open database: %v", err) + } + s.db = db + return nil +} diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/introducer.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/introducer.go deleted file mode 100644 index d7dca644c5dcb52943dd2f4c5a36fa06e89d3d08..0000000000000000000000000000000000000000 --- a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/introducer.go +++ /dev/null @@ -1,34 +0,0 @@ -package storage - -import ( - "strings" - - "github.com/rs/zerolog/log" -) - -func MaybeGetIntroducerURLByName(introducerName string) string { - if introducerName == "" { - return "" - } - switch strings.HasPrefix(introducerName, "obfsvpnintro://") { - // does not have prefix schema, let's treat it as a name in the internal storage - case false: - db, err := NewStorageWithDefaultDir() - if err != nil { - log.Fatal().Err(err).Msg("cannot open storage") - } - defer db.Close() - - _i, err := db.GetIntroducerByName(introducerName) - if err != nil { - log.Fatal().Err(err).Msg("cannot get introducer by name") - } - // We got a valid (and unique) introducer from storage, so that - // we can retrieve the URL value. This is assumed to have been validated - // when introducing it, but I should be more paranoid here. - // TODO(atanarjuat): be more paranoid and validate the URL/signature etc. - return _i.URL - default: - return "" - } -} diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/storage.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/storage.go new file mode 100644 index 0000000000000000000000000000000000000000..b0a66d3ac0c1f47fa24ec07c36b08fb68d2fdd60 --- /dev/null +++ b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/storage.go @@ -0,0 +1,460 @@ +// storage provides an embeddable database that bitmask uses to persist +// a series of values. +// For now, we store the following items in the database: +// - Introducer metadata. +// - Private bridges. +// +// Example Usage to initilialize the default storage struct: +// +// InitAppStorage() +// +// To work with the initialized storage call: +// +// GetStorage() +// + +package storage + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "os" + "path/filepath" + "time" + + homedir "github.com/mitchellh/go-homedir" + + "0xacab.org/leap/bitmask-core/pkg/models" +) + +var AppName = "bitmask" +var appStorage *Storage + +const ( + INTRODUCER = "INTRODUCER" + BRIDGE = "BRIDGE" + COUNTRYCODE = "COUNTRYCODE" +) + +type Storage struct { + store Store +} + +// CompareIntroducer is a function type for the comparison of introducers +type CompareIntroducer func(introducer models.Introducer) bool + +// CompareBridge is a function type for the comparison of bridges +type CompareBridge func(bridge models.Bridge) bool + +// InitAppStorage initializes the global storage with a given storage instance +func InitAppStorageWith(store Store) { + if appStorage != nil { + appStorage.Close() + } + appStorage = NewStorageWithStore(store) +} + +// Init AppStorage initializes the global storage with the default storage directory +func InitAppStorage() error { + if appStorage != nil { + appStorage.Close() + } + storage, err := NewStorageWithDefaultDir() + if err != nil { + return fmt.Errorf("failed to initialize app storage: %v", err) + } + appStorage = storage + return nil +} + +// NewStorage initializes a new storage with a given path. +// `NewStorageWithDefaultDir` should be preferred to initialize a storage, since it will try to pick a default path. +func NewStorage(path string) (*Storage, error) { + store, err := NewDBStore(path) + if err != nil { + return nil, err + } + return &Storage{ + store: store, + }, nil +} + +// NewStorageWithStore initializes the storage struct with a custom store. This +// can be used to implement custom storage adapters e.g. for different database types +func NewStorageWithStore(store Store) *Storage { + return &Storage{ + store: store, + } +} + +// NewStoreageWithDefaultDir initializes a storage struct with a storm DB +// and looks up the correct default paths for Linux, MacOS and Windows. +// Don't call this method in context of mobile app development +func NewStorageWithDefaultDir() (*Storage, error) { + home, err := homedir.Dir() + if err != nil { + return nil, err + } + + var configPath string + switch _os := os.Getenv("GOOS"); _os { + case "windows": + configPath = filepath.Join(os.Getenv("APPDATA"), AppName) + default: + // This will cover both 'darwin' (macOS) and 'linux' + configPath = filepath.Join(home, ".config", AppName) + } + + err = os.MkdirAll(configPath, 0700) + if err != nil { + return nil, err + } + + return NewStorage(configPath) +} + +// AddIntroducers adds a new Introducer model from URL +// to the storage +func (s *Storage) AddIntroducer(url string) error { + fqdn, err := getFQDNFromIntroducerURL(url) + if err != nil { + return err + } + + item := &models.Introducer{ + FQDN: *fqdn, + URL: url, + CreatedAt: time.Now(), + } + + // delete existing introducer with same FQDN as we only want + // to allow one for per provider for now + err = s.DeleteIntroducer(*fqdn) + if err != nil { + return err + } + + introducers, err := s.getAllIntroducers() + + if err != nil { + return err + } + introducers = append(introducers, *item) + return s.saveIntroducers(introducers) +} + +func (s *Storage) getAllIntroducers() ([]models.Introducer, error) { + // Create an empty slice of Introducer + emptySlice := []models.Introducer{} + bytes, _ := json.Marshal(emptySlice) + + introducerString := s.store.GetByteArrayWithDefault(INTRODUCER, bytes) + introducers, err := unmarshalJSON[[]models.Introducer](introducerString) + if err != nil { + return nil, err + } + return *introducers, err +} + +func (s *Storage) saveIntroducers(introducers []models.Introducer) error { + bytes, err := json.Marshal(introducers) + if err != nil { + return err + } + s.store.SetByteArray(INTRODUCER, bytes) + return nil +} + +func unmarshalJSON[T any](data []byte) (*T, error) { + var result T + err := json.Unmarshal(data, &result) + if err != nil { + return nil, err + } + return &result, nil +} + +// ListIntroducers returns an array of all introducers +func (s *Storage) ListIntroducers() ([]models.Introducer, error) { + return s.getAllIntroducers() +} + +// GetIntroducerByFQDN returns the first introducer for a given fqdn. +func (s *Storage) GetIntroducerByFQDN(fqdn string) (*models.Introducer, error) { + compare := func(intro models.Introducer) bool { + return intro.FQDN == fqdn + } + return s.getFirstIntroducer(compare) +} + +// GetIntroducerByURL will return the Introducer with the given URL, if found, and an error. +func (s *Storage) GetIntroducerByURL(url string) (*models.Introducer, error) { + compare := func(intro models.Introducer) bool { + return intro.URL == url + } + return s.getFirstIntroducer(compare) +} + +func (s *Storage) getFirstIntroducer(compare CompareIntroducer) (*models.Introducer, error) { + introducers, err := s.getAllIntroducers() + if err != nil { + return nil, err + } + for _, intro := range introducers { + if compare(intro) { + return &intro, nil // Return a pointer to the found Introducer + } + } + return nil, fmt.Errorf("introducer not found") +} + +func (s *Storage) updateFirstIntroducer(compare CompareIntroducer, lastUsed time.Time) ([]models.Introducer, error) { + introducers, err := s.getAllIntroducers() + if err != nil { + return nil, err + } + + found := false + for _, intro := range introducers { + if compare(intro) { + intro.LastUsed = lastUsed + found = true + break + } + } + + if !found { + return nil, fmt.Errorf("introducer not found") + } + + return introducers, nil +} + +// DeleteIntroducer deletes all introducers for a given fqdn. +func (s *Storage) DeleteIntroducer(fqdn string) error { + if fqdn == "" { + return errors.New("need to pass fully qualified domain name of the introducer") + } + var introducers, err = s.getAllIntroducers() + if err != nil { + return err + } + + compare := func(intro models.Introducer) bool { + return fqdn == intro.FQDN + } + + updatedIntroducers := func() []models.Introducer { + for i, intro := range introducers { + if compare(intro) { + // Swap with the last element + lastIndex := len(introducers) - 1 + introducers[i] = introducers[lastIndex] // Move the last element to the current position + return introducers[:lastIndex] // Return the truncated slice + } + } + return introducers // Return original slice if ID not found + }() + + return s.saveIntroducers(updatedIntroducers) +} + +// NewBridge creates a new Bridge from the passed parameters. +func (s *Storage) NewBridge(name, bridgeType, location, raw string) error { + item := &models.Bridge{ + Name: name, + Type: bridgeType, + Location: location, + Raw: raw, + } + item.CreatedAt = time.Now() + bridges, err := s.getAllBridges() + if err != nil { + return err + } + + comparison := func(intro models.Bridge) bool { + return intro.Name == name || intro.Raw == raw + } + bridge, _ := s.getFirstBridge(comparison) + if bridge != nil { + return fmt.Errorf("bridge %v already saved", bridge) + } + + bridges = append(bridges, *item) + return s.saveBridges(bridges) +} + +func (s *Storage) getAllBridges() ([]models.Bridge, error) { + // Create an empty slice of Introducer + emptySlice := []models.Bridge{} + bytes, _ := json.Marshal(emptySlice) + + bridgeBytes := s.store.GetByteArrayWithDefault(BRIDGE, bytes) + bridges, err := unmarshalJSON[[]models.Bridge](bridgeBytes) + if err != nil { + return nil, err + } + return *bridges, err +} + +func (s *Storage) saveBridges(bridges []models.Bridge) error { + bytes, err := json.Marshal(bridges) + if err != nil { + return err + } + s.store.SetByteArray(BRIDGE, bytes) + return nil +} + +// ListBridges returns an array of all the bridges. +func (s *Storage) ListBridges() ([]models.Bridge, error) { + return s.getAllBridges() +} + +func (s *Storage) getFirstBridge(compare CompareBridge) (*models.Bridge, error) { + bridges, err := s.getAllBridges() + if err != nil { + return nil, err + } + for _, bridge := range bridges { + if compare(bridge) { + return &bridge, nil // Return a pointer to the found Bridge + } + } + return nil, fmt.Errorf("bridge not found") +} + +func (s *Storage) getBridges(compare CompareBridge) ([]models.Bridge, error) { + bridges, err := s.getAllBridges() + if err != nil { + return nil, err + } + var result []models.Bridge + + for _, bridge := range bridges { + if compare(bridge) { + result = append(result, bridge) + } + } + + if len(result) == 0 { + return nil, fmt.Errorf("bridge not found") + } + + return result, nil +} + +// GetBridgeByName will return the Bridge with the given Name, if found, and an error. +func (s *Storage) GetBridgeByName(name string) (*models.Bridge, error) { + compare := func(bridge models.Bridge) bool { + return bridge.Name == name + } + return s.getFirstBridge(compare) +} + +// GetBridgesByType will return all Bridges with the given Type, if found, and an error. +func (s *Storage) GetBridgesByType(bridgeType string) ([]models.Bridge, error) { + compare := func(bridge models.Bridge) bool { + return bridge.Type == bridgeType + } + return s.getBridges(compare) +} + +// GetBridgesByLocation will return all Bridges with the given Location, if found, and an error. +func (s *Storage) GetBridgesByLocation(location string) ([]models.Bridge, error) { + compare := func(bridge models.Bridge) bool { + return bridge.Location == location + } + return s.getBridges(compare) +} + +// DeleteBridge accepts a name and an ID. If you want to delete by name, pass 0 as the ID; +// if you want to delete by ID, pass the empty string as name. +func (s *Storage) DeleteBridge(name string) error { + bridges, err := s.getAllBridges() + if err != nil { + return err + } + + compare := func(bridge models.Bridge) bool { + return bridge.Name == name + } + + updatedBridges := func() []models.Bridge { + for i, bridge := range bridges { + if compare(bridge) { + // Swap with the last element + lastIndex := len(bridges) - 1 + bridges[i] = bridges[lastIndex] // Move the last element to the current position + return bridges[:lastIndex] // Return the truncated slice + } + } + return bridges // Return original slice if ID not found + }() + + return s.saveBridges(updatedBridges) + +} + +// Close closes the db connection +func (s *Storage) Close() { + _ = s.store.Close() +} + +// GetStorage returns an initialized storage instance +// if the global appStorage was initialized it has precedence +func GetStorage() (*Storage, error) { + var storage *Storage + var err error + if appStorage == nil { + storage, err = NewStorageWithDefaultDir() + if err != nil { + return nil, err + } + } else { + storage = appStorage + } + return storage, nil + +} + +func getFQDNFromIntroducerURL(introducerURL string) (*string, error) { + // Parse the introducer URL string + u, err := url.Parse(introducerURL) + if err != nil { + return nil, fmt.Errorf("failed to parse introducer URL: %w", err) + } + + // Extract FQDN from query parameters + fqdn := u.Query().Get("fqdn") + if fqdn == "" { + return nil, fmt.Errorf("FQDN not found in the introducer URL") + } + + return &fqdn, nil +} + +// UpdateLastUsedForIntroducer will attempt to update the LastUsed timestamp for the introducer +// that matches the passed URL. +func (s *Storage) UpdateLastUsedForIntroducer(url string) error { + var compare CompareIntroducer = func(introducer models.Introducer) bool { + return introducer.URL == url + } + introducers, err := s.updateFirstIntroducer(compare, time.Now()) + if err != nil { + return err + } + + return s.saveIntroducers(introducers) +} + +func (s *Storage) SaveFallbackCountryCode(cc string) { + s.store.SetString(COUNTRYCODE, cc) +} + +func (s *Storage) GetFallbackCountryCode() string { + return s.store.GetString(COUNTRYCODE) +} diff --git a/vendor/0xacab.org/leap/bitmask-core/pkg/storage/store.go b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/store.go new file mode 100644 index 0000000000000000000000000000000000000000..85ec2c53df67c0b07f94cf766ce1ccc68e88a126 --- /dev/null +++ b/vendor/0xacab.org/leap/bitmask-core/pkg/storage/store.go @@ -0,0 +1,28 @@ +package storage + +type Store interface { + // Key-Value Getters + GetString(key string) string + GetStringWithDefault(key string, value string) string + GetBoolean(key string) bool + GetBooleanWithDefault(key string, value bool) bool + GetInt(key string) int + GetIntWithDefault(key string, value int) int + GetLong(key string) int64 + GetLongWithDefault(key string, value int64) int64 + GetByteArray(key string) []byte + GetByteArrayWithDefault(key string, value []byte) []byte + + // Key-Value Setters + SetString(key string, value string) + SetBoolean(key string, value bool) + SetInt(key string, value int) + SetLong(key string, value int64) + SetByteArray(key string, value []byte) + + Open() error + Close() error + Contains(key string) (bool, error) + Remove(key string) error + Clear() error +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 823605be4edca90513ef1056218944526a3d5996..fa83a78a184fccae88220c9e5d3709aef058d1bf 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# 0xacab.org/leap/bitmask-core v0.0.0-20240830161617-121267e4c4eb +# 0xacab.org/leap/bitmask-core v0.0.0-20241015185856-0812b9aadf98 ## explicit; go 1.22.2 0xacab.org/leap/bitmask-core/models 0xacab.org/leap/bitmask-core/pkg/bootstrap