From 360cf1d82f03a99797a6a3e7ed4703fd65f9052c Mon Sep 17 00:00:00 2001 From: peppi-lotta Date: Mon, 30 Jun 2025 12:14:42 +0000 Subject: [PATCH 1/2] Add Config to FlagConfig struct This allows the Serve-function to be called with out having a config file. This makes the function more easilly callable from other projects. This way configuration is not limited to an existing yaml file but can be specified in the project that is using this package. Signed-off-by: peppi-lotta --- web/handler.go | 18 ++- web/tls_config.go | 73 ++++++++---- web/tls_config_test.go | 255 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 317 insertions(+), 29 deletions(-) diff --git a/web/handler.go b/web/handler.go index 0a2718d5..a1f32a21 100644 --- a/web/handler.go +++ b/web/handler.go @@ -78,6 +78,7 @@ HeadersLoop: type webHandler struct { tlsConfigPath string + config *Config handler http.Handler logger *slog.Logger cache *cache @@ -88,11 +89,18 @@ type webHandler struct { } func (u *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - c, err := getConfig(u.tlsConfigPath) - if err != nil { - u.logger.Error("Unable to parse configuration", "err", err.Error()) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return + var c *Config + var err error + + if u.config == nil { + c, err = getConfig(u.tlsConfigPath) + if err != nil { + u.logger.Error("Unable to parse configuration", "err", err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } else { + c = u.config } if u.limiter != nil && !u.limiter.Allow() { diff --git a/web/tls_config.go b/web/tls_config.go index c760d88c..29a58478 100644 --- a/web/tls_config.go +++ b/web/tls_config.go @@ -67,7 +67,8 @@ type TLSConfig struct { type FlagConfig struct { WebListenAddresses *[]string WebSystemdSocket *bool - WebConfigFile *string + WebConfigFile *string // Optional: path to the TLS config file. Ether this or TLSConfig must be set. + WebConfig *Config // Optional: Configuration. If set, it overrides WebConfigFile. } // SetDirectory joins any relative file paths with dir. @@ -352,14 +353,28 @@ func parseVsockPort(address string) (uint32, error) { // WebConfigFile in the FlagConfig, TLS or basic auth could be enabled. func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger *slog.Logger) error { logger.Info("Listening on", "address", l.Addr().String()) - tlsConfigPath := *flags.WebConfigFile - if tlsConfigPath == "" { - logger.Info("TLS is disabled.", "http2", false, "address", l.Addr().String()) - return server.Serve(l) - } + var c *Config + var err error + + // WebConfig overrides WebConfigFile. If WebConfig field is not set, then WebConfigFile is used. + if flags.WebConfig == nil { + tlsConfigPath := *flags.WebConfigFile + if tlsConfigPath == "" { + logger.Info("TLS is disabled.", "http2", false, "address", l.Addr().String()) + return server.Serve(l) + } - if err := validateUsers(tlsConfigPath); err != nil { - return err + if err := validateUsers(tlsConfigPath); err != nil { + return err + } + + c, err = getConfig(tlsConfigPath) + if err != nil { + return err + } + } else { + // Use the provided config. + c = flags.WebConfig } // Setup basic authentication. @@ -368,11 +383,6 @@ func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger *slog. handler = server.Handler } - c, err := getConfig(tlsConfigPath) - if err != nil { - return err - } - var limiter *rate.Limiter if c.RateLimiterConfig.Interval != 0 { limiter = rate.NewLimiter(rate.Every(c.RateLimiterConfig.Interval), c.RateLimiterConfig.Burst) @@ -380,11 +390,11 @@ func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger *slog. } server.Handler = &webHandler{ - tlsConfigPath: tlsConfigPath, - logger: logger, - handler: handler, - cache: newCache(), - limiter: limiter, + config: c, + logger: logger, + handler: handler, + cache: newCache(), + limiter: limiter, } config, err := ConfigToTLSConfig(&c.TLSConfig) @@ -409,12 +419,29 @@ func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger *slog. // Set the GetConfigForClient method of the HTTPS server so that the config // and certs are reloaded on new connections. server.TLSConfig.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) { - config, err := getTLSConfig(tlsConfigPath) - if err != nil { - return nil, err + var tlsConfig *tls.Config + var err error + // Config overrides WebConfigFile. If Config fiels is not set, then WebConfigFile is used. + if flags.WebConfig == nil { + tlsConfigPath := *flags.WebConfigFile + + if err := validateUsers(tlsConfigPath); err != nil { + return nil, err + } + + tlsConfig, err = getTLSConfig(tlsConfigPath) + if err != nil { + return nil, err + } + } else { + // Use the provided config. + tlsConfig, err = ConfigToTLSConfig(&flags.WebConfig.TLSConfig) + if err != nil { + return nil, err + } } - config.NextProtos = server.TLSConfig.NextProtos - return config, nil + tlsConfig.NextProtos = server.TLSConfig.NextProtos + return tlsConfig, nil } return server.ServeTLS(l, "", "") } diff --git a/web/tls_config_test.go b/web/tls_config_test.go index a0d700ce..1a0f5599 100644 --- a/web/tls_config_test.go +++ b/web/tls_config_test.go @@ -90,6 +90,7 @@ type TestInputs struct { Name string Server func() *http.Server YAMLConfigPath string + WebConfig bool ExpectedError *regexp.Regexp UseTLSClient bool ClientMaxTLSVersion uint16 @@ -208,6 +209,10 @@ func TestServerBehaviour(t *testing.T) { YAMLConfigPath: "", ExpectedError: nil, }, + { + Name: `default client`, + ExpectedError: nil, + }, { Name: `empty string YAMLConfigPath and TLS client`, YAMLConfigPath: "", @@ -219,18 +224,38 @@ func TestServerBehaviour(t *testing.T) { YAMLConfigPath: "testdata/web_config_noAuth.good.yml", ExpectedError: ErrorMap["HTTP Request to HTTPS server"], }, + { + Name: `WebConfig: valid tls config and default client`, + YAMLConfigPath: "testdata/web_config_noAuth.good.yml", + WebConfig: true, + ExpectedError: ErrorMap["HTTP Request to HTTPS server"], + }, { Name: `valid tls config yml and tls client`, YAMLConfigPath: "testdata/web_config_noAuth.good.yml", UseTLSClient: true, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config and tls client`, + YAMLConfigPath: "testdata/web_config_noAuth.good.yml", + WebConfig: true, + UseTLSClient: true, + ExpectedError: nil, + }, { Name: `valid tls config yml (cert and key inline) and tls client`, YAMLConfigPath: "testdata/web_config_noAuth_tlsInline.good.yml", UseTLSClient: true, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config (cert and key inline) and tls client`, + YAMLConfigPath: "testdata/web_config_noAuth_tlsInline.good.yml", + WebConfig: true, + UseTLSClient: true, + ExpectedError: nil, + }, { Name: `valid tls config yml with TLS 1.1 client`, YAMLConfigPath: "testdata/web_config_noAuth.good.yml", @@ -238,12 +263,27 @@ func TestServerBehaviour(t *testing.T) { ClientMaxTLSVersion: tls.VersionTLS11, ExpectedError: ErrorMap["Incompatible TLS version"], }, + { + Name: `WebConfig: valid tls config with TLS 1.1 client`, + YAMLConfigPath: "testdata/web_config_noAuth.good.yml", + WebConfig: true, + UseTLSClient: true, + ClientMaxTLSVersion: tls.VersionTLS11, + ExpectedError: ErrorMap["Incompatible TLS version"], + }, { Name: `valid tls config yml with all ciphers`, YAMLConfigPath: "testdata/web_config_noAuth_allCiphers.good.yml", UseTLSClient: true, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config with all ciphers`, + YAMLConfigPath: "testdata/web_config_noAuth_allCiphers.good.yml", + WebConfig: true, + UseTLSClient: true, + ExpectedError: nil, + }, { Name: `valid tls config yml with some ciphers`, YAMLConfigPath: "testdata/web_config_noAuth_someCiphers.good.yml", @@ -251,6 +291,14 @@ func TestServerBehaviour(t *testing.T) { CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config with some ciphers`, + YAMLConfigPath: "testdata/web_config_noAuth_someCiphers.good.yml", + WebConfig: true, + 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/web_config_noAuth_someCiphers.good.yml", @@ -258,6 +306,14 @@ func TestServerBehaviour(t *testing.T) { CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA}, ExpectedError: ErrorMap["Handshake failure"], }, + { + Name: `WebConfig: valid tls config with no common cipher`, + YAMLConfigPath: "testdata/web_config_noAuth_someCiphers.good.yml", + WebConfig: true, + 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/web_config_noAuth_someCiphers.good.yml", @@ -269,6 +325,18 @@ func TestServerBehaviour(t *testing.T) { ActualCipher: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config with multiple client ciphers`, + YAMLConfigPath: "testdata/web_config_noAuth_someCiphers.good.yml", + WebConfig: true, + 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 multiple client ciphers, client chooses cipher`, YAMLConfigPath: "testdata/web_config_noAuth_someCiphers_noOrder.good.yml", @@ -280,12 +348,31 @@ func TestServerBehaviour(t *testing.T) { ActualCipher: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config with multiple client ciphers, client chooses cipher`, + YAMLConfigPath: "testdata/web_config_noAuth_someCiphers_noOrder.good.yml", + WebConfig: true, + 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/web_config_noAuth_allCurves.good.yml", UseTLSClient: true, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config with all curves`, + YAMLConfigPath: "testdata/web_config_noAuth_allCurves.good.yml", + WebConfig: true, + UseTLSClient: true, + ExpectedError: nil, + }, { Name: `valid tls config yml with some curves`, YAMLConfigPath: "testdata/web_config_noAuth_someCurves.good.yml", @@ -293,6 +380,14 @@ func TestServerBehaviour(t *testing.T) { CurvePreferences: []tls.CurveID{tls.CurveP521}, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config with some curves`, + YAMLConfigPath: "testdata/web_config_noAuth_someCurves.good.yml", + WebConfig: true, + UseTLSClient: true, + CurvePreferences: []tls.CurveID{tls.CurveP521}, + ExpectedError: nil, + }, { Name: `valid tls config yml with no common curves`, YAMLConfigPath: "testdata/web_config_noAuth_someCurves.good.yml", @@ -300,37 +395,82 @@ func TestServerBehaviour(t *testing.T) { CurvePreferences: []tls.CurveID{tls.CurveP384}, ExpectedError: ErrorMap["Handshake failure"], }, + { + Name: `WebConfig: valid tls config with no common curves`, + YAMLConfigPath: "testdata/web_config_noAuth_someCurves.good.yml", + WebConfig: true, + UseTLSClient: true, + CurvePreferences: []tls.CurveID{tls.CurveP384}, + ExpectedError: ErrorMap["Handshake failure"], + }, { Name: `valid tls config yml with non-http2 ciphers`, YAMLConfigPath: "testdata/web_config_noAuth_noHTTP2.good.yml", UseTLSClient: true, ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config with non-http2 ciphers`, + YAMLConfigPath: "testdata/web_config_noAuth_noHTTP2.good.yml", + WebConfig: true, + UseTLSClient: true, + ExpectedError: nil, + }, { Name: `valid tls config yml with non-http2 ciphers but http2 enabled`, YAMLConfigPath: "testdata/web_config_noAuth_noHTTP2Cipher.bad.yml", UseTLSClient: true, ExpectedError: ErrorMap["No HTTP2 cipher"], }, + { + Name: `WebConfig: valid tls config with non-http2 ciphers but http2 enabled`, + YAMLConfigPath: "testdata/web_config_noAuth_noHTTP2Cipher.bad.yml", + WebConfig: true, + UseTLSClient: true, + ExpectedError: ErrorMap["No HTTP2 cipher"], + }, { Name: `valid headers config`, YAMLConfigPath: "testdata/web_config_headers.good.yml", }, + { + Name: `WebConfig: valid headers config`, + YAMLConfigPath: "testdata/web_config_headers.good.yml", + WebConfig: true, + }, { Name: `invalid X-Content-Type-Options headers config`, YAMLConfigPath: "testdata/web_config_headers_content_type_options.bad.yml", ExpectedError: ErrorMap["Invalid value"], }, + { + Name: `WebConfig: invalid X-Content-Type-Options headers config`, + YAMLConfigPath: "testdata/web_config_headers_content_type_options.bad.yml", + WebConfig: true, + ExpectedError: ErrorMap["Invalid value"], + }, { Name: `invalid X-Frame-Options headers config`, YAMLConfigPath: "testdata/web_config_headers_frame_options.bad.yml", ExpectedError: ErrorMap["Invalid value"], }, + { + Name: `WebConfig: invalid X-Frame-Options headers config`, + YAMLConfigPath: "testdata/web_config_headers_frame_options.bad.yml", + WebConfig: true, + ExpectedError: ErrorMap["Invalid value"], + }, { Name: `HTTP header that can not be overridden`, YAMLConfigPath: "testdata/web_config_headers_extra_header.bad.yml", ExpectedError: ErrorMap["Invalid header"], }, + { + Name: `WebConfig: HTTP header that can not be overridden`, + YAMLConfigPath: "testdata/web_config_headers_extra_header.bad.yml", + WebConfig: true, + ExpectedError: ErrorMap["Invalid header"], + }, { Name: `valid tls config yml and tls client with RequireAnyClientCert (present certificate)`, YAMLConfigPath: "testdata/tls_config_noAuth.requireanyclientcert.good.yml", @@ -338,6 +478,14 @@ func TestServerBehaviour(t *testing.T) { ClientCertificate: "client_selfsigned", ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config and tls client with RequireAnyClientCert (present certificate)`, + YAMLConfigPath: "testdata/tls_config_noAuth.requireanyclientcert.good.yml", + WebConfig: true, + UseTLSClient: true, + ClientCertificate: "client_selfsigned", + ExpectedError: nil, + }, { Name: `valid tls config yml (cert from file, key inline) and tls client with RequireAnyClientCert (present certificate)`, YAMLConfigPath: "testdata/tls_config_noAuth.requireanyclientcert.good.yml", @@ -345,6 +493,14 @@ func TestServerBehaviour(t *testing.T) { ClientCertificate: "client_selfsigned", ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config (cert from file, key inline) and tls client with RequireAnyClientCert (present certificate)`, + YAMLConfigPath: "testdata/tls_config_noAuth.requireanyclientcert.good.yml", + WebConfig: true, + UseTLSClient: true, + ClientCertificate: "client_selfsigned", + ExpectedError: nil, + }, { Name: `valid tls config yml and tls client with RequireAndVerifyClientCert (present certificate)`, YAMLConfigPath: "testdata/tls_config_noAuth.requireandverifyclientcert.good.yml", @@ -352,6 +508,14 @@ func TestServerBehaviour(t *testing.T) { ClientCertificate: "client_selfsigned", ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config and tls client with RequireAndVerifyClientCert (present certificate)`, + YAMLConfigPath: "testdata/tls_config_noAuth.requireandverifyclientcert.good.yml", + WebConfig: true, + UseTLSClient: true, + ClientCertificate: "client_selfsigned", + ExpectedError: nil, + }, { Name: `valid tls config yml and tls client with VerifyPeerCertificate (present good SAN DNS entry)`, YAMLConfigPath: "testdata/web_config_auth_client_san.good.yaml", @@ -359,6 +523,14 @@ func TestServerBehaviour(t *testing.T) { ClientCertificate: "client2_selfsigned", ExpectedError: nil, }, + { + Name: `WebConfig: valid tls config and tls client with VerifyPeerCertificate (present good SAN DNS entry)`, + YAMLConfigPath: "testdata/web_config_auth_client_san.good.yaml", + WebConfig: true, + UseTLSClient: true, + ClientCertificate: "client2_selfsigned", + ExpectedError: nil, + }, { Name: `valid tls config yml and tls client with VerifyPeerCertificate (present invalid SAN DNS entries)`, YAMLConfigPath: "testdata/web_config_auth_client_san.bad.yaml", @@ -366,6 +538,14 @@ func TestServerBehaviour(t *testing.T) { ClientCertificate: "client2_selfsigned", ExpectedError: ErrorMap["Invalid client cert"], }, + { + Name: "valid rate limiter (no rate limiter set up) that doesn't block", + YAMLConfigPath: "testdata/web_config_rate_limiter_nonblocking.yaml", + WebConfig: true, + UseTLSClient: false, + Requests: 10, + ExpectedError: nil, + }, { Name: "valid rate limiter (no rate limiter set up) that doesn't block", YAMLConfigPath: "testdata/web_config_rate_limiter_nonblocking.yaml", @@ -373,6 +553,14 @@ func TestServerBehaviour(t *testing.T) { Requests: 10, ExpectedError: nil, }, + { + Name: "valid rate limiter with an interval of one second", + YAMLConfigPath: "testdata/web_config_rate_limiter_one_second.yaml", + WebConfig: true, + UseTLSClient: false, + Requests: 10, + ExpectedError: ErrorMap["Too Many Requests"], + }, { Name: "valid rate limiter with an interval of one second", YAMLConfigPath: "testdata/web_config_rate_limiter_one_second.yaml", @@ -496,7 +684,15 @@ func (test *TestInputs) Test(t *testing.T) { flags := FlagConfig{ WebListenAddresses: &([]string{port}), WebSystemdSocket: OfBool(false), - WebConfigFile: &test.YAMLConfigPath, + } + if test.WebConfig { + webConfig, err := getConfig(test.YAMLConfigPath) + if err != nil { + recordConnectionError(fmt.Errorf("Could not parse configuration: %v", err)) + } + flags.WebConfig = webConfig + } else { + flags.WebConfigFile = &test.YAMLConfigPath } err := ListenAndServe(server, &flags, testlogger) recordConnectionError(err) @@ -665,6 +861,12 @@ func TestUsers(t *testing.T) { YAMLConfigPath: "testdata/web_config_users_noTLS.good.yml", ExpectedError: ErrorMap["Unauthorized"], }, + { + Name: `WebConfig: without basic auth`, + YAMLConfigPath: "testdata/web_config_users_noTLS.good.yml", + WebConfig: true, + ExpectedError: ErrorMap["Unauthorized"], + }, { Name: `with correct basic auth`, YAMLConfigPath: "testdata/web_config_users_noTLS.good.yml", @@ -672,12 +874,27 @@ func TestUsers(t *testing.T) { Password: "dave123", ExpectedError: nil, }, + { + Name: `WebConfig: with correct basic auth`, + YAMLConfigPath: "testdata/web_config_users_noTLS.good.yml", + WebConfig: true, + Username: "dave", + Password: "dave123", + ExpectedError: nil, + }, { Name: `without basic auth and TLS`, YAMLConfigPath: "testdata/web_config_users.good.yml", UseTLSClient: true, ExpectedError: ErrorMap["Unauthorized"], }, + { + Name: `WebConfig: without basic auth and TLS`, + YAMLConfigPath: "testdata/web_config_users.good.yml", + WebConfig: true, + UseTLSClient: true, + ExpectedError: ErrorMap["Unauthorized"], + }, { Name: `with correct basic auth and TLS`, YAMLConfigPath: "testdata/web_config_users.good.yml", @@ -686,6 +903,15 @@ func TestUsers(t *testing.T) { Password: "dave123", ExpectedError: nil, }, + { + Name: `WebConfig: with correct basic auth and TLS`, + YAMLConfigPath: "testdata/web_config_users.good.yml", + WebConfig: true, + UseTLSClient: true, + Username: "dave", + Password: "dave123", + ExpectedError: nil, + }, { Name: `with another correct basic auth and TLS`, YAMLConfigPath: "testdata/web_config_users.good.yml", @@ -694,6 +920,15 @@ func TestUsers(t *testing.T) { Password: "carol123", ExpectedError: nil, }, + { + Name: `WebConfig: with another correct basic auth and TLS`, + YAMLConfigPath: "testdata/web_config_users.good.yml", + WebConfig: true, + UseTLSClient: true, + Username: "carol", + Password: "carol123", + ExpectedError: nil, + }, { Name: `with bad password and TLS`, YAMLConfigPath: "testdata/web_config_users.good.yml", @@ -702,6 +937,15 @@ func TestUsers(t *testing.T) { Password: "bad", ExpectedError: ErrorMap["Unauthorized"], }, + { + Name: `WebConfig: with bad password and TLS`, + YAMLConfigPath: "testdata/web_config_users.good.yml", + WebConfig: true, + UseTLSClient: true, + Username: "dave", + Password: "bad", + ExpectedError: ErrorMap["Unauthorized"], + }, { Name: `with bad username and TLS`, YAMLConfigPath: "testdata/web_config_users.good.yml", @@ -710,6 +954,15 @@ func TestUsers(t *testing.T) { Password: "nonexistent", ExpectedError: ErrorMap["Unauthorized"], }, + { + Name: `WebConfig: with bad username and TLS`, + YAMLConfigPath: "testdata/web_config_users.good.yml", + WebConfig: true, + UseTLSClient: true, + Username: "nonexistent", + Password: "nonexistent", + ExpectedError: ErrorMap["Unauthorized"], + }, } for _, testInputs := range testTables { t.Run(testInputs.Name, testInputs.Test) From 3c641443489efb97506fc0b944116e41885a5933 Mon Sep 17 00:00:00 2001 From: peppi-lotta Date: Fri, 18 Jul 2025 13:54:08 +0000 Subject: [PATCH 2/2] Add validation for WebConfig Signed-off-by: peppi-lotta --- web/handler.go | 16 ++++++++------- web/tls_config.go | 45 +++++++++++++++++++++++++++++++----------- web/tls_config_test.go | 39 ++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 18 deletions(-) diff --git a/web/handler.go b/web/handler.go index a1f32a21..e35eda41 100644 --- a/web/handler.go +++ b/web/handler.go @@ -38,14 +38,9 @@ var extraHTTPHeaders = map[string][]string{ "Content-Security-Policy": nil, } -func validateUsers(configPath string) error { - c, err := getConfig(configPath) - if err != nil { - return err - } - +func validateUsers(c *Config) error { for _, p := range c.Users { - _, err = bcrypt.Cost([]byte(p)) + _, err := bcrypt.Cost([]byte(p)) if err != nil { return err } @@ -108,6 +103,13 @@ func (u *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + err = ValidateWebConfig(c) + if err != nil { + u.logger.Error("Invalid web configuration", "err", err.Error()) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + // Configure http headers. for k, v := range c.HTTPConfig.Header { w.Header().Set(k, v) diff --git a/web/tls_config.go b/web/tls_config.go index 29a58478..98368029 100644 --- a/web/tls_config.go +++ b/web/tls_config.go @@ -144,6 +144,11 @@ func getTLSConfig(configPath string) (*tls.Config, error) { if err != nil { return nil, err } + + if err := validateUsers(c); err != nil { + return nil, err + } + return ConfigToTLSConfig(&c.TLSConfig) } @@ -364,10 +369,6 @@ func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger *slog. return server.Serve(l) } - if err := validateUsers(tlsConfigPath); err != nil { - return err - } - c, err = getConfig(tlsConfigPath) if err != nil { return err @@ -377,6 +378,11 @@ func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger *slog. c = flags.WebConfig } + err = ValidateWebConfig(c) + if err != nil { + return err + } + // Setup basic authentication. var handler http.Handler = http.DefaultServeMux if server.Handler != nil { @@ -425,15 +431,15 @@ func Serve(l net.Listener, server *http.Server, flags *FlagConfig, logger *slog. if flags.WebConfig == nil { tlsConfigPath := *flags.WebConfigFile - if err := validateUsers(tlsConfigPath); err != nil { - return nil, err - } - tlsConfig, err = getTLSConfig(tlsConfigPath) if err != nil { return nil, err } } else { + err = ValidateWebConfig(flags.WebConfig) + if err != nil { + return nil, err + } // Use the provided config. tlsConfig, err = ConfigToTLSConfig(&flags.WebConfig.TLSConfig) if err != nil { @@ -451,13 +457,13 @@ func Validate(tlsConfigPath string) error { if tlsConfigPath == "" { return nil } - if err := validateUsers(tlsConfigPath); err != nil { - return err - } c, err := getConfig(tlsConfigPath) if err != nil { return err } + if err := validateUsers(c); err != nil { + return err + } _, err = ConfigToTLSConfig(&c.TLSConfig) if err == errNoTLSConfig { return nil @@ -465,6 +471,23 @@ func Validate(tlsConfigPath string) error { return err } +// ValidateWebConfig validates the web configuration, including the TLS config and HTTP headers. +func ValidateWebConfig(config *Config) error { + if config == nil { + return nil + } + if err := validateUsers(config); err != nil { + return err + } + if err := validateHeaderConfig(config.HTTPConfig.Header); err != nil { + return err + } + if _, err := ConfigToTLSConfig(&config.TLSConfig); err != nil && err != errNoTLSConfig { + return err + } + return nil +} + type Cipher uint16 func (c *Cipher) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/web/tls_config_test.go b/web/tls_config_test.go index 1a0f5599..e6b4991c 100644 --- a/web/tls_config_test.go +++ b/web/tls_config_test.go @@ -30,6 +30,8 @@ import ( "sync" "testing" "time" + + "gopkg.in/yaml.v2" ) // Helpers for literal FlagConfig @@ -109,6 +111,7 @@ func TestYAMLFiles(t *testing.T) { Name: `path to config yml invalid`, YAMLConfigPath: "somefile", ExpectedError: ErrorMap["No such file"], + WebConfig: false, }, { Name: `empty config yml`, @@ -199,6 +202,9 @@ func TestYAMLFiles(t *testing.T) { for _, testInputs := range testTables { t.Run("run/"+testInputs.Name, testInputs.Test) t.Run("validate/"+testInputs.Name, testInputs.TestValidate) + if testInputs.WebConfig { + t.Run("validateWebConfig/"+testInputs.Name, testInputs.TestValidateWebConfig) + } } } @@ -790,6 +796,39 @@ func (test *TestInputs) TestValidate(t *testing.T) { } } +func (test *TestInputs) TestValidateWebConfig(t *testing.T) { + content, err := os.ReadFile(test.YAMLConfigPath) + if err != nil { + t.Fatalf("Could not read configuration file: %v", err) + } + c := &Config{ + TLSConfig: TLSConfig{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + PreferServerCipherSuites: true, + }, + HTTPConfig: HTTPConfig{HTTP2: true}, + } + err = yaml.UnmarshalStrict(content, c) + if err != nil { + t.Fatalf("Could not parse configuration: %v", err) + } + validationErr := ValidateWebConfig(c) + if test.ExpectedError == nil { + if validationErr != nil { + t.Errorf("Expected no error, got error: %v", validationErr) + } + return + } + if validationErr == nil { + t.Errorf("Got no error, expected: %v", test.ExpectedError) + return + } + if !test.ExpectedError.MatchString(validationErr.Error()) { + t.Errorf("Expected error %v, got error: %v", test.ExpectedError, validationErr) + } +} + func (test *TestInputs) isCorrectError(returnedError error) bool { switch { case returnedError == nil && test.ExpectedError == nil: