2021-08-16 21:24:37 +05:30
package hosterrorscache
import (
"net"
"net/url"
"regexp"
"strings"
2023-01-02 09:22:06 +01:00
"sync"
"sync/atomic"
2021-08-16 21:24:37 +05:30
"github.com/bluele/gcache"
2021-08-17 14:50:54 +05:30
"github.com/projectdiscovery/gologger"
2021-08-16 21:24:37 +05:30
)
2022-07-18 15:35:53 -05:00
// CacheInterface defines the signature of the hosterrorscache so that
// users of Nuclei as embedded lib may implement their own cache
type CacheInterface interface {
SetVerbose ( verbose bool ) // log verbosely
Close ( ) // close the cache
Check ( value string ) bool // return true if the host should be skipped
MarkFailed ( value string , err error ) // record a failure (and cause) for the host
}
2021-08-16 21:24:37 +05:30
// Cache is a cache for host based errors. It allows skipping
// certain hosts based on an error threshold.
//
// It uses an LRU cache internally for skipping unresponsive hosts
// that remain so for a duration.
type Cache struct {
2021-11-25 17:03:56 +02:00
MaxHostError int
2021-08-17 14:50:54 +05:30
verbose bool
2021-08-16 21:24:37 +05:30
failedTargets gcache . Cache
2023-03-14 01:29:42 -07:00
TrackError [ ] string
2021-08-16 21:24:37 +05:30
}
2023-01-02 09:22:06 +01:00
type cacheItem struct {
errors atomic . Int32
sync . Once
}
2021-08-16 21:24:37 +05:30
const DefaultMaxHostsCount = 10000
// New returns a new host max errors cache
2023-03-14 01:29:42 -07:00
func New ( maxHostError , maxHostsCount int , trackError [ ] string ) * Cache {
2021-08-16 21:24:37 +05:30
gc := gcache . New ( maxHostsCount ) .
ARC ( ) .
Build ( )
2023-03-14 01:29:42 -07:00
return & Cache { failedTargets : gc , MaxHostError : maxHostError , TrackError : trackError }
2021-08-16 21:24:37 +05:30
}
2021-08-17 14:50:54 +05:30
// SetVerbose sets the cache to log at verbose level
2022-07-18 15:35:53 -05:00
func ( c * Cache ) SetVerbose ( verbose bool ) {
2021-08-17 14:50:54 +05:30
c . verbose = verbose
}
2021-08-16 21:24:37 +05:30
// Close closes the host errors cache
func ( c * Cache ) Close ( ) {
c . failedTargets . Purge ( )
}
func ( c * Cache ) normalizeCacheValue ( value string ) string {
finalValue := value
if strings . HasPrefix ( value , "http" ) {
if parsed , err := url . Parse ( value ) ; err == nil {
hostname := parsed . Host
finalPort := parsed . Port ( )
if finalPort == "" {
if parsed . Scheme == "https" {
finalPort = "443"
} else {
finalPort = "80"
}
hostname = net . JoinHostPort ( parsed . Host , finalPort )
}
finalValue = hostname
}
}
return finalValue
}
// ErrUnresponsiveHost is returned when a host is unresponsive
2021-11-25 17:03:56 +02:00
// var ErrUnresponsiveHost = errors.New("skipping as host is unresponsive")
2021-08-16 21:24:37 +05:30
// Check returns true if a host should be skipped as it has been
// unresponsive for a certain number of times.
//
// The value can be many formats -
// - URL: https?:// type
// - Host:port type
// - host type
func ( c * Cache ) Check ( value string ) bool {
finalValue := c . normalizeCacheValue ( value )
2023-01-02 09:22:06 +01:00
existingCacheItem , err := c . failedTargets . GetIFPresent ( finalValue )
2021-08-16 21:24:37 +05:30
if err != nil {
return false
}
2023-01-02 09:22:06 +01:00
existingCacheItemValue := existingCacheItem . ( * cacheItem )
2021-08-17 14:50:54 +05:30
2023-01-02 09:22:06 +01:00
if existingCacheItemValue . errors . Load ( ) >= int32 ( c . MaxHostError ) {
2023-01-02 19:00:10 +05:30
existingCacheItemValue . Do ( func ( ) {
gologger . Info ( ) . Msgf ( "Skipped %s from target list as found unresponsive %d times" , finalValue , existingCacheItemValue . errors . Load ( ) )
} )
2021-08-17 14:50:54 +05:30
return true
}
return false
2021-08-16 21:24:37 +05:30
}
// MarkFailed marks a host as failed previously
2022-07-18 15:35:53 -05:00
func ( c * Cache ) MarkFailed ( value string , err error ) {
if ! c . checkError ( err ) {
return
}
2021-08-16 21:24:37 +05:30
finalValue := c . normalizeCacheValue ( value )
2023-01-02 09:22:06 +01:00
existingCacheItem , err := c . failedTargets . GetIFPresent ( finalValue )
if err != nil || existingCacheItem == nil {
newItem := & cacheItem { errors : atomic . Int32 { } }
newItem . errors . Store ( 1 )
_ = c . failedTargets . Set ( finalValue , newItem )
2021-08-16 21:24:37 +05:30
return
}
2023-01-02 09:22:06 +01:00
existingCacheItemValue := existingCacheItem . ( * cacheItem )
existingCacheItemValue . errors . Add ( 1 )
_ = c . failedTargets . Set ( finalValue , existingCacheItemValue )
2021-08-16 21:24:37 +05:30
}
2024-04-24 19:34:13 +05:30
var reCheckError = regexp . MustCompile ( ` (no address found for host|Client\.Timeout exceeded while awaiting headers|could not resolve host|connection refused|connection reset by peer|i/o timeout|could not connect to any address found for host) ` )
2021-08-16 21:24:37 +05:30
2022-07-18 15:35:53 -05:00
// checkError checks if an error represents a type that should be
2021-08-16 21:24:37 +05:30
// added to the host skipping table.
2022-07-18 15:35:53 -05:00
func ( c * Cache ) checkError ( err error ) bool {
2023-03-14 01:29:42 -07:00
if err == nil {
return false
}
2021-08-16 21:24:37 +05:30
errString := err . Error ( )
2023-03-14 01:29:42 -07:00
for _ , msg := range c . TrackError {
if strings . Contains ( errString , msg ) {
return true
}
}
2023-01-02 09:22:06 +01:00
return reCheckError . MatchString ( errString )
2021-08-16 21:24:37 +05:30
}