ban.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. //
  2. // Copyright (C) 2017-2021 Marcus Rohrmoser, http://purl.mro.name/ShaarliGo
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. //
  17. package main
  18. import (
  19. "fmt"
  20. "io/ioutil"
  21. "log"
  22. "net"
  23. "net/http"
  24. "os"
  25. "path/filepath"
  26. "time"
  27. "gopkg.in/yaml.v2"
  28. )
  29. var banFileName string
  30. func init() {
  31. banFileName = filepath.Join(dirApp, "var", "bans.yaml")
  32. }
  33. type Penalty struct {
  34. Badness int
  35. End time.Time
  36. }
  37. type BanPenalties struct {
  38. Penalties map[string]Penalty
  39. }
  40. func remoteAddressToKey(addr string) string {
  41. h, _, _ := net.SplitHostPort(addr)
  42. return h
  43. }
  44. func isBanned(r *http.Request, now time.Time) (bool, error) {
  45. key := remoteAddressToKey(r.RemoteAddr)
  46. if data, err := ioutil.ReadFile(banFileName); err == nil || os.IsNotExist(err) {
  47. bans := BanPenalties{}
  48. if err := yaml.Unmarshal(data, &bans); err == nil {
  49. return bans.isRemoteAddrBanned(key, now), nil
  50. } else {
  51. return true, err
  52. }
  53. } else {
  54. return true, err
  55. }
  56. }
  57. func squealFailure(r *http.Request, now time.Time, reason string) error {
  58. key := remoteAddressToKey(r.RemoteAddr)
  59. var err error
  60. var data []byte
  61. if data, err = ioutil.ReadFile(banFileName); err == nil || os.IsNotExist(err) {
  62. bans := BanPenalties{Penalties: map[string]Penalty{}}
  63. if err = yaml.Unmarshal(data, &bans); err == nil {
  64. if bans.squealFailure(key, now, reason) {
  65. if data, err = yaml.Marshal(bans); err == nil {
  66. tmpFileName := fmt.Sprintf("%s~%d", banFileName, os.Getpid()) // want the mv to be atomic, so use the same dir
  67. if err = ioutil.WriteFile(tmpFileName, data, 0600); err == nil {
  68. err = os.Rename(tmpFileName, banFileName)
  69. }
  70. }
  71. }
  72. }
  73. }
  74. return err
  75. }
  76. const banThreshold = 4
  77. const banDuration = 4 * time.Hour
  78. func (bans BanPenalties) isRemoteAddrBanned(key string, now time.Time) bool {
  79. pen := bans.Penalties[key]
  80. if pen.Badness <= banThreshold { // allow for some failed tries
  81. return false
  82. }
  83. return pen.End.After(now)
  84. }
  85. func (bans *BanPenalties) squealFailure(key string, now time.Time, reason string) bool {
  86. pen := bans.Penalties[key]
  87. if pen.Badness < 0 {
  88. return false // we're known and welcome. So we do not ban.
  89. }
  90. if pen.End.Before(now) {
  91. pen.End = now
  92. }
  93. if pen.Badness > banThreshold && pen.End.After(now.Add(1*time.Hour)) {
  94. // already banned for more than an hour left, so don't bother adding to the ban.
  95. // But rather reduce I/O load a bit.
  96. return false
  97. }
  98. pen.End = pen.End.Add(banDuration)
  99. pen.Badness += 1
  100. bans.Penalties[key] = pen
  101. log.Printf("squeal %d %s %s", pen.Badness, key, reason)
  102. for ip, pen := range bans.Penalties {
  103. if pen.End.Before(now) {
  104. delete(bans.Penalties, ip)
  105. }
  106. }
  107. return true
  108. }