VictoriaMetrics/lib/promscrape/client_test.go
Nikolay 08cbbf8134
lib/promscrape: fixes proxy autorization (#6783)
* Adds custom dial func for HTTP-Connect and socks5 proxy tunnels.
  Standard golang http.transport exposes GetProxyConnectHeader function,
  but it doesn't allow to use separate tls config for proxy.
  It also not possible to enforce HTTP-Connect with standard http lib.
* For http scrape targets, by default http.Transport.Proxy function must
  be used. Since it has special case with full uri forward.
* Adds proxy.URL json methods that allow to properly copy internal
fields, like User/Password.
It should fix bug with proxy_url. When credentials specified at URL was
ignored.
* Adds tests for scrape client proxy requests

related issue https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6771
2024-08-19 22:50:39 +02:00

189 lines
5.6 KiB
Go

package promscrape
import (
"context"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
)
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func proxyTunnel(w http.ResponseWriter, r *http.Request) {
transfer := func(src io.ReadCloser, dst io.WriteCloser) {
defer dst.Close()
defer src.Close()
io.Copy(dst, src) //nolint
}
destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
}
go transfer(clientConn, destConn)
transfer(destConn, clientConn)
}
type testProxyServer struct {
ba *promauth.BasicAuthConfig
receivedProxyRequest bool
}
func checkBasicAuthHeader(w http.ResponseWriter, headerValue string, ba *promauth.BasicAuthConfig) bool {
userPasswordEncoded := base64.StdEncoding.EncodeToString([]byte(ba.Username + ":" + ba.Password.String()))
expectedAuthValue := "Basic " + userPasswordEncoded
if headerValue != expectedAuthValue {
w.WriteHeader(403)
fmt.Fprintf(w, "Proxy Requires authorization got header value=%q, want=%q", headerValue, expectedAuthValue)
return false
}
return true
}
func (tps *testProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
tps.receivedProxyRequest = true
if tps.ba != nil {
if !checkBasicAuthHeader(w, r.Header.Get("Proxy-Authorization"), tps.ba) {
return
}
}
if r.Method == http.MethodConnect {
proxyTunnel(w, r)
return
}
resp, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body) //nolint
}
func newClientTestServer(useTLS bool, rh http.Handler) *httptest.Server {
var s *httptest.Server
if useTLS {
s = httptest.NewTLSServer(rh)
} else {
s = httptest.NewServer(rh)
}
return s
}
func newTestAuthConfig(t *testing.T, isTLS bool, ba *promauth.BasicAuthConfig) *promauth.Config {
a := promauth.Options{
BasicAuth: ba,
}
if isTLS {
a.TLSConfig = &promauth.TLSConfig{InsecureSkipVerify: true}
}
ac, err := a.NewConfig()
if err != nil {
t.Fatalf("cannot setup promauth.Confg: %s", err)
}
return ac
}
func TestClientProxyReadOk(t *testing.T) {
ctx := context.Background()
f := func(isBackendTLS, isProxyTLS bool, backendAuth, proxyAuth *promauth.BasicAuthConfig) {
t.Helper()
proxyHandler := &testProxyServer{ba: proxyAuth}
ps := newClientTestServer(isProxyTLS, proxyHandler)
expectedBackendResponse := `metric_name{key="value"} 123\n`
backend := newClientTestServer(isBackendTLS, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if backendAuth != nil && !checkBasicAuthHeader(w, r.Header.Get("Authorization"), backendAuth) {
return
}
w.Write([]byte(expectedBackendResponse))
}))
defer backend.Close()
defer ps.Close()
c, err := newClient(ctx, &ScrapeWork{
ScrapeURL: backend.URL,
ProxyURL: proxy.MustNewURL(ps.URL),
ScrapeTimeout: 2 * time.Second,
AuthConfig: newTestAuthConfig(t, isBackendTLS, backendAuth),
ProxyAuthConfig: newTestAuthConfig(t, isProxyTLS, proxyAuth),
MaxScrapeSize: 16000,
})
if err != nil {
t.Fatalf("failed to create client: %s", err)
}
var bb bytesutil.ByteBuffer
if err := c.ReadData(&bb); err != nil {
t.Fatalf("unexpected error at ReadData: %s", err)
}
got, err := io.ReadAll(bb.NewReader())
if err != nil {
t.Fatalf("err read: %s", err)
}
if !proxyHandler.receivedProxyRequest {
t.Fatalf("proxy server didn't recieved request")
}
if string(got) != expectedBackendResponse {
t.Fatalf("not expected response: ")
}
}
// no tls
f(false, false, nil, nil)
// both tls no auth
f(true, true, nil, nil)
// backend tls, proxy http no auth
f(true, false, nil, nil)
// backend http, proxy tls no auth
f(false, true, nil, nil)
// no tls with auth
f(false, false, &promauth.BasicAuthConfig{Username: "test", Password: promauth.NewSecret("1234")}, &promauth.BasicAuthConfig{Username: "proxy-test"})
// proxy tls and auth
f(false, true, &promauth.BasicAuthConfig{Username: "test", Password: promauth.NewSecret("1234")}, &promauth.BasicAuthConfig{Username: "proxy-test"})
// backend tls and auth
f(true, false, &promauth.BasicAuthConfig{Username: "test", Password: promauth.NewSecret("1234")}, &promauth.BasicAuthConfig{Username: "proxy-test"})
// tls with auth
f(true, true, &promauth.BasicAuthConfig{Username: "test", Password: promauth.NewSecret("1234")}, &promauth.BasicAuthConfig{Username: "proxy-test"})
// tls with backend auth
f(true, true, &promauth.BasicAuthConfig{Username: "test", Password: promauth.NewSecret("1234")}, nil)
// tls with proxy auth
f(true, true, nil, &promauth.BasicAuthConfig{Username: "proxy-test", Password: promauth.NewSecret("1234")})
// proxy tls with backend auth
f(false, true, &promauth.BasicAuthConfig{Username: "test", Password: promauth.NewSecret("1234")}, nil)
// backend tls and proxy auth
f(true, false, nil, &promauth.BasicAuthConfig{Username: "proxy-test", Password: promauth.NewSecret("1234")})
}