From 7c102c00288ba73f2bb994ee368507ea27af8759 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Sun, 30 Aug 2020 14:48:57 +0000 Subject: [PATCH] Fix #135 --- Dockerfile | 1 + README.md | 1 + cmd/gluetun/main.go | 19 ++++++- internal/params/params.go | 6 +++ internal/settings/dns.go | 4 -- internal/settings/settings.go | 31 ++++++++--- internal/settings/shadowsocks.go | 4 +- internal/settings/tinyproxy.go | 4 +- internal/version/github.go | 58 +++++++++++++++++++++ internal/version/version.go | 88 ++++++++++++++++++++++++++++++++ internal/version/version_test.go | 64 +++++++++++++++++++++++ 11 files changed, 262 insertions(+), 18 deletions(-) create mode 100644 internal/version/github.go create mode 100644 internal/version/version.go create mode 100644 internal/version/version_test.go diff --git a/Dockerfile b/Dockerfile index 68dba486..96eb6fdc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,7 @@ LABEL \ org.opencontainers.image.title="VPN client for PIA, Mullvad, Windscribe, Surfshark and Cyberghost" \ org.opencontainers.image.description="VPN client to tunnel to PIA, Mullvad, Windscribe, Surfshark and Cyberghost servers using OpenVPN, IPtables, DNS over TLS and Alpine Linux" ENV VPNSP=pia \ + VERSION_INFORMATION=on \ PROTOCOL=udp \ OPENVPN_VERBOSITY=1 \ OPENVPN_ROOT=no \ diff --git a/README.md b/README.md index 2b88fcfd..3527240e 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ That one is important if you want to connect to the container from your LAN for | Variable | Default | Choices | Description | | --- | --- | --- | --- | | `PUBLICIP_PERIOD` | `12h` | Valid duration | Period to check for public IP address. Set to `0` to disable. | +| `VERSION_INFORMATION` | `on` | `on`, `off` | Logs a message indicating if a newer version is available once the VPN is connected | ## Connect to it diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index b63bc8dd..9bd60835 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "net/http" "os" "os/signal" "strings" @@ -25,6 +26,7 @@ import ( "github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/storage" "github.com/qdm12/gluetun/internal/tinyproxy" + versionpkg "github.com/qdm12/gluetun/internal/version" "github.com/qdm12/golibs/command" "github.com/qdm12/golibs/files" "github.com/qdm12/golibs/logging" @@ -220,6 +222,18 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go restartShadowsocks() } + versionInformation := func() { + if !allSettings.VersionInformation { + return + } + client := &http.Client{Timeout: 5 * time.Second} + message, err := versionpkg.GetMessage(version, commit, client) + if err != nil { + logger.Error(err) + return + } + logger.Info(message) + } go func() { var restartTickerContext context.Context var restartTickerCancel context.CancelFunc = func() {} @@ -232,7 +246,7 @@ func _main(background context.Context, args []string) int { //nolint:gocognit,go restartTickerCancel() restartTickerContext, restartTickerCancel = context.WithCancel(ctx) go unboundLooper.RunRestartTicker(restartTickerContext) - onConnected(allSettings, logger, routingConf, portForward, restartUnbound, restartPublicIP) + onConnected(allSettings, logger, routingConf, portForward, restartUnbound, restartPublicIP, versionInformation) } } }() @@ -336,7 +350,7 @@ func collectStreamLines(ctx context.Context, streamMerger command.StreamMerger, } func onConnected(allSettings settings.Settings, logger logging.Logger, routingConf routing.Routing, - portForward, restartUnbound, restartPublicIP func(), + portForward, restartUnbound, restartPublicIP, versionInformation func(), ) { restartUnbound() restartPublicIP() @@ -354,4 +368,5 @@ func onConnected(allSettings settings.Settings, logger logging.Logger, routingCo logger.Info("Gateway VPN IP address: %s", vpnGatewayIP) } } + versionInformation() } diff --git a/internal/params/params.go b/internal/params/params.go index b44df5cd..1c765dd8 100644 --- a/internal/params/params.go +++ b/internal/params/params.go @@ -108,6 +108,8 @@ type Reader interface { // Public IP getters GetPublicIPPeriod() (period time.Duration, err error) + + GetVersionInformation() (enabled bool, err error) } type reader struct { @@ -138,3 +140,7 @@ func (r *reader) GetVPNSP() (vpnServiceProvider models.VPNProvider, err error) { } return models.VPNProvider(s), err } + +func (r *reader) GetVersionInformation() (enabled bool, err error) { + return r.envParams.GetOnOff("VERSION_INFORMATION", libparams.Default("on")) +} diff --git a/internal/settings/dns.go b/internal/settings/dns.go index 283b6374..b2150c69 100644 --- a/internal/settings/dns.go +++ b/internal/settings/dns.go @@ -31,10 +31,6 @@ type DNS struct { } func (d *DNS) String() string { - const ( - enabled = "enabled" - disabled = "disabled" - ) if !d.Enabled { return fmt.Sprintf("DNS over TLS disabled, using plaintext DNS %s", d.PlaintextAddress) } diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 1eac74e8..ab8131ca 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -8,19 +8,29 @@ import ( "github.com/qdm12/gluetun/internal/params" ) +const ( + enabled = "enabled" + disabled = "disabled" +) + // Settings contains all settings for the program to run type Settings struct { - VPNSP models.VPNProvider - OpenVPN OpenVPN - System System - DNS DNS - Firewall Firewall - TinyProxy TinyProxy - ShadowSocks ShadowSocks - PublicIPPeriod time.Duration + VPNSP models.VPNProvider + OpenVPN OpenVPN + System System + DNS DNS + Firewall Firewall + TinyProxy TinyProxy + ShadowSocks ShadowSocks + PublicIPPeriod time.Duration + VersionInformation bool } func (s *Settings) String() string { + versionInformation := disabled + if s.VersionInformation { + versionInformation = enabled + } return strings.Join([]string{ "Settings summary below:", s.OpenVPN.String(), @@ -30,6 +40,7 @@ func (s *Settings) String() string { s.TinyProxy.String(), s.ShadowSocks.String(), "Public IP check period: " + s.PublicIPPeriod.String(), + "Version information: " + versionInformation, "", // new line at the end }, "\n") } @@ -69,5 +80,9 @@ func GetAllSettings(paramsReader params.Reader) (settings Settings, err error) { if err != nil { return settings, err } + settings.VersionInformation, err = paramsReader.GetVersionInformation() + if err != nil { + return settings, err + } return settings, nil } diff --git a/internal/settings/shadowsocks.go b/internal/settings/shadowsocks.go index 8cacee35..c49e5161 100644 --- a/internal/settings/shadowsocks.go +++ b/internal/settings/shadowsocks.go @@ -20,9 +20,9 @@ func (s *ShadowSocks) String() string { if !s.Enabled { return "ShadowSocks settings: disabled" } - log := "disabled" + log := disabled if s.Log { - log = "enabled" + log = enabled } settingsList := []string{ "ShadowSocks settings:", diff --git a/internal/settings/tinyproxy.go b/internal/settings/tinyproxy.go index c55300de..f578e77e 100644 --- a/internal/settings/tinyproxy.go +++ b/internal/settings/tinyproxy.go @@ -21,9 +21,9 @@ func (t *TinyProxy) String() string { if !t.Enabled { return "TinyProxy settings: disabled" } - auth := "disabled" + auth := disabled if t.User != "" { - auth = "enabled" + auth = enabled } settingsList := []string{ fmt.Sprintf("Port: %d", t.Port), diff --git a/internal/version/github.go b/internal/version/github.go new file mode 100644 index 00000000..46fc490a --- /dev/null +++ b/internal/version/github.go @@ -0,0 +1,58 @@ +package version + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "time" +) + +type githubRelease struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + Prerelease bool `json:"prerelease"` + PublishedAt time.Time `json:"published_at"` +} + +type githubCommit struct { + Sha string `json:"sha"` + Commit struct { + Committer struct { + Date time.Time `json:"date"` + } `json:"committer"` + } +} + +func getGithubReleases(client *http.Client) (releases []githubRelease, err error) { + const url = "https://api.github.com/repos/qdm12/gluetun/releases" + response, err := client.Get(url) + if err != nil { + return nil, err + } + defer response.Body.Close() + b, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, &releases); err != nil { + return nil, err + } + return releases, nil +} + +func getGithubCommits(client *http.Client) (commits []githubCommit, err error) { + const url = "https://api.github.com/repos/qdm12/gluetun/commits" + response, err := client.Get(url) + if err != nil { + return nil, err + } + defer response.Body.Close() + b, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, &commits); err != nil { + return nil, err + } + return commits, nil +} diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 00000000..dea6c9da --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,88 @@ +package version + +import ( + "fmt" + "net/http" + "time" +) + +// GetMessage returns a message for the user describing if there is a newer version +// available. It should only be called once the tunnel is established. +func GetMessage(version, commitShort string, client *http.Client) (message string, err error) { + if version == "latest" { + // Find # of commits between current commit and latest commit + commitsSince, err := getCommitsSince(client, commitShort) + if err != nil { + return "", fmt.Errorf("cannot get version information: %w", err) + } else if commitsSince == 0 { + return fmt.Sprintf("You are running on the bleeding edge of %s!", version), nil + } + commits := "commits" + if commitsSince == 1 { + commits = "commit" + } + return fmt.Sprintf("You are running %d %s behind the most recent %s", commitsSince, commits, version), nil + } + tagName, name, releaseTime, err := getLatestRelease(client) + if err != nil { + return "", fmt.Errorf("cannot get version information: %w", err) + } + if tagName == version { + return fmt.Sprintf("You are running the latest release %s", version), nil + } + timeSinceRelease := formatDuration(time.Since(releaseTime)) + return fmt.Sprintf("There is a new release %s (%s) created %s ago", + tagName, name, timeSinceRelease), + nil +} + +func formatDuration(duration time.Duration) string { + switch { + case duration < time.Minute: + seconds := int(duration.Round(time.Second).Seconds()) + if seconds < 2 { + return fmt.Sprintf("%d second", seconds) + } + return fmt.Sprintf("%d seconds", seconds) + case duration <= time.Hour: + minutes := int(duration.Round(time.Minute).Minutes()) + if minutes == 1 { + return "1 minute" + } + return fmt.Sprintf("%d minutes", minutes) + case duration < 48*time.Hour: + hours := int(duration.Truncate(time.Hour).Hours()) + return fmt.Sprintf("%d hours", hours) + default: + days := int(duration.Truncate(time.Hour).Hours() / 24) + return fmt.Sprintf("%d days", days) + } +} + +func getLatestRelease(client *http.Client) (tagName, name string, time time.Time, err error) { + releases, err := getGithubReleases(client) + if err != nil { + return "", "", time, err + } + for _, release := range releases { + if release.Prerelease { + continue + } + return release.TagName, release.Name, release.PublishedAt, nil + } + return "", "", time, fmt.Errorf("no releases found") +} + +func getCommitsSince(client *http.Client, commitShort string) (n int, err error) { + commits, err := getGithubCommits(client) + if err != nil { + return 0, err + } + for i := range commits { + if commits[i].Sha[:7] == commitShort { + return n, nil + } + n++ + } + return 0, fmt.Errorf("no commit matching %q was found", commitShort) +} diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 00000000..bd560665 --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,64 @@ +package version + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_formatDuration(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + duration time.Duration + s string + }{ + "zero": { + s: "0 second", + }, + "one second": { + duration: time.Second, + s: "1 second", + }, + "59 seconds": { + duration: 59 * time.Second, + s: "59 seconds", + }, + "1 minute": { + duration: time.Minute, + s: "1 minute", + }, + "2 minutes": { + duration: 2 * time.Minute, + s: "2 minutes", + }, + "1 hour": { + duration: time.Hour, + s: "60 minutes", + }, + "2 hours": { + duration: 2 * time.Hour, + s: "2 hours", + }, + "26 hours": { + duration: 26 * time.Hour, + s: "26 hours", + }, + "28 hours": { + duration: 28 * time.Hour, + s: "28 hours", + }, + "55 hours": { + duration: 55 * time.Hour, + s: "2 days", + }, + } + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + s := formatDuration(testCase.duration) + assert.Equal(t, testCase.s, s) + }) + } +}