15 Commits

Author SHA1 Message Date
WeebDataHoarder
6e53bc224a bind/tls: allow configuring min/max TLS version, ciphers, and curves 2025-06-18 16:08:35 +02:00
WeebDataHoarder
c1cb81e758 bind/tls: allow specifying multiple tls entries 2025-06-18 15:44:12 +02:00
WeebDataHoarder
9a6f25df59 http/query: preserve raw query state when modifying url query 2025-06-09 13:49:37 +02:00
Alan Orth
c16f0863ae examples/generic.yml: use path.matches in condition
The string here uses a character set with path.contains, which will
not work in CEL. We need to use path.matches to use regex syntax.
2025-05-17 23:50:36 +03:00
Alan Orth
85a8f0d9ec examples: remove erroneous whitespace 2025-05-17 23:45:39 +03:00
WeebDataHoarder
a5e2e6625b cmd: move http/backend error logs to debug level 2025-05-17 18:55:48 +02:00
WeebDataHoarder
d24e4b521a examples/snippets: add CGNAT range to networks-private 2025-05-14 21:12:48 +02:00
WeebDataHoarder
3ac6b9d366 cmd/go-away: log private key fingerprint on load 2025-05-14 01:30:48 +02:00
WeebDataHoarder
484a5e3535 challenge/context: clear cookies by issuing a new cookie instead of clearing it 2025-05-14 01:30:31 +02:00
WeebDataHoarder
6032ac0b78 http: add cache-control headers to prevent caching by other proxies elsewhere 2025-05-13 23:48:21 +02:00
WeebDataHoarder
163fce6cfc challenge/resource-load: use proper redirect URL to current issued challenge, add static/dynamic cache bust 2025-05-13 23:43:31 +02:00
WeebDataHoarder
3abdc2ee5b examples: add private / localhost networks to snippets and forgejo/generic examples 2025-05-13 03:06:23 +02:00
WeebDataHoarder
3b045e9608 state/template: fix not allowing external templates to be defined 2025-05-08 12:14:01 +02:00
WeebDataHoarder
1d2f4e8a5b challenge/context: use additional HTTP headers in challenge key generation if the challenge allows for it 2025-05-04 20:22:34 +02:00
Alan Orth
c6a1d50f39 examples/config.yml: fix YAML syntax 2025-05-04 12:25:44 +03:00
21 changed files with 320 additions and 59 deletions

View File

