diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9da6e16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Ignore the build folder +build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31196e9 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +MAIN_PATH := cmd/server/main.go + +APP_NAME := goAdBlock + +BUILD_DIR := build + +.PHONY: build +build: + @echo "Building $(APP_NAME)..." + @mkdir -p $(BUILD_DIR) + @go build -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH) + @echo "Build complete: $(BUILD_DIR)/$(APP_NAME)" + +.PHONY: run +run: build + @echo "Running $(APP_NAME) with default settings..." + @$(BUILD_DIR)/$(APP_NAME) + diff --git a/cmd/server/main.go b/cmd/server/main.go index 9ccec43..aeed8f5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "log" "os" "os/signal" @@ -10,10 +11,19 @@ import ( "github.com/vivek-pk/goadblock/internal/api" "github.com/vivek-pk/goadblock/internal/blocker" + "github.com/vivek-pk/goadblock/internal/config" "github.com/vivek-pk/goadblock/internal/dns" ) func main() { + configErr := config.InitConfig() + if configErr != nil { + log.Fatalf("Failed to load configs : %v", configErr) + } + + log.Printf("Configuration loaded - Using DNS port: %d, HTTP port: %d", + config.GetDnsPort(), config.GetHttpPort()) + // Initialize ad blocker adblocker := blocker.New() @@ -51,7 +61,7 @@ func main() { signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // Create API server first - apiServer, err := api.NewAPIServer(nil, 8080) + apiServer, err := api.NewAPIServer(nil, config.GetHttpPort()) if err != nil { log.Fatalf("Failed to create API server: %v", err) } @@ -69,10 +79,10 @@ func main() { apiServer.SetDNSServer(dnsServer) // Start servers one by one - log.Println("Starting DNS server on :53") + log.Printf("Starting DNS server on :%d", config.GetDnsPort()) dnsErrChan := make(chan error, 1) go func() { - if err := dnsServer.Start(":53"); err != nil { + if err := dnsServer.Start(fmt.Sprintf(":%d", config.GetDnsPort())); err != nil { dnsErrChan <- err } }() @@ -88,7 +98,7 @@ func main() { } // Now start the API server - log.Println("Starting API server on :8080") + log.Printf("Starting API server on :%d", config.GetHttpPort()) apiErrChan := make(chan error, 1) go func() { if err := apiServer.Start(); err != nil { diff --git a/go.mod b/go.mod index a471849..9264468 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,33 @@ module github.com/vivek-pk/goadblock require ( github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 github.com/miekg/dns v1.1.55 + github.com/spf13/pflag v1.0.6 + github.com/spf13/viper v1.20.0 + github.com/stretchr/testify v1.10.0 ) require ( - github.com/gorilla/mux v1.8.1 // indirect - golang.org/x/mod v0.7.0 // indirect - golang.org/x/net v0.2.0 // indirect - golang.org/x/sys v0.2.0 // indirect - golang.org/x/tools v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) go 1.23.1 diff --git a/go.sum b/go.sum index 1a95314..61839a0 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,66 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= +github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..3d0b418 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,73 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func InitConfig() error { + //VIPER Priority : flags -> env -> config -> default + + // Flags + pflag.Int("dns-port", 53, "Port for the DNS server") + pflag.Int("http-port", 8080, "Port for the HTTP server") + pflag.String("config", "", "Config file path") + + pflag.Parse() + + bindFlagsWithFormatting(pflag.CommandLine) + + // Env + viper.SetEnvPrefix("GOADBLOCK") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + viper.AutomaticEnv() + + // Config + configPath := viper.GetString("config") + if configPath != "" { + viper.SetConfigFile(configPath) + } else { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(".") + viper.AddConfigPath("$HOME/.goablock") + viper.AddConfigPath("/etc/goablock") + } + + if err := viper.ReadInConfig(); err != nil { + // It's okay if the config file doesn't exist + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return fmt.Errorf("error reading config file: %w", err) + } + } + + // Default Values + viper.SetDefault("http.port", 8080) + viper.SetDefault("dns.port", 53) + viper.SetDefault("config", "") + + return nil +} + +func bindFlagsWithFormatting(flagSet *pflag.FlagSet) { + flagSet.VisitAll(func(flag *pflag.Flag) { + // Convert hyphen to dot notation for viper + name := strings.ReplaceAll(flag.Name, "-", ".") + viper.BindPFlag(name, flag) + }) +} + +func GetDnsPort() int { + return viper.GetInt("dns.port") +} + +func GetHttpPort() int { + return viper.GetInt("http.port") +} + +func GetConfigPath() string { + return viper.GetString("config") +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..eacd020 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,125 @@ +package config + +import ( + "os" + "testing" + + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func resetViper() { + viper.Reset() + pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) + os.Args = []string{"cmd"} +} + +func TestConfigDefaults(t *testing.T) { + resetViper() + + err := InitConfig() + assert.NoError(t, err) + assert.EqualValues(t, 53, GetDnsPort()) + assert.EqualValues(t, 8080, GetHttpPort()) +} + +func TestConfigFromFlags(t *testing.T) { + resetViper() + + os.Args = []string{"cmd", "--dns-port=54", "--http-port=3000"} + + err := InitConfig() + assert.NoError(t, err) + assert.EqualValues(t, 54, GetDnsPort()) + assert.EqualValues(t, 3000, GetHttpPort()) +} + +func TestConfigFromEnvVars(t *testing.T) { + resetViper() + + os.Setenv("GOADBLOCK_DNS_PORT", "55") + os.Setenv("GOADBLOCK_HTTP_PORT", "3001") + + err := InitConfig() + assert.NoError(t, err) + assert.EqualValues(t, 55, GetDnsPort()) + assert.EqualValues(t, 3001, GetHttpPort()) + + os.Unsetenv("GOADBLOCK_DNS_PORT") + os.Unsetenv("GOADBLOCK_HTTP_PORT") +} + +func TestConfigFromConfigFile(t *testing.T) { + resetViper() + configContent := `dns: + port: 50 + upstream: '8.8.8.8' + cache_size: 5000 + cache_ttl: 3600 + +http: + port: 5000 + username: 'admin' + password: 'changeme'` + + tmpfile, err := os.CreateTemp("", "config*.yaml") + assert.NoError(t, err, "Should create temp file") + defer os.Remove(tmpfile.Name()) + + _, err = tmpfile.WriteString(configContent) + assert.NoError(t, err, "Should write to temp file") + tmpfile.Close() + + // provide temp file as env + os.Setenv("GOADBLOCK_CONFIG", tmpfile.Name()) + + err = InitConfig() + assert.NoError(t, err) + assert.EqualValues(t, 50, GetDnsPort()) + assert.EqualValues(t, 5000, GetHttpPort()) + + os.Unsetenv("GOADBLOCK_CONFIG") +} + +func TestPriorityOrder(t *testing.T) { + resetViper() + + // Flags + os.Args = []string{"cmd", "--dns-port=60"} + + // Env Vars + os.Setenv("GOADBLOCK_DNS_PORT", "55") + os.Setenv("GOADBLOCK_HTTP_PORT", "3678") + + //Config File + configContent := `dns: + port: 50 + upstream: '8.8.8.8' + cache_size: 5000 + cache_ttl: 3600 + +http: + port: 5000 + username: 'admin' + password: 'changeme'` + tmpfile, err := os.CreateTemp("", "config*.yaml") + assert.NoError(t, err, "Should create temp file") + defer os.Remove(tmpfile.Name()) + + _, err = tmpfile.WriteString(configContent) + assert.NoError(t, err, "Should write to temp file") + tmpfile.Close() + + // provide temp file as env + os.Setenv("GOADBLOCK_CONFIG", tmpfile.Name()) + + err = InitConfig() + assert.NoError(t, err) + assert.EqualValues(t, 60, GetDnsPort()) + assert.EqualValues(t, 3678, GetHttpPort()) + + os.Unsetenv("GOADBLOCK_DNS_PORT") + os.Unsetenv("GOADBLOCK_HTTP_PORT") + os.Unsetenv("GOADBLOCK_CONFIG") +}