Skip to content

Commit

Permalink
Support dig format for resolver (#47)
Browse files Browse the repository at this point in the history
* support dig format for resolver

* fix comment
  • Loading branch information
didil committed Apr 11, 2023
1 parent 6294ca1 commit 126cf22
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 37 deletions.
10 changes: 7 additions & 3 deletions cmd/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ var dnsCmd = &cobra.Command{
Short: "Resolve a DNS record similarly to dig",
Long: `Performs DNS lookups and displays the answers that are returned from the name server(s) that were queried.
The default nameserver depends on the probe and is defined by the user's local settings or DHCP.
This command provides 2 different ways to provide the dns resolver:
Using the --resolver argument. For example:
dns jsdelivr.com from Berlin --resolver 1.1.1.1
Using the dig format @resolver. For example:
dns jsdelivr.com @1.1.1.1 from Berlin
Examples:
Examples:
# Resolve google.com from 2 probes in New York
dns google.com from New York --limit 2
Expand All @@ -35,7 +40,6 @@ Examples:
# Resolve jsdelivr.com from a probe in ASN 123 with json output
dns jsdelivr.com from 123 --json`,
Args: checkCommandFormat(),
RunE: func(cmd *cobra.Command, args []string) error {
// Create context

Expand All @@ -54,7 +58,7 @@ Examples:
Options: &model.MeasurementOptions{
Protocol: protocol,
Port: port,
Resolver: resolver,
Resolver: overrideOpt(ctx.Resolver, resolver),
Query: &model.QueryOptions{
Type: queryType,
},
Expand Down
9 changes: 7 additions & 2 deletions cmd/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ When the full url is supplied, the tool autoparses the scheme, host, port, domai
As an alternative that can be useful for scripting, the scheme, host, port, domain, path and query can be provided as separate command line flags. For example:
http jsdelivr.com --host www.jsdelivr.com --protocol https --port 443 --path "/package/npm/test" --query "nav=stats"
This command also provides 2 different ways to provide the dns resolver:
Using the --resolver argument. For example:
http jsdelivr.com from Berlin --resolver 1.1.1.1
Using the dig format @resolver. For example:
http jsdelivr.com @1.1.1.1 from Berlin
Examples:
# HTTP HEAD request to jsdelivr.com from 2 probes in New York (protocol, port and path are inferred from the URL)
http https://www.jsdelivr.com:443/package/npm/test?nav=stats from New York --limit 2
Expand All @@ -109,7 +115,6 @@ Examples:
# HTTP GET request google.com from a probe in ASN 123 with a dns resolver 1.1.1.1 and json output
http google.com from 123 --resolver 1.1.1.1 --json`,
Args: checkCommandFormat(),
RunE: httpCmdRun,
}

Expand Down Expand Up @@ -169,7 +174,7 @@ func buildHttpMeasurementRequest() (model.PostMeasurement, error) {
// TODO: Headers: headers,
Method: httpCmdOpts.Method,
},
Resolver: httpCmdOpts.Resolver,
Resolver: overrideOpt(ctx.Resolver, httpCmdOpts.Resolver),
}

return m, nil
Expand Down
1 change: 0 additions & 1 deletion cmd/mtr.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ Examples:
# MTR jsdelivr.com from a probe in ASN 123 with json output
mtr jsdelivr.com from 123 --json`,
Args: checkCommandFormat(),
RunE: func(cmd *cobra.Command, args []string) error {
// Create context
err := createContext(cmd.CalledAs(), args)
Expand Down
1 change: 0 additions & 1 deletion cmd/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ Examples:
# Ping jsdelivr.com from a probe in ASN 123 with json output
ping jsdelivr.com from 123 --json`,
Args: checkCommandFormat(),
RunE: func(cmd *cobra.Command, args []string) error {
// Create context
err := createContext(cmd.CalledAs(), args)
Expand Down
35 changes: 12 additions & 23 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package cmd

import (
"errors"
"os"
"strings"

"github.com/jsdelivr/globalping-cli/lib"
"github.com/jsdelivr/globalping-cli/model"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -49,40 +48,30 @@ func Execute() {

func init() {
// Global flags
rootCmd.PersistentFlags().StringVarP(&ctx.From, "from", "F", "", `Comma-separated list of location values to match against. For example the partial or full name of a continent, region (e.g eastern europe), country, US state, city or network (default "world").`)
rootCmd.PersistentFlags().StringVarP(&ctx.From, "from", "F", "world", `Comma-separated list of location values to match against. For example the partial or full name of a continent, region (e.g eastern europe), country, US state, city or network (default "world").`)
rootCmd.PersistentFlags().IntVarP(&ctx.Limit, "limit", "L", 1, "Limit the number of probes to use")
rootCmd.PersistentFlags().BoolVarP(&ctx.JsonOutput, "json", "J", false, "Output results in JSON format (default false)")
rootCmd.PersistentFlags().BoolVarP(&ctx.CI, "ci", "C", false, "Disable realtime terminal updates and color suitable for CI and scripting (default false)")
rootCmd.PersistentFlags().BoolVar(&ctx.Latency, "latency", false, "Output only the stats of a measurement (default false). Only applies to the dns, http and ping commands")
}

// checkCommandFormat checks if the command is in the correct format if using the from arg
func checkCommandFormat() cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) > 1 && args[1] != "from" {
return errors.New("invalid command format")
}
return nil
}
}

func createContext(cmd string, args []string) error {
ctx.Cmd = cmd // Get the command name

// Target
if len(args) == 0 {
return errors.New("provided target is empty")
// parse target query
targetQuery, err := lib.ParseTargetQuery(cmd, args)
if err != nil {
return err
}
ctx.Target = args[0]

// If no from arg is provided, use the default value
if len(args) == 1 && ctx.From == "" {
ctx.From = "world"
ctx.Target = targetQuery.Target

if targetQuery.From != "" {
ctx.From = targetQuery.From
}

// If from args are provided, use it
if len(args) > 1 && args[1] == "from" {
ctx.From = strings.TrimSpace(strings.Join(args[2:], " "))
if targetQuery.Resolver != "" {
ctx.Resolver = targetQuery.Resolver
}

// Check env for CI
Expand Down
4 changes: 2 additions & 2 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func testContextNoArg(t *testing.T) {
err := createContext("test", []string{"1.1.1.1"})
assert.Equal(t, "test", ctx.Cmd)
assert.Equal(t, "1.1.1.1", ctx.Target)
assert.Equal(t, "world", ctx.From)
assert.Equal(t, "", ctx.From)
assert.NoError(t, err)
}

Expand Down Expand Up @@ -86,7 +86,7 @@ func testContextCIEnv(t *testing.T) {
err := createContext("test", []string{"1.1.1.1"})
assert.Equal(t, "test", ctx.Cmd)
assert.Equal(t, "1.1.1.1", ctx.Target)
assert.Equal(t, "world", ctx.From)
assert.Equal(t, "", ctx.From)
assert.True(t, ctx.CI)
assert.NoError(t, err)
}
1 change: 0 additions & 1 deletion cmd/traceroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ Examples:
# Traceroute jsdelivr.com from a probe in ASN 123 with json output
traceroute jsdelivr.com from 123 --json`,
Args: checkCommandFormat(),
RunE: func(cmd *cobra.Command, args []string) error {
// Create context
err := createContext(cmd.CalledAs(), args)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/rivo/uniseg v0.4.4 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
71 changes: 71 additions & 0 deletions lib/target_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package lib

import (
"errors"
"fmt"
"strings"

"golang.org/x/exp/slices"
)

type TargetQuery struct {
Target string
From string
Resolver string
}

var commandsWithResolver = []string{
"dns",
"http",
}

func ParseTargetQuery(cmd string, args []string) (*TargetQuery, error) {
targetQuery := &TargetQuery{}
if len(args) == 0 {
return nil, errors.New("provided target is empty")
}

resolver, argsWithoutResolver := findAndRemoveResolver(args)
if resolver != "" {
// resolver was found
if !slices.Contains(commandsWithResolver, cmd) {
return nil, fmt.Errorf("command %s does not accept a resolver argument. @%s was provided", cmd, resolver)
}

targetQuery.Resolver = resolver
}

targetQuery.Target = argsWithoutResolver[0]

if len(argsWithoutResolver) > 1 {
if argsWithoutResolver[1] == "from" {
targetQuery.From = strings.TrimSpace(strings.Join(argsWithoutResolver[2:], " "))
} else {
return nil, errors.New("invalid command format")
}
}

return targetQuery, nil
}

func findAndRemoveResolver(args []string) (string, []string) {
var resolver string
resolverIndex := -1
for i := 0; i < len(args); i++ {
if len(args[i]) > 0 && args[i][0] == '@' {
resolver = args[i][1:]
resolverIndex = i
break
}
}

if resolverIndex == -1 {
// resolver was not found
return "", args
}

argsClone := slices.Clone(args)
argsWithoutResolver := slices.Delete(argsClone, resolverIndex, resolverIndex+1)

return resolver, argsWithoutResolver
}
91 changes: 91 additions & 0 deletions lib/target_query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package lib

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseTargetQuery_Simple(t *testing.T) {
cmd := "ping"
args := []string{"example.com"}

q, err := ParseTargetQuery(cmd, args)
assert.NoError(t, err)

assert.Equal(t, TargetQuery{Target: "example.com", From: ""}, *q)
}

func TestParseTargetQuery_SimpleWithResolver(t *testing.T) {
cmd := "dns"
args := []string{"example.com", "@1.1.1.1"}

q, err := ParseTargetQuery(cmd, args)
assert.NoError(t, err)

assert.Equal(t, TargetQuery{Target: "example.com", From: "", Resolver: "1.1.1.1"}, *q)
}

func TestParseTargetQuery_ResolverNotAllowed(t *testing.T) {
cmd := "ping"
args := []string{"example.com", "@1.1.1.1"}

_, err := ParseTargetQuery(cmd, args)
assert.ErrorContains(t, err, "does not accept a resolver argument")
}

func TestParseTargetQuery_TargetFromX(t *testing.T) {
cmd := "ping"
args := []string{"example.com", "from", "London"}

q, err := ParseTargetQuery(cmd, args)
assert.NoError(t, err)

assert.Equal(t, TargetQuery{Target: "example.com", From: "London"}, *q)
}

func TestParseTargetQuery_TargetFromXWithResolver(t *testing.T) {
cmd := "http"
args := []string{"example.com", "from", "London", "@1.1.1.1"}

q, err := ParseTargetQuery(cmd, args)
assert.NoError(t, err)

assert.Equal(t, TargetQuery{Target: "example.com", From: "London", Resolver: "1.1.1.1"}, *q)
}

func TestFindAndRemoveResolver_SimpleNoResolver(t *testing.T) {
args := []string{"example.com"}

resolver, argsWithoutResolver := findAndRemoveResolver(args)

assert.Equal(t, "", resolver)
assert.Equal(t, args, argsWithoutResolver)
}

func TestFindAndRemoveResolver_NoResolver(t *testing.T) {
args := []string{"example.com", "from", "London"}

resolver, argsWithoutResolver := findAndRemoveResolver(args)

assert.Equal(t, "", resolver)
assert.Equal(t, args, argsWithoutResolver)
}

func TestFindAndRemoveResolver_ResolverAndFrom(t *testing.T) {
args := []string{"example.com", "@1.1.1.1", "from", "London"}

resolver, argsWithoutResolver := findAndRemoveResolver(args)

assert.Equal(t, "1.1.1.1", resolver)
assert.Equal(t, []string{"example.com", "from", "London"}, argsWithoutResolver)
}

func TestFindAndRemoveResolver_ResolverOnly(t *testing.T) {
args := []string{"example.com", "@1.1.1.1"}

resolver, argsWithoutResolver := findAndRemoveResolver(args)

assert.Equal(t, "1.1.1.1", resolver)
assert.Equal(t, []string{"example.com"}, argsWithoutResolver)
}
9 changes: 5 additions & 4 deletions model/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package model

// Used in thc client TUI
type Context struct {
Cmd string
Target string
From string
Limit int
Cmd string
Target string
From string
Limit int
Resolver string
// JsonOutput is a flag that determines whether the output should be in JSON format.
JsonOutput bool
// Latency is a flag that outputs only stats of a measurement
Expand Down

0 comments on commit 126cf22

Please sign in to comment.