With this change, setting targetSNI to 'auto' causes anubis to use the request host name as the SNI name, allowing multiple sites to use the same anubis instance and same backend, while still securely connecting to the backend via https. See https://github.com/TecharoHQ/anubis/issues/424
577 lines
22 KiB
Go
577 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"embed"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/TecharoHQ/anubis"
|
|
"github.com/TecharoHQ/anubis/data"
|
|
"github.com/TecharoHQ/anubis/internal"
|
|
libanubis "github.com/TecharoHQ/anubis/lib"
|
|
botPolicy "github.com/TecharoHQ/anubis/lib/policy"
|
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
|
"github.com/TecharoHQ/anubis/lib/thoth"
|
|
"github.com/TecharoHQ/anubis/web"
|
|
"github.com/facebookgo/flagenv"
|
|
_ "github.com/joho/godotenv/autoload"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
|
)
|
|
|
|
var (
|
|
basePrefix = flag.String("base-prefix", "", "base prefix (root URL) the application is served under e.g. /myapp")
|
|
bind = flag.String("bind", ":8923", "network address to bind HTTP to")
|
|
bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
|
|
challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
|
|
cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for")
|
|
cookieDynamicDomain = flag.Bool("cookie-dynamic-domain", false, "if set, automatically set the cookie Domain value based on the request domain")
|
|
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
|
|
cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis")
|
|
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
|
difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims")
|
|
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.")
|
|
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
|
|
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
|
|
cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies")
|
|
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.")
|
|
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
|
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
|
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
|
metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to")
|
|
socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.")
|
|
robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots")
|
|
policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)")
|
|
redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.")
|
|
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
|
stripBasePrefix = flag.Bool("strip-base-prefix", false, "if true, strips the base prefix from requests forwarded to the target server")
|
|
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")
|
|
targetSNI = flag.String("target-sni", "", "if set, TLS handshake hostname when forwarding requests to the target, if set to auto, use Host header")
|
|
targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target")
|
|
targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend")
|
|
targetDisableKeepAlive = flag.Bool("target-disable-keepalive", false, "if true, disables HTTP keep-alive for the backend")
|
|
healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis")
|
|
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")
|
|
debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate")
|
|
ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough")
|
|
ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time")
|
|
ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache")
|
|
extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
|
|
webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
|
|
versionFlag = flag.Bool("version", false, "print Anubis version")
|
|
publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).")
|
|
xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For")
|
|
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)")
|
|
|
|
thothInsecure = flag.Bool("thoth-insecure", false, "if set, connect to Thoth over plain HTTP/2, don't enable this unless support told you to")
|
|
thothURL = flag.String("thoth-url", "", "if set, URL for Thoth, the IP reputation database for Anubis")
|
|
thothToken = flag.String("thoth-token", "", "if set, API token for Thoth, the IP reputation database for Anubis")
|
|
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")
|
|
)
|
|
|
|
func keyFromHex(value string) (ed25519.PrivateKey, error) {
|
|
keyBytes, err := hex.DecodeString(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("supplied key is not hex-encoded: %w", err)
|
|
}
|
|
|
|
if len(keyBytes) != ed25519.SeedSize {
|
|
return nil, fmt.Errorf("supplied key is not %d bytes long, got %d bytes", ed25519.SeedSize, len(keyBytes))
|
|
}
|
|
|
|
return ed25519.NewKeyFromSeed(keyBytes), nil
|
|
}
|
|
|
|
func doHealthCheck() error {
|
|
resp, err := http.Get("http://localhost" + *metricsBind + "/healthz")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch metrics: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseBindNetFromAddr determine bind network and address based on the given network and address.
|
|
func parseBindNetFromAddr(address string) (string, string) {
|
|
defaultScheme := "http://"
|
|
if !strings.Contains(address, "://") {
|
|
if strings.HasPrefix(address, ":") {
|
|
address = defaultScheme + "localhost" + address
|
|
} else {
|
|
address = defaultScheme + address
|
|
}
|
|
}
|
|
|
|
bindUri, err := url.Parse(address)
|
|
if err != nil {
|
|
log.Fatal(fmt.Errorf("failed to parse bind URL: %w", err))
|
|
}
|
|
|
|
switch bindUri.Scheme {
|
|
case "unix":
|
|
return "unix", bindUri.Path
|
|
case "tcp", "http", "https":
|
|
return "tcp", bindUri.Host
|
|
default:
|
|
log.Fatal(fmt.Errorf("unsupported network scheme %s in address %s", bindUri.Scheme, address))
|
|
}
|
|
return "", address
|
|
}
|
|
|
|
func parseSameSite(s string) (http.SameSite) {
|
|
switch strings.ToLower(s) {
|
|
case "none":
|
|
return http.SameSiteNoneMode
|
|
case "lax":
|
|
return http.SameSiteLaxMode
|
|
case "strict":
|
|
return http.SameSiteStrictMode
|
|
case "default":
|
|
return http.SameSiteDefaultMode
|
|
default:
|
|
log.Fatalf("invalid cookie same-site mode: %s, valid values are None, Lax, Strict, and Default", s)
|
|
}
|
|
return http.SameSiteDefaultMode
|
|
}
|
|
|
|
func setupListener(network string, address string) (net.Listener, string) {
|
|
formattedAddress := ""
|
|
|
|
if network == "" {
|
|
// keep compatibility
|
|
network, address = parseBindNetFromAddr(address)
|
|
}
|
|
|
|
switch network {
|
|
case "unix":
|
|
formattedAddress = "unix:" + address
|
|
case "tcp":
|
|
if strings.HasPrefix(address, ":") { // assume it's just a port e.g. :4259
|
|
formattedAddress = "http://localhost" + address
|
|
} else {
|
|
formattedAddress = "http://" + address
|
|
}
|
|
default:
|
|
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
|
|
}
|
|
|
|
listener, err := net.Listen(network, address)
|
|
if err != nil {
|
|
log.Fatal(fmt.Errorf("failed to bind to %s: %w", formattedAddress, err))
|
|
}
|
|
|
|
// additional permission handling for unix sockets
|
|
if network == "unix" {
|
|
mode, err := strconv.ParseUint(*socketMode, 8, 0)
|
|
if err != nil {
|
|
listener.Close()
|
|
log.Fatal(fmt.Errorf("could not parse socket mode %s: %w", *socketMode, err))
|
|
}
|
|
|
|
err = os.Chmod(address, os.FileMode(mode))
|
|
if err != nil {
|
|
err := listener.Close()
|
|
if err != nil {
|
|
log.Printf("failed to close listener: %v", err)
|
|
}
|
|
log.Fatal(fmt.Errorf("could not change socket mode: %w", err))
|
|
}
|
|
}
|
|
|
|
return listener, formattedAddress
|
|
}
|
|
|
|
func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool, targetDisableKeepAlive bool) (http.Handler, error) {
|
|
targetUri, err := url.Parse(target)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse target URL: %w", err)
|
|
}
|
|
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
|
|
if targetDisableKeepAlive {
|
|
transport.DisableKeepAlives = true
|
|
}
|
|
|
|
// https://github.com/oauth2-proxy/oauth2-proxy/blob/4e2100a2879ef06aea1411790327019c1a09217c/pkg/upstream/http.go#L124
|
|
if targetUri.Scheme == "unix" {
|
|
// clean path up so we don't use the socket path in proxied requests
|
|
addr := targetUri.Path
|
|
targetUri.Path = ""
|
|
// tell transport how to dial unix sockets
|
|
transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
dialer := net.Dialer{}
|
|
return dialer.DialContext(ctx, "unix", addr)
|
|
}
|
|
// tell transport how to handle the unix url scheme
|
|
transport.RegisterProtocol("unix", libanubis.UnixRoundTripper{Transport: transport})
|
|
}
|
|
|
|
if insecureSkipVerify || targetSNI != "" {
|
|
transport.TLSClientConfig = &tls.Config{}
|
|
}
|
|
if insecureSkipVerify {
|
|
slog.Warn("TARGET_INSECURE_SKIP_VERIFY is set to true, TLS certificate validation will not be performed", "target", target)
|
|
transport.TLSClientConfig.InsecureSkipVerify = true
|
|
}
|
|
if targetSNI != "" && targetSNI != "auto" {
|
|
transport.TLSClientConfig.ServerName = targetSNI
|
|
}
|
|
|
|
rp := httputil.NewSingleHostReverseProxy(targetUri)
|
|
rp.Transport = transport
|
|
|
|
if targetHost != "" || targetSNI == "auto" {
|
|
originalDirector := rp.Director
|
|
rp.Director = func(req *http.Request) {
|
|
originalDirector(req)
|
|
if targetHost != "" {
|
|
req.Host = targetHost
|
|
}
|
|
if targetSNI == "auto" {
|
|
transport.TLSClientConfig.ServerName = req.Host
|
|
}
|
|
}
|
|
}
|
|
|
|
return rp, nil
|
|
}
|
|
|
|
func main() {
|
|
flagenv.Parse()
|
|
flag.Parse()
|
|
|
|
if *versionFlag {
|
|
fmt.Println("Anubis", anubis.Version)
|
|
return
|
|
}
|
|
|
|
internal.InitSlog(*slogLevel)
|
|
internal.SetHealth("anubis", healthv1.HealthCheckResponse_NOT_SERVING)
|
|
|
|
if *healthcheck {
|
|
log.Println("running healthcheck")
|
|
if err := doHealthCheck(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if *extractResources != "" {
|
|
if err := extractEmbedFS(data.BotPolicies, ".", *extractResources); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if err := extractEmbedFS(web.Static, "static", *extractResources); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf("Extracted embedded static files to %s\n", *extractResources)
|
|
return
|
|
}
|
|
|
|
// install signal handler
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
wg := new(sync.WaitGroup)
|
|
|
|
if *metricsBind != "" {
|
|
wg.Add(1)
|
|
go metricsServer(ctx, wg.Done)
|
|
}
|
|
|
|
var rp http.Handler
|
|
// when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
|
|
if strings.TrimSpace(*target) != "" {
|
|
var err error
|
|
rp, err = makeReverseProxy(*target, *targetSNI, *targetHost, *targetInsecureSkipVerify, *targetDisableKeepAlive)
|
|
if err != nil {
|
|
log.Fatalf("can't make reverse proxy: %v", err)
|
|
}
|
|
}
|
|
|
|
if *cookieDomain != "" && *cookieDynamicDomain {
|
|
log.Fatalf("you can't set COOKIE_DOMAIN and COOKIE_DYNAMIC_DOMAIN at the same time")
|
|
}
|
|
|
|
// Thoth configuration
|
|
switch {
|
|
case *thothURL != "" && *thothToken == "":
|
|
slog.Warn("THOTH_URL is set but no THOTH_TOKEN is set")
|
|
case *thothURL == "" && *thothToken != "":
|
|
slog.Warn("THOTH_TOKEN is set but no THOTH_URL is set")
|
|
case *thothURL != "" && *thothToken != "":
|
|
slog.Debug("connecting to Thoth")
|
|
thothClient, err := thoth.New(ctx, *thothURL, *thothToken, *thothInsecure)
|
|
if err != nil {
|
|
log.Fatalf("can't dial thoth at %s: %v", *thothURL, err)
|
|
}
|
|
|
|
ctx = thoth.With(ctx, thothClient)
|
|
}
|
|
|
|
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty)
|
|
if err != nil {
|
|
log.Fatalf("can't parse policy file: %v", err)
|
|
}
|
|
|
|
// Warn if persistent storage is used without a configured signing key
|
|
if policy.Store.IsPersistent() {
|
|
if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" {
|
|
slog.Warn("[misconfiguration] persistent storage backend is configured, but no private key is set. " +
|
|
"Challenges will be invalidated when Anubis restarts. " +
|
|
"Set HS512_SECRET, ED25519_PRIVATE_KEY_HEX, or ED25519_PRIVATE_KEY_HEX_FILE to ensure challenges survive service restarts. " +
|
|
"See: https://anubis.techaro.lol/docs/admin/installation#key-generation")
|
|
}
|
|
}
|
|
|
|
ruleErrorIDs := make(map[string]string)
|
|
for _, rule := range policy.Bots {
|
|
if rule.Action != config.RuleDeny {
|
|
continue
|
|
}
|
|
|
|
hash := rule.Hash()
|
|
ruleErrorIDs[rule.Name] = hash
|
|
}
|
|
|
|
// replace the bot policy rules with a single rule that always benchmarks
|
|
if *debugBenchmarkJS {
|
|
policy.Bots = []botPolicy.Bot{{
|
|
Name: "",
|
|
Rules: botPolicy.NewHeaderExistsChecker("User-Agent"),
|
|
Action: config.RuleBenchmark,
|
|
}}
|
|
}
|
|
if *basePrefix != "" && !strings.HasPrefix(*basePrefix, "/") {
|
|
log.Fatalf("[misconfiguration] base-prefix must start with a slash, eg: /%s", *basePrefix)
|
|
} else if strings.HasSuffix(*basePrefix, "/") {
|
|
log.Fatalf("[misconfiguration] base-prefix must not end with a slash")
|
|
}
|
|
if *stripBasePrefix && *basePrefix == "" {
|
|
log.Fatalf("[misconfiguration] strip-base-prefix is set to true, but base-prefix is not set, " +
|
|
"this may result in unexpected behavior")
|
|
}
|
|
|
|
var ed25519Priv ed25519.PrivateKey
|
|
if *hs512Secret != "" && (*ed25519PrivateKeyHex != "" || *ed25519PrivateKeyHexFile != "") {
|
|
log.Fatal("do not specify both HS512 and ED25519 secrets")
|
|
} else if *hs512Secret != "" {
|
|
ed25519Priv = ed25519.PrivateKey(*hs512Secret)
|
|
} else if *ed25519PrivateKeyHex != "" && *ed25519PrivateKeyHexFile != "" {
|
|
log.Fatal("do not specify both ED25519_PRIVATE_KEY_HEX and ED25519_PRIVATE_KEY_HEX_FILE")
|
|
} else if *ed25519PrivateKeyHex != "" {
|
|
ed25519Priv, err = keyFromHex(*ed25519PrivateKeyHex)
|
|
if err != nil {
|
|
log.Fatalf("failed to parse and validate ED25519_PRIVATE_KEY_HEX: %v", err)
|
|
}
|
|
} else if *ed25519PrivateKeyHexFile != "" {
|
|
hexFile, err := os.ReadFile(*ed25519PrivateKeyHexFile)
|
|
if err != nil {
|
|
log.Fatalf("failed to read ED25519_PRIVATE_KEY_HEX_FILE %s: %v", *ed25519PrivateKeyHexFile, err)
|
|
}
|
|
|
|
ed25519Priv, err = keyFromHex(string(bytes.TrimSpace(hexFile)))
|
|
if err != nil {
|
|
log.Fatalf("failed to parse and validate content of ED25519_PRIVATE_KEY_HEX_FILE: %v", err)
|
|
}
|
|
} else {
|
|
_, ed25519Priv, err = ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
log.Fatalf("failed to generate ed25519 key: %v", err)
|
|
}
|
|
|
|
slog.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")
|
|
}
|
|
|
|
var redirectDomainsList []string
|
|
if *redirectDomains != "" {
|
|
domains := strings.Split(*redirectDomains, ",")
|
|
for _, domain := range domains {
|
|
_, err = url.Parse(domain)
|
|
if err != nil {
|
|
log.Fatalf("cannot parse redirect-domain %q: %s", domain, err.Error())
|
|
}
|
|
redirectDomainsList = append(redirectDomainsList, strings.TrimSpace(domain))
|
|
}
|
|
} else {
|
|
slog.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")
|
|
}
|
|
|
|
anubis.CookieName = *cookiePrefix + "-auth"
|
|
anubis.TestCookieName = *cookiePrefix + "-cookie-verification"
|
|
anubis.ForcedLanguage = *forcedLanguage
|
|
anubis.UseSimplifiedExplanation = *useSimplifiedExplanation
|
|
|
|
// If OpenGraph configuration values are not set in the config file, use the
|
|
// values from flags / envvars.
|
|
if !policy.OpenGraph.Enabled {
|
|
policy.OpenGraph.Enabled = *ogPassthrough
|
|
policy.OpenGraph.ConsiderHost = *ogCacheConsiderHost
|
|
policy.OpenGraph.TimeToLive = *ogTimeToLive
|
|
policy.OpenGraph.Override = map[string]string{}
|
|
}
|
|
|
|
s, err := libanubis.New(libanubis.Options{
|
|
BasePrefix: *basePrefix,
|
|
StripBasePrefix: *stripBasePrefix,
|
|
Next: rp,
|
|
Policy: policy,
|
|
ServeRobotsTXT: *robotsTxt,
|
|
ED25519PrivateKey: ed25519Priv,
|
|
HS512Secret: []byte(*hs512Secret),
|
|
CookieDomain: *cookieDomain,
|
|
CookieDynamicDomain: *cookieDynamicDomain,
|
|
CookieExpiration: *cookieExpiration,
|
|
CookiePartitioned: *cookiePartitioned,
|
|
RedirectDomains: redirectDomainsList,
|
|
Target: *target,
|
|
WebmasterEmail: *webmasterEmail,
|
|
OpenGraph: policy.OpenGraph,
|
|
CookieSecure: *cookieSecure,
|
|
CookieSameSite: parseSameSite(*cookieSameSite),
|
|
PublicUrl: *publicUrl,
|
|
JWTRestrictionHeader: *jwtRestrictionHeader,
|
|
DifficultyInJWT: *difficultyInJWT,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("can't construct libanubis.Server: %v", err)
|
|
}
|
|
|
|
var h http.Handler
|
|
h = s
|
|
h = internal.CustomRealIPHeader(*customRealIPHeader, h)
|
|
h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
|
|
h = internal.XForwardedForToXRealIP(h)
|
|
h = internal.XForwardedForUpdate(*xffStripPrivate, h)
|
|
h = internal.JA4H(h)
|
|
|
|
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
|
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
|
slog.Info(
|
|
"listening",
|
|
"url", listenerUrl,
|
|
"difficulty", *challengeDifficulty,
|
|
"serveRobotsTXT", *robotsTxt,
|
|
"target", *target,
|
|
"version", anubis.Version,
|
|
"use-remote-address", *useRemoteAddress,
|
|
"debug-benchmark-js", *debugBenchmarkJS,
|
|
"og-passthrough", *ogPassthrough,
|
|
"og-expiry-time", *ogTimeToLive,
|
|
"base-prefix", *basePrefix,
|
|
"cookie-expiration-time", *cookieExpiration,
|
|
"rule-error-ids", ruleErrorIDs,
|
|
"public-url", *publicUrl,
|
|
)
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(c); err != nil {
|
|
log.Printf("cannot shut down: %v", err)
|
|
}
|
|
}()
|
|
|
|
internal.SetHealth("anubis", healthv1.HealthCheckResponse_SERVING)
|
|
|
|
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
|
log.Fatal(err)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func metricsServer(ctx context.Context, done func()) {
|
|
defer done()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/metrics", promhttp.Handler())
|
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
|
st, ok := internal.GetHealth("anubis")
|
|
if !ok {
|
|
slog.Error("health service anubis does not exist, file a bug")
|
|
}
|
|
|
|
switch st {
|
|
case healthv1.HealthCheckResponse_NOT_SERVING:
|
|
http.Error(w, "NOT OK", http.StatusInternalServerError)
|
|
return
|
|
case healthv1.HealthCheckResponse_SERVING:
|
|
fmt.Fprintln(w, "OK")
|
|
return
|
|
default:
|
|
http.Error(w, "UNKNOWN", http.StatusFailedDependency)
|
|
return
|
|
}
|
|
})
|
|
|
|
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
|
|
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
|
|
slog.Debug("listening for metrics", "url", metricsUrl)
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(c); err != nil {
|
|
log.Printf("cannot shut down: %v", err)
|
|
}
|
|
}()
|
|
|
|
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func extractEmbedFS(fsys embed.FS, root string, destDir string) error {
|
|
return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
relPath, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
destPath := filepath.Join(destDir, root, relPath)
|
|
|
|
if d.IsDir() {
|
|
return os.MkdirAll(destPath, 0o700)
|
|
}
|
|
|
|
embeddedData, err := fs.ReadFile(fsys, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(destPath, embeddedData, 0o644)
|
|
})
|
|
}
|