diff --git a/cmd/whatismyip.go b/cmd/whatismyip.go index 74d0e49..78b3fe7 100644 --- a/cmd/whatismyip.go +++ b/cmd/whatismyip.go @@ -15,17 +15,17 @@ import ( "github.com/dcarrillo/whatismyip/internal/metrics" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/resolver" + "github.com/dcarrillo/whatismyip/router" "github.com/dcarrillo/whatismyip/server" "github.com/dcarrillo/whatismyip/service" "github.com/gin-contrib/secure" "github.com/patrickmn/go-cache" - "github.com/dcarrillo/whatismyip/router" "github.com/gin-gonic/gin" ) func main() { - o, err := setting.Setup(os.Args[1:]) + cfg, o, err := setting.Setup(os.Args[1:]) if err != nil { if errors.Is(err, flag.ErrHelp) || errors.Is(err, setting.ErrVersion) { fmt.Print(o) @@ -35,33 +35,44 @@ func main() { os.Exit(1) } - servers := []server.Server{} - engine := setupEngine() - - if setting.App.Resolver.Domain != "" { - store := cache.New(1*time.Minute, 10*time.Minute) - var dnsEngine *resolver.Resolver - if dnsEngine, err = resolver.Setup(store); err != nil { - log.Fatalf("Invalid resolver configuration: %s", err) - } - nameServer := server.NewDNSServer(context.Background(), dnsEngine.Handler()) - servers = append(servers, nameServer) - engine.Use(router.GetDNSDiscoveryHandler(store, setting.App.Resolver.Domain, setting.App.Resolver.RedirectPort)) - } - var geoSvc *service.Geo - if setting.App.GeodbPath.City != "" || setting.App.GeodbPath.ASN != "" { - if geoSvc, err = service.NewGeo(context.Background(), setting.App.GeodbPath.City, setting.App.GeodbPath.ASN); err != nil { + if cfg.GeodbPath.City != "" || cfg.GeodbPath.ASN != "" { + if geoSvc, err = service.NewGeo(context.Background(), cfg.GeodbPath.City, cfg.GeodbPath.ASN); err != nil { log.Fatalf("Failed to load geo databases: %s", err) } } - router.SetupTemplate(engine) - router.Setup(engine, geoSvc) - servers = slices.Concat(servers, setupHTTPServers(context.Background(), engine.Handler())) + servers := []server.Server{} + engine := setupEngine(cfg) - if setting.App.PrometheusAddress != "" { - prometheusServer := server.NewPrometheusServer(context.Background()) + if cfg.Resolver.Domain != "" { + store := cache.New(1*time.Minute, 10*time.Minute) + var dnsEngine *resolver.Resolver + if dnsEngine, err = resolver.Setup(store, resolver.Settings{ + Domain: cfg.Resolver.Domain, + ResourceRecords: cfg.Resolver.ResourceRecords, + RedirectPort: cfg.Resolver.RedirectPort, + IPv4: cfg.Resolver.Ipv4, + IPv6: cfg.Resolver.Ipv6, + }); err != nil { + log.Fatalf("Invalid resolver configuration: %s", err) + } + nameServer := server.NewDNSServer(context.Background(), dnsEngine.Handler()) + servers = append(servers, nameServer) + engine.Use(router.GetDNSDiscoveryHandler(store, geoSvc, cfg.Resolver.Domain, cfg.Resolver.RedirectPort)) + } + + rt := router.NewRouter(geoSvc, cfg.TrustedHeader, cfg.TrustedPortHeader, cfg.TemplatePath, cfg.DisableTCPScan) + router.SetupTemplate(engine, cfg.TemplatePath) + router.Setup(engine, rt) + servers = slices.Concat(servers, setupHTTPServers(context.Background(), engine.Handler(), cfg)) + + if cfg.PrometheusAddress != "" { + prometheusServer := server.NewPrometheusServer(context.Background(), cfg.PrometheusAddress, + server.Timeouts{ + ReadTimeout: cfg.Server.ReadTimeout, + WriteTimeout: cfg.Server.WriteTimeout, + }) servers = append(servers, prometheusServer) } @@ -69,18 +80,18 @@ func main() { whatismyip.Run() } -func setupEngine() *gin.Engine { +func setupEngine(cfg setting.Settings) *gin.Engine { gin.DisableConsoleColor() if os.Getenv(gin.EnvGinMode) == "" { gin.SetMode(gin.ReleaseMode) } engine := gin.New() engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter), gin.Recovery()) - if setting.App.PrometheusAddress != "" { + if cfg.PrometheusAddress != "" { metrics.Enable() engine.Use(metrics.GinMiddleware()) } - if setting.App.EnableSecureHeaders { + if cfg.EnableSecureHeaders { engine.Use(secure.New(secure.Config{ BrowserXssFilter: true, ContentTypeNosniff: true, @@ -88,24 +99,28 @@ func setupEngine() *gin.Engine { })) } _ = engine.SetTrustedProxies(nil) - engine.TrustedPlatform = setting.App.TrustedHeader + engine.TrustedPlatform = cfg.TrustedHeader return engine } -func setupHTTPServers(ctx context.Context, handler http.Handler) []server.Server { +func setupHTTPServers(ctx context.Context, handler http.Handler, cfg setting.Settings) []server.Server { var servers []server.Server + timeouts := server.Timeouts{ + ReadTimeout: cfg.Server.ReadTimeout, + WriteTimeout: cfg.Server.WriteTimeout, + } - if setting.App.BindAddress != "" { - tcpServer := server.NewTCPServer(ctx, handler) + if cfg.BindAddress != "" { + tcpServer := server.NewTCPServer(ctx, handler, cfg.BindAddress, timeouts) servers = append(servers, tcpServer) } - if setting.App.TLSAddress != "" { - tlsServer := server.NewTLSServer(ctx, handler) + if cfg.TLSAddress != "" { + tlsServer := server.NewTLSServer(ctx, handler, cfg.TLSAddress, cfg.TLSCrtPath, cfg.TLSKeyPath, timeouts) servers = append(servers, tlsServer) - if setting.App.EnableHTTP3 { - quicServer := server.NewQuicServer(ctx, tlsServer) + if cfg.EnableHTTP3 { + quicServer := server.NewQuicServer(ctx, tlsServer, cfg.TLSAddress, cfg.TLSCrtPath, cfg.TLSKeyPath) servers = append(servers, quicServer) } } diff --git a/internal/httputils/http.go b/internal/httputils/http.go index 746bc58..60b4910 100644 --- a/internal/httputils/http.go +++ b/internal/httputils/http.go @@ -7,7 +7,6 @@ import ( "sort" "strings" - "github.com/dcarrillo/whatismyip/internal/setting" "github.com/gin-gonic/gin" ) @@ -30,10 +29,10 @@ func HeadersToSortedString(headers http.Header) string { } // GetHeadersWithoutTrustedHeaders returns a copy of the request headers with the trusted headers removed -func GetHeadersWithoutTrustedHeaders(ctx *gin.Context) http.Header { +func GetHeadersWithoutTrustedHeaders(ctx *gin.Context, trustedHeader, trustedPortHeader string) http.Header { h := ctx.Request.Header.Clone() - for _, k := range []string{setting.App.TrustedHeader, setting.App.TrustedPortHeader} { + for _, k := range []string{trustedHeader, trustedPortHeader} { delete(h, textproto.CanonicalMIMEHeaderKey(k)) } diff --git a/internal/setting/app.go b/internal/setting/app.go index 9035c16..9a6239a 100644 --- a/internal/setting/app.go +++ b/internal/setting/app.go @@ -29,7 +29,7 @@ type resolver struct { Ipv6 []string `yaml:"ipv6,omitempty"` } -type settings struct { +type Settings struct { GeodbPath geodbConf TemplatePath string BindAddress string @@ -51,75 +51,72 @@ const defaultAddress = ":8080" var ErrVersion = errors.New("setting: version requested") -var App = settings{ - // hard-coded for the time being - Server: serverSettings{ - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - }, -} - -func Setup(args []string) (output string, err error) { +func Setup(args []string) (cfg Settings, output string, err error) { flags := flag.NewFlagSet("whatismyip", flag.ContinueOnError) var buf bytes.Buffer var resolverConf string flags.SetOutput(&buf) - flags.StringVar(&App.GeodbPath.City, "geoip2-city", "", "Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory)") - flags.StringVar(&App.GeodbPath.ASN, "geoip2-asn", "", "Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory)") - flags.StringVar(&App.TemplatePath, "template", "", "Path to the template file") + cfg.Server = serverSettings{ + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + + flags.StringVar(&cfg.GeodbPath.City, "geoip2-city", "", "Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory)") + flags.StringVar(&cfg.GeodbPath.ASN, "geoip2-asn", "", "Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory)") + flags.StringVar(&cfg.TemplatePath, "template", "", "Path to the template file") flags.StringVar( &resolverConf, "resolver", "", "Path to the resolver configuration. It actually enables the resolver for DNS client discovery.") flags.StringVar( - &App.BindAddress, + &cfg.BindAddress, "bind", defaultAddress, "Listening address (see https://pkg.go.dev/net?#Listen)", ) flags.StringVar( - &App.TLSAddress, + &cfg.TLSAddress, "tls-bind", "", "Listening address for TLS (see https://pkg.go.dev/net?#Listen)", ) - flags.StringVar(&App.TLSCrtPath, "tls-crt", "", "When using TLS, path to certificate file") - flags.StringVar(&App.TLSKeyPath, "tls-key", "", "When using TLS, path to private key file") + flags.StringVar(&cfg.TLSCrtPath, "tls-crt", "", "When using TLS, path to certificate file") + flags.StringVar(&cfg.TLSKeyPath, "tls-key", "", "When using TLS, path to private key file") flags.StringVar( - &App.PrometheusAddress, + &cfg.PrometheusAddress, "metrics-bind", "", "Listening address for Prometheus metrics endpoint (see https://pkg.go.dev/net?#Listen). It enables the metrics available at the given address/port via the /metrics endpoint.", ) flags.StringVar( - &App.TrustedHeader, + &cfg.TrustedHeader, "trusted-header", "", "Trusted request header for remote IP (e.g. X-Real-IP). When using this feature if -trusted-port-header is not set the client port is shown as 'unknown'", ) flags.StringVar( - &App.TrustedPortHeader, + &cfg.TrustedPortHeader, "trusted-port-header", "", "Trusted request header for remote client port (e.g. X-Real-Port). When this parameter is set -trusted-header becomes mandatory", ) - flags.BoolVar(&App.version, "version", false, "Output version information and exit") + flags.BoolVar(&cfg.version, "version", false, "Output version information and exit") flags.BoolVar( - &App.EnableSecureHeaders, + &cfg.EnableSecureHeaders, "enable-secure-headers", false, "Add sane security-related headers to every response", ) flags.BoolVar( - &App.EnableHTTP3, + &cfg.EnableHTTP3, "enable-http3", false, "Enable HTTP/3 protocol. HTTP/3 requires --tls-bind set, as HTTP/3 starts as a TLS connection that then gets upgraded to UDP. The UDP port is the same as the one used for the TLS server.", ) flags.BoolVar( - &App.DisableTCPScan, + &cfg.DisableTCPScan, "disable-scan", false, "Disable TCP port scanning functionality", @@ -127,48 +124,48 @@ func Setup(args []string) (output string, err error) { err = flags.Parse(args) if err != nil { - return buf.String(), err + return cfg, buf.String(), err } - if App.version { - return fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion + if cfg.version { + return cfg, fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion } - if (App.GeodbPath.City != "" && App.GeodbPath.ASN == "") || (App.GeodbPath.City == "" && App.GeodbPath.ASN != "") { - return "", errors.New("both --geoip2-city and --geoip2-asn are mandatory to enable geo information") + if (cfg.GeodbPath.City != "" && cfg.GeodbPath.ASN == "") || (cfg.GeodbPath.City == "" && cfg.GeodbPath.ASN != "") { + return cfg, "", errors.New("both --geoip2-city and --geoip2-asn are mandatory to enable geo information") } - if App.TrustedPortHeader != "" && App.TrustedHeader == "" { - return "", errors.New("trusted-header is mandatory when trusted-port-header is set") + if cfg.TrustedPortHeader != "" && cfg.TrustedHeader == "" { + return cfg, "", errors.New("trusted-header is mandatory when trusted-port-header is set") } - if (App.TLSAddress != "") && (App.TLSCrtPath == "" || App.TLSKeyPath == "") { - return "", errors.New("in order to use TLS, the -tls-crt and -tls-key flags are mandatory") + if (cfg.TLSAddress != "") && (cfg.TLSCrtPath == "" || cfg.TLSKeyPath == "") { + return cfg, "", errors.New("in order to use TLS, the -tls-crt and -tls-key flags are mandatory") } - if App.EnableHTTP3 && App.TLSAddress == "" { - return "", errors.New("in order to use HTTP3, the -tls-bind is mandatory") + if cfg.EnableHTTP3 && cfg.TLSAddress == "" { + return cfg, "", errors.New("in order to use HTTP3, the -tls-bind is mandatory") } - if App.TemplatePath != "" { - info, err := os.Stat(App.TemplatePath) + if cfg.TemplatePath != "" { + info, err := os.Stat(cfg.TemplatePath) if err != nil { - return "", fmt.Errorf("template path: %w", err) + return cfg, "", fmt.Errorf("template path: %w", err) } if info.IsDir() { - return "", fmt.Errorf("%s must be a file", App.TemplatePath) + return cfg, "", fmt.Errorf("%s must be a file", cfg.TemplatePath) } } if resolverConf != "" { var err error - App.Resolver, err = readYAML(resolverConf) + cfg.Resolver, err = readYAML(resolverConf) if err != nil { - return "", fmt.Errorf("reading resolver configuration: %w", err) + return cfg, "", fmt.Errorf("reading resolver configuration: %w", err) } } - return buf.String(), nil + return cfg, buf.String(), nil } func readYAML(path string) (resolver resolver, err error) { diff --git a/internal/setting/app_test.go b/internal/setting/app_test.go index 96e956d..5951468 100644 --- a/internal/setting/app_test.go +++ b/internal/setting/app_test.go @@ -55,7 +55,7 @@ func TestParseMandatoryFlags(t *testing.T) { for _, tt := range mandatoryFlags { t.Run(strings.Join(tt.args, " "), func(t *testing.T) { - _, err := Setup(tt.args) + _, _, err := Setup(tt.args) require.Error(t, err) assert.Contains(t, err.Error(), "mandatory") }) @@ -65,11 +65,11 @@ func TestParseMandatoryFlags(t *testing.T) { func TestParseFlags(t *testing.T) { flags := []struct { args []string - conf settings + conf Settings }{ { []string{}, - settings{ + Settings{ BindAddress: ":8080", Server: serverSettings{ ReadTimeout: 10 * time.Second, @@ -79,7 +79,7 @@ func TestParseFlags(t *testing.T) { }, { []string{"-disable-scan"}, - settings{ + Settings{ BindAddress: ":8080", Server: serverSettings{ ReadTimeout: 10 * time.Second, @@ -90,7 +90,7 @@ func TestParseFlags(t *testing.T) { }, { []string{"-bind", ":8001", "-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path"}, - settings{ + Settings{ GeodbPath: geodbConf{ City: "/city-path", ASN: "/asn-path", @@ -107,7 +107,7 @@ func TestParseFlags(t *testing.T) { "-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-tls-bind", ":9000", "-tls-crt", "/crt-path", "-tls-key", "/key-path", }, - settings{ + Settings{ GeodbPath: geodbConf{ City: "/city-path", ASN: "/asn-path", @@ -127,7 +127,7 @@ func TestParseFlags(t *testing.T) { "-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-trusted-header", "header", "-trusted-port-header", "port-header", }, - settings{ + Settings{ GeodbPath: geodbConf{ City: "/city-path", ASN: "/asn-path", @@ -146,7 +146,7 @@ func TestParseFlags(t *testing.T) { "-geoip2-city", "/city-path", "-geoip2-asn", "/asn-path", "-trusted-header", "header", "-enable-secure-headers", }, - settings{ + Settings{ GeodbPath: geodbConf{ City: "/city-path", ASN: "/asn-path", @@ -164,9 +164,9 @@ func TestParseFlags(t *testing.T) { for _, tt := range flags { t.Run(strings.Join(tt.args, " "), func(t *testing.T) { - _, err := Setup(tt.args) + cfg, _, err := Setup(tt.args) require.Nil(t, err) - assert.True(t, reflect.DeepEqual(App, tt.conf)) + assert.True(t, reflect.DeepEqual(cfg, tt.conf)) }) } } @@ -176,7 +176,7 @@ func TestParseFlagsUsage(t *testing.T) { for _, arg := range usageArgs { t.Run(arg, func(t *testing.T) { - output, err := Setup([]string{arg}) + _, output, err := Setup([]string{arg}) assert.ErrorIs(t, err, flag.ErrHelp) assert.Contains(t, output, "Usage of") }) @@ -184,7 +184,7 @@ func TestParseFlagsUsage(t *testing.T) { } func TestParseFlagVersion(t *testing.T) { - output, err := Setup([]string{"-version"}) + _, output, err := Setup([]string{"-version"}) assert.ErrorIs(t, err, ErrVersion) assert.Contains(t, output, "whatismyip version") } @@ -209,7 +209,7 @@ func TestParseFlagTemplate(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := Setup(tc.flags) + _, _, err := Setup(tc.flags) require.Error(t, err) assert.Contains(t, err.Error(), tc.errMsg) }) diff --git a/resolver/setup.go b/resolver/setup.go index a73bb96..8ff7a8f 100644 --- a/resolver/setup.go +++ b/resolver/setup.go @@ -7,12 +7,19 @@ import ( "strings" "github.com/dcarrillo/whatismyip/internal/metrics" - "github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/internal/validator/uuid" "github.com/miekg/dns" "github.com/patrickmn/go-cache" ) +type Settings struct { + Domain string + ResourceRecords []string + RedirectPort string + IPv4 []string + IPv6 []string +} + type Resolver struct { handler *dns.ServeMux store *cache.Cache @@ -29,11 +36,11 @@ func ensureDotSuffix(s string) string { return s } -func Setup(store *cache.Cache) (*Resolver, error) { - domain := ensureDotSuffix(setting.App.Resolver.Domain) +func Setup(store *cache.Cache, cfg Settings) (*Resolver, error) { + domain := ensureDotSuffix(cfg.Domain) - rr := make([]dns.RR, 0, len(setting.App.Resolver.ResourceRecords)) - for _, res := range setting.App.Resolver.ResourceRecords { + rr := make([]dns.RR, 0, len(cfg.ResourceRecords)) + for _, res := range cfg.ResourceRecords { record, err := dns.NewRR(domain + " " + res) if err != nil { return nil, fmt.Errorf("parsing resource record %q: %w", res, err) @@ -41,11 +48,11 @@ func Setup(store *cache.Cache) (*Resolver, error) { rr = append(rr, record) } - ipv4, err := parseIPs(setting.App.Resolver.Ipv4) + ipv4, err := parseIPs(cfg.IPv4) if err != nil { return nil, fmt.Errorf("parsing ipv4 addresses: %w", err) } - ipv6, err := parseIPs(setting.App.Resolver.Ipv6) + ipv6, err := parseIPs(cfg.IPv6) if err != nil { return nil, fmt.Errorf("parsing ipv6 addresses: %w", err) } diff --git a/router/dns.go b/router/dns.go index cedad18..7eb7044 100644 --- a/router/dns.go +++ b/router/dns.go @@ -7,6 +7,7 @@ import ( "strings" validator "github.com/dcarrillo/whatismyip/internal/validator/uuid" + "github.com/dcarrillo/whatismyip/service" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/patrickmn/go-cache" @@ -27,7 +28,7 @@ type dnsData struct { // TODO // Implement a proper vhost manager instead of using a middleware -func GetDNSDiscoveryHandler(store *cache.Cache, domain string, redirectPort string) gin.HandlerFunc { +func GetDNSDiscoveryHandler(store *cache.Cache, geoSvc *service.Geo, domain string, redirectPort string) gin.HandlerFunc { return func(ctx *gin.Context) { host := hostWithoutPort(ctx.Request.Host) if host != domain && !strings.HasSuffix(host, "."+domain) { @@ -41,7 +42,7 @@ func GetDNSDiscoveryHandler(store *cache.Cache, domain string, redirectPort stri return } - handleDNS(ctx, store) + handleDNS(ctx, store, geoSvc) ctx.Abort() } } @@ -53,7 +54,7 @@ func hostWithoutPort(host string) string { return host } -func handleDNS(ctx *gin.Context, store *cache.Cache) { +func handleDNS(ctx *gin.Context, store *cache.Cache, geoSvc *service.Geo) { d := strings.Split(ctx.Request.Host, ".")[0] if !validator.IsValid(d) { ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) diff --git a/router/dns_test.go b/router/dns_test.go index 5688516..2370dd8 100644 --- a/router/dns_test.go +++ b/router/dns_test.go @@ -16,7 +16,7 @@ import ( func TestGetDNSDiscoveryHandler(t *testing.T) { store := cache.New(cache.NoExpiration, cache.NoExpiration) - handler := GetDNSDiscoveryHandler(store, domain, "") + handler := GetDNSDiscoveryHandler(store, rt.geo, domain, "") t.Run("calls next if host does not have domain suffix", func(t *testing.T) { req, _ := http.NewRequest("GET", "/", nil) @@ -106,7 +106,7 @@ func TestHandleDNS(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req - handleDNS(c, store) + handleDNS(c, store, rt.geo) assert.Equal(t, http.StatusNotFound, w.Code) }) } @@ -144,7 +144,7 @@ func TestAcceptDNSRequest(t *testing.T) { c.Request = req store.Set(u, testIP.ipv4, cache.DefaultExpiration) - handleDNS(c, store) + handleDNS(c, store, rt.geo) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, tt.want, w.Body.String()) diff --git a/router/generic.go b/router/generic.go index 7ce4459..34dab49 100644 --- a/router/generic.go +++ b/router/generic.go @@ -6,7 +6,6 @@ import ( "path/filepath" "github.com/dcarrillo/whatismyip/internal/httputils" - "github.com/dcarrillo/whatismyip/internal/setting" "github.com/gin-gonic/gin" ) @@ -31,31 +30,31 @@ type JSONResponse struct { GeoResponse } -func getRoot(ctx *gin.Context) { +func (rt *Router) getRoot(ctx *gin.Context) { switch ctx.NegotiateFormat(gin.MIMEPlain, gin.MIMEHTML, gin.MIMEJSON) { case gin.MIMEHTML: name := "home" - if setting.App.TemplatePath != "" { - name = filepath.Base(setting.App.TemplatePath) + if rt.templatePath != "" { + name = filepath.Base(rt.templatePath) } - ctx.HTML(http.StatusOK, name, jsonOutput(ctx)) + ctx.HTML(http.StatusOK, name, rt.jsonOutput(ctx)) case gin.MIMEJSON: - getJSON(ctx) + rt.getJSON(ctx) default: ctx.String(http.StatusOK, ctx.ClientIP()+"\n") } } -func getClientPort(ctx *gin.Context) string { +func (rt *Router) getClientPort(ctx *gin.Context) string { var port string - if setting.App.TrustedPortHeader == "" { - if setting.App.TrustedHeader != "" { + if rt.trustedPortHeader == "" { + if rt.trustedHeader != "" { port = "unknown" } else { _, port, _ = net.SplitHostPort(ctx.Request.RemoteAddr) } } else { - port = ctx.GetHeader(setting.App.TrustedPortHeader) + port = ctx.GetHeader(rt.trustedPortHeader) if port == "" { port = "unknown" } @@ -64,37 +63,37 @@ func getClientPort(ctx *gin.Context) string { return port } -func getClientPortAsString(ctx *gin.Context) { - ctx.String(http.StatusOK, getClientPort(ctx)+"\n") +func (rt *Router) getClientPortAsString(ctx *gin.Context) { + ctx.String(http.StatusOK, rt.getClientPort(ctx)+"\n") } -func getAllAsString(ctx *gin.Context) { +func (rt *Router) getAllAsString(ctx *gin.Context) { ip := net.ParseIP(ctx.ClientIP()) output := "IP: " + ip.String() + "\n" - output += "Client Port: " + getClientPort(ctx) + "\n" + output += "Client Port: " + rt.getClientPort(ctx) + "\n" - if geoSvc != nil { - if cityRecord := geoSvc.LookUpCity(ip); cityRecord != nil { + if rt.geo != nil { + if cityRecord := rt.geo.LookUpCity(ip); cityRecord != nil { output += geoCityRecordToString(cityRecord) + "\n" } - if asnRecord := geoSvc.LookUpASN(ip); asnRecord != nil { + if asnRecord := rt.geo.LookUpASN(ip); asnRecord != nil { output += geoASNRecordToString(asnRecord) + "\n" } } - h := httputils.GetHeadersWithoutTrustedHeaders(ctx) + h := httputils.GetHeadersWithoutTrustedHeaders(ctx, rt.trustedHeader, rt.trustedPortHeader) h.Set("Host", ctx.Request.Host) output += httputils.HeadersToSortedString(h) ctx.String(http.StatusOK, output) } -func getJSON(ctx *gin.Context) { - ctx.JSON(http.StatusOK, jsonOutput(ctx)) +func (rt *Router) getJSON(ctx *gin.Context) { + ctx.JSON(http.StatusOK, rt.jsonOutput(ctx)) } -func jsonOutput(ctx *gin.Context) JSONResponse { +func (rt *Router) jsonOutput(ctx *gin.Context) JSONResponse { ip := net.ParseIP(ctx.ClientIP()) var version byte = 4 @@ -103,8 +102,8 @@ func jsonOutput(ctx *gin.Context) JSONResponse { } geoResp := GeoResponse{} - if geoSvc != nil { - if cityRecord := geoSvc.LookUpCity(ip); cityRecord != nil { + if rt.geo != nil { + if cityRecord := rt.geo.LookUpCity(ip); cityRecord != nil { geoResp.Country = cityRecord.Country.Names["en"] geoResp.CountryCode = cityRecord.Country.ISOCode geoResp.City = cityRecord.City.Names["en"] @@ -113,7 +112,7 @@ func jsonOutput(ctx *gin.Context) JSONResponse { geoResp.PostalCode = cityRecord.Postal.Code geoResp.TimeZone = cityRecord.Location.TimeZone } - if asnRecord := geoSvc.LookUpASN(ip); asnRecord != nil { + if asnRecord := rt.geo.LookUpASN(ip); asnRecord != nil { geoResp.ASN = asnRecord.AutonomousSystemNumber geoResp.ASNOrganization = asnRecord.AutonomousSystemOrganization } @@ -122,9 +121,9 @@ func jsonOutput(ctx *gin.Context) JSONResponse { return JSONResponse{ IP: ip.String(), IPVersion: version, - ClientPort: getClientPort(ctx), + ClientPort: rt.getClientPort(ctx), Host: ctx.Request.Host, - Headers: httputils.GetHeadersWithoutTrustedHeaders(ctx), + Headers: httputils.GetHeadersWithoutTrustedHeaders(ctx, rt.trustedHeader, rt.trustedPortHeader), GeoResponse: geoResp, } } diff --git a/router/generic_test.go b/router/generic_test.go index 02ee2f3..4f13064 100644 --- a/router/generic_test.go +++ b/router/generic_test.go @@ -1,12 +1,14 @@ package router import ( + "context" "net" "net/http" "net/http/httptest" "testing" - "github.com/dcarrillo/whatismyip/internal/setting" + "github.com/dcarrillo/whatismyip/service" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) @@ -99,8 +101,9 @@ func TestHost(t *testing.T) { func TestClientPort(t *testing.T) { type args struct { - params []string - headers map[string][]string + trustedHeader string + trustedPortHeader string + headers map[string][]string } tests := []struct { name string @@ -114,35 +117,23 @@ func TestClientPort(t *testing.T) { { name: "Trusted header only set", args: args{ - params: []string{ - "-geoip2-city", "city", - "-geoip2-asn", "asn", - "-trusted-header", trustedHeader, - }, + trustedHeader: trustedHeader, }, expected: "unknown\n", }, { name: "Trusted and port header set but not included in headers", args: args{ - params: []string{ - "-geoip2-city", "city", - "-geoip2-asn", "asn", - "-trusted-header", trustedHeader, - "-trusted-port-header", trustedPortHeader, - }, + trustedHeader: trustedHeader, + trustedPortHeader: trustedPortHeader, }, expected: "unknown\n", }, { name: "Trusted and port header set and included in headers", args: args{ - params: []string{ - "-geoip2-city", "city", - "-geoip2-asn", "asn", - "-trusted-header", trustedHeader, - "-trusted-port-header", trustedPortHeader, - }, + trustedHeader: trustedHeader, + trustedPortHeader: trustedPortHeader, headers: map[string][]string{ trustedHeader: {testIP.ipv4}, trustedPortHeader: {"1001"}, @@ -153,19 +144,22 @@ func TestClientPort(t *testing.T) { } for _, tt := range tests { - _, _ = setting.Setup(tt.args.params) t.Run(tt.name, func(t *testing.T) { + engine := gin.Default() + engine.TrustedPlatform = tt.args.trustedHeader + r := NewRouter(nil, tt.args.trustedHeader, tt.args.trustedPortHeader, "", false) + Setup(engine, r) + req, _ := http.NewRequest("GET", "/client-port", nil) req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000") req.Header = tt.args.headers w := httptest.NewRecorder() - app.ServeHTTP(w, req) + engine.ServeHTTP(w, req) assert.Equal(t, 200, w.Code) assert.Equal(t, contentType.text, w.Header().Get("Content-Type")) assert.Equal(t, tt.expected, w.Body.String()) - t.Log(w.Header()) }) } } @@ -181,14 +175,11 @@ func TestNotFound(t *testing.T) { } func TestJSON(t *testing.T) { - _, _ = setting.Setup( - []string{ - "-geoip2-city", "city", - "-geoip2-asn", "asn", - "-trusted-header", trustedHeader, - "-trusted-port-header", trustedPortHeader, - }, - ) + svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb") + engine := gin.Default() + engine.TrustedPlatform = trustedHeader + r := NewRouter(svc, trustedHeader, trustedPortHeader, "", false) + Setup(engine, r) type args struct { ip string @@ -222,7 +213,7 @@ func TestJSON(t *testing.T) { req.Header.Set(trustedPortHeader, "1001") w := httptest.NewRecorder() - app.ServeHTTP(w, req) + engine.ServeHTTP(w, req) assert.Equal(t, 200, w.Code) assert.Equal(t, contentType.json, w.Header().Get("Content-Type")) @@ -248,14 +239,11 @@ ASN Organization: Header1: one Host: test ` - _, _ = setting.Setup( - []string{ - "-geoip2-city", "city", - "-geoip2-asn", "asn", - "-trusted-header", trustedHeader, - "-trusted-port-header", trustedPortHeader, - }, - ) + svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb") + engine := gin.Default() + engine.TrustedPlatform = trustedHeader + r := NewRouter(svc, trustedHeader, trustedPortHeader, "", false) + Setup(engine, r) req, _ := http.NewRequest("GET", "/all", nil) req.RemoteAddr = net.JoinHostPort(testIP.ipv4, "1000") @@ -265,7 +253,7 @@ Host: test req.Header.Set("Header1", "one") w := httptest.NewRecorder() - app.ServeHTTP(w, req) + engine.ServeHTTP(w, req) assert.Equal(t, 200, w.Code) assert.Equal(t, contentType.text, w.Header().Get("Content-Type")) diff --git a/router/geo.go b/router/geo.go index 5a1f97c..29784e1 100644 --- a/router/geo.go +++ b/router/geo.go @@ -81,13 +81,13 @@ var asnOutput = map[string]asnDataFormatter{ }, } -func getGeoAsString(ctx *gin.Context) { - if geoSvc == nil { +func (rt *Router) getGeoAsString(ctx *gin.Context) { + if rt.geo == nil { ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) return } - record := geoSvc.LookUpCity(net.ParseIP(ctx.ClientIP())) + record := rt.geo.LookUpCity(net.ParseIP(ctx.ClientIP())) if record == nil { ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) return @@ -107,13 +107,13 @@ func getGeoAsString(ctx *gin.Context) { ctx.String(http.StatusOK, g.format(record)) } -func getASNAsString(ctx *gin.Context) { - if geoSvc == nil { +func (rt *Router) getASNAsString(ctx *gin.Context) { + if rt.geo == nil { ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) return } - record := geoSvc.LookUpASN(net.ParseIP(ctx.ClientIP())) + record := rt.geo.LookUpASN(net.ParseIP(ctx.ClientIP())) if record == nil { ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) return diff --git a/router/headers.go b/router/headers.go index da4b863..61b0733 100644 --- a/router/headers.go +++ b/router/headers.go @@ -9,15 +9,14 @@ import ( "github.com/gin-gonic/gin" ) -func getHeadersAsSortedString(ctx *gin.Context) { - h := httputils.GetHeadersWithoutTrustedHeaders(ctx) +func (rt *Router) getHeadersAsSortedString(ctx *gin.Context) { + h := httputils.GetHeadersWithoutTrustedHeaders(ctx, rt.trustedHeader, rt.trustedPortHeader) h.Set("Host", ctx.Request.Host) - ctx.String(http.StatusOK, httputils.HeadersToSortedString(h)) } -func getHeaderAsString(ctx *gin.Context) { - headers := httputils.GetHeadersWithoutTrustedHeaders(ctx) +func (rt *Router) getHeaderAsString(ctx *gin.Context) { + headers := httputils.GetHeadersWithoutTrustedHeaders(ctx, rt.trustedHeader, rt.trustedPortHeader) h := ctx.Params.ByName("header") if v := headers.Get(ctx.Params.ByName("header")); v != "" { diff --git a/router/headers_test.go b/router/headers_test.go index 889a507..97ec1fd 100644 --- a/router/headers_test.go +++ b/router/headers_test.go @@ -1,11 +1,13 @@ package router import ( + "context" "net/http" "net/http/httptest" "testing" - "github.com/dcarrillo/whatismyip/internal/setting" + "github.com/dcarrillo/whatismyip/service" + "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) @@ -27,12 +29,11 @@ Header2: value22 Header3: value3 Host: ` - _, _ = setting.Setup([]string{ - "-geoip2-city", "city", - "-geoip2-asn", "asn", - "-trusted-header", trustedHeader, - "-trusted-port-header", trustedPortHeader, - }) + svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb") + engine := gin.Default() + engine.TrustedPlatform = trustedHeader + r := NewRouter(svc, trustedHeader, trustedPortHeader, "", false) + Setup(engine, r) req, _ := http.NewRequest("GET", "/headers", nil) req.Header = map[string][]string{ "Header1": {"value1"}, @@ -43,7 +44,7 @@ Host: req.Header.Set(trustedPortHeader, "1025") w := httptest.NewRecorder() - app.ServeHTTP(w, req) + engine.ServeHTTP(w, req) assert.Equal(t, 200, w.Code) assert.Equal(t, contentType.text, w.Header().Get("Content-Type")) diff --git a/router/port_scanner.go b/router/port_scanner.go index c6f604b..423702a 100644 --- a/router/port_scanner.go +++ b/router/port_scanner.go @@ -17,7 +17,7 @@ type JSONScanResponse struct { Reason string `json:"reason"` } -func scanTCPPort(ctx *gin.Context) { +func (rt *Router) scanTCPPort(ctx *gin.Context) { port, err := strconv.Atoi(ctx.Params.ByName("port")) if err == nil && (port < 1 || port > 65535) { err = fmt.Errorf("%d is not a valid port number", port) diff --git a/router/setup.go b/router/setup.go index cd65694..1c28a52 100644 --- a/router/setup.go +++ b/router/setup.go @@ -4,35 +4,49 @@ import ( "html/template" "log" - "github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/service" "github.com/gin-gonic/gin" ) -var geoSvc *service.Geo +type Router struct { + geo *service.Geo + trustedHeader string + trustedPortHeader string + templatePath string + disableScan bool +} -func SetupTemplate(r *gin.Engine) { - if setting.App.TemplatePath == "" { +func NewRouter(geo *service.Geo, trustedHeader, trustedPortHeader, templatePath string, disableScan bool) *Router { + return &Router{ + geo: geo, + trustedHeader: trustedHeader, + trustedPortHeader: trustedPortHeader, + templatePath: templatePath, + disableScan: disableScan, + } +} + +func SetupTemplate(r *gin.Engine, templatePath string) { + if templatePath == "" { r.SetHTMLTemplate(template.Must(template.New("home").Parse(home))) } else { - log.Printf("Template %s has been loaded", setting.App.TemplatePath) - r.LoadHTMLFiles(setting.App.TemplatePath) + log.Printf("Template %s has been loaded", templatePath) + r.LoadHTMLFiles(templatePath) } } -func Setup(r *gin.Engine, geo *service.Geo) { - geoSvc = geo - r.GET("/", getRoot) - if !setting.App.DisableTCPScan { - r.GET("/scan/tcp/:port", scanTCPPort) +func Setup(r *gin.Engine, rt *Router) { + r.GET("/", rt.getRoot) + if !rt.disableScan { + r.GET("/scan/tcp/:port", rt.scanTCPPort) } - r.GET("/client-port", getClientPortAsString) - r.GET("/geo", getGeoAsString) - r.GET("/geo/:field", getGeoAsString) - r.GET("/asn", getASNAsString) - r.GET("/asn/:field", getASNAsString) - r.GET("/headers", getHeadersAsSortedString) - r.GET("/all", getAllAsString) - r.GET("/json", getJSON) - r.GET("/:header", getHeaderAsString) + r.GET("/client-port", rt.getClientPortAsString) + r.GET("/geo", rt.getGeoAsString) + r.GET("/geo/:field", rt.getGeoAsString) + r.GET("/asn", rt.getASNAsString) + r.GET("/asn/:field", rt.getASNAsString) + r.GET("/headers", rt.getHeadersAsSortedString) + r.GET("/all", rt.getAllAsString) + r.GET("/json", rt.getJSON) + r.GET("/:header", rt.getHeaderAsString) } diff --git a/router/setup_test.go b/router/setup_test.go index b463c4f..27f62a4 100644 --- a/router/setup_test.go +++ b/router/setup_test.go @@ -47,11 +47,14 @@ const ( domain = "dns.example.com" ) +var rt *Router + func TestMain(m *testing.M) { app = gin.Default() app.TrustedPlatform = trustedHeader svc, _ := service.NewGeo(context.Background(), "../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb") - Setup(app, svc) + rt = NewRouter(svc, trustedHeader, "", "", false) + Setup(app, rt) os.Exit(m.Run()) } diff --git a/server/prometheus.go b/server/prometheus.go index 88990c5..be12ee0 100644 --- a/server/prometheus.go +++ b/server/prometheus.go @@ -6,18 +6,21 @@ import ( "log" "net/http" - "github.com/dcarrillo/whatismyip/internal/setting" "github.com/prometheus/client_golang/prometheus/promhttp" ) type Prometheus struct { - server *http.Server - ctx context.Context + server *http.Server + ctx context.Context + addr string + timeouts Timeouts } -func NewPrometheusServer(ctx context.Context) *Prometheus { +func NewPrometheusServer(ctx context.Context, addr string, timeouts Timeouts) *Prometheus { return &Prometheus{ - ctx: ctx, + ctx: ctx, + addr: addr, + timeouts: timeouts, } } @@ -26,13 +29,13 @@ func (p *Prometheus) Start() { mux.Handle("/metrics", promhttp.Handler()) p.server = &http.Server{ - Addr: setting.App.PrometheusAddress, + Addr: p.addr, Handler: mux, - ReadTimeout: setting.App.Server.ReadTimeout, - WriteTimeout: setting.App.Server.WriteTimeout, + ReadTimeout: p.timeouts.ReadTimeout, + WriteTimeout: p.timeouts.WriteTimeout, } - log.Printf("Starting Prometheus server listening on %s", setting.App.PrometheusAddress) + log.Printf("Starting Prometheus server listening on %s", p.addr) go func() { if err := p.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) diff --git a/server/quic.go b/server/quic.go index 877204f..29b68f2 100644 --- a/server/quic.go +++ b/server/quic.go @@ -6,7 +6,6 @@ import ( "log" "net/http" - "github.com/dcarrillo/whatismyip/internal/setting" "github.com/quic-go/quic-go/http3" ) @@ -14,18 +13,24 @@ type Quic struct { server *http3.Server tlsServer *TLS ctx context.Context + addr string + crtPath string + keyPath string } -func NewQuicServer(ctx context.Context, tlsServer *TLS) *Quic { +func NewQuicServer(ctx context.Context, tlsServer *TLS, addr, crt, key string) *Quic { return &Quic{ tlsServer: tlsServer, ctx: ctx, + addr: addr, + crtPath: crt, + keyPath: key, } } func (q *Quic) Start() { q.server = &http3.Server{ - Addr: setting.App.TLSAddress, + Addr: q.addr, Handler: q.tlsServer.server.Handler, } @@ -38,9 +43,9 @@ func (q *Quic) Start() { parentHandler.ServeHTTP(rw, req) }) - log.Printf("Starting QUIC server listening on %s (udp)", setting.App.TLSAddress) + log.Printf("Starting QUIC server listening on %s (udp)", q.addr) go func() { - if err := q.server.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil && + if err := q.server.ListenAndServeTLS(q.crtPath, q.keyPath); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) } diff --git a/server/tcp.go b/server/tcp.go index 35075b6..326b5d4 100644 --- a/server/tcp.go +++ b/server/tcp.go @@ -5,32 +5,40 @@ import ( "errors" "log" "net/http" - - "github.com/dcarrillo/whatismyip/internal/setting" + "time" ) -type TCP struct { - server *http.Server - handler http.Handler - ctx context.Context +type Timeouts struct { + ReadTimeout time.Duration + WriteTimeout time.Duration } -func NewTCPServer(ctx context.Context, handler http.Handler) *TCP { +type TCP struct { + server *http.Server + handler http.Handler + ctx context.Context + addr string + timeouts Timeouts +} + +func NewTCPServer(ctx context.Context, handler http.Handler, addr string, timeouts Timeouts) *TCP { return &TCP{ - handler: handler, - ctx: ctx, + handler: handler, + ctx: ctx, + addr: addr, + timeouts: timeouts, } } func (t *TCP) Start() { t.server = &http.Server{ - Addr: setting.App.BindAddress, + Addr: t.addr, Handler: t.handler, - ReadTimeout: setting.App.Server.ReadTimeout, - WriteTimeout: setting.App.Server.WriteTimeout, + ReadTimeout: t.timeouts.ReadTimeout, + WriteTimeout: t.timeouts.WriteTimeout, } - log.Printf("Starting TCP server listening on %s", setting.App.BindAddress) + log.Printf("Starting TCP server listening on %s", t.addr) go func() { if err := t.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) diff --git a/server/tls.go b/server/tls.go index 795116a..cd3ff36 100644 --- a/server/tls.go +++ b/server/tls.go @@ -6,37 +6,43 @@ import ( "errors" "log" "net/http" - - "github.com/dcarrillo/whatismyip/internal/setting" ) type TLS struct { - server *http.Server - handler http.Handler - ctx context.Context + server *http.Server + handler http.Handler + ctx context.Context + addr string + crtPath string + keyPath string + timeouts Timeouts } -func NewTLSServer(ctx context.Context, handler http.Handler) *TLS { +func NewTLSServer(ctx context.Context, handler http.Handler, addr, crt, key string, timeouts Timeouts) *TLS { return &TLS{ - handler: handler, - ctx: ctx, + handler: handler, + ctx: ctx, + addr: addr, + crtPath: crt, + keyPath: key, + timeouts: timeouts, } } func (t *TLS) Start() { t.server = &http.Server{ - Addr: setting.App.TLSAddress, + Addr: t.addr, Handler: t.handler, - ReadTimeout: setting.App.Server.ReadTimeout, - WriteTimeout: setting.App.Server.WriteTimeout, + ReadTimeout: t.timeouts.ReadTimeout, + WriteTimeout: t.timeouts.WriteTimeout, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS12, }, } - log.Printf("Starting TLS server listening on %s", setting.App.TLSAddress) + log.Printf("Starting TLS server listening on %s", t.addr) go func() { - if err := t.server.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil && + if err := t.server.ListenAndServeTLS(t.crtPath, t.keyPath); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) }