@@ -210,7 +210,7 @@ func main() {
fatal(fmt.Errorf("backend %s: failed to make reverse proxy: %w", k, err))
}
backend.ErrorLog = slog.NewLogLogger(slog.With("backend", k).Handler(), slog.LevelError)
backend.ErrorLog = slog.NewLogLogger(slog.With("backend", k).Handler(), slog.LevelDebug)
createdBackends[k] = backend
}
@@ -291,7 +291,7 @@ func main() {
fatal(fmt.Errorf("failed to create server: %w", err))
}
server.ErrorLog = slog.NewLogLogger(slog.With("server", "http").Handler(), slog.LevelError)
server.ErrorLog = slog.NewLogLogger(slog.With("server", "http").Handler(), slog.LevelDebug)
go func() {
handler, err := loadPolicyState()
@@ -302,6 +302,7 @@ func main() {
swap(handler)
slog.Warn(
"handler configuration loaded",
"key_fingerprint", hex.EncodeToString(handler.PrivateKeyFingerprint()),
)
// allow reloading from now on
@@ -336,7 +337,7 @@ func main() {
debugServer := http.Server{
Addr: opt.BindDebug,
Handler: mux,
ErrorLog: slog.NewLogLogger(slog.With("server", "debug").Handler(), slog.LevelError),
ErrorLog: slog.NewLogLogger(slog.With("server", "debug").Handler(), slog.LevelDebug),
}
slog.Warn(
@@ -356,7 +357,7 @@ func main() {
metricsServer := http.Server{
Addr: opt.BindMetrics,
Handler: mux,
ErrorLog: slog.NewLogLogger(slog.With("server", "metrics").Handler(), slog.LevelError),
ErrorLog: slog.NewLogLogger(slog.With("server", "metrics").Handler(), slog.LevelDebug),
}
slog.Warn(

View File

@@ -24,7 +24,7 @@ bind:
#bind-debug: ":6060"
# Bind the Prometheus metrics onto /metrics path on this port
#bind-metrics ":9090"
#bind-metrics: ":9090"
# These links will be shown on the presented challenge or error pages
links:

View File

@@ -104,6 +104,15 @@ rules:
- *is-bot-yandexbot
action: pass
# Matches private networks and localhost.
# Uncomment this if you want to let your own tools this way
# - name: allow-private-networks
# conditions:
# # Allows localhost and private networks CIDR
# - *is-network-localhost
# - *is-network-private
# action: pass
- name: undesired-networks
conditions:
- 'remoteAddress.network("huawei-cloud") || remoteAddress.network("alibaba-cloud") || remoteAddress.network("zenlayer-inc")'
@@ -287,7 +296,7 @@ rules:
# Map OpenGraph or similar <meta> tags back to the reply, even if denied/challenged
proxy-meta-tags: "true"
# proxy-safe-link-tags: "true"
# Set additional response headers
#response-headers:
# X-Clacks-Overhead:

View File

@@ -10,7 +10,7 @@ networks:
challenges:
# Challenges will get included from snippets
conditions:
# Conditions will get replaced on rules AST when found as ($condition-name)
@@ -27,7 +27,7 @@ conditions:
# Old IE browsers
- 'userAgent.matches("MSIE ([2-9]|10|11)\\.")'
# Old Linux browsers
- 'userAgent.contains("Linux i[63]86") || userAgent.contains("FreeBSD i[63]86")'
- 'userAgent.matches("Linux i[63]86") || userAgent.matches("FreeBSD i[63]86")'
# Old Windows browsers
- 'userAgent.matches("Windows (3|95|98|CE)") || userAgent.matches("Windows NT [1-5]\\.")'
# Old mobile browsers
@@ -60,6 +60,15 @@ rules:
- *is-bot-yandexbot
action: pass
# Matches private networks and localhost.
# Uncomment this if you want to let your own tools this way
# - name: allow-private-networks
# conditions:
# # Allows localhost and private networks CIDR
# - *is-network-localhost
# - *is-network-private
# action: pass
- name: undesired-crawlers
conditions:
- '($is-headless-chromium)'

View File

@@ -0,0 +1,22 @@
networks:
localhost:
# localhost and loopback addresses
- prefixes:
- "127.0.0.0/8"
- "::1/128"
private:
# Private network CIDR blocks
- prefixes:
# private networks
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
- "fc00::/7"
# CGNAT
- "100.64.0.0/10"
conditions:
is-network-localhost:
- &is-network-localhost 'remoteAddress.network("localhost")'
is-network-private:
- &is-network-private 'remoteAddress.network("private")'

View File

@@ -28,7 +28,9 @@ func (a Block) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Reques
data := challenge.RequestDataFromContext(r.Context())
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Connection", "close")
data.ResponseHeaders(w)
w.WriteHeader(a.Code)
_, _ = w.Write([]byte(fmt.Errorf("access blocked: blocked by administrative rule %s/%s", data.Id.String(), a.RuleHash).Error()))

View File

@@ -42,7 +42,11 @@ type CodeSettings struct {
type Code int
func (a Code) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request, done func() (backend http.Handler)) (next bool, err error) {
challenge.RequestDataFromContext(r.Context()).ResponseHeaders(w)
data := challenge.RequestDataFromContext(r.Context())
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
data.ResponseHeaders(w)
w.WriteHeader(int(a))
return false, nil

View File

@@ -33,6 +33,8 @@ func (a Drop) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "0")
w.Header().Set("Connection", "close")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.WriteHeader(http.StatusForbidden)
return false, nil

View File

@@ -295,8 +295,8 @@ func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request)
challengeMap, err := d.verifyChallengeState()
if err != nil {
if !errors.Is(err, http.ErrNoCookie) {
//clear invalid cookie and continue
utils.ClearCookie(d.cookieName, w, r)
//queue resend invalid cookie and continue
d.challengeMapModified = true
}
challengeMap = make(TokenChallengeMap)
}
@@ -329,6 +329,9 @@ func (d *RequestData) ResponseHeaders(w http.ResponseWriter) {
//w.Header().Set("Accept-CH", "Sec-CH-UA, Sec-CH-UA-Platform")
//w.Header().Set("Critical-CH", "Sec-CH-UA, Sec-CH-UA-Platform")
// send Vary header to mark that response may vary based on Cookie values and other client headers
w.Header().Set("Vary", "Cookie, Accept, Accept-Encoding, Accept-Language, User-Agent")
if d.State.Settings().MainName != "" {
w.Header().Add("Via", fmt.Sprintf("%s %s@%s", d.r.Proto, d.State.Settings().MainName, d.State.Settings().MainVersion))
}

View File

@@ -8,7 +8,9 @@ import (
"git.gammaspectra.live/git/go-away/utils"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
var ErrInvalidToken = errors.New("invalid token")
@@ -47,6 +49,7 @@ const (
QueryArgRequestId = QueryArgPrefix + "_id"
QueryArgChallenge = QueryArgPrefix + "_challenge"
QueryArgToken = QueryArgPrefix + "_token"
QueryArgBust = QueryArgPrefix + "_bust"
)
const MakeChallengeUrlSuffix = "/make-challenge"
@@ -91,12 +94,13 @@ func VerifyUrl(r *http.Request, reg *Registration, token string) (*url.URL, erro
uri.Path = reg.Path + VerifyChallengeUrlSuffix
data := RequestDataFromContext(r.Context())
values := uri.Query()
values.Set(QueryArgRequestId, data.Id.String())
values.Set(QueryArgRedirect, redirectUrl.String())
values.Set(QueryArgToken, token)
values.Set(QueryArgChallenge, reg.Name)
uri.RawQuery = values.Encode()
values, _ := utils.ParseRawQuery(r.URL.RawQuery)
values.Set(QueryArgRequestId, url.QueryEscape(data.Id.String()))
values.Set(QueryArgRedirect, url.QueryEscape(redirectUrl.String()))
values.Set(QueryArgToken, url.QueryEscape(token))
values.Set(QueryArgChallenge, url.QueryEscape(reg.Name))
values.Set(QueryArgBust, url.QueryEscape(strconv.FormatInt(time.Now().UTC().UnixMilli(), 10)))
uri.RawQuery = utils.EncodeRawQuery(values)
return uri, nil
}
@@ -108,13 +112,13 @@ func RedirectUrl(r *http.Request, reg *Registration) (*url.URL, error) {
}
data := RequestDataFromContext(r.Context())
values := uri.Query()
values.Set(QueryArgRequestId, data.Id.String())
values, _ := utils.ParseRawQuery(r.URL.RawQuery)
values.Set(QueryArgRequestId, url.QueryEscape(data.Id.String()))
if ref := r.Referer(); ref != "" {
values.Set(QueryArgReferer, r.Referer())
values.Set(QueryArgReferer, url.QueryEscape(r.Referer()))
}
values.Set(QueryArgChallenge, reg.Name)
uri.RawQuery = values.Encode()
values.Set(QueryArgChallenge, url.QueryEscape(reg.Name))
uri.RawQuery = utils.EncodeRawQuery(values)
return uri, nil
}

View File

@@ -52,15 +52,13 @@ func GetChallengeKeyForRequest(state StateInterface, reg *Registration, until ti
hasher.Write([]byte{0})
// specific headers
for _, k := range []string{
"Accept-Language",
// General browser information
"User-Agent",
// TODO: not sent in preload
//"Sec-Ch-Ua",
//"Sec-Ch-Ua-Platform",
} {
hasher.Write([]byte(r.Header.Get(k)))
for _, k := range reg.KeyHeaders {
hasher.Write([]byte(k))
hasher.Write([]byte{0})
for _, v := range r.Header.Values(k) {
hasher.Write([]byte(v))
hasher.Write([]byte{1})
}
hasher.Write([]byte{0})
}
hasher.Write([]byte{0})

View File

@@ -44,6 +44,9 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
reg.Class = challenge.ClassTransparent
// some of regular headers are not sent in default headers
reg.KeyHeaders = challenge.MinimalKeyHeaders
ob := challenge.NewAwaiter[string]()
reg.Object = ob
@@ -66,9 +69,9 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
}
// remove redirect args
values := uri.Query()
values, _ := utils.ParseRawQuery(uri.RawQuery)
values.Del(challenge.QueryArgRedirect)
uri.RawQuery = values.Encode()
uri.RawQuery = utils.EncodeRawQuery(values)
// Redirect URI must be absolute to work
uri.Scheme = utils.GetRequestScheme(r)
@@ -98,6 +101,7 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
mux.HandleFunc("GET "+reg.Path+challenge.VerifyChallengeUrlSuffix, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Content-Length", "0")
data := challenge.RequestDataFromContext(r.Context())

View File

@@ -35,6 +35,24 @@ var idCounter Id
// DefaultDuration TODO: adjust
const DefaultDuration = time.Hour * 24 * 7
var DefaultKeyHeaders = []string{
// General browser information
"User-Agent",
// Accept headers
"Accept-Language",
"Accept-Encoding",
// NOTE: not sent in preload
"Sec-Ch-Ua",
"Sec-Ch-Ua-Platform",
}
var MinimalKeyHeaders = []string{
"Accept-Language",
// General browser information
"User-Agent",
}
func (r Register) Create(state StateInterface, name string, pol policy.Challenge, replacer *strings.Replacer) (*Registration, Id, error) {
runtime, ok := Runtimes[pol.Runtime]
if !ok {
@@ -42,9 +60,10 @@ func (r Register) Create(state StateInterface, name string, pol policy.Challenge
}
reg := &Registration{
Name: name,
Path: path.Join(state.UrlPath(), "challenge", name),
Duration: pol.Duration,
Name: name,
Path: path.Join(state.UrlPath(), "challenge", name),
Duration: pol.Duration,
KeyHeaders: DefaultKeyHeaders,
}
if reg.Duration == 0 {
@@ -126,6 +145,9 @@ type Registration struct {
Verify VerifyFunc
VerifyProbability float64
// KeyHeaders The client headers used in key generation, in this order
KeyHeaders []string
// IssueChallenge Issues a challenge to a request.
// If Class is ClassTransparent and VerifyResult is !VerifyResult.Ok(), continue with other challenges
// TODO: have this return error as well

View File

@@ -23,9 +23,13 @@ func FillRegistrationHeader(state challenge.StateInterface, reg *challenge.Regis
return challenge.VerifyResultFail
}
redirectUri, err := challenge.RedirectUrl(r, reg)
if err != nil {
return challenge.VerifyResultFail
}
// self redirect!
//TODO: adjust deadline
w.Header().Set("Refresh", "2; url="+r.URL.String())
w.Header().Set("Refresh", "2; url="+redirectUri.String())
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
"LinkTags": []map[string]string{
@@ -44,6 +48,7 @@ func FillRegistrationHeader(state challenge.StateInterface, reg *challenge.Regis
mux.HandleFunc("GET "+reg.Path+challenge.VerifyChallengeUrlSuffix, challenge.VerifyHandlerFunc(state, reg, nil, func(state challenge.StateInterface, data *challenge.RequestData, w http.ResponseWriter, r *http.Request, verifyResult challenge.VerifyResult, err error, redirect string) {
//TODO: add other types inside css that need to be loaded!
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Content-Length", "0")
data.ResponseHeaders(w)

View File

@@ -23,6 +23,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat
//TODO: log
panic(err)
}
data.ResponseHeaders(w)
w.WriteHeader(http.StatusOK)
@@ -30,7 +31,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat
"Id": data.Id.String(),
"Path": reg.Path,
"Parameters": paramData,
"Random": utils.CacheBust(),
"Random": utils.StaticCacheBust(),
"Challenge": reg.Name,
"ChallengeScript": script,
"Strings": data.State.Strings(),

View File

@@ -97,7 +97,7 @@ func FillJavaScriptRegistration(state challenge.StateInterface, reg *challenge.R
reg.IssueChallenge = func(w http.ResponseWriter, r *http.Request, key challenge.Key, expiry time.Time) challenge.VerifyResult {
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
"EndTags": []template.HTML{
template.HTML(fmt.Sprintf("<script async type=\"module\" src=\"%s?cacheBust=%s\"></script>", reg.Path+"/script.mjs", utils.CacheBust())),
template.HTML(fmt.Sprintf("<script async type=\"module\" src=\"%s?cacheBust=%s\"></script>", reg.Path+"/script.mjs", utils.StaticCacheBust())),
},
})
return challenge.VerifyResultNone
@@ -164,6 +164,9 @@ func FillJavaScriptRegistration(state challenge.StateInterface, reg *challenge.R
w.Header()[k] = v
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out.Data)))
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
data.ResponseHeaders(w)
w.WriteHeader(out.Code)
_, _ = w.Write(out.Data)
return nil

View File

@@ -246,19 +246,20 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
if fromChallenge {
r.Header.Del("Referer")
}
q := r.URL.Query()
q := r.URL.Query()
if ref := q.Get(challenge.QueryArgReferer); ref != "" {
r.Header.Set("Referer", ref)
}
rawQ, _ := utils.ParseRawQuery(r.URL.RawQuery)
// delete query parameters that were set by go-away
for k := range q {
for k := range rawQ {
if strings.HasPrefix(k, challenge.QueryArgPrefix) {
q.Del(k)
rawQ.Del(k)
}
}
r.URL.RawQuery = q.Encode()
r.URL.RawQuery = utils.EncodeRawQuery(rawQ)
data.ExtraHeaders.Set("X-Away-Rule", ruleName)
data.ExtraHeaders.Set("X-Away-Action", string(ruleAction))

View File

@@ -13,10 +13,18 @@ import (
"net/http"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
)
type TLSEntry struct {
// Certificate Path to the certificate file
Certificate string `yaml:"certificate"`
// Key Path to the corresponding key file
Key string `yaml:"key"`
}
type Bind struct {
Address string `yaml:"address"`
Network string `yaml:"network"`
@@ -28,11 +36,35 @@ type Bind struct {
// TLSAcmeAutoCert URL to ACME directory, or letsencrypt
TLSAcmeAutoCert string `yaml:"tls-acme-autocert"`
// TLSCertificate Alternate to TLSAcmeAutoCert
// TLSEntries Alternate to TLSAcmeAutoCert. Allows multiple entries with matching.
// Entries on this list can be live-reloaded if application implements SIGHUP handling
TLSEntries []TLSEntry `yaml:"tls-entries"`
// TLSCertificate Alternate to TLSAcmeAutoCert. Preferred over TLSEntries if specified.
TLSCertificate string `yaml:"tls-certificate"`
// TLSPrivateKey Alternate to TLSAcmeAutoCert
// TLSPrivateKey Alternate to TLSAcmeAutoCert. Preferred over TLSEntries if specified.
TLSPrivateKey string `yaml:"tls-key"`
// General TLS config
// TLSMinVersion TLS Minimum supported version.
// Default is Golang's default, at writing time it's TLS 1.2. Lowest supported is TLS 1.0
TLSMinVersion string `yaml:"tls-min-version"`
// TLSMaxVersion TLS Maximum supported version.
// Default is Golang's default, at writing time it's TLS 1.3, and is automatically increased.
// Lowest supported is TLS 1.2
TLSMaxVersion string `yaml:"tls-max-version"`
// TLSCurves List of supported TLS curve ids from Golang internals
// See this list https://github.com/golang/go/blob/go1.24.0/src/crypto/tls/common.go#L138-L153 for supported values
// Default values are chosen by Golang. It's recommended to leave the default
TLSCurves []tls.CurveID `yaml:"tls-curves"`
// TLSCiphers List of supported TLS ciphers from Golang internals, case sensitive. TLS 1.3 suites are not configurable.
// See this list https://github.com/golang/go/blob/go1.24.0/src/crypto/tls/cipher_suites.go#L56-L73 for supported values
// Default values are chosen by Golang. It's recommended to leave the default
TLSCiphers []string `yaml:"tls-ciphers"`
// ReadTimeout is the maximum duration for reading the entire
// request, including the body. A zero or negative value means
// there will be no timeout.
@@ -104,6 +136,105 @@ func (b *Bind) Server(backends map[string]http.Handler, acmeCachePath string) (*
"TLS enabled",
"certificate", b.TLSCertificate,
)
} else if len(b.TLSEntries) > 0 {
tlsConfig = &tls.Config{}
var err error
var certificatesPtr atomic.Pointer[[]tls.Certificate]
swapTls := func() error {
certs := make([]tls.Certificate, 0, len(b.TLSEntries))
for _, entry := range b.TLSEntries {
cert, err := tls.LoadX509KeyPair(entry.Certificate, entry.Key)
if err != nil {
return fmt.Errorf("failed to load TLS certificate %s: %w", entry.Certificate, err)
}
certs = append(certs, cert)
}
certificatesPtr.Swap(&certs)
return nil
}
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
certs := certificatesPtr.Load()
if certs == nil || len(*certs) == 0 {
panic("no certificates found")
}
for _, cert := range *certs {
if err := clientHello.SupportsCertificate(&cert); err == nil {
return &cert, nil
}
}
// if none match, return first
return &(*certs)[0], nil
}
err = swapTls()
if err != nil {
return nil, nil, err
}
slog.Warn(
"TLS enabled with multiple certificates",
"certificates", len(b.TLSEntries),
)
}
if tlsConfig != nil {
if b.TLSMinVersion != "" {
switch strings.NewReplacer("-", "", "_", "", " ", "", ".", "").Replace(strings.ToLower(b.TLSMinVersion)) {
case "13", "tls13":
tlsConfig.MinVersion = tls.VersionTLS13
case "12", "tls12":
tlsConfig.MinVersion = tls.VersionTLS12
case "11", "tls11":
tlsConfig.MinVersion = tls.VersionTLS11
case "10", "tls10":
tlsConfig.MinVersion = tls.VersionTLS10
default:
return nil, nil, fmt.Errorf("unsupported minimum TLS version: %s", b.TLSMinVersion)
}
}
if b.TLSMaxVersion != "" {
switch strings.NewReplacer("-", "", "_", "", " ", "", ".", "").Replace(strings.ToLower(b.TLSMaxVersion)) {
case "13", "tls13":
tlsConfig.MaxVersion = tls.VersionTLS13
case "12", "tls12":
tlsConfig.MaxVersion = tls.VersionTLS12
default:
return nil, nil, fmt.Errorf("unsupported maximum TLS version: %s", b.TLSMinVersion)
}
}
if len(b.TLSCiphers) > 0 {
for _, cipher := range b.TLSCiphers {
if c := func() *tls.CipherSuite {
for _, c := range tls.CipherSuites() {
if c.Name == cipher {
return c
}
}
for _, c := range tls.InsecureCipherSuites() {
if c.Name == cipher {
return c
}
}
return nil
}(); c != nil {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
} else {
return nil, nil, fmt.Errorf("unsupported TLS cipher suite: %s", cipher)
}
}
}
if len(b.TLSCurves) > 0 {
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, b.TLSCurves...)
}
}
var serverHandler atomic.Pointer[http.Handler]

View File

@@ -114,9 +114,9 @@ func NewState(p policy.Policy, opt settings.Settings, settings policy.StateSetti
return nil, fmt.Errorf("error loading template %s: %w", state.opt.ChallengeTemplate, err)
}
state.opt.ChallengeTemplate = name
} else {
return nil, fmt.Errorf("no template defined for %s", state.opt.ChallengeTemplate)
}
return nil, fmt.Errorf("no template defined for %s", state.opt.ChallengeTemplate)
}
state.networks = make(map[string]func() cidranger.Ranger)

View File

@@ -78,7 +78,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
data := challenge.RequestDataFromContext(r.Context())
input := make(map[string]any)
input["Id"] = data.Id.String()
input["Random"] = utils.CacheBust()
input["Random"] = utils.StaticCacheBust()
input["Path"] = state.UrlPath()
input["Links"] = state.opt.Links
@@ -100,6 +100,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
state.addCachedTags(data, r, input)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
buf := bytes.NewBuffer(make([]byte, 0, 8192))
@@ -116,12 +117,13 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int, err error, redirect string) {
data := challenge.RequestDataFromContext(r.Context())
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
buf := bytes.NewBuffer(make([]byte, 0, 8192))
input := map[string]any{
"Id": data.Id.String(),
"Random": utils.CacheBust(),
"Random": utils.StaticCacheBust(),
"Error": err.Error(),
"Path": state.UrlPath(),
"Theme": "",

View File

@@ -7,11 +7,13 @@ import (
"encoding/base64"
"errors"
"fmt"
"maps"
"net"
"net/http"
"net/http/httputil"
"net/netip"
"net/url"
"slices"
"strings"
"time"
)
@@ -167,15 +169,51 @@ func GetRemoteAddress(ctx context.Context) *netip.AddrPort {
return &ip
}
func CacheBust() string {
return cacheBust
}
var cacheBust string
func init() {
buf := make([]byte, 16)
func RandomCacheBust(n int) string {
buf := make([]byte, n)
_, _ = rand.Read(buf)
cacheBust = base64.RawURLEncoding.EncodeToString(buf)
return base64.RawURLEncoding.EncodeToString(buf)
}
var staticCacheBust = RandomCacheBust(16)
func StaticCacheBust() string {
return staticCacheBust
}
func ParseRawQuery(rawQuery string) (m url.Values, err error) {
m = make(url.Values)
for rawQuery != "" {
var key string
key, rawQuery, _ = strings.Cut(rawQuery, "&")
if strings.Contains(key, ";") {
err = fmt.Errorf("invalid semicolon separator in query")
continue
}
if key == "" {
continue
}
key, value, _ := strings.Cut(key, "=")
m[key] = append(m[key], value)
}
return m, err
}
func EncodeRawQuery(v url.Values) string {
if len(v) == 0 {
return ""
}
var buf strings.Builder
for _, k := range slices.Sorted(maps.Keys(v)) {
vs := v[k]
for _, v := range vs {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
buf.WriteString(v)
}
}
return buf.String()
}