package ipinfo import ( "context" "encoding/json" "fmt" "net/http" "strings" "github.com/anotherhadi/iknowyou/internal/tools" ) const ( name = "ipinfo" description = "IP geolocation via ipinfo.io — returns city, region, country, coordinates, ASN/org, timezone, and hostname." link = "https://ipinfo.io" icon = "" ) type Config struct { Token string `yaml:"token" iky:"desc=ipinfo.io API token (optional — free tier allows 50k req/month without one);required=false"` } type Runner struct { cfg Config } func New() tools.ToolRunner { cfg := Config{} tools.ApplyDefaults(&cfg) return &Runner{cfg: cfg} } func (r *Runner) Name() string { return name } func (r *Runner) Description() string { return description } func (r *Runner) Link() string { return link } func (r *Runner) Icon() string { return icon } func (r *Runner) InputTypes() []tools.InputType { return []tools.InputType{tools.InputTypeIP} } func (r *Runner) ConfigPtr() interface{} { return &r.cfg } func (r *Runner) ConfigFields() []tools.ConfigField { return tools.ReflectConfigFields(r.cfg) } type ipinfoResponse struct { IP string `json:"ip"` Hostname string `json:"hostname"` City string `json:"city"` Region string `json:"region"` Country string `json:"country"` Loc string `json:"loc"` Org string `json:"org"` Postal string `json:"postal"` Timezone string `json:"timezone"` Bogon bool `json:"bogon"` } func (r *Runner) Run(ctx context.Context, target string, _ tools.InputType, out chan<- tools.Event) error { defer close(out) url := fmt.Sprintf("https://ipinfo.io/%s/json", target) if r.cfg.Token != "" { url += "?token=" + r.cfg.Token } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } req.Header.Set("Accept", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { if ctx.Err() != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "scan cancelled"} } else { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: err.Error()} } out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } defer resp.Body.Close() var info ipinfoResponse if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "failed to parse response"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if info.Bogon { out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: fmt.Sprintf("IP: %s\nType: Bogon/Private address", info.IP)} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 1} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } var sb strings.Builder field := func(label, value string) { if value != "" { sb.WriteString(fmt.Sprintf("%-12s %s\n", label+":", value)) } } field("IP", info.IP) field("Hostname", info.Hostname) field("City", info.City) field("Region", info.Region) field("Country", info.Country) field("Coordinates", info.Loc) field("Postal", info.Postal) field("Timezone", info.Timezone) field("Org/ASN", info.Org) result := strings.TrimSpace(sb.String()) count := 0 if result != "" { out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: result} count = 1 } out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: count} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil }