mirror of
https://github.com/anotherhadi/iknowyou.git
synced 2026-04-12 00:47:26 +02:00
init
This commit is contained in:
232
back/internal/tools/whoisfreaks/tool.go
Normal file
232
back/internal/tools/whoisfreaks/tool.go
Normal file
@@ -0,0 +1,232 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user