1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
|
// 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 any) {
str, ok := data.(string)
if !ok {
return
}
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")
}
}()
}
func getProfileInteractive(profiles *srvtypes.Profiles, data any) (string, error) {
fmt.Printf("Multiple VPN profiles found. Please select a profile by entering e.g. 1")
ps := ""
var options []string
i := 0
for k, v := range profiles.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(profiles.Map) {
fmt.Fprintln(os.Stderr, "invalid profile chosen, please retry")
return getProfileInteractive(profiles, data)
}
p := options[idx-1]
fmt.Println("Sending profile ID", p)
return p, nil
}
func sendProfile(profile string, data any) {
d, ok := data.(*srvtypes.RequiredAskTransition)
if !ok {
fmt.Fprintf(os.Stderr, "\ninvalid data type: %v\n", reflect.TypeOf(data))
os.Exit(1)
}
sps, ok := d.Data.(*srvtypes.Profiles)
if !ok {
fmt.Fprintf(os.Stderr, "\ninvalid data type for profiles: %v\n", reflect.TypeOf(d.Data))
os.Exit(1)
}
if profile == "" {
gprof, err := getProfileInteractive(sps, data)
if err != nil {
fmt.Fprintf(os.Stderr, "failed getting profile interactively: %v\n", err)
os.Exit(1)
}
profile = gprof
}
if err := d.C.Send(profile); err != nil {
fmt.Fprintf(os.Stderr, "failed setting profile with error: %v\n", err)
os.Exit(1)
}
}
// 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(_ client.FSMStateID, newState client.FSMStateID, data any, prof string, dir string) {
if newState == client.StateOAuthStarted {
openBrowser(data)
}
if newState == client.StateAskProfile {
sendProfile(prof, data)
}
if newState == client.StateAskLocation {
// removing is best effort
_ = os.RemoveAll(dir)
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, prof 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
}
}
if prof != "" {
// this is best effort, e.g. if no server was chosen before this fails
_ = state.SetProfileID(prof) //nolint:errcheck
}
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, prof string, debug bool) error {
var c *client.Client
var err error
var dir string
dir, err = os.MkdirTemp("", "eduvpn-common")
if err != nil {
return err
}
// removing is best effort
defer os.RemoveAll(dir) //nolint:errcheck
c, err = client.New(
"org.eduvpn.app.linux",
fmt.Sprintf("%s-cli", version.Version),
dir,
func(oldState client.FSMStateID, newState client.FSMStateID, data any) bool {
stateCallback(oldState, newState, data, prof, dir)
return true
},
debug,
)
if err != nil {
return err
}
_ = c.Register()
ck := cookie.NewWithContext(context.Background())
_, err = c.DiscoOrganizations(ck, "")
if err != nil {
return err
}
_, err = c.DiscoServers(ck, "")
if err != nil {
return err
}
defer c.Deregister()
cfg, err := getConfig(c, url, srvType, cc, prof)
if err != nil {
return err
}
fmt.Println(cfg.VPNConfig)
return nil
}
// 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")
prof := flag.String("profile", "", "The profile ID to choose")
debug := flag.Bool("debug", false, "Whether or not to enable debugging")
flag.Parse()
// Connect to a VPN by getting an Institute Access config
var err error
switch {
case *cu != "":
err = printConfig(*cu, "", srvtypes.TypeCustom, *prof, *debug)
case *u != "":
err = printConfig(*u, "", srvtypes.TypeInstituteAccess, *prof, *debug)
case *sec != "":
err = printConfig(*sec, *cc, srvtypes.TypeSecureInternet, *prof, *debug)
default:
flag.PrintDefaults()
}
if err != nil {
fmt.Fprintf(os.Stderr, "failed to get a VPN config: %v\n", err)
}
}
|