mirror of https://github.com/mpolden/echoip
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:
commit
ab280dbeaa
|
@ -1,2 +1,3 @@
|
|||
/data/
|
||||
/custom.html
|
||||
/vendor/
|
||||
|
|
18
.travis.yml
18
.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=
|
||||
|
|
|
@ -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"]
|
43
Makefile
43
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)
|
||||
|
|
20
README.md
20
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
130
http/http.go
130
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
16
index.html
16
index.html
|
@ -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 }}&layer=mapnik&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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue