config.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. package config
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "io/fs"
  7. "net"
  8. "net/http"
  9. "os"
  10. "regexp"
  11. "strings"
  12. "time"
  13. "github.com/TecharoHQ/anubis/data"
  14. "k8s.io/apimachinery/pkg/util/yaml"
  15. )
  16. var (
  17. ErrNoBotRulesDefined = errors.New("config: must define at least one (1) bot rule")
  18. ErrBotMustHaveName = errors.New("config.Bot: must set name")
  19. ErrBotMustHaveUserAgentOrPath = errors.New("config.Bot: must set either user_agent_regex, path_regex, headers_regex, or remote_addresses")
  20. ErrBotMustHaveUserAgentOrPathNotBoth = errors.New("config.Bot: must set either user_agent_regex, path_regex, and not both")
  21. ErrUnknownAction = errors.New("config.Bot: unknown action")
  22. ErrInvalidUserAgentRegex = errors.New("config.Bot: invalid user agent regex")
  23. ErrInvalidPathRegex = errors.New("config.Bot: invalid path regex")
  24. ErrInvalidHeadersRegex = errors.New("config.Bot: invalid headers regex")
  25. ErrInvalidCIDR = errors.New("config.Bot: invalid CIDR")
  26. ErrRegexEndsWithNewline = errors.New("config.Bot: regular expression ends with newline (try >- instead of > in yaml)")
  27. ErrInvalidImportStatement = errors.New("config.ImportStatement: invalid source file")
  28. ErrCantSetBotAndImportValuesAtOnce = errors.New("config.BotOrImport: can't set bot rules and import values at the same time")
  29. ErrMustSetBotOrImportRules = errors.New("config.BotOrImport: rule definition is invalid, you must set either bot rules or an import statement, not both")
  30. ErrStatusCodeNotValid = errors.New("config.StatusCode: status code not valid, must be between 100 and 599")
  31. )
  32. type Rule string
  33. const (
  34. RuleUnknown Rule = ""
  35. RuleAllow Rule = "ALLOW"
  36. RuleDeny Rule = "DENY"
  37. RuleChallenge Rule = "CHALLENGE"
  38. RuleWeigh Rule = "WEIGH"
  39. RuleBenchmark Rule = "DEBUG_BENCHMARK"
  40. )
  41. func (r Rule) Valid() error {
  42. switch r {
  43. case RuleAllow, RuleDeny, RuleChallenge, RuleWeigh, RuleBenchmark:
  44. return nil
  45. default:
  46. return ErrUnknownAction
  47. }
  48. }
  49. const DefaultAlgorithm = "fast"
  50. type BotConfig struct {
  51. UserAgentRegex *string `json:"user_agent_regex,omitempty" yaml:"user_agent_regex,omitempty"`
  52. PathRegex *string `json:"path_regex,omitempty" yaml:"path_regex,omitempty"`
  53. HeadersRegex map[string]string `json:"headers_regex,omitempty" yaml:"headers_regex,omitempty"`
  54. Expression *ExpressionOrList `json:"expression,omitempty" yaml:"expression,omitempty"`
  55. Challenge *ChallengeRules `json:"challenge,omitempty" yaml:"challenge,omitempty"`
  56. Weight *Weight `json:"weight,omitempty" yaml:"weight,omitempty"`
  57. // Thoth features
  58. GeoIP *GeoIP `json:"geoip,omitempty"`
  59. ASNs *ASNs `json:"asns,omitempty"`
  60. Name string `json:"name" yaml:"name"`
  61. Action Rule `json:"action" yaml:"action"`
  62. RemoteAddr []string `json:"remote_addresses,omitempty" yaml:"remote_addresses,omitempty"`
  63. }
  64. func (b BotConfig) Zero() bool {
  65. for _, cond := range []bool{
  66. b.Name != "",
  67. b.UserAgentRegex != nil,
  68. b.PathRegex != nil,
  69. len(b.HeadersRegex) != 0,
  70. b.Action != "",
  71. len(b.RemoteAddr) != 0,
  72. b.Challenge != nil,
  73. b.GeoIP != nil,
  74. b.ASNs != nil,
  75. } {
  76. if cond {
  77. return false
  78. }
  79. }
  80. return true
  81. }
  82. func (b *BotConfig) Valid() error {
  83. var errs []error
  84. if b.Name == "" {
  85. errs = append(errs, ErrBotMustHaveName)
  86. }
  87. allFieldsEmpty := b.UserAgentRegex == nil &&
  88. b.PathRegex == nil &&
  89. len(b.RemoteAddr) == 0 &&
  90. len(b.HeadersRegex) == 0 &&
  91. b.ASNs == nil &&
  92. b.GeoIP == nil
  93. if allFieldsEmpty && b.Expression == nil {
  94. errs = append(errs, ErrBotMustHaveUserAgentOrPath)
  95. }
  96. if b.UserAgentRegex != nil && b.PathRegex != nil {
  97. errs = append(errs, ErrBotMustHaveUserAgentOrPathNotBoth)
  98. }
  99. if b.UserAgentRegex != nil {
  100. if strings.HasSuffix(*b.UserAgentRegex, "\n") {
  101. errs = append(errs, fmt.Errorf("%w: user agent regex: %q", ErrRegexEndsWithNewline, *b.UserAgentRegex))
  102. }
  103. if _, err := regexp.Compile(*b.UserAgentRegex); err != nil {
  104. errs = append(errs, ErrInvalidUserAgentRegex, err)
  105. }
  106. }
  107. if b.PathRegex != nil {
  108. if strings.HasSuffix(*b.PathRegex, "\n") {
  109. errs = append(errs, fmt.Errorf("%w: path regex: %q", ErrRegexEndsWithNewline, *b.PathRegex))
  110. }
  111. if _, err := regexp.Compile(*b.PathRegex); err != nil {
  112. errs = append(errs, ErrInvalidPathRegex, err)
  113. }
  114. }
  115. if len(b.HeadersRegex) > 0 {
  116. for name, expr := range b.HeadersRegex {
  117. if name == "" {
  118. continue
  119. }
  120. if strings.HasSuffix(expr, "\n") {
  121. errs = append(errs, fmt.Errorf("%w: header %s regex: %q", ErrRegexEndsWithNewline, name, expr))
  122. }
  123. if _, err := regexp.Compile(expr); err != nil {
  124. errs = append(errs, ErrInvalidHeadersRegex, err)
  125. }
  126. }
  127. }
  128. if len(b.RemoteAddr) > 0 {
  129. for _, cidr := range b.RemoteAddr {
  130. if _, _, err := net.ParseCIDR(cidr); err != nil {
  131. errs = append(errs, ErrInvalidCIDR, err)
  132. }
  133. }
  134. }
  135. if b.Expression != nil {
  136. if err := b.Expression.Valid(); err != nil {
  137. errs = append(errs, err)
  138. }
  139. }
  140. switch b.Action {
  141. case RuleAllow, RuleBenchmark, RuleChallenge, RuleDeny, RuleWeigh:
  142. // okay
  143. default:
  144. errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action))
  145. }
  146. if b.Action == RuleChallenge && b.Challenge != nil {
  147. if err := b.Challenge.Valid(); err != nil {
  148. errs = append(errs, err)
  149. }
  150. }
  151. if b.Action == RuleWeigh && b.Weight == nil {
  152. b.Weight = &Weight{Adjust: 5}
  153. }
  154. if len(errs) != 0 {
  155. return fmt.Errorf("config: bot entry for %q is not valid:\n%w", b.Name, errors.Join(errs...))
  156. }
  157. return nil
  158. }
  159. type ChallengeRules struct {
  160. Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"`
  161. Difficulty int `json:"difficulty,omitempty" yaml:"difficulty,omitempty"`
  162. ReportAs int `json:"report_as,omitempty" yaml:"report_as,omitempty"`
  163. }
  164. var (
  165. ErrChallengeDifficultyTooLow = errors.New("config.ChallengeRules: difficulty is too low (must be >= 0)")
  166. ErrChallengeDifficultyTooHigh = errors.New("config.ChallengeRules: difficulty is too high (must be <= 64)")
  167. ErrChallengeMustHaveAlgorithm = errors.New("config.ChallengeRules: must have algorithm name set")
  168. )
  169. func (cr ChallengeRules) Valid() error {
  170. var errs []error
  171. if cr.Algorithm == "" {
  172. errs = append(errs, ErrChallengeMustHaveAlgorithm)
  173. }
  174. if cr.Difficulty < 0 {
  175. errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooLow, cr.Difficulty))
  176. }
  177. if cr.Difficulty > 64 {
  178. errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooHigh, cr.Difficulty))
  179. }
  180. if len(errs) != 0 {
  181. return fmt.Errorf("config: challenge rules entry is not valid:\n%w", errors.Join(errs...))
  182. }
  183. return nil
  184. }
  185. type ImportStatement struct {
  186. Import string `json:"import"`
  187. Bots []BotConfig
  188. }
  189. func (is *ImportStatement) open() (fs.File, error) {
  190. if strings.HasPrefix(is.Import, "(data)/") {
  191. fname := strings.TrimPrefix(is.Import, "(data)/")
  192. fin, err := data.BotPolicies.Open(fname)
  193. return fin, err
  194. }
  195. return os.Open(is.Import)
  196. }
  197. func (is *ImportStatement) load() error {
  198. fin, err := is.open()
  199. if err != nil {
  200. return fmt.Errorf("%w: %s: %w", ErrInvalidImportStatement, is.Import, err)
  201. }
  202. defer fin.Close()
  203. var imported []BotOrImport
  204. var result []BotConfig
  205. if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&imported); err != nil {
  206. return fmt.Errorf("can't parse %s: %w", is.Import, err)
  207. }
  208. var errs []error
  209. for _, b := range imported {
  210. if err := b.Valid(); err != nil {
  211. errs = append(errs, err)
  212. }
  213. if b.ImportStatement != nil {
  214. result = append(result, b.ImportStatement.Bots...)
  215. }
  216. if b.BotConfig != nil {
  217. result = append(result, *b.BotConfig)
  218. }
  219. }
  220. if len(errs) != 0 {
  221. return fmt.Errorf("config %s is not valid:\n%w", is.Import, errors.Join(errs...))
  222. }
  223. is.Bots = result
  224. return nil
  225. }
  226. func (is *ImportStatement) Valid() error {
  227. return is.load()
  228. }
  229. type BotOrImport struct {
  230. *BotConfig `json:",inline"`
  231. *ImportStatement `json:",inline"`
  232. }
  233. func (boi *BotOrImport) Valid() error {
  234. if boi.BotConfig != nil && boi.ImportStatement != nil {
  235. return ErrCantSetBotAndImportValuesAtOnce
  236. }
  237. if boi.BotConfig != nil {
  238. return boi.BotConfig.Valid()
  239. }
  240. if boi.ImportStatement != nil {
  241. return boi.ImportStatement.Valid()
  242. }
  243. return ErrMustSetBotOrImportRules
  244. }
  245. type StatusCodes struct {
  246. Challenge int `json:"CHALLENGE"`
  247. Deny int `json:"DENY"`
  248. }
  249. func (sc StatusCodes) Valid() error {
  250. var errs []error
  251. if sc.Challenge == 0 || (sc.Challenge < 100 && sc.Challenge >= 599) {
  252. errs = append(errs, fmt.Errorf("%w: challenge is %d", ErrStatusCodeNotValid, sc.Challenge))
  253. }
  254. if sc.Deny == 0 || (sc.Deny < 100 && sc.Deny >= 599) {
  255. errs = append(errs, fmt.Errorf("%w: deny is %d", ErrStatusCodeNotValid, sc.Deny))
  256. }
  257. if len(errs) != 0 {
  258. return fmt.Errorf("status codes not valid:\n%w", errors.Join(errs...))
  259. }
  260. return nil
  261. }
  262. type fileConfig struct {
  263. OpenGraph openGraphFileConfig `json:"openGraph,omitempty"`
  264. Impressum *Impressum `json:"impressum,omitempty"`
  265. Store *Store `json:"store"`
  266. Bots []BotOrImport `json:"bots"`
  267. Thresholds []Threshold `json:"thresholds"`
  268. StatusCodes StatusCodes `json:"status_codes"`
  269. DNSBL bool `json:"dnsbl"`
  270. DNSTTL DnsTTL `json:"dns_ttl"`
  271. Logging *Logging `json:"logging"`
  272. }
  273. func (c *fileConfig) Valid() error {
  274. var errs []error
  275. if len(c.Bots) == 0 {
  276. errs = append(errs, ErrNoBotRulesDefined)
  277. }
  278. for i, b := range c.Bots {
  279. if err := b.Valid(); err != nil {
  280. errs = append(errs, fmt.Errorf("bot %d: %w", i, err))
  281. }
  282. }
  283. if c.OpenGraph.Enabled {
  284. if err := c.OpenGraph.Valid(); err != nil {
  285. errs = append(errs, err)
  286. }
  287. }
  288. if err := c.StatusCodes.Valid(); err != nil {
  289. errs = append(errs, err)
  290. }
  291. for i, t := range c.Thresholds {
  292. if err := t.Valid(); err != nil {
  293. errs = append(errs, fmt.Errorf("threshold %d: %w", i, err))
  294. }
  295. }
  296. if err := c.Logging.Valid(); err != nil {
  297. errs = append(errs, err)
  298. }
  299. if c.Store != nil {
  300. if err := c.Store.Valid(); err != nil {
  301. errs = append(errs, err)
  302. }
  303. }
  304. if len(errs) != 0 {
  305. return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
  306. }
  307. return nil
  308. }
  309. func Load(fin io.Reader, fname string) (*Config, error) {
  310. c := &fileConfig{
  311. StatusCodes: StatusCodes{
  312. Challenge: http.StatusOK,
  313. Deny: http.StatusOK,
  314. },
  315. DNSTTL: DnsTTL{
  316. Forward: 300,
  317. Reverse: 300,
  318. },
  319. Store: &Store{
  320. Backend: "memory",
  321. },
  322. Logging: (Logging{}).Default(),
  323. }
  324. if err := yaml.NewYAMLToJSONDecoder(fin).Decode(&c); err != nil {
  325. return nil, fmt.Errorf("can't parse policy config YAML %s: %w", fname, err)
  326. }
  327. if err := c.Valid(); err != nil {
  328. return nil, err
  329. }
  330. result := &Config{
  331. DNSBL: c.DNSBL,
  332. DNSTTL: c.DNSTTL,
  333. OpenGraph: OpenGraph{
  334. Enabled: c.OpenGraph.Enabled,
  335. ConsiderHost: c.OpenGraph.ConsiderHost,
  336. Override: c.OpenGraph.Override,
  337. },
  338. StatusCodes: c.StatusCodes,
  339. Store: c.Store,
  340. Logging: c.Logging,
  341. }
  342. if c.OpenGraph.TimeToLive != "" {
  343. // XXX(Xe): already validated in Valid()
  344. ogTTL, _ := time.ParseDuration(c.OpenGraph.TimeToLive)
  345. result.OpenGraph.TimeToLive = ogTTL
  346. }
  347. var validationErrs []error
  348. for _, boi := range c.Bots {
  349. if boi.ImportStatement != nil {
  350. if err := boi.load(); err != nil {
  351. validationErrs = append(validationErrs, err)
  352. continue
  353. }
  354. result.Bots = append(result.Bots, boi.ImportStatement.Bots...)
  355. }
  356. if boi.BotConfig != nil {
  357. if err := boi.BotConfig.Valid(); err != nil {
  358. validationErrs = append(validationErrs, err)
  359. continue
  360. }
  361. result.Bots = append(result.Bots, *boi.BotConfig)
  362. }
  363. }
  364. if c.Impressum != nil {
  365. if err := c.Impressum.Valid(); err != nil {
  366. validationErrs = append(validationErrs, err)
  367. }
  368. result.Impressum = c.Impressum
  369. }
  370. if len(c.Thresholds) == 0 {
  371. c.Thresholds = DefaultThresholds
  372. }
  373. for _, t := range c.Thresholds {
  374. if err := t.Valid(); err != nil {
  375. validationErrs = append(validationErrs, err)
  376. continue
  377. }
  378. result.Thresholds = append(result.Thresholds, t)
  379. }
  380. if len(validationErrs) > 0 {
  381. return nil, fmt.Errorf("errors validating policy config %s: %w", fname, errors.Join(validationErrs...))
  382. }
  383. return result, nil
  384. }
  385. type DnsTTL struct {
  386. Forward int `json:"forward"`
  387. Reverse int `json:"reverse"`
  388. }
  389. func (sc DnsTTL) Valid() error {
  390. var errs []error
  391. if sc.Forward < 0 {
  392. errs = append(errs, fmt.Errorf("%w: forward TTL is %d", ErrStatusCodeNotValid, sc.Forward))
  393. }
  394. if sc.Reverse < 0 {
  395. errs = append(errs, fmt.Errorf("%w: reverse TTL is %d", ErrStatusCodeNotValid, sc.Reverse))
  396. }
  397. if len(errs) != 0 {
  398. return fmt.Errorf("dns TTL values not valid:\n%w", errors.Join(errs...))
  399. }
  400. return nil
  401. }
  402. type Config struct {
  403. Impressum *Impressum
  404. Store *Store
  405. OpenGraph OpenGraph
  406. Bots []BotConfig
  407. Thresholds []Threshold
  408. StatusCodes StatusCodes
  409. Logging *Logging
  410. DNSBL bool
  411. DNSTTL DnsTTL
  412. }
  413. func (c Config) Valid() error {
  414. var errs []error
  415. if len(c.Bots) == 0 {
  416. errs = append(errs, ErrNoBotRulesDefined)
  417. }
  418. for _, b := range c.Bots {
  419. if err := b.Valid(); err != nil {
  420. errs = append(errs, err)
  421. }
  422. }
  423. if len(errs) != 0 {
  424. return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
  425. }
  426. return nil
  427. }