diff --git a/.gitignore b/.gitignore index 85427b9..5022fb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /data/ /custom.html +/vendor/ diff --git a/.travis.yml b/.travis.yml index 7ed2851..d0bc237 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,15 @@ -sudo: false +language: minimal -language: go +services: + - docker -go: - - 1.x - - tip +script: + - make docker-build + +deploy: + - provider: script + script: make docker-push + - provider: heroku + app: ifconfig-co + api_key: + secure: IQG/ls5Zu0yua5Ynn5EL9JCPjo1/WcmS0z7BSaXWdgW+JIWFm7oF5z54bUZHl/q1tTuWzAJk59zSTYJijtQqh2Ssl3fLu3uFDwyJSrOuUu1akPlETam8NpdbH4lPkFp75JSDdDXV08c0APmeLL6gqRuTrUuufu69Wigjq4gLo+o= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..711588b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Build +FROM golang:1.12-stretch AS build +WORKDIR /go/src/github.com/mpolden/echoip +COPY . . +# Must build without cgo because libc is unavailable in runtime image +ENV GO111MODULE=on CGO_ENABLED=0 +RUN make + +# Run +FROM scratch +EXPOSE 8080 +COPY --from=build \ + /go/bin/echoip \ + /go/src/github.com/mpolden/echoip/index.html \ + /opt/echoip/ +WORKDIR /opt/echoip +ENTRYPOINT ["/opt/echoip/echoip"] diff --git a/Makefile b/Makefile index 9036973..d1da7df 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,48 @@ +DOCKER_IMAGE := mpolden/echoip OS := $(shell uname) ifeq ($(OS),Linux) TAR_OPTS := --wildcards endif -all: deps test vet install - -fmt: - go fmt ./... - -test: - go test ./... - -vet: - go vet ./... +all: deps lint test install deps: - go get -d -v ./... + go get ./... -install: +test: deps + go test ./... + +vet: deps + go vet ./... + +check-fmt: + bash -c "diff --line-format='%L' <(echo -n) <(gofmt -d -s .)" + +lint: check-fmt vet + +install: deps go install ./... databases := GeoLite2-City GeoLite2-Country $(databases): mkdir -p data - curl -fsSL -m 30 http://geolite.maxmind.com/download/geoip/database/$@.tar.gz | tar $(TAR_OPTS) --strip-components=1 -C $(PWD)/data -xzf - '*.mmdb' + curl -fsSL -m 30 https://geolite.maxmind.com/download/geoip/database/$@.tar.gz | tar $(TAR_OPTS) --strip-components=1 -C $(CURDIR)/data -xzf - '*.mmdb' test ! -f data/GeoLite2-City.mmdb || mv data/GeoLite2-City.mmdb data/city.mmdb test ! -f data/GeoLite2-Country.mmdb || mv data/GeoLite2-Country.mmdb data/country.mmdb geoip-download: $(databases) + +docker-build: + docker build -t $(DOCKER_IMAGE) . + +docker-login: + @echo "$(DOCKER_PASSWORD)" | docker login -u "$(DOCKER_USERNAME)" --password-stdin + +docker-test: + $(eval CONTAINER=$(shell docker run --rm --detach --publish-all $(DOCKER_IMAGE))) + $(eval DOCKER_PORT=$(shell docker port $(CONTAINER) | cut -d ":" -f 2)) + curl -fsS -m 5 localhost:$(DOCKER_PORT) > /dev/null; docker stop $(CONTAINER) + +docker-push: docker-test docker-login + docker push $(DOCKER_IMAGE) diff --git a/README.md b/README.md index 7652809..f41ec21 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ipd +# echoip -[![Build Status](https://travis-ci.org/mpolden/ipd.svg)](https://travis-ci.org/mpolden/ipd) +[![Build Status](https://travis-ci.org/mpolden/echoip.svg)](https://travis-ci.org/mpolden/echoip) A simple service for looking up your IP address. This is the code that powers https://ifconfig.co. @@ -66,9 +66,6 @@ $ curl ifconfig.co/port/80 Pass the appropriate flag (usually `-4` and `-6`) to your client to switch between IPv4 and IPv6 lookup. -The subdomains https://v4.ifconfig.co and https://v6.ifconfig.co can be used to -force IPv4 or IPv6 lookup. - ## Features * Easy to remember domain name @@ -92,17 +89,24 @@ force IPv4 or IPv6 lookup. Compiling requires the [Golang compiler](https://golang.org/) to be installed. This package can be installed with `go get`: -`go get github.com/mpolden/ipd/...` +`go get github.com/mpolden/echoip/...` For more information on building a Go project, see the [official Go documentation](https://golang.org/doc/code.html). +## Docker image + +A Docker image is available on [Docker +Hub](https://hub.docker.com/r/mpolden/echoip), which can be downloaded with: + +`docker pull mpolden/echoip` + ### Usage ``` -$ ipd -h +$ echoip -h Usage: - ipd [OPTIONS] + echoip [OPTIONS] Application Options: -f, --country-db=FILE Path to GeoIP country database diff --git a/cmd/echoip/main.go b/cmd/echoip/main.go new file mode 100644 index 0000000..9f18681 --- /dev/null +++ b/cmd/echoip/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "log" + + flags "github.com/jessevdk/go-flags" + + "os" + + "github.com/mpolden/echoip/http" + "github.com/mpolden/echoip/iputil" + "github.com/mpolden/echoip/iputil/geo" +) + +func main() { + var opts struct { + CountryDBPath string `short:"f" long:"country-db" description:"Path to GeoIP country database" value-name:"FILE" default:""` + CityDBPath string `short:"c" long:"city-db" description:"Path to GeoIP city database" value-name:"FILE" default:""` + ASNDBPath string `short:"a" long:"asn-db" description:"Path to GeoIP ASN database" value-name:"FILE" default:""` + Listen string `short:"l" long:"listen" description:"Listening address" value-name:"ADDR" default:":8080"` + ReverseLookup bool `short:"r" long:"reverse-lookup" description:"Perform reverse hostname lookups"` + PortLookup bool `short:"p" long:"port-lookup" description:"Enable port lookup"` + Template string `short:"t" long:"template" description:"Path to template" default:"index.html" value-name:"FILE"` + IPHeaders []string `short:"H" long:"trusted-header" description:"Header to trust for remote IP, if present (e.g. X-Real-IP)" value-name:"NAME"` + } + _, err := flags.ParseArgs(&opts, os.Args) + if err != nil { + os.Exit(1) + } + + log := log.New(os.Stderr, "echoip: ", 0) + r, err := geo.Open(opts.CountryDBPath, opts.CityDBPath, opts.ASNDBPath) + if err != nil { + log.Fatal(err) + } + + server := http.New(r) + server.IPHeaders = opts.IPHeaders + if _, err := os.Stat(opts.Template); err == nil { + server.Template = opts.Template + } else { + log.Printf("Not configuring default handler: Template not found: %s", opts.Template) + } + if opts.ReverseLookup { + log.Println("Enabling reverse lookup") + server.LookupAddr = iputil.LookupAddr + } + if opts.PortLookup { + log.Println("Enabling port lookup") + server.LookupPort = iputil.LookupPort + } + if len(opts.IPHeaders) > 0 { + log.Printf("Trusting header(s) %+v to contain correct remote IP", opts.IPHeaders) + } + + listen := opts.Listen + if listen == ":8080" { + listen = "0.0.0.0:8080" + } + log.Printf("Listening on http://%s", listen) + if err := server.ListenAndServe(opts.Listen); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/ipd/main.go b/cmd/ipd/main.go deleted file mode 100644 index 9710dfc..0000000 --- a/cmd/ipd/main.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "log" - - flags "github.com/jessevdk/go-flags" - - "os" - - "github.com/mpolden/ipd/http" - "github.com/mpolden/ipd/iputil" - "github.com/mpolden/ipd/iputil/database" -) - -func main() { - var opts struct { - CountryDBPath string `short:"f" long:"country-db" description:"Path to GeoIP country database" value-name:"FILE" default:""` - CityDBPath string `short:"c" long:"city-db" description:"Path to GeoIP city database" value-name:"FILE" default:""` - ASNDBPath string `short:"a" long:"asn-db" description:"Path to GeoIP ASN database" value-name:"FILE" default:""` - Listen string `short:"l" long:"listen" description:"Listening address" value-name:"ADDR" default:":8080"` - ReverseLookup bool `short:"r" long:"reverse-lookup" description:"Perform reverse hostname lookups"` - PortLookup bool `short:"p" long:"port-lookup" description:"Enable port lookup"` - Template string `short:"t" long:"template" description:"Path to template" default:"index.html" value-name:"FILE"` - IPHeader string `short:"H" long:"trusted-header" description:"Header to trust for remote IP, if present (e.g. X-Real-IP)" value-name:"NAME"` - } - _, err := flags.ParseArgs(&opts, os.Args) - if err != nil { - os.Exit(1) - } - - log := log.New(os.Stderr, "ipd: ", 0) - db, err := database.New(opts.CountryDBPath, opts.CityDBPath, opts.ASNDBPath) - if err != nil { - log.Fatal(err) - } - - server := http.New(db) - server.Template = opts.Template - server.IPHeader = opts.IPHeader - if opts.ReverseLookup { - log.Println("Enabling reverse lookup") - server.LookupAddr = iputil.LookupAddr - } - if opts.PortLookup { - log.Println("Enabling port lookup") - server.LookupPort = iputil.LookupPort - } - if opts.IPHeader != "" { - log.Printf("Trusting header %s to contain correct remote IP", opts.IPHeader) - } - - log.Printf("Listening on http://%s", opts.Listen) - if err := server.ListenAndServe(opts.Listen); err != nil { - log.Fatal(err) - } -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2d40e34 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +// +heroku install ./... +// +heroku goVersion go1.12 +module github.com/mpolden/echoip + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jessevdk/go-flags v1.4.0 + github.com/oschwald/geoip2-golang v1.2.1 + github.com/oschwald/maxminddb-golang v1.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.2.2 // indirect + golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..673f1b2 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +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/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU= +github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE= +github.com/oschwald/maxminddb-golang v1.2.1 h1:1wUyw1BYyCY7E0bbG8lD7P5aPDFIsRr611otw6LOJtM= +github.com/oschwald/maxminddb-golang v1.2.1/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY= +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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5 h1:MF92a0wJ3gzSUVBpjcwdrDr5+klMFRNEEu6Mev4n00I= +golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 0000000..0d550a5 --- /dev/null +++ b/heroku.yml @@ -0,0 +1,7 @@ +build: + languages: + - go + pre: + - make geoip-download +run: + web: echoip -f data/country.mmdb -c data/city.mmdb -p -r -H CF-Connecting-IP -H X-Forwarded-For -l :$PORT diff --git a/http/http.go b/http/http.go index 05017d3..3ca9965 100644 --- a/http/http.go +++ b/http/http.go @@ -5,11 +5,13 @@ import ( "fmt" "html/template" "path/filepath" + "strings" - "github.com/mpolden/ipd/iputil" - "github.com/mpolden/ipd/iputil/database" - "github.com/mpolden/ipd/useragent" + "github.com/mpolden/echoip/iputil" + "github.com/mpolden/echoip/iputil/geo" + "github.com/mpolden/echoip/useragent" + "math/big" "net" "net/http" "strconv" @@ -22,22 +24,22 @@ const ( type Server struct { Template string - IPHeader string + IPHeaders []string LookupAddr func(net.IP) (string, error) LookupPort func(net.IP, uint64) error - db database.Client + gr geo.Reader } type Response struct { - IP net.IP `json:"ip"` - IPDecimal uint64 `json:"ip_decimal"` - Country string `json:"country,omitempty"` - CountryISO string `json:"country_iso,omitempty"` - IsInEuropeanUnion bool `json:"is_in_european_union,omitempty"` - City string `json:"city,omitempty"` - Hostname string `json:"hostname,omitempty"` - LocationLatitude float64 `json:"location_latitude,omitempty"` - LocationLongitude float64 `json:"location_longitude,omitempty"` + IP net.IP `json:"ip"` + IPDecimal *big.Int `json:"ip_decimal"` + Country string `json:"country,omitempty"` + CountryEU *bool `json:"country_eu,omitempty"` + CountryISO string `json:"country_iso,omitempty"` + City string `json:"city,omitempty"` + Hostname string `json:"hostname,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` AutonomousSystemNumber string `json:"asn_number,omitempty"` AutonomousSystemOrganization string `json:"asn_organization,omitempty"` } @@ -48,12 +50,29 @@ type PortResponse struct { Reachable bool `json:"reachable"` } -func New(db database.Client) *Server { - return &Server{db: db} +func New(db geo.Reader) *Server { + return &Server{gr: db} } -func ipFromRequest(header string, r *http.Request) (net.IP, error) { - remoteIP := r.Header.Get(header) +func ipFromForwardedForHeader(v string) string { + sep := strings.Index(v, ",") + if sep == -1 { + return v + } + return v[:sep] +} + +func ipFromRequest(headers []string, r *http.Request) (net.IP, error) { + remoteIP := "" + for _, header := range headers { + remoteIP = r.Header.Get(header) + if http.CanonicalHeaderKey(header) == "X-Forwarded-For" { + remoteIP = ipFromForwardedForHeader(remoteIP) + } + if remoteIP != "" { + break + } + } if remoteIP == "" { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { @@ -69,14 +88,14 @@ func ipFromRequest(header string, r *http.Request) (net.IP, error) { } func (s *Server) newResponse(r *http.Request) (Response, error) { - ip, err := ipFromRequest(s.IPHeader, r) + ip, err := ipFromRequest(s.IPHeaders, r) if err != nil { return Response{}, err } ipDecimal := iputil.ToDecimal(ip) - country, _ := s.db.Country(ip) - city, _ := s.db.City(ip) - asn, _ := s.db.ASN(ip) + country, _ := s.gr.Country(ip) + city, _ := s.gr.City(ip) + asn, _ := s.gr.ASN(ip) var hostname string if s.LookupAddr != nil { hostname, _ = s.LookupAddr(ip) @@ -86,15 +105,15 @@ func (s *Server) newResponse(r *http.Request) (Response, error) { autonomousSystemNumber = "AS" + strconv.FormatUint(uint64(asn.AutonomousSystemNumber), 10); } return Response{ - IP: ip, - IPDecimal: ipDecimal, - Country: country.Name, - CountryISO: country.ISO, - IsInEuropeanUnion: country.IsInEuropeanUnion, - City: city.Name, - Hostname: hostname, - LocationLatitude: city.Latitude, - LocationLongitude: city.Longitude, + IP: ip, + IPDecimal: ipDecimal, + Country: country.Name, + CountryISO: country.ISO, + CountryEU: country.IsEU, + City: city.Name, + Hostname: hostname, + Latitude: city.Latitude, + Longitude: city.Longitude, AutonomousSystemNumber: autonomousSystemNumber, AutonomousSystemOrganization: asn.AutonomousSystemOrganization, }, nil @@ -103,10 +122,10 @@ func (s *Server) newResponse(r *http.Request) (Response, error) { func (s *Server) newPortResponse(r *http.Request) (PortResponse, error) { lastElement := filepath.Base(r.URL.Path) port, err := strconv.ParseUint(lastElement, 10, 16) - if err != nil || port < 1 || port > 65355 { - return PortResponse{Port: port}, fmt.Errorf("invalid port: %d", port) + if err != nil || port < 1 || port > 65535 { + return PortResponse{Port: port}, fmt.Errorf("invalid port: %s", lastElement) } - ip, err := ipFromRequest(s.IPHeader, r) + ip, err := ipFromRequest(s.IPHeaders, r) if err != nil { return PortResponse{Port: port}, err } @@ -119,7 +138,7 @@ func (s *Server) newPortResponse(r *http.Request) (PortResponse, error) { } func (s *Server) CLIHandler(w http.ResponseWriter, r *http.Request) *appError { - ip, err := ipFromRequest(s.IPHeader, r) + ip, err := ipFromRequest(s.IPHeaders, r) if err != nil { return internalServerError(err) } @@ -159,11 +178,7 @@ func (s *Server) CLICoordinatesHandler(w http.ResponseWriter, r *http.Request) * if err != nil { return internalServerError(err) } - var str string - str += FloatToString(response.LocationLatitude) - str += "," - str += FloatToString(response.LocationLongitude) - fmt.Fprintln(w, str) + fmt.Fprintf(w, "%s,%s\n", formatCoordinate(response.Latitude), formatCoordinate(response.Longitude)) return nil } @@ -181,10 +196,16 @@ func (s *Server) JSONHandler(w http.ResponseWriter, r *http.Request) *appError { return nil } +func (s *Server) HealthHandler(w http.ResponseWriter, r *http.Request) *appError { + w.Header().Set("Content-Type", jsonMediaType) + w.Write([]byte(`{"status":"OK"}`)) + return nil +} + func (s *Server) PortHandler(w http.ResponseWriter, r *http.Request) *appError { response, err := s.newPortResponse(r) if err != nil { - return badRequest(err).WithMessage(fmt.Sprintf("Invalid port: %d", response.Port)).AsJSON() + return badRequest(err).WithMessage(err.Error()).AsJSON() } b, err := json.Marshal(response) if err != nil { @@ -210,12 +231,20 @@ func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appErro } var data = struct { Response - Host string - JSON string - Port bool + Host string + BoxLatTop float64 + BoxLatBottom float64 + BoxLonLeft float64 + BoxLonRight float64 + JSON string + Port bool }{ response, r.Host, + response.Latitude + 0.05, + response.Latitude - 0.05, + response.Longitude - 0.05, + response.Longitude + 0.05, string(json), s.LookupPort != nil, } @@ -269,6 +298,9 @@ func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) Handler() http.Handler { r := NewRouter() + // Health + r.Route("GET", "/health", s.HealthHandler) + // JSON r.Route("GET", "/", s.JSONHandler).Header("Accept", jsonMediaType) r.Route("GET", "/json", s.JSONHandler) @@ -277,7 +309,7 @@ func (s *Server) Handler() http.Handler { r.Route("GET", "/", s.CLIHandler).MatcherFunc(cliMatcher) r.Route("GET", "/", s.CLIHandler).Header("Accept", textMediaType) r.Route("GET", "/ip", s.CLIHandler) - if !s.db.IsEmpty() { + if !s.gr.IsEmpty() { r.Route("GET", "/country", s.CLICountryHandler) r.Route("GET", "/country-iso", s.CLICountryISOHandler) r.Route("GET", "/city", s.CLICityHandler) @@ -285,7 +317,9 @@ func (s *Server) Handler() http.Handler { } // Browser - r.Route("GET", "/", s.DefaultHandler) + if s.Template != "" { + r.Route("GET", "/", s.DefaultHandler) + } // Port testing if s.LookupPort != nil { @@ -299,6 +333,6 @@ func (s *Server) ListenAndServe(addr string) error { return http.ListenAndServe(addr, s.Handler()) } -func FloatToString(input_num float64) string { - return strconv.FormatFloat(input_num, 'f', 6, 64) +func formatCoordinate(c float64) string { + return strconv.FormatFloat(c, 'f', 6, 64) } diff --git a/http/http_test.go b/http/http_test.go index ade01a7..e1494c1 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -8,7 +8,7 @@ import ( "net/http/httptest" "testing" - "github.com/mpolden/ipd/iputil/database" + "github.com/mpolden/echoip/iputil/geo" ) func lookupAddr(net.IP) (string, error) { return "localhost", nil } @@ -16,8 +16,8 @@ func lookupPort(net.IP, uint64) error { return nil } type testDb struct{} -func (t *testDb) Country(net.IP) (database.Country, error) { - return database.Country{Name: "Elbonia", ISO: "EB"}, nil +func (t *testDb) Country(net.IP) (geo.Country, error) { + return geo.Country{Name: "Elbonia", ISO: "EB", IsEU: new(bool)}, nil } func (t *testDb) City(net.IP) (database.City, error) { @@ -31,7 +31,7 @@ func (t *testDb) ASN(net.IP) (database.ASN, error) { func (t *testDb) IsEmpty() bool { return false } func testServer() *Server { - return &Server{db: &testDb{}, LookupAddr: lookupAddr, LookupPort: lookupPort} + return &Server{gr: &testDb{}, LookupAddr: lookupAddr, LookupPort: lookupPort} } func httpGet(url string, acceptMediaType string, userAgent string) (string, int, error) { @@ -71,6 +71,7 @@ func TestCLIHandlers(t *testing.T) { {s.URL + "/ip", "127.0.0.1\n", 200, "", ""}, {s.URL + "/country", "Elbonia\n", 200, "", ""}, {s.URL + "/country-iso", "EB\n", 200, "", ""}, + {s.URL + "/coordinates", "63.416667,10.416667\n", 200, "", ""}, {s.URL + "/city", "Bornyasherk\n", 200, "", ""}, {s.URL + "/foo", "404 page not found", 404, "", ""}, } @@ -94,7 +95,7 @@ func TestDisabledHandlers(t *testing.T) { server := testServer() server.LookupPort = nil server.LookupAddr = nil - server.db, _ = database.New("", "", "") + server.gr, _ = geo.Open("", "", "") s := httptest.NewServer(server.Handler()) var tests = []struct { @@ -132,12 +133,13 @@ func TestJSONHandlers(t *testing.T) { out string status int }{ - {s.URL, `{"ip":"127.0.0.1","ip_decimal":2130706433,"country":"Elbonia","country_iso":"EB","city":"Bornyasherk","hostname":"localhost","asn_number":"AS59795","asn_organization":"Hosting4Real"}`, 200}, - {s.URL + "/port/foo", `{"error":"Invalid port: 0"}`, 400}, - {s.URL + "/port/0", `{"error":"Invalid port: 0"}`, 400}, - {s.URL + "/port/65356", `{"error":"Invalid port: 65356"}`, 400}, + {s.URL, `{"ip":"127.0.0.1","ip_decimal":2130706433,"country":"Elbonia","country_eu":false,"country_iso":"EB","city":"Bornyasherk","hostname":"localhost","latitude":63.416667,"longitude":10.416667,"asn_number":"AS59795","asn_organization":"Hosting4Real"}`, 200}, + {s.URL + "/port/foo", `{"error":"invalid port: foo"}`, 400}, + {s.URL + "/port/0", `{"error":"invalid port: 0"}`, 400}, + {s.URL + "/port/65537", `{"error":"invalid port: 65537"}`, 400}, {s.URL + "/port/31337", `{"ip":"127.0.0.1","port":31337,"reachable":true}`, 200}, {s.URL + "/foo", `{"error":"404 page not found"}`, 404}, + {s.URL + "/health", `{"status":"OK"}`, 200}, } for _, tt := range tests { @@ -156,16 +158,20 @@ func TestJSONHandlers(t *testing.T) { func TestIPFromRequest(t *testing.T) { var tests = []struct { - remoteAddr string - headerKey string - headerValue string - trustedHeader string - out string + remoteAddr string + headerKey string + headerValue string + trustedHeaders []string + out string }{ - {"127.0.0.1:9999", "", "", "", "127.0.0.1"}, // No header given - {"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", "", "127.0.0.1"}, // Trusted header is empty - {"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", "X-Foo-Bar", "127.0.0.1"}, // Trusted header does not match - {"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", "X-Real-IP", "1.3.3.7"}, // Trusted header matches + {"127.0.0.1:9999", "", "", nil, "127.0.0.1"}, // No header given + {"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", nil, "127.0.0.1"}, // Trusted header is empty + {"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", []string{"X-Foo-Bar"}, "127.0.0.1"}, // Trusted header does not match + {"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", []string{"X-Real-IP", "X-Forwarded-For"}, "1.3.3.7"}, // Trusted header matches + {"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7", []string{"X-Real-IP", "X-Forwarded-For"}, "1.3.3.7"}, // Second trusted header matches + {"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7,4.2.4.2", []string{"X-Forwarded-For"}, "1.3.3.7"}, // X-Forwarded-For with multiple entries (commas separator) + {"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7, 4.2.4.2", []string{"X-Forwarded-For"}, "1.3.3.7"}, // X-Forwarded-For with multiple entries (space+comma separator) + {"127.0.0.1:9999", "X-Forwarded-For", "", []string{"X-Forwarded-For"}, "127.0.0.1"}, // Empty header } for _, tt := range tests { r := &http.Request{ @@ -173,7 +179,7 @@ func TestIPFromRequest(t *testing.T) { Header: http.Header{}, } r.Header.Add(tt.headerKey, tt.headerValue) - ip, err := ipFromRequest(tt.trustedHeader, r) + ip, err := ipFromRequest(tt.trustedHeaders, r) if err != nil { t.Fatal(err) } diff --git a/index.html b/index.html index d5b1d70..39f5c2c 100644 --- a/index.html +++ b/index.html @@ -10,6 +10,7 @@ + diff --git a/iputil/database/database.go b/iputil/geo/geo.go similarity index 82% rename from iputil/database/database.go rename to iputil/geo/geo.go index 3e4220f..4543d3d 100644 --- a/iputil/database/database.go +++ b/iputil/geo/geo.go @@ -1,13 +1,13 @@ -package database +package geo import ( - "net" "math" + "net" geoip2 "github.com/oschwald/geoip2-golang" ) -type Client interface { +type Reader interface { Country(net.IP) (Country, error) City(net.IP) (City, error) ASN(net.IP) (ASN, error) @@ -15,9 +15,9 @@ type Client interface { } type Country struct { - Name string - ISO string - IsInEuropeanUnion bool + Name string + ISO string + IsEU *bool } type City struct { @@ -37,7 +37,7 @@ type geoip struct { asn *geoip2.Reader } -func New(countryDB, cityDB, asnDB string) (Client, error) { +func Open(countryDB, cityDB string, asnDB string) (Reader, error) { var country, city, asn *geoip2.Reader if countryDB != "" { r, err := geoip2.Open(countryDB) @@ -84,10 +84,8 @@ func (g *geoip) Country(ip net.IP) (Country, error) { if record.RegisteredCountry.IsoCode != "" && country.ISO == "" { country.ISO = record.RegisteredCountry.IsoCode } - country.IsInEuropeanUnion = record.Country.IsInEuropeanUnion - if record.RegisteredCountry.IsoCode != "" && country.ISO == "" { - country.IsInEuropeanUnion = record.RegisteredCountry.IsInEuropeanUnion - } + isEU := record.Country.IsInEuropeanUnion || record.RegisteredCountry.IsInEuropeanUnion + country.IsEU = &isEU return country, nil } @@ -103,10 +101,10 @@ func (g *geoip) City(ip net.IP) (City, error) { if c, exists := record.City.Names["en"]; exists { city.Name = c } - if math.IsNaN(record.Location.Latitude) == false { + if !math.IsNaN(record.Location.Latitude) { city.Latitude = record.Location.Latitude } - if math.IsNaN(record.Location.Longitude) == false { + if !math.IsNaN(record.Location.Longitude) { city.Longitude = record.Location.Longitude } return city, nil diff --git a/iputil/iputil.go b/iputil/iputil.go index d181845..b26bb47 100644 --- a/iputil/iputil.go +++ b/iputil/iputil.go @@ -27,12 +27,12 @@ func LookupPort(ip net.IP, port uint64) error { return nil } -func ToDecimal(ip net.IP) uint64 { +func ToDecimal(ip net.IP) *big.Int { i := big.NewInt(0) if to4 := ip.To4(); to4 != nil { i.SetBytes(to4) } else { i.SetBytes(ip) } - return i.Uint64() + return i } diff --git a/iputil/iputil_test.go b/iputil/iputil_test.go index abe8c94..6ad5e07 100644 --- a/iputil/iputil_test.go +++ b/iputil/iputil_test.go @@ -1,21 +1,26 @@ package iputil import ( + "math/big" "net" "testing" ) func TestToDecimal(t *testing.T) { + var msb = new(big.Int) + msb, _ = msb.SetString("80000000000000000000000000000000", 16) + var tests = []struct { in string - out uint64 + out *big.Int }{ - {"127.0.0.1", 2130706433}, - {"::1", 1}, + {"127.0.0.1", big.NewInt(2130706433)}, + {"::1", big.NewInt(1)}, + {"8000::", msb}, } for _, tt := range tests { i := ToDecimal(net.ParseIP(tt.in)) - if tt.out != i { + if tt.out.Cmp(i) != 0 { t.Errorf("Expected %d, got %d for IP %s", tt.out, i, tt.in) } }