diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index b8a355cc..2feb20e1 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -130,6 +130,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return cli.OpenvpnConfig(logger, env) case "update": return cli.Update(ctx, args[2:], logger) + case "format-servers": + return cli.FormatServers(args[2:]) default: return fmt.Errorf("%w: %s", errCommandUnknown, args[1]) } diff --git a/internal/cli/cli.go b/internal/cli/cli.go index d28f1634..ebc47bf9 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -8,6 +8,7 @@ type CLIer interface { HealthChecker OpenvpnConfigMaker Updater + ServersFormatter } type CLI struct { diff --git a/internal/cli/formatservers.go b/internal/cli/formatservers.go new file mode 100644 index 00000000..397ebba3 --- /dev/null +++ b/internal/cli/formatservers.go @@ -0,0 +1,124 @@ +package cli + +import ( + "errors" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/storage" +) + +type ServersFormatter interface { + FormatServers(args []string) error +} + +var ( + ErrFormatNotRecognized = errors.New("format is not recognized") + ErrProviderUnspecified = errors.New("VPN provider to format was not specified") + ErrOpenOutputFile = errors.New("cannot open output file") + ErrWriteOutput = errors.New("cannot write to output file") + ErrCloseOutputFile = errors.New("cannot close output file") +) + +func (c *CLI) FormatServers(args []string) error { + var format, output string + var cyberghost, fastestvpn, hideMyAss, ipvanish, ivpn, mullvad, + nordvpn, pia, privado, privatevpn, protonvpn, purevpn, surfshark, + torguard, vpnUnlimited, vyprvpn, windscribe bool + flagSet := flag.NewFlagSet("markdown", flag.ExitOnError) + flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'") + flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to") + flagSet.BoolVar(&cyberghost, "cyberghost", false, "Format Cyberghost servers") + flagSet.BoolVar(&fastestvpn, "fastestvpn", false, "Format FastestVPN servers") + flagSet.BoolVar(&hideMyAss, "hidemyass", false, "Format HideMyAss servers") + flagSet.BoolVar(&ipvanish, "ipvanish", false, "Format IpVanish servers") + flagSet.BoolVar(&ivpn, "ivpn", false, "Format IVPN servers") + flagSet.BoolVar(&mullvad, "mullvad", false, "Format Mullvad servers") + flagSet.BoolVar(&nordvpn, "nordvpn", false, "Format Nordvpn servers") + flagSet.BoolVar(&pia, "pia", false, "Format Private Internet Access servers") + flagSet.BoolVar(&privado, "privado", false, "Format Privado servers") + flagSet.BoolVar(&privatevpn, "privatevpn", false, "Format Private VPN servers") + flagSet.BoolVar(&protonvpn, "protonvpn", false, "Format Protonvpn servers") + flagSet.BoolVar(&purevpn, "purevpn", false, "Format Purevpn servers") + flagSet.BoolVar(&surfshark, "surfshark", false, "Format Surfshark servers") + flagSet.BoolVar(&torguard, "torguard", false, "Format Torguard servers") + flagSet.BoolVar(&vpnUnlimited, "vpnunlimited", false, "Format VPN Unlimited servers") + flagSet.BoolVar(&vyprvpn, "vyprvpn", false, "Format Vyprvpn servers") + flagSet.BoolVar(&windscribe, "windscribe", false, "Format Windscribe servers") + if err := flagSet.Parse(args); err != nil { + return err + } + + if format != "markdown" { + return fmt.Errorf("%w: %s", ErrFormatNotRecognized, format) + } + + logger := newNoopLogger() + storage, err := storage.New(logger, constants.ServersData) + if err != nil { + return fmt.Errorf("%w: %s", ErrNewStorage, err) + } + currentServers := storage.GetServers() + + var formatted string + switch { + case cyberghost: + formatted = currentServers.Cyberghost.ToMarkdown() + case fastestvpn: + formatted = currentServers.Fastestvpn.ToMarkdown() + case hideMyAss: + formatted = currentServers.HideMyAss.ToMarkdown() + case ipvanish: + formatted = currentServers.Ipvanish.ToMarkdown() + case ivpn: + formatted = currentServers.Ivpn.ToMarkdown() + case mullvad: + formatted = currentServers.Mullvad.ToMarkdown() + case nordvpn: + formatted = currentServers.Nordvpn.ToMarkdown() + case pia: + formatted = currentServers.Pia.ToMarkdown() + case privado: + formatted = currentServers.Privado.ToMarkdown() + case privatevpn: + formatted = currentServers.Privatevpn.ToMarkdown() + case protonvpn: + formatted = currentServers.Protonvpn.ToMarkdown() + case purevpn: + formatted = currentServers.Purevpn.ToMarkdown() + case surfshark: + formatted = currentServers.Surfshark.ToMarkdown() + case torguard: + formatted = currentServers.Torguard.ToMarkdown() + case vpnUnlimited: + formatted = currentServers.VPNUnlimited.ToMarkdown() + case vyprvpn: + formatted = currentServers.Vyprvpn.ToMarkdown() + case windscribe: + formatted = currentServers.Windscribe.ToMarkdown() + default: + return ErrProviderUnspecified + } + + output = filepath.Clean(output) + file, err := os.OpenFile(output, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("%w: %s", ErrOpenOutputFile, err) + } + + _, err = fmt.Fprint(file, formatted) + if err != nil { + _ = file.Close() + return fmt.Errorf("%w: %s", ErrWriteOutput, err) + } + + err = file.Close() + if err != nil { + return fmt.Errorf("%w: %s", ErrCloseOutputFile, err) + } + + return nil +} diff --git a/internal/cli/nooplogger.go b/internal/cli/nooplogger.go new file mode 100644 index 00000000..6fb70108 --- /dev/null +++ b/internal/cli/nooplogger.go @@ -0,0 +1,16 @@ +package cli + +import "github.com/qdm12/golibs/logging" + +type noopLogger struct{} + +func newNoopLogger() *noopLogger { + return new(noopLogger) +} + +func (l *noopLogger) Debug(s string) {} +func (l *noopLogger) Info(s string) {} +func (l *noopLogger) Warn(s string) {} +func (l *noopLogger) Error(s string) {} +func (l *noopLogger) PatchLevel(level logging.Level) {} +func (l *noopLogger) PatchPrefix(prefix string) {} diff --git a/internal/models/markdown.go b/internal/models/markdown.go new file mode 100644 index 00000000..560c252d --- /dev/null +++ b/internal/models/markdown.go @@ -0,0 +1,252 @@ +package models + +import ( + "fmt" + "strings" +) + +func boolToMarkdown(b bool) string { + if b { + return "✅" + } + return "❎" +} + +func markdownTableHeading(legendFields ...string) (markdown string) { + return "| " + strings.Join(legendFields, " | ") + " |\n" + + "|" + strings.Repeat(" --- |", len(legendFields)) + "\n" +} + +func (s *CyberghostServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Region", "Group", "Hostname") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s CyberghostServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | `%s` |", s.Region, s.Group, s.Hostname) +} + +func (s *FastestvpnServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *FastestvpnServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | `%s` | %s | %s |", + s.Country, s.Hostname, boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *HideMyAssServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "Region", "City", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *HideMyAssServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | %s | `%s` | %s | %s |", + s.Country, s.Region, s.City, s.Hostname, + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *IpvanishServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "City", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *IpvanishServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | `%s` | %s | %s |", + s.Country, s.City, s.Hostname, + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *IvpnServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "City", "ISP", "Hostname", "VPN", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *IvpnServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | %s | `%s` | %s | %s | %s |", + s.Country, s.City, s.ISP, s.Hostname, s.VPN, + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *MullvadServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "City", "ISP", "Owned", + "Hostname", "VPN") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *MullvadServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | %s | %s | `%s` | %s |", + s.Country, s.City, s.ISP, boolToMarkdown(s.Owned), + s.Hostname, s.VPN) +} + +func (s *NordvpnServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Region", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *NordvpnServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | `%s` | %s | %s |", + s.Region, s.Hostname, + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *PrivadoServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "Region", "City", "Hostname") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *PrivadoServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | %s | `%s` |", + s.Country, s.Region, s.City, s.Hostname) +} + +func (s *PiaServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Region", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *PIAServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | `%s` | %s | %s |", + s.Region, s.Hostname, + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *PrivatevpnServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "City", "Hostname") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *PrivatevpnServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | `%s` |", + s.Country, s.City, s.Hostname) +} + +func (s *ProtonvpnServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "Region", "City", "Hostname", "Free tier") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *ProtonvpnServer) ToMarkdown() (markdown string) { + isFree := strings.Contains(strings.ToLower(s.Name), "free") + return fmt.Sprintf("| %s | %s | %s | `%s` | %s |", + s.Country, s.Region, s.City, s.Hostname, boolToMarkdown(isFree)) +} + +func (s *PurevpnServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "Region", "City", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *PurevpnServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | %s | `%s` | %s | %s |", + s.Country, s.Region, s.City, s.Hostname, + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *SurfsharkServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Region", "Country", "City", "Hostname", "Multi-hop", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *SurfsharkServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | %s | `%s` | %s | %s | %s |", + s.Region, s.Country, s.City, s.Hostname, boolToMarkdown(s.MultiHop), + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *TorguardServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "City", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *TorguardServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | `%s` | %s | %s |", + s.Country, s.City, s.Hostname, + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *VPNUnlimitedServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Country", "City", "Hostname", "Free tier", "Streaming", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *VPNUnlimitedServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | `%s` | %s | %s | %s | %s |", + s.Country, s.City, s.Hostname, + boolToMarkdown(s.Free), boolToMarkdown(s.Stream), + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *VyprvpnServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Region", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *VyprvpnServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | `%s` | %s | %s |", + s.Region, s.Hostname, + boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + +func (s *WindscribeServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("Region", "City", "Hostname", "VPN") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *WindscribeServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | `%s` | %s |", + s.Region, s.City, s.Hostname, s.VPN) +} diff --git a/internal/models/markdown_test.go b/internal/models/markdown_test.go new file mode 100644 index 00000000..7074550a --- /dev/null +++ b/internal/models/markdown_test.go @@ -0,0 +1,45 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_CyberghostServers_ToMarkdown(t *testing.T) { + t.Parallel() + + servers := CyberghostServers{ + Servers: []CyberghostServer{ + {Region: "a", Group: "A", Hostname: "xa"}, + {Region: "b", Group: "A", Hostname: "xb"}, + }, + } + + markdown := servers.ToMarkdown() + const expected = "| Region | Group | Hostname |\n" + + "| --- | --- | --- |\n" + + "| a | A | `xa` |\n" + + "| b | A | `xb` |\n" + + assert.Equal(t, expected, markdown) +} + +func Test_FastestvpnServers_ToMarkdown(t *testing.T) { + t.Parallel() + + servers := FastestvpnServers{ + Servers: []FastestvpnServer{ + {Country: "a", Hostname: "xa", TCP: true}, + {Country: "b", Hostname: "xb", UDP: true}, + }, + } + + markdown := servers.ToMarkdown() + const expected = "| Country | Hostname | TCP | UDP |\n" + + "| --- | --- | --- | --- |\n" + + "| a | `xa` | ✅ | ❎ |\n" + + "| b | `xb` | ❎ | ✅ |\n" + + assert.Equal(t, expected, markdown) +}