prometheusmetricshost-metricsmachine-metricsnode-metricsprocfsprometheus-exportersystem-informationsystem-metrics
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
577 lines
16 KiB
577 lines
16 KiB
// Copyright 2019 The Prometheus Authors |
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
|
// you may not use this file except in compliance with the License. |
|
// You may obtain a copy of the License at |
|
// |
|
// http://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// Unless required by applicable law or agreed to in writing, software |
|
// distributed under the License is distributed on an "AS IS" BASIS, |
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
// See the License for the specific language governing permissions and |
|
// limitations under the License. |
|
|
|
// +build go1.14 |
|
|
|
package https |
|
|
|
import ( |
|
"crypto/tls" |
|
"crypto/x509" |
|
"errors" |
|
"fmt" |
|
"io/ioutil" |
|
"net" |
|
"net/http" |
|
"regexp" |
|
"sync" |
|
"testing" |
|
"time" |
|
) |
|
|
|
var ( |
|
port = getPort() |
|
testlogger = &testLogger{} |
|
|
|
ErrorMap = map[string]*regexp.Regexp{ |
|
"HTTP Response to HTTPS": regexp.MustCompile(`server gave HTTP response to HTTPS client`), |
|
"No such file": regexp.MustCompile(`no such file`), |
|
"Invalid argument": regexp.MustCompile(`invalid argument`), |
|
"YAML error": regexp.MustCompile(`yaml`), |
|
"Invalid ClientAuth": regexp.MustCompile(`invalid ClientAuth`), |
|
"TLS handshake": regexp.MustCompile(`tls`), |
|
"HTTP Request to HTTPS server": regexp.MustCompile(`HTTP`), |
|
"Invalid CertPath": regexp.MustCompile(`missing cert_file`), |
|
"Invalid KeyPath": regexp.MustCompile(`missing key_file`), |
|
"ClientCA set without policy": regexp.MustCompile(`Client CA's have been configured without a Client Auth Policy`), |
|
"Bad password": regexp.MustCompile(`hashedSecret too short to be a bcrypted password`), |
|
"Unauthorized": regexp.MustCompile(`Unauthorized`), |
|
"Forbidden": regexp.MustCompile(`Forbidden`), |
|
"Handshake failure": regexp.MustCompile(`handshake failure`), |
|
"Unknown cipher": regexp.MustCompile(`unknown cipher`), |
|
"Unknown curve": regexp.MustCompile(`unknown curve`), |
|
"Unknown TLS version": regexp.MustCompile(`unknown TLS version`), |
|
"No HTTP2 cipher": regexp.MustCompile(`TLSConfig.CipherSuites is missing an HTTP/2-required`), |
|
"Incompatible TLS version": regexp.MustCompile(`protocol version not supported`), |
|
} |
|
) |
|
|
|
type testLogger struct{} |
|
|
|
func (t *testLogger) Log(keyvals ...interface{}) error { |
|
return nil |
|
} |
|
|
|
func getPort() string { |
|
listener, err := net.Listen("tcp", ":0") |
|
if err != nil { |
|
panic(err) |
|
} |
|
defer listener.Close() |
|
p := listener.Addr().(*net.TCPAddr).Port |
|
return fmt.Sprintf(":%v", p) |
|
} |
|
|
|
type TestInputs struct { |
|
Name string |
|
Server func() *http.Server |
|
UseNilServer bool |
|
YAMLConfigPath string |
|
ExpectedError *regexp.Regexp |
|
UseTLSClient bool |
|
ClientMaxTLSVersion uint16 |
|
CipherSuites []uint16 |
|
ActualCipher uint16 |
|
CurvePreferences []tls.CurveID |
|
Username string |
|
Password string |
|
} |
|
|
|
func TestYAMLFiles(t *testing.T) { |
|
testTables := []*TestInputs{ |
|
{ |
|
Name: `path to config yml invalid`, |
|
YAMLConfigPath: "somefile", |
|
ExpectedError: ErrorMap["No such file"], |
|
}, |
|
{ |
|
Name: `empty config yml`, |
|
YAMLConfigPath: "testdata/tls_config_empty.yml", |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `invalid config yml (invalid structure)`, |
|
YAMLConfigPath: "testdata/tls_config_junk.yml", |
|
ExpectedError: ErrorMap["YAML error"], |
|
}, |
|
{ |
|
Name: `invalid config yml (invalid key)`, |
|
YAMLConfigPath: "testdata/tls_config_junk_key.yml", |
|
ExpectedError: ErrorMap["YAML error"], |
|
}, |
|
{ |
|
Name: `invalid config yml (cert path empty)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_certPath_empty.bad.yml", |
|
ExpectedError: ErrorMap["Invalid CertPath"], |
|
}, |
|
{ |
|
Name: `invalid config yml (key path empty)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_keyPath_empty.bad.yml", |
|
ExpectedError: ErrorMap["Invalid KeyPath"], |
|
}, |
|
{ |
|
Name: `invalid config yml (cert path and key path empty)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_certPath_keyPath_empty.bad.yml", |
|
ExpectedError: ErrorMap["Invalid CertPath"], |
|
}, |
|
{ |
|
Name: `invalid config yml (cert path invalid)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_certPath_invalid.bad.yml", |
|
ExpectedError: ErrorMap["No such file"], |
|
}, |
|
{ |
|
Name: `invalid config yml (key path invalid)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_keyPath_invalid.bad.yml", |
|
ExpectedError: ErrorMap["No such file"], |
|
}, |
|
{ |
|
Name: `invalid config yml (cert path and key path invalid)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_certPath_keyPath_invalid.bad.yml", |
|
ExpectedError: ErrorMap["No such file"], |
|
}, |
|
{ |
|
Name: `invalid config yml (invalid ClientAuth)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth.bad.yml", |
|
ExpectedError: ErrorMap["ClientCA set without policy"], |
|
}, |
|
{ |
|
Name: `invalid config yml (invalid ClientCAs filepath)`, |
|
YAMLConfigPath: "testdata/tls_config_auth_clientCAs_invalid.bad.yml", |
|
ExpectedError: ErrorMap["No such file"], |
|
}, |
|
{ |
|
Name: `invalid config yml (invalid user list)`, |
|
YAMLConfigPath: "testdata/tls_config_auth_user_list_invalid.bad.yml", |
|
ExpectedError: ErrorMap["Bad password"], |
|
}, |
|
{ |
|
Name: `invalid config yml (bad cipher)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_inventedCiphers.bad.yml", |
|
ExpectedError: ErrorMap["Unknown cipher"], |
|
}, |
|
{ |
|
Name: `invalid config yml (bad curves)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_inventedCurves.bad.yml", |
|
ExpectedError: ErrorMap["Unknown curve"], |
|
}, |
|
{ |
|
Name: `invalid config yml (bad TLS version)`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_wrongTLSVersion.bad.yml", |
|
ExpectedError: ErrorMap["Unknown TLS version"], |
|
}, |
|
} |
|
for _, testInputs := range testTables { |
|
t.Run(testInputs.Name, testInputs.Test) |
|
} |
|
} |
|
|
|
func TestServerBehaviour(t *testing.T) { |
|
testTables := []*TestInputs{ |
|
{ |
|
Name: `empty string YAMLConfigPath and default client`, |
|
YAMLConfigPath: "", |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `empty string YAMLConfigPath and TLS client`, |
|
YAMLConfigPath: "", |
|
UseTLSClient: true, |
|
ExpectedError: ErrorMap["HTTP Response to HTTPS"], |
|
}, |
|
{ |
|
Name: `valid tls config yml and default client`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth.good.yml", |
|
ExpectedError: ErrorMap["HTTP Request to HTTPS server"], |
|
}, |
|
{ |
|
Name: `valid tls config yml and tls client`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth.good.yml", |
|
UseTLSClient: true, |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `valid tls config yml with TLS 1.1 client`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth.good.yml", |
|
UseTLSClient: true, |
|
ClientMaxTLSVersion: tls.VersionTLS11, |
|
ExpectedError: ErrorMap["Incompatible TLS version"], |
|
}, |
|
{ |
|
Name: `valid tls config yml with all ciphers`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_allCiphers.good.yml", |
|
UseTLSClient: true, |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `valid tls config yml with some ciphers`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_someCiphers.good.yml", |
|
UseTLSClient: true, |
|
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `valid tls config yml with no common cipher`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_someCiphers.good.yml", |
|
UseTLSClient: true, |
|
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA}, |
|
ExpectedError: ErrorMap["Handshake failure"], |
|
}, |
|
{ |
|
Name: `valid tls config yml with multiple client ciphers`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_someCiphers.good.yml", |
|
UseTLSClient: true, |
|
CipherSuites: []uint16{ |
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, |
|
}, |
|
ActualCipher: tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `valid tls config yml with multiple client ciphers, client chooses cipher`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_someCiphers_noOrder.good.yml", |
|
UseTLSClient: true, |
|
CipherSuites: []uint16{ |
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, |
|
}, |
|
ActualCipher: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `valid tls config yml with all curves`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_allCurves.good.yml", |
|
UseTLSClient: true, |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `valid tls config yml with some curves`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_someCurves.good.yml", |
|
UseTLSClient: true, |
|
CurvePreferences: []tls.CurveID{tls.CurveP521}, |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `valid tls config yml with no common curves`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_someCurves.good.yml", |
|
UseTLSClient: true, |
|
CurvePreferences: []tls.CurveID{tls.CurveP384}, |
|
ExpectedError: ErrorMap["Handshake failure"], |
|
}, |
|
{ |
|
Name: `valid tls config yml with non-http2 ciphers`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_noHTTP2.good.yml", |
|
UseTLSClient: true, |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `valid tls config yml with non-http2 ciphers but http2 enabled`, |
|
YAMLConfigPath: "testdata/tls_config_noAuth_noHTTP2Cipher.bad.yml", |
|
UseTLSClient: true, |
|
ExpectedError: ErrorMap["No HTTP2 cipher"], |
|
}, |
|
} |
|
for _, testInputs := range testTables { |
|
t.Run(testInputs.Name, testInputs.Test) |
|
} |
|
} |
|
|
|
func TestConfigReloading(t *testing.T) { |
|
errorChannel := make(chan error, 1) |
|
var once sync.Once |
|
recordConnectionError := func(err error) { |
|
once.Do(func() { |
|
errorChannel <- err |
|
}) |
|
} |
|
defer func() { |
|
if recover() != nil { |
|
recordConnectionError(errors.New("Panic in test function")) |
|
} |
|
}() |
|
|
|
goodYAMLPath := "testdata/tls_config_noAuth.good.yml" |
|
badYAMLPath := "testdata/tls_config_noAuth.good.blocking.yml" |
|
|
|
server := &http.Server{ |
|
Addr: port, |
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
w.Write([]byte("Hello World!")) |
|
}), |
|
} |
|
defer func() { |
|
server.Close() |
|
}() |
|
|
|
go func() { |
|
defer func() { |
|
if recover() != nil { |
|
recordConnectionError(errors.New("Panic starting server")) |
|
} |
|
}() |
|
err := Listen(server, badYAMLPath, testlogger) |
|
recordConnectionError(err) |
|
}() |
|
|
|
client := getTLSClient() |
|
|
|
TestClientConnection := func() error { |
|
time.Sleep(250 * time.Millisecond) |
|
r, err := client.Get("https://localhost" + port) |
|
if err != nil { |
|
return (err) |
|
} |
|
body, err := ioutil.ReadAll(r.Body) |
|
if err != nil { |
|
return (err) |
|
} |
|
if string(body) != "Hello World!" { |
|
return (errors.New(string(body))) |
|
} |
|
return (nil) |
|
} |
|
|
|
err := TestClientConnection() |
|
if err == nil { |
|
recordConnectionError(errors.New("connection accepted but should have failed")) |
|
} else { |
|
swapFileContents(goodYAMLPath, badYAMLPath) |
|
defer swapFileContents(goodYAMLPath, badYAMLPath) |
|
err = TestClientConnection() |
|
if err != nil { |
|
recordConnectionError(errors.New("connection failed but should have been accepted")) |
|
} else { |
|
|
|
recordConnectionError(nil) |
|
} |
|
} |
|
|
|
err = <-errorChannel |
|
if err != nil { |
|
t.Errorf(" *** Failed test: %s *** Returned error: %v", "TestConfigReloading", err) |
|
} |
|
} |
|
|
|
func (test *TestInputs) Test(t *testing.T) { |
|
errorChannel := make(chan error, 1) |
|
var once sync.Once |
|
recordConnectionError := func(err error) { |
|
once.Do(func() { |
|
errorChannel <- err |
|
}) |
|
} |
|
defer func() { |
|
if recover() != nil { |
|
recordConnectionError(errors.New("Panic in test function")) |
|
} |
|
}() |
|
|
|
var server *http.Server |
|
if test.UseNilServer { |
|
server = nil |
|
} else { |
|
server = &http.Server{ |
|
Addr: port, |
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
w.Write([]byte("Hello World!")) |
|
}), |
|
} |
|
defer func() { |
|
server.Close() |
|
}() |
|
} |
|
go func() { |
|
defer func() { |
|
if recover() != nil { |
|
recordConnectionError(errors.New("Panic starting server")) |
|
} |
|
}() |
|
err := Listen(server, test.YAMLConfigPath, testlogger) |
|
recordConnectionError(err) |
|
}() |
|
|
|
ClientConnection := func() (*http.Response, error) { |
|
var client *http.Client |
|
var proto string |
|
if test.UseTLSClient { |
|
client = getTLSClient() |
|
t := client.Transport.(*http.Transport) |
|
t.TLSClientConfig.MaxVersion = test.ClientMaxTLSVersion |
|
if len(test.CipherSuites) > 0 { |
|
t.TLSClientConfig.CipherSuites = test.CipherSuites |
|
} |
|
if len(test.CurvePreferences) > 0 { |
|
t.TLSClientConfig.CurvePreferences = test.CurvePreferences |
|
} |
|
proto = "https" |
|
} else { |
|
client = http.DefaultClient |
|
proto = "http" |
|
} |
|
req, err := http.NewRequest("GET", proto+"://localhost"+port, nil) |
|
if err != nil { |
|
t.Error(err) |
|
} |
|
if test.Username != "" { |
|
req.SetBasicAuth(test.Username, test.Password) |
|
} |
|
return client.Do(req) |
|
} |
|
go func() { |
|
time.Sleep(250 * time.Millisecond) |
|
r, err := ClientConnection() |
|
if err != nil { |
|
recordConnectionError(err) |
|
return |
|
} |
|
|
|
if test.ActualCipher != 0 { |
|
if r.TLS.CipherSuite != test.ActualCipher { |
|
recordConnectionError( |
|
fmt.Errorf("bad cipher suite selected. Expected: %s, got: %s", |
|
tls.CipherSuiteName(r.TLS.CipherSuite), |
|
tls.CipherSuiteName(test.ActualCipher), |
|
), |
|
) |
|
} |
|
} |
|
|
|
body, err := ioutil.ReadAll(r.Body) |
|
if err != nil { |
|
recordConnectionError(err) |
|
return |
|
} |
|
if string(body) != "Hello World!" { |
|
recordConnectionError(errors.New(string(body))) |
|
return |
|
} |
|
recordConnectionError(nil) |
|
}() |
|
err := <-errorChannel |
|
if test.isCorrectError(err) == false { |
|
if test.ExpectedError == nil { |
|
t.Logf("Expected no error, got error: %v", err) |
|
} else { |
|
t.Logf("Expected error matching regular expression: %v", test.ExpectedError) |
|
t.Logf("Got: %v", err) |
|
} |
|
t.Fail() |
|
} |
|
} |
|
|
|
func (test *TestInputs) isCorrectError(returnedError error) bool { |
|
switch { |
|
case returnedError == nil && test.ExpectedError == nil: |
|
case returnedError != nil && test.ExpectedError != nil && test.ExpectedError.MatchString(returnedError.Error()): |
|
default: |
|
return false |
|
} |
|
return true |
|
} |
|
|
|
func getTLSClient() *http.Client { |
|
cert, err := ioutil.ReadFile("testdata/tls-ca-chain.pem") |
|
if err != nil { |
|
panic("Unable to start TLS client. Check cert path") |
|
} |
|
client := &http.Client{ |
|
Transport: &http.Transport{ |
|
TLSClientConfig: &tls.Config{ |
|
RootCAs: func() *x509.CertPool { |
|
caCertPool := x509.NewCertPool() |
|
caCertPool.AppendCertsFromPEM(cert) |
|
return caCertPool |
|
}(), |
|
}, |
|
}, |
|
} |
|
return client |
|
} |
|
|
|
func swapFileContents(file1, file2 string) error { |
|
content1, err := ioutil.ReadFile(file1) |
|
if err != nil { |
|
return err |
|
} |
|
content2, err := ioutil.ReadFile(file2) |
|
if err != nil { |
|
return err |
|
} |
|
err = ioutil.WriteFile(file1, content2, 0644) |
|
if err != nil { |
|
return err |
|
} |
|
err = ioutil.WriteFile(file2, content1, 0644) |
|
if err != nil { |
|
return err |
|
} |
|
return nil |
|
} |
|
|
|
func TestUsers(t *testing.T) { |
|
testTables := []*TestInputs{ |
|
{ |
|
Name: `without basic auth`, |
|
YAMLConfigPath: "testdata/tls_config_users_noTLS.good.yml", |
|
ExpectedError: ErrorMap["Unauthorized"], |
|
}, |
|
{ |
|
Name: `with correct basic auth`, |
|
YAMLConfigPath: "testdata/tls_config_users_noTLS.good.yml", |
|
Username: "dave", |
|
Password: "dave123", |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `without basic auth and TLS`, |
|
YAMLConfigPath: "testdata/tls_config_users.good.yml", |
|
UseTLSClient: true, |
|
ExpectedError: ErrorMap["Unauthorized"], |
|
}, |
|
{ |
|
Name: `with correct basic auth and TLS`, |
|
YAMLConfigPath: "testdata/tls_config_users.good.yml", |
|
UseTLSClient: true, |
|
Username: "dave", |
|
Password: "dave123", |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `with another correct basic auth and TLS`, |
|
YAMLConfigPath: "testdata/tls_config_users.good.yml", |
|
UseTLSClient: true, |
|
Username: "carol", |
|
Password: "carol123", |
|
ExpectedError: nil, |
|
}, |
|
{ |
|
Name: `with bad password and TLS`, |
|
YAMLConfigPath: "testdata/tls_config_users.good.yml", |
|
UseTLSClient: true, |
|
Username: "dave", |
|
Password: "bad", |
|
ExpectedError: ErrorMap["Forbidden"], |
|
}, |
|
{ |
|
Name: `with bad username and TLS`, |
|
YAMLConfigPath: "testdata/tls_config_users.good.yml", |
|
UseTLSClient: true, |
|
Username: "nonexistent", |
|
Password: "nonexistent", |
|
ExpectedError: ErrorMap["Forbidden"], |
|
}, |
|
} |
|
for _, testInputs := range testTables { |
|
t.Run(testInputs.Name, testInputs.Test) |
|
} |
|
}
|
|
|