summaryrefslogtreecommitdiff
path: root/cmd/eduvpn-cli
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/eduvpn-cli')
-rw-r--r--cmd/eduvpn-cli/main.go182
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()
+ }
+}