diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index aea27d1..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: ci
-
-on:
- push:
- branches: [master]
- pull_request:
- branches: [master]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- env:
- DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
- DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- steps:
- - uses: actions/checkout@v2
- - name: install go
- uses: actions/setup-go@v2
- with:
- go-version: 1.16
- - name: build and test
- run: make
- - name: enable experimental docker features
- if: ${{ github.ref == 'refs/heads/master' }}
- run: |
- echo '{"experimental":true}' | sudo tee /etc/docker/daemon.json
- sudo service docker restart
- - name: publish multi-arch docker image
- if: ${{ github.ref == 'refs/heads/master' }}
- run: make docker-pushx
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..27c4e28
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,22 @@
+name: Go
+
+on: [push]
+
+jobs:
+ test:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: "1.13.x"
+
+ - name: Display Go version
+ run: go version
+
+ - name: Run tests
+ run: go test ./...
diff --git a/.gitignore b/.gitignore
index ac30f00..56d06b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
/custom.html
/vendor/
.vscode/
+.idea/
/bin/
diff --git a/cmd/echoip/main.go b/cmd/echoip/main.go
index 7c0a668..b47e74c 100644
--- a/cmd/echoip/main.go
+++ b/cmd/echoip/main.go
@@ -7,11 +7,11 @@ import (
"os"
- "github.com/mpolden/echoip/http"
- "github.com/mpolden/echoip/iputil"
- "github.com/mpolden/echoip/iputil/geo"
- "github.com/mpolden/echoip/iputil/ipstack"
- parser "github.com/mpolden/echoip/iputil/paser"
+ "github.com/levelsoftware/echoip/http"
+ "github.com/levelsoftware/echoip/iputil"
+ "github.com/levelsoftware/echoip/iputil/geo"
+ "github.com/levelsoftware/echoip/iputil/ipstack"
+ parser "github.com/levelsoftware/echoip/iputil/paser"
ipstackApi "github.com/qioalice/ipstack"
)
diff --git a/go.mod b/go.mod
index 55d4af8..38293f9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,9 @@
-module github.com/mpolden/echoip
+module github.com/levelsoftware/echoip
go 1.13
require (
github.com/oschwald/geoip2-golang v1.5.0
- github.com/qioalice/ipstack v1.0.1 // indirect
+ github.com/qioalice/ipstack v1.0.1
golang.org/x/sys v0.0.0-20210223212115-eede4237b368 // indirect
)
diff --git a/go.sum b/go.sum
index e79f682..8bfbc1b 100644
--- a/go.sum
+++ b/go.sum
@@ -12,7 +12,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210223212115-eede4237b368 h1:fDE3p0qf2V1co1vfj3/o87Ps8Hq6QTGNxJ5Xe7xSp80=
golang.org/x/sys v0.0.0-20210223212115-eede4237b368/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/html/index.html b/html/index.html
index fd75ead..23fba4c 100644
--- a/html/index.html
+++ b/html/index.html
@@ -113,10 +113,9 @@
Timezone |
{{ .Timezone }} |
- {{ end }} {{ if .IsDayLightSavings }}
Is Daylight Savings? |
- {{ .IsDayLightSavings }} |
+ {{ .TimezoneEtc.IsDaylightSavings }} |
{{ end }} {{ if .ASN }}
@@ -153,35 +152,35 @@
{{ if .IPStackSecurityEnabled }}
Is Proxy? |
- {{ .IsProxy }} |
+ {{ .Security.IsProxy }} |
Is Crawler? |
- {{ .IsCrawler }} |
+ {{ .Security.IsCrawler }} |
- {{ if .IsCrawler }}
+ {{ if .Security.IsCrawler }}
Crawler Name |
- {{ .CrawlerName }} |
+ {{ .Security.CrawlerName }} |
Crawler Type |
- {{ .CrawlerType }} |
+ {{ .Security.CrawlerType }} |
{{ end }}
Is Tor? |
- {{ .IsTor }} |
+ {{ .Security.IsTor }} |
Threat Level |
- {{ .ThreatLevel }} |
+ {{ .Security.ThreatLevel }} |
Threat Types |
- {{ .ThreatTypes }} |
+ {{ .Security.ThreatTypes }} |
{{ end }}
diff --git a/http/cache.go b/http/cache.go
index 2882fc9..99fcc79 100644
--- a/http/cache.go
+++ b/http/cache.go
@@ -7,7 +7,7 @@ import (
"net"
"sync"
- parser "github.com/mpolden/echoip/iputil/paser"
+ parser "github.com/levelsoftware/echoip/iputil/paser"
)
type Cache struct {
diff --git a/http/cache_test.go b/http/cache_test.go
index c33aff9..76bbc1b 100644
--- a/http/cache_test.go
+++ b/http/cache_test.go
@@ -5,7 +5,7 @@ import (
"net"
"testing"
- parser "github.com/mpolden/echoip/iputil/paser"
+ parser "github.com/levelsoftware/echoip/iputil/paser"
)
func TestCacheCapacity(t *testing.T) {
diff --git a/http/http.go b/http/http.go
index 9693d2b..39fe1c5 100644
--- a/http/http.go
+++ b/http/http.go
@@ -11,8 +11,8 @@ import (
"net/http/pprof"
- parser "github.com/mpolden/echoip/iputil/paser"
- "github.com/mpolden/echoip/useragent"
+ parser "github.com/levelsoftware/echoip/iputil/paser"
+ "github.com/levelsoftware/echoip/useragent"
"net"
"net/http"
diff --git a/http/http_test.go b/http/http_test.go
index b621645..f6c54e1 100644
--- a/http/http_test.go
+++ b/http/http_test.go
@@ -11,9 +11,9 @@ import (
"strings"
"testing"
- "github.com/mpolden/echoip/iputil"
- "github.com/mpolden/echoip/iputil/geo"
- parser "github.com/mpolden/echoip/iputil/paser"
+ "github.com/levelsoftware/echoip/iputil"
+ "github.com/levelsoftware/echoip/iputil/geo"
+ parser "github.com/levelsoftware/echoip/iputil/paser"
)
func lookupAddr(net.IP) (string, error) { return "localhost", nil }
@@ -163,7 +163,7 @@ func TestDisabledHandlers(t *testing.T) {
{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", "{\n \"UsingGeoIP\": true,\n \"UsingIPStack\": false,\n \"IPStackSecurityEnabled\": false,\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433\n}", 200},
+ {s.URL + "/json", "{\n \"UsingGeoIP\": true,\n \"UsingIPStack\": false,\n \"IPStackSecurityEnabled\": false,\n \"timezone_etc\": {},\n \"security\": {\n \"is_proxy\": false,\n \"is_crawler\": false,\n \"is_tor\": false\n },\n \"currency\": {},\n \"location\": {\n \"country_flag\": {}\n },\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433\n}", 200},
}
for _, tt := range tests {
@@ -189,7 +189,7 @@ func TestJSONHandlers(t *testing.T) {
out string
status int
}{
- {s.URL, "{\n \"UsingGeoIP\": true,\n \"UsingIPStack\": false,\n \"IPStackSecurityEnabled\": false,\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433,\n \"country\": \"Elbonia\",\n \"country_iso\": \"EB\",\n \"country_eu\": false,\n \"region_name\": \"North Elbonia\",\n \"region_code\": \"1234\",\n \"metro_code\": 1234,\n \"zip_code\": \"1234\",\n \"city\": \"Bornyasherk\",\n \"latitude\": 63.416667,\n \"longitude\": 10.416667,\n \"time_zone\": \"Europe/Bornyasherk\",\n \"asn\": \"AS59795\",\n \"asn_org\": \"Hosting4Real\",\n \"hostname\": \"localhost\",\n \"user_agent\": {\n \"product\": \"curl\",\n \"version\": \"7.2.6.0\",\n \"raw_value\": \"curl/7.2.6.0\"\n }\n}", 200},
+ {s.URL, "{\n \"UsingGeoIP\": true,\n \"UsingIPStack\": false,\n \"IPStackSecurityEnabled\": false,\n \"timezone_etc\": {},\n \"security\": {\n \"is_proxy\": false,\n \"is_crawler\": false,\n \"is_tor\": false\n },\n \"currency\": {},\n \"location\": {\n \"country_flag\": {}\n },\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433,\n \"country\": \"Elbonia\",\n \"country_iso\": \"EB\",\n \"country_eu\": false,\n \"region_name\": \"North Elbonia\",\n \"region_code\": \"1234\",\n \"metro_code\": 1234,\n \"zip_code\": \"1234\",\n \"city\": \"Bornyasherk\",\n \"latitude\": 63.416667,\n \"longitude\": 10.416667,\n \"timezone\": \"Europe/Bornyasherk\",\n \"asn\": \"AS59795\",\n \"asn_org\": \"Hosting4Real\",\n \"hostname\": \"localhost\",\n \"user_agent\": {\n \"product\": \"curl\",\n \"version\": \"7.2.6.0\",\n \"raw_value\": \"curl/7.2.6.0\"\n }\n}", 200},
{s.URL + "/port/foo", "{\n \"status\": 400,\n \"error\": \"invalid port: foo\"\n}", 400},
{s.URL + "/port/0", "{\n \"status\": 400,\n \"error\": \"invalid port: 0\"\n}", 400},
{s.URL + "/port/65537", "{\n \"status\": 400,\n \"error\": \"invalid port: 65537\"\n}", 400},
diff --git a/iputil/geo/geo.go b/iputil/geo/geo.go
index fdacf2c..d92f36b 100644
--- a/iputil/geo/geo.go
+++ b/iputil/geo/geo.go
@@ -5,8 +5,8 @@ import (
"math"
"net"
- "github.com/mpolden/echoip/iputil"
- parser "github.com/mpolden/echoip/iputil/paser"
+ "github.com/levelsoftware/echoip/iputil"
+ parser "github.com/levelsoftware/echoip/iputil/paser"
geoip2 "github.com/oschwald/geoip2-golang"
)
@@ -84,22 +84,24 @@ func (g *geoip) Parse(ip net.IP, hostname string) (parser.Response, error) {
UsingGeoIP: true,
UsingIPStack: false,
IPStackSecurityEnabled: false,
- IP: ip,
- IPDecimal: ipDecimal,
- Country: country.Name,
- CountryISO: country.ISO,
- CountryEU: country.IsEU,
- RegionName: city.RegionName,
- RegionCode: city.RegionCode,
- MetroCode: city.MetroCode,
- PostalCode: city.PostalCode,
- City: city.Name,
- Latitude: city.Latitude,
- Longitude: city.Longitude,
- Timezone: city.Timezone,
- ASN: autonomousSystemNumber,
- ASNOrg: asn.AutonomousSystemOrganization,
- Hostname: hostname,
+
+ /* kept for backward compatibility */
+ IP: ip,
+ IPDecimal: ipDecimal,
+ Country: country.Name,
+ CountryISO: country.ISO,
+ CountryEU: country.IsEU,
+ RegionName: city.RegionName,
+ RegionCode: city.RegionCode,
+ MetroCode: city.MetroCode,
+ PostalCode: city.PostalCode,
+ City: city.Name,
+ Latitude: city.Latitude,
+ Longitude: city.Longitude,
+ Timezone: city.Timezone,
+ ASN: autonomousSystemNumber,
+ ASNOrg: asn.AutonomousSystemOrganization,
+ Hostname: hostname,
}, nil
}
diff --git a/iputil/ipstack/ipstack.go b/iputil/ipstack/ipstack.go
index 6ec3be5..6f0fe09 100644
--- a/iputil/ipstack/ipstack.go
+++ b/iputil/ipstack/ipstack.go
@@ -3,10 +3,11 @@ package ipstack
import (
"fmt"
"net"
- "reflect"
+ "time"
+
+ "github.com/levelsoftware/echoip/iputil"
+ parser "github.com/levelsoftware/echoip/iputil/paser"
- "github.com/mpolden/echoip/iputil"
- parser "github.com/mpolden/echoip/iputil/paser"
"github.com/qioalice/ipstack"
)
@@ -17,6 +18,7 @@ type IPStack struct {
func (ips *IPStack) Parse(ip net.IP, hostname string) (parser.Response, error) {
res, err := ipstack.IP(ip.String())
ips.response = res
+
if err != nil {
return parser.Response{}, err
}
@@ -26,52 +28,91 @@ func (ips *IPStack) Parse(ip net.IP, hostname string) (parser.Response, error) {
parserResponse := parser.Response{
UsingGeoIP: false,
UsingIPStack: true,
- Latitude: float64(res.Latitide),
- Longitude: float64(res.Longitude),
- Hostname: hostname,
- IP: ip,
- IPDecimal: ipDecimal,
- Country: res.CountryName,
- CountryISO: res.CountryCode,
- RegionName: res.RegionName,
- RegionCode: res.RegionCode,
- MetroCode: 0,
- PostalCode: res.Zip,
- City: res.City,
+
+ /* kept for backward compatibility */
+ Latitude: float64(res.Latitide),
+ Longitude: float64(res.Longitude),
+ Hostname: hostname,
+ IP: ip,
+ IPDecimal: ipDecimal,
+ Country: res.CountryName,
+ CountryISO: res.CountryCode,
+ RegionName: res.RegionName,
+ RegionCode: res.RegionCode,
+ MetroCode: 0,
+ PostalCode: res.Zip,
+ City: res.City,
}
- if res.Timezone != nil {
- parserResponse.Timezone = res.Timezone.ID
- parserResponse.IsDayLightSavings = res.Timezone.IsDaylightSaving
- }
-
- if res.Security != nil {
- parserResponse.IPStackSecurityEnabled = true
- parserResponse.IsProxy = res.Security.IsProxy
- parserResponse.IsCrawler = res.Security.IsCrawler
- parserResponse.CrawlerName = res.Security.CrawlerName
- parserResponse.CrawlerType = res.Security.CrawlerType
- parserResponse.IsTor = res.Security.IsTOR
- parserResponse.ThreatLevel = res.Security.ThreatLevel
-
- if !reflect.ValueOf(&res.Security.ThreatTypes).IsNil() {
- parserResponse.ThreatTypes = &res.Security.ThreatTypes
- }
- }
-
- if res.Location != nil {
- parserResponse.CountryEU = &res.Location.IsEU
- }
-
- if res.Connection != nil {
- if res.Connection.ASN > 0 {
- parserResponse.ASN = fmt.Sprintf("AS%d", res.Connection.ASN)
- }
- }
+ ips.ParseSecurityResponse(&parserResponse)
+ ips.ParseTimezoneResponse(&parserResponse)
+ ips.ParseLocationResponse(&parserResponse)
+ ips.ParseConnectionResponse(&parserResponse)
return parserResponse, nil
}
+func (ips *IPStack) ParseSecurityResponse(parserResponse *parser.Response) {
+ if ips.response.Security != nil {
+ parserResponse.IPStackSecurityEnabled = true
+
+ parserResponse.Security = parser.Security{
+ IsProxy: ips.response.Security.IsProxy,
+ IsTor: ips.response.Security.IsTOR,
+ CrawlerName: ips.response.Security.CrawlerName,
+ CrawlerType: ips.response.Security.CrawlerType,
+ ThreatLevel: ips.response.Security.ThreatLevel,
+ ThreatTypes: ips.response.Security.ThreatTypes.([]string),
+ }
+ }
+}
+
+func (ips *IPStack) ParseTimezoneResponse(parserResponse *parser.Response) {
+ if ips.response.Timezone != nil {
+ parserResponse.TimezoneEtc = parser.Timezone{
+ ID: ips.response.Timezone.ID,
+ CurrentTime: ips.response.Timezone.CurrentTime.Format(time.RFC3339),
+ GmtOffset: ips.response.Timezone.GMTOffset,
+ Code: ips.response.Timezone.Code,
+ IsDaylightSavings: ips.response.Timezone.IsDaylightSaving,
+ }
+
+ /* kept for backward compatibility */
+ parserResponse.Timezone = ips.response.Timezone.ID
+ }
+}
+
+func (ips *IPStack) ParseLocationResponse(parserResponse *parser.Response) {
+ if ips.response.Location != nil {
+ var languages []parser.Language
+ for i := 0; i < len(ips.response.Location.Languages); i++ {
+ languages = append(languages, parser.Language{
+ Code: ips.response.Location.Languages[i].Code,
+ Name: ips.response.Location.Languages[i].Name,
+ Native: ips.response.Location.Languages[i].NativeName,
+ })
+ }
+ parserResponse.Location = parser.Location{
+ Languages: languages,
+ CountryFlag: parser.CountryFlag{
+ Flag: ips.response.Location.CountryFlagLink,
+ Emoji: ips.response.Location.CountryFlagEmoji,
+ EmojiUnicode: ips.response.Location.CountryFlagEmojiUnicode,
+ },
+ }
+
+ /* kept for backward compatibility */
+ parserResponse.CountryEU = &ips.response.Location.IsEU
+ }
+}
+
+func (ips *IPStack) ParseConnectionResponse(parserResponse *parser.Response) {
+ if ips.response.Connection != nil && ips.response.Connection.ASN > 0 {
+ /* kept for backward compatibility */
+ parserResponse.ASN = fmt.Sprintf("AS%d", ips.response.Connection.ASN)
+ }
+}
+
func (ips *IPStack) IsEmpty() bool {
return false
}
diff --git a/iputil/paser/parser.go b/iputil/paser/parser.go
index 3a9635d..bef9948 100644
--- a/iputil/paser/parser.go
+++ b/iputil/paser/parser.go
@@ -4,7 +4,7 @@ import (
"math/big"
"net"
- "github.com/mpolden/echoip/useragent"
+ "github.com/levelsoftware/echoip/useragent"
)
type Parser interface {
@@ -12,34 +12,75 @@ type Parser interface {
IsEmpty() bool
}
-type Response struct {
- UsingGeoIP bool `json:"UsingGeoIP"`
- UsingIPStack bool `json:"UsingIPStack"`
- IPStackSecurityEnabled bool `json:"IPStackSecurityEnabled"`
- IP net.IP `json:"ip"`
- IPDecimal *big.Int `json:"ip_decimal"`
- Country string `json:"country,omitempty"`
- CountryISO string `json:"country_iso,omitempty"`
- CountryEU *bool `json:"country_eu,omitempty"`
- RegionName string `json:"region_name,omitempty"`
- RegionCode string `json:"region_code,omitempty"`
- MetroCode uint `json:"metro_code,omitempty"`
- PostalCode string `json:"zip_code,omitempty"`
- City string `json:"city,omitempty"`
- Latitude float64 `json:"latitude,omitempty"`
- Longitude float64 `json:"longitude,omitempty"`
- Timezone string `json:"time_zone,omitempty"`
- IsDayLightSavings bool `json:"is_daylight_savings,omitempty"`
- ASN string `json:"asn,omitempty"`
- ASNOrg string `json:"asn_org,omitempty"`
- Hostname string `json:"hostname,omitempty"`
- UserAgent *useragent.UserAgent `json:"user_agent,omitempty"`
- CurrencyCode string `json:"currency_code,omitempty"`
- IsProxy bool `json:"is_proxy,omitempty"`
- IsCrawler bool `json:"is_crawler,omitempty"`
- CrawlerName string `json:"crawler_name,omitempty"`
- CrawlerType string `json:"crawler_type,omitempty"`
- IsTor bool `json:"is_tor,omitempty"`
- ThreatLevel string `json:"threat_level,omitempty"`
- ThreatTypes *interface{} `json:"threat_types,omitempty"`
+type Currency struct {
+ Code string `json:"code,omitempty"`
+ Name string `json:"name,omitempty"`
+ Plural string `json:"plural,omitempty"`
+ Symbol string `json:"symbol,omitempty"`
+ SymbolNative string `json:"symbol_native,omitempty"`
+}
+
+type Security struct {
+ IsProxy bool `json:"is_proxy"`
+ IsCrawler bool `json:"is_crawler"`
+ CrawlerName string `json:"crawler_name,omitempty"`
+ CrawlerType string `json:"crawler_type,omitempty"`
+ IsTor bool `json:"is_tor"`
+ ThreatLevel string `json:"threat_level,omitempty"`
+ ThreatTypes []string `json:"threat_types,omitempty"`
+}
+
+type Timezone struct {
+ ID string `json:"id,omitempty"`
+ CurrentTime string `json:"current_time,omitempty"`
+ GmtOffset int `json:"gmt_offset,omitempty"`
+ Code string `json:"code,omitempty"`
+ IsDaylightSavings bool `json:"is_daylight_savings,omitempty"`
+}
+
+type Language struct {
+ Code string `json:"code,omitempty"`
+ Name string `json:"name,omitempty"`
+ Native string `json:"native,omitempty"`
+}
+
+type CountryFlag struct {
+ Flag string `json:"flag,omitempty"`
+ Emoji string `json:"emoji,omitempty"`
+ EmojiUnicode string `json:"emoji_unicode,omitempty"`
+}
+
+type Location struct {
+ Languages interface{} `json:"languages,omitempty"`
+ CountryFlag CountryFlag `json:"country_flag,omitempty"`
+}
+
+type Response struct {
+ UsingGeoIP bool `json:"UsingGeoIP"`
+ UsingIPStack bool `json:"UsingIPStack"`
+ IPStackSecurityEnabled bool `json:"IPStackSecurityEnabled"`
+
+ TimezoneEtc Timezone `json:"timezone_etc,omitempty"`
+ Security Security `json:"security,omitempty"`
+ Currency Currency `json:"currency,omitempty"`
+ Location Location `json:"location,omitempty"`
+
+ /* Kept to prevent breaking changes */
+ IP net.IP `json:"ip"`
+ IPDecimal *big.Int `json:"ip_decimal"`
+ Country string `json:"country,omitempty"`
+ CountryISO string `json:"country_iso,omitempty"`
+ CountryEU *bool `json:"country_eu,omitempty"`
+ RegionName string `json:"region_name,omitempty"`
+ RegionCode string `json:"region_code,omitempty"`
+ MetroCode uint `json:"metro_code,omitempty"`
+ PostalCode string `json:"zip_code,omitempty"`
+ City string `json:"city,omitempty"`
+ Latitude float64 `json:"latitude,omitempty"`
+ Longitude float64 `json:"longitude,omitempty"`
+ Timezone string `json:"timezone,omitempty"`
+ ASN string `json:"asn,omitempty"`
+ ASNOrg string `json:"asn_org,omitempty"`
+ Hostname string `json:"hostname,omitempty"`
+ UserAgent *useragent.UserAgent `json:"user_agent,omitempty"`
}