package client import ( "context" "fmt" "os" "os/exec" "strconv" "strings" "testing" "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 == "" { t.Skip("Skipping server test as no SERVER_URI env var has been passed") } serverURI, parseErr := httpw.EnsureValidURL(serverURI, true) if parseErr != nil { t.Skip("Skipping server test as the server uri is not valid") } return serverURI } func runCommand(errBuffer *strings.Builder, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stderr = errBuffer err := cmd.Start() if err != nil { return err } return cmd.Wait() } func loginOAuthSelenium(ck *cookie.Cookie, url string) { // We could use the go selenium library // But it does not support the latest selenium v4 just yet var errBuffer strings.Builder err := runCommand(&errBuffer, "python3", "../selenium_eduvpn.py", url) if err != nil { _ = ck.Cancel() panic(fmt.Sprintf( "Login OAuth with selenium script failed with error %v and stderr %s", err, errBuffer.String(), )) } } func stateCallback( ck *cookie.Cookie, _ FSMStateID, newState FSMStateID, data any, ) { if newState == StateOAuthStarted { url, ok := data.(string) if !ok { panic("data is not a string for OAuth URL") } loginOAuthSelenium(ck, url) } } func TestServer(t *testing.T) { serverURI := getServerURI(t) ck := cookie.NewWithContext(context.Background()) defer ck.Cancel() //nolint:errcheck dir := t.TempDir() var state *Client state, err := New( "org.letsconnect-vpn.app.linux", "0.1.0-test", dir, func(oldState FSMStateID, newState FSMStateID, data any) bool { // test if main server server list succeeds if newState == StateMain { _, listErr := state.ServerList() if listErr != nil { t.Fatalf("Got server list error: %v", listErr) } } go stateCallback(ck, oldState, newState, data) return true }, ) if err != nil { t.Fatalf("Creating client error: %v", err) } err = state.Register() if err != nil { t.Fatalf("Registering error: %v", err) } addErr := state.AddServer(ck, serverURI, srvtypes.TypeCustom, nil) if addErr != nil { t.Fatalf("Add error: %v", addErr) } _, configErr := state.GetConfig(ck, serverURI, srvtypes.TypeCustom, false, false) if configErr != nil { t.Fatalf("Connect error: %v", configErr) } } func TestTokenExpired(t *testing.T) { serverURI := getServerURI(t) expiredTTL := os.Getenv("OAUTH_EXPIRED_TTL") if expiredTTL == "" { t.Log( "No expired TTL present, skipping this test. Set OAUTH_EXPIRED_TTL env variable to run this test", ) return } // Convert the env variable to an int and signal error if it is not possible expiredInt, expiredErr := strconv.Atoi(expiredTTL) if expiredErr != nil { t.Fatalf("Cannot convert EXPIRED_TTL env variable to an int with error %v", expiredErr) } // Get a vpn state ck := cookie.NewWithContext(context.Background()) defer ck.Cancel() //nolint:errcheck dir := t.TempDir() state, err := New( "org.letsconnect-vpn.app.linux", "0.1.0-test", dir, func(oldState FSMStateID, newState FSMStateID, data any) bool { go stateCallback(ck, oldState, newState, data) return true }, ) if err != nil { t.Fatalf("Creating client error: %v", err) } err = state.Register() if err != nil { t.Fatalf("Registering error: %v", err) } addErr := state.AddServer(ck, serverURI, srvtypes.TypeCustom, nil) if addErr != nil { t.Fatalf("Add error: %v", addErr) } _, configErr := state.GetConfig(ck, serverURI, srvtypes.TypeCustom, false, false) if configErr != nil { t.Fatalf("Connect error before expired: %v", configErr) } // get token before tb, err := state.retrieveTokens(serverURI, srvtypes.TypeCustom) if err != nil { t.Fatalf("No tokens found: %v", err) } // Wait for TTL so that the tokens expire time.Sleep(time.Duration(expiredInt) * time.Second) _, configErr = state.GetConfig(ck, serverURI, srvtypes.TypeCustom, false, false) if configErr != nil { t.Fatalf("Connect error after expiry: %v", configErr) } ta, err := state.retrieveTokens(serverURI, srvtypes.TypeCustom) if err != nil { t.Fatalf("No tokens found after: %v", err) } if tb.Access == ta.Access { t.Errorf("Access token is the same after refresh") } } // Test if an invalid profile will be corrected. func TestInvalidProfileCorrected(t *testing.T) { dir := t.TempDir() serverURI := getServerURI(t) ck := cookie.NewWithContext(context.Background()) defer ck.Cancel() //nolint:errcheck state, err := New( "org.letsconnect-vpn.app.linux", "0.1.0-test", dir, func(oldState FSMStateID, newState FSMStateID, data any) bool { go stateCallback(ck, oldState, newState, data) return true }, ) if err != nil { t.Fatalf("Creating client error: %v", err) } err = state.Register() if err != nil { t.Fatalf("Registering error: %v", err) } addErr := state.AddServer(ck, serverURI, srvtypes.TypeCustom, nil) if addErr != nil { t.Fatalf("Add error: %v", addErr) } _, configErr := state.GetConfig(ck, serverURI, srvtypes.TypeCustom, false, false) if configErr != nil { t.Fatalf("First connect error: %v", configErr) } s, err := state.Servers.CurrentServer() if err != nil { t.Fatalf("Got error when getting current server: %v", err) } // set invalid profile invp := "IDONOTEXIST" s.Profiles.Current = invp _, configErr = state.GetConfig(ck, serverURI, srvtypes.TypeCustom, false, false) if configErr != nil { t.Fatalf("Second connect error: %v", configErr) } if s.Profiles.Current == invp { t.Fatalf( "Profiles do no match: current %s and previous %s", s.Profiles.Current, invp, ) } } // TestConfigStartup tests if the 'startup' variable for getconfig behaves as expected func TestConfigStartup(t *testing.T) { serverURI := getServerURI(t) ck := cookie.NewWithContext(context.Background()) defer ck.Cancel() //nolint:errcheck dir := t.TempDir() state, err := New( "org.letsconnect-vpn.app.linux", "0.1.0-test", dir, func(oldState FSMStateID, newState FSMStateID, data any) bool { go stateCallback(ck, oldState, newState, data) return true }, ) if err != nil { t.Fatalf("Creating client error: %v", err) } err = state.Register() if err != nil { t.Fatalf("Failed to register with error: %v", err) } // we set true as last argument here such that no callbacks are ran var ot int64 = 5 err = state.AddServer(ck, serverURI, srvtypes.TypeCustom, &ot) if err != nil { t.Fatalf("Failed to add server for trying config startup: %v", err) } testTrue := func() { // Now get config with setting startup to true startup := true _, err := state.GetConfig(ck, serverURI, srvtypes.TypeCustom, false, startup) // this should fail as we have not authorized yet/chosen profile and startup=true does not do these callbacks if err == nil { t.Fatal("Got no error after getting config with startup true") } if !strings.HasPrefix(err.Error(), "The client tried to autoconnect to the VPN server") { t.Fatalf("GetConfig error for GetConfig with startup=true is not what we expect: %v", err) } } testFalse := func() { startup := false _, err := state.GetConfig(ck, serverURI, srvtypes.TypeCustom, false, startup) if err != nil { t.Fatalf("Got error after getting config with startup=false: %v", err) } } testTrue() testFalse() // set invalid authorization and test again // we cannot test by setting invalid profile because the server only has 1 profile err = state.tokCacher.Set(serverURI, srvtypes.TypeCustom, eduoauth.Token{}) if err != nil { t.Fatalf("Failed to set token cache: %v", err) } testTrue() testFalse() } // Test if prefer tcp is handled correctly by checking the returned config and config type. func TestPreferTCP(t *testing.T) { serverURI := getServerURI(t) ck := cookie.NewWithContext(context.Background()) defer ck.Cancel() //nolint:errcheck dir := t.TempDir() state, err := New( "org.letsconnect-vpn.app.linux", "0.1.0-test", dir, func(oldState FSMStateID, newState FSMStateID, data any) bool { go stateCallback(ck, oldState, newState, data) return true }, ) if err != nil { t.Fatalf("Creating client error: %v", err) } err = state.Register() if err != nil { t.Fatalf("Registering error: %v", err) } addErr := state.AddServer(ck, serverURI, srvtypes.TypeCustom, nil) if addErr != nil { t.Fatalf("Add error: %v", addErr) } // get a config with preferTCP set to true config, configErr := state.GetConfig(ck, serverURI, srvtypes.TypeCustom, true, false) // Test server should accept prefer TCP! if config.Protocol != protocol.OpenVPN && config.Protocol != protocol.WireGuardProxy { t.Fatalf("Invalid protocol for prefer TCP") } if configErr != nil { t.Fatalf("Config error: %v", configErr) } // We also test for script security 0 here if config.Protocol == protocol.OpenVPN && !strings.HasSuffix(config.VPNConfig, "udp\nscript-security 0") { t.Fatalf("Suffix for prefer TCP is not in the right order for config: %s", config.VPNConfig) } // get a config with preferTCP set to false config, configErr = state.GetConfig(ck, serverURI, srvtypes.TypeCustom, false, false) if configErr != nil { t.Fatalf("Config error: %v", configErr) } // We also test for script security 0 here if config.Protocol == protocol.OpenVPN && !strings.HasSuffix(config.VPNConfig, "tcp\nscript-security 0") { t.Fatalf("Suffix for disable prefer TCP is not in the right order for config: %s", config.VPNConfig) } } func TestInvalidClientID(t *testing.T) { tests := map[string]bool{ "test": false, "org.letsconnect-vpn.app.linux": true, "org.letsconnect-vpn": false, "org.letsconnect-vpn.app": false, "org.letsconnect-vpn.linuxsd": false, "org.letsconnect-vpn.app.macos": true, } dir := t.TempDir() for k, v := range tests { _, err := New( k, "0.1.0-test", dir, func(_ FSMStateID, _ FSMStateID, _ any) bool { return true }, ) if v { if err != nil { t.Fatalf("expected valid register with clientID: %v, got error: %v", k, err) } continue } if err == nil { t.Fatalf("expected invalid register with clientID: %v, but got no error", k) } if !strings.HasPrefix(err.Error(), "An internal error occurred. The cause of the error is: The client registered with an invalid client ID") { t.Fatalf("register error has invalid prefix: %v", err.Error()) } } }