From 99dae50aa54b2854014eeb8c3be4d82556ed202d Mon Sep 17 00:00:00 2001 From: krashanoff Date: Wed, 1 Jul 2020 21:20:50 -0700 Subject: [PATCH 01/17] Rudimentary implementation of --block-hostname --- cmd/options.go | 19 ++++++++ js/runner.go | 9 ++-- lib/netext/dialer.go | 22 +++++++-- lib/netext/httpext/error_codes.go | 10 ++-- lib/options.go | 78 +++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 10 deletions(-) diff --git a/cmd/options.go b/cmd/options.go index decdfe453d2..b2d048a8663 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -22,6 +22,7 @@ package cmd import ( "fmt" + "regexp" "strings" "time" @@ -63,6 +64,7 @@ func optionFlagSet() *pflag.FlagSet { flags.Duration("min-iteration-duration", 0, "minimum amount of time k6 will take executing a single iteration") flags.BoolP("throw", "w", false, "throw warnings (like failed http requests) as errors") flags.StringSlice("blacklist-ip", nil, "blacklist an `ip range` from being called") + flags.StringSlice("block-hostname", nil, "block a `hostname`, with or without prepended wildcard, from being called") // The comment about system-tags also applies for summary-trend-stats. The default values // are set in applyDefault(). @@ -151,6 +153,23 @@ func getOptions(flags *pflag.FlagSet) (lib.Options, error) { opts.BlacklistIPs = append(opts.BlacklistIPs, net) } + blockedHostnameStrings, err := flags.GetStringSlice("block-hostname") + if err != nil { + return opts, err + } + r, compErr := regexp.Compile("^\\*?(\\pL|[0-9\\.])*") + if compErr != nil { + return opts, errors.Wrap(compErr, "block-hostname") + } + opts.BlockedHostnames = lib.NewHostnameTrie() + for _, s := range blockedHostnameStrings { + if len(r.FindString(s)) != len(s) { + return opts, errors.New("block-hostname: invalid hostname pattern %s", s) + } + + opts.BlockedHostnames.Insert(s) + } + if flags.Changed("summary-trend-stats") { trendStats, errSts := flags.GetStringSlice("summary-trend-stats") if errSts != nil { diff --git a/js/runner.go b/js/runner.go index 03da8c07bd8..e2a2f14edf3 100644 --- a/js/runner.go +++ b/js/runner.go @@ -154,10 +154,11 @@ func (r *Runner) newVU(samplesOut chan<- stats.SampleContainer) (*VU, error) { } dialer := &netext.Dialer{ - Dialer: r.BaseDialer, - Resolver: r.Resolver, - Blacklist: r.Bundle.Options.BlacklistIPs, - Hosts: r.Bundle.Options.Hosts, + Dialer: r.BaseDialer, + Resolver: r.Resolver, + Blacklist: r.Bundle.Options.BlacklistIPs, + BlockedHostnames: r.Bundle.Options.BlockedHostnames, + Hosts: r.Bundle.Options.Hosts, } tlsConfig := &tls.Config{ InsecureSkipVerify: r.Bundle.Options.InsecureSkipTLSVerify.Bool, diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go index cecc4e735a7..d3cb07ca5d5 100644 --- a/lib/netext/dialer.go +++ b/lib/netext/dialer.go @@ -40,9 +40,10 @@ import ( type Dialer struct { net.Dialer - Resolver *dnscache.Resolver - Blacklist []*lib.IPNet - Hosts map[string]net.IP + Resolver *dnscache.Resolver + Blacklist []*lib.IPNet + BlockedHostnames *lib.HostnameTrie + Hosts map[string]net.IP BytesRead int64 BytesWritten int64 @@ -66,11 +67,26 @@ func (b BlackListedIPError) Error() string { return fmt.Sprintf("IP (%s) is in a blacklisted range (%s)", b.ip, b.net) } +// BlockedHostError is returned when a given hostname is blocked +type BlockedHostError struct { + hostname string + match string +} + +func (b BlockedHostError) Error() string { + return fmt.Sprintf("hostname (%s) is in a blocked pattern (%s)", b.hostname, b.match) +} + // DialContext wraps the net.Dialer.DialContext and handles the k6 specifics func (d *Dialer) DialContext(ctx context.Context, proto, addr string) (net.Conn, error) { delimiter := strings.LastIndex(addr, ":") host := addr[:delimiter] + // check if host is blocked. + if blocked, match := d.BlockedHostnames.Contains(host); blocked { + return nil, BlockedHostError{hostname: host, match: match} + } + // lookup for domain defined in Hosts option before trying to resolve DNS. ip, ok := d.Hosts[host] if !ok { diff --git a/lib/netext/httpext/error_codes.go b/lib/netext/httpext/error_codes.go index b240d6841c2..6cb204b7d48 100644 --- a/lib/netext/httpext/error_codes.go +++ b/lib/netext/httpext/error_codes.go @@ -46,9 +46,10 @@ const ( defaultErrorCode errCode = 1000 defaultNetNonTCPErrorCode errCode = 1010 // DNS errors - defaultDNSErrorCode errCode = 1100 - dnsNoSuchHostErrorCode errCode = 1101 - blackListedIPErrorCode errCode = 1110 + defaultDNSErrorCode errCode = 1100 + dnsNoSuchHostErrorCode errCode = 1101 + blackListedIPErrorCode errCode = 1110 + blockedHostnameErrorCode errCode = 1111 // tcp errors defaultTCPErrorCode errCode = 1200 tcpBrokenPipeErrorCode errCode = 1201 @@ -90,6 +91,7 @@ const ( netUnknownErrnoErrorCodeMsg = "%s: unknown errno `%d` on %s with message `%s`" dnsNoSuchHostErrorCodeMsg = "lookup: no such host" blackListedIPErrorCodeMsg = "ip is blacklisted" + blockedHostnameErrorMsg = "hostname is blocked" http2GoAwayErrorCodeMsg = "http2: received GoAway with http2 ErrCode %s" http2StreamErrorCodeMsg = "http2: stream error with http2 ErrCode %s" http2ConnectionErrorCodeMsg = "http2: connection error with http2 ErrCode %s" @@ -118,6 +120,8 @@ func errorCodeForError(err error) (errCode, string) { } case netext.BlackListedIPError: return blackListedIPErrorCode, blackListedIPErrorCodeMsg + case netext.BlockedHostError: + return blockedHostnameErrorCode, blockedHostnameErrorMsg case *http2.GoAwayError: return unknownHTTP2GoAwayErrorCode + http2ErrCodeOffset(e.ErrCode), fmt.Sprintf(http2GoAwayErrorCodeMsg, e.ErrCode) diff --git a/lib/options.go b/lib/options.go index f94341a3ff3..1129fd4e7d8 100644 --- a/lib/options.go +++ b/lib/options.go @@ -187,6 +187,78 @@ func ParseCIDR(s string) (*IPNet, error) { return &parsedIPNet, nil } +// HostnameTrie is a tree-structured list of hostname matches with support +// for wildcards exclusively at the start of the pattern. Items may only +// be inserted and searched. +// Internationalized hostnames are valid. +type HostnameTrie struct { + r rune + children []*HostnameTrie + terminal bool // end of a valid match +} + +// NewHostnameTrie returns a valid head node for a HostnameTrie. +func NewHostnameTrie() *HostnameTrie { + return &HostnameTrie{-1, make([]*HostnameTrie, 0), false} +} + +// Insert a string into the given HostnameTrie. +func (t *HostnameTrie) Insert(s string) { + if len(s) == 0 { + return + } + + rStr := []rune(s) // need to iterate by runes for intl' names + last := len(rStr) - 1 + for _, c := range t.children { + if c.r == rStr[last] { + c.Insert(string(rStr[:last])) + return + } + } + + n := &HostnameTrie{rStr[last], make([]*HostnameTrie, 0), len(rStr) == 1} + t.children = append(t.children, n) + n.Insert(string(rStr[:last])) +} + +func (t *HostnameTrie) childContains(s string, match string) (bool, string) { + if len(s) == 0 { + return false, "" + } + + rStr := []rune(s) + last := len(rStr) - 1 + + switch { + case t.r == '*': + return true, string(t.r) + match + case t.r != rStr[last]: + return false, "" + case len(s) == 1: + return t.terminal, string(t.r) + match + default: + for _, c := range t.children { + if b, m := c.childContains(string(rStr[:last]), string(rStr[:last])+match); b { + return b, m + } + } + } + + return false, "" +} + +// Contains returns whether s matches a pattern in the HostnameTrie +// along with the matching pattern, if one was found. +func (t *HostnameTrie) Contains(s string) (bool, string) { + for _, c := range t.children { + if b, m := c.childContains(s, ""); b { + return b, m + } + } + return false, "" +} + type Options struct { // Should the test start in a paused state? Paused null.Bool `json:"paused" envconfig:"K6_PAUSED"` @@ -242,6 +314,9 @@ type Options struct { // Blacklist IP ranges that tests may not contact. Mainly useful in hosted setups. BlacklistIPs []*IPNet `json:"blacklistIPs" envconfig:"K6_BLACKLIST_IPS"` + // Block hostnames that tests may not contact. + BlockedHostnames *HostnameTrie `json:"blockHostnames" envconfig:"K6_BLOCK_HOSTNAMES"` + // Hosts overrides dns entries for given hosts Hosts map[string]net.IP `json:"hosts" envconfig:"K6_HOSTS"` @@ -389,6 +464,9 @@ func (o Options) Apply(opts Options) Options { if opts.BlacklistIPs != nil { o.BlacklistIPs = opts.BlacklistIPs } + if opts.BlockedHostnames != nil { + o.BlockedHostnames = opts.BlockedHostnames + } if opts.Hosts != nil { o.Hosts = opts.Hosts } From 8c6f43390d7a6079c3dd1e38b79e6e9d63d94d65 Mon Sep 17 00:00:00 2001 From: krashanoff Date: Thu, 2 Jul 2020 20:18:08 -0700 Subject: [PATCH 02/17] Rework HostnameTrie interface --- cmd/options.go | 13 ++------ lib/netext/dialer.go | 6 ++-- lib/options.go | 70 +++++++++++++++++++++++++++++--------------- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/cmd/options.go b/cmd/options.go index b2d048a8663..33f62e6eb2d 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -22,7 +22,6 @@ package cmd import ( "fmt" - "regexp" "strings" "time" @@ -157,17 +156,11 @@ func getOptions(flags *pflag.FlagSet) (lib.Options, error) { if err != nil { return opts, err } - r, compErr := regexp.Compile("^\\*?(\\pL|[0-9\\.])*") - if compErr != nil { - return opts, errors.Wrap(compErr, "block-hostname") - } - opts.BlockedHostnames = lib.NewHostnameTrie() + opts.BlockedHostnames = &lib.HostnameTrie{} for _, s := range blockedHostnameStrings { - if len(r.FindString(s)) != len(s) { - return opts, errors.New("block-hostname: invalid hostname pattern %s", s) + if insertErr := opts.BlockedHostnames.Insert(s); insertErr != nil { + return opts, errors.Wrap(insertErr, "block-hostname") } - - opts.BlockedHostnames.Insert(s) } if flags.Changed("summary-trend-stats") { diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go index d3cb07ca5d5..4617037faa3 100644 --- a/lib/netext/dialer.go +++ b/lib/netext/dialer.go @@ -83,8 +83,10 @@ func (d *Dialer) DialContext(ctx context.Context, proto, addr string) (net.Conn, host := addr[:delimiter] // check if host is blocked. - if blocked, match := d.BlockedHostnames.Contains(host); blocked { - return nil, BlockedHostError{hostname: host, match: match} + if d.BlockedHostnames != nil { + if blocked, match := d.BlockedHostnames.Contains(host); blocked { + return nil, BlockedHostError{hostname: host, match: match} + } } // lookup for domain defined in Hosts option before trying to resolve DNS. diff --git a/lib/options.go b/lib/options.go index 1129fd4e7d8..b087d8c48d6 100644 --- a/lib/options.go +++ b/lib/options.go @@ -26,6 +26,8 @@ import ( "fmt" "net" "reflect" + "regexp" + "strings" "github.com/loadimpact/k6/lib/scheduler" "github.com/loadimpact/k6/lib/types" @@ -189,37 +191,70 @@ func ParseCIDR(s string) (*IPNet, error) { // HostnameTrie is a tree-structured list of hostname matches with support // for wildcards exclusively at the start of the pattern. Items may only -// be inserted and searched. -// Internationalized hostnames are valid. +// be inserted and searched. Internationalized hostnames are valid. type HostnameTrie struct { r rune children []*HostnameTrie terminal bool // end of a valid match } -// NewHostnameTrie returns a valid head node for a HostnameTrie. -func NewHostnameTrie() *HostnameTrie { - return &HostnameTrie{-1, make([]*HostnameTrie, 0), false} +// describes a valid hostname pattern to block by. Global var to avoid +// compilation penalty each call to ValidHostname. +var validHostnamePattern *regexp.Regexp = regexp.MustCompile("^\\*?(\\pL|[0-9\\.])*") + +// ValidHostname returns whether the provided hostname pattern +// has an optional wildcard at the start, and is composed entirely +// of letters, numbers, or '.'s. +func ValidHostname(s string) error { + if len(validHostnamePattern.FindString(s)) != len(s) { + return fmt.Errorf("block-hostname: invalid hostname pattern %s", s) + } + return nil +} + +// UnmarshalText forms a HostnameTrie from the given comma-delimited +// hostname patterns list. +func (t *HostnameTrie) UnmarshalText(b []byte) error { + for _, s := range strings.Split(string(b), ",") { + if err := t.Insert(s); err != nil { + return err + } + } + return nil } // Insert a string into the given HostnameTrie. -func (t *HostnameTrie) Insert(s string) { +func (t *HostnameTrie) Insert(s string) error { if len(s) == 0 { - return + return nil + } + + if err := ValidHostname(s); err != nil { + return err } rStr := []rune(s) // need to iterate by runes for intl' names last := len(rStr) - 1 for _, c := range t.children { if c.r == rStr[last] { - c.Insert(string(rStr[:last])) - return + return c.Insert(string(rStr[:last])) } } n := &HostnameTrie{rStr[last], make([]*HostnameTrie, 0), len(rStr) == 1} t.children = append(t.children, n) - n.Insert(string(rStr[:last])) + return n.Insert(string(rStr[:last])) +} + +// Contains returns whether s matches a pattern in the HostnameTrie +// along with the matching pattern, if one was found. +func (t *HostnameTrie) Contains(s string) (bool, string) { + for _, c := range t.children { + if b, m := c.childContains(s, ""); b { + return b, m + } + } + return false, "" } func (t *HostnameTrie) childContains(s string, match string) (bool, string) { @@ -231,7 +266,7 @@ func (t *HostnameTrie) childContains(s string, match string) (bool, string) { last := len(rStr) - 1 switch { - case t.r == '*': + case t.r == '*': // wildcard encounters validate the string return true, string(t.r) + match case t.r != rStr[last]: return false, "" @@ -239,7 +274,7 @@ func (t *HostnameTrie) childContains(s string, match string) (bool, string) { return t.terminal, string(t.r) + match default: for _, c := range t.children { - if b, m := c.childContains(string(rStr[:last]), string(rStr[:last])+match); b { + if b, m := c.childContains(string(rStr[:last]), string(t.r)+match); b { return b, m } } @@ -248,17 +283,6 @@ func (t *HostnameTrie) childContains(s string, match string) (bool, string) { return false, "" } -// Contains returns whether s matches a pattern in the HostnameTrie -// along with the matching pattern, if one was found. -func (t *HostnameTrie) Contains(s string) (bool, string) { - for _, c := range t.children { - if b, m := c.childContains(s, ""); b { - return b, m - } - } - return false, "" -} - type Options struct { // Should the test start in a paused state? Paused null.Bool `json:"paused" envconfig:"K6_PAUSED"` From 9d761f2aace7c4ab907bc644c873cc21d3c12ba9 Mon Sep 17 00:00:00 2001 From: krashanoff Date: Thu, 2 Jul 2020 21:07:43 -0700 Subject: [PATCH 03/17] HostnameTrie unmarshal functions --- lib/options.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/options.go b/lib/options.go index b087d8c48d6..bad5dac8bd6 100644 --- a/lib/options.go +++ b/lib/options.go @@ -27,7 +27,6 @@ import ( "net" "reflect" "regexp" - "strings" "github.com/loadimpact/k6/lib/scheduler" "github.com/loadimpact/k6/lib/types" @@ -212,17 +211,26 @@ func ValidHostname(s string) error { return nil } -// UnmarshalText forms a HostnameTrie from the given comma-delimited -// hostname patterns list. -func (t *HostnameTrie) UnmarshalText(b []byte) error { - for _, s := range strings.Split(string(b), ",") { - if err := t.Insert(s); err != nil { - return err +// UnmarshalJSON forms a HostnameTrie from the provided hostname pattern +// list. +func (t *HostnameTrie) UnmarshalJSON(data []byte) error { + m := make([]string, 0) + if err := json.Unmarshal(data, &m); err != nil { + return err + } + for _, h := range m { + if insertErr := t.Insert(h); insertErr != nil { + return insertErr } } return nil } +// UnmarshalText forms a HostnameTrie from a given hostname pattern. +func (t *HostnameTrie) UnmarshalText(b []byte) error { + return t.Insert(string(b)) +} + // Insert a string into the given HostnameTrie. func (t *HostnameTrie) Insert(s string) error { if len(s) == 0 { From 9dee57f0630db4c7e1f72fe0632033cc7260b76d Mon Sep 17 00:00:00 2001 From: krashanoff Date: Thu, 2 Jul 2020 22:23:35 -0700 Subject: [PATCH 04/17] block-hostname tests and error messages --- cmd/options.go | 2 +- js/runner_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++ lib/options.go | 4 +-- lib/options_test.go | 11 +++++++ 4 files changed, 89 insertions(+), 3 deletions(-) diff --git a/cmd/options.go b/cmd/options.go index 33f62e6eb2d..480bcfd2346 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -63,7 +63,7 @@ func optionFlagSet() *pflag.FlagSet { flags.Duration("min-iteration-duration", 0, "minimum amount of time k6 will take executing a single iteration") flags.BoolP("throw", "w", false, "throw warnings (like failed http requests) as errors") flags.StringSlice("blacklist-ip", nil, "blacklist an `ip range` from being called") - flags.StringSlice("block-hostname", nil, "block a `hostname`, with or without prepended wildcard, from being called") + flags.StringSlice("block-hostname", nil, "block a hostname `pattern`, with optional leading wildcard, from being called") // The comment about system-tags also applies for summary-trend-stats. The default values // are set in applyDefault(). diff --git a/js/runner_test.go b/js/runner_test.go index 13f2c3a0eda..a3e58d609dd 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -818,6 +818,81 @@ func TestVUIntegrationBlacklistScript(t *testing.T) { } } +func TestVUIntegrationBlockHostnamesOption(t *testing.T) { + r1, err := getSimpleRunner("/script.js", ` + import http from "k6/http"; + export default function() { http.get("https://k6.io/"); } + `) + if !assert.NoError(t, err) { + return + } + + hostnames := lib.HostnameTrie{} + if err := hostnames.Insert("*.io"); !assert.NoError(t, err) { + return + } + require.NoError(t, r1.SetOptions(lib.Options{ + Throw: null.BoolFrom(true), + BlockedHostnames: &hostnames, + })) + + r2, err := NewFromArchive(r1.MakeArchive(), lib.RuntimeOptions{}) + if !assert.NoError(t, err) { + return + } + + runners := map[string]*Runner{"Source": r1, "Archive": r2} + + for name, r := range runners { + r := r + t.Run(name, func(t *testing.T) { + vu, err := r.NewVU(make(chan stats.SampleContainer, 100)) + if !assert.NoError(t, err) { + return + } + err = vu.RunOnce(context.Background()) + require.Error(t, err) + assert.Contains(t, err.Error(), "hostname (k6.io) is in a blocked pattern (*.io)") + }) + } +} + +func TestVUIntegrationBlockHostnamesScript(t *testing.T) { + r1, err := getSimpleRunner("/script.js", ` + import http from "k6/http"; + + export let options = { + throw: true, + blockHostnames: ["*.io"], + }; + + export default function() { http.get("https://k6.io/"); } + `) + if !assert.NoError(t, err) { + return + } + + r2, err := NewFromArchive(r1.MakeArchive(), lib.RuntimeOptions{}) + if !assert.NoError(t, err) { + return + } + + runners := map[string]*Runner{"Source": r1, "Archive": r2} + + for name, r := range runners { + r := r + t.Run(name, func(t *testing.T) { + vu, err := r.NewVU(make(chan stats.SampleContainer, 100)) + if !assert.NoError(t, err) { + return + } + err = vu.RunOnce(context.Background()) + require.Error(t, err) + assert.Contains(t, err.Error(), "hostname (k6.io) is in a blocked pattern (*.io)") + }) + } +} + func TestVUIntegrationHosts(t *testing.T) { tb := httpmultibin.NewHTTPMultiBin(t) defer tb.Cleanup() diff --git a/lib/options.go b/lib/options.go index bad5dac8bd6..0fa187fcfdf 100644 --- a/lib/options.go +++ b/lib/options.go @@ -206,7 +206,7 @@ var validHostnamePattern *regexp.Regexp = regexp.MustCompile("^\\*?(\\pL|[0-9\\. // of letters, numbers, or '.'s. func ValidHostname(s string) error { if len(validHostnamePattern.FindString(s)) != len(s) { - return fmt.Errorf("block-hostname: invalid hostname pattern %s", s) + return fmt.Errorf("invalid hostname pattern %s", s) } return nil } @@ -249,7 +249,7 @@ func (t *HostnameTrie) Insert(s string) error { } } - n := &HostnameTrie{rStr[last], make([]*HostnameTrie, 0), len(rStr) == 1} + n := &HostnameTrie{rStr[last], nil, len(rStr) == 1} t.children = append(t.children, n) return n.Insert(string(rStr[:last])) } diff --git a/lib/options_test.go b/lib/options_test.go index c7d86928636..99c27137e73 100644 --- a/lib/options_test.go +++ b/lib/options_test.go @@ -318,6 +318,17 @@ func TestOptions(t *testing.T) { assert.Equal(t, net.IPv4zero, opts.BlacklistIPs[0].IP) assert.Equal(t, net.CIDRMask(1, 1), opts.BlacklistIPs[0].Mask) }) + t.Run("BlockedHostnames", func(t *testing.T) { + hostnames := HostnameTrie{} + hostnames.Insert("*") + opts := Options{}.Apply(Options{ + BlockedHostnames: &hostnames, + }) + assert.NotNil(t, opts.BlockedHostnames) + assert.NotEmpty(t, opts.BlockedHostnames) + matches, _ := opts.BlockedHostnames.Contains("loadimpact.com") + assert.True(t, matches) + }) t.Run("Hosts", func(t *testing.T) { opts := Options{}.Apply(Options{Hosts: map[string]net.IP{ From 9b591002c49d7be8effad561fd59a71c06e94124 Mon Sep 17 00:00:00 2001 From: krashanoff Date: Thu, 2 Jul 2020 23:31:05 -0700 Subject: [PATCH 05/17] Fix K6_BLOCK_HOSTNAMES val getting overwritten. --- cmd/options.go | 4 +++- lib/options.go | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cmd/options.go b/cmd/options.go index 480bcfd2346..61921b6bee4 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -156,7 +156,9 @@ func getOptions(flags *pflag.FlagSet) (lib.Options, error) { if err != nil { return opts, err } - opts.BlockedHostnames = &lib.HostnameTrie{} + if len(blockedHostnameStrings) > 0 { + opts.BlockedHostnames = &lib.HostnameTrie{} + } for _, s := range blockedHostnameStrings { if insertErr := opts.BlockedHostnames.Insert(s); insertErr != nil { return opts, errors.Wrap(insertErr, "block-hostname") diff --git a/lib/options.go b/lib/options.go index 0fa187fcfdf..09e45556a4b 100644 --- a/lib/options.go +++ b/lib/options.go @@ -27,6 +27,7 @@ import ( "net" "reflect" "regexp" + "strings" "github.com/loadimpact/k6/lib/scheduler" "github.com/loadimpact/k6/lib/types" @@ -226,9 +227,15 @@ func (t *HostnameTrie) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalText forms a HostnameTrie from a given hostname pattern. +// UnmarshalText forms a HostnameTrie from a comma-delimited list +// of hostname patterns. func (t *HostnameTrie) UnmarshalText(b []byte) error { - return t.Insert(string(b)) + for _, s := range strings.Split(string(b), ",") { + if err := t.Insert(s); err != nil { + return err + } + } + return nil } // Insert a string into the given HostnameTrie. From 0e963443baf8cade0c27d88a45d5545e4e6d2351 Mon Sep 17 00:00:00 2001 From: krashanoff Date: Fri, 3 Jul 2020 00:33:32 -0700 Subject: [PATCH 06/17] Remove unnecessary export. Clarify comments. Address golangci-lint warnings. Closes #972 --- js/runner_test.go | 2 +- lib/netext/dialer.go | 1 - lib/options.go | 29 +++++++++++++++-------------- lib/options_test.go | 3 ++- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/js/runner_test.go b/js/runner_test.go index a3e58d609dd..a62f683cd23 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -828,7 +828,7 @@ func TestVUIntegrationBlockHostnamesOption(t *testing.T) { } hostnames := lib.HostnameTrie{} - if err := hostnames.Insert("*.io"); !assert.NoError(t, err) { + if insertErr := hostnames.Insert("*.io"); !assert.NoError(t, insertErr) { return } require.NoError(t, r1.SetOptions(lib.Options{ diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go index 4617037faa3..f0892d76193 100644 --- a/lib/netext/dialer.go +++ b/lib/netext/dialer.go @@ -82,7 +82,6 @@ func (d *Dialer) DialContext(ctx context.Context, proto, addr string) (net.Conn, delimiter := strings.LastIndex(addr, ":") host := addr[:delimiter] - // check if host is blocked. if d.BlockedHostnames != nil { if blocked, match := d.BlockedHostnames.Contains(host); blocked { return nil, BlockedHostError{hostname: host, match: match} diff --git a/lib/options.go b/lib/options.go index 09e45556a4b..617c9505957 100644 --- a/lib/options.go +++ b/lib/options.go @@ -193,21 +193,20 @@ func ParseCIDR(s string) (*IPNet, error) { // for wildcards exclusively at the start of the pattern. Items may only // be inserted and searched. Internationalized hostnames are valid. type HostnameTrie struct { - r rune children []*HostnameTrie + r rune terminal bool // end of a valid match } -// describes a valid hostname pattern to block by. Global var to avoid -// compilation penalty each call to ValidHostname. -var validHostnamePattern *regexp.Regexp = regexp.MustCompile("^\\*?(\\pL|[0-9\\.])*") +// Regex description of hostname pattern to enforce blocks by. Global var +// to avoid compilation penalty at runtime. +// Matches against strings composed entirely of letters, numbers, or '.'s +// with an optional wildcard at the start. +var legalHostnamePattern *regexp.Regexp = regexp.MustCompile("^\\*?(\\pL|[0-9\\.])*") -// ValidHostname returns whether the provided hostname pattern -// has an optional wildcard at the start, and is composed entirely -// of letters, numbers, or '.'s. -func ValidHostname(s string) error { - if len(validHostnamePattern.FindString(s)) != len(s) { - return fmt.Errorf("invalid hostname pattern %s", s) +func legalHostname(s string) error { + if len(legalHostnamePattern.FindString(s)) != len(s) { + return errors.Errorf("invalid hostname pattern %s", s) } return nil } @@ -238,13 +237,14 @@ func (t *HostnameTrie) UnmarshalText(b []byte) error { return nil } -// Insert a string into the given HostnameTrie. +// Insert a hostname pattern into the given HostnameTrie. Returns an error +// if hostname pattern is illegal. func (t *HostnameTrie) Insert(s string) error { if len(s) == 0 { return nil } - if err := ValidHostname(s); err != nil { + if err := legalHostname(s); err != nil { return err } @@ -256,7 +256,7 @@ func (t *HostnameTrie) Insert(s string) error { } } - n := &HostnameTrie{rStr[last], nil, len(rStr) == 1} + n := &HostnameTrie{nil, rStr[last], len(rStr) == 1} t.children = append(t.children, n) return n.Insert(string(rStr[:last])) } @@ -272,6 +272,7 @@ func (t *HostnameTrie) Contains(s string) (bool, string) { return false, "" } +// recursively traverse HostnameTrie children searching for a match. func (t *HostnameTrie) childContains(s string, match string) (bool, string) { if len(s) == 0 { return false, "" @@ -353,7 +354,7 @@ type Options struct { // Blacklist IP ranges that tests may not contact. Mainly useful in hosted setups. BlacklistIPs []*IPNet `json:"blacklistIPs" envconfig:"K6_BLACKLIST_IPS"` - // Block hostnames that tests may not contact. + // Block hostname patterns that tests may not contact. BlockedHostnames *HostnameTrie `json:"blockHostnames" envconfig:"K6_BLOCK_HOSTNAMES"` // Hosts overrides dns entries for given hosts diff --git a/lib/options_test.go b/lib/options_test.go index 99c27137e73..765fa63b162 100644 --- a/lib/options_test.go +++ b/lib/options_test.go @@ -320,7 +320,8 @@ func TestOptions(t *testing.T) { }) t.Run("BlockedHostnames", func(t *testing.T) { hostnames := HostnameTrie{} - hostnames.Insert("*") + err := hostnames.Insert("*") + assert.Nil(t, err) opts := Options{}.Apply(Options{ BlockedHostnames: &hostnames, }) From a86dd3e0bc7df5919f60243c5179624dd52c9c1f Mon Sep 17 00:00:00 2001 From: krashanoff Date: Sun, 12 Jul 2020 19:03:04 -0700 Subject: [PATCH 07/17] More tests for BlockedHostnames. Don't lint pattern regex. --- js/runner_test.go | 12 +++--------- lib/options.go | 3 ++- lib/options_test.go | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/js/runner_test.go b/js/runner_test.go index a62f683cd23..f4f9f261719 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -823,14 +823,10 @@ func TestVUIntegrationBlockHostnamesOption(t *testing.T) { import http from "k6/http"; export default function() { http.get("https://k6.io/"); } `) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) hostnames := lib.HostnameTrie{} - if insertErr := hostnames.Insert("*.io"); !assert.NoError(t, insertErr) { - return - } + require.NoError(t, hostnames.Insert("*.io")) require.NoError(t, r1.SetOptions(lib.Options{ Throw: null.BoolFrom(true), BlockedHostnames: &hostnames, @@ -847,9 +843,7 @@ func TestVUIntegrationBlockHostnamesOption(t *testing.T) { r := r t.Run(name, func(t *testing.T) { vu, err := r.NewVU(make(chan stats.SampleContainer, 100)) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) err = vu.RunOnce(context.Background()) require.Error(t, err) assert.Contains(t, err.Error(), "hostname (k6.io) is in a blocked pattern (*.io)") diff --git a/lib/options.go b/lib/options.go index 617c9505957..1026731feff 100644 --- a/lib/options.go +++ b/lib/options.go @@ -202,7 +202,8 @@ type HostnameTrie struct { // to avoid compilation penalty at runtime. // Matches against strings composed entirely of letters, numbers, or '.'s // with an optional wildcard at the start. -var legalHostnamePattern *regexp.Regexp = regexp.MustCompile("^\\*?(\\pL|[0-9\\.])*") +//nolint:gochecknoglobals +var legalHostnamePattern *regexp.Regexp = regexp.MustCompile(`^\*?(\pL|[0-9\.])*`) func legalHostname(s string) error { if len(legalHostnamePattern.FindString(s)) != len(s) { diff --git a/lib/options_test.go b/lib/options_test.go index 765fa63b162..7226fc744dd 100644 --- a/lib/options_test.go +++ b/lib/options_test.go @@ -320,14 +320,24 @@ func TestOptions(t *testing.T) { }) t.Run("BlockedHostnames", func(t *testing.T) { hostnames := HostnameTrie{} - err := hostnames.Insert("*") - assert.Nil(t, err) + assert.NoError(t, hostnames.Insert("test.k6.io")) + assert.Error(t, hostnames.Insert("inval*d.pattern")) + assert.NoError(t, hostnames.Insert("*valid.pattern")) opts := Options{}.Apply(Options{ BlockedHostnames: &hostnames, }) assert.NotNil(t, opts.BlockedHostnames) assert.NotEmpty(t, opts.BlockedHostnames) - matches, _ := opts.BlockedHostnames.Contains("loadimpact.com") + matches, _ := opts.BlockedHostnames.Contains("k6.io") + assert.False(t, matches) + matches, _ = opts.BlockedHostnames.Contains("test.k6.io") + assert.True(t, matches) + matches, _ = opts.BlockedHostnames.Contains("blocked.valid.pattern") + assert.True(t, matches) + matches, _ = opts.BlockedHostnames.Contains("example.test.k6.io") + assert.False(t, matches) + assert.NoError(t, opts.BlockedHostnames.Insert("*.test.k6.io")) + matches, _ = opts.BlockedHostnames.Contains("example.test.k6.io") assert.True(t, matches) }) From 372f1401dc1e5eb98fb24dcd42aaac76b8f7ec79 Mon Sep 17 00:00:00 2001 From: krashanoff Date: Sat, 15 Aug 2020 02:41:06 -0700 Subject: [PATCH 08/17] Change implementation of HostnameTrie. Case insensitivity. --- cmd/options.go | 2 +- lib/netext/dialer.go | 2 +- lib/options.go | 58 +++++++++++++++++--------------------------- lib/options_test.go | 16 ++++++++---- 4 files changed, 35 insertions(+), 43 deletions(-) diff --git a/cmd/options.go b/cmd/options.go index 61921b6bee4..9977d8757d4 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -63,7 +63,7 @@ func optionFlagSet() *pflag.FlagSet { flags.Duration("min-iteration-duration", 0, "minimum amount of time k6 will take executing a single iteration") flags.BoolP("throw", "w", false, "throw warnings (like failed http requests) as errors") flags.StringSlice("blacklist-ip", nil, "blacklist an `ip range` from being called") - flags.StringSlice("block-hostname", nil, "block a hostname `pattern`, with optional leading wildcard, from being called") + flags.StringSlice("block-hostname", nil, "block a case-insensitive hostname `pattern`, with optional leading wildcard, from being called") // The comment about system-tags also applies for summary-trend-stats. The default values // are set in applyDefault(). diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go index f0892d76193..72ee1d9fff7 100644 --- a/lib/netext/dialer.go +++ b/lib/netext/dialer.go @@ -83,7 +83,7 @@ func (d *Dialer) DialContext(ctx context.Context, proto, addr string) (net.Conn, host := addr[:delimiter] if d.BlockedHostnames != nil { - if blocked, match := d.BlockedHostnames.Contains(host); blocked { + if match, blocked := d.BlockedHostnames.Contains(host); blocked { return nil, BlockedHostError{hostname: host, match: match} } } diff --git a/lib/options.go b/lib/options.go index 1026731feff..9a7e3495a89 100644 --- a/lib/options.go +++ b/lib/options.go @@ -193,9 +193,7 @@ func ParseCIDR(s string) (*IPNet, error) { // for wildcards exclusively at the start of the pattern. Items may only // be inserted and searched. Internationalized hostnames are valid. type HostnameTrie struct { - children []*HostnameTrie - r rune - terminal bool // end of a valid match + children map[rune]*HostnameTrie } // Regex description of hostname pattern to enforce blocks by. Global var @@ -241,6 +239,7 @@ func (t *HostnameTrie) UnmarshalText(b []byte) error { // Insert a hostname pattern into the given HostnameTrie. Returns an error // if hostname pattern is illegal. func (t *HostnameTrie) Insert(s string) error { + s = strings.ToLower(s) if len(s) == 0 { return nil } @@ -249,55 +248,42 @@ func (t *HostnameTrie) Insert(s string) error { return err } + // mask creation of the trie by initializing the root here + if t.children == nil { + t.children = make(map[rune]*HostnameTrie) + } + rStr := []rune(s) // need to iterate by runes for intl' names last := len(rStr) - 1 - for _, c := range t.children { - if c.r == rStr[last] { - return c.Insert(string(rStr[:last])) - } + if c, ok := t.children[rStr[last]]; ok { + return c.Insert(string(rStr[:last])) } - n := &HostnameTrie{nil, rStr[last], len(rStr) == 1} - t.children = append(t.children, n) - return n.Insert(string(rStr[:last])) + t.children[rStr[last]] = &HostnameTrie{make(map[rune]*HostnameTrie)} + return t.children[rStr[last]].Insert(string(rStr[:last])) } // Contains returns whether s matches a pattern in the HostnameTrie // along with the matching pattern, if one was found. -func (t *HostnameTrie) Contains(s string) (bool, string) { - for _, c := range t.children { - if b, m := c.childContains(s, ""); b { - return b, m - } - } - return false, "" -} - -// recursively traverse HostnameTrie children searching for a match. -func (t *HostnameTrie) childContains(s string, match string) (bool, string) { +func (t *HostnameTrie) Contains(s string) (matchedPattern string, matchFound bool) { + s = strings.ToLower(s) if len(s) == 0 { - return false, "" + return s, len(t.children) == 0 } rStr := []rune(s) last := len(rStr) - 1 - - switch { - case t.r == '*': // wildcard encounters validate the string - return true, string(t.r) + match - case t.r != rStr[last]: - return false, "" - case len(s) == 1: - return t.terminal, string(t.r) + match - default: - for _, c := range t.children { - if b, m := c.childContains(string(rStr[:last]), string(t.r)+match); b { - return b, m - } + if c, ok := t.children[rStr[last]]; ok { + if match, matched := c.Contains(string(rStr[:last])); matched { + return match + string(rStr[last]), true } } - return false, "" + if _, wild := t.children['*']; wild { + return "*", true + } + + return "", false } type Options struct { diff --git a/lib/options_test.go b/lib/options_test.go index 7226fc744dd..23ff4d44d4a 100644 --- a/lib/options_test.go +++ b/lib/options_test.go @@ -328,17 +328,23 @@ func TestOptions(t *testing.T) { }) assert.NotNil(t, opts.BlockedHostnames) assert.NotEmpty(t, opts.BlockedHostnames) - matches, _ := opts.BlockedHostnames.Contains("k6.io") + _, matches := opts.BlockedHostnames.Contains("K6.Io") assert.False(t, matches) - matches, _ = opts.BlockedHostnames.Contains("test.k6.io") + match, matches := opts.BlockedHostnames.Contains("tEsT.k6.Io") assert.True(t, matches) - matches, _ = opts.BlockedHostnames.Contains("blocked.valid.pattern") + assert.Equal(t, "test.k6.io", match) + match, matches = opts.BlockedHostnames.Contains("TEST.K6.IO") assert.True(t, matches) - matches, _ = opts.BlockedHostnames.Contains("example.test.k6.io") + assert.Equal(t, "test.k6.io", match) + match, matches = opts.BlockedHostnames.Contains("blocked.valId.paTtern") + assert.True(t, matches) + assert.Equal(t, "*valid.pattern", match) + _, matches = opts.BlockedHostnames.Contains("example.test.k6.io") assert.False(t, matches) assert.NoError(t, opts.BlockedHostnames.Insert("*.test.k6.io")) - matches, _ = opts.BlockedHostnames.Contains("example.test.k6.io") + match, matches = opts.BlockedHostnames.Contains("example.test.k6.io") assert.True(t, matches) + assert.Equal(t, "*.test.k6.io", match) }) t.Run("Hosts", func(t *testing.T) { From 63f39ba99c13ddb5745039dcd1d2b97193ab5805 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Fri, 9 Oct 2020 16:10:18 +0300 Subject: [PATCH 09/17] Refactoring blockedhostnames to work well with (un)marshaling options --- cmd/options.go | 10 ++-- js/runner.go | 2 +- js/runner_test.go | 6 +-- lib/options.go | 119 ++++++++++++++++++++++++++++++++------------ lib/options_test.go | 22 ++++---- 5 files changed, 105 insertions(+), 54 deletions(-) diff --git a/cmd/options.go b/cmd/options.go index bbdd84f42dc..634ce60e982 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -193,12 +193,10 @@ func getOptions(flags *pflag.FlagSet) (lib.Options, error) { if err != nil { return opts, err } - if len(blockedHostnameStrings) > 0 { - opts.BlockedHostnames = &lib.HostnameTrie{} - } - for _, s := range blockedHostnameStrings { - if insertErr := opts.BlockedHostnames.Insert(s); insertErr != nil { - return opts, errors.Wrap(insertErr, "block-hostname") + if flags.Changed("block-hostname") { + opts.BlockedHostnames, err = lib.NewNullHostnameTrie(blockedHostnameStrings) + if err != nil { + return opts, err } } diff --git a/js/runner.go b/js/runner.go index d7736a7c948..eb584e64bed 100644 --- a/js/runner.go +++ b/js/runner.go @@ -162,7 +162,7 @@ func (r *Runner) newVU(id int64, samplesOut chan<- stats.SampleContainer) (*VU, Dialer: r.BaseDialer, Resolver: r.Resolver, Blacklist: r.Bundle.Options.BlacklistIPs, - BlockedHostnames: r.Bundle.Options.BlockedHostnames, + BlockedHostnames: r.Bundle.Options.BlockedHostnames.Trie, Hosts: r.Bundle.Options.Hosts, } tlsConfig := &tls.Config{ diff --git a/js/runner_test.go b/js/runner_test.go index b9e010b81a9..8401d0d6383 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -892,11 +892,11 @@ func TestVUIntegrationBlockHostnamesOption(t *testing.T) { `) require.NoError(t, err) - hostnames := lib.HostnameTrie{} - require.NoError(t, hostnames.Insert("*.io")) + hostnames, err := lib.NewNullHostnameTrie([]string{"*.io"}) + require.NoError(t, err) require.NoError(t, r1.SetOptions(lib.Options{ Throw: null.BoolFrom(true), - BlockedHostnames: &hostnames, + BlockedHostnames: hostnames, })) r2, err := NewFromArchive(testutils.NewLogger(t), r1.MakeArchive(), lib.RuntimeOptions{}) diff --git a/lib/options.go b/lib/options.go index 926983379ca..a2947133ae3 100644 --- a/lib/options.go +++ b/lib/options.go @@ -21,6 +21,7 @@ package lib import ( + "bytes" "crypto/tls" "encoding/json" "fmt" @@ -279,13 +280,91 @@ func ParseCIDR(s string) (*IPNet, error) { return &parsedIPNet, nil } +// NullHostnameTrie is a nullable HostnameTrie, in the same vein as the nullable types provided by +// package gopkg.in/guregu/null.v3 +type NullHostnameTrie struct { + Trie *HostnameTrie + Valid bool +} + +// UnmarshalText converts text data to a valid NullHostnameTrie +func (d *NullHostnameTrie) UnmarshalText(data []byte) error { + if len(data) == 0 { + *d = NullHostnameTrie{} + return nil + } + var err error + d.Trie, err = NewHostnameTrie(strings.Split(string(data), ",")) + if err != nil { + return err + } + d.Valid = true + return nil +} + +// UnmarshalJSON converts JSON data to a valid NullHostnameTrie +func (d *NullHostnameTrie) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte(`null`)) { + d.Valid = false + return nil + } + + var m []string + var err error + if err = json.Unmarshal(data, &m); err != nil { + return err + } + d.Trie, err = NewHostnameTrie(m) + if err != nil { + return err + } + d.Valid = true + return nil +} + +// MarshalJSON implements json.Marshaler interface +func (d NullHostnameTrie) MarshalJSON() ([]byte, error) { + if !d.Valid { + return []byte(`null`), nil + } + return json.Marshal(d.Trie.source) +} + // HostnameTrie is a tree-structured list of hostname matches with support // for wildcards exclusively at the start of the pattern. Items may only // be inserted and searched. Internationalized hostnames are valid. type HostnameTrie struct { + source []string + children map[rune]*HostnameTrie } +// NewNullHostnameTrie returns a NullHostnameTrie encapsulating HostnameTrie or an error if the +// input is incorrect +func NewNullHostnameTrie(source []string) (NullHostnameTrie, error) { + h, err := NewHostnameTrie(source) + if err != nil { + return NullHostnameTrie{}, err + } + return NullHostnameTrie{ + Valid: true, + Trie: h, + }, nil +} + +// NewHostnameTrie returns a pointer to a new HostnameTrie or an error if the input is incorrect +func NewHostnameTrie(source []string) (*HostnameTrie, error) { + h := &HostnameTrie{ + source: source, + } + for _, s := range h.source { + if err := h.insert(s); err != nil { + return nil, err + } + } + return h, nil +} + // Regex description of hostname pattern to enforce blocks by. Global var // to avoid compilation penalty at runtime. // Matches against strings composed entirely of letters, numbers, or '.'s @@ -300,35 +379,9 @@ func legalHostname(s string) error { return nil } -// UnmarshalJSON forms a HostnameTrie from the provided hostname pattern -// list. -func (t *HostnameTrie) UnmarshalJSON(data []byte) error { - m := make([]string, 0) - if err := json.Unmarshal(data, &m); err != nil { - return err - } - for _, h := range m { - if insertErr := t.Insert(h); insertErr != nil { - return insertErr - } - } - return nil -} - -// UnmarshalText forms a HostnameTrie from a comma-delimited list -// of hostname patterns. -func (t *HostnameTrie) UnmarshalText(b []byte) error { - for _, s := range strings.Split(string(b), ",") { - if err := t.Insert(s); err != nil { - return err - } - } - return nil -} - -// Insert a hostname pattern into the given HostnameTrie. Returns an error +// insert a hostname pattern into the given HostnameTrie. Returns an error // if hostname pattern is illegal. -func (t *HostnameTrie) Insert(s string) error { +func (t *HostnameTrie) insert(s string) error { s = strings.ToLower(s) if len(s) == 0 { return nil @@ -346,11 +399,11 @@ func (t *HostnameTrie) Insert(s string) error { rStr := []rune(s) // need to iterate by runes for intl' names last := len(rStr) - 1 if c, ok := t.children[rStr[last]]; ok { - return c.Insert(string(rStr[:last])) + return c.insert(string(rStr[:last])) } - t.children[rStr[last]] = &HostnameTrie{make(map[rune]*HostnameTrie)} - return t.children[rStr[last]].Insert(string(rStr[:last])) + t.children[rStr[last]] = &HostnameTrie{children: make(map[rune]*HostnameTrie)} + return t.children[rStr[last]].insert(string(rStr[:last])) } // Contains returns whether s matches a pattern in the HostnameTrie @@ -439,7 +492,7 @@ type Options struct { BlacklistIPs []*IPNet `json:"blacklistIPs" envconfig:"K6_BLACKLIST_IPS"` // Block hostname patterns that tests may not contact. - BlockedHostnames *HostnameTrie `json:"blockHostnames" envconfig:"K6_BLOCK_HOSTNAMES"` + BlockedHostnames NullHostnameTrie `json:"blockHostnames" envconfig:"K6_BLOCK_HOSTNAMES"` // Hosts overrides dns entries for given hosts Hosts map[string]*HostAddress `json:"hosts" envconfig:"K6_HOSTS"` @@ -596,7 +649,7 @@ func (o Options) Apply(opts Options) Options { if opts.BlacklistIPs != nil { o.BlacklistIPs = opts.BlacklistIPs } - if opts.BlockedHostnames != nil { + if opts.BlockedHostnames.Valid { o.BlockedHostnames = opts.BlockedHostnames } if opts.Hosts != nil { diff --git a/lib/options_test.go b/lib/options_test.go index 1d259075ace..1b50652954e 100644 --- a/lib/options_test.go +++ b/lib/options_test.go @@ -316,29 +316,29 @@ func TestOptions(t *testing.T) { }) t.Run("BlockedHostnames", func(t *testing.T) { hostnames := HostnameTrie{} - assert.NoError(t, hostnames.Insert("test.k6.io")) - assert.Error(t, hostnames.Insert("inval*d.pattern")) - assert.NoError(t, hostnames.Insert("*valid.pattern")) + assert.NoError(t, hostnames.insert("test.k6.io")) + assert.Error(t, hostnames.insert("inval*d.pattern")) + assert.NoError(t, hostnames.insert("*valid.pattern")) opts := Options{}.Apply(Options{ - BlockedHostnames: &hostnames, + BlockedHostnames: NullHostnameTrie{Trie: &hostnames, Valid: true}, }) assert.NotNil(t, opts.BlockedHostnames) assert.NotEmpty(t, opts.BlockedHostnames) - _, matches := opts.BlockedHostnames.Contains("K6.Io") + _, matches := opts.BlockedHostnames.Trie.Contains("K6.Io") assert.False(t, matches) - match, matches := opts.BlockedHostnames.Contains("tEsT.k6.Io") + match, matches := opts.BlockedHostnames.Trie.Contains("tEsT.k6.Io") assert.True(t, matches) assert.Equal(t, "test.k6.io", match) - match, matches = opts.BlockedHostnames.Contains("TEST.K6.IO") + match, matches = opts.BlockedHostnames.Trie.Contains("TEST.K6.IO") assert.True(t, matches) assert.Equal(t, "test.k6.io", match) - match, matches = opts.BlockedHostnames.Contains("blocked.valId.paTtern") + match, matches = opts.BlockedHostnames.Trie.Contains("blocked.valId.paTtern") assert.True(t, matches) assert.Equal(t, "*valid.pattern", match) - _, matches = opts.BlockedHostnames.Contains("example.test.k6.io") + _, matches = opts.BlockedHostnames.Trie.Contains("example.test.k6.io") assert.False(t, matches) - assert.NoError(t, opts.BlockedHostnames.Insert("*.test.k6.io")) - match, matches = opts.BlockedHostnames.Contains("example.test.k6.io") + assert.NoError(t, opts.BlockedHostnames.Trie.insert("*.test.k6.io")) + match, matches = opts.BlockedHostnames.Trie.Contains("example.test.k6.io") assert.True(t, matches) assert.Equal(t, "*.test.k6.io", match) }) From 46d7dd0c9c4d759045bfc6fc00361fff224c7ebd Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Fri, 9 Oct 2020 16:24:46 +0300 Subject: [PATCH 10/17] move HostnameTrie to lib/types --- cmd/options.go | 2 +- js/runner_test.go | 2 +- lib/netext/dialer.go | 3 +- lib/options.go | 154 +------------------------------ lib/options_test.go | 29 +----- lib/types/hostnametrie.go | 159 +++++++++++++++++++++++++++++++++ lib/types/hostnametrie_test.go | 33 +++++++ 7 files changed, 201 insertions(+), 181 deletions(-) create mode 100644 lib/types/hostnametrie.go create mode 100644 lib/types/hostnametrie_test.go diff --git a/cmd/options.go b/cmd/options.go index 634ce60e982..17bf1b477ca 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -194,7 +194,7 @@ func getOptions(flags *pflag.FlagSet) (lib.Options, error) { return opts, err } if flags.Changed("block-hostname") { - opts.BlockedHostnames, err = lib.NewNullHostnameTrie(blockedHostnameStrings) + opts.BlockedHostnames, err = types.NewNullHostnameTrie(blockedHostnameStrings) if err != nil { return opts, err } diff --git a/js/runner_test.go b/js/runner_test.go index 8401d0d6383..00a46631fc4 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -892,7 +892,7 @@ func TestVUIntegrationBlockHostnamesOption(t *testing.T) { `) require.NoError(t, err) - hostnames, err := lib.NewNullHostnameTrie([]string{"*.io"}) + hostnames, err := types.NewNullHostnameTrie([]string{"*.io"}) require.NoError(t, err) require.NoError(t, r1.SetOptions(lib.Options{ Throw: null.BoolFrom(true), diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go index 54907de52d4..3777722a76c 100644 --- a/lib/netext/dialer.go +++ b/lib/netext/dialer.go @@ -33,6 +33,7 @@ import ( "github.com/loadimpact/k6/lib" "github.com/loadimpact/k6/lib/metrics" + "github.com/loadimpact/k6/lib/types" "github.com/loadimpact/k6/stats" ) @@ -49,7 +50,7 @@ type Dialer struct { Resolver dnsResolver Blacklist []*lib.IPNet - BlockedHostnames *lib.HostnameTrie + BlockedHostnames *types.HostnameTrie Hosts map[string]*lib.HostAddress BytesRead int64 diff --git a/lib/options.go b/lib/options.go index a2947133ae3..20555d4fd41 100644 --- a/lib/options.go +++ b/lib/options.go @@ -21,15 +21,12 @@ package lib import ( - "bytes" "crypto/tls" "encoding/json" "fmt" "net" "reflect" - "regexp" "strconv" - "strings" "github.com/pkg/errors" "gopkg.in/guregu/null.v3" @@ -280,155 +277,6 @@ func ParseCIDR(s string) (*IPNet, error) { return &parsedIPNet, nil } -// NullHostnameTrie is a nullable HostnameTrie, in the same vein as the nullable types provided by -// package gopkg.in/guregu/null.v3 -type NullHostnameTrie struct { - Trie *HostnameTrie - Valid bool -} - -// UnmarshalText converts text data to a valid NullHostnameTrie -func (d *NullHostnameTrie) UnmarshalText(data []byte) error { - if len(data) == 0 { - *d = NullHostnameTrie{} - return nil - } - var err error - d.Trie, err = NewHostnameTrie(strings.Split(string(data), ",")) - if err != nil { - return err - } - d.Valid = true - return nil -} - -// UnmarshalJSON converts JSON data to a valid NullHostnameTrie -func (d *NullHostnameTrie) UnmarshalJSON(data []byte) error { - if bytes.Equal(data, []byte(`null`)) { - d.Valid = false - return nil - } - - var m []string - var err error - if err = json.Unmarshal(data, &m); err != nil { - return err - } - d.Trie, err = NewHostnameTrie(m) - if err != nil { - return err - } - d.Valid = true - return nil -} - -// MarshalJSON implements json.Marshaler interface -func (d NullHostnameTrie) MarshalJSON() ([]byte, error) { - if !d.Valid { - return []byte(`null`), nil - } - return json.Marshal(d.Trie.source) -} - -// HostnameTrie is a tree-structured list of hostname matches with support -// for wildcards exclusively at the start of the pattern. Items may only -// be inserted and searched. Internationalized hostnames are valid. -type HostnameTrie struct { - source []string - - children map[rune]*HostnameTrie -} - -// NewNullHostnameTrie returns a NullHostnameTrie encapsulating HostnameTrie or an error if the -// input is incorrect -func NewNullHostnameTrie(source []string) (NullHostnameTrie, error) { - h, err := NewHostnameTrie(source) - if err != nil { - return NullHostnameTrie{}, err - } - return NullHostnameTrie{ - Valid: true, - Trie: h, - }, nil -} - -// NewHostnameTrie returns a pointer to a new HostnameTrie or an error if the input is incorrect -func NewHostnameTrie(source []string) (*HostnameTrie, error) { - h := &HostnameTrie{ - source: source, - } - for _, s := range h.source { - if err := h.insert(s); err != nil { - return nil, err - } - } - return h, nil -} - -// Regex description of hostname pattern to enforce blocks by. Global var -// to avoid compilation penalty at runtime. -// Matches against strings composed entirely of letters, numbers, or '.'s -// with an optional wildcard at the start. -//nolint:gochecknoglobals -var legalHostnamePattern *regexp.Regexp = regexp.MustCompile(`^\*?(\pL|[0-9\.])*`) - -func legalHostname(s string) error { - if len(legalHostnamePattern.FindString(s)) != len(s) { - return errors.Errorf("invalid hostname pattern %s", s) - } - return nil -} - -// insert a hostname pattern into the given HostnameTrie. Returns an error -// if hostname pattern is illegal. -func (t *HostnameTrie) insert(s string) error { - s = strings.ToLower(s) - if len(s) == 0 { - return nil - } - - if err := legalHostname(s); err != nil { - return err - } - - // mask creation of the trie by initializing the root here - if t.children == nil { - t.children = make(map[rune]*HostnameTrie) - } - - rStr := []rune(s) // need to iterate by runes for intl' names - last := len(rStr) - 1 - if c, ok := t.children[rStr[last]]; ok { - return c.insert(string(rStr[:last])) - } - - t.children[rStr[last]] = &HostnameTrie{children: make(map[rune]*HostnameTrie)} - return t.children[rStr[last]].insert(string(rStr[:last])) -} - -// Contains returns whether s matches a pattern in the HostnameTrie -// along with the matching pattern, if one was found. -func (t *HostnameTrie) Contains(s string) (matchedPattern string, matchFound bool) { - s = strings.ToLower(s) - if len(s) == 0 { - return s, len(t.children) == 0 - } - - rStr := []rune(s) - last := len(rStr) - 1 - if c, ok := t.children[rStr[last]]; ok { - if match, matched := c.Contains(string(rStr[:last])); matched { - return match + string(rStr[last]), true - } - } - - if _, wild := t.children['*']; wild { - return "*", true - } - - return "", false -} - type Options struct { // Should the test start in a paused state? Paused null.Bool `json:"paused" envconfig:"K6_PAUSED"` @@ -492,7 +340,7 @@ type Options struct { BlacklistIPs []*IPNet `json:"blacklistIPs" envconfig:"K6_BLACKLIST_IPS"` // Block hostname patterns that tests may not contact. - BlockedHostnames NullHostnameTrie `json:"blockHostnames" envconfig:"K6_BLOCK_HOSTNAMES"` + BlockedHostnames types.NullHostnameTrie `json:"blockHostnames" envconfig:"K6_BLOCK_HOSTNAMES"` // Hosts overrides dns entries for given hosts Hosts map[string]*HostAddress `json:"hosts" envconfig:"K6_HOSTS"` diff --git a/lib/options_test.go b/lib/options_test.go index 1b50652954e..a7eb55b4147 100644 --- a/lib/options_test.go +++ b/lib/options_test.go @@ -315,32 +315,11 @@ func TestOptions(t *testing.T) { assert.Equal(t, net.CIDRMask(1, 1), opts.BlacklistIPs[0].Mask) }) t.Run("BlockedHostnames", func(t *testing.T) { - hostnames := HostnameTrie{} - assert.NoError(t, hostnames.insert("test.k6.io")) - assert.Error(t, hostnames.insert("inval*d.pattern")) - assert.NoError(t, hostnames.insert("*valid.pattern")) - opts := Options{}.Apply(Options{ - BlockedHostnames: NullHostnameTrie{Trie: &hostnames, Valid: true}, - }) + blockedHostnames, err := types.NewNullHostnameTrie([]string{"test.k6.io", "*valid.pattern"}) + require.NoError(t, err) + opts := Options{}.Apply(Options{BlockedHostnames: blockedHostnames}) assert.NotNil(t, opts.BlockedHostnames) - assert.NotEmpty(t, opts.BlockedHostnames) - _, matches := opts.BlockedHostnames.Trie.Contains("K6.Io") - assert.False(t, matches) - match, matches := opts.BlockedHostnames.Trie.Contains("tEsT.k6.Io") - assert.True(t, matches) - assert.Equal(t, "test.k6.io", match) - match, matches = opts.BlockedHostnames.Trie.Contains("TEST.K6.IO") - assert.True(t, matches) - assert.Equal(t, "test.k6.io", match) - match, matches = opts.BlockedHostnames.Trie.Contains("blocked.valId.paTtern") - assert.True(t, matches) - assert.Equal(t, "*valid.pattern", match) - _, matches = opts.BlockedHostnames.Trie.Contains("example.test.k6.io") - assert.False(t, matches) - assert.NoError(t, opts.BlockedHostnames.Trie.insert("*.test.k6.io")) - match, matches = opts.BlockedHostnames.Trie.Contains("example.test.k6.io") - assert.True(t, matches) - assert.Equal(t, "*.test.k6.io", match) + assert.Equal(t, blockedHostnames, opts.BlockedHostnames) }) t.Run("Hosts", func(t *testing.T) { diff --git a/lib/types/hostnametrie.go b/lib/types/hostnametrie.go new file mode 100644 index 00000000000..dadd86b5be7 --- /dev/null +++ b/lib/types/hostnametrie.go @@ -0,0 +1,159 @@ +package types + +import ( + "bytes" + "encoding/json" + "regexp" + "strings" + + "github.com/pkg/errors" +) + +// NullHostnameTrie is a nullable HostnameTrie, in the same vein as the nullable types provided by +// package gopkg.in/guregu/null.v3 +type NullHostnameTrie struct { + Trie *HostnameTrie + Valid bool +} + +// UnmarshalText converts text data to a valid NullHostnameTrie +func (d *NullHostnameTrie) UnmarshalText(data []byte) error { + if len(data) == 0 { + *d = NullHostnameTrie{} + return nil + } + var err error + d.Trie, err = NewHostnameTrie(strings.Split(string(data), ",")) + if err != nil { + return err + } + d.Valid = true + return nil +} + +// UnmarshalJSON converts JSON data to a valid NullHostnameTrie +func (d *NullHostnameTrie) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte(`null`)) { + d.Valid = false + return nil + } + + var m []string + var err error + if err = json.Unmarshal(data, &m); err != nil { + return err + } + d.Trie, err = NewHostnameTrie(m) + if err != nil { + return err + } + d.Valid = true + return nil +} + +// MarshalJSON implements json.Marshaler interface +func (d NullHostnameTrie) MarshalJSON() ([]byte, error) { + if !d.Valid { + return []byte(`null`), nil + } + return json.Marshal(d.Trie.source) +} + +// HostnameTrie is a tree-structured list of hostname matches with support +// for wildcards exclusively at the start of the pattern. Items may only +// be inserted and searched. Internationalized hostnames are valid. +type HostnameTrie struct { + source []string + + children map[rune]*HostnameTrie +} + +// NewNullHostnameTrie returns a NullHostnameTrie encapsulating HostnameTrie or an error if the +// input is incorrect +func NewNullHostnameTrie(source []string) (NullHostnameTrie, error) { + h, err := NewHostnameTrie(source) + if err != nil { + return NullHostnameTrie{}, err + } + return NullHostnameTrie{ + Valid: true, + Trie: h, + }, nil +} + +// NewHostnameTrie returns a pointer to a new HostnameTrie or an error if the input is incorrect +func NewHostnameTrie(source []string) (*HostnameTrie, error) { + h := &HostnameTrie{ + source: source, + } + for _, s := range h.source { + if err := h.insert(s); err != nil { + return nil, err + } + } + return h, nil +} + +// Regex description of hostname pattern to enforce blocks by. Global var +// to avoid compilation penalty at runtime. +// Matches against strings composed entirely of letters, numbers, or '.'s +// with an optional wildcard at the start. +//nolint:gochecknoglobals +var legalHostnamePattern *regexp.Regexp = regexp.MustCompile(`^\*?(\pL|[0-9\.])*`) + +func legalHostname(s string) error { + if len(legalHostnamePattern.FindString(s)) != len(s) { + return errors.Errorf("invalid hostname pattern %s", s) + } + return nil +} + +// insert a hostname pattern into the given HostnameTrie. Returns an error +// if hostname pattern is illegal. +func (t *HostnameTrie) insert(s string) error { + s = strings.ToLower(s) + if len(s) == 0 { + return nil + } + + if err := legalHostname(s); err != nil { + return err + } + + // mask creation of the trie by initializing the root here + if t.children == nil { + t.children = make(map[rune]*HostnameTrie) + } + + rStr := []rune(s) // need to iterate by runes for intl' names + last := len(rStr) - 1 + if c, ok := t.children[rStr[last]]; ok { + return c.insert(string(rStr[:last])) + } + + t.children[rStr[last]] = &HostnameTrie{children: make(map[rune]*HostnameTrie)} + return t.children[rStr[last]].insert(string(rStr[:last])) +} + +// Contains returns whether s matches a pattern in the HostnameTrie +// along with the matching pattern, if one was found. +func (t *HostnameTrie) Contains(s string) (matchedPattern string, matchFound bool) { + s = strings.ToLower(s) + if len(s) == 0 { + return s, len(t.children) == 0 + } + + rStr := []rune(s) + last := len(rStr) - 1 + if c, ok := t.children[rStr[last]]; ok { + if match, matched := c.Contains(string(rStr[:last])); matched { + return match + string(rStr[last]), true + } + } + + if _, wild := t.children['*']; wild { + return "*", true + } + + return "", false +} diff --git a/lib/types/hostnametrie_test.go b/lib/types/hostnametrie_test.go new file mode 100644 index 00000000000..dc0a5ccb373 --- /dev/null +++ b/lib/types/hostnametrie_test.go @@ -0,0 +1,33 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHostnameTrieInsert(t *testing.T) { + hostnames := HostnameTrie{} + assert.NoError(t, hostnames.insert("test.k6.io")) + assert.Error(t, hostnames.insert("inval*d.pattern")) + assert.NoError(t, hostnames.insert("*valid.pattern")) +} + +func TestHostnameTrieContains(t *testing.T) { + trie, err := NewHostnameTrie([]string{"test.k6.io", "*valid.pattern"}) + require.NoError(t, err) + _, matches := trie.Contains("K6.Io") + assert.False(t, matches) + match, matches := trie.Contains("tEsT.k6.Io") + assert.True(t, matches) + assert.Equal(t, "test.k6.io", match) + match, matches = trie.Contains("TEST.K6.IO") + assert.True(t, matches) + assert.Equal(t, "test.k6.io", match) + match, matches = trie.Contains("blocked.valId.paTtern") + assert.True(t, matches) + assert.Equal(t, "*valid.pattern", match) + _, matches = trie.Contains("example.test.k6.io") + assert.False(t, matches) +} From e5a920219fc3282441db3b90bc7d09d8663dc7b5 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Fri, 9 Oct 2020 16:47:26 +0300 Subject: [PATCH 11/17] Don't block ips with blockHostnames --- lib/netext/dialer.go | 16 ++++++++++------ lib/netext/dialer_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go index 3777722a76c..f3de72f93ed 100644 --- a/lib/netext/dialer.go +++ b/lib/netext/dialer.go @@ -175,14 +175,13 @@ func (d *Dialer) findRemote(addr string) (*lib.HostAddress, error) { return nil, err } - if d.BlockedHostnames != nil { - if match, blocked := d.BlockedHostnames.Contains(host); blocked { - return nil, BlockedHostError{hostname: host, match: match} - } - } - remote, err := d.getConfiguredHost(addr, host, port) if err != nil || remote != nil { + if err == nil && d.BlockedHostnames != nil && net.ParseIP(host) == nil { + if match, blocked := d.BlockedHostnames.Contains(host); blocked { + return nil, BlockedHostError{hostname: host, match: match} + } + } return remote, err } @@ -191,6 +190,11 @@ func (d *Dialer) findRemote(addr string) (*lib.HostAddress, error) { return lib.NewHostAddress(ip, port) } + if d.BlockedHostnames != nil { + if match, blocked := d.BlockedHostnames.Contains(host); blocked { + return nil, BlockedHostError{hostname: host, match: match} + } + } return d.fetchRemoteFromResolver(host, port) } diff --git a/lib/netext/dialer_test.go b/lib/netext/dialer_test.go index 15a1d67c9c6..71db79cc07e 100644 --- a/lib/netext/dialer_test.go +++ b/lib/netext/dialer_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/loadimpact/k6/lib" + "github.com/loadimpact/k6/lib/types" "github.com/stretchr/testify/require" ) @@ -74,6 +75,42 @@ func TestDialerAddr(t *testing.T) { {"[::1.2.3.4]", "", "address [::1.2.3.4]: missing port in address"}, {"example-ipv6-deny-resolver.com:80", "", "IP (::1) is in a blacklisted range (::/24)"}, {"example-ipv6-deny-host.com:80", "", "IP (::1) is in a blacklisted range (::/24)"}, + {"example-ipv6-deny-host.com:80", "", "IP (::1) is in a blacklisted range (::/24)"}, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.address, func(t *testing.T) { + addr, err := dialer.getDialAddr(tc.address) + + if tc.expErr != "" { + require.EqualError(t, err, tc.expErr) + } else { + require.NoError(t, err) + require.Equal(t, tc.expAddress, addr) + } + }) + } +} + +func TestDialerAddrBlockHostnamesStar(t *testing.T) { + dialer := newDialerWithResolver(net.Dialer{}, newResolver()) + dialer.Hosts = map[string]*lib.HostAddress{ + "example.com": {IP: net.ParseIP("3.4.5.6")}, + } + + blocked, err := types.NewHostnameTrie([]string{"*"}) + require.NoError(t, err) + dialer.BlockedHostnames = blocked + testCases := []struct { + address, expAddress, expErr string + }{ + // IPv4 + {"example.com:80", "", "hostname (example.com) is in a blocked pattern (*)"}, + {"example.com:443", "", "hostname (example.com) is in a blocked pattern (*)"}, + {"not.com:30", "", "hostname (not.com) is in a blocked pattern (*)"}, + {"1.2.3.4:80", "1.2.3.4:80", ""}, } for _, tc := range testCases { From 3cb4151575aec6bd191dabd2f8da6909f637e816 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Fri, 9 Oct 2020 17:24:37 +0300 Subject: [PATCH 12/17] blockhostnames: Better regex handling hostnames with hyphens inside them --- lib/types/hostnametrie.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/types/hostnametrie.go b/lib/types/hostnametrie.go index dadd86b5be7..db19f067a5d 100644 --- a/lib/types/hostnametrie.go +++ b/lib/types/hostnametrie.go @@ -96,10 +96,9 @@ func NewHostnameTrie(source []string) (*HostnameTrie, error) { // Regex description of hostname pattern to enforce blocks by. Global var // to avoid compilation penalty at runtime. -// Matches against strings composed entirely of letters, numbers, or '.'s -// with an optional wildcard at the start. -//nolint:gochecknoglobals -var legalHostnamePattern *regexp.Regexp = regexp.MustCompile(`^\*?(\pL|[0-9\.])*`) +// based on regex from https://stackoverflow.com/a/106223/5427244 +//nolint:gochecknoglobals,lll +var legalHostnamePattern *regexp.Regexp = regexp.MustCompile(`^(\*\.?)?((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$`) func legalHostname(s string) error { if len(legalHostnamePattern.FindString(s)) != len(s) { @@ -112,14 +111,18 @@ func legalHostname(s string) error { // if hostname pattern is illegal. func (t *HostnameTrie) insert(s string) error { s = strings.ToLower(s) - if len(s) == 0 { - return nil - } - if err := legalHostname(s); err != nil { return err } + return t.childInsert(s) +} + +func (t *HostnameTrie) childInsert(s string) error { + if len(s) == 0 { + return nil + } + // mask creation of the trie by initializing the root here if t.children == nil { t.children = make(map[rune]*HostnameTrie) @@ -128,11 +131,11 @@ func (t *HostnameTrie) insert(s string) error { rStr := []rune(s) // need to iterate by runes for intl' names last := len(rStr) - 1 if c, ok := t.children[rStr[last]]; ok { - return c.insert(string(rStr[:last])) + return c.childInsert(string(rStr[:last])) } t.children[rStr[last]] = &HostnameTrie{children: make(map[rune]*HostnameTrie)} - return t.children[rStr[last]].insert(string(rStr[:last])) + return t.children[rStr[last]].childInsert(string(rStr[:last])) } // Contains returns whether s matches a pattern in the HostnameTrie From 2f729ecaa42762c41731963b3d6a6faf17c64862 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Wed, 14 Oct 2020 11:38:04 +0300 Subject: [PATCH 13/17] Add copyright headers to new files --- lib/types/hostnametrie.go | 20 ++++++++++++++++++++ lib/types/hostnametrie_test.go | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/types/hostnametrie.go b/lib/types/hostnametrie.go index db19f067a5d..0f398f47be5 100644 --- a/lib/types/hostnametrie.go +++ b/lib/types/hostnametrie.go @@ -1,3 +1,23 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2020 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + package types import ( diff --git a/lib/types/hostnametrie_test.go b/lib/types/hostnametrie_test.go index dc0a5ccb373..206ea8e6ba0 100644 --- a/lib/types/hostnametrie_test.go +++ b/lib/types/hostnametrie_test.go @@ -1,3 +1,23 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2020 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + package types import ( From c4262f920ce49a4389ec90cff6a9b77282feddd1 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Wed, 14 Oct 2020 11:40:12 +0300 Subject: [PATCH 14/17] hostnametrie: reword legal to valid --- lib/types/hostnametrie.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/types/hostnametrie.go b/lib/types/hostnametrie.go index 0f398f47be5..b9b0c6e401d 100644 --- a/lib/types/hostnametrie.go +++ b/lib/types/hostnametrie.go @@ -118,20 +118,20 @@ func NewHostnameTrie(source []string) (*HostnameTrie, error) { // to avoid compilation penalty at runtime. // based on regex from https://stackoverflow.com/a/106223/5427244 //nolint:gochecknoglobals,lll -var legalHostnamePattern *regexp.Regexp = regexp.MustCompile(`^(\*\.?)?((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$`) +var validHostnamePattern *regexp.Regexp = regexp.MustCompile(`^(\*\.?)?((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$`) -func legalHostname(s string) error { - if len(legalHostnamePattern.FindString(s)) != len(s) { +func isValidHostnamePattern(s string) error { + if len(validHostnamePattern.FindString(s)) != len(s) { return errors.Errorf("invalid hostname pattern %s", s) } return nil } // insert a hostname pattern into the given HostnameTrie. Returns an error -// if hostname pattern is illegal. +// if hostname pattern is valid. func (t *HostnameTrie) insert(s string) error { s = strings.ToLower(s) - if err := legalHostname(s); err != nil { + if err := isValidHostnamePattern(s); err != nil { return err } From ce17b17ad528018677d8f909b7549aac5a3e8f16 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Wed, 14 Oct 2020 11:52:24 +0300 Subject: [PATCH 15/17] blockhostnames: Dry up the code --- lib/netext/dialer.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go index f3de72f93ed..050c4660067 100644 --- a/lib/netext/dialer.go +++ b/lib/netext/dialer.go @@ -175,26 +175,22 @@ func (d *Dialer) findRemote(addr string) (*lib.HostAddress, error) { return nil, err } + ip := net.ParseIP(host) + if d.BlockedHostnames != nil && ip == nil { + if match, blocked := d.BlockedHostnames.Contains(host); blocked { + return nil, BlockedHostError{hostname: host, match: match} + } + } + remote, err := d.getConfiguredHost(addr, host, port) if err != nil || remote != nil { - if err == nil && d.BlockedHostnames != nil && net.ParseIP(host) == nil { - if match, blocked := d.BlockedHostnames.Contains(host); blocked { - return nil, BlockedHostError{hostname: host, match: match} - } - } return remote, err } - ip := net.ParseIP(host) if ip != nil { return lib.NewHostAddress(ip, port) } - if d.BlockedHostnames != nil { - if match, blocked := d.BlockedHostnames.Contains(host); blocked { - return nil, BlockedHostError{hostname: host, match: match} - } - } return d.fetchRemoteFromResolver(host, port) } From 8321e9b88c7f38eec8ca6faa045fb9d720d3da12 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Wed, 14 Oct 2020 12:04:22 +0300 Subject: [PATCH 16/17] rewrite TestHostnameTrieContains as table test --- lib/types/hostnametrie_test.go | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/types/hostnametrie_test.go b/lib/types/hostnametrie_test.go index 206ea8e6ba0..50fda4d48fd 100644 --- a/lib/types/hostnametrie_test.go +++ b/lib/types/hostnametrie_test.go @@ -37,17 +37,24 @@ func TestHostnameTrieInsert(t *testing.T) { func TestHostnameTrieContains(t *testing.T) { trie, err := NewHostnameTrie([]string{"test.k6.io", "*valid.pattern"}) require.NoError(t, err) - _, matches := trie.Contains("K6.Io") - assert.False(t, matches) - match, matches := trie.Contains("tEsT.k6.Io") - assert.True(t, matches) - assert.Equal(t, "test.k6.io", match) - match, matches = trie.Contains("TEST.K6.IO") - assert.True(t, matches) - assert.Equal(t, "test.k6.io", match) - match, matches = trie.Contains("blocked.valId.paTtern") - assert.True(t, matches) - assert.Equal(t, "*valid.pattern", match) - _, matches = trie.Contains("example.test.k6.io") - assert.False(t, matches) + cases := map[string]string{ + "K6.Io": "", + "tEsT.k6.Io": "test.k6.io", + "TESt.K6.IO": "test.k6.io", + "blocked.valId.paTtern": "*valid.pattern", + "example.test.k6.io": "", + } + for key, value := range cases { + host, pattern := key, value + t.Run(host, func(t *testing.T) { + match, matches := trie.Contains(host) + if pattern == "" { + assert.False(t, matches) + assert.Empty(t, match) + } else { + assert.True(t, matches) + assert.Equal(t, pattern, match) + } + }) + } } From 9a751126c82fdad64677af5414f34144312e2c0d Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Wed, 14 Oct 2020 17:31:28 +0300 Subject: [PATCH 17/17] Update lib/types/hostnametrie.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ivan Mirić --- lib/types/hostnametrie.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types/hostnametrie.go b/lib/types/hostnametrie.go index b9b0c6e401d..eb3ab7d500d 100644 --- a/lib/types/hostnametrie.go +++ b/lib/types/hostnametrie.go @@ -128,7 +128,7 @@ func isValidHostnamePattern(s string) error { } // insert a hostname pattern into the given HostnameTrie. Returns an error -// if hostname pattern is valid. +// if hostname pattern is invalid. func (t *HostnameTrie) insert(s string) error { s = strings.ToLower(s) if err := isValidHostnamePattern(s); err != nil {