Skip to content

Commit

Permalink
add ipapi provider.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonhadfield committed May 6, 2024
1 parent 0cbdb28 commit a921ff0
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 10 deletions.
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ func initConfig(cmd *cobra.Command) error {
sess.Providers.Shodan.Enabled = v.GetBool("providers.shodan.enabled")
sess.Providers.PTR.Enabled = v.GetBool("providers.ptr.enabled")
sess.Providers.PTR.Nameservers = v.GetStringSlice("providers.ptr.nameservers")
sess.Providers.IPAPI.Enabled = v.GetBool("providers.ipapi.enabled")
sess.Config.Global.Ports = v.GetStringSlice("global.ports")
sess.Config.Global.MaxValueChars = v.GetInt32("global.max_value_chars")
sess.Config.Global.MaxAge = v.GetString("global.max_age")
Expand Down
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ require (

//replace github.com/jonhadfield/ip-fetcher => ../ip-fetcher

//replace github.com/likexian/whois-parser => ../whois-parser

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jedib0t/go-pretty/v6 v6.5.8 h1:8BCzJdSvUbaDuRba4YVh+SKMGcAAKdkcF3SVFbrHAtQ=
github.com/jedib0t/go-pretty/v6 v6.5.8/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU=
github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/jonhadfield/ip-fetcher v0.0.0-20240503180217-558f6741c36f h1:HxE19TTjhpHylMfgeoply4qrztj/seduEAO+Q/pQcJA=
github.com/jonhadfield/ip-fetcher v0.0.0-20240503180217-558f6741c36f/go.mod h1:xckxqrzpzUyfNGSRKc+Ugp47f5iwogDYQB4iUUxfXHg=
github.com/jonhadfield/ip-fetcher v0.0.0-20240506070010-1122a63c5fee h1:ITMrFZihhr2Iodo8XFBAhLq/LW841acLNIZli6doaTI=
github.com/jonhadfield/ip-fetcher v0.0.0-20240506070010-1122a63c5fee/go.mod h1:xckxqrzpzUyfNGSRKc+Ugp47f5iwogDYQB4iUUxfXHg=
github.com/jszwec/csvutil v1.10.0 h1:upMDUxhQKqZ5ZDCs/wy+8Kib8rZR8I8lOR34yJkdqhI=
Expand Down
2 changes: 2 additions & 0 deletions process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package process
import (
"encoding/json"
"fmt"
"github.com/jonhadfield/ipscout/providers/ipapi"
"log/slog"
"os"
"path/filepath"
Expand Down Expand Up @@ -53,6 +54,7 @@ func getProviderClients(sess session.Session) (map[string]providers.ProviderClie
{Name: criminalip.ProviderName, Enabled: sess.Providers.CriminalIP.Enabled, APIKey: sess.Providers.CriminalIP.APIKey, NewClient: criminalip.NewProviderClient},
{Name: digitalocean.ProviderName, Enabled: sess.Providers.DigitalOcean.Enabled, APIKey: "", NewClient: digitalocean.NewProviderClient},
{Name: gcp.ProviderName, Enabled: sess.Providers.GCP.Enabled, APIKey: "", NewClient: gcp.NewProviderClient},
{Name: ipapi.ProviderName, Enabled: sess.Providers.IPAPI.Enabled, APIKey: "", NewClient: ipapi.NewProviderClient},
{Name: ipurl.ProviderName, Enabled: sess.Providers.IPURL.Enabled, APIKey: "", NewClient: ipurl.NewProviderClient},
{Name: icloudpr.ProviderName, Enabled: sess.Providers.ICloudPR.Enabled, APIKey: "", NewClient: icloudpr.NewProviderClient},
{Name: linode.ProviderName, Enabled: sess.Providers.Linode.Enabled, APIKey: "", NewClient: linode.NewProviderClient},
Expand Down
5 changes: 2 additions & 3 deletions providers/criminalip/criminalip.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,7 @@ func (c *Client) FindHost() ([]byte, error) {
return result.Raw, nil
}

type GeneratePortDataForTableInput struct {
}
type GeneratePortDataForTableInput struct{}

type GeneratePortDataForTableOutput struct {
entries []WrappedPortDataEntry
Expand Down Expand Up @@ -645,7 +644,7 @@ type HostSearchResult struct {
OrgCountryCode string `json:"org_country_code"`
ConfirmedTime string `json:"confirmed_time"`
} `json:"data"`
} `json:"whois"`
} `json:"ipapi"`
Hostname struct {
Count int `json:"count"`
Data []struct {
Expand Down
299 changes: 299 additions & 0 deletions providers/ipapi/ipapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
package ipapi

import (
"encoding/json"
"errors"
"fmt"
"github.com/hashicorp/go-retryablehttp"
"log/slog"
"net/netip"
"os"
"strings"
"time"

"github.com/jonhadfield/ipscout/providers"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/jonhadfield/ipscout/cache"
"github.com/jonhadfield/ipscout/session"
)

const (
ProviderName = "ipapi"
MaxColumnWidth = 120
IndentPipeHyphens = " |-----"
portLastModifiedFormat = "2006-01-02T15:04:05+07:00"
ResultTTL = 1 * time.Hour
apiDomain = "https://ipapi.co"
)

type Client struct {
session.Session
}

type Config struct {
_ struct{}
session.Session
Host netip.Addr
APIKey string
}

func NewProviderClient(c session.Session) (providers.ProviderClient, error) {
c.Logger.Debug("creating ipapi client")

tc := Client{
c,
}

return &tc, nil
}

type Provider interface {
LoadData() ([]byte, error)
CreateTable([]byte) (*table.Writer, error)
}

func (c *Client) Enabled() bool {
return c.Session.Providers.IPAPI.Enabled
}

func (c *Client) GetConfig() *session.Session {
return &c.Session
}

func (c *Client) Initialise() error {
if c.Session.Cache == nil {
return errors.New("cache not set")
}

start := time.Now()
defer func() {
c.Session.Stats.Mu.Lock()
c.Session.Stats.InitialiseDuration[ProviderName] = time.Since(start)
c.Session.Stats.Mu.Unlock()
}()

c.Session.Logger.Debug("initialising ipapi client")

return nil
}

func (c *Client) FindHost() ([]byte, error) {
start := time.Now()
defer func() {
c.Session.Stats.Mu.Lock()
c.Session.Stats.FindHostDuration[ProviderName] = time.Since(start)
c.Session.Stats.Mu.Unlock()
}()

result, err := fetchData(c.Session)
if err != nil {
return nil, err
}

c.Session.Logger.Debug("ipapi host match data", "size", len(result.Raw))

return result.Raw, nil
}

func (c *Client) CreateTable(data []byte) (*table.Writer, error) {
start := time.Now()
defer func() {
c.Session.Stats.Mu.Lock()
c.Session.Stats.CreateTableDuration[ProviderName] = time.Since(start)
c.Session.Stats.Mu.Unlock()
}()

var findHostData HostSearchResult
if err := json.Unmarshal(data, &findHostData); err != nil {
return nil, fmt.Errorf("error unmarshalling ipapi data: %w", err)
}

tw := table.NewWriter()

tw.AppendRow(table.Row{"Organisation", providers.DashIfEmpty(findHostData.Org)})
tw.AppendRow(table.Row{"Hostname", providers.DashIfEmpty(findHostData.Hostname)})
tw.AppendRow(table.Row{"Country", providers.DashIfEmpty(findHostData.CountryName)})
tw.AppendRow(table.Row{"Region", providers.DashIfEmpty(findHostData.Region)})
tw.AppendRow(table.Row{"City", providers.DashIfEmpty(findHostData.City)})
tw.AppendRow(table.Row{"Postal", providers.DashIfEmpty(findHostData.Postal)})
tw.AppendRow(table.Row{"ASN", providers.DashIfEmpty(findHostData.Asn)})

tw.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, AutoMerge: false, WidthMax: MaxColumnWidth, WidthMin: 50},
{Number: 1, AutoMerge: true},
})

tw.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, AutoMerge: true, WidthMax: MaxColumnWidth, WidthMin: 50},
})
tw.SetAutoIndex(false)
// tw.SetStyle(table.StyleColoredDark)
// tw.Style().Options.DrawBorder = true
tw.SetTitle("IPAPI | Host: %s", c.Session.Host.String())

