diff options
| -rw-r--r-- | client/client.go | 28 | ||||
| -rw-r--r-- | client/client_test.go | 76 | ||||
| -rw-r--r-- | client/discovery.go | 2 | ||||
| -rw-r--r-- | client/fsm.go | 2 | ||||
| -rw-r--r-- | cmd/eduvpn-cli/main.go | 4 | ||||
| -rw-r--r-- | exports/exports.go | 5 | ||||
| -rw-r--r-- | exports/exports_test_wrapper.go | 4 | ||||
| -rw-r--r-- | i18n/err/i18nerr.go (renamed from i18nerr/i18nerr.go) | 0 | ||||
| -rw-r--r-- | i18n/i18n.go (renamed from util/util.go) | 36 | ||||
| -rw-r--r-- | i18n/i18n_test.go | 47 | ||||
| -rw-r--r-- | internal/config/config.go | 3 | ||||
| -rw-r--r-- | internal/log/log.go | 4 | ||||
| -rw-r--r-- | internal/server/secureinternet.go | 32 | ||||
| -rw-r--r-- | internal/server/secureinternet_test.go (renamed from internal/util/util_test.go) | 2 | ||||
| -rw-r--r-- | internal/util/util.go | 44 | ||||
| -rw-r--r-- | util/util_test.go | 126 |
16 files changed, 193 insertions, 222 deletions
diff --git a/client/client.go b/client/client.go index d668eb0..59b425d 100644 --- a/client/client.go +++ b/client/client.go @@ -7,11 +7,12 @@ import ( "context" "errors" "log/slog" + "net" "os" "sync" "time" - "codeberg.org/eduVPN/eduvpn-common/i18nerr" + "codeberg.org/eduVPN/eduvpn-common/i18n/err" "codeberg.org/eduVPN/eduvpn-common/internal/api" "codeberg.org/eduVPN/eduvpn-common/internal/config" "codeberg.org/eduVPN/eduvpn-common/internal/discovery" @@ -25,6 +26,31 @@ import ( "github.com/jwijenbergh/eduoauth-go" ) +// CalculateGateway takes a CIDR encoded subnet `cidr` and returns the gateway and an error +// TODO: move this somewhere else? +func CalculateGateway(cidr string) (string, error) { + _, ipn, err := net.ParseCIDR(cidr) + if err != nil { + return "", i18nerr.WrapInternalf(err, "failed to parse CIDR for calculating gateway: %v", cidr) + } + + ret := make(net.IP, len(ipn.IP)) + copy(ret, ipn.IP) + + for i := len(ret) - 1; i >= 0; i-- { + ret[i]++ + if ret[i] > 0 { + break + } + } + + if !ipn.Contains(ret) { + return "", i18nerr.Newf("IP network does not contain incremented IP: %v", ret) + } + + return ret.String(), nil +} + // Client is the main struct for the VPN client. type Client struct { // The name of the client diff --git a/client/client_test.go b/client/client_test.go index 9f302c4..d623037 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -11,12 +11,88 @@ import ( "time" httpw "codeberg.org/eduVPN/eduvpn-common/internal/http" + "codeberg.org/eduVPN/eduvpn-common/internal/test" "codeberg.org/eduVPN/eduvpn-common/types/cookie" "codeberg.org/eduVPN/eduvpn-common/types/protocol" srvtypes "codeberg.org/eduVPN/eduvpn-common/types/server" "github.com/jwijenbergh/eduoauth-go" ) +func TestCalculateGateway(t *testing.T) { + cases := []struct { + in string + want string + err string + }{ + // normal cases + { + in: "10.10.10.5/24", + want: "10.10.10.1", + err: "", + }, + { + in: "10.10.10.130/25", + want: "10.10.10.129", + err: "", + }, + { + in: "fd42::5/112", + want: "fd42::1", + err: "", + }, + { + in: "5502:df9::/64", + want: "5502:df9::1", + err: "", + }, + // unrealistic scenario but we have to handle these! + { + in: "5502:df9::0/128", + want: "", + err: "IP network does not contain incremented IP: 5502:df9::1", + }, + { + in: "5502:df9::ffff/128", + want: "", + err: "IP network does not contain incremented IP: 5502:df9::1:0", + }, + { + in: "10.0.0.0/32", + want: "", + err: "IP network does not contain incremented IP: 10.0.0.1", + }, + { + in: "10.0.0.255/32", + want: "", + err: "IP network does not contain incremented IP: 10.0.1.0", + }, + // parsing errors + { + in: "10.0.0.1", + want: "", + err: "An internal error occurred. The cause of the error is: invalid CIDR address: 10.0.0.1.", + }, + { + in: "bla", + want: "", + err: "An internal error occurred. The cause of the error is: invalid CIDR address: bla.", + }, + { + in: "5502:df9::ffff", + want: "", + err: "An internal error occurred. The cause of the error is: invalid CIDR address: 5502:df9::ffff.", + }, + } + + for _, c := range cases { + got, err := CalculateGateway(c.in) + test.AssertError(t, err, c.err) + if got != c.want { + t.Fatalf("got: %v not equal to want: %v", got, c.want) + } + } +} + func getServerURI(t *testing.T) string { serverURI := os.Getenv("SERVER_URI") if serverURI == "" { diff --git a/client/discovery.go b/client/discovery.go index b1cae32..b434025 100644 --- a/client/discovery.go +++ b/client/discovery.go @@ -5,7 +5,7 @@ import ( "sort" "strings" - "codeberg.org/eduVPN/eduvpn-common/i18nerr" + "codeberg.org/eduVPN/eduvpn-common/i18n/err" "codeberg.org/eduVPN/eduvpn-common/types/cookie" discotypes "codeberg.org/eduVPN/eduvpn-common/types/discovery" ) diff --git a/client/fsm.go b/client/fsm.go index 6fffc8a..673f3fb 100644 --- a/client/fsm.go +++ b/client/fsm.go @@ -4,7 +4,7 @@ import ( "fmt" "log/slog" - "codeberg.org/eduVPN/eduvpn-common/i18nerr" + "codeberg.org/eduVPN/eduvpn-common/i18n/err" "codeberg.org/eduVPN/eduvpn-common/internal/fsm" ) diff --git a/cmd/eduvpn-cli/main.go b/cmd/eduvpn-cli/main.go index 2dd0c31..92c37f5 100644 --- a/cmd/eduvpn-cli/main.go +++ b/cmd/eduvpn-cli/main.go @@ -10,10 +10,10 @@ import ( "strings" "codeberg.org/eduVPN/eduvpn-common/client" + "codeberg.org/eduVPN/eduvpn-common/i18n" "codeberg.org/eduVPN/eduvpn-common/internal/version" "codeberg.org/eduVPN/eduvpn-common/types/cookie" srvtypes "codeberg.org/eduVPN/eduvpn-common/types/server" - "codeberg.org/eduVPN/eduvpn-common/util" "github.com/pkg/browser" ) @@ -39,7 +39,7 @@ func getProfileInteractive(profiles *srvtypes.Profiles, data any) (string, error var options []string i := 0 for k, v := range profiles.Map { - ps += fmt.Sprintf("\n%d - %s", i+1, util.GetLanguageMatched(v.DisplayName, "en")) + ps += fmt.Sprintf("\n%d - %s", i+1, i18n.GetLanguageMatched(v.DisplayName, "en")) options = append(options, k) i++ } diff --git a/exports/exports.go b/exports/exports.go index 47b9f14..9be3b66 100644 --- a/exports/exports.go +++ b/exports/exports.go @@ -26,11 +26,10 @@ import ( "unsafe" "codeberg.org/eduVPN/eduvpn-common/client" - "codeberg.org/eduVPN/eduvpn-common/i18nerr" + "codeberg.org/eduVPN/eduvpn-common/i18n/err" "codeberg.org/eduVPN/eduvpn-common/types/cookie" errtypes "codeberg.org/eduVPN/eduvpn-common/types/error" srvtypes "codeberg.org/eduVPN/eduvpn-common/types/server" - "codeberg.org/eduVPN/eduvpn-common/util" ) // goString copies a null-terminated *C.char to a Go string. @@ -1053,7 +1052,7 @@ func SetTokenHandler(getter C.TokenGetter, setter C.TokenSetter) *C.char { // //export CalculateGateway func CalculateGateway(subnet *C.char) (*C.char, *C.char) { - gw, err := util.CalculateGateway(goString(subnet)) + gw, err := client.CalculateGateway(goString(subnet)) if err != nil { return nil, getCError(err) } diff --git a/exports/exports_test_wrapper.go b/exports/exports_test_wrapper.go index de6aa21..0c9b9dc 100644 --- a/exports/exports_test_wrapper.go +++ b/exports/exports_test_wrapper.go @@ -21,9 +21,9 @@ import ( "testing" "time" + "codeberg.org/eduVPN/eduvpn-common/i18n" "codeberg.org/eduVPN/eduvpn-common/internal/test" "codeberg.org/eduVPN/eduvpn-common/types/error" - "codeberg.org/eduVPN/eduvpn-common/util" httpw "codeberg.org/eduVPN/eduvpn-common/internal/http" ) @@ -49,7 +49,7 @@ func getError(t *testing.T, gerr *C.char) string { t.Fatalf("failed getting error JSON, val: %v, err: %v", jsonErr, jerr) } - return util.GetLanguageMatched(transl.Message, "en") + return i18n.GetLanguageMatched(transl.Message, "en") } // ClonedAskTransition is a clone of the struct types/server.go RequiredAskTransition diff --git a/i18nerr/i18nerr.go b/i18n/err/i18nerr.go index 8254dd4..8254dd4 100644 --- a/i18nerr/i18nerr.go +++ b/i18n/err/i18nerr.go diff --git a/util/util.go b/i18n/i18n.go index 4609199..02b9028 100644 --- a/util/util.go +++ b/i18n/i18n.go @@ -1,37 +1,7 @@ -// Package util defines public utility functions to be used by applications -// these are outside of the client package as they can be used even if a client hasn't been created yet -package util +// Package i18n implements utility functions for internationalization +package i18n -import ( - "net" - "strings" - - "codeberg.org/eduVPN/eduvpn-common/i18nerr" -) - -// CalculateGateway takes a CIDR encoded subnet `cidr` and returns the gateway and an error -func CalculateGateway(cidr string) (string, error) { - _, ipn, err := net.ParseCIDR(cidr) - if err != nil { - return "", i18nerr.WrapInternalf(err, "failed to parse CIDR for calculating gateway: %v", cidr) - } - - ret := make(net.IP, len(ipn.IP)) - copy(ret, ipn.IP) - - for i := len(ret) - 1; i >= 0; i-- { - ret[i]++ - if ret[i] > 0 { - break - } - } - - if !ipn.Contains(ret) { - return "", i18nerr.Newf("IP network does not contain incremented IP: %v", ret) - } - - return ret.String(), nil -} +import "strings" // GetLanguageMatched uses a map from language tags to strings to extract the right language given the tag // It implements it according to https://github.com/eduvpn/documentation/blob/dc4d53c47dd7a69e95d6650eec408e16eaa814a2/SERVER_DISCOVERY.md#language-matching diff --git a/i18n/i18n_test.go b/i18n/i18n_test.go new file mode 100644 index 0000000..fc671a4 --- /dev/null +++ b/i18n/i18n_test.go @@ -0,0 +1,47 @@ +package i18n + +import "testing" + +func TestGetLanguageMatched(t *testing.T) { + // exact match + returned := GetLanguageMatched(map[string]string{"en": "test", "de": "test2"}, "en") + if returned != "test" { + t.Fatalf("Got: %s, want: %s", returned, "test") + } + + // starts with language tag + returned = GetLanguageMatched(map[string]string{"en-US-test": "test", "de": "test2"}, "en-US") + if returned != "test" { + t.Fatalf("Got: %s, want: %s", returned, "test") + } + + // starts with en- + returned = GetLanguageMatched(map[string]string{"en-UK": "test", "en": "test2"}, "en-US") + if returned != "test" { + t.Fatalf("Got: %s, want: %s", returned, "test") + } + + // exact match for en + returned = GetLanguageMatched(map[string]string{"de": "test", "en": "test2"}, "en-US") + if returned != "test2" { + t.Fatalf("Got: %s, want: %s", returned, "test2") + } + + // We default to english + returned = GetLanguageMatched(map[string]string{"es": "test", "en": "test2"}, "nl-NL") + if returned != "test2" { + t.Fatalf("Got: %s, want: %s", returned, "test2") + } + + // We default to english with a - as well + returned = GetLanguageMatched(map[string]string{"est": "test", "en-": "test2"}, "en-US") + if returned != "test2" { + t.Fatalf("Got: %s, want: %s", returned, "test2") + } + + // None found just return one + returned = GetLanguageMatched(map[string]string{"es": "test"}, "en-US") + if returned != "test" { + t.Fatalf("Got: %s, want: %s", returned, "test") + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 06da9b3..e324ebb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,7 +12,6 @@ import ( "codeberg.org/eduVPN/eduvpn-common/internal/config/v1" "codeberg.org/eduVPN/eduvpn-common/internal/config/v2" "codeberg.org/eduVPN/eduvpn-common/internal/discovery" - "codeberg.org/eduVPN/eduvpn-common/internal/util" ) const stateFile = "state.json" @@ -40,7 +39,7 @@ func (c *Config) HasSecureInternet() bool { // Save saves the state file to disk func (c *Config) Save() error { - if err := util.EnsureDirectory(c.directory); err != nil { + if err := os.MkdirAll(c.directory, 0o700); err != nil { return err } diff --git a/internal/log/log.go b/internal/log/log.go index 47cdcf3..53671b3 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -7,8 +7,6 @@ import ( "log/slog" "os" "path" - - "codeberg.org/eduVPN/eduvpn-common/internal/util" ) // Init initializes the logger by setting a max level 'level' and a directory 'directory' where the log should be stored @@ -17,7 +15,7 @@ import ( // It returns the log file and the error // This log file should be closed at the end func Init(lvl slog.Level, dir string) (*os.File, error) { - err := util.EnsureDirectory(dir) + err := os.MkdirAll(dir, 0o700) if err != nil { return nil, err } diff --git a/internal/server/secureinternet.go b/internal/server/secureinternet.go index f97cef1..e0d081a 100644 --- a/internal/server/secureinternet.go +++ b/internal/server/secureinternet.go @@ -4,16 +4,42 @@ import ( "context" "errors" "log/slog" + "net/url" + "strings" "time" "codeberg.org/eduVPN/eduvpn-common/internal/api" "codeberg.org/eduVPN/eduvpn-common/internal/config/v2" "codeberg.org/eduVPN/eduvpn-common/internal/discovery" - "codeberg.org/eduVPN/eduvpn-common/internal/util" "codeberg.org/eduVPN/eduvpn-common/types/server" "github.com/jwijenbergh/eduoauth-go" ) +// ReplaceWAYF replaces an authorization template containing of @RETURN_TO@ and @ORG_ID@ with the authorization URL and the organization ID +// See https://github.com/eduvpn/documentation/blob/dc4d53c47dd7a69e95d6650eec408e16eaa814a2/SERVER_DISCOVERY_SKIP_WAYF.md +func ReplaceWAYF(template string, authURL string, orgID string) string { + // We just return the authURL in the cases where the template is not given or is invalid + if template == "" { + return authURL + } + if !strings.Contains(template, "@RETURN_TO@") { + return authURL + } + if !strings.Contains(template, "@ORG_ID@") { + return authURL + } + // Replace authURL + template = strings.Replace(template, "@RETURN_TO@", url.QueryEscape(authURL), 1) + + // If now there is no more ORG_ID, return as there weren't enough @ symbols + if !strings.Contains(template, "@ORG_ID@") { + return authURL + } + // Replace ORG ID + template = strings.Replace(template, "@ORG_ID@", url.QueryEscape(orgID), 1) + return template +} + // AddSecure adds a secure internet server // `ctx` is the context used for cancellation // `disco` are the discovery servers @@ -47,7 +73,7 @@ func (s *Servers) AddSecure(ctx context.Context, discom *discovery.Manager, orgI if err != nil { return "", err } - ret := util.ReplaceWAYF(updsrv.AuthenticationURLTemplate, url, updorg.OrgID) + ret := ReplaceWAYF(updsrv.AuthenticationURLTemplate, url, updorg.OrgID) return ret, nil }, } @@ -127,7 +153,7 @@ func (s *Servers) GetSecure(ctx context.Context, orgID string, discom *discovery if err != nil { return "", err } - ret := util.ReplaceWAYF(updsrv.AuthenticationURLTemplate, url, updorg.OrgID) + ret := ReplaceWAYF(updsrv.AuthenticationURLTemplate, url, updorg.OrgID) return ret, nil }, DisableAuthorize: disableAuth, diff --git a/internal/util/util_test.go b/internal/server/secureinternet_test.go index 827fbe1..8a4466e 100644 --- a/internal/util/util_test.go +++ b/internal/server/secureinternet_test.go @@ -1,4 +1,4 @@ -package util +package server import "testing" diff --git a/internal/util/util.go b/internal/util/util.go deleted file mode 100644 index 97b4151..0000000 --- a/internal/util/util.go +++ /dev/null @@ -1,44 +0,0 @@ -// Package util implements several utility functions that are used across the codebase -package util - -import ( - "fmt" - "net/url" - "os" - "strings" -) - -// EnsureDirectory creates a directory with permission 700. -func EnsureDirectory(dir string) error { - // Create with 700 permissions, read, write, execute only for the owner - err := os.MkdirAll(dir, 0o700) - if err != nil { - return fmt.Errorf("failed to create directory '%s' with error: %w", dir, err) - } - return nil -} - -// ReplaceWAYF replaces an authorization template containing of @RETURN_TO@ and @ORG_ID@ with the authorization URL and the organization ID -// See https://github.com/eduvpn/documentation/blob/dc4d53c47dd7a69e95d6650eec408e16eaa814a2/SERVER_DISCOVERY_SKIP_WAYF.md -func ReplaceWAYF(template string, authURL string, orgID string) string { - // We just return the authURL in the cases where the template is not given or is invalid - if template == "" { - return authURL - } - if !strings.Contains(template, "@RETURN_TO@") { - return authURL - } - if !strings.Contains(template, "@ORG_ID@") { - return authURL - } - // Replace authURL - template = strings.Replace(template, "@RETURN_TO@", url.QueryEscape(authURL), 1) - - // If now there is no more ORG_ID, return as there weren't enough @ symbols - if !strings.Contains(template, "@ORG_ID@") { - return authURL - } - // Replace ORG ID - template = strings.Replace(template, "@ORG_ID@", url.QueryEscape(orgID), 1) - return template -} diff --git a/util/util_test.go b/util/util_test.go deleted file mode 100644 index fd35088..0000000 --- a/util/util_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package util - -import ( - "testing" - - "codeberg.org/eduVPN/eduvpn-common/internal/test" -) - -func TestCalculateGateway(t *testing.T) { - cases := []struct { - in string - want string - err string - }{ - // normal cases - { - in: "10.10.10.5/24", - want: "10.10.10.1", - err: "", - }, - { - in: "10.10.10.130/25", - want: "10.10.10.129", - err: "", - }, - { - in: "fd42::5/112", - want: "fd42::1", - err: "", - }, - { - in: "5502:df9::/64", - want: "5502:df9::1", - err: "", - }, - // unrealistic scenario but we have to handle these! - { - in: "5502:df9::0/128", - want: "", - err: "IP network does not contain incremented IP: 5502:df9::1", - }, - { - in: "5502:df9::ffff/128", - want: "", - err: "IP network does not contain incremented IP: 5502:df9::1:0", - }, - { - in: "10.0.0.0/32", - want: "", - err: "IP network does not contain incremented IP: 10.0.0.1", - }, - { - in: "10.0.0.255/32", - want: "", - err: "IP network does not contain incremented IP: 10.0.1.0", - }, - // parsing errors - { - in: "10.0.0.1", - want: "", - err: "An internal error occurred. The cause of the error is: invalid CIDR address: 10.0.0.1.", - }, - { - in: "bla", - want: "", - err: "An internal error occurred. The cause of the error is: invalid CIDR address: bla.", - }, - { - in: "5502:df9::ffff", - want: "", - err: "An internal error occurred. The cause of the error is: invalid CIDR address: 5502:df9::ffff.", - }, - } - - for _, c := range cases { - got, err := CalculateGateway(c.in) - test.AssertError(t, err, c.err) - if got != c.want { - t.Fatalf("got: %v not equal to want: %v", got, c.want) - } - } -} - -func TestGetLanguageMatched(t *testing.T) { - // exact match - returned := GetLanguageMatched(map[string]string{"en": "test", "de": "test2"}, "en") - if returned != "test" { - t.Fatalf("Got: %s, want: %s", returned, "test") - } - - // starts with language tag - returned = GetLanguageMatched(map[string]string{"en-US-test": "test", "de": "test2"}, "en-US") - if returned != "test" { - t.Fatalf("Got: %s, want: %s", returned, "test") - } - - // starts with en- - returned = GetLanguageMatched(map[string]string{"en-UK": "test", "en": "test2"}, "en-US") - if returned != "test" { - t.Fatalf("Got: %s, want: %s", returned, "test") - } - - // exact match for en - returned = GetLanguageMatched(map[string]string{"de": "test", "en": "test2"}, "en-US") - if returned != "test2" { - t.Fatalf("Got: %s, want: %s", returned, "test2") - } - - // We default to english - returned = GetLanguageMatched(map[string]string{"es": "test", "en": "test2"}, "nl-NL") - if returned != "test2" { - t.Fatalf("Got: %s, want: %s", returned, "test2") - } - - // We default to english with a - as well - returned = GetLanguageMatched(map[string]string{"est": "test", "en-": "test2"}, "en-US") - if returned != "test2" { - t.Fatalf("Got: %s, want: %s", returned, "test2") - } - - // None found just return one - returned = GetLanguageMatched(map[string]string{"es": "test"}, "en-US") - if returned != "test" { - t.Fatalf("Got: %s, want: %s", returned, "test") - } -} |
