diff --git a/.dockerignore b/.dockerignore index 30ecf57..18fb1ea 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ Dockerfile -Dockerfile.geoip \ No newline at end of file +/etc/systemd +Dockerfile.geoip diff --git a/.gitignore b/.gitignore index 56d06b1..6337cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .vscode/ .idea/ /bin/ +.envrc diff --git a/Dockerfile b/Dockerfile index 7a4c32a..8e0d960 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Build -FROM golang:1.15-buster AS build -WORKDIR /go/src/github.com/mpolden/echoip +FROM golang:1.18-buster AS build +WORKDIR /go/src/github.com/levelsoftware/echoip COPY . . # Must build without cgo because libc is unavailable in runtime image @@ -8,7 +8,7 @@ ENV GO111MODULE=on CGO_ENABLED=0 RUN make # Run -FROM scratch +FROM alpine EXPOSE 8080 COPY --from=build /go/bin/echoip /opt/echoip/ diff --git a/Makefile b/Makefile index ea19fd9..059eb74 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ DOCKER ?= docker -DOCKER_IMAGE ?= mpolden/echoip +DOCKER_IMAGE ?= levelsoftware/echoip OS := $(shell uname) ifeq ($(OS),Linux) TAR_OPTS := --wildcards @@ -22,11 +22,11 @@ check-fmt: lint: check-fmt vet -install: install-config +install: go install ./... install-config: - sudo install -D etc/echoip/config.toml /etc/echoip/config.toml + install -D etc/echoip/config.toml /etc/echoip/config.toml databases := GeoLite2-City GeoLite2-Country GeoLite2-ASN @@ -51,7 +51,7 @@ docker-multiarch-builder: $(DOCKER) run --rm --privileged multiarch/qemu-user-static --reset -p yes docker-build: - $(DOCKER) build -t $(DOCKER_IMAGE) . + $(DOCKER) build -t $(DOCKER_IMAGE) . docker-login: @echo "$(DOCKER_PASSWORD)" | $(DOCKER) login -u "$(DOCKER_USERNAME)" --password-stdin diff --git a/README.md b/README.md index c10182e..a7fe331 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,28 @@ CityFile = "" AsnFile = "" ``` +### Environment Variables for Configuration +You can also use environment variables for configuration, most likely used for Docker. + +``` +ECHOIP_LISTEN=":8080" +ECHOIP_TEMPLATE_DIR="html/" +ECHOIP_REDIS_URL="redis://localhost:6379" +ECHOIP_DATABASE="ipstack" +ECHOIP_TRUSTED_HEADERS="X-Real-IP,X-Forwaded-For" +ECHOIP_IPSTACK_API_KEY="askdfj39sjdkf29dsjfk39sdfkj3" +ECHOIP_GEOIP_COUNTRY_FILE="/full/path/to/file.db" +ECHOIP_GEOIP_CITY_FILE="/full/path/to/file.db" +ECHOIP_GEOIP_ASN_FILE="/full/path/to/file.db" +ECHOIP_CACHE_TTL=3600 +ECHOIP_REVERSE_LOOKUP=true +ECHOIP_PORT_LOOKUP=true +ECHOIP_SHOW_SPONSOR=true +ECHOIP_PROFILE=false +ECHOIP_IPSTACK_USE_HTTPS=true +ECHOIP_IPSTACK_ENABLE_SECURITY=true +``` + ### Caching with Redis You can connect EchoIP to a Redis client to cache each request per IP. You can configure the life of the key in `config.CacheTtl`. diff --git a/cmd/echoip/main.go b/cmd/echoip/main.go index 3d43e53..166e7f9 100644 --- a/cmd/echoip/main.go +++ b/cmd/echoip/main.go @@ -9,6 +9,7 @@ import ( "github.com/BurntSushi/toml" "github.com/levelsoftware/echoip/cache" + "github.com/levelsoftware/echoip/config" "github.com/levelsoftware/echoip/http" "github.com/levelsoftware/echoip/iputil" "github.com/levelsoftware/echoip/iputil/geo" @@ -33,61 +34,38 @@ func init() { log.SetFlags(log.Lshortfile) } -type IPStack struct { - ApiKey string - UseHttps bool - EnableSecurity bool -} - -type GeoIP struct { - CountryFile string - CityFile string - AsnFile string -} - -type Config struct { - Listen string - TemplateDir string - RedisUrl string - CacheTtl int - ReverseLookup bool - PortLookup bool - ShowSponsor bool - TrustedHeaders []string - - Database string - Profile bool - - IPStack IPStack - GeoIP GeoIP -} - func main() { - file, err := os.Open("/etc/echoip/config.toml") + runConfig, err := config.GetConfig() + if err != nil { - panic(err) + log.Fatalf("Error building configuration: %s", err) } + + file, err := os.Open("/etc/echoip/config.toml") defer file.Close() - var config Config - - b, err := io.ReadAll(file) if err != nil { - panic(err) - } + log.Printf("Error opening config file (/etc/echoip/config.toml): %s", err) + } else { + var b []byte + b, err = io.ReadAll(file) + if err != nil { + log.Printf("Error reading config file (/etc/echoip/config.toml): %s", err) + } - err = toml.Unmarshal(b, &config) - if err != nil { - panic(err) + err = toml.Unmarshal(b, &runConfig) + if err != nil { + log.Fatalf("Error parsing config file: %s", err) + } } var parser parser.Parser - if config.Database == "geoip" { + if runConfig.Database == "geoip" { log.Print("Using GeoIP for IP database") geo, err := geo.Open( - config.GeoIP.CountryFile, - config.GeoIP.CityFile, - config.GeoIP.AsnFile, + runConfig.GeoIP.CountryFile, + runConfig.GeoIP.CityFile, + runConfig.GeoIP.AsnFile, ) if err != nil { log.Fatal(err) @@ -95,27 +73,27 @@ func main() { parser = &geo } - if config.Database == "ipstack" { - log.Print("Using GeoIP for IP database") - if config.IPStack.EnableSecurity { - log.Print("Enable Security Module ( Requires Professional Plus account )") + if runConfig.Database == "ipstack" { + log.Print("Using IP Stack for IP database") + if runConfig.IPStack.EnableSecurity { + log.Print("Enable IP Stack Security Module ( Requires Professional Plus account )") } - enableSecurity := ipstackApi.ParamEnableSecurity(config.IPStack.EnableSecurity) - apiKey := ipstackApi.ParamToken(config.IPStack.ApiKey) - useHttps := ipstackApi.ParamUseHTTPS(config.IPStack.UseHttps) - if config.IPStack.UseHttps { + enableSecurity := ipstackApi.ParamEnableSecurity(runConfig.IPStack.EnableSecurity) + apiKey := ipstackApi.ParamToken(runConfig.IPStack.ApiKey) + useHttps := ipstackApi.ParamUseHTTPS(runConfig.IPStack.UseHttps) + if runConfig.IPStack.UseHttps { log.Print("Use IP Stack HTTPS API ( Requires non-free account )") } if err := ipstackApi.Init(apiKey, enableSecurity, useHttps); err != nil { - log.Fatal(err) + log.Fatalf("Error initializing IP Stack client: %s", err) } ips := ipstack.IPStack{} parser = &ips } var serverCache cache.Cache - if len(config.RedisUrl) > 0 { - redisCache, err := cache.RedisCache(config.RedisUrl) + if len(runConfig.RedisUrl) > 0 { + redisCache, err := cache.RedisCache(runConfig.RedisUrl) serverCache = &redisCache if err != nil { log.Fatal(err) @@ -124,34 +102,35 @@ func main() { serverCache = &cache.Null{} } - server := http.New(parser, serverCache, config.CacheTtl, config.Profile) - server.IPHeaders = config.TrustedHeaders + server := http.New(parser, serverCache, runConfig.CacheTtl, runConfig.Profile) + server.IPHeaders = runConfig.TrustedHeaders - if _, err := os.Stat(config.TemplateDir); err == nil { - server.Template = config.TemplateDir + if _, err := os.Stat(runConfig.TemplateDir); err == nil { + server.Template = runConfig.TemplateDir } else { - log.Printf("Not configuring default handler: Template not found: %s", config.TemplateDir) + log.Printf("Not configuring default handler: Template not found: %s", runConfig.TemplateDir) } - if config.ReverseLookup { + if runConfig.ReverseLookup { log.Println("Enabling reverse lookup") server.LookupAddr = iputil.LookupAddr } - if config.PortLookup { + if runConfig.PortLookup { log.Println("Enabling port lookup") server.LookupPort = iputil.LookupPort } - if config.ShowSponsor { + if runConfig.ShowSponsor { log.Println("Enabling sponsor logo") - server.Sponsor = config.ShowSponsor + server.Sponsor = runConfig.ShowSponsor } - if len(config.TrustedHeaders) > 0 { - log.Printf("Trusting remote IP from header(s): %s", config.TrustedHeaders) + if len(runConfig.TrustedHeaders) > 0 { + log.Printf("Trusting remote IP from header(s): %s", runConfig.TrustedHeaders) } - if config.Profile { + if runConfig.Profile { log.Printf("Enabling profiling handlers") } - log.Printf("Listening on http://%s", config.Listen) - if err := server.ListenAndServe(config.Listen); err != nil { + + log.Printf("Listening on http://%s", runConfig.Listen) + if err := server.ListenAndServe(runConfig.Listen); err != nil { log.Fatal(err) } } diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..93d5c6e --- /dev/null +++ b/config/config.go @@ -0,0 +1,138 @@ +package config + +import ( + "os" + "strconv" + "strings" +) + +type IPStack struct { + ApiKey string + UseHttps bool + EnableSecurity bool +} + +type GeoIP struct { + CountryFile string + CityFile string + AsnFile string +} + +type Config struct { + Listen string + TemplateDir string + RedisUrl string + CacheTtl int + ReverseLookup bool + PortLookup bool + ShowSponsor bool + TrustedHeaders []string + + Database string + Profile bool + + IPStack IPStack + GeoIP GeoIP +} + +func GetConfig() (Config, error) { + defaultConfig := Config{ + Listen: getenv_string("ECHOIP_LISTEN", ":8080"), + TemplateDir: getenv_string("ECHOIP_TEMPLATE_DIR", "html/"), + RedisUrl: getenv_string("ECHOIP_REDIS_URL", ""), + Database: getenv_string("ECHOIP_DATABASE", "geoip"), + IPStack: IPStack{ + ApiKey: getenv_string("ECHOIP_IPSTACK_API_KEY", ""), + }, + GeoIP: GeoIP{ + CountryFile: getenv_string("ECHOIP_GEOIP_COUNTRY_FILE", ""), + CityFile: getenv_string("ECHOIP_GEOIP_CITY_FILE", ""), + AsnFile: getenv_string("ECHOIP_GEOIP_ASN_FILE", ""), + }, + } + + cacheTtl, err := getenv_int("ECHOIP_CACHE_TTL", 3600) + if err != nil { + return Config{}, err + } + defaultConfig.CacheTtl = cacheTtl + + reverseLookup, err := getenv_bool("ECHOIP_REVERSE_LOOKUP", false) + if err != nil { + return Config{}, err + } + defaultConfig.ReverseLookup = reverseLookup + + portLookup, err := getenv_bool("ECHOIP_PORT_LOOKUP", false) + if err != nil { + return Config{}, err + } + defaultConfig.PortLookup = portLookup + + showSponsor, err := getenv_bool("ECHOIP_SHOW_SPONSOR", false) + if err != nil { + return Config{}, err + } + defaultConfig.ShowSponsor = showSponsor + + profile, err := getenv_bool("ECHOIP_PROFILE", false) + if err != nil { + return Config{}, err + } + defaultConfig.Profile = profile + + ipStackUseHttps, err := getenv_bool("ECHOIP_IPSTACK_USE_HTTPS", false) + if err != nil { + return Config{}, err + } + defaultConfig.IPStack.UseHttps = ipStackUseHttps + + ipStackEnableSecurity, err := getenv_bool("ECHOIP_IPSTACK_ENABLE_SECURITY", false) + if err != nil { + return Config{}, err + } + defaultConfig.IPStack.EnableSecurity = ipStackEnableSecurity + + trustedHeaders := getenv_string("ECHOIP_TRUSTED_HEADERS", "") + defaultConfig.TrustedHeaders = strings.Split(trustedHeaders, ",") + + return defaultConfig, nil +} + +func getenv_int(key string, fallback int) (int, error) { + value := os.Getenv(key) + + if len(value) > 0 { + intValue, err := strconv.Atoi(value) + if err != nil { + return 0, err + } + + return intValue, nil + } + + return fallback, nil +} + +func getenv_string(key string, fallback string) string { + value := os.Getenv(key) + if len(value) == 0 { + return fallback + } + return value +} + +func getenv_bool(key string, fallback bool) (bool, error) { + value := os.Getenv(key) + + if len(value) > 0 { + boolValue, err := strconv.ParseBool(value) + if err != nil { + return false, err + } + + return boolValue, nil + } + + return fallback, nil +} diff --git a/docker-compose.yaml b/docker-compose.yaml index d52d03b..30de17a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,14 @@ version: '3.8' services: + echoip: + image: hub.01a.in/echoip:latest + environment: + - ECHOIP_DATABASE="ipstack" + - ECHOIP_IPSTACK_API_KEY="ipstack" + ports: + - '8080:8080' + cache: image: redis:6.2-alpine restart: always diff --git a/etc/echoip/config.toml b/etc/echoip/config.toml index f369a47..a4ff37d 100644 --- a/etc/echoip/config.toml +++ b/etc/echoip/config.toml @@ -1,5 +1,5 @@ Listen = ":8080" -TemplateDir = "html/index.html" +TemplateDir = "html" RedisUrl = "redis://localhost:6379" CacheTtl = 3600 # in seconds ReverseLookup = true diff --git a/go.mod b/go.mod index 8d8d475..dd74956 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/vmihailenco/go-tinylfu v0.2.2 // indirect github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.4.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index 016aac8..11a4dd1 100644 --- a/go.sum +++ b/go.sum @@ -128,15 +128,17 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -160,8 +162,9 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -174,8 +177,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=