Skip to content

Commit

Permalink
feat: integrate Globalping API for global network diagnostics
Browse files Browse the repository at this point in the history
  • Loading branch information
radulucut authored and mr-karan committed Sep 30, 2024
1 parent 44818dd commit 020d3de
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 37 deletions.
25 changes: 24 additions & 1 deletion cmd/doggo/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
"time"

"github.com/jsdelivr/globalping-cli/globalping"
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/koanf/v2"
"github.com/mr-karan/doggo/internal/app"
Expand Down Expand Up @@ -43,6 +44,20 @@ func main() {
logger := utils.InitLogger(cfg.debug)
app := initializeApp(logger, cfg)

if app.QueryFlags.From != "" {
res, err := app.GlobalpingMeasurement()
if err != nil {
logger.Error("Error fetching globalping measurement", "error", err)
os.Exit(2)
}
err = app.OutputGlobalping(res)
if err != nil {
logger.Error("Error outputting globalping measurement", "error", err)
os.Exit(2)
}
return
}

if cfg.reverseLookup {
app.ReverseLookup()
}
Expand Down Expand Up @@ -121,6 +136,9 @@ func setupFlags() *flag.FlagSet {
f.StringSliceP("nameserver", "n", []string{}, "Address of the nameserver to send packets to")
f.BoolP("reverse", "x", false, "Performs a DNS Lookup for an IPv4 or IPv6 address")

f.String("from", "", "Probe locations as a comma-separated list")
f.Int("limit", 1, "Limit the number of responses")

f.DurationP("timeout", "T", 5*time.Second, "Sets the timeout for a query")
f.Bool("search", true, "Use the search list provided in resolv.conf")
f.Int("ndots", -1, "Specify the ndots parameter")
Expand Down Expand Up @@ -162,7 +180,12 @@ func parseAndLoadFlags(f *flag.FlagSet) error {
}

func initializeApp(logger *slog.Logger, cfg *config) *app.App {
app := app.New(logger, buildVersion)
globlpingClient := globalping.NewClient(globalping.Config{
APIURL: "https://api.globalping.io/v1",
APIToken: os.Getenv("GLOBALPING_TOKEN"),
})

app := app.New(logger, globlpingClient, buildVersion)

if err := k.Unmarshal("", &app.QueryFlags); err != nil {
logger.Error("Error loading args", "error", err)
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/ameshkov/dnsstamps v1.0.3
github.com/fatih/color v1.17.0
github.com/go-chi/chi/v5 v5.1.0
github.com/jsdelivr/globalping-cli v1.3.1-0.20240717104136-2edb7127957b
github.com/knadh/koanf/parsers/toml v0.1.0
github.com/knadh/koanf/providers/env v0.1.0
github.com/knadh/koanf/providers/file v1.0.0
Expand All @@ -23,6 +24,7 @@ require (
github.com/AdguardTeam/golibs v0.24.1 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP
github.com/ameshkov/dnscrypt/v2 v2.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
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/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
Expand All @@ -26,6 +28,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg=
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/jsdelivr/globalping-cli v1.3.1-0.20240717104136-2edb7127957b h1:ZL7LfEaU+P2r6/Lxo99Qt6qw4Mb3BXt5UB4r+RoI6LA=
github.com/jsdelivr/globalping-cli v1.3.1-0.20240717104136-2edb7127957b/go.mod h1:2+lO4/xYSauKsf+pZ62bro1c4StxDO3cYcrLx4jsYmI=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=
Expand Down
10 changes: 9 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app
import (
"log/slog"

"github.com/jsdelivr/globalping-cli/globalping"
"github.com/miekg/dns"
"github.com/mr-karan/doggo/pkg/models"
"github.com/mr-karan/doggo/pkg/resolvers"
Expand All @@ -17,10 +18,16 @@ type App struct {
Resolvers []resolvers.Resolver
ResolverOpts resolvers.Options
Nameservers []models.Nameserver

globalping globalping.Client
}

// NewApp initializes an instance of App which holds app wide configuration.
func New(logger *slog.Logger, buildVersion string) App {
func New(
logger *slog.Logger,
globalping globalping.Client,
buildVersion string,
) App {
app := App{
Logger: logger,
Version: buildVersion,
Expand All @@ -31,6 +38,7 @@ func New(logger *slog.Logger, buildVersion string) App {
Nameservers: []string{},
},
Nameservers: []models.Nameserver{},
globalping: globalping,
}
return app
}
153 changes: 153 additions & 0 deletions internal/app/globalping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package app

import (
"errors"
"fmt"
"net"
"strings"
"time"

"github.com/fatih/color"
"github.com/jsdelivr/globalping-cli/globalping"
"github.com/olekukonko/tablewriter"
)

var (
ErrTargetIPVersionNotAllowed = errors.New("ipVersion is not allowed when target is not a domain")
ErrResolverIPVersionNotAllowed = errors.New("ipVersion is not allowed when resolver is not a domain")
)

func (app *App) GlobalpingMeasurement() (*globalping.Measurement, error) {
target := app.QueryFlags.QNames[0]
resolver := ""
if len(app.QueryFlags.Nameservers) > 0 {
resolver = app.QueryFlags.Nameservers[0]
}

if app.QueryFlags.UseIPv4 || app.QueryFlags.UseIPv6 {
if net.ParseIP(target) != nil {
return nil, ErrTargetIPVersionNotAllowed
}
if resolver != "" && net.ParseIP(resolver) != nil {
return nil, ErrResolverIPVersionNotAllowed
}
}

o := &globalping.MeasurementCreate{
Type: "dns",
Target: target,
Limit: app.QueryFlags.Limit,
Locations: parseGlobalpingLocations(app.QueryFlags.From),
Options: &globalping.MeasurementOptions{
// TODO: Add support for these flags.
// Protocol: opts.Protocol,
// Port: opts.Port,
},
}
if app.QueryFlags.UseIPv4 {
o.Options.IPVersion = globalping.IPVersion4
} else if app.QueryFlags.UseIPv6 {
o.Options.IPVersion = globalping.IPVersion6
}
if len(app.QueryFlags.Nameservers) > 0 {
o.Options.Resolver = app.QueryFlags.Nameservers[0]
}
if len(app.QueryFlags.QTypes) > 0 {
o.Options.Query = &globalping.QueryOptions{
Type: app.QueryFlags.QTypes[0],
}
}
res, err := app.globalping.CreateMeasurement(o)
if err != nil {
return nil, err
}
measurement, err := app.globalping.GetMeasurement(res.ID)
if err != nil {
return nil, err
}
for measurement.Status == globalping.StatusInProgress {
time.Sleep(500 * time.Millisecond)
measurement, err = app.globalping.GetMeasurement(res.ID)
if err != nil {
return nil, err
}
}

if measurement.Status != globalping.StatusFinished {
return nil, &globalping.MeasurementError{
Message: "measurement did not complete successfully",
}
}
return measurement, nil
}

// TODO: Add support for json output && short output
func (app *App) OutputGlobalping(m *globalping.Measurement) error {
// Disables colorized output if user specified.
if !app.QueryFlags.Color {
color.NoColor = true
}

table := tablewriter.NewWriter(color.Output)
header := []string{"Location", "Name", "Type", "Class", "TTL", "Address", "Nameserver"}

// Formatting options for the table.
table.SetHeader(header)
table.SetAutoWrapText(true)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true)

for i := range m.Results {
table.Append([]string{getGlobalPingLocationText(&m.Results[i]), "", "", "", "", "", ""})
answers, err := globalping.DecodeDNSAnswers(m.Results[i].Result.AnswersRaw)
if err != nil {
return err
}
resolver := m.Results[i].Result.Resolver
for _, ans := range answers {
typOut := getColoredType(ans.Type)
output := []string{"", TerminalColorGreen(ans.Name), typOut, ans.Class, fmt.Sprintf("%ds", ans.TTL), ans.Value, resolver}
table.Append(output)
}
}
table.Render()
return nil
}

func parseGlobalpingLocations(from string) []globalping.Locations {
if from == "" {
return []globalping.Locations{
{
Magic: "world",
},
}
}
fromArr := strings.Split(from, ",")
locations := make([]globalping.Locations, len(fromArr))
for i, v := range fromArr {
locations[i] = globalping.Locations{
Magic: strings.TrimSpace(v),
}
}
return locations
}

func getGlobalPingLocationText(m *globalping.ProbeMeasurement) string {
state := ""
if m.Probe.State != "" {
state = " (" + m.Probe.State + ")"
}
return m.Probe.City + state + ", " +
m.Probe.Country + ", " +
m.Probe.Continent + ", " +
m.Probe.Network + " " +
"(AS" + fmt.Sprint(m.Probe.ASN) + ")"
}
71 changes: 37 additions & 34 deletions internal/app/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import (
"github.com/olekukonko/tablewriter"
)

var (
TerminalColorGreen = color.New(color.FgGreen, color.Bold).SprintFunc()
TerminalColorBlue = color.New(color.FgBlue, color.Bold).SprintFunc()
TerminalColorYellow = color.New(color.FgYellow, color.Bold).SprintFunc()
TerminalColorCyan = color.New(color.FgCyan, color.Bold).SprintFunc()
TerminalColorRed = color.New(color.FgRed, color.Bold).SprintFunc()
TerminalColorMagenta = color.New(color.FgMagenta, color.Bold).SprintFunc()
)

func (app *App) outputJSON(rsp []resolvers.Response) {
jsonOutput := struct {
Responses []resolvers.Response `json:"responses"`
Expand All @@ -36,15 +45,6 @@ func (app *App) outputShort(rsp []resolvers.Response) {
}

func (app *App) outputTerminal(rsp []resolvers.Response) {
var (
green = color.New(color.FgGreen, color.Bold).SprintFunc()
blue = color.New(color.FgBlue, color.Bold).SprintFunc()
yellow = color.New(color.FgYellow, color.Bold).SprintFunc()
cyan = color.New(color.FgCyan, color.Bold).SprintFunc()
red = color.New(color.FgRed, color.Bold).SprintFunc()
magenta = color.New(color.FgMagenta, color.Bold).SprintFunc()
)

// Disables colorized output if user specified.
if !app.QueryFlags.Color {
color.NoColor = true
Expand Down Expand Up @@ -92,57 +92,60 @@ func (app *App) outputTerminal(rsp []resolvers.Response) {

for _, r := range rsp {
for _, ans := range r.Answers {
var typOut string
switch typ := ans.Type; typ {
case "A":
typOut = blue(ans.Type)
case "AAAA":
typOut = blue(ans.Type)
case "MX":
typOut = magenta(ans.Type)
case "NS":
typOut = cyan(ans.Type)
case "CNAME":
typOut = yellow(ans.Type)
case "TXT":
typOut = yellow(ans.Type)
case "SOA":
typOut = red(ans.Type)
default:
typOut = blue(ans.Type)
}
output := []string{green(ans.Name), typOut, ans.Class, ans.TTL, ans.Address, ans.Nameserver}
typOut := getColoredType(ans.Type)
output := []string{TerminalColorGreen(ans.Name), typOut, ans.Class, ans.TTL, ans.Address, ans.Nameserver}
// Print how long it took
if app.QueryFlags.DisplayTimeTaken {
output = append(output, ans.RTT)
}
if outputStatus {
output = append(output, red(ans.Status))
output = append(output, TerminalColorRed(ans.Status))
}
table.Append(output)
}
for _, auth := range r.Authorities {
var typOut string
switch typ := auth.Type; typ {
case "SOA":
typOut = red(auth.Type)
typOut = TerminalColorRed(auth.Type)
default:
typOut = blue(auth.Type)
typOut = TerminalColorBlue(auth.Type)
}
output := []string{green(auth.Name), typOut, auth.Class, auth.TTL, auth.MName, auth.Nameserver}
output := []string{TerminalColorGreen(auth.Name), typOut, auth.Class, auth.TTL, auth.MName, auth.Nameserver}
// Print how long it took
if app.QueryFlags.DisplayTimeTaken {
output = append(output, auth.RTT)
}
if outputStatus {
output = append(output, red(auth.Status))
output = append(output, TerminalColorRed(auth.Status))
}
table.Append(output)
}
}
table.Render()
}

func getColoredType(t string) string {
switch t {
case "A":
return TerminalColorBlue(t)
case "AAAA":
return TerminalColorBlue(t)
case "MX":
return TerminalColorMagenta(t)
case "NS":
return TerminalColorCyan(t)
case "CNAME":
return TerminalColorYellow(t)
case "TXT":
return TerminalColorYellow(t)
case "SOA":
return TerminalColorRed(t)
default:
return TerminalColorBlue(t)
}
}

// Output takes a list of `dns.Answers` and based
// on the output format specified displays the information.
func (app *App) Output(responses []resolvers.Response) {
Expand Down
Loading

0 comments on commit 020d3de

Please sign in to comment.