github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/common/hosterrorscache/hosterrorscache.go (about)

     1  package hosterrorscache
     2  
     3  import (
     4  	"net"
     5  	"net/url"
     6  	"regexp"
     7  	"strings"
     8  	"sync"
     9  	"sync/atomic"
    10  
    11  	"github.com/bluele/gcache"
    12  	"github.com/projectdiscovery/gologger"
    13  )
    14  
    15  // CacheInterface defines the signature of the hosterrorscache so that
    16  // users of Nuclei as embedded lib may implement their own cache
    17  type CacheInterface interface {
    18  	SetVerbose(verbose bool)            // log verbosely
    19  	Close()                             // close the cache
    20  	Check(value string) bool            // return true if the host should be skipped
    21  	MarkFailed(value string, err error) // record a failure (and cause) for the host
    22  }
    23  
    24  // Cache is a cache for host based errors. It allows skipping
    25  // certain hosts based on an error threshold.
    26  //
    27  // It uses an LRU cache internally for skipping unresponsive hosts
    28  // that remain so for a duration.
    29  type Cache struct {
    30  	MaxHostError  int
    31  	verbose       bool
    32  	failedTargets gcache.Cache
    33  	TrackError    []string
    34  }
    35  
    36  type cacheItem struct {
    37  	errors atomic.Int32
    38  	sync.Once
    39  }
    40  
    41  const DefaultMaxHostsCount = 10000
    42  
    43  // New returns a new host max errors cache
    44  func New(maxHostError, maxHostsCount int, trackError []string) *Cache {
    45  	gc := gcache.New(maxHostsCount).
    46  		ARC().
    47  		Build()
    48  	return &Cache{failedTargets: gc, MaxHostError: maxHostError, TrackError: trackError}
    49  }
    50  
    51  // SetVerbose sets the cache to log at verbose level
    52  func (c *Cache) SetVerbose(verbose bool) {
    53  	c.verbose = verbose
    54  }
    55  
    56  // Close closes the host errors cache
    57  func (c *Cache) Close() {
    58  	c.failedTargets.Purge()
    59  }
    60  
    61  func (c *Cache) normalizeCacheValue(value string) string {
    62  	finalValue := value
    63  	if strings.HasPrefix(value, "http") {
    64  		if parsed, err := url.Parse(value); err == nil {
    65  			hostname := parsed.Host
    66  			finalPort := parsed.Port()
    67  			if finalPort == "" {
    68  				if parsed.Scheme == "https" {
    69  					finalPort = "443"
    70  				} else {
    71  					finalPort = "80"
    72  				}
    73  				hostname = net.JoinHostPort(parsed.Host, finalPort)
    74  			}
    75  			finalValue = hostname
    76  		}
    77  	}
    78  	return finalValue
    79  }
    80  
    81  // ErrUnresponsiveHost is returned when a host is unresponsive
    82  // var ErrUnresponsiveHost = errors.New("skipping as host is unresponsive")
    83  
    84  // Check returns true if a host should be skipped as it has been
    85  // unresponsive for a certain number of times.
    86  //
    87  // The value can be many formats -
    88  //   - URL: https?:// type
    89  //   - Host:port type
    90  //   - host type
    91  func (c *Cache) Check(value string) bool {
    92  	finalValue := c.normalizeCacheValue(value)
    93  
    94  	existingCacheItem, err := c.failedTargets.GetIFPresent(finalValue)
    95  	if err != nil {
    96  		return false
    97  	}
    98  	existingCacheItemValue := existingCacheItem.(*cacheItem)
    99  
   100  	if existingCacheItemValue.errors.Load() >= int32(c.MaxHostError) {
   101  		existingCacheItemValue.Do(func() {
   102  			gologger.Info().Msgf("Skipped %s from target list as found unresponsive %d times", finalValue, existingCacheItemValue.errors.Load())
   103  		})
   104  		return true
   105  	}
   106  	return false
   107  }
   108  
   109  // MarkFailed marks a host as failed previously
   110  func (c *Cache) MarkFailed(value string, err error) {
   111  	if !c.checkError(err) {
   112  		return
   113  	}
   114  	finalValue := c.normalizeCacheValue(value)
   115  	existingCacheItem, err := c.failedTargets.GetIFPresent(finalValue)
   116  	if err != nil || existingCacheItem == nil {
   117  		newItem := &cacheItem{errors: atomic.Int32{}}
   118  		newItem.errors.Store(1)
   119  		_ = c.failedTargets.Set(finalValue, newItem)
   120  		return
   121  	}
   122  	existingCacheItemValue := existingCacheItem.(*cacheItem)
   123  	existingCacheItemValue.errors.Add(1)
   124  	_ = c.failedTargets.Set(finalValue, existingCacheItemValue)
   125  }
   126  
   127  var reCheckError = regexp.MustCompile(`(no address found for host|Client\.Timeout exceeded while awaiting headers|could not resolve host|connection refused)`)
   128  
   129  // checkError checks if an error represents a type that should be
   130  // added to the host skipping table.
   131  func (c *Cache) checkError(err error) bool {
   132  	if err == nil {
   133  		return false
   134  	}
   135  	errString := err.Error()
   136  	for _, msg := range c.TrackError {
   137  		if strings.Contains(errString, msg) {
   138  			return true
   139  		}
   140  	}
   141  	return reCheckError.MatchString(errString)
   142  }