diff options
Diffstat (limited to 'cmd/eduvpn-cli')
| -rw-r--r-- | cmd/eduvpn-cli/main.go | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/cmd/eduvpn-cli/main.go b/cmd/eduvpn-cli/main.go new file mode 100644 index 0000000..263f55c --- /dev/null +++ b/cmd/eduvpn-cli/main.go @@ -0,0 +1,182 @@ +// Package main implements an example CLI client +package main + +import ( + "context" + "flag" + "fmt" + "os" + "reflect" + "strings" + + "codeberg.org/eduVPN/eduvpn-common/client" + "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" +) + +// Open a browser with xdg-open. +func openBrowser(data interface{}) { + str, ok := data.(string) + if !ok { + return + } + fmt.Printf("OAuth: Authorization URL: %s\n", str) + fmt.Println("Opening browser...") + go func() { + err := browser.OpenURL(str) + if err != nil { + fmt.Fprintln(os.Stderr, "failed to open browser with error:", err) + fmt.Println("Please open your browser manually") + } + }() +} + +// Ask for a profile in the command line. +func sendProfile(state *client.Client, data interface{}) { + fmt.Printf("Multiple VPN profiles found. Please select a profile by entering e.g. 1") + d, ok := data.(*srvtypes.RequiredAskTransition) + if !ok { + fmt.Fprintf(os.Stderr, "\ninvalid data type: %v\n", reflect.TypeOf(data)) + return + } + sps, ok := d.Data.(*srvtypes.Profiles) + if !ok { + fmt.Fprintf(os.Stderr, "\ninvalid data type for profiles: %v\n", reflect.TypeOf(d.Data)) + return + } + + ps := "" + var options []string + i := 0 + for k, v := range sps.Map { + ps += fmt.Sprintf("\n%d - %s", i+1, util.GetLanguageMatched(v.DisplayName, "en")) + options = append(options, k) + i++ + } + + // Show the profiles + fmt.Println(ps) + + var idx int + if _, err := fmt.Scanf("%d", &idx); err != nil || idx <= 0 || + idx > len(sps.Map) { + fmt.Fprintln(os.Stderr, "invalid profile chosen, please retry") + sendProfile(state, data) + return + } + + p := options[idx-1] + fmt.Println("Sending profile ID", p) + if err := d.C.Send(p); err != nil { + fmt.Fprintln(os.Stderr, "failed setting profile with error", err) + } +} + +// The callback function +// If OAuth is started we open the browser with the Auth URL +// If we ask for a profile, we send the profile using command line input +// Note that this has an additional argument, the vpn state which was wrapped into this callback function below. +func stateCallback(state *client.Client, _ client.FSMStateID, newState client.FSMStateID, data interface{}) { + if newState == client.StateOAuthStarted { + openBrowser(data) + } + + if newState == client.StateAskProfile { + sendProfile(state, data) + } + + if newState == client.StateAskLocation { + fmt.Fprint(os.Stderr, "An invalid secure location is stored. This CLI doesn't support interactively choosing a location yet. Give a correct location with the -country-code flag") + os.Exit(1) + } +} + +// Get a config for Institute Access or Secure Internet Server. +func getConfig(state *client.Client, url string, srvType srvtypes.Type, cc string) (*srvtypes.Configuration, error) { + if !strings.HasPrefix(url, "http") { + url = "https://" + url + } + ck := cookie.NewWithContext(context.Background()) + defer ck.Cancel() //nolint:errcheck + err := state.AddServer(ck, url, srvType, nil) + if err != nil { + // TODO: This is quite hacky :^) + if !strings.Contains(err.Error(), "a secure internet server already exists.") { + return nil, err + } + } + if cc != "" { + err = state.SetSecureLocation(url, cc) + if err != nil { + return nil, err + } + } + + return state.GetConfig(ck, url, srvType, false, false) +} + +// Get a config for a single server, Institute Access or Secure Internet. +func printConfig(url string, cc string, srvType srvtypes.Type, debug bool) { + var c *client.Client + c, err := client.New( + "org.eduvpn.app.linux", + fmt.Sprintf("%s-cli", version.Version), + "configs", + func(oldState client.FSMStateID, newState client.FSMStateID, data interface{}) bool { + stateCallback(c, oldState, newState, data) + return true + }, + debug, + ) + if err != nil { + fmt.Printf("Register error: %v", err) + return + } + _ = c.Register() + + ck := cookie.NewWithContext(context.Background()) + _, err = c.DiscoOrganizations(ck, "") + if err != nil { + panic(err) + } + _, err = c.DiscoServers(ck, "") + if err != nil { + panic(err) + } + + defer c.Deregister() + + cfg, err := getConfig(c, url, srvType, cc) + if err != nil { + fmt.Fprintf(os.Stderr, "failed getting a config: %v\n", err) + return + } + fmt.Printf("Obtained config:\n%s\n", cfg.VPNConfig) +} + +// The main function +// It parses the arguments and executes the correct functions. +func main() { + cu := flag.String("get-custom", "", "The url of a custom server to connect to") + u := flag.String("get-institute", "", "The url of an institute to connect to") + sec := flag.String("get-secure", "", "Gets secure internet servers") + cc := flag.String("country-code", "", "The country code to use in case of a secure internet server") + debug := flag.Bool("debug", false, "Whether or not to enable debugging") + flag.Parse() + + // Connect to a VPN by getting an Institute Access config + switch { + case *cu != "": + printConfig(*cu, "", srvtypes.TypeCustom, *debug) + case *u != "": + printConfig(*u, "", srvtypes.TypeInstituteAccess, *debug) + case *sec != "": + printConfig(*sec, *cc, srvtypes.TypeSecureInternet, *debug) + default: + flag.PrintDefaults() + } +} |
