diff --git a/.gitignore b/.gitignore index f0c128e..c380d33 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.push-* /.container-* /.dockerfile-* +.idea diff --git a/cmd/mullfast/main.go b/cmd/mullfast/main.go index 5806519..13dbbe5 100644 --- a/cmd/mullfast/main.go +++ b/cmd/mullfast/main.go @@ -1,188 +1,25 @@ package main import ( - "encoding/base64" - "encoding/json" "fmt" - "io/ioutil" - "net/http" - "os" "sort" "strings" - "sync" - "time" ct "github.com/daviddengcn/go-colortext" - ui "github.com/manifoldco/promptui" - "github.com/nuttapp/pinghist/ping" ) -// Set the banner for advertisement -const BANNER = "ICAgKiAgICAgICAgICAgICAgICAgICgKICggIGAgICAgICAgICAgKCAgICggIClcICkgICAgICAgICAgICAgICkKIClcKSkoICAgICAoICAgKVwgIClcKCgpLyggICAgKSAgICAgICggLygKKChfKSgpXCAgICkpXCAoKF8pKChfKS8oXykpKCAvKCAgKCAgIClcKCkpCihfKCkoKF8pIC8oKF8pIF8gICBfIChfKSlffCkoXykpIClcIChfKSkvCnwgIFwvICB8KF8pKSggfCB8IHwgfHwgfF8gKChfKV8gKChfKXwgfF8KfCB8XC98IHx8IHx8IHx8IHwgfCB8fCBfX3wvIF9gIHwoXy08fCAgX3wKfF98ICB8X3wgXF8sX3x8X3wgfF98fF98ICBcX18sX3wvX18vIFxfX3wK" +func printResults(active []*Server) { + // we already printed banner i think + // ct.ChangeColor(ct.Red, false, ct.None, false) + // fmt.Printf("\n\n%s\n", banner()) -// Set the Mullvad API URL -const URL = "https://api.mullvad.net/www/relays/all" + cPrintln(ct.Magenta, ` + ========================================== + ============ TOP 10 RESULTS ============== + ========================================== + `) -// Server struct -type Server struct { - List []float64 - Last float64 - URL string - Hostname string - Country_code string - Country_name string - City_name string - Active bool - Type string -} - -// Variable server lists -var ( - servers []*Server - active []*Server -) - -// Type for server lists -type ByLast []*Server - -// Length method -func (a ByLast) Len() int { return len(a) } - -// Swap method -func (a ByLast) Swap(i, j int) { a[i], a[j] = a[j], a[i] } - -// Less method -func (a ByLast) Less(i, j int) bool { return a[i].Last < a[j].Last } - -// Ping server to determine latency -func (s *Server) Ping() (float64, error) { - // for i := 0; i < 3; i++ { - ct.ChangeColor(ct.Cyan, false, ct.None, false) - fmt.Println("[*] Testing server:", s.URL) - - r, err := ping.Ping(s.URL) - if err != nil { - return 0, err - } - - if r.Time < 1 { - r.Time = 999999999999 - } - - s.List = append(s.List, r.Time) - s.Last = r.Time - return s.Last, nil -} - -// main function -func main() { - // Decode banner - banner, err := base64.RawStdEncoding.DecodeString(BANNER) - - ct.ChangeColor(ct.Red, false, ct.None, false) - if err != nil { - fmt.Println(err) - } else { - fmt.Printf("%s\n", banner) - } - ct.ResetColor() - - // Create prompt for VPN type - prompt := ui.Select{ - Label: "Select Mullvad VPN type", - Items: []string{"WireGuard", "OpenVPN", "Cancel"}, - } - _, promptResult, err := prompt.Run() - - if err != nil { - ct.ChangeColor(ct.Red, false, ct.None, false) - fmt.Println("\n[!] User prompt failed", err) - return - } else if promptResult == "Cancel" { - ct.ChangeColor(ct.Red, false, ct.None, false) - fmt.Printf("[!] Server test canceled by user, exiting...\n") - os.Exit(0) - } else { - ct.ChangeColor(ct.Cyan, true, ct.None, false) - fmt.Printf("[*] You selected %q servers for testing\n\n", promptResult) - } - - ct.ChangeColor(ct.Cyan, false, ct.None, false) - fmt.Println("[*] Retrieving list of Mullvad servers...") - - // Make API call for server list - response, err := http.Get(URL) - - if err != nil { - ct.ChangeColor(ct.Red, false, ct.None, false) - fmt.Println("[!] Request Error\n", err.Error()) - } - - // Wait until the response finished - defer response.Body.Close() - - // Read the io return of the response - body, err := ioutil.ReadAll(response.Body) - if err != nil { - ct.ChangeColor(ct.Red, false, ct.None, false) - fmt.Println("[!] Error in response body\n", err.Error()) - } - - // Unmarshal the JSON body return - jsonErr := json.Unmarshal(body, &servers) - if jsonErr != nil { - ct.ChangeColor(ct.Red, false, ct.None, false) - fmt.Println("[!] Json Error", jsonErr) - } - - ct.ChangeColor(ct.Green, false, ct.None, false) - fmt.Printf("[*] Retrieved %d %s servers\n", len(servers), promptResult) - - // Add the active servers to a list - for i := range servers { - servers[i].URL = servers[i].Hostname + ".mullvad.net" - if servers[i].Type == strings.ToLower(promptResult) && servers[i].Active { - active = append(active, servers[i]) - } - } - - ct.ChangeColor(ct.Green, false, ct.None, false) - fmt.Printf("[*] There are %d active servers\n\n", len(active)) - - // Test the valid servers. - var wg sync.WaitGroup - wg.Add(len(active) - 1) - - for _, server := range active { - time.Sleep(100 * time.Millisecond) - - go func(wg *sync.WaitGroup, server *Server) { - for i := 0; i < 3; i++ { - _, err := server.Ping() - if err != nil { - ct.ChangeColor(ct.Red, false, ct.None, false) - fmt.Println("[!] Error on ping", server.URL, err) - continue - } - - break - } - - wg.Done() - }(&wg, server) - } - - wg.Wait() - - ct.ChangeColor(ct.Red, false, ct.None, false) - fmt.Printf("\n\n%s\n", banner) - ct.ChangeColor(ct.Magenta, false, ct.None, false) - fmt.Println("==========================================") - fmt.Println("============ TOP 10 RESULTS ==============") - fmt.Println("==========================================") - ct.ResetColor() - - sort.Sort(ByLast(active)) + sort.Sort(SortedSlice(active)) printed := 0 for _, server := range active { if server.Last < 1 { @@ -202,3 +39,44 @@ func main() { printed++ } } + +// main should be generally as small as possible, in Go we like to break things out +func main() { + printBanner() + + var ( + err error + userChoice string + ) + + if userChoice, err = interactivePrompt(); err != nil { + cPrintln(ct.Red, "[!] UI Error\n", err.Error()) + return + } + + servers := getServers() + + cPrintln(ct.Green, "[*] Retrieved %d %s servers\n", len(servers), userChoice) + + // Add the active servers to a slice + var active []*Server + for i := range servers { + servers[i].URL = servers[i].Hostname + ".mullvad.net" + if servers[i].Type == strings.ToLower(userChoice) && servers[i].Active { + active = append(active, servers[i]) + } + } + + if len(active) < 1 { + cPrintln(ct.Red, "[!] No active VPN endpoints found!") + return + } + + cPrintln(ct.Green, "[*] There are %d active servers\n\n", len(active)) + + // launches ping waitgroups and stores results into our pointers to the substantiated Server types + testServers(active) + + // because active is a slice of pointers we are able to continue to use it throughout different functions + printResults(active) +} diff --git a/cmd/mullfast/servers.go b/cmd/mullfast/servers.go new file mode 100644 index 0000000..388b99b --- /dev/null +++ b/cmd/mullfast/servers.go @@ -0,0 +1,113 @@ +package main + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "os" + "sync" + "time" + + ct "github.com/daviddengcn/go-colortext" + "github.com/nuttapp/pinghist/ping" +) + +// URL is the base Mullvad API endpoint. +const URL = "https://api.mullvad.net/www/relays/all" + +// Server struct +type Server struct { + List []float64 + Last float64 + URL string + Hostname string + CountryCode string + CountryName string + CityName string + Active bool + Type string +} + +// Ping server to determine latency +func (s *Server) Ping() (float64, error) { + // for i := 0; i < 3; i++ { + cPrintln(ct.Cyan, "[*] Testing server:", s.URL) + + r, err := ping.Ping(s.URL) + if err != nil { + return -1, err + } + + if r.Time < 1 { + return -1, errors.New("presumably invalid sub 1ms latency") + } + + s.List = append(s.List, r.Time) + s.Last = r.Time + return s.Last, nil +} + +func getServers() []*Server { + var ( + response *http.Response + servers []*Server + err error + ) + // Make API call for server list + response, err = http.Get(URL) + + switch { + case err != nil: + cPrintln(ct.Red, "[!] Request Error\n", err.Error()) + os.Exit(1) + case response == nil: + cPrintln(ct.Red, "[!] Empty response from API\n") + os.Exit(1) + default: + } + + // Wait until the response finished + defer response.Body.Close() + + // Read the io return of the response + body, err := ioutil.ReadAll(response.Body) + if err != nil { + cPrintln(ct.Red, "[!] Error in response body\n", err.Error()) + os.Exit(1) + } + + // Unmarshal the JSON body return + jsonErr := json.Unmarshal(body, &servers) + if jsonErr != nil { + cPrintln(ct.Red, "[!] Json Error", jsonErr) + os.Exit(1) + } + return servers +} + +func testServers(active []*Server) { + // Test the valid servers. + var wg sync.WaitGroup + wg.Add(len(active) - 1) + + for _, server := range active { + time.Sleep(100 * time.Millisecond) + + go func(wg *sync.WaitGroup, server *Server) { + // note: you might want to carry on with all 3 tries even if it doesn't error + // this would give you more data to go off of + for i := 0; i < 3; i++ { + _, err := server.Ping() + if err != nil { + println(ct.Red, "[!] Error on ping", server.URL, err) + continue + } + break + } + wg.Done() + }(&wg, server) + } + + wg.Wait() +} diff --git a/cmd/mullfast/slice.go b/cmd/mullfast/slice.go new file mode 100644 index 0000000..21898a0 --- /dev/null +++ b/cmd/mullfast/slice.go @@ -0,0 +1,13 @@ +package main + +// SortedSlice list a slice of pointers to Server types. +type SortedSlice []*Server + +// Len returns the length of our slice. +func (s SortedSlice) Len() int { return len(s) } + +// Swap swaps two values placements within the slice. +func (s SortedSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less method +func (s SortedSlice) Less(i, j int) bool { return s[i].Last < s[j].Last } diff --git a/cmd/mullfast/ui.go b/cmd/mullfast/ui.go new file mode 100644 index 0000000..f939469 --- /dev/null +++ b/cmd/mullfast/ui.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/base64" + "fmt" + + ct "github.com/daviddengcn/go-colortext" + ui "github.com/manifoldco/promptui" +) + +// Banner contains our encoded banner. +const Banner = "ICAgKiAgICAgICAgICAgICAgICAgICgKICggIGAgICAgICAgICAgKCAgICggIClcICkgICAgICAgICAgICAgICkKIClcKSkoICAgICAoICAgKVwgIClcKCgpLyggICAgKSAgICAgICggLygKKChfKSgpXCAgICkpXCAoKF8pKChfKS8oXykpKCAvKCAgKCAgIClcKCkpCihfKCkoKF8pIC8oKF8pIF8gICBfIChfKSlffCkoXykpIClcIChfKSkvCnwgIFwvICB8KF8pKSggfCB8IHwgfHwgfF8gKChfKV8gKChfKXwgfF8KfCB8XC98IHx8IHx8IHx8IHwgfCB8fCBfX3wvIF9gIHwoXy08fCAgX3wKfF98ICB8X3wgXF8sX3x8X3wgfF98fF98ICBcX18sX3wvX18vIFxfX3wK" + +func banner() string { + // Decode banner + // this is a constant so we can safely assume no error here + b, _ := base64.RawStdEncoding.DecodeString(Banner) + return string(b) +} + +func cPrintln(color ct.Color, s string, i ...interface{}) { + ct.ChangeColor(color, false, ct.None, false) + defer ct.ResetColor() + fmt.Println(s, i) +} + +func printBanner() { + ct.ChangeColor(ct.Red, false, ct.None, false) + fmt.Printf("%s\n", banner()) + ct.ResetColor() +} + +func interactivePrompt() (promptResult string, err error) { + // Create prompt for VPN type + prompt := ui.Select{ + Label: "Select Mullvad VPN type", + Items: []string{"WireGuard", "OpenVPN", "Cancel"}, + } + _, promptResult, err = prompt.Run() + + switch { + case err != nil: + ct.ChangeColor(ct.Red, false, ct.None, false) + fmt.Println("\n[!] User prompt failed", err) + return + case promptResult == "Cancel": + ct.ChangeColor(ct.Red, false, ct.None, false) + fmt.Printf("[!] Server test canceled by user, exiting...\n") + return + default: + ct.ChangeColor(ct.Cyan, true, ct.None, false) + fmt.Printf("[*] You selected %q servers for testing\n\n", promptResult) + } + + ct.ChangeColor(ct.Cyan, false, ct.None, false) + fmt.Printf("[*] Retrieving list of %s servers from Mullvad...\n", promptResult) + return +}