From ed66d85f3fa7d8cef17aad567c0c3e719e76eba1 Mon Sep 17 00:00:00 2001 From: Rob Prentiss Date: Wed, 2 Oct 2019 05:45:58 -0700 Subject: [PATCH] Use glob matching for package paths (#1409) - Match the behavior of GOPRIVATE/GONOPROXY/GONOSUMDB --- cmd/proxy/actions/sumdb.go | 31 +--------- pkg/download/mode/mode.go | 6 +- pkg/download/mode/mode_test.go | 22 +++++++ pkg/paths/path.go | 27 +++++++++ pkg/paths/path_test.go | 105 +++++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 31 deletions(-) create mode 100644 pkg/paths/path_test.go diff --git a/cmd/proxy/actions/sumdb.go b/cmd/proxy/actions/sumdb.go index 6fdd0e11..491081d8 100644 --- a/cmd/proxy/actions/sumdb.go +++ b/cmd/proxy/actions/sumdb.go @@ -4,8 +4,9 @@ import ( "net/http" "net/http/httputil" "net/url" - "path" "strings" + + "github.com/gomods/athens/pkg/paths" ) func sumdbPoxy(url *url.URL, nosumPatterns []string) http.Handler { @@ -25,7 +26,7 @@ func noSumWrapper(h http.Handler, host string, patterns []string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/lookup/") { for _, p := range patterns { - if matchesPattern(p, r.URL.Path[len("/lookup/"):]) { + if paths.MatchesPattern(p, r.URL.Path[len("/lookup/"):]) { w.WriteHeader(http.StatusForbidden) return } @@ -34,29 +35,3 @@ func noSumWrapper(h http.Handler, host string, patterns []string) http.Handler { h.ServeHTTP(w, r) }) } - -// matchesPattern is adopted from -// https://github.com/golang/go/blob/a11644a26557ea436d456f005f39f4e01902bafe/src/cmd/go/internal/str/path.go#L58 -// this function matches based on path prefixes and -// tries to keep the same behavior as GONOSUMDB and friends -func matchesPattern(pattern, target string) bool { - n := strings.Count(pattern, "/") - prefix := target - for i := 0; i < len(target); i++ { - if target[i] == '/' { - if n == 0 { - prefix = target[:i] - break - } - n-- - } - } - if n > 0 { - return false - } - matched, _ := path.Match(pattern, prefix) - if matched { - return true - } - return false -} diff --git a/pkg/download/mode/mode.go b/pkg/download/mode/mode.go index 8848b484..73c6c598 100644 --- a/pkg/download/mode/mode.go +++ b/pkg/download/mode/mode.go @@ -4,10 +4,10 @@ import ( "encoding/base64" "fmt" "io/ioutil" - "path" "strings" "github.com/gomods/athens/pkg/errors" + "github.com/gomods/athens/pkg/paths" "github.com/hashicorp/hcl2/gohcl" "github.com/hashicorp/hcl2/hclparse" ) @@ -118,7 +118,7 @@ func (d *DownloadFile) validate() error { // exist or match. func (d *DownloadFile) Match(mod string) Mode { for _, p := range d.Paths { - if hasMatch, err := path.Match(p.Pattern, mod); hasMatch && err == nil { + if paths.MatchesPattern(p.Pattern, mod) { return p.Mode } } @@ -130,7 +130,7 @@ func (d *DownloadFile) Match(mod string) Mode { // the top level downloadURL is returned. func (d *DownloadFile) URL(mod string) string { for _, p := range d.Paths { - if hasMatch, err := path.Match(p.Pattern, mod); hasMatch && err == nil { + if paths.MatchesPattern(p.Pattern, mod) { if p.DownloadURL != "" { return p.DownloadURL } diff --git a/pkg/download/mode/mode_test.go b/pkg/download/mode/mode_test.go index bf5ee616..1aea0bde 100644 --- a/pkg/download/mode/mode_test.go +++ b/pkg/download/mode/mode_test.go @@ -36,6 +36,28 @@ var testCases = []struct { input: "github.com/gomods/athens", expectedMode: None, }, + { + name: "multiple depth pattern match", + file: &DownloadFile{ + Mode: Sync, + Paths: []*DownloadPath{ + {Pattern: "github.com/*", Mode: None}, + }, + }, + input: "github.com/gomods/athens/pkg/mode", + expectedMode: None, + }, + { + name: "subdomain pattern match", + file: &DownloadFile{ + Mode: Sync, + Paths: []*DownloadPath{ + {Pattern: "*.github.com/gomods/*", Mode: None}, + }, + }, + input: "athens.github.com/gomods/pkg/mode", + expectedMode: None, + }, { name: "pattern fallback", file: &DownloadFile{ diff --git a/pkg/paths/path.go b/pkg/paths/path.go index d5fba01d..d8109384 100644 --- a/pkg/paths/path.go +++ b/pkg/paths/path.go @@ -2,6 +2,8 @@ package paths import ( "net/http" + "path" + "strings" "github.com/gomods/athens/pkg/errors" "github.com/gorilla/mux" @@ -50,3 +52,28 @@ func GetAllParams(r *http.Request) (*AllPathParams, error) { return &AllPathParams{Module: mod, Version: version}, nil } + +// MatchesPattern reports whether the path prefix of target matches +// pattern (as defined by path.Match) +// +// This tries to keep the same behavior as GOPRIVATE/GONOPROXY/GONOSUMDB, +// and is adopted from: +// https://github.com/golang/go/blob/a11644a26557ea436d456f005f39f4e01902bafe/src/cmd/go/internal/str/path.go#L58 +func MatchesPattern(pattern, target string) bool { + n := strings.Count(pattern, "/") + prefix := target + for i := 0; i < len(target); i++ { + if target[i] == '/' { + if n == 0 { + prefix = target[:i] + break + } + n-- + } + } + if n > 0 { + return false + } + matched, _ := path.Match(pattern, prefix) + return matched +} diff --git a/pkg/paths/path_test.go b/pkg/paths/path_test.go new file mode 100644 index 00000000..1514c98f --- /dev/null +++ b/pkg/paths/path_test.go @@ -0,0 +1,105 @@ +package paths + +import ( + "fmt" + "strings" + "testing" +) + +func TestMatchesPattern(t *testing.T) { + type args struct { + pattern string + name string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "standard match", + args: args{ + pattern: "example.com/*", + name: "example.com/athens", + }, + want: true, + }, + { + name: "mutiple depth match", + args: args{ + pattern: "example.com/*", + name: "example.com/athens/pkg", + }, + want: true, + }, + { + name: "subdomain match", + args: args{ + pattern: "*.example.com/*", + name: "go.example.com/athens/pkg", + }, + want: true, + }, + { + name: "subdirectory exact match", + args: args{ + pattern: "*.example.com/mod", + name: "go.example.com/mod/example", + }, + want: true, + }, + { + name: "subdirectory mismatch", + args: args{ + pattern: "*.example.com/mod", + name: "go.example.com/pkg/example", + }, + want: false, + }, + { + name: "shorter name mismatch", + args: args{ + pattern: "*.example.com/mod/pkg", + name: "go.example.com/pkg", + }, + want: false, + }, + { + name: "no subdirectory mismatch", + args: args{ + pattern: "*.example.com/mod/pkg", + name: "go.example.com/pkg", + }, + want: false, + }, + { + name: "bad pattern", + args: args{ + pattern: "[]a]", + name: "go.example.com/pkg", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := MatchesPattern(tt.args.pattern, tt.args.name) + if got != tt.want { + t.Errorf("MatchGlobPattern() = %v, want %v", got, tt.want) + } + }) + } +} + +func BenchmarkMatchesPattern(b *testing.B) { + for i := 1; i < 5; i++ { + target := "git.example.com" + strings.Repeat("/path", i) + "/pkg" + b.Run(fmt.Sprintf("MatchPattern/%d", i), func(b *testing.B) { + for n := 0; n < b.N; n++ { + if !MatchesPattern("*.example.com/*", target) { + b.Error("mismatch") + } + } + }) + } +}