Skip to content
Open
63 changes: 63 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const (
regexLabelPattern = "^[a-zA-Z0-9]([a-zA-Z0-9-_]{0,254}[a-zA-Z0-9])?$"
)

var domainRegex = regexp.MustCompile(
`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`,
)

var viperInstance = viper.NewWithOptions(viper.KeyDelimiter(KeyDelimiter))

func RegisterRunner(r func(cmd *cobra.Command, args []string)) {
Expand Down Expand Up @@ -157,6 +161,7 @@ func ResolveConfig() (*Config, error) {
Features: viperInstance.GetStringSlice(FeaturesKey),
Labels: resolveLabels(),
LibDir: viperInstance.GetString(LibDirPathKey),
ExternalDataSource: resolveExternalDataSource(),
SyslogServer: resolveSyslogServer(),
}

Expand Down Expand Up @@ -475,6 +480,7 @@ func registerFlags() {
registerCollectorFlags(fs)
registerClientFlags(fs)
registerDataPlaneFlags(fs)
registerExternalDataSourceFlags(fs)

fs.SetNormalizeFunc(normalizeFunc)

Expand All @@ -489,6 +495,24 @@ func registerFlags() {
})
}

func registerExternalDataSourceFlags(fs *flag.FlagSet) {
fs.String(
ExternalDataSourceProxyUrlKey,
DefExternalDataSourceProxyUrl,
"Url to the proxy service for fetching external files.",
)
fs.StringSlice(
ExternalDataSourceAllowDomainsKey,
[]string{},
"List of allowed domains for external data sources.",
)
fs.Int64(
ExternalDataSourceMaxBytesKey,
DefExternalDataSourceMaxBytes,
"Maximum size in bytes for external data sources.",
)
}

func registerDataPlaneFlags(fs *flag.FlagSet) {
fs.Duration(
NginxReloadMonitoringPeriodKey,
Expand Down Expand Up @@ -628,6 +652,11 @@ func registerClientFlags(fs *flag.FlagSet) {
DefMaxFileSize,
"Max file size in bytes.",
)
fs.Duration(
ClientFileDownloadTimeoutKey,
DefClientFileDownloadTimeout,
"Timeout value in seconds, for downloading a file during a config apply.",
)

fs.Int(
ClientGRPCMaxParallelFileOperationsKey,
Expand Down Expand Up @@ -1120,6 +1149,7 @@ func resolveClient() *Client {
RandomizationFactor: viperInstance.GetFloat64(ClientBackoffRandomizationFactorKey),
Multiplier: viperInstance.GetFloat64(ClientBackoffMultiplierKey),
},
FileDownloadTimeout: viperInstance.GetDuration(ClientFileDownloadTimeoutKey),
}
}

Expand Down Expand Up @@ -1560,3 +1590,36 @@ func areCommandServerProxyTLSSettingsSet() bool {
viperInstance.IsSet(CommandServerProxyTLSSkipVerifyKey) ||
viperInstance.IsSet(CommandServerProxyTLSServerNameKey)
}

func resolveExternalDataSource() *ExternalDataSource {
proxyURLStruct := ProxyURL{
URL: viperInstance.GetString(ExternalDataSourceProxyUrlKey),
}
externalDataSource := &ExternalDataSource{
ProxyURL: proxyURLStruct,
AllowedDomains: viperInstance.GetStringSlice(ExternalDataSourceAllowDomainsKey),
MaxBytes: viperInstance.GetInt64(ExternalDataSourceMaxBytesKey),
}

if err := validateAllowedDomains(externalDataSource.AllowedDomains); err != nil {
return nil
}

return externalDataSource
}

func validateAllowedDomains(domains []string) error {
if len(domains) == 0 {
return nil
}

for _, domain := range domains {
// Validating syntax using the RFC-compliant regex
if !domainRegex.MatchString(domain) || domain == "" {
slog.Error("domain specified in allowed_domains is invalid", "domain", domain)
return errors.New("invalid domain found in allowed_domains")
}
}

return nil
}
77 changes: 77 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,13 @@ func createConfig() *Config {
config.FeatureCertificates, config.FeatureFileWatcher, config.FeatureMetrics,
config.FeatureAPIAction, config.FeatureLogsNap,
},
ExternalDataSource: &ExternalDataSource{
ProxyURL: ProxyURL{
URL: "http://proxy.example.com",
},
AllowedDomains: []string{"example.com", "api.example.com"},
MaxBytes: 1048576,
},
}
}

