Add initial prototype (#1)

* Add survey library to select server

* Add speedtest

* Add --random flag to connect

* Add list command

* Cache server list

* Tail the openvpn logs so that it appears in vpngate logs

* Add goreleaser action

* Add golangci-lint action
This commit is contained in:
Dave Gallant
2020-12-31 02:56:01 -05:00
committed by GitHub
parent c52d1e990a
commit a2afbc1e35
17 changed files with 1355 additions and 0 deletions

89
pkg/vpn/cache.go Normal file
View File

@@ -0,0 +1,89 @@
package vpn
import (
"encoding/json"
"io/ioutil"
"os"
"path"
"time"
"github.com/rs/zerolog/log"
"github.com/spf13/afero"
)
const serverCachefile = "servers.json"
func getCacheDir() string {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Error().Msgf("Failed to get user's home directory: %s ", err)
return ""
}
cacheDir := path.Join(homeDir, ".vpngate", "cache")
return cacheDir
}
func createCacheDir() error {
cacheDir := getCacheDir()
var AppFs = afero.NewOsFs()
return AppFs.MkdirAll(cacheDir, 0700)
}
func getVpnListCache() (*[]Server, error) {
cacheFile := path.Join(getCacheDir(), serverCachefile)
serversFile, err := os.Open(cacheFile)
if err != nil {
return nil, err
}
byteValue, err := ioutil.ReadAll(serversFile)
if err != nil {
return nil, err
}
var servers []Server
err = json.Unmarshal(byteValue, &servers)
if err != nil {
return nil, err
}
return &servers, nil
}
func writeVpnListToCache(servers []Server) error {
err := createCacheDir()
if err != nil {
return err
}
f, err := json.MarshalIndent(servers, "", " ")
if err != nil {
return err
}
cacheFile := path.Join(getCacheDir(), serverCachefile)
err = ioutil.WriteFile(cacheFile, f, 0644)
return err
}
func vpnListCacheIsExpired() bool {
file, err := os.Stat(path.Join(getCacheDir(), serverCachefile))
if err != nil {
return true
}
lastModified := file.ModTime()
return (time.Since(lastModified)) > time.Duration(24*time.Hour)
}

48
pkg/vpn/client.go Normal file
View File

@@ -0,0 +1,48 @@
package vpn
import (
"io/ioutil"
"os"
"time"
"github.com/davegallant/vpngate/pkg/exec"
"github.com/davegallant/vpngate/pkg/network"
"github.com/hpcloud/tail"
"github.com/juju/errors"
"github.com/rs/zerolog/log"
)
// Connect to a specified OpenVPN configuration
func Connect(configPath string) error {
tmpLogFile, err := ioutil.TempFile("", "vpngate-openvpn-log-")
if err != nil {
return errors.Annotate(err, "Unable to create a temporary log file")
}
defer os.Remove(tmpLogFile.Name())
go func() {
for {
err = network.TestSpeed()
if err != nil {
log.Error().Msg("Failed to test network speed")
}
time.Sleep(time.Minute)
}
}()
go func() {
// Tail the temporary openvpn log file
t, err := tail.TailFile(tmpLogFile.Name(), tail.Config{Follow: true})
if err != nil {
log.Error().Msgf("%s", err)
}
for line := range t.Lines {
log.Debug().Msg(line.Text)
}
}()
_, err = exec.Run("openvpn", ".", "--verb", "4", "--log", tmpLogFile.Name(), "--config", configPath)
return err
}

104
pkg/vpn/list.go Normal file
View File

@@ -0,0 +1,104 @@
package vpn
import (
"net/http"
"bytes"
"io"
"github.com/jszwec/csvutil"
"github.com/rs/zerolog/log"
"github.com/juju/errors"
)
const (
vpnList = "https://www.vpngate.net/api/iphone/"
)
// Server holds in formation about a vpn relay server
type Server struct {
HostName string `csv:"#HostName"`
CountryLong string `csv:"CountryLong"`
CountryShort string `csv:"CountryShort"`
Score int `csv:"Score"`
IPAddr string `csv:"IP"`
OpenVpnConfigData string `csv:"OpenVPN_ConfigData_Base64"`
Ping string `csv:"Ping"`
}
func streamToBytes(stream io.Reader) []byte {
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(stream)
if err != nil {
log.Error().Msg("Unable to stream bytes")
}
return buf.Bytes()
}
// parse csv
func parseVpnList(r io.Reader) (*[]Server, error) {
var servers []Server
serverList := streamToBytes(r)
// Trim known invalid rows
serverList = bytes.TrimPrefix(serverList, []byte("*vpn_servers\r\n"))
serverList = bytes.TrimSuffix(serverList, []byte("*\r\n"))
if err := csvutil.Unmarshal(serverList, &servers); err != nil {
return nil, errors.Annotatef(err, "Unable to parse CSV")
}
return &servers, nil
}
// GetList returns a list of vpn servers
func GetList() (*[]Server, error) {
cacheExpired := vpnListCacheIsExpired()
var servers *[]Server
if !cacheExpired {
servers, err := getVpnListCache()
if err != nil {
log.Info().Msg("Unable to retrieve vpn list from cache")
} else {
return servers, nil
}
} else {
log.Info().Msg("The vpn server list cache has expired")
}
log.Info().Msg("Fetching the latest server list")
r, err := http.Get(vpnList)
if err != nil {
return nil, errors.Annotate(err, "Unable to retrieve vpn list")
}
defer r.Body.Close()
if r.StatusCode != 200 {
return nil, errors.Annotatef(err, "Unexpected status code when retrieving vpn list: %d", r.StatusCode)
}
servers, err = parseVpnList(r.Body)
if err != nil {
return nil, errors.Annotate(err, "unable to parse vpn list")
}
err = writeVpnListToCache(*servers)
if err != nil {
log.Warn().Msgf("Unable to write servers to cache: %s", err)
}
return servers, nil
}