Refactor #1

Open
kayos wants to merge 1 commits from kayos/mullfast:master into master
5 changed files with 236 additions and 173 deletions

1
.gitignore vendored

@ -3,3 +3,4 @@
/.push-*
/.container-*
/.dockerfile-*
.idea

@ -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)
}

113
cmd/mullfast/servers.go Normal file

@ -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()
}

13
cmd/mullfast/slice.go Normal file

@ -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 }

58
cmd/mullfast/ui.go Normal file

@ -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
}