Skip to content

Commit add2db1

Browse files
committed
app/vmauth: properly proxy HTTP requests without body
The Request.Body for requests without body can be nil. This could break readTrackingBody.Read() logic, which could incorrectly return "cannot read data after closing the reader" error in this case. Fix this by initializing the readTrackingBody.r with zeroReader. While at it, properly set Host header if it is specified in 'headers' section. It must be set net/http.Request.Host instead of net/http.Request.Header.Set(), since the net/http.Client overwrites the Host header with the value from req.Host before sending the request. While at it, add tests for requestHandler(). Additional tests for various requestHandler() cases will be added in future commits. Updates VictoriaMetrics#6445 Updates VictoriaMetrics#5707 Updates VictoriaMetrics#5240 Updates VictoriaMetrics#6525
1 parent b2e6462 commit add2db1

File tree

6 files changed

+253
-52
lines changed

6 files changed

+253
-52
lines changed

app/vmauth/auth_config.go

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,6 @@ type UserInfo struct {
8484
concurrencyLimitCh chan struct{}
8585
concurrencyLimitReached *metrics.Counter
8686

87-
// Whether to use backend host header in requests to backend.
88-
useBackendHostHeader bool
89-
9087
rt http.RoundTripper
9188

9289
requests *metrics.Counter
@@ -96,8 +93,9 @@ type UserInfo struct {
9693

9794
// HeadersConf represents config for request and response headers.
9895
type HeadersConf struct {
99-
RequestHeaders []*Header `yaml:"headers,omitempty"`
100-
ResponseHeaders []*Header `yaml:"response_headers,omitempty"`
96+
RequestHeaders []*Header `yaml:"headers,omitempty"`
97+
ResponseHeaders []*Header `yaml:"response_headers,omitempty"`
98+
KeepOriginalHost *bool `yaml:"keep_original_host,omitempty"`
10199
}
102100

103101
func (ui *UserInfo) beginConcurrencyLimit() error {
@@ -152,15 +150,6 @@ func (h *Header) MarshalYAML() (any, error) {
152150
return h.sOriginal, nil
153151
}
154152

155-
func hasEmptyHostHeader(headers []*Header) bool {
156-
for _, h := range headers {
157-
if h.Name == "Host" && h.Value == "" {
158-
return true
159-
}
160-
}
161-
return false
162-
}
163-
164153
// URLMap is a mapping from source paths to target urls.
165154
type URLMap struct {
166155
// SrcPaths is an optional list of regular expressions, which must match the request path.
@@ -608,7 +597,7 @@ func initAuthConfig() {
608597
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240
609598
sighupCh := procutil.NewSighupChan()
610599

611-
_, err := loadAuthConfig()
600+
_, err := reloadAuthConfig()
612601
if err != nil {
613602
logger.Fatalf("cannot load auth config: %s", err)
614603
}
@@ -640,7 +629,7 @@ func authConfigReloader(sighupCh <-chan os.Signal) {
640629

641630
updateFn := func() {
642631
configReloads.Inc()
643-
updated, err := loadAuthConfig()
632+
updated, err := reloadAuthConfig()
644633
if err != nil {
645634
logger.Errorf("failed to load auth config; using the last successfully loaded config; error: %s", err)
646635
configSuccess.Set(0)
@@ -666,27 +655,45 @@ func authConfigReloader(sighupCh <-chan os.Signal) {
666655
}
667656
}
668657

669-
// authConfigData stores the yaml definition for this config.
670-
// authConfigData needs to be updated each time authConfig is updated.
671-
var authConfigData atomic.Pointer[[]byte]
672-
673658
var (
674-
authConfig atomic.Pointer[AuthConfig]
675-
authUsers atomic.Pointer[map[string]*UserInfo]
659+
// authConfigData stores the yaml definition for this config.
660+
// authConfigData needs to be updated each time authConfig is updated.
661+
authConfigData atomic.Pointer[[]byte]
662+
663+
// authConfig contains the currently loaded auth config
664+
authConfig atomic.Pointer[AuthConfig]
665+
666+
// authUsers contains the currently loaded auth users
667+
authUsers atomic.Pointer[map[string]*UserInfo]
668+
676669
authConfigWG sync.WaitGroup
677670
stopCh chan struct{}
678671
)
679672

680-
// loadAuthConfig loads and applies the config from *authConfigPath.
673+
// reloadAuthConfig loads and applies the config from *authConfigPath.
681674
// It returns bool value to identify if new config was applied.
682675
// The config can be not applied if there is a parsing error
683676
// or if there are no changes to the current authConfig.
684-
func loadAuthConfig() (bool, error) {
677+
func reloadAuthConfig() (bool, error) {
685678
data, err := fscore.ReadFileOrHTTP(*authConfigPath)
686679
if err != nil {
687680
return false, fmt.Errorf("failed to read -auth.config=%q: %w", *authConfigPath, err)
688681
}
689682

683+
ok, err := reloadAuthConfigData(data)
684+
if err != nil {
685+
return false, fmt.Errorf("failed to pars -auth.config=%q: %w", *authConfigPath, err)
686+
}
687+
if !ok {
688+
return false, nil
689+
}
690+
691+
mp := authUsers.Load()
692+
logger.Infof("loaded information about %d users from -auth.config=%q", len(*mp), *authConfigPath)
693+
return true, nil
694+
}
695+
696+
func reloadAuthConfigData(data []byte) (bool, error) {
690697
oldData := authConfigData.Load()
691698
if oldData != nil && bytes.Equal(data, *oldData) {
692699
// there are no updates in the config - skip reloading.
@@ -695,14 +702,13 @@ func loadAuthConfig() (bool, error) {
695702

696703
ac, err := parseAuthConfig(data)
697704
if err != nil {
698-
return false, fmt.Errorf("failed to parse -auth.config=%q: %w", *authConfigPath, err)
705+
return false, fmt.Errorf("failed to parse auth config: %w", err)
699706
}
700707

701708
m, err := parseAuthConfigUsers(ac)
702709
if err != nil {
703-
return false, fmt.Errorf("failed to parse users from -auth.config=%q: %w", *authConfigPath, err)
710+
return false, fmt.Errorf("failed to parse users from auth config: %w", err)
704711
}
705-
logger.Infof("loaded information about %d users from -auth.config=%q", len(m), *authConfigPath)
706712

707713
acPrev := authConfig.Load()
708714
if acPrev != nil {
@@ -749,7 +755,6 @@ func parseAuthConfig(data []byte) (*AuthConfig, error) {
749755
if err := ui.initURLs(); err != nil {
750756
return nil, err
751757
}
752-
ui.useBackendHostHeader = hasEmptyHostHeader(ui.HeadersConf.RequestHeaders)
753758

754759
metricLabels, err := ui.getMetricLabels()
755760
if err != nil {
@@ -814,7 +819,6 @@ func parseAuthConfigUsers(ac *AuthConfig) (map[string]*UserInfo, error) {
814819
_ = ac.ms.GetOrCreateGauge(`vmauth_user_concurrent_requests_current`+metricLabels, func() float64 {
815820
return float64(len(ui.concurrencyLimitCh))
816821
})
817-
ui.useBackendHostHeader = hasEmptyHostHeader(ui.HeadersConf.RequestHeaders)
818822

819823
rt, err := newRoundTripper(ui.TLSCAFile, ui.TLSCertFile, ui.TLSKeyFile, ui.TLSServerName, ui.TLSInsecureSkipVerify)
820824
if err != nil {

app/vmauth/auth_config_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ users:
8282
headers: foobar
8383
`)
8484

85+
// Invalid keep_original_host value
86+
f(`
87+
users:
88+
- username: foo
89+
url_prefix: http://foo.bar
90+
keep_original_host: foobar
91+
`)
92+
8593
// empty url_prefix
8694
f(`
8795
users:
@@ -458,6 +466,7 @@ users:
458466
})
459467

460468
// with default url
469+
keepOriginalHost := true
461470
f(`
462471
users:
463472
- bearer_token: foo
@@ -469,6 +478,7 @@ users:
469478
headers:
470479
- "foo: bar"
471480
- "xxx: y"
481+
keep_original_host: true
472482
default_url:
473483
- http://default1/select/0/prometheus
474484
- http://default2/select/0/prometheus
@@ -491,6 +501,7 @@ users:
491501
mustNewHeader("'foo: bar'"),
492502
mustNewHeader("'xxx: y'"),
493503
},
504+
KeepOriginalHost: &keepOriginalHost,
494505
},
495506
},
496507
},
@@ -517,6 +528,7 @@ users:
517528
mustNewHeader("'foo: bar'"),
518529
mustNewHeader("'xxx: y'"),
519530
},
531+
KeepOriginalHost: &keepOriginalHost,
520532
},
521533
},
522534
},
@@ -536,12 +548,17 @@ users:
536548
metric_labels:
537549
dc: eu
538550
team: dev
551+
keep_original_host: true
539552
- username: foo-same
540553
password: bar
541554
url_prefix: https://bar/x
542555
metric_labels:
543556
backend_env: test
544557
team: accounting
558+
headers:
559+
- "foo: bar"
560+
response_headers:
561+
- "Abc: def"
545562
`, map[string]*UserInfo{
546563
getHTTPAuthBasicToken("foo-same", "baz"): {
547564
Username: "foo-same",
@@ -551,6 +568,9 @@ users:
551568
"dc": "eu",
552569
"team": "dev",
553570
},
571+
HeadersConf: HeadersConf{
572+
KeepOriginalHost: &keepOriginalHost,
573+
},
554574
},
555575
getHTTPAuthBasicToken("foo-same", "bar"): {
556576
Username: "foo-same",
@@ -560,6 +580,14 @@ users:
560580
"backend_env": "test",
561581
"team": "accounting",
562582
},
583+
HeadersConf: HeadersConf{
584+
RequestHeaders: []*Header{
585+
mustNewHeader("'foo: bar'"),
586+
},
587+
ResponseHeaders: []*Header{
588+
mustNewHeader("'Abc: def'"),
589+
},
590+
},
563591
},
564592
})
565593
}

app/vmauth/main.go

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -236,18 +236,18 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
236236
}
237237

238238
func tryProcessingRequest(w http.ResponseWriter, r *http.Request, targetURL *url.URL, hc HeadersConf, retryStatusCodes []int, ui *UserInfo) bool {
239-
// This code has been copied from net/http/httputil/reverseproxy.go
240239
req := sanitizeRequestHeaders(r)
241-
req.URL = targetURL
242240

243-
if req.URL.Scheme == "https" || ui.useBackendHostHeader {
244-
// Override req.Host only for https requests, since https server verifies hostnames during TLS handshake,
245-
// so it expects the targetURL.Host in the request.
246-
// There is no need in overriding the req.Host for http requests, since it is expected that backend server
247-
// may properly process queries with the original req.Host.
248-
req.Host = targetURL.Host
249-
}
241+
req.URL = targetURL
250242
updateHeadersByConfig(req.Header, hc.RequestHeaders)
243+
if hc.KeepOriginalHost == nil || !*hc.KeepOriginalHost {
244+
if host := getHostHeader(hc.RequestHeaders); host != "" {
245+
req.Host = host
246+
} else {
247+
req.Host = targetURL.Host
248+
}
249+
}
250+
251251
var trivialRetries int
252252
rtb, rtbOK := req.Body.(*readTrackingBody)
253253
again:
@@ -338,12 +338,21 @@ func copyHeader(dst, src http.Header) {
338338
}
339339
}
340340

341-
func updateHeadersByConfig(headers http.Header, config []*Header) {
342-
for _, h := range config {
341+
func getHostHeader(headers []*Header) string {
342+
for _, h := range headers {
343+
if h.Name == "Host" {
344+
return h.Value
345+
}
346+
}
347+
return ""
348+
}
349+
350+
func updateHeadersByConfig(dst http.Header, src []*Header) {
351+
for _, h := range src {
343352
if h.Value == "" {
344-
headers.Del(h.Name)
353+
dst.Del(h.Name)
345354
} else {
346-
headers.Set(h.Name, h.Value)
355+
dst.Set(h.Name, h.Value)
347356
}
348357
}
349358
}
@@ -537,10 +546,24 @@ func getReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
537546
maxBodySize = 0
538547
}
539548
rtb.maxBodySize = maxBodySize
549+
550+
if r == nil {
551+
// This is GET request without request body
552+
r = (*zeroReader)(nil)
553+
}
540554
rtb.r = r
541555
return rtb
542556
}
543557

558+
type zeroReader struct{}
559+
560+
func (r *zeroReader) Read(_ []byte) (int, error) {
561+
return 0, io.EOF
562+
}
563+
func (r *zeroReader) Close() error {
564+
return nil
565+
}
566+
544567
func putReadTrackingBody(rtb *readTrackingBody) {
545568
rtb.reset()
546569
readTrackingBodyPool.Put(rtb)
@@ -560,7 +583,7 @@ func (rtb *readTrackingBody) Read(p []byte) (int, error) {
560583
if rtb.bufComplete {
561584
return 0, io.EOF
562585
}
563-
return 0, fmt.Errorf("cannot read data after closing the reader")
586+
return 0, fmt.Errorf("cannot read client request body after closing client reader")
564587
}
565588

566589
n, err := rtb.r.Read(p)

0 commit comments

Comments
 (0)