mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-25 14:35:48 +03:00
06d465b0d1
Merge in DNS/adguard-home from AG-22594-imp-whois to master Squashed commit of the following: commit 093feed53291d02469fb1bd8d99472597ebd5015 Merge: 956d20dc4ca313521d
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Jun 21 12:42:40 2023 +0300 Merge branch 'master' into AG-22594-imp-whois commit 956d20dc473dcec90895b6f618fc56e96e9ff833 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Jun 20 18:30:48 2023 +0300 whois: imp code more commit c771fd9c5e4d90e76d079a0d25ab097ab5652a42 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Jun 20 15:05:45 2023 +0300 whois: imp code commit 21900fd468e10d9aee22149a6312b8596ff39810 Merge: 8dbe132c0371261b2c
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Jun 20 11:34:06 2023 +0300 Merge branch 'master' into AG-22594-imp-whois commit 8dbe132c08d3ad4a63b0d4bdb9d00a5bc25971f4 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Jun 20 11:33:26 2023 +0300 whois: imp code more commit f5e761a260237579c67cbd48f01ea90499bff6b0 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Jun 19 16:04:35 2023 +0300 whois: imp code commit 2780f7e16aacddad8736f83b77ef9bfa1271f8b1 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jun 16 17:33:47 2023 +0300 all: imp code commit 1fc67016068b745a46b3d0d341ab14f9f5bdc9aa Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jun 16 17:29:19 2023 +0300 whois: imp tests commit 204761870764fb10feea20065d79dee8c321e70b Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jun 16 11:55:37 2023 +0300 all: upd deps commit ded4f59498c5c544277b9c8e249567626547680e Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Jun 14 20:43:32 2023 +0300 all: imp tests commit 0eed9834ff9dd94d0788ce69d0bb0521fa725410 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Jun 14 19:31:49 2023 +0300 all: imp code commit 9f867587c8ad87363b8c8b061ead536c1ec59c5d Merge: 504e9484d681c604c2
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Jun 13 14:20:44 2023 +0300 Merge branch 'master' into AG-22594-imp-whois commit 504e9484dd84ab9d7c84a3f8399993d6422d3b67 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Tue Jun 13 14:18:06 2023 +0300 all: imp cache commit c492abe41ace7ad76fcd4e297c22b910a90fec30 Merge: db36adb9c826b314f1
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jun 9 16:06:12 2023 +0300 Merge branch 'master' into AG-22594-imp-whois commit db36adb9c14ce92b3971db0d87ec313d5bcd787e Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jun 9 15:53:33 2023 +0300 all: add todo commit 5cf192de9f93cd0d8521a3a6b4ded7f2bc5e0031 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Jun 8 14:59:26 2023 +0300 all: imp docs commit 021aa3eb5b9476a93b4af5fc90cc9ccf014ca152 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Jun 5 18:35:25 2023 +0300 all: imp naming commit 4626c3a7fa3f2543501806c9fa1a19531547f394 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jun 2 17:41:00 2023 +0300 all: imp tests commit 1afcc9605ca176e4c7f76a03a2c996cf7d6bde13 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Jun 2 12:44:32 2023 +0300 all: imp docs commit cdd0544ff1a63faed5ced3dae6bfb3b783e45428 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Jun 1 17:21:37 2023 +0300 all: add docs ... and 2 more commits
376 lines
9.6 KiB
Go
376 lines
9.6 KiB
Go
// Package whois provides WHOIS functionality.
|
|
package whois
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/AdguardTeam/golibs/stringutil"
|
|
"github.com/bluele/gcache"
|
|
)
|
|
|
|
const (
|
|
// DefaultServer is the default WHOIS server.
|
|
DefaultServer = "whois.arin.net"
|
|
|
|
// DefaultPort is the default port for WHOIS requests.
|
|
DefaultPort = 43
|
|
)
|
|
|
|
// Interface provides WHOIS functionality.
|
|
type Interface interface {
|
|
// Process makes WHOIS request and returns WHOIS information or nil.
|
|
// changed indicates that Info was updated since last request.
|
|
Process(ctx context.Context, ip netip.Addr) (info *Info, changed bool)
|
|
}
|
|
|
|
// Empty is an empty [Interface] implementation which does nothing.
|
|
type Empty struct{}
|
|
|
|
// type check
|
|
var _ Interface = (*Empty)(nil)
|
|
|
|
// Process implements the [Interface] interface for Empty.
|
|
func (Empty) Process(_ context.Context, _ netip.Addr) (info *Info, changed bool) {
|
|
return nil, false
|
|
}
|
|
|
|
// Config is the configuration structure for Default.
|
|
type Config struct {
|
|
// DialContext specifies the dial function for creating unencrypted TCP
|
|
// connections.
|
|
DialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
|
|
|
|
// ServerAddr is the address of the WHOIS server.
|
|
ServerAddr string
|
|
|
|
// Timeout is the timeout for WHOIS requests.
|
|
Timeout time.Duration
|
|
|
|
// CacheTTL is the Time to Live duration for cached IP addresses.
|
|
CacheTTL time.Duration
|
|
|
|
// MaxConnReadSize is an upper limit in bytes for reading from net.Conn.
|
|
MaxConnReadSize int64
|
|
|
|
// MaxRedirects is the maximum redirects count.
|
|
MaxRedirects int
|
|
|
|
// MaxInfoLen is the maximum length of Info fields returned by Process.
|
|
MaxInfoLen int
|
|
|
|
// CacheSize is the maximum size of the cache. It must be greater than
|
|
// zero.
|
|
CacheSize int
|
|
|
|
// Port is the port for WHOIS requests.
|
|
Port uint16
|
|
}
|
|
|
|
// Default is the default WHOIS information processor.
|
|
type Default struct {
|
|
// cache is the cache containing IP addresses of clients. An active IP
|
|
// address is resolved once again after it expires. If IP address couldn't
|
|
// be resolved, it stays here for some time to prevent further attempts to
|
|
// resolve the same IP.
|
|
cache gcache.Cache
|
|
|
|
// dialContext connects to a remote server resolving hostname using our own
|
|
// DNS server and unecrypted TCP connection.
|
|
dialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
|
|
|
|
// serverAddr is the address of the WHOIS server.
|
|
serverAddr string
|
|
|
|
// portStr is the port for WHOIS requests.
|
|
portStr string
|
|
|
|
// timeout is the timeout for WHOIS requests.
|
|
timeout time.Duration
|
|
|
|
// cacheTTL is the Time to Live duration for cached IP addresses.
|
|
cacheTTL time.Duration
|
|
|
|
// maxConnReadSize is an upper limit in bytes for reading from net.Conn.
|
|
maxConnReadSize int64
|
|
|
|
// maxRedirects is the maximum redirects count.
|
|
maxRedirects int
|
|
|
|
// maxInfoLen is the maximum length of Info fields returned by Process.
|
|
maxInfoLen int
|
|
}
|
|
|
|
// New returns a new default WHOIS information processor. conf must not be
|
|
// nil.
|
|
func New(conf *Config) (w *Default) {
|
|
return &Default{
|
|
serverAddr: conf.ServerAddr,
|
|
dialContext: conf.DialContext,
|
|
timeout: conf.Timeout,
|
|
cache: gcache.New(conf.CacheSize).LRU().Build(),
|
|
maxConnReadSize: conf.MaxConnReadSize,
|
|
maxRedirects: conf.MaxRedirects,
|
|
portStr: strconv.Itoa(int(conf.Port)),
|
|
maxInfoLen: conf.MaxInfoLen,
|
|
cacheTTL: conf.CacheTTL,
|
|
}
|
|
}
|
|
|
|
// trimValue trims s and replaces the last 3 characters of the cut with "..."
|
|
// to fit into max. max must be greater than 3.
|
|
func trimValue(s string, max int) string {
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
|
|
return s[:max-3] + "..."
|
|
}
|
|
|
|
// isWHOISComment returns true if the data is empty or is a WHOIS comment.
|
|
func isWHOISComment(data []byte) (ok bool) {
|
|
return len(data) == 0 || data[0] == '#' || data[0] == '%'
|
|
}
|
|
|
|
// whoisParse parses a subset of plain-text data from the WHOIS response into a
|
|
// string map. It trims values of the returned map to maxLen.
|
|
func whoisParse(data []byte, maxLen int) (info map[string]string) {
|
|
info = map[string]string{}
|
|
|
|
var orgname string
|
|
lines := bytes.Split(data, []byte("\n"))
|
|
for _, l := range lines {
|
|
if isWHOISComment(l) {
|
|
continue
|
|
}
|
|
|
|
before, after, found := bytes.Cut(l, []byte(":"))
|
|
if !found {
|
|
continue
|
|
}
|
|
|
|
key := strings.ToLower(string(before))
|
|
val := strings.TrimSpace(string(after))
|
|
if val == "" {
|
|
continue
|
|
}
|
|
|
|
switch key {
|
|
case "orgname", "org-name":
|
|
key = "orgname"
|
|
val = trimValue(val, maxLen)
|
|
orgname = val
|
|
case "city", "country":
|
|
val = trimValue(val, maxLen)
|
|
case "descr", "netname":
|
|
key = "orgname"
|
|
val = stringutil.Coalesce(orgname, val)
|
|
orgname = val
|
|
case "whois":
|
|
key = "whois"
|
|
case "referralserver":
|
|
key = "whois"
|
|
val = strings.TrimPrefix(val, "whois://")
|
|
default:
|
|
continue
|
|
}
|
|
|
|
info[key] = val
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// query sends request to a server and returns the response or error.
|
|
func (w *Default) query(ctx context.Context, target, serverAddr string) (data []byte, err error) {
|
|
addr, _, _ := net.SplitHostPort(serverAddr)
|
|
if addr == DefaultServer {
|
|
// Display type flags for query.
|
|
//
|
|
// See https://www.arin.net/resources/registry/whois/rws/api/#nicname-whois-queries.
|
|
target = "n + " + target
|
|
}
|
|
|
|
conn, err := w.dialContext(ctx, "tcp", serverAddr)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return nil, err
|
|
}
|
|
defer func() { err = errors.WithDeferred(err, conn.Close()) }()
|
|
|
|
r, err := aghio.LimitReader(conn, w.maxConnReadSize)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return nil, err
|
|
}
|
|
|
|
_ = conn.SetReadDeadline(time.Now().Add(w.timeout))
|
|
_, err = io.WriteString(conn, target+"\r\n")
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return nil, err
|
|
}
|
|
|
|
// This use of ReadAll is now safe, because we limited the conn Reader.
|
|
data, err = io.ReadAll(r)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return nil, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// queryAll queries WHOIS server and handles redirects.
|
|
func (w *Default) queryAll(ctx context.Context, target string) (info map[string]string, err error) {
|
|
server := net.JoinHostPort(w.serverAddr, w.portStr)
|
|
var data []byte
|
|
|
|
for i := 0; i < w.maxRedirects; i++ {
|
|
data, err = w.query(ctx, target, server)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return nil, err
|
|
}
|
|
|
|
log.Debug("whois: received response (%d bytes) from %q about %q", len(data), server, target)
|
|
|
|
info = whoisParse(data, w.maxInfoLen)
|
|
redir, ok := info["whois"]
|
|
if !ok {
|
|
return info, nil
|
|
}
|
|
|
|
redir = strings.ToLower(redir)
|
|
|
|
_, _, err = net.SplitHostPort(redir)
|
|
if err != nil {
|
|
server = net.JoinHostPort(redir, w.portStr)
|
|
} else {
|
|
server = redir
|
|
}
|
|
|
|
log.Debug("whois: redirected to %q about %q", redir, target)
|
|
}
|
|
|
|
return nil, fmt.Errorf("whois: redirect loop")
|
|
}
|
|
|
|
// type check
|
|
var _ Interface = (*Default)(nil)
|
|
|
|
// Process makes WHOIS request and returns WHOIS information or nil. changed
|
|
// indicates that Info was updated since last request.
|
|
func (w *Default) Process(ctx context.Context, ip netip.Addr) (wi *Info, changed bool) {
|
|
if netutil.IsSpecialPurposeAddr(ip) {
|
|
return nil, false
|
|
}
|
|
|
|
wi, expired := w.findInCache(ip)
|
|
if wi != nil && !expired {
|
|
// Don't return an empty struct so that the frontend doesn't get
|
|
// confused.
|
|
if (*wi == Info{}) {
|
|
return nil, false
|
|
}
|
|
|
|
return wi, false
|
|
}
|
|
|
|
var info Info
|
|
|
|
defer func() {
|
|
item := toCacheItem(info, w.cacheTTL)
|
|
err := w.cache.Set(ip, item)
|
|
if err != nil {
|
|
log.Debug("whois: cache: adding item %q: %s", ip, err)
|
|
}
|
|
}()
|
|
|
|
kv, err := w.queryAll(ctx, ip.String())
|
|
if err != nil {
|
|
log.Debug("whois: quering about %q: %s", ip, err)
|
|
|
|
return nil, true
|
|
}
|
|
|
|
info = Info{
|
|
City: kv["city"],
|
|
Country: kv["country"],
|
|
Orgname: kv["orgname"],
|
|
}
|
|
|
|
// Don't return an empty struct so that the frontend doesn't get confused.
|
|
if (info == Info{}) {
|
|
return nil, true
|
|
}
|
|
|
|
return &info, wi == nil || info != *wi
|
|
}
|
|
|
|
// findInCache finds Info in the cache. expired indicates that Info is valid.
|
|
func (w *Default) findInCache(ip netip.Addr) (wi *Info, expired bool) {
|
|
val, err := w.cache.Get(ip)
|
|
if err != nil {
|
|
if !errors.Is(err, gcache.KeyNotFoundError) {
|
|
log.Debug("whois: cache: retrieving info about %q: %s", ip, err)
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
item, ok := val.(*cacheItem)
|
|
if !ok {
|
|
log.Debug("whois: cache: %q bad type %T", ip, val)
|
|
|
|
return nil, false
|
|
}
|
|
|
|
return fromCacheItem(item)
|
|
}
|
|
|
|
// Info is the filtered WHOIS data for a runtime client.
|
|
type Info struct {
|
|
City string `json:"city,omitempty"`
|
|
Country string `json:"country,omitempty"`
|
|
Orgname string `json:"orgname,omitempty"`
|
|
}
|
|
|
|
// cacheItem represents an item that we will store in the cache.
|
|
type cacheItem struct {
|
|
// expiry is the time when cacheItem will expire.
|
|
expiry time.Time
|
|
|
|
// info is the WHOIS data for a runtime client.
|
|
info *Info
|
|
}
|
|
|
|
// toCacheItem creates a cached item from a WHOIS info and Time to Live
|
|
// duration.
|
|
func toCacheItem(info Info, ttl time.Duration) (item *cacheItem) {
|
|
return &cacheItem{
|
|
expiry: time.Now().Add(ttl),
|
|
info: &info,
|
|
}
|
|
}
|
|
|
|
// fromCacheItem creates a WHOIS info from the cached item. expired indicates
|
|
// that WHOIS info is valid. item must not be nil.
|
|
func fromCacheItem(item *cacheItem) (info *Info, expired bool) {
|
|
if time.Now().After(item.expiry) {
|
|
return item.info, true
|
|
}
|
|
|
|
return item.info, false
|
|
}
|