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 }