mirror of
https://github.com/davegallant/vpngate.git
synced 2025-08-06 00:33:40 +00:00
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:
43
pkg/exec/run.go
Normal file
43
pkg/exec/run.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/juju/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Run executes anycommand in workDir and returns stdout and error.
|
||||
func Run(path string, workDir string, args ...string) (string, error) {
|
||||
_, err := exec.LookPath(path)
|
||||
if err != nil {
|
||||
log.Error().Msgf("%s is required, please install it", path)
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := exec.Command(path, args...)
|
||||
cmd.Dir = workDir
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
log.Debug().Msgf("Executing " + strings.Join(cmd.Args, " "))
|
||||
err = cmd.Run()
|
||||
output := strings.TrimSpace(stdout.String())
|
||||
errOut := strings.TrimSpace(stderr.String())
|
||||
if output != "" {
|
||||
log.Debug().Msgf(output)
|
||||
}
|
||||
if errOut != "" {
|
||||
log.Debug().Msgf(errOut)
|
||||
}
|
||||
if _, ok := err.(*exec.ExitError); !ok {
|
||||
return output, errors.Trace(err)
|
||||
}
|
||||
if err != nil {
|
||||
return output, errors.Annotatef(err, path, cmd.Args, errOut)
|
||||
}
|
||||
return output, nil
|
||||
}
|
52
pkg/network/speedtest.go
Normal file
52
pkg/network/speedtest.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"github.com/juju/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/showwin/speedtest-go/speedtest"
|
||||
)
|
||||
|
||||
// TestSpeed tests the speed of an active network connection
|
||||
func TestSpeed() error {
|
||||
user, err := speedtest.FetchUserInfo()
|
||||
|
||||
if err != nil {
|
||||
return errors.Annotate(err, "Unable to fetch user info")
|
||||
}
|
||||
|
||||
serverList, err := speedtest.FetchServerList(user)
|
||||
|
||||
if err != nil {
|
||||
return errors.Annotate(err, "Unable to fetch server list")
|
||||
}
|
||||
|
||||
targets, _ := serverList.FindServer([]int{})
|
||||
|
||||
if err != nil {
|
||||
return errors.Annotate(err, "Unable to find server")
|
||||
}
|
||||
|
||||
for _, s := range targets {
|
||||
err := s.PingTest()
|
||||
|
||||
if err != nil {
|
||||
log.Error().Msg("Failed to obtain ping")
|
||||
}
|
||||
|
||||
err = s.DownloadTest(true)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Msg("Failed download test")
|
||||
}
|
||||
|
||||
err = s.UploadTest(true)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Msg("Failed upload test")
|
||||
}
|
||||
|
||||
log.Info().Msgf("Latency: %s, Download: %f, Upload: %f", s.Latency, s.DLSpeed, s.ULSpeed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
89
pkg/vpn/cache.go
Normal file
89
pkg/vpn/cache.go
Normal 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
48
pkg/vpn/client.go
Normal 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
104
pkg/vpn/list.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user