13 Commits

Author SHA1 Message Date
renovate[bot]
184ea2acf2 fix(deps): update module golang.org/x/net to v0.51.0 (#175) 2026-02-28 06:55:09 -05:00
renovate[bot]
e2a3d5ab73 chore(deps): update goreleaser/goreleaser-action action to v7 (#174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 12:43:22 -05:00
github-actions[bot]
7948580d1d chore: update vendorHash in flake.nix 2026-02-16 14:41:10 +00:00
bb88db92c1 Refactor codebase 2026-02-16 09:40:34 -05:00
323709b0a1 Ignore windows arm 2026-02-14 07:49:46 -05:00
renovate[bot]
65509016cb chore(deps): update dependency go to v1.26.0 (#169)
* Update dependency go to v1.26.0

* Add nix dev shell

* Go mod tidy

* Remove version in glangci-lint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dave Gallant <davegallant@proton.me>
2026-02-14 07:38:10 -05:00
renovate[bot]
9892ebe864 Update actions/checkout action to v6 (#172)
* Update actions/checkout action to v6

* chore: update vendorHash in flake.nix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 07:15:06 -05:00
renovate[bot]
a3e448a658 Update cachix/install-nix-action action to v31 (#173)
* Update cachix/install-nix-action action to v31

* chore: update vendorHash in flake.nix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 07:14:54 -05:00
github-actions[bot]
33a1b1b1d3 chore: update vendorHash in flake.nix 2026-02-14 12:13:48 +00:00
renovate[bot]
c03d27afcb Update module github.com/olekukonko/tablewriter to v1.1.3 (#171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 07:11:23 -05:00
4027784d1d Update flake.nix vendor-hash in a workflow 2026-02-14 06:59:08 -05:00
renovate[bot]
34054180ed Update module github.com/olekukonko/tablewriter to v1 (#155)
* Update module github.com/olekukonko/tablewriter to v1

* Fix lint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dave Gallant <davegallant@proton.me>
2026-02-13 22:06:53 -05:00
renovate[bot]
7da150493c Update module golang.org/x/net to v0.50.0 (#170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 21:58:48 -05:00
17 changed files with 332 additions and 223 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -19,7 +19,5 @@ jobs:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v9 uses: golangci/golangci-lint-action@v9
with:
version: v2.6.2
- name: test - name: test
run: make test run: make test

View File

@@ -21,9 +21,8 @@ jobs:
go-version: "1.26" go-version: "1.26"
- -
name: Run GoReleaser name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
version: '~> v2'
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}

View File

@@ -0,0 +1,58 @@
name: Update vendorHash in flake.nix
on:
push:
paths:
- 'go.mod'
- 'go.sum'
workflow_dispatch:
permissions:
contents: write
jobs:
update-hash:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixpkgs-unstable
- name: Calculate new vendorHash
id: hash
run: |
# Set vendorHash to empty string to trigger hash mismatch
sed -i 's|vendorHash = .*|vendorHash = "";|' flake.nix
# Try to build and extract the expected hash from error message
BUILD_OUTPUT=$(nix build .#vpngate 2>&1 || true)
HASH=$(echo "$BUILD_OUTPUT" | grep -oP 'got:\s*\K(sha256-[a-zA-Z0-9+/]+={0,2})' | head -1)
if [ -z "$HASH" ]; then
echo "Build output:"
echo "$BUILD_OUTPUT"
echo "Failed to extract hash from build output"
exit 1
fi
echo "hash=$HASH" >> $GITHUB_OUTPUT
echo "Calculated hash: $HASH"
- name: Update flake.nix with correct hash
run: |
sed -i "s|vendorHash = \"\";|vendorHash = \"${{ steps.hash.outputs.hash }}\";|" flake.nix
- name: Commit and push if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet flake.nix; then
echo "No changes to commit"
else
git add flake.nix
git commit -m "chore: update vendorHash in flake.nix"
git push
fi

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
dist dist
.vscode .vscode
# direnv
.direnv
.envrc.local

View File

@@ -21,6 +21,9 @@ builds:
- arm64 - arm64
goarm: goarm:
- "7" - "7"
ignore:
- goos: windows
goarch: arm
ldflags: ldflags:
- -s -w - -s -w
mod_timestamp: "{{ .CommitTimestamp }}" mod_timestamp: "{{ .CommitTimestamp }}"

View File

@@ -31,7 +31,6 @@ func init() {
var connectCmd = &cobra.Command{ var connectCmd = &cobra.Command{
Use: "connect", Use: "connect",
Short: "Connect to a vpn server (survey selection appears if hostname is not provided)", Short: "Connect to a vpn server (survey selection appears if hostname is not provided)",
Long: `Connect to a vpn from a list of relay servers`, Long: `Connect to a vpn from a list of relay servers`,
Args: cobra.RangeArgs(0, 1), Args: cobra.RangeArgs(0, 1),
@@ -39,49 +38,43 @@ var connectCmd = &cobra.Command{
vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy) vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy)
if err != nil { if err != nil {
log.Fatal().Msg(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
serverSelection := []string{} // Build server selection options and hostname lookup map
serverSelected := vpn.Server{} serverSelection := make([]string, len(*vpnServers))
serverMap := make(map[string]vpn.Server, len(*vpnServers))
for _, s := range *vpnServers { for i, s := range *vpnServers {
serverSelection = append(serverSelection, fmt.Sprintf("%s (%s)", s.HostName, s.CountryLong)) serverSelection[i] = fmt.Sprintf("%s (%s)", s.HostName, s.CountryLong)
serverMap[s.HostName] = s
} }
selection := "" selection := ""
var serverSelected vpn.Server
if !flagRandom {
if len(args) > 0 {
selection = args[0]
} else {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Choose a server:", Message: "Choose a server:",
Options: serverSelection, Options: serverSelection,
} }
if !flagRandom {
if len(args) > 0 {
selection = args[0]
} else {
err := survey.AskOne(prompt, &selection, survey.WithPageSize(10)) err := survey.AskOne(prompt, &selection, survey.WithPageSize(10))
if err != nil { if err != nil {
log.Error().Msg("Unable to obtain hostname from survey") log.Fatal().Msg("Unable to obtain hostname from survey")
os.Exit(1)
} }
} }
// Server lookup from selection could be more optimized with a hash map // Lookup server from selection using map for O(1) lookup
for _, s := range *vpnServers { hostname := extractHostname(selection)
if strings.Contains(selection, s.HostName) { if server, exists := serverMap[hostname]; exists {
serverSelected = s serverSelected = server
} } else {
}
if serverSelected.HostName == "" {
log.Fatal().Msgf("Server '%s' was not found", selection) log.Fatal().Msgf("Server '%s' was not found", selection)
os.Exit(1)
} }
} }
for { for {
if flagRandom { if flagRandom {
// Select a random server // Select a random server
serverSelected = (*vpnServers)[rand.Intn(len(*vpnServers))] serverSelected = (*vpnServers)[rand.Intn(len(*vpnServers))]
@@ -90,23 +83,19 @@ var connectCmd = &cobra.Command{
decodedConfig, err := base64.StdEncoding.DecodeString(serverSelected.OpenVpnConfigData) decodedConfig, err := base64.StdEncoding.DecodeString(serverSelected.OpenVpnConfigData)
if err != nil { if err != nil {
log.Fatal().Msg(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
tmpfile, err := os.CreateTemp("", "vpngate-openvpn-config-") tmpfile, err := os.CreateTemp("", "vpngate-openvpn-config-")
if err != nil { if err != nil {
log.Fatal().Msg(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
if _, err := tmpfile.Write(decodedConfig); err != nil { if _, err := tmpfile.Write(decodedConfig); err != nil {
log.Fatal().Msg(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
if err := tmpfile.Close(); err != nil { if err := tmpfile.Close(); err != nil {
log.Fatal().Msg(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
log.Info().Msgf("Connecting to %s (%s) in %s", serverSelected.HostName, serverSelected.IPAddr, serverSelected.CountryLong) log.Info().Msgf("Connecting to %s (%s) in %s", serverSelected.HostName, serverSelected.IPAddr, serverSelected.CountryLong)
@@ -114,16 +103,22 @@ var connectCmd = &cobra.Command{
err = vpn.Connect(tmpfile.Name()) err = vpn.Connect(tmpfile.Name())
if err != nil && !flagReconnect { if err != nil && !flagReconnect {
log.Fatal().Msg(err.Error()) // VPN connection failed and reconnect is disabled
os.Exit(1) _ = os.Remove(tmpfile.Name())
} else { log.Fatal().Msg("VPN connection failed")
err = os.Remove(tmpfile.Name())
if err != nil {
log.Fatal().Msg(err.Error())
os.Exit(1)
}
} }
// Always try to clean up temporary file
_ = os.Remove(tmpfile.Name())
} }
}, },
} }
// extractHostname extracts the hostname from the selection string (format: "hostname (country)")
func extractHostname(selection string) string {
parts := strings.Split(selection, " (")
if len(parts) > 0 {
return parts[0]
}
return selection
}

View File

@@ -4,7 +4,7 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/olekukonko/tablewriter" tw "github.com/olekukonko/tablewriter"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/davegallant/vpngate/pkg/vpn" "github.com/davegallant/vpngate/pkg/vpn"
@@ -27,15 +27,20 @@ var listCmd = &cobra.Command{
vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy) vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy)
if err != nil { if err != nil {
log.Fatal().Msg(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
table := tablewriter.NewWriter(os.Stdout) table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"#", "HostName", "Country", "Ping", "Score"}) table.Header([]string{"#", "HostName", "Country", "Ping", "Score"})
for i, v := range *vpnServers { for i, v := range *vpnServers {
table.Append([]string{strconv.Itoa(i + 1), v.HostName, v.CountryLong, v.Ping, strconv.Itoa(v.Score)}) err := table.Append([]string{strconv.Itoa(i + 1), v.HostName, v.CountryLong, v.Ping, strconv.Itoa(v.Score)})
if err != nil {
log.Fatal().Msg(err.Error())
}
}
err = table.Render()
if err != nil {
log.Fatal().Msg(err.Error())
} }
table.Render() // Send output
}, },
} }

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1763934636, "lastModified": 1770843696,
"narHash": "sha256-9glbI7f1uU+yzQCq5LwLgdZqx6svOhZWkd4JRY265fc=", "narHash": "sha256-LovWTGDwXhkfCOmbgLVA10bvsi/P8eDDpRudgk68HA8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ee09932cedcef15aaf476f9343d1dea2cb77e261", "rev": "2343bbb58f99267223bc2aac4fc9ea301a155a16",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -1,4 +1,6 @@
{ {
description = "vpngate - VPN server connector";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
@@ -16,7 +18,7 @@
pkgs.buildGo125Module rec { pkgs.buildGo125Module rec {
name = "vpngate"; name = "vpngate";
src = ./.; src = ./.;
vendorHash = "sha256-tVNffrT+r3pA+0pvBaNKsq9K4wkB7WepkuSa1nCWloc="; vendorHash = "sha256-ZPLGGyg056/wGOE90KaHjGc9eypHrGJBZMDg5KpBWqw=";
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper ]; nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper ];
env.CGO_ENABLED = 0; env.CGO_ENABLED = 0;
doCheck = false; doCheck = false;
@@ -33,12 +35,19 @@
default = vg; default = vg;
vpngate = vg; vpngate = vg;
}; };
devShell = pkgs.mkShell { devShells.default = pkgs.mkShell {
name = "vpngate-dev";
description = "Development environment for vpngate";
packages = with pkgs; [ packages = with pkgs; [
go_1_26
gopls gopls
gotools gotools
go_1_25 golangci-lint
]; ];
shellHook = ''
echo "Welcome to the vpngate dev environment"
go version
'';
}; };
}; };
in in

23
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/davegallant/vpngate module github.com/davegallant/vpngate
go 1.24.0 go 1.25.0
toolchain go1.26.0 toolchain go1.26.0
@@ -8,29 +8,34 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/jszwec/csvutil v1.10.0 github.com/jszwec/csvutil v1.10.0
github.com/juju/errors v1.0.0 github.com/juju/errors v1.0.0
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v1.1.3
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
golang.org/x/net v0.49.0 golang.org/x/net v0.51.0
) )
require ( require (
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.39.0 // indirect golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

66
go.sum
View File

@@ -2,6 +2,12 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -10,6 +16,8 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
@@ -34,21 +42,23 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
@@ -56,12 +66,6 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -79,12 +83,10 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -96,30 +98,18 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@@ -2,46 +2,58 @@ package exec
import ( import (
"bufio" "bufio"
"os" "io"
"os/exec" "os/exec"
"strings"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// Run executes a command in workDir and returns stdout and error. // Run executes a command in workDir and logs its output.
// The spawned process will exit upon termination of this application // If the command fails to start or setup fails, an error is logged and returned.
// to ensure a clean exit // If the command exits with a non-zero status, the error is returned without logging
// (this allows the caller to decide how to handle it).
func Run(path string, workDir string, args ...string) error { func Run(path string, workDir string, args ...string) error {
_, err := exec.LookPath(path) _, err := exec.LookPath(path)
if err != nil { if err != nil {
log.Error().Msgf("%s is required, please install it", path) log.Error().Msgf("%s is required, please install it", path)
os.Exit(1) return err
} }
cmd := exec.Command(path, args...) cmd := exec.Command(path, args...)
cmd.Dir = workDir cmd.Dir = workDir
log.Debug().Msg("Executing " + strings.Join(cmd.Args, " "))
log.Debug().Strs("command", cmd.Args).Msg("Executing command")
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
log.Fatal().Msgf("Failed to get stdout pipe: %v", err) log.Error().Msgf("Failed to get stdout pipe: %v", err)
return err
} }
stderr, err := cmd.StderrPipe()
if err != nil {
log.Error().Msgf("Failed to get stderr pipe: %v", err)
return err
}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
log.Fatal().Msgf("Failed to start command: %v", err) log.Error().Msgf("Failed to start command: %v", err)
return err
} }
scanner := bufio.NewScanner(stdout) // Combine stdout and stderr into a single reader
combined := io.MultiReader(stdout, stderr)
scanner := bufio.NewScanner(combined)
for scanner.Scan() { for scanner.Scan() {
log.Debug().Msg(scanner.Text()) log.Debug().Msg(scanner.Text())
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
log.Fatal().Msgf("Error reading stdout: %v", err) log.Error().Msgf("Error reading output: %v", err)
}
if err := cmd.Wait(); err != nil {
log.Fatal().Msgf("Command finished with error: %v", err)
return err return err
} }
return nil
// cmd.Wait() returns an error if the command exits with non-zero status
// We return this without logging since it's expected behavior for some commands
return cmd.Wait()
} }

View File

@@ -2,6 +2,7 @@ package util
import ( import (
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -11,7 +12,7 @@ func Retry(attempts int, delay time.Duration,fn func() error) error {
if err = fn(); err == nil { if err = fn(); err == nil {
return nil return nil
} }
log.Error().Msgf("Retrying after %d seconds. An error occured: %s", delay, err) log.Error().Msgf("Retrying after %v. An error occurred: %s", delay, err)
time.Sleep(delay) time.Sleep(delay)
} }
return err return err

View File

@@ -4,37 +4,42 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"os" "os"
"path" "path/filepath"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/spf13/afero"
) )
const serverCachefile = "servers.json" const serverCachefile = "servers.json"
func getCacheDir() string { func getCacheDir() (string, error) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
log.Error().Msgf("Failed to get user's home directory: %s ", err) return "", err
return ""
} }
cacheDir := path.Join(homeDir, ".vpngate", "cache") cacheDir := filepath.Join(homeDir, ".vpngate", "cache")
return cacheDir return cacheDir, nil
} }
func createCacheDir() error { func createCacheDir() error {
cacheDir := getCacheDir() cacheDir, err := getCacheDir()
AppFs := afero.NewOsFs() if err != nil {
return AppFs.MkdirAll(cacheDir, 0o700) return err
}
return os.MkdirAll(cacheDir, 0o700)
} }
func getVpnListCache() (*[]Server, error) { func getVpnListCache() (*[]Server, error) {
cacheFile := path.Join(getCacheDir(), serverCachefile) cacheDir, err := getCacheDir()
if err != nil {
return nil, err
}
cacheFile := filepath.Join(cacheDir, serverCachefile)
serversFile, err := os.Open(cacheFile) serversFile, err := os.Open(cacheFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() {
_ = serversFile.Close()
}()
byteValue, err := io.ReadAll(serversFile) byteValue, err := io.ReadAll(serversFile)
if err != nil { if err != nil {
@@ -44,7 +49,6 @@ func getVpnListCache() (*[]Server, error) {
var servers []Server var servers []Server
err = json.Unmarshal(byteValue, &servers) err = json.Unmarshal(byteValue, &servers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -53,8 +57,7 @@ func getVpnListCache() (*[]Server, error) {
} }
func writeVpnListToCache(servers []Server) error { func writeVpnListToCache(servers []Server) error {
err := createCacheDir() if err := createCacheDir(); err != nil {
if err != nil {
return err return err
} }
@@ -63,20 +66,26 @@ func writeVpnListToCache(servers []Server) error {
return err return err
} }
cacheFile := path.Join(getCacheDir(), serverCachefile) cacheDir, err := getCacheDir()
if err != nil {
err = os.WriteFile(cacheFile, f, 0o644)
return err return err
} }
cacheFile := filepath.Join(cacheDir, serverCachefile)
return os.WriteFile(cacheFile, f, 0o644)
}
func vpnListCacheIsExpired() bool { func vpnListCacheIsExpired() bool {
file, err := os.Stat(path.Join(getCacheDir(), serverCachefile)) cacheDir, err := getCacheDir()
if err != nil {
return true
}
file, err := os.Stat(filepath.Join(cacheDir, serverCachefile))
if err != nil { if err != nil {
return true return true
} }
lastModified := file.ModTime() lastModified := file.ModTime()
return (time.Since(lastModified)) > time.Duration(24*time.Hour) return time.Since(lastModified) > 24*time.Hour
} }

View File

@@ -1,28 +1,17 @@
package vpn package vpn
import ( import (
"os"
"runtime" "runtime"
"github.com/davegallant/vpngate/pkg/exec" "github.com/davegallant/vpngate/pkg/exec"
"github.com/juju/errors"
) )
// Connect to a specified OpenVPN configuration // Connect to a specified OpenVPN configuration
func Connect(configPath string) error { func Connect(configPath string) error {
tmpLogFile, err := os.CreateTemp("", "vpngate-openvpn-log-")
if err != nil {
return errors.Annotate(err, "Unable to create a temporary log file")
}
defer func() {
_ = os.Remove(tmpLogFile.Name())
}()
executable := "openvpn" executable := "openvpn"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
executable = "C:\\Program Files\\OpenVPN\\bin\\openvpn.exe" executable = "C:\\Program Files\\OpenVPN\\bin\\openvpn.exe"
} }
err = exec.Run(executable, ".", "--verb", "4", "--config", configPath, "--data-ciphers", "AES-128-CBC") return exec.Run(executable, ".", "--verb", "4", "--config", configPath, "--data-ciphers", "AES-128-CBC")
return err
} }

View File

@@ -2,10 +2,12 @@ package vpn
import ( import (
"bytes" "bytes"
"context"
"io" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os" "time"
"github.com/jszwec/csvutil" "github.com/jszwec/csvutil"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -17,6 +19,8 @@ import (
const ( const (
vpnList = "https://www.vpngate.net/api/iphone/" vpnList = "https://www.vpngate.net/api/iphone/"
httpClientTimeout = 30 * time.Second
dialTimeout = 10 * time.Second
) )
// Server holds information about a vpn relay server // Server holds information about a vpn relay server
@@ -30,20 +34,14 @@ type Server struct {
Ping string `csv:"Ping"` Ping string `csv:"Ping"`
} }
func streamToBytes(stream io.Reader) []byte { // parseVpnList parses the VPN server list from CSV format
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) { func parseVpnList(r io.Reader) (*[]Server, error) {
var servers []Server var servers []Server
serverList := streamToBytes(r) serverList, err := io.ReadAll(r)
if err != nil {
return nil, errors.Annotate(err, "Unable to read stream")
}
// Trim known invalid rows // Trim known invalid rows
serverList = bytes.TrimPrefix(serverList, []byte("*vpn_servers\r\n")) serverList = bytes.TrimPrefix(serverList, []byte("*vpn_servers\r\n"))
@@ -57,86 +55,119 @@ func parseVpnList(r io.Reader) (*[]Server, error) {
return &servers, nil return &servers, nil
} }
// createHTTPClient creates an HTTP client with optional proxy configuration
func createHTTPClient(httpProxy string, socks5Proxy string) (*http.Client, error) {
if httpProxy != "" {
proxyURL, err := url.Parse(httpProxy)
if err != nil {
return nil, errors.Annotatef(err, "Error parsing HTTP proxy: %s", httpProxy)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
return &http.Client{
Transport: transport,
Timeout: httpClientTimeout,
}, nil
}
if socks5Proxy != "" {
dialer, err := proxy.SOCKS5("tcp", socks5Proxy, nil, proxy.Direct)
if err != nil {
return nil, errors.Annotatef(err, "Error creating SOCKS5 dialer: %v", err)
}
// Create a DialContext function from the SOCKS5 dialer
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
// Check if context is already done
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Use the dialer with a timeout
conn, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
// Respect context cancellation after connection
go func() {
<-ctx.Done()
_ = conn.Close()
}()
return conn, nil
}
httpTransport := &http.Transport{
DialContext: dialContext,
}
return &http.Client{
Transport: httpTransport,
Timeout: httpClientTimeout,
}, nil
}
return &http.Client{
Timeout: httpClientTimeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: dialTimeout,
}).DialContext,
},
}, nil
}
// GetList returns a list of vpn servers // GetList returns a list of vpn servers
func GetList(httpProxy string, socks5Proxy string) (*[]Server, error) { func GetList(httpProxy string, socks5Proxy string) (*[]Server, error) {
cacheExpired := vpnListCacheIsExpired() cacheExpired := vpnListCacheIsExpired()
var servers *[]Server // Try to use cached list if not expired
var client *http.Client
if !cacheExpired { if !cacheExpired {
servers, err := getVpnListCache() servers, err := getVpnListCache()
if err == nil {
if err != nil {
log.Info().Msg("Unable to retrieve vpn list from cache")
} else {
return servers, nil return servers, nil
} }
log.Info().Msg("Unable to retrieve vpn list from cache")
} else { } else {
log.Info().Msg("The vpn server list cache has expired") log.Info().Msg("The vpn server list cache has expired")
} }
log.Info().Msg("Fetching the latest server list") log.Info().Msg("Fetching the latest server list")
if httpProxy != "" { client, err := createHTTPClient(httpProxy, socks5Proxy)
proxyURL, err := url.Parse(httpProxy)
if err != nil { if err != nil {
log.Error().Msgf("Error parsing proxy: %s", err) return nil, err
os.Exit(1)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
} }
client = &http.Client{ var servers *[]Server
Transport: transport,
}
} else if socks5Proxy != "" { err = util.Retry(5, 1, func() error {
dialer, err := proxy.SOCKS5("tcp", socks5Proxy, nil, proxy.Direct) resp, err := client.Get(vpnList)
if err != nil {
log.Error().Msgf("Error creating SOCKS5 dialer: %v", err)
os.Exit(1)
}
httpTransport := &http.Transport{
Dial: dialer.Dial,
}
client = &http.Client{
Transport: httpTransport,
}
} else {
client = &http.Client{}
}
var r *http.Response
err := util.Retry(5, 1, func() error {
var err error
r, err = client.Get(vpnList)
if err != nil { if err != nil {
return err return err
} }
defer func() { defer func() {
_ = r.Body.Close() _ = resp.Body.Close()
}() }()
if r.StatusCode != 200 { if resp.StatusCode != http.StatusOK {
return errors.Annotatef(err, "Unexpected status code when retrieving vpn list: %d", r.StatusCode) return errors.Annotatef(err, "Unexpected status code when retrieving vpn list: %d", resp.StatusCode)
} }
servers, err = parseVpnList(r.Body) parsedServers, err := parseVpnList(resp.Body)
if err != nil { if err != nil {
return err return err
} }
err = writeVpnListToCache(*servers) servers = parsedServers
if err != nil { // Cache the servers for future use
log.Warn().Msgf("Unable to write servers to cache: %s", err) cacheErr := writeVpnListToCache(*servers)
if cacheErr != nil {
log.Warn().Msgf("Unable to write servers to cache: %s", cacheErr)
} }
return nil return nil
}) })