├── .gitignore ├── go.mod ├── Makefile ├── go.sum ├── README.md ├── main.go └── pkg └── gotunl └── gotunl.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.txt 3 | build 4 | vendor 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cghdev/gotunl 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/olekukonko/tablewriter v0.0.5 7 | github.com/tidwall/gjson v1.14.3 8 | github.com/tidwall/sjson v1.2.5 9 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 10 | ) 11 | 12 | require ( 13 | github.com/mattn/go-runewidth v0.0.13 // indirect 14 | github.com/rivo/uniseg v0.3.4 // indirect 15 | github.com/tidwall/match v1.1.1 // indirect 16 | github.com/tidwall/pretty v1.2.0 // indirect 17 | golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET=./build 2 | ARCHS=amd64 arm64 3 | LDFLAGS="-s -w" 4 | 5 | current: 6 | @go build -ldflags=${LDFLAGS} -o ./gotunl; \ 7 | echo "Done." 8 | 9 | windows: 10 | @for GOARCH in ${ARCHS}; do \ 11 | echo "Building for windows $${GOARCH} ..." ; \ 12 | mkdir -p ${TARGET}/gotunl-windows-$${GOARCH} ; \ 13 | GOOS=windows GOARCH=$${GOARCH} go build -trimpath -ldflags=${LDFLAGS} -o ${TARGET}/gotunl-windows-$${GOARCH}/gotunl.exe ; \ 14 | done; \ 15 | echo "Done." 16 | 17 | linux: 18 | @for GOARCH in ${ARCHS}; do \ 19 | echo "Building for linux $${GOARCH} ..." ; \ 20 | mkdir -p ${TARGET}/gotunl-linux-$${GOARCH} ; \ 21 | GOOS=linux GOARCH=$${GOARCH} go build -trimpath -ldflags=${LDFLAGS} -o ${TARGET}/gotunl-linux-$${GOARCH}/gotunl ; \ 22 | done; \ 23 | echo "Done." 24 | 25 | darwin: 26 | @for GOARCH in ${ARCHS}; do \ 27 | echo "Building for darwin $${GOARCH} ..." ; \ 28 | mkdir -p ${TARGET}/gotunl-darwin-$${GOARCH} ; \ 29 | GOOS=darwin GOARCH=$${GOARCH} go build -trimpath -ldflags=${LDFLAGS} -o ${TARGET}/gotunl-darwin-$${GOARCH}/gotunl ; \ 30 | done; \ 31 | mkdir ${TARGET}/gotunl-darwin-universal &>/dev/null; \ 32 | lipo -create -output ${TARGET}/gotunl-darwin-universal/gotunl ${TARGET}/gotunl-darwin-amd64/gotunl ${TARGET}/gotunl-darwin-arm64/gotunl ; \ 33 | echo "Done." 34 | 35 | all: darwin linux windows 36 | 37 | test: 38 | @go test -v -race ./... ; \ 39 | echo "Done." 40 | 41 | clean: 42 | @rm -rf ${TARGET}/* ; \ 43 | echo "Done." 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 2 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 3 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 4 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 5 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 6 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 7 | github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= 8 | github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 9 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 10 | github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= 11 | github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 12 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 13 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 14 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 15 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 16 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 17 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 18 | golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 h1:qSa+Hg9oBe6UJXrznE+yYvW51V9UbyIj/nj/KpDigo8= 19 | golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= 21 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gotunl 2 | 3 | gotunl is a command line client for Pritunl written in Go. 4 | 5 | **Note:** gotunl package has been moved to this project under pkg/ directory. 6 | 7 | ## Installation: 8 | 9 | Using go mod, requires go>=1.13: 10 | 11 | ``` 12 | git clone https://github.com/cghdev/gotunl.git 13 | cd gotunl 14 | go install 15 | ``` 16 | 17 | Alternatively, you can download a pre-compiled version from https://github.com/cghdev/gotunl/releases/latest 18 | 19 | 20 | ## Usage: 21 | 22 | ```bash 23 | Pritunl command line client 24 | 25 | Usage: 26 | -c Connect to profile ID or Name 27 | -d Disconnect profile or "all" 28 | -l List connections 29 | -o Output format table|tsv (default is table) 30 | -v Show version 31 | ``` 32 | 33 | 34 | ## Examples: 35 | 36 | ```bash 37 | $ ./gotunl -l 38 | +----+------------------------+--------------+ 39 | | ID | Name | Status | 40 | |----+------------------------+--------------| 41 | | 1 | US VPN | Disconnected | 42 | | 2 | UK VPN | Disconnected | 43 | | 3 | Netherlands VPN | Disconnected | 44 | | 4 | Hong Kong VPN | Disconnected | 45 | | 5 | Test VPN | Disconnected | 46 | +----+------------------------+--------------+ 47 | $ ./gotunl -c 3 48 | $ ./gotunl -c "Test VPN" 49 | Enter the username: user1 50 | Enter the password: ************* 51 | $ ./gotunl -l 52 | +----+------------------------+--------------+---------------+---------------+---------------+ 53 | | ID | Name | Status | Connected for | Client IP | Server IP | 54 | +----+------------------------+--------------+---------------+---------------+---------------+ 55 | | 1 | US VPN | Disconnected | | | | 56 | | 2 | UK VPN | Disconnected | | | | 57 | | 3 | Netherlands VPN | Connected | 16 secs | 10.10.1.5 | 172.16.25.1 | 58 | | 4 | Hong Kong VPN | Disconnected | | | | 59 | | 5 | Test VPN | Connected | 8 secs | 192.168.65.3 | 172.16.32.1 | 60 | +----+------------------------+--------------+---------------+---------------+---------------+ 61 | $ ./gotunl -d all 62 | $ ./gotunl -l -o tsv 63 | ID Name Status Connected for Client IP Server IP 64 | 1 US VPN Disconnected 65 | 2 UK VPN Disconnected 66 | 3 Netherlands VPN Connected 16 secs 10.10.1.5 172.16.25.1 67 | 4 Hong Kong VPN Disconnected 68 | 5 Test VPN Connected 8 secs 192.168.65.3 172.16.32.1 69 | ``` 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/cghdev/gotunl/pkg/gotunl" 14 | "github.com/olekukonko/tablewriter" 15 | "github.com/tidwall/gjson" 16 | ) 17 | 18 | var version = "1.2.4" 19 | var color = map[string]string{ 20 | "red": "\x1b[31;1m", 21 | "green": "\x1b[32;1m", 22 | "reset": "\x1b[0m"} 23 | 24 | type connections struct { 25 | id int 26 | name string 27 | status string 28 | timestamp int64 29 | clientAddr string 30 | serverAddr string 31 | } 32 | 33 | func listConnections(gt *gotunl.Gotunl, output string) { // add output format as json? 34 | if len(gt.Profiles) == 0 { 35 | fmt.Println("No profiles found in Pritunl") 36 | os.Exit(1) 37 | } 38 | cons := gt.GetConnections() 39 | c := []connections{} 40 | stdis := "" 41 | stcon := "" 42 | anycon := false 43 | for pid, p := range gt.Profiles { 44 | ptmp := connections{} 45 | if runtime.GOOS != "windows" { 46 | stdis = color["red"] + "Disconnected" + color["reset"] 47 | stcon = color["green"] + "Connected" + color["reset"] 48 | } else { 49 | stdis = "Disconnected" 50 | stcon = "Connected" 51 | } 52 | ptmp.status = stdis 53 | ptmp.name = gjson.Get(p.Conf, "name").String() 54 | ptmp.id = p.ID 55 | if strings.Contains(cons, pid) { 56 | ptmp.status = strings.Title(gjson.Get(cons, pid+".status").String()) 57 | ptmp.serverAddr = gjson.Get(cons, pid+".server_addr").String() 58 | ptmp.clientAddr = gjson.Get(cons, pid+".client_addr").String() 59 | ptmp.timestamp = gjson.Get(cons, pid+".timestamp").Int() 60 | if ptmp.status == "Connected" { 61 | ptmp.status = stcon 62 | anycon = true 63 | } 64 | } 65 | c = append(c, ptmp) 66 | } 67 | sort.SliceStable(c, func(i, j int) bool { 68 | return c[i].id < c[j].id 69 | }) 70 | table := tablewriter.NewWriter(os.Stdout) 71 | if anycon { 72 | table.SetHeader([]string{"ID", "Name", "Status", "Connected for", "Client IP", "Server IP"}) 73 | } else { 74 | table.SetHeader([]string{"ID", "Name", "Status"}) 75 | } 76 | setOutputFormat(table, output) 77 | for _, p := range c { 78 | since := "" 79 | if p.timestamp > 0 { 80 | ts := time.Unix(p.timestamp, 0) 81 | since = formatSince(ts) 82 | } 83 | pid := strconv.Itoa(p.id) 84 | if anycon { 85 | table.Append([]string{pid, p.name, p.status, since, p.clientAddr, p.serverAddr}) 86 | } else { 87 | table.Append([]string{pid, p.name, p.status}) 88 | } 89 | } 90 | table.Render() 91 | } 92 | 93 | func setOutputFormat(tbl *tablewriter.Table, output string) *tablewriter.Table { 94 | switch output { 95 | case "tsv": 96 | tbl.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 97 | tbl.SetAlignment(tablewriter.ALIGN_LEFT) 98 | tbl.SetCenterSeparator("") 99 | tbl.SetColumnSeparator("\t") 100 | tbl.SetRowSeparator("") 101 | tbl.SetHeaderLine(false) 102 | tbl.SetBorder(false) 103 | tbl.SetTablePadding("\t") 104 | tbl.SetAutoFormatHeaders(true) 105 | tbl.SetAutoWrapText(false) 106 | tbl.SetAutoFormatHeaders(false) 107 | case "table": 108 | tbl.SetAutoFormatHeaders(false) 109 | default: 110 | fmt.Println("Define output format") 111 | os.Exit(1) 112 | } 113 | return tbl 114 | } 115 | 116 | func disconnect(gt *gotunl.Gotunl, id string) { 117 | if id == "all" { 118 | gt.StopConnections() 119 | } else { 120 | for pid, p := range gt.Profiles { 121 | if id == gjson.Get(p.Conf, "name").String() || id == strconv.Itoa(p.ID) { 122 | gt.DisconnectProfile(pid) 123 | } 124 | } 125 | } 126 | 127 | } 128 | 129 | func connect(gt *gotunl.Gotunl, id string) { 130 | for pid, p := range gt.Profiles { 131 | if id == gjson.Get(p.Conf, "name").String() || id == strconv.Itoa(p.ID) { 132 | gt.ConnectProfile(pid, "", "") 133 | } 134 | } 135 | } 136 | 137 | func formatSince(t time.Time) string { 138 | Day := 24 * time.Hour 139 | ts := time.Since(t) 140 | sign := time.Duration(1) 141 | var days, hours, minutes, seconds string 142 | if ts < 0 { 143 | sign = -1 144 | ts = -ts 145 | } 146 | d := sign * (ts / Day) 147 | ts = ts % Day 148 | h := ts / time.Hour 149 | ts = ts % time.Hour 150 | m := ts / time.Minute 151 | ts = ts % time.Minute 152 | s := ts / time.Second 153 | if d > 0 { 154 | days = fmt.Sprintf("%d days ", d) 155 | } 156 | if h > 0 { 157 | hours = fmt.Sprintf("%d hrs ", h) 158 | } 159 | if m > 0 { 160 | minutes = fmt.Sprintf("%d mins ", m) 161 | } 162 | seconds = fmt.Sprintf("%d secs", s) 163 | return fmt.Sprintf("%v%v%v%v", days, hours, minutes, seconds) 164 | } 165 | 166 | func usage(a *flag.Flag) { 167 | if a.Name == "l" || a.Name == "v" { 168 | fmt.Printf(" -%v \t\t%v\n", a.Name, a.Usage) 169 | } else if a.Name == "o" { 170 | fmt.Printf(" -%v \t%v\n", a.Name, a.Usage) 171 | } else { 172 | fmt.Printf(" -%v \t%v\n", a.Name, a.Usage) 173 | } 174 | } 175 | 176 | func main() { 177 | gt := *gotunl.New() 178 | flag.Usage = func() { 179 | fmt.Print("Pritunl command line client\n\n") 180 | fmt.Println("Usage:") 181 | flag.VisitAll(usage) 182 | } 183 | l := flag.Bool("l", false, "List connections") 184 | c := flag.String("c", "", "Connect to profile ID or Name") 185 | d := flag.String("d", "", "Disconnect profile or \"all\"") 186 | o := flag.String("o", "table", "Output format table|tsv (default is table)") 187 | v := flag.Bool("v", false, "Show version") 188 | 189 | flag.Parse() 190 | if len(os.Args) < 2 { 191 | flag.Usage() 192 | os.Exit(1) 193 | } 194 | if *l { 195 | listConnections(>, *o) 196 | os.Exit(0) 197 | } 198 | if *c != "" { 199 | connect(>, *c) 200 | os.Exit(0) 201 | } 202 | if *d != "" { 203 | disconnect(>, *d) 204 | os.Exit(0) 205 | } 206 | if *v { 207 | fmt.Println(version) 208 | } 209 | os.Exit(1) 210 | } 211 | -------------------------------------------------------------------------------- /pkg/gotunl/gotunl.go: -------------------------------------------------------------------------------- 1 | package gotunl 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | b64 "encoding/base64" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "runtime" 16 | "strings" 17 | 18 | "github.com/tidwall/gjson" 19 | "github.com/tidwall/sjson" 20 | "golang.org/x/term" 21 | ) 22 | 23 | type profile struct { 24 | Path string 25 | ID int 26 | Conf string 27 | } 28 | type Gotunl struct { 29 | authKey string 30 | profPath string 31 | service string 32 | unixSocket string 33 | Profiles map[string]profile 34 | } 35 | 36 | func _getKey() string { 37 | keyPath := "" 38 | if runtime.GOOS == "windows" { 39 | keyPath = "c:\\ProgramData\\Pritunl\\auth" 40 | } else { 41 | keyPath = "/var/run/pritunl.auth" 42 | } 43 | if _, err := os.Stat(keyPath); !os.IsNotExist(err) { 44 | key, err := ioutil.ReadFile(keyPath) 45 | if err != nil { 46 | log.Fatalf("Error getting key: %s\n", err) 47 | } 48 | return string(key) 49 | } 50 | return "" 51 | } 52 | 53 | func _getProfilePath() string { 54 | home := "" 55 | profPath := "" 56 | switch oS := runtime.GOOS; oS { 57 | case "darwin": 58 | home = os.Getenv("HOME") 59 | profPath = home + "/Library/Application Support/pritunl/profiles" 60 | case "windows": 61 | home = os.Getenv("APPDATA") 62 | profPath = home + "\\pritunl\\profiles" 63 | case "linux": 64 | home = os.Getenv("HOME") 65 | profPath = home + "/.config/pritunl/profiles" 66 | } 67 | if _, err := os.Stat(profPath); !os.IsNotExist(err) { 68 | return profPath 69 | } 70 | return "" 71 | } 72 | 73 | func New() *Gotunl { 74 | service := "" 75 | if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { 76 | service = "http://unix/" 77 | } else { 78 | service = "http://localhost:9770/" 79 | } 80 | g := Gotunl{_getKey(), _getProfilePath(), service, "/var/run/pritunl.sock", map[string]profile{}} 81 | g.loadProfiles() 82 | return &g 83 | } 84 | 85 | func (g Gotunl) makeReq(verb string, endpoint string, data string) string { 86 | url := g.service + endpoint 87 | req, err := http.NewRequest(verb, url, bytes.NewBuffer([]byte(data))) 88 | if err != nil { 89 | log.Fatalf("Error making request: %s\n", err) 90 | } 91 | req.Header.Set("User-Agent", "pritunl") 92 | req.Header.Set("Content-Type", "application/json") 93 | req.Header.Set("Auth-Key", g.authKey) 94 | client := http.Client{} 95 | // pritunl now uses unix sockets for linux and MacOS. 96 | // ref: https://gist.github.com/teknoraver/5ffacb8757330715bcbcc90e6d46ac74 97 | if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { 98 | client = http.Client{ 99 | Transport: &http.Transport{ 100 | DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { 101 | return net.Dial("unix", g.unixSocket) 102 | }, 103 | }, 104 | } 105 | } 106 | res, err := client.Do(req) 107 | if err != nil { 108 | log.Fatalf("Error making request (do): %s\n", err) 109 | } 110 | if res.StatusCode == 200 { 111 | body, _ := ioutil.ReadAll(res.Body) 112 | res.Body.Close() 113 | return string(body) 114 | } 115 | return string(res.StatusCode) 116 | 117 | } 118 | 119 | func (g Gotunl) CheckStatus() string { 120 | s := g.makeReq("GET", "status", "") 121 | return gjson.Get(s, "status").String() 122 | } 123 | 124 | func (g Gotunl) Ping() bool { 125 | p := g.makeReq("GET", "ping", "") 126 | return p == "" 127 | } 128 | 129 | func (g Gotunl) GetConnections() string { 130 | cons := g.makeReq("GET", "profile", "") 131 | return cons 132 | } 133 | 134 | func (g Gotunl) StopConnections() { 135 | g.makeReq("POST", "stop", "") 136 | } 137 | 138 | func (g Gotunl) loadProfiles() { 139 | res, _ := filepath.Glob(g.profPath + "/*.conf") 140 | for i, f := range res { 141 | c := i + 1 142 | prof := strings.Split(filepath.Base(f), ".")[0] 143 | conf, err := ioutil.ReadFile(f) 144 | if err != nil { 145 | log.Fatalf("Error loading profiles: %s\n", err) 146 | } 147 | config := string(conf) // keep the whole config file to use later, instead of reading the file again. 148 | if gjson.Get(config, "name").String() == "" { // If "name": null it will set the name automatically. 149 | user := gjson.Get(config, "user").String() 150 | server := gjson.Get(config, "server").String() 151 | config, _ = sjson.Set(config, "name", fmt.Sprintf("%v (%v)", user, server)) 152 | } 153 | g.Profiles[prof] = profile{f, c, config} 154 | } 155 | } 156 | 157 | func (g Gotunl) GetProfile(id string) (string, string) { 158 | auth := "" 159 | key := "" 160 | g.loadProfiles() 161 | prof := g.Profiles[id] 162 | ovpnFile := strings.Replace(prof.Path, id+".conf", id+".ovpn", 1) 163 | ovpn, err := ioutil.ReadFile(ovpnFile) 164 | if err != nil { 165 | log.Fatalf("Error getting profile: %s\n", err) 166 | } 167 | for _, l := range strings.Split(string(ovpn), "\n") { 168 | if strings.Contains(l, "auth-user-pass") && len(l) <= 17 { //check if it needs credentials and they are not provided as parameter 169 | auth = "creds" 170 | } 171 | } 172 | mode := gjson.Get(prof.Conf, "password_mode").String() 173 | if auth != "" && strings.Contains(prof.Conf, "password_mode") && mode != "" { 174 | auth = mode 175 | } 176 | if runtime.GOOS == "darwin" { 177 | command := "security find-generic-password -w -s pritunl -a " + id 178 | out, err := exec.Command("bash", "-c", command).Output() 179 | if err != nil { 180 | if strings.Contains("exit status 36", err.Error()) { 181 | log.Println("There was an error accessing the Keychain (probably connected through SSH)") 182 | log.Fatal("Run '/usr/bin/security unlock-keychain' to unlock the Keychain and try again") 183 | } 184 | log.Fatalf("Error getting profiles (find-generic-password): %s\n", err) 185 | } 186 | res, err := b64.StdEncoding.DecodeString(string(out)) 187 | if err != nil { 188 | log.Fatalf("Error decoding base64: %s\n", err) 189 | } 190 | key = string(res) 191 | } 192 | vpn := string(ovpn) + "\n" + key 193 | return vpn, auth 194 | 195 | } 196 | 197 | func (g Gotunl) ConnectProfile(id string, user string, password string) { 198 | data := fmt.Sprintf(`{"id": "%v", "reconnect": true, "timeout": true}`, id) 199 | ovpn, auth := g.GetProfile(id) 200 | if (auth != "") && (user == "" || password == "") { 201 | auth_method := auth[len(auth)-3:] 202 | if auth_method == "otp" || auth_method == "pin" { 203 | var otp string 204 | user = "pritunl" 205 | if password == "" { 206 | fmt.Printf("Enter the PIN: ") 207 | pass, err := term.ReadPassword(int(os.Stdin.Fd())) 208 | if err != nil { 209 | log.Fatalf("\nError connecting to profile (ReadPassword): %s\n", err) 210 | } 211 | if strings.Contains(auth, "otp_pin") { 212 | fmt.Printf("\nEnter the OTP code: ") 213 | fmt.Scanln(&otp) 214 | } 215 | password = string(pass) + otp 216 | } 217 | } 218 | if user == "" { 219 | fmt.Printf("Enter the username: ") 220 | fmt.Scanln(&user) 221 | 222 | } 223 | if password == "" { 224 | fmt.Printf("Enter the password: ") 225 | pass, _ := term.ReadPassword(int(os.Stdin.Fd())) 226 | password = string(pass) 227 | } 228 | } 229 | data, _ = sjson.Set(data, "username", user) 230 | data, _ = sjson.Set(data, "password", password) 231 | data, _ = sjson.Set(data, "data", ovpn) 232 | g.makeReq("POST", "profile", data) 233 | } 234 | 235 | func (g Gotunl) DisconnectProfile(id string) { 236 | g.makeReq("DELETE", "profile", `{"id": "`+id+`"}`) 237 | } 238 | --------------------------------------------------------------------------------