config.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. package lib
  2. import (
  3. "context"
  4. "crypto/ed25519"
  5. "crypto/rand"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "log/slog"
  10. "net/http"
  11. "os"
  12. "strings"
  13. "time"
  14. "github.com/TecharoHQ/anubis"
  15. "github.com/TecharoHQ/anubis/data"
  16. "github.com/TecharoHQ/anubis/internal"
  17. "github.com/TecharoHQ/anubis/internal/honeypot/naive"
  18. "github.com/TecharoHQ/anubis/internal/ogtags"
  19. "github.com/TecharoHQ/anubis/lib/challenge"
  20. "github.com/TecharoHQ/anubis/lib/config"
  21. "github.com/TecharoHQ/anubis/lib/localization"
  22. "github.com/TecharoHQ/anubis/lib/policy"
  23. "github.com/TecharoHQ/anubis/web"
  24. "github.com/TecharoHQ/anubis/xess"
  25. "github.com/a-h/templ"
  26. )
  27. type Options struct {
  28. Next http.Handler
  29. Policy *policy.ParsedConfig
  30. Target string
  31. TargetHost string
  32. TargetSNI string
  33. TargetInsecureSkipVerify bool
  34. CookieDynamicDomain bool
  35. CookieDomain string
  36. CookieExpiration time.Duration
  37. CookiePartitioned bool
  38. BasePrefix string
  39. WebmasterEmail string
  40. RedirectDomains []string
  41. ED25519PrivateKey ed25519.PrivateKey
  42. HS512Secret []byte
  43. StripBasePrefix bool
  44. OpenGraph config.OpenGraph
  45. ServeRobotsTXT bool
  46. CookieSecure bool
  47. CookieSameSite http.SameSite
  48. Logger *slog.Logger
  49. LogLevel string
  50. PublicUrl string
  51. JWTRestrictionHeader string
  52. DifficultyInJWT bool
  53. }
  54. func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string) (*policy.ParsedConfig, error) {
  55. var fin io.ReadCloser
  56. var err error
  57. if fname != "" {
  58. fin, err = os.Open(fname)
  59. if err != nil {
  60. return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
  61. }
  62. } else {
  63. fname = "(data)/botPolicies.yaml"
  64. fin, err = data.BotPolicies.Open("botPolicies.yaml")
  65. if err != nil {
  66. return nil, fmt.Errorf("[unexpected] can't parse builtin policy file %s: %w", fname, err)
  67. }
  68. }
  69. defer func(fin io.ReadCloser) {
  70. err := fin.Close()
  71. if err != nil {
  72. slog.Error("failed to close policy file", "file", fname, "err", err)
  73. }
  74. }(fin)
  75. anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel)
  76. if err != nil {
  77. return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
  78. }
  79. var validationErrs []error
  80. for _, b := range anubisPolicy.Bots {
  81. if _, ok := challenge.Get(b.Challenge.Algorithm); !ok {
  82. validationErrs = append(validationErrs, fmt.Errorf("%w %s", policy.ErrChallengeRuleHasWrongAlgorithm, b.Challenge.Algorithm))
  83. }
  84. }
  85. if len(validationErrs) != 0 {
  86. return nil, fmt.Errorf("can't do final validation of Anubis config: %w", errors.Join(validationErrs...))
  87. }
  88. return anubisPolicy, err
  89. }
  90. func New(opts Options) (*Server, error) {
  91. if opts.Logger == nil {
  92. opts.Logger = slog.With("subsystem", "anubis")
  93. }
  94. if opts.ED25519PrivateKey == nil && opts.HS512Secret == nil {
  95. opts.Logger.Debug("opts.PrivateKey not set, generating a new one")
  96. _, priv, err := ed25519.GenerateKey(rand.Reader)
  97. if err != nil {
  98. return nil, fmt.Errorf("lib: can't generate private key: %v", err)
  99. }
  100. opts.ED25519PrivateKey = priv
  101. }
  102. anubis.BasePrefix = strings.TrimRight(opts.BasePrefix, "/")
  103. anubis.PublicUrl = opts.PublicUrl
  104. result := &Server{
  105. next: opts.Next,
  106. ed25519Priv: opts.ED25519PrivateKey,
  107. hs512Secret: opts.HS512Secret,
  108. policy: opts.Policy,
  109. opts: opts,
  110. OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store, ogtags.TargetOptions{
  111. Host: opts.TargetHost,
  112. SNI: opts.TargetSNI,
  113. InsecureSkipVerify: opts.TargetInsecureSkipVerify,
  114. }),
  115. store: opts.Policy.Store,
  116. logger: opts.Logger,
  117. }
  118. mux := http.NewServeMux()
  119. xess.Mount(mux)
  120. // Helper to add global prefix
  121. registerWithPrefix := func(pattern string, handler http.Handler, method string) {
  122. if method != "" {
  123. method = method + " " // methods must end with a space to register with them
  124. }
  125. // Ensure there's no double slash when concatenating BasePrefix and pattern
  126. basePrefix := strings.TrimSuffix(anubis.BasePrefix, "/")
  127. prefix := method + basePrefix
  128. // If pattern doesn't start with a slash, add one
  129. if !strings.HasPrefix(pattern, "/") {
  130. pattern = "/" + pattern
  131. }
  132. mux.Handle(prefix+pattern, handler)
  133. }
  134. // Ensure there's no double slash when concatenating BasePrefix and StaticPath
  135. stripPrefix := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath
  136. registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "")
  137. if opts.ServeRobotsTXT {
  138. registerWithPrefix("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  139. http.ServeFileFS(w, r, web.Static, "static/robots.txt")
  140. }), "GET")
  141. registerWithPrefix("/.well-known/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  142. http.ServeFileFS(w, r, web.Static, "static/robots.txt")
  143. }), "GET")
  144. }
  145. if opts.Policy.Impressum != nil {
  146. registerWithPrefix(anubis.APIPrefix+"imprint", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  147. templ.Handler(
  148. web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)),
  149. ).ServeHTTP(w, r)
  150. }), "GET")
  151. }
  152. registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
  153. registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
  154. registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
  155. mazeGen, err := naive.New(result.store, result.logger)
  156. if err == nil {
  157. registerWithPrefix(anubis.APIPrefix+"honeypot/{id}/{stage}", mazeGen, http.MethodGet)
  158. opts.Policy.Bots = append(
  159. opts.Policy.Bots,
  160. policy.Bot{
  161. Rules: mazeGen.CheckNetwork(),
  162. Action: config.RuleWeigh,
  163. Weight: &config.Weight{
  164. Adjust: 30,
  165. },
  166. Name: "honeypot/network",
  167. },
  168. policy.Bot{
  169. Rules: mazeGen.CheckUA(),
  170. Action: config.RuleWeigh,
  171. Weight: &config.Weight{
  172. Adjust: 30,
  173. },
  174. Name: "honeypot/user-agent",
  175. },
  176. )
  177. } else {
  178. result.logger.Error("can't init honeypot subsystem", "err", err)
  179. }
  180. //goland:noinspection GoBoolExpressions
  181. if anubis.Version == "devel" {
  182. // make-challenge is only used in tests. Only enable while version is devel
  183. registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
  184. }
  185. for _, implKind := range challenge.Methods() {
  186. impl, _ := challenge.Get(implKind)
  187. impl.Setup(mux)
  188. }
  189. result.mux = mux
  190. return result, nil
  191. }