main.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. package main
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/ed25519"
  6. "crypto/rand"
  7. "crypto/tls"
  8. "embed"
  9. "encoding/hex"
  10. "errors"
  11. "flag"
  12. "fmt"
  13. "io/fs"
  14. "log"
  15. "log/slog"
  16. "net"
  17. "net/http"
  18. "net/http/httputil"
  19. "net/url"
  20. "os"
  21. "os/signal"
  22. "path/filepath"
  23. "strconv"
  24. "strings"
  25. "sync"
  26. "syscall"
  27. "time"
  28. "github.com/TecharoHQ/anubis"
  29. "github.com/TecharoHQ/anubis/data"
  30. "github.com/TecharoHQ/anubis/internal"
  31. libanubis "github.com/TecharoHQ/anubis/lib"
  32. "github.com/TecharoHQ/anubis/lib/config"
  33. botPolicy "github.com/TecharoHQ/anubis/lib/policy"
  34. "github.com/TecharoHQ/anubis/lib/thoth"
  35. "github.com/TecharoHQ/anubis/web"
  36. "github.com/facebookgo/flagenv"
  37. _ "github.com/joho/godotenv/autoload"
  38. "github.com/prometheus/client_golang/prometheus/promhttp"
  39. healthv1 "google.golang.org/grpc/health/grpc_health_v1"
  40. )
  41. var (
  42. basePrefix = flag.String("base-prefix", "", "base prefix (root URL) the application is served under e.g. /myapp")
  43. bind = flag.String("bind", ":8923", "network address to bind HTTP to")
  44. bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
  45. challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
  46. cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for")
  47. cookieDynamicDomain = flag.Bool("cookie-dynamic-domain", false, "if set, automatically set the cookie Domain value based on the request domain")
  48. cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
  49. cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis")
  50. cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
  51. difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims")
  52. useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.")
  53. forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
  54. hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
  55. cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies")
  56. cookieSameSite = flag.String("cookie-same-site", "None", "sets the same site option on Anubis cookies, will auto-downgrade None to Lax if cookie-secure is false. Valid values are None, Lax, Strict, and Default.")
  57. ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
  58. ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
  59. metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
  60. metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to")
  61. socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.")
  62. robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots")
  63. policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)")
  64. redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.")
  65. slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
  66. stripBasePrefix = flag.Bool("strip-base-prefix", false, "if true, strips the base prefix from requests forwarded to the target server")
  67. target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request")
  68. targetSNI = flag.String("target-sni", "", "if set, TLS handshake hostname when forwarding requests to the target, if set to auto, use Host header")
  69. targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target")
  70. targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend")
  71. targetDisableKeepAlive = flag.Bool("target-disable-keepalive", false, "if true, disables HTTP keep-alive for the backend")
  72. healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis")
  73. useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal")
  74. debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate")
  75. ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough")
  76. ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time")
  77. ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache")
  78. extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
  79. webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
  80. versionFlag = flag.Bool("version", false, "print Anubis version")
  81. publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).")
  82. xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For")
  83. customRealIPHeader = flag.String("custom-real-ip-header", "", "if set, read remote IP from header of this name (in case your environment doesn't set X-Real-IP header)")
  84. thothInsecure = flag.Bool("thoth-insecure", false, "if set, connect to Thoth over plain HTTP/2, don't enable this unless support told you to")
  85. thothURL = flag.String("thoth-url", "", "if set, URL for Thoth, the IP reputation database for Anubis")
  86. thothToken = flag.String("thoth-token", "", "if set, API token for Thoth, the IP reputation database for Anubis")
  87. jwtRestrictionHeader = flag.String("jwt-restriction-header", "X-Real-IP", "If set, the JWT is only valid if the current value of this header matched the value when the JWT was created")
  88. )
  89. func keyFromHex(value string) (ed25519.PrivateKey, error) {
  90. keyBytes, err := hex.DecodeString(value)
  91. if err != nil {
  92. return nil, fmt.Errorf("supplied key is not hex-encoded: %w", err)
  93. }
  94. if len(keyBytes) != ed25519.SeedSize {
  95. return nil, fmt.Errorf("supplied key is not %d bytes long, got %d bytes", ed25519.SeedSize, len(keyBytes))
  96. }
  97. return ed25519.NewKeyFromSeed(keyBytes), nil
  98. }
  99. func doHealthCheck() error {
  100. resp, err := http.Get("http://localhost" + *metricsBind + "/healthz")
  101. if err != nil {
  102. return fmt.Errorf("failed to fetch metrics: %w", err)
  103. }
  104. defer resp.Body.Close()
  105. if resp.StatusCode != http.StatusOK {
  106. return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
  107. }
  108. return nil
  109. }
  110. // parseBindNetFromAddr determine bind network and address based on the given network and address.
  111. func parseBindNetFromAddr(address string) (string, string) {
  112. defaultScheme := "http://"
  113. if !strings.Contains(address, "://") {
  114. if strings.HasPrefix(address, ":") {
  115. address = defaultScheme + "localhost" + address
  116. } else {
  117. address = defaultScheme + address
  118. }
  119. }
  120. bindUri, err := url.Parse(address)
  121. if err != nil {
  122. log.Fatal(fmt.Errorf("failed to parse bind URL: %w", err))
  123. }
  124. switch bindUri.Scheme {
  125. case "unix":
  126. return "unix", bindUri.Path
  127. case "tcp", "http", "https":
  128. return "tcp", bindUri.Host
  129. default:
  130. log.Fatal(fmt.Errorf("unsupported network scheme %s in address %s", bindUri.Scheme, address))
  131. }
  132. return "", address
  133. }
  134. func parseSameSite(s string) http.SameSite {
  135. switch strings.ToLower(s) {
  136. case "none":
  137. return http.SameSiteNoneMode
  138. case "lax":
  139. return http.SameSiteLaxMode
  140. case "strict":
  141. return http.SameSiteStrictMode
  142. case "default":
  143. return http.SameSiteDefaultMode
  144. default:
  145. log.Fatalf("invalid cookie same-site mode: %s, valid values are None, Lax, Strict, and Default", s)
  146. }
  147. return http.SameSiteDefaultMode
  148. }
  149. func setupListener(network string, address string) (net.Listener, string) {
  150. formattedAddress := ""
  151. if network == "" {
  152. // keep compatibility
  153. network, address = parseBindNetFromAddr(address)
  154. }
  155. switch network {
  156. case "unix":
  157. formattedAddress = "unix:" + address
  158. case "tcp":
  159. if strings.HasPrefix(address, ":") { // assume it's just a port e.g. :4259
  160. formattedAddress = "http://localhost" + address
  161. } else {
  162. formattedAddress = "http://" + address
  163. }
  164. default:
  165. formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
  166. }
  167. listener, err := net.Listen(network, address)
  168. if err != nil {
  169. log.Fatal(fmt.Errorf("failed to bind to %s: %w", formattedAddress, err))
  170. }
  171. // additional permission handling for unix sockets
  172. if network == "unix" {
  173. mode, err := strconv.ParseUint(*socketMode, 8, 0)
  174. if err != nil {
  175. listener.Close()
  176. log.Fatal(fmt.Errorf("could not parse socket mode %s: %w", *socketMode, err))
  177. }
  178. err = os.Chmod(address, os.FileMode(mode))
  179. if err != nil {
  180. err := listener.Close()
  181. if err != nil {
  182. log.Printf("failed to close listener: %v", err)
  183. }
  184. log.Fatal(fmt.Errorf("could not change socket mode: %w", err))
  185. }
  186. }
  187. return listener, formattedAddress
  188. }
  189. func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool, targetDisableKeepAlive bool) (http.Handler, error) {
  190. targetUri, err := url.Parse(target)
  191. if err != nil {
  192. return nil, fmt.Errorf("failed to parse target URL: %w", err)
  193. }
  194. transport := http.DefaultTransport.(*http.Transport).Clone()
  195. if targetDisableKeepAlive {
  196. transport.DisableKeepAlives = true
  197. }
  198. // https://github.com/oauth2-proxy/oauth2-proxy/blob/4e2100a2879ef06aea1411790327019c1a09217c/pkg/upstream/http.go#L124
  199. if targetUri.Scheme == "unix" {
  200. // clean path up so we don't use the socket path in proxied requests
  201. addr := targetUri.Path
  202. targetUri.Path = ""
  203. // tell transport how to dial unix sockets
  204. transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
  205. dialer := net.Dialer{}
  206. return dialer.DialContext(ctx, "unix", addr)
  207. }
  208. // tell transport how to handle the unix url scheme
  209. transport.RegisterProtocol("unix", libanubis.UnixRoundTripper{Transport: transport})
  210. }
  211. if insecureSkipVerify || targetSNI != "" {
  212. transport.TLSClientConfig = &tls.Config{}
  213. }
  214. if insecureSkipVerify {
  215. slog.Warn("TARGET_INSECURE_SKIP_VERIFY is set to true, TLS certificate validation will not be performed", "target", target)
  216. transport.TLSClientConfig.InsecureSkipVerify = true
  217. }
  218. if targetSNI != "" && targetSNI != "auto" {
  219. transport.TLSClientConfig.ServerName = targetSNI
  220. }
  221. rp := httputil.NewSingleHostReverseProxy(targetUri)
  222. rp.Transport = transport
  223. if targetHost != "" || targetSNI == "auto" {
  224. originalDirector := rp.Director
  225. rp.Director = func(req *http.Request) {
  226. originalDirector(req)
  227. if targetHost != "" {
  228. req.Host = targetHost
  229. }
  230. if targetSNI == "auto" {
  231. transport.TLSClientConfig.ServerName = req.Host
  232. }
  233. }
  234. }
  235. return rp, nil
  236. }
  237. func main() {
  238. flagenv.Parse()
  239. flag.Parse()
  240. if *versionFlag {
  241. fmt.Println("Anubis", anubis.Version)
  242. return
  243. }
  244. internal.SetHealth("anubis", healthv1.HealthCheckResponse_NOT_SERVING)
  245. lg := internal.InitSlog(*slogLevel, os.Stderr)
  246. lg.Info("starting up Anubis")
  247. if *healthcheck {
  248. log.Println("running healthcheck")
  249. if err := doHealthCheck(); err != nil {
  250. log.Fatal(err)
  251. }
  252. return
  253. }
  254. if *extractResources != "" {
  255. if err := extractEmbedFS(data.BotPolicies, ".", *extractResources); err != nil {
  256. log.Fatal(err)
  257. }
  258. if err := extractEmbedFS(web.Static, "static", *extractResources); err != nil {
  259. log.Fatal(err)
  260. }
  261. fmt.Printf("Extracted embedded static files to %s\n", *extractResources)
  262. return
  263. }
  264. // install signal handler
  265. ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  266. defer stop()
  267. wg := new(sync.WaitGroup)
  268. if *metricsBind != "" {
  269. wg.Add(1)
  270. go metricsServer(ctx, *lg.With("subsystem", "metrics"), wg.Done)
  271. }
  272. var rp http.Handler
  273. // when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
  274. if strings.TrimSpace(*target) != "" {
  275. var err error
  276. rp, err = makeReverseProxy(*target, *targetSNI, *targetHost, *targetInsecureSkipVerify, *targetDisableKeepAlive)
  277. if err != nil {
  278. log.Fatalf("can't make reverse proxy: %v", err)
  279. }
  280. }
  281. if *cookieDomain != "" && *cookieDynamicDomain {
  282. log.Fatalf("you can't set COOKIE_DOMAIN and COOKIE_DYNAMIC_DOMAIN at the same time")
  283. }
  284. // Thoth configuration
  285. switch {
  286. case *thothURL != "" && *thothToken == "":
  287. lg.Warn("THOTH_URL is set but no THOTH_TOKEN is set")
  288. case *thothURL == "" && *thothToken != "":
  289. lg.Warn("THOTH_TOKEN is set but no THOTH_URL is set")
  290. case *thothURL != "" && *thothToken != "":
  291. lg.Debug("connecting to Thoth")
  292. thothClient, err := thoth.New(ctx, *thothURL, *thothToken, *thothInsecure)
  293. if err != nil {
  294. log.Fatalf("can't dial thoth at %s: %v", *thothURL, err)
  295. }
  296. ctx = thoth.With(ctx, thothClient)
  297. }
  298. lg.Info("loading policy file", "fname", *policyFname)
  299. policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel)
  300. if err != nil {
  301. log.Fatalf("can't parse policy file: %v", err)
  302. }
  303. lg = policy.Logger
  304. lg.Debug("swapped to new logger")
  305. slog.SetDefault(lg)
  306. // Warn if persistent storage is used without a configured signing key
  307. if policy.Store.IsPersistent() {
  308. if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" {
  309. lg.Warn("[misconfiguration] persistent storage backend is configured, but no private key is set. " +
  310. "Challenges will be invalidated when Anubis restarts. " +
  311. "Set HS512_SECRET, ED25519_PRIVATE_KEY_HEX, or ED25519_PRIVATE_KEY_HEX_FILE to ensure challenges survive service restarts. " +
  312. "See: https://anubis.techaro.lol/docs/admin/installation#key-generation")
  313. }
  314. }
  315. ruleErrorIDs := make(map[string]string)
  316. for _, rule := range policy.Bots {
  317. if rule.Action != config.RuleDeny {
  318. continue
  319. }
  320. hash := rule.Hash()
  321. ruleErrorIDs[rule.Name] = hash
  322. }
  323. // replace the bot policy rules with a single rule that always benchmarks
  324. if *debugBenchmarkJS {
  325. policy.Bots = []botPolicy.Bot{{
  326. Name: "",
  327. Rules: botPolicy.NewHeaderExistsChecker("User-Agent"),
  328. Action: config.RuleBenchmark,
  329. }}
  330. }
  331. if *basePrefix != "" && !strings.HasPrefix(*basePrefix, "/") {
  332. log.Fatalf("[misconfiguration] base-prefix must start with a slash, eg: /%s", *basePrefix)
  333. } else if strings.HasSuffix(*basePrefix, "/") {
  334. log.Fatalf("[misconfiguration] base-prefix must not end with a slash")
  335. }
  336. if *stripBasePrefix && *basePrefix == "" {
  337. log.Fatalf("[misconfiguration] strip-base-prefix is set to true, but base-prefix is not set, " +
  338. "this may result in unexpected behavior")
  339. }
  340. var ed25519Priv ed25519.PrivateKey
  341. if *hs512Secret != "" && (*ed25519PrivateKeyHex != "" || *ed25519PrivateKeyHexFile != "") {
  342. log.Fatal("do not specify both HS512 and ED25519 secrets")
  343. } else if *hs512Secret != "" {
  344. ed25519Priv = ed25519.PrivateKey(*hs512Secret)
  345. } else if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
  346. log.Fatal("do not specify both ED25519_PRIVATE_KEY_HEX and ED25519_PRIVATE_KEY_HEX_FILE")
  347. } else if *ed25519PrivateKeyHex != "" {
  348. ed25519Priv, err = keyFromHex(*ed25519PrivateKeyHex)
  349. if err != nil {
  350. log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err)
  351. }
  352. } else if *ed25519PrivateKeyHexFile != "" {
  353. hexFile, err := os.ReadFile(*ed25519PrivateKeyHexFile)
  354. if err != nil {
  355. log.Fatalf("failed to read ED25519_PRIVATE_KEY_HEX_FILE %s: %v", *ed25519PrivateKeyHexFile, err)
  356. }
  357. ed25519Priv, err = keyFromHex(string(bytes.TrimSpace(hexFile)))
  358. if err != nil {
  359. log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err)
  360. }
  361. } else {
  362. _, ed25519Priv, err = ed25519.GenerateKey(rand.Reader)
  363. if err != nil {
  364. log.Fatalf("failed to generate ed25519 key: %v", err)
  365. }
  366. lg.Warn("generating random key, Anubis will have strange behavior when multiple instances are behind the same load balancer target, for more information: see https://anubis.techaro.lol/docs/admin/installation#key-generation")
  367. }
  368. var redirectDomainsList []string
  369. if *redirectDomains != "" {
  370. domains := strings.Split(*redirectDomains, ",")
  371. for _, domain := range domains {
  372. _, err = url.Parse(domain)
  373. if err != nil {
  374. log.Fatalf("cannot parse redirect-domain %q: %s", domain, err.Error())
  375. }
  376. redirectDomainsList = append(redirectDomainsList, strings.TrimSpace(domain))
  377. }
  378. } else {
  379. lg.Warn("REDIRECT_DOMAINS is not set, Anubis will only redirect to the same domain a request is coming from, see https://anubis.techaro.lol/docs/admin/configuration/redirect-domains")
  380. }
  381. anubis.CookieName = *cookiePrefix + "-auth"
  382. anubis.TestCookieName = *cookiePrefix + "-cookie-verification"
  383. anubis.ForcedLanguage = *forcedLanguage
  384. anubis.UseSimplifiedExplanation = *useSimplifiedExplanation
  385. // If OpenGraph configuration values are not set in the config file, use the
  386. // values from flags / envvars.
  387. if !policy.OpenGraph.Enabled {
  388. policy.OpenGraph.Enabled = *ogPassthrough
  389. policy.OpenGraph.ConsiderHost = *ogCacheConsiderHost
  390. policy.OpenGraph.TimeToLive = *ogTimeToLive
  391. policy.OpenGraph.Override = map[string]string{}
  392. }
  393. s, err := libanubis.New(libanubis.Options{
  394. BasePrefix: *basePrefix,
  395. StripBasePrefix: *stripBasePrefix,
  396. Next: rp,
  397. Policy: policy,
  398. TargetHost: *targetHost,
  399. TargetSNI: *targetSNI,
  400. TargetInsecureSkipVerify: *targetInsecureSkipVerify,
  401. ServeRobotsTXT: *robotsTxt,
  402. ED25519PrivateKey: ed25519Priv,
  403. HS512Secret: []byte(*hs512Secret),
  404. CookieDomain: *cookieDomain,
  405. CookieDynamicDomain: *cookieDynamicDomain,
  406. CookieExpiration: *cookieExpiration,
  407. CookiePartitioned: *cookiePartitioned,
  408. RedirectDomains: redirectDomainsList,
  409. Target: *target,
  410. WebmasterEmail: *webmasterEmail,
  411. OpenGraph: policy.OpenGraph,
  412. CookieSecure: *cookieSecure,
  413. CookieSameSite: parseSameSite(*cookieSameSite),
  414. PublicUrl: *publicUrl,
  415. JWTRestrictionHeader: *jwtRestrictionHeader,
  416. Logger: policy.Logger.With("subsystem", "anubis"),
  417. DifficultyInJWT: *difficultyInJWT,
  418. })
  419. if err != nil {
  420. log.Fatalf("can't construct libanubis.Server: %v", err)
  421. }
  422. var h http.Handler
  423. h = s
  424. h = internal.CustomRealIPHeader(*customRealIPHeader, h)
  425. h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
  426. h = internal.XForwardedForToXRealIP(h)
  427. h = internal.XForwardedForUpdate(*xffStripPrivate, h)
  428. h = internal.JA4H(h)
  429. srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
  430. listener, listenerUrl := setupListener(*bindNetwork, *bind)
  431. lg.Info(
  432. "listening",
  433. "url", listenerUrl,
  434. "difficulty", *challengeDifficulty,
  435. "serveRobotsTXT", *robotsTxt,
  436. "target", *target,
  437. "version", anubis.Version,
  438. "use-remote-address", *useRemoteAddress,
  439. "debug-benchmark-js", *debugBenchmarkJS,
  440. "og-passthrough", *ogPassthrough,
  441. "og-expiry-time", *ogTimeToLive,
  442. "base-prefix", *basePrefix,
  443. "cookie-expiration-time", *cookieExpiration,
  444. "rule-error-ids", ruleErrorIDs,
  445. "public-url", *publicUrl,
  446. )
  447. go func() {
  448. <-ctx.Done()
  449. c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  450. defer cancel()
  451. if err := srv.Shutdown(c); err != nil {
  452. log.Printf("cannot shut down: %v", err)
  453. }
  454. }()
  455. internal.SetHealth("anubis", healthv1.HealthCheckResponse_SERVING)
  456. if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
  457. log.Fatal(err)
  458. }
  459. wg.Wait()
  460. }
  461. func metricsServer(ctx context.Context, lg slog.Logger, done func()) {
  462. defer done()
  463. mux := http.NewServeMux()
  464. mux.Handle("/metrics", promhttp.Handler())
  465. mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
  466. st, ok := internal.GetHealth("anubis")
  467. if !ok {
  468. slog.Error("health service anubis does not exist, file a bug")
  469. }
  470. switch st {
  471. case healthv1.HealthCheckResponse_NOT_SERVING:
  472. http.Error(w, "NOT OK", http.StatusInternalServerError)
  473. return
  474. case healthv1.HealthCheckResponse_SERVING:
  475. fmt.Fprintln(w, "OK")
  476. return
  477. default:
  478. http.Error(w, "UNKNOWN", http.StatusFailedDependency)
  479. return
  480. }
  481. })
  482. srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
  483. listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
  484. lg.Debug("listening for metrics", "url", metricsUrl)
  485. go func() {
  486. <-ctx.Done()
  487. c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  488. defer cancel()
  489. if err := srv.Shutdown(c); err != nil {
  490. log.Printf("cannot shut down: %v", err)
  491. }
  492. }()
  493. if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
  494. log.Fatal(err)
  495. }
  496. }
  497. func extractEmbedFS(fsys embed.FS, root string, destDir string) error {
  498. return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
  499. if err != nil {
  500. return err
  501. }
  502. relPath, err := filepath.Rel(root, path)
  503. if err != nil {
  504. return err
  505. }
  506. destPath := filepath.Join(destDir, root, relPath)
  507. if d.IsDir() {
  508. return os.MkdirAll(destPath, 0o700)
  509. }
  510. embeddedData, err := fs.ReadFile(fsys, path)
  511. if err != nil {
  512. return err
  513. }
  514. return os.WriteFile(destPath, embeddedData, 0o644)
  515. })
  516. }