From 27b95b4911da055fe9b5fb37b5fb4a33eda6b989 Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Mon, 25 Aug 2025 10:59:37 +0200 Subject: All: Remove util packages Was giving linting errors and it's not a good idea anyways --- client/client.go | 28 +++++- client/client_test.go | 76 +++++++++++++++ client/discovery.go | 2 +- client/fsm.go | 2 +- cmd/eduvpn-cli/main.go | 4 +- exports/exports.go | 5 +- exports/exports_test_wrapper.go | 4 +- i18n/err/i18nerr.go | 168 +++++++++++++++++++++++++++++++++ i18n/i18n.go | 54 +++++++++++ i18n/i18n_test.go | 47 +++++++++ i18nerr/i18nerr.go | 168 --------------------------------- internal/config/config.go | 3 +- internal/log/log.go | 4 +- internal/server/secureinternet.go | 32 ++++++- internal/server/secureinternet_test.go | 45 +++++++++ internal/util/util.go | 44 --------- internal/util/util_test.go | 45 --------- util/util.go | 84 ----------------- util/util_test.go | 126 ------------------------- 19 files changed, 456 insertions(+), 485 deletions(-) create mode 100644 i18n/err/i18nerr.go create mode 100644 i18n/i18n.go create mode 100644 i18n/i18n_test.go delete mode 100644 i18nerr/i18nerr.go create mode 100644 internal/server/secureinternet_test.go delete mode 100644 internal/util/util.go delete mode 100644 internal/util/util_test.go delete mode 100644 util/util.go delete mode 100644 util/util_test.go 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/i18n/err/i18nerr.go b/i18n/err/i18nerr.go new file mode 100644 index 0000000..8254dd4 --- /dev/null +++ b/i18n/err/i18nerr.go @@ -0,0 +1,168 @@ +// Package i18nerr implements errors with internationalization using gotext +package i18nerr + +import ( + "context" + "errors" + "fmt" + "log/slog" + "sync" + + "codeberg.org/eduVPN/eduvpn-common/internal/http" + + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +var ( + printers sync.Map + once sync.Once +) + +// TranslatedInner defines errors that are used as inner causes but are still translated because they can happen frequently +func TranslatedInner(inner error) (string, bool) { + unwrapped := inner + for errors.Unwrap(unwrapped) != nil { + unwrapped = errors.Unwrap(unwrapped) + } + + var tErr *http.TimeoutError + switch { + case errors.As(inner, &tErr): + return printerOrNew(language.English).Sprintf("Timeout reached contacting URL: '%s'", tErr.URL), false + case errors.Is(inner, context.Canceled): + return unwrapped.Error(), true + } + return unwrapped.Error(), false +} + +// Error wraps an actual error with the translation key +// This translation key is later used to lookup translation +// The inner error always consists of the translation key and some formatting +type Error struct { + key message.Reference + args []any + wrapped *Error + Misc bool +} + +func (e *Error) translated(t language.Tag) string { + once.Do(func() { + inititializeLangs() + }) + msg := printerOrNew(t).Sprintf(e.key, e.args...) + if e.wrapped != nil { + return printerOrNew(t).Sprintf("%s. The cause of the error is: %s.", msg, e.wrapped.Error()) + } + return msg +} + +// Error gets the error string +// it does this by simply forwarding the error method from the actual inner error +func (e *Error) Error() string { + return e.translated(language.English) +} + +// Translations returns all the translations for the error including the source translation (english) +func (e *Error) Translations() map[string]string { + translations := make(map[string]string) + // add the source transltaion first + source := e.Error() + translations[language.English.String()] = source + for _, t := range message.DefaultCatalog.Languages() { + // already added + if t == language.English { + continue + } + // get the final translation string for the tag + // and add it if it's not equal to the english version + f := e.translated(t) + if f != source { + translations[t.String()] = f + } + } + return translations +} + +// Unwrap returns the unwrapped error +// it does this by unwrapping the inner error +func (e *Error) Unwrap() error { + if e.wrapped == nil { + return nil + } + return e.wrapped.Unwrap() +} + +// printerOrNew gets a message printer from the global printers map using the tag 'tag' +// If the printer cannot be found in the sync map, we return a new printer +func printerOrNew(tag language.Tag) *message.Printer { + v, ok := printers.Load(tag) + if !ok { + slog.Debug("i18n could not load printer from map", "tag", tag) + return message.NewPrinter(tag) + } + p, ok := v.(*message.Printer) + if !ok { + slog.Debug("i18n could not load printer from map with incorrect type", "tag", tag, "type", fmt.Sprintf("%T", p)) + return message.NewPrinter(tag) + } + return p +} + +// New creates a new i18n error using a message reference +func New(key message.Reference) *Error { + _ = printerOrNew(language.English).Sprint(key) + return &Error{key: key} +} + +// Newf creates a new i18n error using a message reference and arguments. +// It formats this with fmt.Errorf +func Newf(key message.Reference, args ...any) *Error { + _ = printerOrNew(language.English).Sprintf(key, args...) + return &Error{key: key, args: args} +} + +// Wrap creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key'. +// It formats this with fmt.Errorf +func Wrap(err error, key message.Reference) *Error { + _ = printerOrNew(language.English).Sprintf(key) + t, misc := TranslatedInner(err) + return &Error{key: key, wrapped: &Error{key: t, Misc: misc}, Misc: misc} +} + +// Wrapf creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key' with format arguments 'args'. +// It formats this with fmt.Errorf +func Wrapf(err error, key message.Reference, args ...any) *Error { + _ = printerOrNew(language.English).Sprintf(key, args...) + t, misc := TranslatedInner(err) + return &Error{key: key, args: args, wrapped: &Error{key: t, Misc: misc}, Misc: misc} +} + +// NewInternal creates an internal localised error from a display string +func NewInternal(disp string) *Error { + return Wrap(errors.New(disp), "An internal error occurred") +} + +// NewInternalf creates an internal localised error from a display string and arguments +func NewInternalf(disp string, args ...any) *Error { + return NewInternal(fmt.Sprintf(disp, args...)) +} + +// WrapInternal wraps an error and a display string into a localised internal error +func WrapInternal(err error, disp string) *Error { + return Wrap(fmt.Errorf("%s with internal cause: %w", disp, err), "An internal error occurred") +} + +// WrapInternalf wraps an error and a display string with args into a localised internal error +func WrapInternalf(err error, disp string, args ...any) *Error { + return WrapInternal(err, fmt.Sprintf(disp, args...)) +} + +// initializeLangs initializes the printers from the default catalog into the sync map +// we cannot do this in init() because this is too early +func inititializeLangs() { + slog.Debug("i18n initializing languages") + for _, t := range message.DefaultCatalog.Languages() { + printers.Store(t, message.NewPrinter(t)) + } +} diff --git a/i18n/i18n.go b/i18n/i18n.go new file mode 100644 index 0000000..02b9028 --- /dev/null +++ b/i18n/i18n.go @@ -0,0 +1,54 @@ +// Package i18n implements utility functions for internationalization +package i18n + +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 +func GetLanguageMatched(langMap map[string]string, langTag string) string { + // If no map is given, return the empty string + if len(langMap) == 0 { + return "" + } + // Try to find the exact match + if val, ok := langMap[langTag]; ok { + return val + } + // Try to find a key that starts with the OS language setting + for k := range langMap { + if strings.HasPrefix(k, langTag) { + return langMap[k] + } + } + // Try to find a key that starts with the first part of the OS language (e.g. de-) + pts := strings.Split(langTag, "-") + // We have a "-" + if len(pts) > 1 { + for k := range langMap { + if strings.HasPrefix(k, pts[0]+"-") { + return langMap[k] + } + } + } + // search for just the language (e.g. de) + for k := range langMap { + if k == pts[0] { + return langMap[k] + } + } + + // Pick one that is deemed best, e.g. en-US or en, but note that not all languages are always available! + // We force an entry that is english exactly or with an english prefix + for k := range langMap { + if k == "en" || strings.HasPrefix(k, "en-") { + return langMap[k] + } + } + + // Otherwise just return one + for k := range langMap { + return langMap[k] + } + + return "" +} 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/i18nerr/i18nerr.go b/i18nerr/i18nerr.go deleted file mode 100644 index 8254dd4..0000000 --- a/i18nerr/i18nerr.go +++ /dev/null @@ -1,168 +0,0 @@ -// Package i18nerr implements errors with internationalization using gotext -package i18nerr - -import ( - "context" - "errors" - "fmt" - "log/slog" - "sync" - - "codeberg.org/eduVPN/eduvpn-common/internal/http" - - "golang.org/x/text/language" - "golang.org/x/text/message" -) - -var ( - printers sync.Map - once sync.Once -) - -// TranslatedInner defines errors that are used as inner causes but are still translated because they can happen frequently -func TranslatedInner(inner error) (string, bool) { - unwrapped := inner - for errors.Unwrap(unwrapped) != nil { - unwrapped = errors.Unwrap(unwrapped) - } - - var tErr *http.TimeoutError - switch { - case errors.As(inner, &tErr): - return printerOrNew(language.English).Sprintf("Timeout reached contacting URL: '%s'", tErr.URL), false - case errors.Is(inner, context.Canceled): - return unwrapped.Error(), true - } - return unwrapped.Error(), false -} - -// Error wraps an actual error with the translation key -// This translation key is later used to lookup translation -// The inner error always consists of the translation key and some formatting -type Error struct { - key message.Reference - args []any - wrapped *Error - Misc bool -} - -func (e *Error) translated(t language.Tag) string { - once.Do(func() { - inititializeLangs() - }) - msg := printerOrNew(t).Sprintf(e.key, e.args...) - if e.wrapped != nil { - return printerOrNew(t).Sprintf("%s. The cause of the error is: %s.", msg, e.wrapped.Error()) - } - return msg -} - -// Error gets the error string -// it does this by simply forwarding the error method from the actual inner error -func (e *Error) Error() string { - return e.translated(language.English) -} - -// Translations returns all the translations for the error including the source translation (english) -func (e *Error) Translations() map[string]string { - translations := make(map[string]string) - // add the source transltaion first - source := e.Error() - translations[language.English.String()] = source - for _, t := range message.DefaultCatalog.Languages() { - // already added - if t == language.English { - continue - } - // get the final translation string for the tag - // and add it if it's not equal to the english version - f := e.translated(t) - if f != source { - translations[t.String()] = f - } - } - return translations -} - -// Unwrap returns the unwrapped error -// it does this by unwrapping the inner error -func (e *Error) Unwrap() error { - if e.wrapped == nil { - return nil - } - return e.wrapped.Unwrap() -} - -// printerOrNew gets a message printer from the global printers map using the tag 'tag' -// If the printer cannot be found in the sync map, we return a new printer -func printerOrNew(tag language.Tag) *message.Printer { - v, ok := printers.Load(tag) - if !ok { - slog.Debug("i18n could not load printer from map", "tag", tag) - return message.NewPrinter(tag) - } - p, ok := v.(*message.Printer) - if !ok { - slog.Debug("i18n could not load printer from map with incorrect type", "tag", tag, "type", fmt.Sprintf("%T", p)) - return message.NewPrinter(tag) - } - return p -} - -// New creates a new i18n error using a message reference -func New(key message.Reference) *Error { - _ = printerOrNew(language.English).Sprint(key) - return &Error{key: key} -} - -// Newf creates a new i18n error using a message reference and arguments. -// It formats this with fmt.Errorf -func Newf(key message.Reference, args ...any) *Error { - _ = printerOrNew(language.English).Sprintf(key, args...) - return &Error{key: key, args: args} -} - -// Wrap creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key'. -// It formats this with fmt.Errorf -func Wrap(err error, key message.Reference) *Error { - _ = printerOrNew(language.English).Sprintf(key) - t, misc := TranslatedInner(err) - return &Error{key: key, wrapped: &Error{key: t, Misc: misc}, Misc: misc} -} - -// Wrapf creates a new i18n error using an error to be wrapped 'err' and a prefix message reference 'key' with format arguments 'args'. -// It formats this with fmt.Errorf -func Wrapf(err error, key message.Reference, args ...any) *Error { - _ = printerOrNew(language.English).Sprintf(key, args...) - t, misc := TranslatedInner(err) - return &Error{key: key, args: args, wrapped: &Error{key: t, Misc: misc}, Misc: misc} -} - -// NewInternal creates an internal localised error from a display string -func NewInternal(disp string) *Error { - return Wrap(errors.New(disp), "An internal error occurred") -} - -// NewInternalf creates an internal localised error from a display string and arguments -func NewInternalf(disp string, args ...any) *Error { - return NewInternal(fmt.Sprintf(disp, args...)) -} - -// WrapInternal wraps an error and a display string into a localised internal error -func WrapInternal(err error, disp string) *Error { - return Wrap(fmt.Errorf("%s with internal cause: %w", disp, err), "An internal error occurred") -} - -// WrapInternalf wraps an error and a display string with args into a localised internal error -func WrapInternalf(err error, disp string, args ...any) *Error { - return WrapInternal(err, fmt.Sprintf(disp, args...)) -} - -// initializeLangs initializes the printers from the default catalog into the sync map -// we cannot do this in init() because this is too early -func inititializeLangs() { - slog.Debug("i18n initializing languages") - for _, t := range message.DefaultCatalog.Languages() { - printers.Store(t, message.NewPrinter(t)) - } -} 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/server/secureinternet_test.go b/internal/server/secureinternet_test.go new file mode 100644 index 0000000..8a4466e --- /dev/null +++ b/internal/server/secureinternet_test.go @@ -0,0 +1,45 @@ +package server + +import "testing" + +func TestReplaceWAYF(t *testing.T) { + // We expect url encoding but the spaces to be correctly replace with a + instead of a %20 + // And we expect that the return to and org_id are correctly replaced + replaced := ReplaceWAYF( + "@RETURN_TO@@ORG_ID@", + "127.0.0.1:8000/&%$3#kM_- ", + "idp-test.nl.org/", + ) + wantReplaced := "127.0.0.1%3A8000%2F%26%25%243%23kM_-++++++++++++idp-test.nl.org%2F" + if replaced != wantReplaced { + t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) + } + + // No RETURN_TO in template + replaced = ReplaceWAYF("@ORG_ID@", "127.0.0.1:8000", "idp-test.nl.org/") + wantReplaced = "127.0.0.1:8000" + if replaced != wantReplaced { + t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) + } + + // NO ORG_ID in template + replaced = ReplaceWAYF("@RETURN_TO@", "127.0.0.1:8000", "idp-test.nl.org") + wantReplaced = "127.0.0.1:8000" + if replaced != wantReplaced { + t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) + } + + // Template is empty + replaced = ReplaceWAYF("", "127.0.0.1:8000", "idp-test.nl.org") + wantReplaced = "127.0.0.1:8000" + if replaced != wantReplaced { + t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) + } + + // Template contains both @RETURN_TO@ and @ORG_ID@ but there is not enough to replace both + replaced = ReplaceWAYF("@RETURN_TO@ORG_ID@", "127.0.0.1:8000", "idp-test.nl.org") + wantReplaced = "127.0.0.1:8000" + if replaced != wantReplaced { + t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) + } +} 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/internal/util/util_test.go b/internal/util/util_test.go deleted file mode 100644 index 827fbe1..0000000 --- a/internal/util/util_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package util - -import "testing" - -func TestReplaceWAYF(t *testing.T) { - // We expect url encoding but the spaces to be correctly replace with a + instead of a %20 - // And we expect that the return to and org_id are correctly replaced - replaced := ReplaceWAYF( - "@RETURN_TO@@ORG_ID@", - "127.0.0.1:8000/&%$3#kM_- ", - "idp-test.nl.org/", - ) - wantReplaced := "127.0.0.1%3A8000%2F%26%25%243%23kM_-++++++++++++idp-test.nl.org%2F" - if replaced != wantReplaced { - t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) - } - - // No RETURN_TO in template - replaced = ReplaceWAYF("@ORG_ID@", "127.0.0.1:8000", "idp-test.nl.org/") - wantReplaced = "127.0.0.1:8000" - if replaced != wantReplaced { - t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) - } - - // NO ORG_ID in template - replaced = ReplaceWAYF("@RETURN_TO@", "127.0.0.1:8000", "idp-test.nl.org") - wantReplaced = "127.0.0.1:8000" - if replaced != wantReplaced { - t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) - } - - // Template is empty - replaced = ReplaceWAYF("", "127.0.0.1:8000", "idp-test.nl.org") - wantReplaced = "127.0.0.1:8000" - if replaced != wantReplaced { - t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) - } - - // Template contains both @RETURN_TO@ and @ORG_ID@ but there is not enough to replace both - replaced = ReplaceWAYF("@RETURN_TO@ORG_ID@", "127.0.0.1:8000", "idp-test.nl.org") - wantReplaced = "127.0.0.1:8000" - if replaced != wantReplaced { - t.Fatalf("Got: %s, want: %s", replaced, wantReplaced) - } -} diff --git a/util/util.go b/util/util.go deleted file mode 100644 index 4609199..0000000 --- a/util/util.go +++ /dev/null @@ -1,84 +0,0 @@ -// 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 - -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 -} - -// 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 -func GetLanguageMatched(langMap map[string]string, langTag string) string { - // If no map is given, return the empty string - if len(langMap) == 0 { - return "" - } - // Try to find the exact match - if val, ok := langMap[langTag]; ok { - return val - } - // Try to find a key that starts with the OS language setting - for k := range langMap { - if strings.HasPrefix(k, langTag) { - return langMap[k] - } - } - // Try to find a key that starts with the first part of the OS language (e.g. de-) - pts := strings.Split(langTag, "-") - // We have a "-" - if len(pts) > 1 { - for k := range langMap { - if strings.HasPrefix(k, pts[0]+"-") { - return langMap[k] - } - } - } - // search for just the language (e.g. de) - for k := range langMap { - if k == pts[0] { - return langMap[k] - } - } - - // Pick one that is deemed best, e.g. en-US or en, but note that not all languages are always available! - // We force an entry that is english exactly or with an english prefix - for k := range langMap { - if k == "en" || strings.HasPrefix(k, "en-") { - return langMap[k] - } - } - - // Otherwise just return one - for k := range langMap { - return langMap[k] - } - - return "" -} 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") - } -} -- cgit v1.2.3