Expand Down Expand Up @@ -1569,3 +1576,73 @@ func TestValidateLabel(t *testing.T) {
})
}
}

func TestValidateAllowedDomains(t *testing.T) {
tests := []struct {
name string
domains []string
wantErr bool
}{
{
name: "Test 1: Success: Empty slice",
domains: []string{},
wantErr: false,
},
{
name: "Test 2: Success: Nil slice",
domains: nil,
wantErr: false,
},
{
name: "Test 3: Success: Valid domains",
domains: []string{"example.com", "api.nginx.com", "sub.domain.io"},
wantErr: false,
},
{
name: "Test 4: Failure: Domain contains space",
domains: []string{"valid.com", "bad domain.com"},
wantErr: true,
},
{
name: "Test 5: Failure: Empty string domain",
domains: []string{"valid.com", ""},
wantErr: true,
},
{
name: "Test 6: Failure: Domain contains forward slash /",
domains: []string{"domain.com/path"},
wantErr: true,
},
{
name: "Test 7: Failure: Domain contains backward slash \\",
domains: []string{"domain.com\\path"},
wantErr: true,
},
{
name: "Test 8: Failure: Mixed valid and invalid (first is invalid)",
domains: []string{" only.com", "good.com"},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var logBuffer bytes.Buffer
logHandler := slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{Level: slog.LevelError})

originalLogger := slog.Default()
slog.SetDefault(slog.New(logHandler))
defer slog.SetDefault(originalLogger)

actualErr := validateAllowedDomains(tt.domains)

if tt.wantErr {
require.Error(t, actualErr, "Expected an error but got nil.")
assert.Contains(t, logBuffer.String(), "domain specified in allowed_domains is invalid",
"Expected the error log message to be present in the output.")
} else {
assert.NoError(t, actualErr, "Did not expect an error but got one: %v", actualErr)
}
})
}
}
6 changes: 6 additions & 0 deletions internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ const (
DefBackoffMaxInterval = 20 * time.Second
DefBackoffMaxElapsedTime = 1 * time.Minute

DefClientFileDownloadTimeout = 60 * time.Second

// Watcher defaults
DefInstanceWatcherMonitoringFrequency = 5 * time.Second
DefInstanceHealthWatcherMonitoringFrequency = 5 * time.Second
Expand Down Expand Up @@ -114,6 +116,10 @@ const (

// File defaults
DefLibDir = "/var/lib/nginx-agent"

DefExternalDataSourceProxyUrl = ""
// Default allow external data sources up to 100 MB
DefExternalDataSourceMaxBytes = 100 * 1024 * 1024
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DefExternalDataSourceMaxBytes = 100 * 1024 * 1024
DefExternalDataSourceMaxBytes = 100 * 1024 * 1024 // default 100MB

)

func DefaultFeatures() []string {
Expand Down
7 changes: 7 additions & 0 deletions internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
InstanceHealthWatcherMonitoringFrequencyKey = "watchers_instance_health_watcher_monitoring_frequency"
FileWatcherKey = "watchers_file_watcher"
LibDirPathKey = "lib_dir"
ExternalDataSourceRootKey = "external_data_source"
)

var (
Expand All @@ -47,6 +48,7 @@ var (
ClientBackoffMaxElapsedTimeKey = pre(ClientRootKey) + "backoff_max_elapsed_time"
ClientBackoffRandomizationFactorKey = pre(ClientRootKey) + "backoff_randomization_factor"
ClientBackoffMultiplierKey = pre(ClientRootKey) + "backoff_multiplier"
ClientFileDownloadTimeoutKey = pre(ClientRootKey) + "file_download_timeout"

CollectorConfigPathKey = pre(CollectorRootKey) + "config_path"
CollectorAdditionalConfigPathsKey = pre(CollectorRootKey) + "additional_config_paths"
Expand Down Expand Up @@ -141,6 +143,11 @@ var (

FileWatcherMonitoringFrequencyKey = pre(FileWatcherKey) + "monitoring_frequency"
NginxExcludeFilesKey = pre(FileWatcherKey) + "exclude_files"

ExternalDataSourceProxyKey = pre(ExternalDataSourceRootKey) + "proxy"
ExternalDataSourceProxyUrlKey = pre(ExternalDataSourceProxyKey) + "url"
ExternalDataSourceMaxBytesKey = pre(ExternalDataSourceRootKey) + "max_bytes"
ExternalDataSourceAllowDomainsKey = pre(ExternalDataSourceRootKey) + "allowed_domains"
)

func pre(prefixes ...string) string {
Expand Down
8 changes: 8 additions & 0 deletions internal/config/testdata/nginx-agent.conf
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,11 @@ collector:
log:
level: "INFO"
path: "/var/log/nginx-agent/opentelemetry-collector-agent.log"

external_data_source:
proxy:
url: "http://proxy.example.com"
allowed_domains:
- example.com
- api.example.com
max_bytes: 1048576
48 changes: 30 additions & 18 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,22 @@ func parseServerType(str string) (ServerType, bool) {

type (
Config struct {
Command *Command `yaml:"command" mapstructure:"command"`
AuxiliaryCommand *Command `yaml:"auxiliary_command" mapstructure:"auxiliary_command"`
Log *Log `yaml:"log" mapstructure:"log"`
DataPlaneConfig *DataPlaneConfig `yaml:"data_plane_config" mapstructure:"data_plane_config"`
Client *Client `yaml:"client" mapstructure:"client"`
Collector *Collector `yaml:"collector" mapstructure:"collector"`
Watchers *Watchers `yaml:"watchers" mapstructure:"watchers"`
SyslogServer *SyslogServer `yaml:"syslog_server" mapstructure:"syslog_server"`
Labels map[string]any `yaml:"labels" mapstructure:"labels"`
Version string `yaml:"-"`
Path string `yaml:"-"`
UUID string `yaml:"-"`
LibDir string `yaml:"-"`
AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"`
Features []string `yaml:"features" mapstructure:"features"`
Command *Command `yaml:"command" mapstructure:"command"`
AuxiliaryCommand *Command `yaml:"auxiliary_command" mapstructure:"auxiliary_command"`
Log *Log `yaml:"log" mapstructure:"log"`
DataPlaneConfig *DataPlaneConfig `yaml:"data_plane_config" mapstructure:"data_plane_config"`
Client *Client `yaml:"client" mapstructure:"client"`
Collector *Collector `yaml:"collector" mapstructure:"collector"`
Watchers *Watchers `yaml:"watchers" mapstructure:"watchers"`
SyslogServer *SyslogServer `yaml:"syslog_server" mapstructure:"syslog_server"`
ExternalDataSource *ExternalDataSource `yaml:"external_data_source" mapstructure:"external_data_source"`
Labels map[string]any `yaml:"labels" mapstructure:"labels"`
Version string `yaml:"-"`
Path string `yaml:"-"`
UUID string `yaml:"-"`
LibDir string `yaml:"-"`
AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"`
Features []string `yaml:"features" mapstructure:"features"`
}

Log struct {
Expand All @@ -74,9 +75,10 @@ type (
}

Client struct {
HTTP *HTTP `yaml:"http" mapstructure:"http"`
Grpc *GRPC `yaml:"grpc" mapstructure:"grpc"`
Backoff *BackOff `yaml:"backoff" mapstructure:"backoff"`
HTTP *HTTP `yaml:"http" mapstructure:"http"`
Grpc *GRPC `yaml:"grpc" mapstructure:"grpc"`
Backoff *BackOff `yaml:"backoff" mapstructure:"backoff"`
FileDownloadTimeout time.Duration `yaml:"file_download_timeout" mapstructure:"file_download_timeout"`
}

HTTP struct {
Expand Down Expand Up @@ -358,6 +360,16 @@ type (
Token string `yaml:"token,omitempty" mapstructure:"token"`
Timeout time.Duration `yaml:"timeout" mapstructure:"timeout"`
}

ProxyURL struct {
URL string `yaml:"url" mapstructure:"url"`
}

ExternalDataSource struct {
ProxyURL ProxyURL `yaml:"proxy" mapstructure:"proxy"`
AllowedDomains []string `yaml:"allowed_domains" mapstructure:"allowed_domains"`
MaxBytes int64 `yaml:"max_bytes" mapstructure:"max_bytes"`
}
)

func (col *Collector) Validate(allowedDirectories []string) error {
Expand Down
Loading
Loading