package whoisfreaks import ( "context" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/anotherhadi/iknowyou/internal/tools" "github.com/tidwall/gjson" ) const ( name = "whoisfreaks" description = "Reverse WHOIS lookup via WhoisFreaks — find all domains registered by an email, owner name, or keyword across 3.6B+ WHOIS records." link = "https://whoisfreaks.com" icon = "" ) type Config struct { APIKey string `yaml:"api_key" iky:"desc=WhoisFreaks API key (required — free account at whoisfreaks.com);required=true"` } 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.InputTypeEmail, tools.InputTypeName, tools.InputTypeDomain, } } func (r *Runner) ConfigPtr() interface{} { return &r.cfg } func (r *Runner) ConfigFields() []tools.ConfigField { return tools.ReflectConfigFields(r.cfg) } var skipKeys = map[string]bool{ "num": true, "status": true, "query_time": true, "update_date": true, "iana_id": true, "whois_server": true, "handle": true, "zip_code": true, "country_code": true, "mailing_address": true, "phone_number": true, "administrative_contact": true, "technical_contact": true, } func prettyResult(r gjson.Result, depth int) string { indent := strings.Repeat(" ", depth) var sb strings.Builder r.ForEach(func(key, val gjson.Result) bool { k := key.String() if skipKeys[k] { return true } switch val.Type { case gjson.JSON: if val.IsArray() { arr := val.Array() if len(arr) == 0 { return true } sb.WriteString(fmt.Sprintf("%s%s:\n", indent, k)) for _, item := range arr { if item.Type == gjson.JSON { sb.WriteString(fmt.Sprintf("%s -\n", indent)) sb.WriteString(prettyResult(item, depth+2)) } else { sb.WriteString(fmt.Sprintf("%s - %s\n", indent, item.String())) } } } else { sb.WriteString(fmt.Sprintf("%s%s:\n", indent, k)) sb.WriteString(prettyResult(val, depth+1)) } default: v := val.String() if v == "" { return true } sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, k, v)) } return true }) return sb.String() } func doRequest(ctx context.Context, req *http.Request) ([]byte, *http.Response, error) { for { resp, err := http.DefaultClient.Do(req) if err != nil { return nil, nil, err } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { return nil, resp, err } if resp.StatusCode != http.StatusTooManyRequests { return body, resp, nil } select { case <-ctx.Done(): return nil, resp, ctx.Err() case <-time.After(60 * time.Second): } // Rebuild the request since the body was consumed req2, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), nil) if err != nil { return nil, resp, err } req2.Header = req.Header req = req2 } } func (r *Runner) Run(ctx context.Context, target string, inputType tools.InputType, out chan<- tools.Event) error { defer close(out) params := url.Values{} params.Set("whois", "reverse") params.Set("apiKey", r.cfg.APIKey) switch inputType { case tools.InputTypeEmail: params.Set("email", target) case tools.InputTypeName: params.Set("owner", target) case tools.InputTypeDomain: params.Set("keyword", target) default: params.Set("keyword", target) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.whoisfreaks.com/v1.0/whois?"+params.Encode(), 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") body, resp, err := doRequest(ctx, 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 } if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "invalid or exhausted API key"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if resp.StatusCode == http.StatusNotFound { out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } if resp.StatusCode != http.StatusOK { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: fmt.Sprintf("API error %d: %s", resp.StatusCode, string(body))} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } j := gjson.ParseBytes(body) if !j.Get("whois_domains_historical").Exists() { out <- tools.Event{Tool: name, Type: tools.EventTypeError, Payload: "unexpected response format"} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } domains := j.Get("whois_domains_historical").Array() if len(domains) == 0 { out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: 0} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil } total := j.Get("total_Result").Int() totalPages := j.Get("total_Pages").Int() currentPage := j.Get("current_Page").Int() var sb strings.Builder sb.WriteString(fmt.Sprintf("Found %d domain(s)", total)) if totalPages > 1 { sb.WriteString(fmt.Sprintf(" across %d pages (showing page %d)", totalPages, currentPage)) } sb.WriteString("\n\n") for _, d := range domains { sb.WriteString(prettyResult(d, 0)) sb.WriteString("\n") } out <- tools.Event{Tool: name, Type: tools.EventTypeOutput, Payload: strings.TrimSpace(sb.String())} out <- tools.Event{Tool: name, Type: tools.EventTypeCount, Payload: int(total)} out <- tools.Event{Tool: name, Type: tools.EventTypeDone} return nil }