diff --git a/.github/workflows/lint_golang.yml b/.github/workflows/lint_golang.yml new file mode 100644 index 0000000..20333ac --- /dev/null +++ b/.github/workflows/lint_golang.yml @@ -0,0 +1,44 @@ +--- + +name: Lint GoLang + +on: + push: + branches: [latest, v*] + paths: + - 'backend/golang/**.go' + - '.github/workflows/lint_golang.yml' + pull_request: + branches: [latest, v*] + paths: + - 'backend/golang/**.go' + - '.github/workflows/lint_golang.yml' + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 2 + + steps: + - uses: actions/checkout@v3 + + - name: Install python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Build binary + run: | + cd backend/golang/main/ + go build + shell: bash + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + args: '--config=../../.golangci.yml --out-format=colored-line-number' + working-directory: 'backend/golang' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint_python.yml similarity index 83% rename from .github/workflows/lint.yml rename to .github/workflows/lint_python.yml index 13cdce5..c2d0294 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint_python.yml @@ -1,18 +1,18 @@ --- -name: Lint +name: Lint Python on: push: branches: [latest] paths: - '**.py' - - '.github/workflows/lint.yml' + - '.github/workflows/lint_python.yml' pull_request: branches: [latest] paths: - '**.py' - - '.github/workflows/lint.yml' + - '.github/workflows/lint_python.yml' jobs: build: @@ -20,6 +20,7 @@ jobs: matrix: python-version: [3.10] runs-on: ubuntu-latest + timeout-minutes: 2 steps: - name: Checkout diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b896b54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +backend/golang/main/main +backend/geoip_lookup_golang +test/geoip_lookup_golang \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..617fbd8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,10 @@ +--- + +run: + go: '1.21' + skip-dirs: [] + skip-files: [] + +issues: + exclude: [] + exclude-rules: [] diff --git a/backend/golang/cnf/ipinfo.go b/backend/golang/cnf/ipinfo.go new file mode 100644 index 0000000..6c8aecb --- /dev/null +++ b/backend/golang/cnf/ipinfo.go @@ -0,0 +1,78 @@ +package cnf + +import "net" + +const DB_TYPE_IPINFO uint8 = 1 + +// IPInfo schema: https://github.com/ipinfo/sample-database/ +var IPINFO_COUNTRY struct { + StartIp net.IP `maxminddb:"start_ip"` + EndIp net.IP `maxminddb:"end_ip"` + Country string `maxminddb:"country"` + CountryName string `maxminddb:"country_name"` + Continent string `maxminddb:"continent"` + ContinentName string `maxminddb:"continent_name"` +} + +var IPINFO_ASN struct { + StartIp net.IP `maxminddb:"start_ip"` + EndIp net.IP `maxminddb:"end_ip"` + ASN string `maxminddb:"asn"` + Name string `maxminddb:"name"` + Domain string `maxminddb:"domain"` +} + +var IPINFO_ASN_EXT struct { + StartIp net.IP `maxminddb:"start_ip"` + EndIp net.IP `maxminddb:"end_ip"` + JoinKey net.IP `maxminddb:"join_key"` + ASN string `maxminddb:"asn"` + Name string `maxminddb:"name"` + Domain string `maxminddb:"domain"` + Type string `maxminddb:"type"` + Country string `maxminddb:"country"` +} + +var IPINFO_COUNTRY_ASN struct { + StartIp net.IP `maxminddb:"start_ip"` + EndIp net.IP `maxminddb:"end_ip"` + Country string `maxminddb:"country"` + CountryName string `maxminddb:"country_name"` + Continent string `maxminddb:"continent"` + ContinentName string `maxminddb:"continent_name"` + ASN string `maxminddb:"asn"` + ASName string `maxminddb:"as_name"` + ASDomain string `maxminddb:"as_domain"` +} + +var IPINFO_PRIVACY struct { + StartIp net.IP `maxminddb:"start_ip"` + EndIp net.IP `maxminddb:"end_ip"` + JoinKey net.IP `maxminddb:"join_key"` + Hosting bool `maxminddb:"hosting"` + Proxy bool `maxminddb:"proxy"` + Tor bool `maxminddb:"tor"` + Vpn bool `maxminddb:"vpn"` + Relay bool `maxminddb:"relay"` + Service string `maxminddb:"service"` +} + +var IPINFO_CITY struct { + StartIp net.IP `maxminddb:"start_ip"` + EndIp net.IP `maxminddb:"end_ip"` + JoinKey net.IP `maxminddb:"join_key"` + City string `maxminddb:"city"` + Region string `maxminddb:"region"` + Country string `maxminddb:"country"` + Latitude float32 `maxminddb:"latitude"` + Longitude float32 `maxminddb:"longitude"` + PostalCode string `maxminddb:"postal_code"` + Timezone string `maxminddb:"timezone"` +} + +// todo: https://github.com/ipinfo/sample-database/tree/main/IP%20to%20Company +// todo: https://github.com/ipinfo/sample-database/tree/main/IP%20to%20Mobile%20Carrier +// todo: https://github.com/ipinfo/sample-database/tree/main/IP%20Geolocation%20Extended +// todo: https://github.com/ipinfo/sample-database/tree/main/Privacy%20Detection%20Extended +// todo: https://github.com/ipinfo/sample-database/tree/main/Abuse%20Contact +// todo: https://github.com/ipinfo/sample-database/tree/main/Hosted%20Domains diff --git a/backend/golang/cnf/main.go b/backend/golang/cnf/main.go new file mode 100644 index 0000000..030221b --- /dev/null +++ b/backend/golang/cnf/main.go @@ -0,0 +1,22 @@ +package cnf + +const PORT = 7069 + +var LOOKUP_MAPPING = map[uint8]interface{}{ + DB_TYPE_IPINFO: map[string]interface{}{ + "country_asn": IPINFO_COUNTRY_ASN, + "country": IPINFO_COUNTRY, + "city": IPINFO_CITY, + "asn": IPINFO_ASN, + "privacy": IPINFO_PRIVACY, + }, + DB_TYPE_MAXMIND: map[string]interface{}{ + "country_asn": nil, + "country": MAXMIND_COUNTRY, + "city": MAXMIND_CITY, + "asn": MAXMIND_ASN, + "privacy": MAXMIND_PRIVACY, + }, +} + +var LOOKUP = LOOKUP_MAPPING[DB_TYPE].(map[string]interface{}) diff --git a/backend/golang/cnf/maxmind.go b/backend/golang/cnf/maxmind.go new file mode 100644 index 0000000..4367ac3 --- /dev/null +++ b/backend/golang/cnf/maxmind.go @@ -0,0 +1,71 @@ +package cnf + +const DB_TYPE_MAXMIND uint8 = 2 + +// MaxMind schema: https://github.com/maxmind/MaxMind-DB/tree/main/source-data +var MAXMIND_COUNTRY struct { + Country struct { + Code string `maxminddb:"iso_code"` + Id uint `maxminddb:"geoname_id"` + EuopeanUnion bool `maxminddb:"is_in_european_union"` + } `maxminddb:"country"` + RegisteredCountry struct { + Code string `maxminddb:"iso_code"` + Id uint `maxminddb:"geoname_id"` + EuopeanUnion bool `maxminddb:"is_in_european_union"` + } `maxminddb:"registered_country"` + Continent struct { + Code string `maxminddb:"code"` + Id uint `maxminddb:"geoname_id"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"continent"` +} + +var MAXMIND_ASN struct { + ASN string `maxminddb:"autonomous_system_number"` + Name string `maxminddb:"autonomous_system_organization"` +} + +var MAXMIND_CITY struct { + City struct { + Code string `maxminddb:"iso_code"` + Id uint `maxminddb:"geoname_id"` + } `maxminddb:"country"` + Country struct { + Code string `maxminddb:"iso_code"` + Id uint `maxminddb:"geoname_id"` + EuopeanUnion bool `maxminddb:"is_in_european_union"` + } `maxminddb:"country"` + RegisteredCountry struct { + Code string `maxminddb:"iso_code"` + Id uint `maxminddb:"geoname_id"` + EuopeanUnion bool `maxminddb:"is_in_european_union"` + } `maxminddb:"registered_country"` + Continent struct { + Code string `maxminddb:"code"` + Id uint `maxminddb:"geoname_id"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"continent"` + Location struct { + AccuracyRadius uint `maxminddb:"accuracy_radius"` + Latitude float32 `maxminddb:"latitude"` + Longitude float32 `maxminddb:"longitude"` + Timezone string `maxminddb:"time_zone"` + } `maxminddb:"location"` + Postal struct { + Code string `maxminddb:"code"` + } `maxminddb:"postal"` + Traits struct { + IsAnycast bool `maxminddb:"is_anycast"` + IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"` + } `maxminddb:"traits"` +} + +var MAXMIND_PRIVACY struct { // also called 'anonymous' + Any bool `maxminddb:"is_anonymous"` + Vpn bool `maxminddb:"is_anonymous_vpn"` + Tor bool `maxminddb:"is_tor_exit_node"` + Hosting bool `maxminddb:"is_hosting_provider"` + PublicProxy bool `maxminddb:"is_public_proxy"` + PrivateProxy bool `maxminddb:"is_residential_proxy"` +} diff --git a/backend/golang/cnf/user.go b/backend/golang/cnf/user.go new file mode 100644 index 0000000..6db7612 --- /dev/null +++ b/backend/golang/cnf/user.go @@ -0,0 +1,11 @@ +package cnf + +const DB_TYPE = DB_TYPE_IPINFO +const DB_QUERY_LANG = "en" // used for MaxMind DB lookups + +// const DB_TYPE = DB_TYPE_MAXMIND + +const DB_COUNTRY = "/etc/geoip/country.mmdb" +const DB_CITY = "/etc/geoip/city.mmdb" +const DB_ASN = "/etc/geoip/asn.mmdb" +const DB_PRIVACY = "/etc/geoip/privacy.mmdb" // vpns, tor, proxies, hosting diff --git a/backend/golang/go.mod b/backend/golang/go.mod new file mode 100644 index 0000000..0b56074 --- /dev/null +++ b/backend/golang/go.mod @@ -0,0 +1,8 @@ +module github.com/superstes/haproxy-geoip/backend/golang + +go 1.21.1 + +require ( + github.com/oschwald/maxminddb-golang v1.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect +) diff --git a/backend/golang/go.sum b/backend/golang/go.sum new file mode 100644 index 0000000..8fbd800 --- /dev/null +++ b/backend/golang/go.sum @@ -0,0 +1,4 @@ +github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= +github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/backend/golang/lookup/ipinfo.go b/backend/golang/lookup/ipinfo.go new file mode 100644 index 0000000..b52d2d2 --- /dev/null +++ b/backend/golang/lookup/ipinfo.go @@ -0,0 +1,27 @@ +package lookup + +import ( + "net" + + "github.com/superstes/haproxy-geoip/backend/golang/cnf" +) + +func IpInfoCountry(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_COUNTRY) +} + +func IpInfoCity(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_CITY) +} + +func IpInfoAsn(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_ASN) +} + +func IpInfoCountryAsn(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_COUNTRY) +} + +func IpInfoPrivacy(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_PRIVACY) +} diff --git a/backend/golang/lookup/mapping.go b/backend/golang/lookup/mapping.go new file mode 100644 index 0000000..a9e3b03 --- /dev/null +++ b/backend/golang/lookup/mapping.go @@ -0,0 +1,42 @@ +package lookup + +import ( + "log" + "net" + + "github.com/oschwald/maxminddb-golang" + "github.com/superstes/haproxy-geoip/backend/golang/cnf" +) + +var FUNC_MAPPING = map[uint8]interface{}{ + cnf.DB_TYPE_IPINFO: map[string]interface{}{ + "country_asn": IpInfoCountryAsn, + "country": IpInfoCountry, + "city": IpInfoCity, + "asn": IpInfoAsn, + "privacy": IpInfoPrivacy, + }, + cnf.DB_TYPE_MAXMIND: map[string]interface{}{ + "country_asn": nil, + "country": MaxMindCountry, + "city": MaxMindCity, + "asn": MaxMindAsn, + "privacy": MaxMindPrivacy, + }, +} + +var FUNC = FUNC_MAPPING[cnf.DB_TYPE].(map[string]interface{}) + +func lookupBase(ip net.IP, dataStructure interface{}, dbFile string) interface{} { + db, err := maxminddb.Open(dbFile) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + err = db.Lookup(ip, &dataStructure) + if err != nil { + log.Panic(err) + } + return dataStructure +} diff --git a/backend/golang/lookup/maxmind.go b/backend/golang/lookup/maxmind.go new file mode 100644 index 0000000..3f0d41c --- /dev/null +++ b/backend/golang/lookup/maxmind.go @@ -0,0 +1,23 @@ +package lookup + +import ( + "net" + + "github.com/superstes/haproxy-geoip/backend/golang/cnf" +) + +func MaxMindCountry(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_COUNTRY) +} + +func MaxMindCity(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_CITY) +} + +func MaxMindAsn(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_ASN) +} + +func MaxMindPrivacy(ip net.IP, dataStructure interface{}) interface{} { + return lookupBase(ip, dataStructure, cnf.DB_PRIVACY) +} diff --git a/backend/golang/main/main.go b/backend/golang/main/main.go new file mode 100644 index 0000000..7b93a32 --- /dev/null +++ b/backend/golang/main/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" +) + +func welcome() { + fmt.Printf("\n ______ ________ __ __ \n") + fmt.Println(" / ____/__ ____ / _/ __ \\ / / ____ ____ / /____ ______ ") + fmt.Println(" / / __/ _ \\/ __ \\ / // /_/ / / / / __ \\/ __ \\/ //_/ / / / __ \\") + fmt.Println("/ /_/ / __/ /_/ // // ____/ / /___/ /_/ / /_/ / ,< / /_/ / /_/ /") + fmt.Println("\\____/\\___/\\____/___/_/ /_____/\\____/\\____/_/|_|\\__,_/ .___/ ") + fmt.Println(" /_/ ") + fmt.Printf("by Superstes (GPLv3)\n\n") +} + +func main() { + welcome() + server() +} diff --git a/backend/golang/main/server.go b/backend/golang/main/server.go new file mode 100644 index 0000000..688ab90 --- /dev/null +++ b/backend/golang/main/server.go @@ -0,0 +1,52 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + + "github.com/superstes/haproxy-geoip/backend/golang/cnf" + "github.com/superstes/haproxy-geoip/backend/golang/lookup" +) + +func geoIpLookup(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("query") + ipStr := r.URL.Query().Get("ip") + if query == "" || ipStr == "" { + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, "Either 'query' or 'ip' were not provided!") + return + } + + ip := net.ParseIP(ipStr) + if ip == nil { + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, "Provided IP is not valid!") + return + } + + dataStructure, lookupExists := cnf.LOOKUP[query] + if !lookupExists || dataStructure == nil { + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, "Provided LOOKUP is not valid!") + return + } + + data := lookup.FUNC[query].(func(net.IP, interface{}) interface{})( + ip, dataStructure, + ) + // todo: allow additional filtering to get single attributes + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) + return +} + +func server() { + http.HandleFunc("/", geoIpLookup) + var listenAddr = fmt.Sprintf("127.0.0.1:%v", cnf.PORT) + fmt.Println("Listening on http://" + listenAddr) + log.Fatal(http.ListenAndServe(listenAddr, nil)) +} diff --git a/test/test.sh b/test/test.sh index d2c3a36..b0421ff 100644 --- a/test/test.sh +++ b/test/test.sh @@ -121,7 +121,10 @@ sleep 2 # echo '' # echo 'TESTING with GOLANG-BACKEND' -# todo: build binary +# cd "$(pwd)/../backend/golang/main +# go build -o ../../../test/geoip_lookup_golang +# cd ../../../test/ +# chmod +x $(pwd)/geoip_lookup_golang" # "$(pwd)/geoip_lookup_golang" > '/tmp/haproxy_geoip_backend.log' & # sleep 2 # cleanup_process 'geoip_lookup_golang'