Files
iknowyou/back/internal/tools/ipinfo/tool.go
2026-04-06 15:12:34 +02:00

134 lines
3.8 KiB
Go

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
}