app/vmauth: parse url_prefix only once during config load

This commit is contained in:
Aliaksandr Valialkin 2021-04-21 10:55:29 +03:00
parent 6dc5d3b357
commit e7c4fde756
5 changed files with 103 additions and 68 deletions

View file

@ -33,7 +33,7 @@ type UserInfo struct {
BearerToken string `yaml:"bearer_token"` BearerToken string `yaml:"bearer_token"`
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
URLPrefix string `yaml:"url_prefix"` URLPrefix *yamlURL `yaml:"url_prefix"`
URLMap []URLMap `yaml:"url_map"` URLMap []URLMap `yaml:"url_map"`
requests *metrics.Counter requests *metrics.Counter
@ -42,7 +42,7 @@ type UserInfo struct {
// URLMap is a mapping from source paths to target urls. // URLMap is a mapping from source paths to target urls.
type URLMap struct { type URLMap struct {
SrcPaths []*SrcPath `yaml:"src_paths"` SrcPaths []*SrcPath `yaml:"src_paths"`
URLPrefix string `yaml:"url_prefix"` URLPrefix *yamlURL `yaml:"url_prefix"`
} }
// SrcPath represents an src path // SrcPath represents an src path
@ -51,6 +51,27 @@ type SrcPath struct {
re *regexp.Regexp re *regexp.Regexp
} }
type yamlURL struct {
u *url.URL
}
func (yu *yamlURL) UnmarshalYAML(f func(interface{}) error) error {
var s string
if err := f(&s); err != nil {
return err
}
u, err := url.Parse(s)
if err != nil {
return fmt.Errorf("cannot unmarshal %q into url: %w", s, err)
}
yu.u = u
return nil
}
func (yu *yamlURL) MarshalYAML() (interface{}, error) {
return yu.u.String(), nil
}
func (sp *SrcPath) match(s string) bool { func (sp *SrcPath) match(s string) bool {
prefix, ok := sp.re.LiteralPrefix() prefix, ok := sp.re.LiteralPrefix()
if ok { if ok {
@ -173,24 +194,27 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
if byAuthToken[authToken] != nil { if byAuthToken[authToken] != nil {
return nil, fmt.Errorf("duplicate auth token found for bearer_token=%q, username=%q: %q", authToken, ui.BearerToken, ui.Username) return nil, fmt.Errorf("duplicate auth token found for bearer_token=%q, username=%q: %q", authToken, ui.BearerToken, ui.Username)
} }
if len(ui.URLPrefix) > 0 { if ui.URLPrefix != nil {
urlPrefix, err := sanitizeURLPrefix(ui.URLPrefix) urlPrefix, err := sanitizeURLPrefix(ui.URLPrefix.u)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ui.URLPrefix = urlPrefix ui.URLPrefix.u = urlPrefix
} }
for _, e := range ui.URLMap { for _, e := range ui.URLMap {
if len(e.SrcPaths) == 0 { if len(e.SrcPaths) == 0 {
return nil, fmt.Errorf("missing `src_paths`") return nil, fmt.Errorf("missing `src_paths` in `url_map`")
} }
urlPrefix, err := sanitizeURLPrefix(e.URLPrefix) if e.URLPrefix == nil {
return nil, fmt.Errorf("missing `url_prefix` in `url_map`")
}
urlPrefix, err := sanitizeURLPrefix(e.URLPrefix.u)
if err != nil { if err != nil {
return nil, err return nil, err
} }
e.URLPrefix = urlPrefix e.URLPrefix.u = urlPrefix
} }
if len(ui.URLMap) == 0 && len(ui.URLPrefix) == 0 { if len(ui.URLMap) == 0 && ui.URLPrefix == nil {
return nil, fmt.Errorf("missing `url_prefix`") return nil, fmt.Errorf("missing `url_prefix`")
} }
if ui.BearerToken != "" { if ui.BearerToken != "" {
@ -218,21 +242,17 @@ func getAuthToken(bearerToken, username, password string) string {
return "Basic " + token64 return "Basic " + token64
} }
func sanitizeURLPrefix(urlPrefix string) (string, error) { func sanitizeURLPrefix(urlPrefix *url.URL) (*url.URL, error) {
// Remove trailing '/' from urlPrefix // Remove trailing '/' from urlPrefix
for strings.HasSuffix(urlPrefix, "/") { for strings.HasSuffix(urlPrefix.Path, "/") {
urlPrefix = urlPrefix[:len(urlPrefix)-1] urlPrefix.Path = urlPrefix.Path[:len(urlPrefix.Path)-1]
} }
// Validate urlPrefix // Validate urlPrefix
target, err := url.Parse(urlPrefix) if urlPrefix.Scheme != "http" && urlPrefix.Scheme != "https" {
if err != nil { return nil, fmt.Errorf("unsupported scheme for `url_prefix: %q`: %q; must be `http` or `https`", urlPrefix, urlPrefix.Scheme)
return "", fmt.Errorf("invalid `url_prefix: %q`: %w", urlPrefix, err)
} }
if target.Scheme != "http" && target.Scheme != "https" { if urlPrefix.Host == "" {
return "", fmt.Errorf("unsupported scheme for `url_prefix: %q`: %q; must be `http` or `https`", urlPrefix, target.Scheme) return nil, fmt.Errorf("missing hostname in `url_prefix %q`", urlPrefix.Host)
}
if target.Host == "" {
return "", fmt.Errorf("missing hostname in `url_prefix %q`", urlPrefix)
} }
return urlPrefix, nil return urlPrefix, nil
} }

View file

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"net/url"
"regexp" "regexp"
"testing" "testing"
@ -55,6 +56,11 @@ users:
- username: foo - username: foo
url_prefix: http:///bar url_prefix: http:///bar
`) `)
f(`
users:
- username: foo
url_prefix: [bar]
`)
// Username and bearer_token in a single config // Username and bearer_token in a single config
f(` f(`
@ -102,6 +108,15 @@ users:
- src_paths: ["/foo/bar"] - src_paths: ["/foo/bar"]
`) `)
// Invalid url_prefix in url_map
f(`
users:
- username: a
url_map:
- src_paths: ["/foo/bar"]
url_prefix: foo.bar
`)
// Missing src_paths in url_map // Missing src_paths in url_map
f(` f(`
users: users:
@ -143,7 +158,7 @@ users:
getAuthToken("", "foo", "bar"): { getAuthToken("", "foo", "bar"): {
Username: "foo", Username: "foo",
Password: "bar", Password: "bar",
URLPrefix: "http://aaa:343/bbb", URLPrefix: mustParseURL("http://aaa:343/bbb"),
}, },
}) })
@ -157,11 +172,11 @@ users:
`, map[string]*UserInfo{ `, map[string]*UserInfo{
getAuthToken("", "foo", ""): { getAuthToken("", "foo", ""): {
Username: "foo", Username: "foo",
URLPrefix: "http://foo", URLPrefix: mustParseURL("http://foo"),
}, },
getAuthToken("", "bar", ""): { getAuthToken("", "bar", ""): {
Username: "bar", Username: "bar",
URLPrefix: "https://bar/x", URLPrefix: mustParseURL("https://bar/x"),
}, },
}) })
@ -180,11 +195,11 @@ users:
URLMap: []URLMap{ URLMap: []URLMap{
{ {
SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}), SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),
URLPrefix: "http://vmselect/select/0/prometheus", URLPrefix: mustParseURL("http://vmselect/select/0/prometheus"),
}, },
{ {
SrcPaths: getSrcPaths([]string{"/api/v1/write"}), SrcPaths: getSrcPaths([]string{"/api/v1/write"}),
URLPrefix: "http://vminsert/insert/0/prometheus", URLPrefix: mustParseURL("http://vminsert/insert/0/prometheus"),
}, },
}, },
}, },
@ -222,3 +237,13 @@ func areEqualConfigs(a, b map[string]*UserInfo) error {
} }
return nil return nil
} }
func mustParseURL(u string) *yamlURL {
pu, err := url.Parse(u)
if err != nil {
panic(fmt.Errorf("BUG: cannot parse %q: %w", u, err))
}
return &yamlURL{
u: pu,
}
}

View file

@ -65,11 +65,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
httpserver.Errorf(w, r, "cannot determine targetURL: %s", err) httpserver.Errorf(w, r, "cannot determine targetURL: %s", err)
return true return true
} }
if _, err := url.Parse(targetURL); err != nil { r.Header.Set("vm-target-url", targetURL.String())
httpserver.Errorf(w, r, "invalid targetURL=%q: %s", targetURL, err)
return true
}
r.Header.Set("vm-target-url", targetURL)
reverseProxy.ServeHTTP(w, r) reverseProxy.ServeHTTP(w, r)
return true return true
} }

View file

@ -7,37 +7,31 @@ import (
"strings" "strings"
) )
func mergeURLs(uiURL string, requestURI *url.URL) (string, error) { func mergeURLs(uiURL, requestURI *url.URL) *url.URL {
prefixURL, err := url.Parse(uiURL) targetURL := *uiURL
if err != nil { targetURL.Path += requestURI.Path
return "", fmt.Errorf("BUG - cannot parse userInfo url: %q, err: %w", uiURL, err)
}
prefixURL.Path += requestURI.Path
requestParams := requestURI.Query() requestParams := requestURI.Query()
// fast path // fast path
if len(requestParams) == 0 { if len(requestParams) == 0 {
return prefixURL.String(), nil return &targetURL
} }
// merge query parameters from requests. // merge query parameters from requests.
userInfoParams := prefixURL.Query() uiParams := targetURL.Query()
for k, v := range requestParams { for k, v := range requestParams {
// skip clashed query params from original request // skip clashed query params from original request
if exist := userInfoParams.Get(k); len(exist) > 0 { if exist := uiParams.Get(k); len(exist) > 0 {
continue continue
} }
for i := range v { for i := range v {
userInfoParams.Add(k, v[i]) uiParams.Add(k, v[i])
} }
} }
prefixURL.RawQuery = userInfoParams.Encode() targetURL.RawQuery = uiParams.Encode()
return prefixURL.String(), nil return &targetURL
} }
func createTargetURL(ui *UserInfo, uOrig *url.URL) (string, error) { func createTargetURL(ui *UserInfo, uOrig *url.URL) (*url.URL, error) {
u, err := url.Parse(uOrig.String()) u := *uOrig
if err != nil {
return "", fmt.Errorf("cannot make a copy of %q: %w", u, err)
}
// Prevent from attacks with using `..` in r.URL.Path // Prevent from attacks with using `..` in r.URL.Path
u.Path = path.Clean(u.Path) u.Path = path.Clean(u.Path)
if !strings.HasPrefix(u.Path, "/") { if !strings.HasPrefix(u.Path, "/") {
@ -46,12 +40,12 @@ func createTargetURL(ui *UserInfo, uOrig *url.URL) (string, error) {
for _, e := range ui.URLMap { for _, e := range ui.URLMap {
for _, sp := range e.SrcPaths { for _, sp := range e.SrcPaths {
if sp.match(u.Path) { if sp.match(u.Path) {
return mergeURLs(e.URLPrefix, u) return mergeURLs(e.URLPrefix.u, &u), nil
} }
} }
} }
if len(ui.URLPrefix) > 0 { if ui.URLPrefix != nil {
return mergeURLs(ui.URLPrefix, u) return mergeURLs(ui.URLPrefix.u, &u), nil
} }
return "", fmt.Errorf("missing route for %q", u) return nil, fmt.Errorf("missing route for %q", u.String())
} }

View file

@ -16,28 +16,28 @@ func TestCreateTargetURLSuccess(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
if target != expectedTarget { if target.String() != expectedTarget {
t.Fatalf("unexpected target; got %q; want %q", target, expectedTarget) t.Fatalf("unexpected target; got %q; want %q", target, expectedTarget)
} }
} }
// Simple routing with `url_prefix` // Simple routing with `url_prefix`
f(&UserInfo{ f(&UserInfo{
URLPrefix: "http://foo.bar", URLPrefix: mustParseURL("http://foo.bar"),
}, "", "http://foo.bar/.") }, "", "http://foo.bar/.")
f(&UserInfo{ f(&UserInfo{
URLPrefix: "http://foo.bar", URLPrefix: mustParseURL("http://foo.bar"),
}, "/", "http://foo.bar/") }, "/", "http://foo.bar/")
f(&UserInfo{ f(&UserInfo{
URLPrefix: "http://foo.bar", URLPrefix: mustParseURL("http://foo.bar"),
}, "a/b?c=d", "http://foo.bar/a/b?c=d") }, "a/b?c=d", "http://foo.bar/a/b?c=d")
f(&UserInfo{ f(&UserInfo{
URLPrefix: "https://sss:3894/x/y", URLPrefix: mustParseURL("https://sss:3894/x/y"),
}, "/z", "https://sss:3894/x/y/z") }, "/z", "https://sss:3894/x/y/z")
f(&UserInfo{ f(&UserInfo{
URLPrefix: "https://sss:3894/x/y", URLPrefix: mustParseURL("https://sss:3894/x/y"),
}, "/../../aaa", "https://sss:3894/x/y/aaa") }, "/../../aaa", "https://sss:3894/x/y/aaa")
f(&UserInfo{ f(&UserInfo{
URLPrefix: "https://sss:3894/x/y", URLPrefix: mustParseURL("https://sss:3894/x/y"),
}, "/./asd/../../aaa?a=d&s=s/../d", "https://sss:3894/x/y/aaa?a=d&s=s%2F..%2Fd") }, "/./asd/../../aaa?a=d&s=s/../d", "https://sss:3894/x/y/aaa?a=d&s=s%2F..%2Fd")
// Complex routing with `url_map` // Complex routing with `url_map`
@ -45,14 +45,14 @@ func TestCreateTargetURLSuccess(t *testing.T) {
URLMap: []URLMap{ URLMap: []URLMap{
{ {
SrcPaths: getSrcPaths([]string{"/api/v1/query"}), SrcPaths: getSrcPaths([]string{"/api/v1/query"}),
URLPrefix: "http://vmselect/0/prometheus", URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
}, },
{ {
SrcPaths: getSrcPaths([]string{"/api/v1/write"}), SrcPaths: getSrcPaths([]string{"/api/v1/write"}),
URLPrefix: "http://vminsert/0/prometheus", URLPrefix: mustParseURL("http://vminsert/0/prometheus"),
}, },
}, },
URLPrefix: "http://default-server", URLPrefix: mustParseURL("http://default-server"),
} }
f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up") f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up")
f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write") f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write")
@ -63,14 +63,14 @@ func TestCreateTargetURLSuccess(t *testing.T) {
URLMap: []URLMap{ URLMap: []URLMap{
{ {
SrcPaths: getSrcPaths([]string{"/api/v1/query(_range)?", "/api/v1/label/[^/]+/values"}), SrcPaths: getSrcPaths([]string{"/api/v1/query(_range)?", "/api/v1/label/[^/]+/values"}),
URLPrefix: "http://vmselect/0/prometheus", URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
}, },
{ {
SrcPaths: getSrcPaths([]string{"/api/v1/write"}), SrcPaths: getSrcPaths([]string{"/api/v1/write"}),
URLPrefix: "http://vminsert/0/prometheus", URLPrefix: mustParseURL("http://vminsert/0/prometheus"),
}, },
}, },
URLPrefix: "http://default-server", URLPrefix: mustParseURL("http://default-server"),
} }
f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up") f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up")
f(ui, "/api/v1/query_range?query=up", "http://vmselect/0/prometheus/api/v1/query_range?query=up") f(ui, "/api/v1/query_range?query=up", "http://vmselect/0/prometheus/api/v1/query_range?query=up")
@ -78,10 +78,10 @@ func TestCreateTargetURLSuccess(t *testing.T) {
f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write") f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write")
f(ui, "/api/v1/foo/bar", "http://default-server/api/v1/foo/bar") f(ui, "/api/v1/foo/bar", "http://default-server/api/v1/foo/bar")
f(&UserInfo{ f(&UserInfo{
URLPrefix: "http://foo.bar?extra_label=team=dev", URLPrefix: mustParseURL("http://foo.bar?extra_label=team=dev"),
}, "/api/v1/query", "http://foo.bar/api/v1/query?extra_label=team=dev") }, "/api/v1/query", "http://foo.bar/api/v1/query?extra_label=team=dev")
f(&UserInfo{ f(&UserInfo{
URLPrefix: "http://foo.bar?extra_label=team=mobile", URLPrefix: mustParseURL("http://foo.bar?extra_label=team=mobile"),
}, "/api/v1/query?extra_label=team=dev", "http://foo.bar/api/v1/query?extra_label=team%3Dmobile") }, "/api/v1/query?extra_label=team=dev", "http://foo.bar/api/v1/query?extra_label=team%3Dmobile")
} }
@ -97,7 +97,7 @@ func TestCreateTargetURLFailure(t *testing.T) {
if err == nil { if err == nil {
t.Fatalf("expecting non-nil error") t.Fatalf("expecting non-nil error")
} }
if target != "" { if target != nil {
t.Fatalf("unexpected target=%q; want empty string", target) t.Fatalf("unexpected target=%q; want empty string", target)
} }
} }
@ -106,7 +106,7 @@ func TestCreateTargetURLFailure(t *testing.T) {
URLMap: []URLMap{ URLMap: []URLMap{
{ {
SrcPaths: getSrcPaths([]string{"/api/v1/query"}), SrcPaths: getSrcPaths([]string{"/api/v1/query"}),
URLPrefix: "http://foobar/baz", URLPrefix: mustParseURL("http://foobar/baz"),
}, },
}, },
}, "/api/v1/write") }, "/api/v1/write")