Refactor IP data lookup with caching and enable gzip compression

This commit is contained in:
skidoodle 2024-09-29 17:58:21 +02:00
parent ff2dee80ee
commit bc9f481717
Signed by: albert
GPG key ID: A06E3070D7D55BF2
2 changed files with 69 additions and 33 deletions

View file

@ -1,7 +1,9 @@
# ipinfo # ipinfo
`ipinfo` is a powerful and efficient IP information service written in Go. It fetches GeoIP data to provide detailed information about an IP address, including geographical location, ASN, and related network details. The service automatically updates its GeoIP databases to ensure accuracy and reliability. `ipinfo` is a powerful and efficient IP information service written in Go. It fetches GeoIP data to provide detailed information about an IP address, including geographical location, ASN, and related network details. The service automatically updates its GeoIP databases to ensure accuracy and reliability.
## Features ## Features
- **IP Geolocation**: Provides city, region, country, continent, and coordinates for any IP address. - **IP Geolocation**: Provides city, region, country, continent, and coordinates for any IP address.
- **ASN Information**: Includes autonomous system number and organization. - **ASN Information**: Includes autonomous system number and organization.
- **Hostname Lookup**: Retrieves the hostname associated with the IP address. - **Hostname Lookup**: Retrieves the hostname associated with the IP address.
@ -9,7 +11,9 @@
- **JSONP Support**: Allows JSONP responses for cross-domain requests. - **JSONP Support**: Allows JSONP responses for cross-domain requests.
## Example Endpoints ## Example Endpoints
### Get information about an IP address ### Get information about an IP address
```sh ```sh
$ curl https://ip.albert.lol/9.9.9.9 $ curl https://ip.albert.lol/9.9.9.9
{ {
@ -25,14 +29,18 @@ $ curl https://ip.albert.lol/9.9.9.9
"loc": "37.8767,-122.2676" "loc": "37.8767,-122.2676"
} }
``` ```
### Get specific information (e.g., city) about an IP address ### Get specific information (e.g., city) about an IP address
```sh ```sh
$ curl https://ip.albert.lol/9.9.9.9/city $ curl https://ip.albert.lol/9.9.9.9/city
{ {
"city": "Berkeley" "city": "Berkeley"
} }
``` ```
### Use JSONP callback function ### Use JSONP callback function
```sh ```sh
$ curl https://test.albert.lol/9.9.9.9?callback=Quad9 $ curl https://test.albert.lol/9.9.9.9?callback=Quad9
/**/ typeof Quad9 === 'function' && Quad9({ /**/ typeof Quad9 === 'function' && Quad9({
@ -48,6 +56,7 @@ $ curl https://test.albert.lol/9.9.9.9?callback=Quad9
"loc": "37.8767,-122.2676" "loc": "37.8767,-122.2676"
}); });
``` ```
```html ```html
<script> <script>
let Quad9 = function(data) { let Quad9 = function(data) {
@ -58,7 +67,9 @@ let Quad9 = function(data) {
``` ```
## Running Locally ## Running Locally
### With Docker ### With Docker
```sh ```sh
git clone https://github.com/skidoodle/ipinfo git clone https://github.com/skidoodle/ipinfo
cd ipinfo cd ipinfo
@ -71,7 +82,9 @@ docker run \
-e GEOIPUPDATE_DB_DIR=<> \ -e GEOIPUPDATE_DB_DIR=<> \
ipinfo:main ipinfo:main
``` ```
### Without Docker ### Without Docker
```sh ```sh
git clone https://github.com/skidoodle/ipinfo git clone https://github.com/skidoodle/ipinfo
cd ipinfo cd ipinfo
@ -79,7 +92,9 @@ go run .
``` ```
## Deploying ## Deploying
### Docker Compose ### Docker Compose
```yaml ```yaml
services: services:
ipinfo: ipinfo:
@ -100,7 +115,9 @@ services:
retries: 3 retries: 3
start_period: 40s start_period: 40s
``` ```
### Docker Run ### Docker Run
```sh ```sh
docker run \ docker run \
-d \ -d \
@ -115,4 +132,5 @@ docker run \
``` ```
## LICENSE ## LICENSE
[GPL-3.0](https://github.com/skidoodle/ipinfo/blob/main/license) [GPL-3.0](https://github.com/skidoodle/ipinfo/blob/main/license)

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"compress/gzip"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@ -11,7 +12,6 @@ import (
var invalidIPBytes = []byte("Please provide a valid IP address.") var invalidIPBytes = []byte("Please provide a valid IP address.")
// Struct to hold IP data
type dataStruct struct { type dataStruct struct {
IP *string `json:"ip"` IP *string `json:"ip"`
Hostname *string `json:"hostname"` Hostname *string `json:"hostname"`
@ -36,9 +36,27 @@ func startServer() {
log.Fatal(http.ListenAndServe(":3000", nil)) log.Fatal(http.ListenAndServe(":3000", nil))
} }
type gzipResponseWriter struct {
http.ResponseWriter
Writer *gzip.Writer
}
func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func handler(w http.ResponseWriter, r *http.Request) { func handler(w http.ResponseWriter, r *http.Request) {
requestedThings := strings.Split(r.URL.Path, "/") requestedThings := strings.Split(r.URL.Path, "/")
// Enable gzip compression if requested
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
w = &gzipResponseWriter{Writer: gz, ResponseWriter: w}
}
// Extract IP and field
var IPAddress, field string var IPAddress, field string
if len(requestedThings) > 1 && net.ParseIP(requestedThings[1]) != nil { if len(requestedThings) > 1 && net.ParseIP(requestedThings[1]) != nil {
IPAddress = requestedThings[1] IPAddress = requestedThings[1]
@ -46,19 +64,23 @@ func handler(w http.ResponseWriter, r *http.Request) {
field = requestedThings[2] field = requestedThings[2]
} }
} else if len(requestedThings) > 1 { } else if len(requestedThings) > 1 {
field = requestedThings[1] IPAddress = requestedThings[1] // This might be an invalid IP
} }
if IPAddress == "" || IPAddress == "self" { // Check if the IP is the client's IP
if IPAddress == "" {
IPAddress = getRealIP(r) IPAddress = getRealIP(r)
} }
// Validate the IP address
ip := net.ParseIP(IPAddress) ip := net.ParseIP(IPAddress)
if ip == nil { if ip == nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") // Send 400 Bad Request with invalid IP message
w.Write(invalidIPBytes) http.Error(w, string(invalidIPBytes), http.StatusBadRequest)
return return
} }
// Check if the IP is a bogon IP
if isBogon(ip) { if isBogon(ip) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bogonData := bogonDataStruct{ bogonData := bogonDataStruct{
@ -69,13 +91,15 @@ func handler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Look up IP data
data := lookupIPData(ip) data := lookupIPData(ip)
if data == nil { if data == nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") // Send 400 Bad Request with invalid IP message
w.Write(invalidIPBytes) http.Error(w, string(invalidIPBytes), http.StatusBadRequest)
return return
} }
// Handle specific field requests
if field != "" { if field != "" {
value := getField(data, field) value := getField(data, field)
if value != nil { if value != nil {
@ -83,12 +107,14 @@ func handler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]*string{field: value}) json.NewEncoder(w).Encode(map[string]*string{field: value})
return return
} else { } else {
// Handle invalid field request
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]*string{field: nil}) json.NewEncoder(w).Encode(map[string]*string{field: nil})
return return
} }
} }
// If no specific field is requested, return the whole data
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
callback := r.URL.Query().Get("callback") callback := r.URL.Query().Get("callback")
enableJSONP := callback != "" && len(callback) < 2000 && callbackJSONP.MatchString(callback) enableJSONP := callback != "" && len(callback) < 2000 && callbackJSONP.MatchString(callback)
@ -106,30 +132,22 @@ func handler(w http.ResponseWriter, r *http.Request) {
} }
} }
// Get specific field from dataStruct var fieldMap = map[string]func(*dataStruct) *string{
func getField(data *dataStruct, field string) *string { "ip": func(d *dataStruct) *string { return d.IP },
switch field { "hostname": func(d *dataStruct) *string { return d.Hostname },
case "ip": "asn": func(d *dataStruct) *string { return d.ASN },
return data.IP "org": func(d *dataStruct) *string { return d.Org },
case "hostname": "city": func(d *dataStruct) *string { return d.City },
return data.Hostname "region": func(d *dataStruct) *string { return d.Region },
case "asn": "country": func(d *dataStruct) *string { return d.Country },
return data.ASN "continent": func(d *dataStruct) *string { return d.Continent },
case "org": "timezone": func(d *dataStruct) *string { return d.Timezone },
return data.Org "loc": func(d *dataStruct) *string { return d.Loc },
case "city": }
return data.City
case "region": func getField(data *dataStruct, field string) *string {
return data.Region if f, ok := fieldMap[field]; ok {
case "country": return f(data)
return data.Country }
case "continent": return nil
return data.Continent
case "timezone":
return data.Timezone
case "loc":
return data.Loc
default:
return nil
}
} }