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
|
package main
import (
"context"
"flag"
"fmt"
"os"
"reflect"
"strings"
"github.com/eduvpn/eduvpn-common/client"
"github.com/eduvpn/eduvpn-common/types/cookie"
srvtypes "github.com/eduvpn/eduvpn-common/types/server"
"github.com/go-errors/errors"
"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...")
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")
}
}
// 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 ""
}
// 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, 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)
}
}
// Get a config for Institute Access or Secure Internet Server.
func getConfig(state *client.Client, url string, srvType srvtypes.Type) (*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, false)
if err != nil {
return nil, err
}
return state.GetConfig(&ck, url, srvType, false)
}
// Get a config for a single server, Institute Access or Secure Internet.
func printConfig(url string, srvType srvtypes.Type) {
var c *client.Client
c, err := client.New(
"org.eduvpn.app.linux",
"2.0.0-cli",
"configs",
func(old client.FSMStateID, new client.FSMStateID, data interface{}) bool {
stateCallback(c, old, new, data)
return true
},
true,
)
if err != nil {
fmt.Printf("Register error: %v", err)
return
}
_ = c.Register()
defer c.Deregister()
cfg, err := getConfig(c, url, srvType)
if err != nil {
err1 := err.(*errors.Error)
// Show the usage of tracebacks and causes
fmt.Fprintf(os.Stderr, "Error getting config: %s\nCause:\n%s\nStack trace:\n%s\n\n'",
err1.Error(), err1.Err, err1.ErrorStack())
return
}
fmt.Println("Obtained config:", 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")
flag.Parse()
// Connect to a VPN by getting an Institute Access config
switch {
case *cu != "":
printConfig(*cu, srvtypes.TypeCustom)
case *u != "":
printConfig(*u, srvtypes.TypeInstituteAccess)
case *sec != "":
printConfig(*sec, srvtypes.TypeSecureInternet)
default:
flag.PrintDefaults()
}
}
|