if c.UseTestData {
tw.SetTitle("IPAPI | Host: 8.8.4.4")
}

c.Session.Logger.Debug("ipapi table created", "host", c.Session.Host.String())

return &tw, nil
}

type ipapiResp struct {
IP string `json:"ip"`
Version string `json:"version"`
City string `json:"city"`
Region string `json:"region"`
RegionCode string `json:"region_code"`
CountryCode string `json:"country_code"`
CountryCodeIso3 string `json:"country_code_iso3"`
CountryName string `json:"country_name"`
CountryCapital string `json:"country_capital"`
CountryTld string `json:"country_tld"`
ContinentCode string `json:"continent_code"`
InEu bool `json:"in_eu"`
Postal string `json:"postal"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Timezone string `json:"timezone"`
UtcOffset string `json:"utc_offset"`
CountryCallingCode string `json:"country_calling_code"`
Currency string `json:"currency"`
CurrencyName string `json:"currency_name"`
Languages string `json:"languages"`
CountryArea float64 `json:"country_area"`
CountryPopulation int `json:"country_population"`
Asn string `json:"asn"`
Org string `json:"org"`
Hostname string `json:"hostname"`
}

func loadResponse(c session.Session) (res *HostSearchResult, err error) {
res = &HostSearchResult{}

req, err := retryablehttp.NewRequest("GET", fmt.Sprintf("%s/%s/json", apiDomain, c.Host.String()), nil)
if err != nil {
return nil, fmt.Errorf("error creating ipapi request: %w", err)
}

req.Header.Set("User-Agent", providers.DefaultUA)

resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error sending ipapi request: %w", err)
}

defer resp.Body.Close()

var apiResp ipapiResp

if err = json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, fmt.Errorf("error decoding ipapi response: %w", err)
}

raw, err := json.Marshal(apiResp)
if err != nil {
return nil, fmt.Errorf("error marshalling ipapi response: %w", err)
}

res.Raw = raw

return res, nil
}

func loadResultsFile(path string) (res *HostSearchResult, err error) {
jf, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("error opening ipapi file: %w", err)
}

defer jf.Close()

decoder := json.NewDecoder(jf)

err = decoder.Decode(&res)
if err != nil {
return res, fmt.Errorf("error decoding ipapi file: %w", err)
}

return res, nil
}

func loadTestData(l *slog.Logger) (*HostSearchResult, error) {
tdf, err := loadResultsFile("providers/ipapi/testdata/ipapi_8_8_4_4_report.json")
if err != nil {
return nil, err
}

raw, err := json.Marshal(tdf)
if err != nil {
return nil, fmt.Errorf("error marshalling ipapi test data: %w", err)
}

tdf.Raw = raw

l.Info("ipapi match returned from test data", "host", "8.8.4.4")

return tdf, nil
}

func fetchData(c session.Session) (*HostSearchResult, error) {
var result *HostSearchResult

var err error

if c.UseTestData {
result, err = loadTestData(c.Logger)
if err != nil {
return nil, fmt.Errorf("error loading ipapi test data: %w", err)
}

return result, nil
}

// load data from cache
cacheKey := fmt.Sprintf("ipapi_%s_report.json", strings.ReplaceAll(c.Host.String(), ".", "_"))

var item *cache.Item
if item, err = cache.Read(c.Logger, c.Cache, cacheKey); err == nil {
if item.Value != nil && len(item.Value) > 0 {
err = json.Unmarshal(item.Value, &result)
if err != nil {
return nil, fmt.Errorf("error unmarshalling cached ipapi response: %w", err)
}

c.Logger.Info("ipapi response found in cache", "host", c.Host.String())

result.Raw = item.Value

c.Stats.Mu.Lock()
c.Stats.FindHostUsedCache[ProviderName] = true
c.Stats.Mu.Unlock()

return result, nil
}
}

result, err = loadResponse(c)
if err != nil {
return nil, fmt.Errorf("loading ipapi api response: %w", err)
}

if err = cache.UpsertWithTTL(c.Logger, c.Cache, cache.Item{
AppVersion: c.App.Version,
Key: cacheKey,
Value: result.Raw,
Created: time.Now(),
}, ResultTTL); err != nil {
return nil, fmt.Errorf("error caching ipapi response: %w", err)
}

return result, nil
}

type HostSearchResult struct {
Raw json.RawMessage `json:"raw,omitempty"`
ipapiResp
}
1 change: 1 addition & 0 deletions providers/ipapi/testdata/ipapi_8_8_4_4_report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"ip":"8.8.4.4","version":"IPv4","city":"Mountain View","region":"California","region_code":"CA","country_code":"US","country_code_iso3":"USA","country_name":"United States","country_capital":"Washington","country_tld":".us","continent_code":"NA","in_eu":false,"postal":"94043","latitude":37.4043,"longitude":-122.0748,"timezone":"America/Los_Angeles","utc_offset":"-0700","country_calling_code":"+1","currency":"USD","currency_name":"Dollar","languages":"en-US,es-US,haw,fr","country_area":9629091,"country_population":327167434,"asn":"AS15169","org":"GOOGLE","hostname":""}
2 changes: 2 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/jonhadfield/ipscout/session"
)

const DefaultUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:125.0) Gecko/20100101 Firefox/125."

var (
ErrFailedToFetchData = errors.New("failed to fetch data")
ErrNoDataFound = errors.New("no data found")
Expand Down
4 changes: 3 additions & 1 deletion session/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ providers:
# url: override the default URL
gcp:
enabled: true
ipapi:
enabled: true
ipurl:
enabled: true
urls:
Expand All @@ -47,4 +49,4 @@ providers:
- 9.9.9.9
shodan:
enabled: false
max_ports: 10
max_ports: 10
Loading

0 comments on commit a921ff0

Please sign in to comment.