master: merged on ASN

Maybe we get all the good stuff from #53 after all.
Heavy lifting done by:

Co-authored-by: raunsbaekdk <mike+git@raunsbaek.dk>
This commit is contained in:
Alphakilo 2019-06-10 16:18:14 +02:00
commit ab280dbeaa
No known key found for this signature in database
GPG Key ID: 454F291F8E83602D
16 changed files with 313 additions and 171 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/data/
/custom.html
/vendor/

View File

@ -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=

17
Dockerfile Normal file
View File

@ -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"]

View File

@ -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)

View File

@ -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

64
cmd/echoip/main.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

13
go.mod Normal file
View File

@ -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
)

14
go.sum Normal file
View File

@ -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=

7
heroku.yml Normal file
View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -10,6 +10,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pure/0.6.2/grids-responsive-min.css">
<style>
html, .pure-g [class *= "pure-u"] {
background-color: white;
font-family: "Open Sans", sans-serif;
}
pre {
@ -96,10 +97,19 @@ $ http {{ .Host }}/port/8080
}</pre>
{{ end }}
</div>
{{ if .City }}
<div class="pure-u-1 pure-u-md-1-1">
<h2>Map</h2>
<iframe width="100%" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://www.openstreetmap.org/export/embed.html?bbox={{ .BoxLonLeft }}%2C{{ .BoxLatBottom }}%2C{{ .BoxLonRight }}%2C{{ .BoxLatTop }}&amp;layer=mapnik&amp;marker={{ .Latitude }}%2C{{ .Longitude }}"></iframe>
</div>
{{ end }}
<div class="pure-u-1 pure-u-md-1-2">
<h2>FAQ</h2>
<h3>How do I force IPv4 or IPv6 lookup?</h3>
<p>IPv4 or IPv6 lookup can be forced by using the <a href="//v4.ifconfig.co">v4</a> and <a href="//v6.ifconfig.co">v6<a> subdomains.</p>
<p>As of 2018-07-25 it's no longer possible to force protocol using
the <i>v4</i> and <i>v6</i> subdomains. IPv4 or IPv6 still can be forced
by passing the appropiate flag to your client, e.g <code>curl -4</code>
or <code>curl -6</code>.</p>
<h3>Is automated use of this service permitted?</h3>
<p>
@ -113,9 +123,9 @@ $ http {{ .Host }}/port/8080
</p>
<h3>Can I run my own service?</h3>
<p>Yes, the source code and documentation is available on <a href="https://github.com/mpolden/ipd">GitHub</a>.</p>
<p>Yes, the source code and documentation is available on <a href="https://github.com/mpolden/echoip">GitHub</a>.</p>
</div>
</div>
<a href="https://github.com/mpolden/ipd" class="github-corner"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
<a href="https://github.com/mpolden/echoip" class="github-corner"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
</body>
</html>

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}