600 lines
13 KiB
Go
600 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"flag"
|
|
"fmt"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/oschwald/geoip2-golang"
|
|
"golang.org/x/exp/rand"
|
|
)
|
|
|
|
var (
|
|
bind = flag.String("b", "0.0.0.0", "Bind to address")
|
|
port = flag.Int("p", 8080, "Port to listen on")
|
|
name = flag.String("n", "iplookup.fr", "Name of the application")
|
|
domain = flag.String("d", "iplookup.fr", "Domain name")
|
|
shortdomain = flag.String("s", "iplk.fr", "Short domain name")
|
|
geocity = flag.String("c", "", "Path to the GeoIP database")
|
|
geoasn = flag.String("a", "", "Path to the GeoASN database")
|
|
verbose = flag.Bool("v", false, "Enable verbose logging")
|
|
ttl = flag.Int("t", 300, "Cache TTL in seconds")
|
|
maxcache = flag.Int("m", 2048, "Max number of entries in cache")
|
|
|
|
cache = make([]Cache, 0)
|
|
|
|
cli = []string{
|
|
"curl",
|
|
"HTTPie",
|
|
"httpie-go",
|
|
"Wget",
|
|
"fetch libfetch",
|
|
"Go",
|
|
"Go-http-client",
|
|
"ddclient",
|
|
"Mikrotik",
|
|
"xh",
|
|
}
|
|
colors = []string{
|
|
"#2488bf",
|
|
"#d84d3d",
|
|
"#f39700",
|
|
"#4caf50",
|
|
}
|
|
)
|
|
|
|
type Ssl struct {
|
|
San []string
|
|
Ocsp []string
|
|
Subject string
|
|
Issuer string
|
|
Alpn string
|
|
Not_before string
|
|
Not_after string
|
|
}
|
|
|
|
type Geo struct {
|
|
Country string
|
|
Ccode string
|
|
City string
|
|
Latitude float64
|
|
Longitude float64
|
|
Postal string
|
|
Timezone string
|
|
}
|
|
|
|
type Asn struct {
|
|
Asn uint
|
|
Org string
|
|
}
|
|
|
|
type Info struct {
|
|
Ip string
|
|
Hostname string
|
|
Query bool
|
|
Port string
|
|
User_agent string
|
|
Mime_type string
|
|
Language string
|
|
Content_type string
|
|
Encoding string
|
|
Method string
|
|
Cache_control string
|
|
Referer string
|
|
X_forwarded_for string
|
|
X_forwarded_proto string
|
|
Cached bool
|
|
Ttl int
|
|
Geo Geo
|
|
Asn Asn
|
|
Ssl Ssl
|
|
}
|
|
|
|
type Cache struct {
|
|
Query string
|
|
Info Info
|
|
Time time.Time
|
|
}
|
|
|
|
type Tmpl struct {
|
|
Color string
|
|
Title string
|
|
Domain string
|
|
Short string
|
|
Argument string
|
|
Info Info
|
|
}
|
|
|
|
func getElem(info Info, elem string) string {
|
|
elem = strings.ToLower(elem)
|
|
elem = strings.ReplaceAll(elem, "-", "_")
|
|
switch elem {
|
|
case "hostname":
|
|
return info.Hostname
|
|
case "host":
|
|
return info.Hostname
|
|
case "ip":
|
|
return info.Ip
|
|
case "port":
|
|
return info.Port
|
|
case "user_agent":
|
|
return info.User_agent
|
|
case "ua":
|
|
return info.User_agent
|
|
case "useragent":
|
|
return info.User_agent
|
|
case "mime_type":
|
|
return info.Mime_type
|
|
case "language":
|
|
return info.Language
|
|
case "content_type":
|
|
return info.Content_type
|
|
case "encoding":
|
|
return info.Encoding
|
|
case "method":
|
|
return info.Method
|
|
case "cache_control":
|
|
return info.Cache_control
|
|
case "referer":
|
|
return info.Referer
|
|
case "x_forwarded_for":
|
|
return info.X_forwarded_for
|
|
case "x_forwarded_proto":
|
|
return info.X_forwarded_proto
|
|
case "country":
|
|
return info.Geo.Country
|
|
case "ccode":
|
|
return info.Geo.Ccode
|
|
case "cc":
|
|
return info.Geo.Ccode
|
|
case "city":
|
|
return info.Geo.City
|
|
case "latitude":
|
|
return fmt.Sprintf("%f", info.Geo.Latitude)
|
|
case "longitude":
|
|
return fmt.Sprintf("%f", info.Geo.Longitude)
|
|
case "postal":
|
|
return info.Geo.Postal
|
|
case "timezone":
|
|
return info.Geo.Timezone
|
|
case "asn":
|
|
return fmt.Sprintf("%d", info.Asn.Asn)
|
|
case "org":
|
|
return info.Asn.Org
|
|
case "organisation":
|
|
return info.Asn.Org
|
|
case "isp":
|
|
return info.Asn.Org
|
|
case "ssl_san":
|
|
return strings.Join(info.Ssl.San, ", ")
|
|
case "ssl_ocsp":
|
|
return strings.Join(info.Ssl.Ocsp, ", ")
|
|
case "ssl_subject":
|
|
return info.Ssl.Subject
|
|
case "ssl_issuer":
|
|
return info.Ssl.Issuer
|
|
case "ssl_alpn":
|
|
return info.Ssl.Alpn
|
|
case "ssl_not_after":
|
|
return info.Ssl.Not_after
|
|
case "ssl_not_before":
|
|
return info.Ssl.Not_before
|
|
case "san":
|
|
return strings.Join(info.Ssl.San, ", ")
|
|
case "ocsp":
|
|
return strings.Join(info.Ssl.Ocsp, ", ")
|
|
case "subject":
|
|
return info.Ssl.Subject
|
|
case "issuer":
|
|
return info.Ssl.Issuer
|
|
case "alpn":
|
|
return info.Ssl.Alpn
|
|
case "not_after":
|
|
return info.Ssl.Not_after
|
|
case "not_before":
|
|
return info.Ssl.Not_before
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func writeSsl(w http.ResponseWriter, ssl Ssl) {
|
|
if ssl.San == nil {
|
|
return
|
|
}
|
|
v := reflect.ValueOf(ssl)
|
|
t := reflect.TypeOf(ssl)
|
|
for i := 0; i < t.NumField(); i++ {
|
|
if v.Field(i).Interface() != "" {
|
|
fmt.Fprintf(w, "%s: %v\n", strings.ToLower(t.Field(i).Name), v.Field(i).Interface())
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeAsn(w http.ResponseWriter, asn Asn) {
|
|
if asn.Asn == 0 {
|
|
return
|
|
}
|
|
v := reflect.ValueOf(asn)
|
|
t := reflect.TypeOf(asn)
|
|
for i := 0; i < t.NumField(); i++ {
|
|
if v.Field(i).Interface() != "" {
|
|
fmt.Fprintf(w, "%s: %v\n", strings.ToLower(t.Field(i).Name), v.Field(i).Interface())
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeGeo(w http.ResponseWriter, geo Geo) {
|
|
if geo.Country == "" {
|
|
return
|
|
}
|
|
v := reflect.ValueOf(geo)
|
|
t := reflect.TypeOf(geo)
|
|
for i := 0; i < t.NumField(); i++ {
|
|
if v.Field(i).Interface() != "" {
|
|
fmt.Fprintf(w, "%s: %v\n", strings.ToLower(t.Field(i).Name), v.Field(i).Interface())
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeInfo(w http.ResponseWriter, info Info) {
|
|
w.WriteHeader(http.StatusOK)
|
|
v := reflect.ValueOf(info)
|
|
t := reflect.TypeOf(info)
|
|
for i := 0; i < t.NumField(); i++ {
|
|
if t.Field(i).Name == "Query" || t.Field(i).Name == "Cached" {
|
|
continue
|
|
} else if t.Field(i).Name == "Geo" {
|
|
writeGeo(w, v.Field(i).Interface().(Geo))
|
|
} else if t.Field(i).Name == "Asn" {
|
|
writeAsn(w, v.Field(i).Interface().(Asn))
|
|
} else if t.Field(i).Name == "Ssl" {
|
|
writeSsl(w, v.Field(i).Interface().(Ssl))
|
|
} else if v.Field(i).Interface() != "" {
|
|
fmt.Fprintf(w, "%s: %v\n", strings.ToLower(t.Field(i).Name), v.Field(i).Interface())
|
|
}
|
|
}
|
|
}
|
|
|
|
func reqtype(ip string) string {
|
|
if net.ParseIP(ip) != nil {
|
|
return "ip"
|
|
}
|
|
for i := 0; i < len(ip); i++ {
|
|
if ip[i] == ':' {
|
|
return "ip"
|
|
}
|
|
if ip[i] < '0' || ip[i] > '9' {
|
|
if strings.Contains(ip, ".") {
|
|
return "domain"
|
|
} else {
|
|
return "info"
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getSslInfo(domain string, info Info) Info {
|
|
lookup, err := net.LookupIP(domain)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return info
|
|
}
|
|
info.Ip = lookup[0].String()
|
|
|
|
ssl := Ssl{}
|
|
conn, err := tls.DialWithDialer(&net.Dialer{
|
|
Timeout: 2 * time.Second,
|
|
}, "tcp", domain+":443", &tls.Config{InsecureSkipVerify: true})
|
|
if err != nil {
|
|
if *verbose {
|
|
fmt.Println(err)
|
|
}
|
|
return info
|
|
}
|
|
defer conn.Close()
|
|
|
|
ssl.San = conn.ConnectionState().PeerCertificates[0].DNSNames
|
|
ssl.Subject = conn.ConnectionState().PeerCertificates[0].Subject.String()
|
|
ssl.Issuer = conn.ConnectionState().PeerCertificates[0].Issuer.String()
|
|
ssl.Alpn = conn.ConnectionState().NegotiatedProtocol
|
|
ssl.Ocsp = conn.ConnectionState().PeerCertificates[0].OCSPServer
|
|
ssl.Not_after = conn.ConnectionState().PeerCertificates[0].NotAfter.String()
|
|
ssl.Not_before = conn.ConnectionState().PeerCertificates[0].NotBefore.String()
|
|
info.Ssl = ssl
|
|
return info
|
|
}
|
|
|
|
func getAsn(ip string) Asn {
|
|
asn := Asn{}
|
|
ipdb, err := geoip2.Open(*geoasn)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return Asn{}
|
|
}
|
|
defer ipdb.Close()
|
|
|
|
record, err := ipdb.ASN(net.ParseIP(ip))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return Asn{}
|
|
}
|
|
|
|
asn.Asn = record.AutonomousSystemNumber
|
|
asn.Org = record.AutonomousSystemOrganization
|
|
return asn
|
|
}
|
|
|
|
func getGeo(ip string) Geo {
|
|
geo := Geo{}
|
|
ipdb, err := geoip2.Open(*geocity)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return Geo{}
|
|
}
|
|
defer ipdb.Close()
|
|
|
|
record, err := ipdb.City(net.ParseIP(ip))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return Geo{}
|
|
}
|
|
|
|
geo.City = record.City.Names["en"]
|
|
geo.Latitude, geo.Longitude = record.Location.Latitude, record.Location.Longitude
|
|
geo.Country = record.Country.Names["en"]
|
|
geo.Ccode = record.Country.IsoCode
|
|
geo.Postal = record.Postal.Code
|
|
geo.Timezone = record.Location.TimeZone
|
|
|
|
return geo
|
|
}
|
|
|
|
func getIP(info Info, r *http.Request) (Info, error) {
|
|
var err error
|
|
if r.Header.Get("X-Forwarded-For") != "" {
|
|
info.Ip = r.Header.Get("X-Forwarded-For")
|
|
} else if r.Header.Get("X-Real-IP") != "" {
|
|
info.Ip = r.Header.Get("X-Real-IP")
|
|
} else if r.Header.Get("X-Client-IP") != "" {
|
|
info.Ip = r.Header.Get("X-Client-IP")
|
|
} else if r.Header.Get("CF-Connecting-IP") != "" {
|
|
info.Ip = r.Header.Get("CF-Connecting-IP")
|
|
} else {
|
|
info.Ip, info.Port, err = net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return info, err
|
|
}
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func getIpInfo(info Info, r *http.Request) (Info, error) {
|
|
hosts, err := net.LookupAddr(info.Ip)
|
|
if err != nil {
|
|
hosts = []string{info.Ip}
|
|
}
|
|
info.Hostname = hosts[0]
|
|
if !info.Query {
|
|
info.User_agent = r.UserAgent()
|
|
info.Mime_type = r.Header.Get("Accept")
|
|
info.Language = r.Header.Get("Accept-Language")
|
|
info.Content_type = r.Header.Get("Content-Type")
|
|
info.Encoding = r.Header.Get("Accept-Encoding")
|
|
info.Method = r.Method
|
|
info.Cache_control = r.Header.Get("Cache-Control")
|
|
info.Referer = r.Header.Get("Referer")
|
|
info.X_forwarded_for = r.Header.Get("X-Forwarded-For")
|
|
info.X_forwarded_proto = r.Header.Get("X-Forwarded-Proto")
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func getCache(query string, info Info) Info {
|
|
for i := 0; i < len(cache); i++ {
|
|
if cache[i].Query == query {
|
|
if time.Since(cache[i].Time) < time.Duration(*ttl)*time.Second {
|
|
cache[i].Info.Ttl = int(time.Duration(*ttl)*time.Second-time.Since(cache[i].Time)) / 1000000000
|
|
return cache[i].Info
|
|
} else {
|
|
cache = append(cache[:i], cache[i+1:]...)
|
|
}
|
|
}
|
|
}
|
|
return info
|
|
}
|
|
|
|
func putCache(query string, info Info) {
|
|
info.Cached = true
|
|
info.Ttl = *ttl
|
|
cache = append(cache, Cache{Info: info, Query: query, Time: time.Now()})
|
|
if len(cache) > *maxcache {
|
|
cache = cache[1:]
|
|
}
|
|
}
|
|
|
|
func handler(w http.ResponseWriter, r *http.Request) {
|
|
info := Info{}
|
|
arg := ""
|
|
isCli := false
|
|
var err error
|
|
|
|
if *verbose {
|
|
fmt.Println("Request:", r)
|
|
}
|
|
|
|
if r.Method != "GET" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
for i := 0; i < len(cli); i++ {
|
|
if strings.Contains(r.UserAgent(), cli[i]) {
|
|
isCli = true
|
|
}
|
|
}
|
|
|
|
if r.URL.Path == "/" {
|
|
info, err = getIP(info, r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
if isCli {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(info.Ip + "\n"))
|
|
return
|
|
}
|
|
info = getCache(info.Ip, info)
|
|
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", info.Ttl))
|
|
if !info.Cached {
|
|
if *geocity != "" {
|
|
info.Geo = getGeo(info.Ip)
|
|
}
|
|
if *geoasn != "" {
|
|
info.Asn = getAsn(info.Ip)
|
|
}
|
|
putCache(info.Ip, info)
|
|
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", *ttl))
|
|
}
|
|
info, err = getIpInfo(info, r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
fmt.Println(err)
|
|
}
|
|
}
|
|
|
|
if r.URL.Path == "/favicon.ico" {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
http.ServeFile(w, r, "favicon.png")
|
|
return
|
|
}
|
|
|
|
args := strings.Split(r.URL.Path, "/")
|
|
if len(args) > 1 && args[1] != "" {
|
|
arg = "/" + args[1]
|
|
rtype := reqtype(args[1])
|
|
if rtype == "" {
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if rtype == "ip" {
|
|
info.Query = true
|
|
info.Ip = args[1]
|
|
info = getCache(args[1], info)
|
|
} else if rtype == "domain" {
|
|
info.Query = true
|
|
info = getCache(args[1], info)
|
|
} else {
|
|
info, err = getIP(info, r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
info = getCache(info.Ip, info)
|
|
}
|
|
|
|
if !info.Cached {
|
|
if rtype == "domain" {
|
|
info.Query = true
|
|
info = getSslInfo(args[1], info)
|
|
}
|
|
if info.Ip == "" {
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if *geocity != "" {
|
|
info.Geo = getGeo(info.Ip)
|
|
}
|
|
if *geoasn != "" {
|
|
info.Asn = getAsn(info.Ip)
|
|
}
|
|
|
|
if rtype != "info" {
|
|
putCache(args[1], info)
|
|
} else {
|
|
putCache(info.Ip, info)
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", info.Ttl))
|
|
|
|
info, err = getIpInfo(info, r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
if rtype == "info" || (len(args) > 2 && args[2] != "") {
|
|
if args[len(args)-1] == "all" {
|
|
writeInfo(w, info)
|
|
return
|
|
}
|
|
if args[len(args)-1] == "geo" {
|
|
writeGeo(w, info.Geo)
|
|
return
|
|
}
|
|
if args[len(args)-1] == "as" {
|
|
writeAsn(w, info.Asn)
|
|
return
|
|
}
|
|
if args[len(args)-1] == "ssl" {
|
|
writeSsl(w, info.Ssl)
|
|
return
|
|
}
|
|
res := getElem(info, args[len(args)-1])
|
|
if res == "" {
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintf(w, "%s\n", res)
|
|
return
|
|
}
|
|
}
|
|
|
|
if isCli {
|
|
writeInfo(w, info)
|
|
return
|
|
}
|
|
|
|
tmpl := Tmpl{
|
|
Title: *name,
|
|
Domain: *domain,
|
|
Short: *shortdomain,
|
|
Color: colors[rand.Intn(len(colors))],
|
|
Argument: arg,
|
|
Info: info,
|
|
}
|
|
t, err := template.ParseFiles("template.html")
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
fmt.Println(err)
|
|
}
|
|
err = t.Execute(w, tmpl)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
fmt.Println("Listening on port", *port)
|
|
http.HandleFunc("/", handler)
|
|
err := http.ListenAndServe(fmt.Sprintf("%s:%d", *bind, *port), nil)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
}
|