openvpn-mgt/openvpn.go

307 lines
6.5 KiB
Go

package main
import (
"bufio"
"encoding/base64"
"errors"
"fmt"
"net"
"strconv"
"sync"
"github.com/pyke369/golang-support/rcache"
)
type OpenVpnPassword struct {
User string
Pass string
OTP string
}
type OpenVpnSrv struct {
Remote string `json:"active-vpn"`
Status string `json:"status"`
Provider string `json:"provider"`
Identifier string `json:"identifier"`
Message string `json:"message"`
chanHold chan bool
chanPass chan OpenVpnPassword
m sync.RWMutex
ret chan []string
buf *bufio.ReadWriter
mgt *OpenVpnMgt
hold bool
authWait bool
authCache *OpenVpnPassword
}
func (v *OpenVpnSrv) Lock() {
v.m.Lock()
}
func (v *OpenVpnSrv) Unlock() {
v.m.Unlock()
}
func NewOpenVpnSrv(conn net.Conn, mgt *OpenVpnMgt) *OpenVpnSrv {
return &OpenVpnSrv{
buf: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)),
chanHold: make(chan bool),
chanPass: make(chan OpenVpnPassword),
ret: make(chan []string),
mgt: mgt,
hold: false,
authWait: false,
Status: "Starting",
Identifier: "Unknown",
Provider: "Unknown",
}
}
func (v *OpenVpnSrv) B64OTP() string {
return base64.StdEncoding.EncodeToString([]byte(v.authCache.OTP))
}
func (v *OpenVpnSrv) B64Pass() string {
return base64.StdEncoding.EncodeToString([]byte(v.authCache.Pass))
}
// send a command to the server. Set the channel to receive the response
func (v *OpenVpnSrv) sendCommand(msg []string) (error, []string) {
v.Lock()
for _, line := range msg {
if _, err := v.buf.WriteString(line + "\r\n"); err != nil {
v.Unlock()
return err, nil
}
}
if err := v.buf.Flush(); err != nil {
return err, nil
}
v.Unlock()
// wait for the response
ret := <-v.ret
return nil, ret
}
func (v *OpenVpnSrv) Signal(signal string) error {
for _, valid := range []string{"SIGHUP", "SIGTERM"} {
if signal == valid {
err, _ := v.sendCommand([]string{fmt.Sprintf("signal %s", signal)})
return err
}
}
return errors.New("unknown signal")
}
func (v *OpenVpnSrv) GetPid() error {
err, infos := v.sendCommand([]string{"pid"})
if err != nil {
v.mgt.Debug(err)
return err
}
pidRegexp := rcache.Get("^SUCCESS: pid=([0-9]+)$")
for _, line := range infos {
match := pidRegexp.FindStringSubmatch(line)
if len(match) == 0 {
continue
}
if len(match) == 2 {
pid, err := strconv.Atoi(match[1])
if err == nil {
v.mgt.SetPid(v, pid)
v.mgt.Debug("Found PID", pid)
v.Lock()
if v.Identifier == "Unknown" {
v.Identifier = fmt.Sprintf("Unknown-%d", pid)
}
v.Unlock()
}
return err
}
}
v.mgt.Debug("Can't find PID")
return errors.New("Can't find PID")
}
func (v *OpenVpnSrv) GetEcho() {
err, infos := v.sendCommand([]string{"echo all"})
if err != nil {
return
}
echoRegexp := rcache.Get("^[0-9]+,([A-Za-z0-9-]*):([A-Za-z0-9-]*)$")
for _, line := range infos {
match := echoRegexp.FindStringSubmatch(line)
if len(match) == 0 {
continue
}
switch match[1] {
case "vpnidentifier":
v.Lock()
v.Identifier = match[2]
v.Unlock()
case "vpnprovider":
if err := v.mgt.getServerList(match[2]); err != nil {
v.mgt.Debug(err)
continue
}
v.Provider = match[2]
}
}
}
func (v *OpenVpnSrv) Response(response []string) {
v.Lock()
v.ret <- response
v.Unlock()
}
func (v *OpenVpnSrv) GetLine() (string, error) {
return v.buf.ReadString('\n')
}
func (v *OpenVpnSrv) ValidRemote(server, port, proto string) {
if !v.authWait {
v.Status = "Connected"
}
if v.Remote != "" {
v.sendCommand([]string{fmt.Sprintf("remote MOD %s %s %s", v.Remote, port, proto)})
return
}
v.Remote = server
v.sendCommand([]string{"remote ACCEPT"})
}
func (v *OpenVpnSrv) Version() (error, []string) {
return v.sendCommand([]string{"version"})
}
func (v *OpenVpnSrv) SetRemote(server string) error {
// already the active server, do nothing
if v.Remote == server {
return nil
}
if v.Remote != "" {
v.Remote = server
v.Signal("SIGHUP")
v.Status = "Reloaded"
}
v.Remote = server
// release Hold if necessary
v.ReleaseHold()
return nil
}
func (v *OpenVpnSrv) NeedPassword(line string) {
if v.authWait {
return
}
v.authWait = true
v.mgt.Debug(line)
v.Status = "Need Password"
v.Message = ""
backup := v.authCache
OtpPassRegexp := rcache.Get("^>PASSWORD:Need 'Auth' username/password SC:([01]),(.*)$")
OtpPassMatch := OtpPassRegexp.FindStringSubmatch(line)
CRV1Regexep := rcache.Get(">PASSWORD:Verification Failed: 'Auth' \\['CRV1:([R,E]*):(.*):(.*):(.*)'\\]")
CRV1Match := CRV1Regexep.FindStringSubmatch(line)
switch {
case len(CRV1Match) > 1:
v.Status = "Need OTP"
v.Message = CRV1Match[4]
v.authCache = nil
case len(OtpPassMatch) > 1:
v.Status = "Need Password and OTP"
v.Message = OtpPassMatch[2]
case line == ">PASSWORD:Need 'Auth' username/password":
v.Status = "Need Password"
case line == ">PASSWORD:Verification Failed: 'Auth'":
v.authCache = nil
v.Status = "Auth Failed"
return
}
if v.authCache == nil {
ident := <-v.chanPass
v.authCache = &ident
}
var cmd []string
switch {
case len(CRV1Match) > 1:
cmd = []string{
fmt.Sprintf("username \"Auth\" %s", B64decode(CRV1Match[3])),
fmt.Sprintf("password \"Auth\" CRV1::%s::%s", CRV1Match[2], v.authCache.OTP),
}
// restore auth backup
v.authCache = backup
case len(OtpPassMatch) > 1:
cmd = []string{
fmt.Sprintf("username \"Auth\" %s", v.authCache.User),
fmt.Sprintf("password \"Auth\" SCRV1:%s:%s", v.B64Pass(), v.B64OTP()),
}
case line == ">PASSWORD:Need 'Auth' username/password":
cmd = []string{
fmt.Sprintf("username \"Auth\" %s", v.authCache.User),
fmt.Sprintf("password \"Auth\" %s", v.authCache.Pass),
}
}
for _, c := range cmd {
v.sendCommand([]string{c})
}
v.authWait = false
v.Message = ""
v.Status = "Connected"
}
func (v *OpenVpnSrv) AuthUserPass(user, pass, otp string) {
auth := OpenVpnPassword{user, pass, otp}
if v.authCache == nil {
v.authCache = &auth
}
switch v.Status {
case "Need Password and OTP":
v.authCache.User = user
v.authCache.Pass = pass
v.authCache.OTP = otp
case "Need OTP":
v.authCache.OTP = otp
case "Need Password":
v.authCache.User = user
v.authCache.Pass = pass
default:
return
}
v.chanPass <- auth
v.Status = "Check Password"
}
func (v *OpenVpnSrv) waitForRelase() {
v.Status = "Hold"
if v.hold {
return
}
v.hold = true
<-v.chanHold
v.sendCommand([]string{"hold release"})
v.sendCommand([]string{"hold off"})
}
func (v *OpenVpnSrv) ReleaseHold() {
if !v.hold {
return
}
v.hold = false
v.chanHold <- true
v.Status = "Waiting for connexion"
}