Files
iplookup/iplookup.go
2024-09-01 12:57:50 +02:00

568 lines
12 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 (
port = flag.Int("p", 8080, "Port to listen on")
name = flag.String("n", "iplookup.fr", "Name of the application")
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
Geo Geo
Asn Asn
Ssl Ssl
}
type Cache struct {
Query string
Info Info
Time time.Time
}
type Tmpl struct {
Hostname string
Color string
Title 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 == "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, error) {
lookup, err := net.LookupIP(domain)
if err != nil {
fmt.Println(err)
return info, err
}
info.Ip = lookup[0].String()
ssl := Ssl{}
conn, err := tls.Dial("tcp", domain+":443", &tls.Config{InsecureSkipVerify: true})
if err != nil {
fmt.Println(err)
return info, err
}
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, err
}
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 {
return cache[i].Info
} else {
cache = append(cache[:i], cache[i+1:]...)
}
}
}
return info
}
func putCache(query string, info Info) {
info.Cached = true
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{}
isCli := false
var err error
if *verbose {
fmt.Println("Request:", r)
}
if r.URL.Path == "/favicon.ico" {
http.Error(w, "Not found", http.StatusNotFound)
return
}
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)
if !info.Cached {
if *geocity != "" {
info.Geo = getGeo(info.Ip)
}
if *geoasn != "" {
info.Asn = getAsn(info.Ip)
}
putCache(info.Ip, info)
}
info, err = getIpInfo(info, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println(err)
}
}
args := strings.Split(r.URL.Path, "/")
if len(args) > 1 && args[1] != "" {
rtype := reqtype(args[1])
if rtype == "" {
http.Error(w, "Not found", http.StatusNotFound)
return
}
info = getCache(args[1], info)
if !info.Cached {
if rtype == "domain" {
info.Query = true
domain := args[1]
info, err = getSslInfo(domain, info)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println(err)
return
}
} else if rtype == "ip" {
info.Query = true
info.Ip = args[1]
} else {
info, err = getIP(info, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Println(err)
return
}
}
if *geocity != "" {
info.Geo = getGeo(info.Ip)
}
if *geoasn != "" {
info.Asn = getAsn(info.Ip)
}
putCache(args[1], info)
}
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,
Color: colors[rand.Intn(len(colors))],
Hostname: r.Host,
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)
http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)
}