mirror of https://github.com/mpolden/echoip
commit
72f121cc9d
|
@ -1,2 +1,3 @@
|
|||
/GeoLite2-Country.mmdb
|
||||
/GeoLite2-City.mmdb
|
||||
/data/
|
||||
/custom.html
|
||||
/vendor/
|
||||
|
|
14
.travis.yml
14
.travis.yml
|
@ -3,5 +3,15 @@ sudo: false
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.x
|
||||
- tip
|
||||
- stable
|
||||
|
||||
# Go module behaviour is disabled by default for packages inside GOPATH.
|
||||
# Turn module support on explicitly:
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
deploy:
|
||||
provider: heroku
|
||||
app: ifconfig-co
|
||||
api_key:
|
||||
secure: IQG/ls5Zu0yua5Ynn5EL9JCPjo1/WcmS0z7BSaXWdgW+JIWFm7oF5z54bUZHl/q1tTuWzAJk59zSTYJijtQqh2Ssl3fLu3uFDwyJSrOuUu1akPlETam8NpdbH4lPkFp75JSDdDXV08c0APmeLL6gqRuTrUuufu69Wigjq4gLo+o=
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# Compile
|
||||
FROM golang:1.11-alpine AS build
|
||||
ADD . /go/src/github.com/mpolden/echoip
|
||||
WORKDIR /go/src/github.com/mpolden/echoip
|
||||
RUN apk --update add git gcc musl-dev
|
||||
ENV GO111MODULE=on
|
||||
RUN go get -d -v .
|
||||
RUN go install ./...
|
||||
|
||||
# Run
|
||||
FROM alpine
|
||||
RUN mkdir -p /opt/
|
||||
COPY --from=build /go/bin/echoip /opt/
|
||||
WORKDIR /opt/
|
||||
ENTRYPOINT ["/opt/echoip"]
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2012-2017, Martin Polden
|
||||
Copyright (c) 2012-2018, Martin Polden
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
43
Makefile
43
Makefile
|
@ -1,20 +1,33 @@
|
|||
all: deps test vet install
|
||||
OS := $(shell uname)
|
||||
ifeq ($(OS),Linux)
|
||||
TAR_OPTS := --wildcards
|
||||
endif
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
all: deps lint test install
|
||||
|
||||
deps:
|
||||
go get -d -v ./...
|
||||
go get ./...
|
||||
|
||||
install:
|
||||
go install
|
||||
test: deps
|
||||
go test ./...
|
||||
|
||||
get-geoip-dbs:
|
||||
curl -s http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.mmdb.gz | gunzip > GeoLite2-Country.mmdb
|
||||
curl -s http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz | gunzip > GeoLite2-City.mmdb
|
||||
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 https://geolite.maxmind.com/download/geoip/database/$@.tar.gz | tar $(TAR_OPTS) --strip-components=1 -C $(PWD)/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)
|
||||
|
|
72
README.md
72
README.md
|
@ -1,9 +1,9 @@
|
|||
# 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
|
||||
https://ifconfig.co.
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -19,7 +19,7 @@ $ http ifconfig.co
|
|||
$ wget -qO- ifconfig.co
|
||||
127.0.0.1
|
||||
|
||||
$ fetch -qo- http://ifconfig.co
|
||||
$ fetch -qo- https://ifconfig.co
|
||||
127.0.0.1
|
||||
|
||||
$ bat -print=b ifconfig.co/ip
|
||||
|
@ -29,41 +29,54 @@ $ bat -print=b ifconfig.co/ip
|
|||
Country and city lookup:
|
||||
|
||||
```
|
||||
$ http ifconfig.co/country
|
||||
$ curl ifconfig.co/country
|
||||
Elbonia
|
||||
|
||||
$ http ifconfig.co/city
|
||||
$ curl ifconfig.co/country-iso
|
||||
EB
|
||||
|
||||
$ curl ifconfig.co/city
|
||||
Bornyasherk
|
||||
```
|
||||
|
||||
As JSON:
|
||||
|
||||
```
|
||||
$ http --json ifconfig.co
|
||||
$ curl -H 'Accept: application/json' ifconfig.co # or curl ifconfig.co/json
|
||||
{
|
||||
"city": "Bornyasherk",
|
||||
"country": "Elbonia",
|
||||
"country_iso": "EB",
|
||||
"ip": "127.0.0.1",
|
||||
"ip_decimal": 2130706433
|
||||
}
|
||||
```
|
||||
|
||||
Pass the appropriate flag (usually `-4` and `-6`) to your tool to switch between
|
||||
IPv4 and IPv6 lookup.
|
||||
Port testing:
|
||||
|
||||
The subdomains https://v4.ifconfig.co and https://v6.ifconfig.co can be used to
|
||||
force IPv4 or IPv6 lookup.
|
||||
```
|
||||
$ curl ifconfig.co/port/80
|
||||
{
|
||||
"ip": "127.0.0.1",
|
||||
"port": 80,
|
||||
"reachable": false
|
||||
}
|
||||
```
|
||||
|
||||
Pass the appropriate flag (usually `-4` and `-6`) to your client to switch
|
||||
between IPv4 and IPv6 lookup.
|
||||
|
||||
## Features
|
||||
|
||||
* Easy to remember domain name
|
||||
* Supports IPv4 and IPv6
|
||||
* Supports HTTPS
|
||||
* Open source under the [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause)
|
||||
* Fast
|
||||
* Supports typical CLI tools (`curl`, `httpie`, `wget` and `fetch`)
|
||||
* JSON output (optional)
|
||||
* Country and city lookup through the MaxMind GeoIP database
|
||||
* Supports IPv6
|
||||
* Supports HTTPS
|
||||
* Supports common command-line clients (e.g. `curl`, `httpie`, `wget` and `fetch`)
|
||||
* JSON output
|
||||
* Country and city lookup using the MaxMind GeoIP database
|
||||
* Port testing
|
||||
* Open source under the [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause)
|
||||
|
||||
## Why?
|
||||
|
||||
|
@ -74,9 +87,9 @@ force IPv4 or IPv6 lookup.
|
|||
## Building
|
||||
|
||||
Compiling requires the [Golang compiler](https://golang.org/) to be installed.
|
||||
This application can be installed by using `go get`:
|
||||
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).
|
||||
|
@ -84,20 +97,19 @@ documentation](https://golang.org/doc/code.html).
|
|||
### Usage
|
||||
|
||||
```
|
||||
$ ipd -h
|
||||
$ echoip -h
|
||||
Usage:
|
||||
ipd [OPTIONS]
|
||||
echoip [OPTIONS]
|
||||
|
||||
Application Options:
|
||||
-f, --country-db=FILE Path to GeoIP country database
|
||||
-c, --city-db=FILE Path to GeoIP city database
|
||||
-l, --listen=ADDR Listening address (default: :8080)
|
||||
-r, --reverse-lookup Perform reverse hostname lookups
|
||||
-p, --port-lookup Enable port lookup
|
||||
-t, --template=FILE Path to template (default: index.html)
|
||||
-H, --trusted-header=NAME Header to trust for remote IP, if present (e.g. X-Real-IP)
|
||||
-L, --log-level=[debug|info|warn|error|fatal|panic] Log level to use (default: info)
|
||||
-f, --country-db=FILE Path to GeoIP country database
|
||||
-c, --city-db=FILE Path to GeoIP city database
|
||||
-l, --listen=ADDR Listening address (default: :8080)
|
||||
-r, --reverse-lookup Perform reverse hostname lookups
|
||||
-p, --port-lookup Enable port lookup
|
||||
-t, --template=FILE Path to template (default: index.html)
|
||||
-H, --trusted-header=NAME Header to trust for remote IP, if present (e.g. X-Real-IP)
|
||||
|
||||
Help Options:
|
||||
-h, --help Show this help message
|
||||
-h, --help Show this help message
|
||||
```
|
||||
|
|
265
api/api.go
265
api/api.go
|
@ -1,265 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/mpolden/ipd/useragent"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
jsonMediaType = "application/json"
|
||||
textMediaType = "text/plain"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
Template string
|
||||
IPHeader string
|
||||
oracle Oracle
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
IP net.IP `json:"ip"`
|
||||
IPDecimal *big.Int `json:"ip_decimal"`
|
||||
Country string `json:"country,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
}
|
||||
|
||||
type PortResponse struct {
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint64 `json:"port"`
|
||||
Reachable bool `json:"reachable"`
|
||||
}
|
||||
|
||||
func New(oracle Oracle, logger *logrus.Logger) *API {
|
||||
return &API{oracle: oracle, log: logger}
|
||||
}
|
||||
|
||||
func ipToDecimal(ip net.IP) *big.Int {
|
||||
i := big.NewInt(0)
|
||||
if to4 := ip.To4(); to4 != nil {
|
||||
i.SetBytes(to4)
|
||||
} else {
|
||||
i.SetBytes(ip)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func ipFromRequest(header string, r *http.Request) (net.IP, error) {
|
||||
remoteIP := r.Header.Get(header)
|
||||
if remoteIP == "" {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteIP = host
|
||||
}
|
||||
ip := net.ParseIP(remoteIP)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("could not parse IP: %s", remoteIP)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (a *API) newResponse(r *http.Request) (Response, error) {
|
||||
ip, err := ipFromRequest(a.IPHeader, r)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
ipDecimal := ipToDecimal(ip)
|
||||
country, err := a.oracle.LookupCountry(ip)
|
||||
if err != nil {
|
||||
a.log.Debug(err)
|
||||
}
|
||||
city, err := a.oracle.LookupCity(ip)
|
||||
if err != nil {
|
||||
a.log.Debug(err)
|
||||
}
|
||||
hostnames, err := a.oracle.LookupAddr(ip)
|
||||
if err != nil {
|
||||
a.log.Debug(err)
|
||||
}
|
||||
return Response{
|
||||
IP: ip,
|
||||
IPDecimal: ipDecimal,
|
||||
Country: country,
|
||||
City: city,
|
||||
Hostname: strings.Join(hostnames, " "),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *API) newPortResponse(r *http.Request) (PortResponse, error) {
|
||||
vars := mux.Vars(r)
|
||||
port, err := strconv.ParseUint(vars["port"], 10, 16)
|
||||
if err != nil {
|
||||
return PortResponse{Port: port}, err
|
||||
}
|
||||
if port < 1 || port > 65355 {
|
||||
return PortResponse{Port: port}, fmt.Errorf("invalid port: %d", port)
|
||||
}
|
||||
ip, err := ipFromRequest(a.IPHeader, r)
|
||||
if err != nil {
|
||||
return PortResponse{Port: port}, err
|
||||
}
|
||||
err = a.oracle.LookupPort(ip, port)
|
||||
return PortResponse{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Reachable: err == nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *API) CLIHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
ip, err := ipFromRequest(a.IPHeader, r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
fmt.Fprintln(w, ip.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) CLICountryHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := a.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
fmt.Fprintln(w, response.Country)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) CLICityHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := a.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
fmt.Fprintln(w, response.City)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) JSONHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := a.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
b, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) PortHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := a.newPortResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(fmt.Sprintf("Invalid port: %d", response.Port)).AsJSON()
|
||||
}
|
||||
b, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) DefaultHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := a.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
t, err := template.New(filepath.Base(a.Template)).ParseFiles(a.Template)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
var data = struct {
|
||||
Host string
|
||||
Response
|
||||
Oracle
|
||||
}{r.Host, response, a.oracle}
|
||||
if err := t.Execute(w, &data); err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) NotFoundHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
err := notFound(nil).WithMessage("404 page not found")
|
||||
if r.Header.Get("accept") == jsonMediaType {
|
||||
err = err.AsJSON()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cliMatcher(r *http.Request, rm *mux.RouteMatch) bool {
|
||||
ua := useragent.Parse(r.UserAgent())
|
||||
switch ua.Product {
|
||||
case "curl", "HTTPie", "Wget", "fetch libfetch", "Go", "Go-http-client", "ddclient":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type appHandler func(http.ResponseWriter, *http.Request) *appError
|
||||
|
||||
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if e := fn(w, r); e != nil { // e is *appError
|
||||
// When Content-Type for error is JSON, we need to marshal the response into JSON
|
||||
if e.IsJSON() {
|
||||
var data = struct {
|
||||
Error string `json:"error"`
|
||||
}{e.Message}
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
e.Message = string(b)
|
||||
}
|
||||
// Set Content-Type of response if set in error
|
||||
if e.ContentType != "" {
|
||||
w.Header().Set("Content-Type", e.ContentType)
|
||||
}
|
||||
w.WriteHeader(e.Code)
|
||||
fmt.Fprint(w, e.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) Router() http.Handler {
|
||||
r := mux.NewRouter()
|
||||
|
||||
// JSON
|
||||
r.Handle("/", appHandler(a.JSONHandler)).Methods("GET").Headers("Accept", jsonMediaType)
|
||||
r.Handle("/json", appHandler(a.JSONHandler)).Methods("GET")
|
||||
|
||||
// CLI
|
||||
r.Handle("/", appHandler(a.CLIHandler)).Methods("GET").MatcherFunc(cliMatcher)
|
||||
r.Handle("/", appHandler(a.CLIHandler)).Methods("GET").Headers("Accept", textMediaType)
|
||||
r.Handle("/ip", appHandler(a.CLIHandler)).Methods("GET")
|
||||
r.Handle("/country", appHandler(a.CLICountryHandler)).Methods("GET")
|
||||
r.Handle("/city", appHandler(a.CLICityHandler)).Methods("GET")
|
||||
|
||||
// Browser
|
||||
r.Handle("/", appHandler(a.DefaultHandler)).Methods("GET")
|
||||
|
||||
// Port testing
|
||||
r.Handle("/port/{port:[0-9]+}", appHandler(a.PortHandler)).Methods("GET")
|
||||
|
||||
// Not found handler which returns JSON when appropriate
|
||||
r.NotFoundHandler = appHandler(a.NotFoundHandler)
|
||||
|
||||
return r
|
||||
}
|
139
api/oracle.go
139
api/oracle.go
|
@ -1,139 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
type Oracle interface {
|
||||
LookupAddr(net.IP) ([]string, error)
|
||||
LookupCountry(net.IP) (string, error)
|
||||
LookupCity(net.IP) (string, error)
|
||||
LookupPort(net.IP, uint64) error
|
||||
IsLookupAddrEnabled() bool
|
||||
IsLookupCountryEnabled() bool
|
||||
IsLookupCityEnabled() bool
|
||||
IsLookupPortEnabled() bool
|
||||
}
|
||||
|
||||
type DefaultOracle struct {
|
||||
lookupAddr func(net.IP) ([]string, error)
|
||||
lookupCountry func(net.IP) (string, error)
|
||||
lookupCity func(net.IP) (string, error)
|
||||
lookupPort func(net.IP, uint64) error
|
||||
lookupAddrEnabled bool
|
||||
lookupCountryEnabled bool
|
||||
lookupCityEnabled bool
|
||||
lookupPortEnabled bool
|
||||
}
|
||||
|
||||
func NewOracle() *DefaultOracle {
|
||||
return &DefaultOracle{
|
||||
lookupAddr: func(net.IP) ([]string, error) { return nil, nil },
|
||||
lookupCountry: func(net.IP) (string, error) { return "", nil },
|
||||
lookupCity: func(net.IP) (string, error) { return "", nil },
|
||||
lookupPort: func(net.IP, uint64) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) LookupAddr(ip net.IP) ([]string, error) {
|
||||
return r.lookupAddr(ip)
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) LookupCountry(ip net.IP) (string, error) {
|
||||
return r.lookupCountry(ip)
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) LookupCity(ip net.IP) (string, error) {
|
||||
return r.lookupCity(ip)
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) LookupPort(ip net.IP, port uint64) error {
|
||||
return r.lookupPort(ip, port)
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) EnableLookupAddr() {
|
||||
r.lookupAddr = lookupAddr
|
||||
r.lookupAddrEnabled = true
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) EnableLookupCountry(filepath string) error {
|
||||
db, err := geoip2.Open(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.lookupCountry = func(ip net.IP) (string, error) {
|
||||
return lookupCountry(db, ip)
|
||||
}
|
||||
r.lookupCountryEnabled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) EnableLookupCity(filepath string) error {
|
||||
db, err := geoip2.Open(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.lookupCity = func(ip net.IP) (string, error) {
|
||||
return lookupCity(db, ip)
|
||||
}
|
||||
r.lookupCityEnabled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) EnableLookupPort() {
|
||||
r.lookupPort = lookupPort
|
||||
r.lookupPortEnabled = true
|
||||
}
|
||||
|
||||
func (r *DefaultOracle) IsLookupAddrEnabled() bool { return r.lookupAddrEnabled }
|
||||
func (r *DefaultOracle) IsLookupCountryEnabled() bool { return r.lookupCountryEnabled }
|
||||
func (r *DefaultOracle) IsLookupCityEnabled() bool { return r.lookupCityEnabled }
|
||||
func (r *DefaultOracle) IsLookupPortEnabled() bool { return r.lookupPortEnabled }
|
||||
|
||||
func lookupAddr(ip net.IP) ([]string, error) {
|
||||
names, err := net.LookupAddr(ip.String())
|
||||
for i, _ := range names {
|
||||
names[i] = strings.TrimRight(names[i], ".") // Always return unrooted name
|
||||
}
|
||||
return names, err
|
||||
}
|
||||
|
||||
func lookupPort(ip net.IP, port uint64) error {
|
||||
address := fmt.Sprintf("[%s]:%d", ip, port)
|
||||
conn, err := net.DialTimeout("tcp", address, 2*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupCountry(db *geoip2.Reader, ip net.IP) (string, error) {
|
||||
record, err := db.Country(ip)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if country, exists := record.Country.Names["en"]; exists {
|
||||
return country, nil
|
||||
}
|
||||
if country, exists := record.RegisteredCountry.Names["en"]; exists {
|
||||
return country, nil
|
||||
}
|
||||
return "Unknown", fmt.Errorf("could not determine country for IP: %s", ip)
|
||||
}
|
||||
|
||||
func lookupCity(db *geoip2.Reader, ip net.IP) (string, error) {
|
||||
record, err := db.City(ip)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if city, exists := record.City.Names["en"]; exists {
|
||||
return city, nil
|
||||
}
|
||||
return "Unknown", fmt.Errorf("could not determine city for IP: %s", ip)
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
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:""`
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
server := http.New(r)
|
||||
server.Template = opts.Template
|
||||
server.IPHeaders = opts.IPHeaders
|
||||
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)
|
||||
}
|
||||
|
||||
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.11
|
||||
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
|
|
@ -1,4 +1,4 @@
|
|||
package api
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mpolden/echoip/iputil"
|
||||
"github.com/mpolden/echoip/iputil/geo"
|
||||
"github.com/mpolden/echoip/useragent"
|
||||
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
jsonMediaType = "application/json"
|
||||
textMediaType = "text/plain"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Template string
|
||||
IPHeaders []string
|
||||
LookupAddr func(net.IP) (string, error)
|
||||
LookupPort func(net.IP, uint64) error
|
||||
gr geo.Reader
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type PortResponse struct {
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint64 `json:"port"`
|
||||
Reachable bool `json:"reachable"`
|
||||
}
|
||||
|
||||
func New(db geo.Reader) *Server {
|
||||
return &Server{gr: db}
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
remoteIP = host
|
||||
}
|
||||
ip := net.ParseIP(remoteIP)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("could not parse IP: %s", remoteIP)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (s *Server) newResponse(r *http.Request) (Response, error) {
|
||||
ip, err := ipFromRequest(s.IPHeaders, r)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
ipDecimal := iputil.ToDecimal(ip)
|
||||
country, _ := s.gr.Country(ip)
|
||||
city, _ := s.gr.City(ip)
|
||||
var hostname string
|
||||
if s.LookupAddr != nil {
|
||||
hostname, _ = s.LookupAddr(ip)
|
||||
}
|
||||
return Response{
|
||||
IP: ip,
|
||||
IPDecimal: ipDecimal,
|
||||
Country: country.Name,
|
||||
CountryISO: country.ISO,
|
||||
CountryEU: country.IsEU,
|
||||
City: city.Name,
|
||||
Hostname: hostname,
|
||||
Latitude: city.Latitude,
|
||||
Longitude: city.Longitude,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
ip, err := ipFromRequest(s.IPHeaders, r)
|
||||
if err != nil {
|
||||
return PortResponse{Port: port}, err
|
||||
}
|
||||
err = s.LookupPort(ip, port)
|
||||
return PortResponse{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Reachable: err == nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CLIHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
ip, err := ipFromRequest(s.IPHeaders, r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
fmt.Fprintln(w, ip.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICountryHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
fmt.Fprintln(w, response.Country)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICountryISOHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
fmt.Fprintln(w, response.CountryISO)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICityHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
fmt.Fprintln(w, response.City)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICoordinatesHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
fmt.Fprintf(w, "%s,%s\n", formatCoordinate(response.Latitude), formatCoordinate(response.Longitude))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) JSONHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
b, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
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()
|
||||
}
|
||||
b, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
t, err := template.ParseFiles(s.Template)
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
json, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
var data = struct {
|
||||
Response
|
||||
Host string
|
||||
JSON string
|
||||
Port bool
|
||||
}{
|
||||
response,
|
||||
r.Host,
|
||||
string(json),
|
||||
s.LookupPort != nil,
|
||||
}
|
||||
if err := t.Execute(w, &data); err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NotFoundHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
err := notFound(nil).WithMessage("404 page not found")
|
||||
if r.Header.Get("accept") == jsonMediaType {
|
||||
err = err.AsJSON()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cliMatcher(r *http.Request) bool {
|
||||
ua := useragent.Parse(r.UserAgent())
|
||||
switch ua.Product {
|
||||
case "curl", "HTTPie", "Wget", "fetch libfetch", "Go", "Go-http-client", "ddclient":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type appHandler func(http.ResponseWriter, *http.Request) *appError
|
||||
|
||||
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if e := fn(w, r); e != nil { // e is *appError
|
||||
// When Content-Type for error is JSON, we need to marshal the response into JSON
|
||||
if e.IsJSON() {
|
||||
var data = struct {
|
||||
Error string `json:"error"`
|
||||
}{e.Message}
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
e.Message = string(b)
|
||||
}
|
||||
// Set Content-Type of response if set in error
|
||||
if e.ContentType != "" {
|
||||
w.Header().Set("Content-Type", e.ContentType)
|
||||
}
|
||||
w.WriteHeader(e.Code)
|
||||
fmt.Fprint(w, e.Message)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// CLI
|
||||
r.Route("GET", "/", s.CLIHandler).MatcherFunc(cliMatcher)
|
||||
r.Route("GET", "/", s.CLIHandler).Header("Accept", textMediaType)
|
||||
r.Route("GET", "/ip", s.CLIHandler)
|
||||
if !s.gr.IsEmpty() {
|
||||
r.Route("GET", "/country", s.CLICountryHandler)
|
||||
r.Route("GET", "/country-iso", s.CLICountryISOHandler)
|
||||
r.Route("GET", "/city", s.CLICityHandler)
|
||||
r.Route("GET", "/coordinates", s.CLICoordinatesHandler)
|
||||
}
|
||||
|
||||
// Browser
|
||||
r.Route("GET", "/", s.DefaultHandler)
|
||||
|
||||
// Port testing
|
||||
if s.LookupPort != nil {
|
||||
r.RoutePrefix("GET", "/port/", s.PortHandler)
|
||||
}
|
||||
|
||||
return r.Handler()
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe(addr string) error {
|
||||
return http.ListenAndServe(addr, s.Handler())
|
||||
}
|
||||
|
||||
func formatCoordinate(c float64) string {
|
||||
return strconv.FormatFloat(c, 'f', 6, 64)
|
||||
}
|
|
@ -1,28 +1,33 @@
|
|||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mpolden/echoip/iputil/geo"
|
||||
)
|
||||
|
||||
type mockOracle struct{}
|
||||
func lookupAddr(net.IP) (string, error) { return "localhost", nil }
|
||||
func lookupPort(net.IP, uint64) error { return nil }
|
||||
|
||||
func (r *mockOracle) LookupAddr(net.IP) ([]string, error) { return []string{"localhost"}, nil }
|
||||
func (r *mockOracle) LookupCountry(net.IP) (string, error) { return "Elbonia", nil }
|
||||
func (r *mockOracle) LookupCity(net.IP) (string, error) { return "Bornyasherk", nil }
|
||||
func (r *mockOracle) LookupPort(net.IP, uint64) error { return nil }
|
||||
func (r *mockOracle) IsLookupAddrEnabled() bool { return true }
|
||||
func (r *mockOracle) IsLookupCountryEnabled() bool { return true }
|
||||
func (r *mockOracle) IsLookupCityEnabled() bool { return true }
|
||||
func (r *mockOracle) IsLookupPortEnabled() bool { return true }
|
||||
type testDb struct{}
|
||||
|
||||
func newTestAPI() *API {
|
||||
return &API{oracle: &mockOracle{}}
|
||||
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) (geo.City, error) {
|
||||
return geo.City{Name: "Bornyasherk", Latitude: 63.416667, Longitude: 10.416667}, nil
|
||||
}
|
||||
|
||||
func (t *testDb) IsEmpty() bool { return false }
|
||||
|
||||
func testServer() *Server {
|
||||
return &Server{gr: &testDb{}, LookupAddr: lookupAddr, LookupPort: lookupPort}
|
||||
}
|
||||
|
||||
func httpGet(url string, acceptMediaType string, userAgent string) (string, int, error) {
|
||||
|
@ -48,7 +53,7 @@ func httpGet(url string, acceptMediaType string, userAgent string) (string, int,
|
|||
|
||||
func TestCLIHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
s := httptest.NewServer(newTestAPI().Router())
|
||||
s := httptest.NewServer(testServer().Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
|
@ -61,6 +66,8 @@ func TestCLIHandlers(t *testing.T) {
|
|||
{s.URL, "127.0.0.1\n", 200, "foo/bar", textMediaType},
|
||||
{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, "", ""},
|
||||
}
|
||||
|
@ -79,25 +86,28 @@ func TestCLIHandlers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestJSONHandlers(t *testing.T) {
|
||||
func TestDisabledHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
s := httptest.NewServer(newTestAPI().Router())
|
||||
server := testServer()
|
||||
server.LookupPort = nil
|
||||
server.LookupAddr = nil
|
||||
server.gr, _ = geo.Open("", "")
|
||||
s := httptest.NewServer(server.Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
out string
|
||||
status int
|
||||
}{
|
||||
{s.URL, `{"ip":"127.0.0.1","ip_decimal":2130706433,"country":"Elbonia","city":"Bornyasherk","hostname":"localhost"}`, 200},
|
||||
{s.URL + "/port/foo", `{"error":"404 page not found"}`, 404},
|
||||
{s.URL + "/port/0", `{"error":"Invalid port: 0"}`, 400},
|
||||
{s.URL + "/port/65356", `{"error":"Invalid port: 65356"}`, 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 + "/port/1337", "404 page not found", 404},
|
||||
{s.URL + "/country", "404 page not found", 404},
|
||||
{s.URL + "/country-iso", "404 page not found", 404},
|
||||
{s.URL + "/city", "404 page not found", 404},
|
||||
{s.URL + "/json", `{"ip":"127.0.0.1","ip_decimal":2130706433}`, 200},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
out, status, err := httpGet(tt.url, jsonMediaType, "curl/7.2.6.0")
|
||||
out, status, err := httpGet(tt.url, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -110,18 +120,54 @@ func TestJSONHandlers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestJSONHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
s := httptest.NewServer(testServer().Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
out string
|
||||
status int
|
||||
}{
|
||||
{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}`, 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 + "/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 {
|
||||
out, status, err := httpGet(tt.url, jsonMediaType, "curl/7.2.6.0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != tt.status {
|
||||
t.Errorf("Expected %d for %s, got %d", tt.status, tt.url, status)
|
||||
}
|
||||
if out != tt.out {
|
||||
t.Errorf("Expected %q for %s, got %q", tt.out, tt.url, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
|
@ -129,7 +175,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)
|
||||
}
|
||||
|
@ -161,24 +207,8 @@ func TestCLIMatcher(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
r := &http.Request{Header: http.Header{"User-Agent": []string{tt.in}}}
|
||||
if got := cliMatcher(r, nil); got != tt.out {
|
||||
if got := cliMatcher(r); got != tt.out {
|
||||
t.Errorf("Expected %t, got %t for %q", tt.out, got, tt.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPToDecimal(t *testing.T) {
|
||||
var tests = []struct {
|
||||
in string
|
||||
out *big.Int
|
||||
}{
|
||||
{"127.0.0.1", big.NewInt(2130706433)},
|
||||
{"::1", big.NewInt(1)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
i := ipToDecimal(net.ParseIP(tt.in))
|
||||
if i.Cmp(tt.out) != 0 {
|
||||
t.Errorf("Expected %d, got %d for IP %s", tt.out, i, tt.in)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type router struct {
|
||||
routes []*route
|
||||
}
|
||||
|
||||
type route struct {
|
||||
method string
|
||||
path string
|
||||
prefix bool
|
||||
handler appHandler
|
||||
matcherFunc func(*http.Request) bool
|
||||
}
|
||||
|
||||
func NewRouter() *router {
|
||||
return &router{}
|
||||
}
|
||||
|
||||
func (r *router) Route(method, path string, handler appHandler) *route {
|
||||
route := route{
|
||||
method: method,
|
||||
path: path,
|
||||
handler: handler,
|
||||
}
|
||||
r.routes = append(r.routes, &route)
|
||||
return &route
|
||||
}
|
||||
|
||||
func (r *router) RoutePrefix(method, path string, handler appHandler) *route {
|
||||
route := r.Route(method, path, handler)
|
||||
route.prefix = true
|
||||
return route
|
||||
}
|
||||
|
||||
func (r *router) Handler() http.Handler {
|
||||
return appHandler(func(w http.ResponseWriter, req *http.Request) *appError {
|
||||
for _, route := range r.routes {
|
||||
if route.match(req) {
|
||||
return route.handler(w, req)
|
||||
}
|
||||
}
|
||||
return NotFoundHandler(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *route) Header(header, value string) {
|
||||
r.MatcherFunc(func(req *http.Request) bool {
|
||||
return req.Header.Get(header) == value
|
||||
})
|
||||
}
|
||||
|
||||
func (r *route) MatcherFunc(f func(*http.Request) bool) {
|
||||
r.matcherFunc = f
|
||||
}
|
||||
|
||||
func (r *route) match(req *http.Request) bool {
|
||||
if req.Method != r.method {
|
||||
return false
|
||||
}
|
||||
if r.prefix {
|
||||
if !strings.HasPrefix(req.URL.Path, r.path) {
|
||||
return false
|
||||
}
|
||||
} else if r.path != req.URL.Path {
|
||||
return false
|
||||
}
|
||||
return r.matcherFunc == nil || r.matcherFunc(req)
|
||||
}
|
73
index.html
73
index.html
|
@ -5,11 +5,12 @@
|
|||
<title>What is my IP address? — {{ .Host }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="What is my IP address?">
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.6.2/pure-min.css">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.6.2/grids-responsive-min.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pure/0.6.2/pure-min.css">
|
||||
<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 {
|
||||
|
@ -42,7 +43,7 @@
|
|||
</div>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-2">
|
||||
<h2>CLI examples:</h2>
|
||||
<h2>CLI examples</h2>
|
||||
<pre>
|
||||
$ curl {{ .Host }}
|
||||
{{ .IP }}
|
||||
|
@ -58,63 +59,67 @@ $ fetch -qo- https://{{ .Host }}
|
|||
|
||||
$ bat -print=b {{ .Host }}/ip
|
||||
{{ .IP }}</pre>
|
||||
{{ if .IsLookupCountryEnabled }}
|
||||
<h2>Country lookup:</h2>
|
||||
{{ if .Country }}
|
||||
<h2>Country lookup</h2>
|
||||
<pre>
|
||||
$ http {{ .Host }}/country
|
||||
{{ .Country }}</pre>
|
||||
{{ .Country }}
|
||||
|
||||
$ http {{ .Host }}/country-iso
|
||||
{{ .CountryISO }}</pre>
|
||||
{{ end }}
|
||||
{{ if .IsLookupCityEnabled }}
|
||||
<h2>City lookup:</h2>
|
||||
{{ if .City }}
|
||||
<h2>City lookup</h2>
|
||||
<pre>
|
||||
$ http {{ .Host }}/city
|
||||
{{ .City }}</pre>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-2">
|
||||
<h2>JSON output:</h2>
|
||||
<h2>JSON output</h2>
|
||||
<pre>
|
||||
$ http {{ .Host }}/json
|
||||
{ {{ if .IsLookupCityEnabled }}
|
||||
"city": "{{ .City }}",{{ end }}{{ if .IsLookupCountryEnabled }}
|
||||
"country": "{{ .Country }}",{{ end }}{{ if .IsLookupAddrEnabled }}
|
||||
"hostname": "{{ .Hostname }}",{{ end }}
|
||||
"ip": "{{ .IP }}",
|
||||
"ip_decimal": {{ .IPDecimal }}
|
||||
}</pre>
|
||||
|
||||
<p>Setting the Accept header to application/json also works.</p>
|
||||
<h2>Plain output:</h2>
|
||||
{{ .JSON }}</pre>
|
||||
<p>Setting the <code>Accept: application/json</code> header also works as expected.</p>
|
||||
<h2>Plain output</h2>
|
||||
<p>Always returns the IP address including a trailing newline, regardless of user agent.</p>
|
||||
<pre>
|
||||
$ http {{ .Host }}/ip
|
||||
{{ .IP }}</pre>
|
||||
{{ if .IsLookupPortEnabled }}
|
||||
<h2>Testing port connectivity:</h2>
|
||||
{{ if .Port }}
|
||||
<h2>Port testing</h2>
|
||||
<pre>
|
||||
$ http {{ .Host }}/port/8080
|
||||
{
|
||||
"ip": "{{ .IP }}",
|
||||
"port": 8080,
|
||||
"reachable": false
|
||||
"ip": "{{ .IP }}",
|
||||
"port": 8080,
|
||||
"reachable": false
|
||||
}</pre>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-1">
|
||||
<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 using this service from automated scripts/tools permitted?</h3>
|
||||
<p>Yes, as long as the rate limit is respected. <em>Please limit
|
||||
automated requests to 1 request per minute</em>. No guarantee is made
|
||||
for requests that exceed this limit. They may be rate-limited (with a
|
||||
429 response code) or dropped entirely.</p>
|
||||
<h3>Is automated use of this service permitted?</h3>
|
||||
<p>
|
||||
Yes, as long as the rate limit is respected. The rate limit is in
|
||||
place to ensure a fair service for all.
|
||||
</p>
|
||||
<p>
|
||||
<em>Please limit automated requests to 1 request per minute</em>. No
|
||||
guarantee is made for requests that exceed this limit. They may be
|
||||
rate-limited, with a 429 status code, or dropped entirely.
|
||||
</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>
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
package geo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net"
|
||||
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
Country(net.IP) (Country, error)
|
||||
City(net.IP) (City, error)
|
||||
IsEmpty() bool
|
||||
}
|
||||
|
||||
type Country struct {
|
||||
Name string
|
||||
ISO string
|
||||
IsEU *bool
|
||||
}
|
||||
|
||||
type City struct {
|
||||
Name string
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
type geoip struct {
|
||||
country *geoip2.Reader
|
||||
city *geoip2.Reader
|
||||
}
|
||||
|
||||
func Open(countryDB, cityDB string) (Reader, error) {
|
||||
var country, city *geoip2.Reader
|
||||
if countryDB != "" {
|
||||
r, err := geoip2.Open(countryDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
country = r
|
||||
}
|
||||
if cityDB != "" {
|
||||
r, err := geoip2.Open(cityDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
city = r
|
||||
}
|
||||
return &geoip{country: country, city: city}, nil
|
||||
}
|
||||
|
||||
func (g *geoip) Country(ip net.IP) (Country, error) {
|
||||
country := Country{}
|
||||
if g.country == nil {
|
||||
return country, nil
|
||||
}
|
||||
record, err := g.country.Country(ip)
|
||||
if err != nil {
|
||||
return country, err
|
||||
}
|
||||
if c, exists := record.Country.Names["en"]; exists {
|
||||
country.Name = c
|
||||
}
|
||||
if c, exists := record.RegisteredCountry.Names["en"]; exists && country.Name == "" {
|
||||
country.Name = c
|
||||
}
|
||||
if record.Country.IsoCode != "" {
|
||||
country.ISO = record.Country.IsoCode
|
||||
}
|
||||
if record.RegisteredCountry.IsoCode != "" && country.ISO == "" {
|
||||
country.ISO = record.RegisteredCountry.IsoCode
|
||||
}
|
||||
isEU := record.Country.IsInEuropeanUnion || record.RegisteredCountry.IsInEuropeanUnion
|
||||
country.IsEU = &isEU
|
||||
return country, nil
|
||||
}
|
||||
|
||||
func (g *geoip) City(ip net.IP) (City, error) {
|
||||
city := City{}
|
||||
if g.city == nil {
|
||||
return city, nil
|
||||
}
|
||||
record, err := g.city.City(ip)
|
||||
if err != nil {
|
||||
return city, err
|
||||
}
|
||||
if c, exists := record.City.Names["en"]; exists {
|
||||
city.Name = c
|
||||
}
|
||||
if !math.IsNaN(record.Location.Latitude) {
|
||||
city.Latitude = record.Location.Latitude
|
||||
}
|
||||
if !math.IsNaN(record.Location.Longitude) {
|
||||
city.Longitude = record.Location.Longitude
|
||||
}
|
||||
return city, nil
|
||||
}
|
||||
|
||||
func (g *geoip) IsEmpty() bool {
|
||||
return g.country == nil && g.city == nil
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package iputil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LookupAddr(ip net.IP) (string, error) {
|
||||
names, err := net.LookupAddr(ip.String())
|
||||
if err != nil || len(names) == 0 {
|
||||
return "", err
|
||||
}
|
||||
// Always return unrooted name
|
||||
return strings.TrimRight(names[0], "."), nil
|
||||
}
|
||||
|
||||
func LookupPort(ip net.IP, port uint64) error {
|
||||
address := fmt.Sprintf("[%s]:%d", ip, port)
|
||||
conn, err := net.DialTimeout("tcp", address, 2*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
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 *big.Int
|
||||
}{
|
||||
{"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.Cmp(i) != 0 {
|
||||
t.Errorf("Expected %d, got %d for IP %s", tt.out, i, tt.in)
|
||||
}
|
||||
}
|
||||
}
|
71
main.go
71
main.go
|
@ -1,71 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
flags "github.com/jessevdk/go-flags"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/mpolden/ipd/api"
|
||||
)
|
||||
|
||||
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:""`
|
||||
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"`
|
||||
LogLevel string `short:"L" long:"log-level" description:"Log level to use" default:"info" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic"`
|
||||
}
|
||||
_, err := flags.ParseArgs(&opts, os.Args)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log := logrus.New()
|
||||
level, err := logrus.ParseLevel(opts.LogLevel)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Level = level
|
||||
|
||||
oracle := api.NewOracle()
|
||||
if opts.ReverseLookup {
|
||||
log.Println("Enabling reverse lookup")
|
||||
oracle.EnableLookupAddr()
|
||||
}
|
||||
if opts.PortLookup {
|
||||
log.Println("Enabling port lookup")
|
||||
oracle.EnableLookupPort()
|
||||
}
|
||||
if opts.CountryDBPath != "" {
|
||||
log.Printf("Enabling country lookup (using database: %s)", opts.CountryDBPath)
|
||||
if err := oracle.EnableLookupCountry(opts.CountryDBPath); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if opts.CityDBPath != "" {
|
||||
log.Printf("Enabling city lookup (using database: %s)", opts.CityDBPath)
|
||||
if err := oracle.EnableLookupCity(opts.CityDBPath); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if opts.IPHeader != "" {
|
||||
log.Printf("Trusting header %s to contain correct remote IP", opts.IPHeader)
|
||||
}
|
||||
|
||||
api := api.New(oracle, log)
|
||||
api.Template = opts.Template
|
||||
api.IPHeader = opts.IPHeader
|
||||
|
||||
log.Printf("Listening on http://%s", opts.Listen)
|
||||
if err := http.ListenAndServe(opts.Listen, api.Router()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue