266 lines
6.9 KiB
Go
266 lines
6.9 KiB
Go
|
|
package whois
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"net"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"b612.me/starnet"
|
||
|
|
netproxy "golang.org/x/net/proxy"
|
||
|
|
)
|
||
|
|
|
||
|
|
// LookupCommonOps are controls shared by RDAP and WHOIS when specific values are absent.
|
||
|
|
type LookupCommonOps struct {
|
||
|
|
Timeout time.Duration
|
||
|
|
Proxy string
|
||
|
|
}
|
||
|
|
|
||
|
|
// LookupRDAPOps are RDAP-only controls, powered by starnet request options.
|
||
|
|
// Proxy here overrides common Proxy for RDAP only.
|
||
|
|
type LookupRDAPOps struct {
|
||
|
|
Timeout time.Duration
|
||
|
|
DialTimeout time.Duration
|
||
|
|
Proxy string
|
||
|
|
SkipTLSVerify *bool
|
||
|
|
CustomIP []string
|
||
|
|
CustomDNS []string
|
||
|
|
Hosts map[string]string
|
||
|
|
LookupFunc func(ctx context.Context, host string) ([]net.IPAddr, error)
|
||
|
|
Headers map[string]string
|
||
|
|
RequestOpts []starnet.RequestOpt
|
||
|
|
}
|
||
|
|
|
||
|
|
// LookupWHOISOps are WHOIS-safe controls.
|
||
|
|
type LookupWHOISOps struct {
|
||
|
|
Timeout time.Duration
|
||
|
|
Proxy string
|
||
|
|
Dialer netproxy.Dialer
|
||
|
|
}
|
||
|
|
|
||
|
|
// LookupOps groups common + protocol-specific controls.
|
||
|
|
type LookupOps struct {
|
||
|
|
Common LookupCommonOps
|
||
|
|
RDAP LookupRDAPOps
|
||
|
|
WHOIS LookupWHOISOps
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupOps sets all lookup ops.
|
||
|
|
func WithLookupOps(ops LookupOps) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops = cloneLookupOps(ops)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupCommonOps sets shared lookup ops.
|
||
|
|
func WithLookupCommonOps(ops LookupCommonOps) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.Common = ops
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupRDAPOps sets RDAP-only lookup ops.
|
||
|
|
func WithLookupRDAPOps(ops LookupRDAPOps) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.RDAP = cloneLookupRDAPOps(ops)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupWHOISOps sets WHOIS-only lookup ops.
|
||
|
|
func WithLookupWHOISOps(ops LookupWHOISOps) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.WHOIS = ops
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupCommonTimeout sets shared timeout.
|
||
|
|
func WithLookupCommonTimeout(timeout time.Duration) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.Common.Timeout = timeout
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupProxy sets common proxy for RDAP + WHOIS (WHOIS expects socks5 proxy).
|
||
|
|
func WithLookupProxy(proxy string) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.Common.Proxy = strings.TrimSpace(proxy)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupRDAPProxy sets RDAP-only proxy, overriding common proxy for RDAP.
|
||
|
|
func WithLookupRDAPProxy(proxy string) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.RDAP.Proxy = strings.TrimSpace(proxy)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupWHOISDialer sets WHOIS TCP dialer.
|
||
|
|
func WithLookupWHOISDialer(d netproxy.Dialer) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.WHOIS.Dialer = d
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupWHOISProxy sets WHOIS-only proxy, overriding common proxy for WHOIS.
|
||
|
|
func WithLookupWHOISProxy(proxy string) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.WHOIS.Proxy = strings.TrimSpace(proxy)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithLookupWHOISTimeout sets WHOIS timeout.
|
||
|
|
func WithLookupWHOISTimeout(timeout time.Duration) LookupOpt {
|
||
|
|
return func(o *LookupOptions) {
|
||
|
|
o.Ops.WHOIS.Timeout = timeout
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneLookupOps(in LookupOps) LookupOps {
|
||
|
|
out := in
|
||
|
|
out.RDAP = cloneLookupRDAPOps(in.RDAP)
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneLookupRDAPOps(in LookupRDAPOps) LookupRDAPOps {
|
||
|
|
out := in
|
||
|
|
out.CustomIP = copyStringSlice(in.CustomIP)
|
||
|
|
out.CustomDNS = copyStringSlice(in.CustomDNS)
|
||
|
|
out.Hosts = copyStringMap(in.Hosts)
|
||
|
|
out.Headers = copyStringMap(in.Headers)
|
||
|
|
if len(in.RequestOpts) > 0 {
|
||
|
|
out.RequestOpts = append([]starnet.RequestOpt(nil), in.RequestOpts...)
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func copyStringMap(in map[string]string) map[string]string {
|
||
|
|
if len(in) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
out := make(map[string]string, len(in))
|
||
|
|
for k, v := range in {
|
||
|
|
out[k] = v
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o LookupOptions) effectiveRDAPRequestOpts() []starnet.RequestOpt {
|
||
|
|
base := []starnet.RequestOpt{
|
||
|
|
starnet.WithHeader("Accept", "application/rdap+json, application/json"),
|
||
|
|
starnet.WithUserAgent("b612-whois-rdap/1.0"),
|
||
|
|
starnet.WithAutoFetch(true),
|
||
|
|
}
|
||
|
|
|
||
|
|
timeout := o.Ops.RDAP.Timeout
|
||
|
|
if timeout <= 0 {
|
||
|
|
timeout = o.Ops.Common.Timeout
|
||
|
|
}
|
||
|
|
if timeout > 0 {
|
||
|
|
base = append(base, starnet.WithTimeout(timeout))
|
||
|
|
}
|
||
|
|
if o.Ops.RDAP.DialTimeout > 0 {
|
||
|
|
base = append(base, starnet.WithDialTimeout(o.Ops.RDAP.DialTimeout))
|
||
|
|
}
|
||
|
|
if p := o.effectiveRDAPProxy(); p != "" {
|
||
|
|
base = append(base, starnet.WithProxy(p))
|
||
|
|
}
|
||
|
|
if o.Ops.RDAP.SkipTLSVerify != nil {
|
||
|
|
base = append(base, starnet.WithSkipTLSVerify(*o.Ops.RDAP.SkipTLSVerify))
|
||
|
|
}
|
||
|
|
if len(o.Ops.RDAP.CustomIP) > 0 {
|
||
|
|
base = append(base, starnet.WithCustomIP(copyStringSlice(o.Ops.RDAP.CustomIP)))
|
||
|
|
}
|
||
|
|
if len(o.Ops.RDAP.CustomDNS) > 0 {
|
||
|
|
base = append(base, starnet.WithCustomDNS(copyStringSlice(o.Ops.RDAP.CustomDNS)))
|
||
|
|
}
|
||
|
|
if len(o.Ops.RDAP.Headers) > 0 {
|
||
|
|
base = append(base, starnet.WithHeaders(copyStringMap(o.Ops.RDAP.Headers)))
|
||
|
|
}
|
||
|
|
|
||
|
|
lookupFn := o.Ops.RDAP.LookupFunc
|
||
|
|
if len(o.Ops.RDAP.Hosts) > 0 {
|
||
|
|
lookupFn = buildHostsLookupFunc(o.Ops.RDAP.Hosts, lookupFn)
|
||
|
|
}
|
||
|
|
if lookupFn != nil {
|
||
|
|
base = append(base, starnet.WithLookupFunc(lookupFn))
|
||
|
|
}
|
||
|
|
|
||
|
|
base = append(base, o.RDAPRequestOpts...)
|
||
|
|
base = append(base, o.Ops.RDAP.RequestOpts...)
|
||
|
|
return base
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o LookupOptions) effectiveRDAPProxy() string {
|
||
|
|
if p := strings.TrimSpace(o.Ops.RDAP.Proxy); p != "" {
|
||
|
|
return p
|
||
|
|
}
|
||
|
|
return strings.TrimSpace(o.Ops.Common.Proxy)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o LookupOptions) effectiveWHOISProxy() string {
|
||
|
|
if p := strings.TrimSpace(o.Ops.WHOIS.Proxy); p != "" {
|
||
|
|
return p
|
||
|
|
}
|
||
|
|
return strings.TrimSpace(o.Ops.Common.Proxy)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o LookupOptions) effectiveWHOISDialer(defaultTimeout time.Duration) (netproxy.Dialer, error) {
|
||
|
|
if o.Ops.WHOIS.Dialer != nil {
|
||
|
|
return o.Ops.WHOIS.Dialer, nil
|
||
|
|
}
|
||
|
|
proxyAddr := o.effectiveWHOISProxy()
|
||
|
|
if proxyAddr == "" {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
if !strings.Contains(proxyAddr, "://") {
|
||
|
|
proxyAddr = "socks5://" + proxyAddr
|
||
|
|
}
|
||
|
|
u, err := url.Parse(proxyAddr)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("whois proxy parse failed: %w", err)
|
||
|
|
}
|
||
|
|
dialer, err := netproxy.FromURL(u, &net.Dialer{Timeout: o.effectiveWHOISTimeout(defaultTimeout)})
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("whois proxy unsupported: %w", err)
|
||
|
|
}
|
||
|
|
return dialer, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (o LookupOptions) effectiveWHOISTimeout(defaultTimeout time.Duration) time.Duration {
|
||
|
|
if o.Ops.WHOIS.Timeout > 0 {
|
||
|
|
return o.Ops.WHOIS.Timeout
|
||
|
|
}
|
||
|
|
if o.Ops.Common.Timeout > 0 {
|
||
|
|
return o.Ops.Common.Timeout
|
||
|
|
}
|
||
|
|
return defaultTimeout
|
||
|
|
}
|
||
|
|
|
||
|
|
func buildHostsLookupFunc(hosts map[string]string, fallback func(ctx context.Context, host string) ([]net.IPAddr, error)) func(ctx context.Context, host string) ([]net.IPAddr, error) {
|
||
|
|
hostMap := make(map[string]net.IP, len(hosts))
|
||
|
|
for host, ipStr := range hosts {
|
||
|
|
h := strings.Trim(strings.ToLower(strings.TrimSpace(host)), ".")
|
||
|
|
ip := net.ParseIP(strings.TrimSpace(ipStr))
|
||
|
|
if h == "" || ip == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
hostMap[h] = ip
|
||
|
|
}
|
||
|
|
if len(hostMap) == 0 {
|
||
|
|
return fallback
|
||
|
|
}
|
||
|
|
|
||
|
|
return func(ctx context.Context, host string) ([]net.IPAddr, error) {
|
||
|
|
h := strings.Trim(strings.ToLower(strings.TrimSpace(host)), ".")
|
||
|
|
if ip, ok := hostMap[h]; ok {
|
||
|
|
return []net.IPAddr{{IP: ip}}, nil
|
||
|
|
}
|
||
|
|
if fallback != nil {
|
||
|
|
return fallback(ctx, host)
|
||
|
|
}
|
||
|
|
return net.DefaultResolver.LookupIPAddr(ctx, host)
|
||
|
|
}
|
||
|
|
}
|