Add nested rules Add backend action, allow wildcard in backends Remove poison from tree, update README with action table Allow defining pass/fail actions on challenge, Remove redirect/referer parameters on backend pass Set challenge cookie tied to host Rewrite DNSBL condition into a challenge Allow passing an arbitrary path for assets to js challenges Optimize programs exhaustively on compilation Activation instead of map for CEL context, faster map access, new network override Return valid host on cookie setting in case Host is an IP address. bug: does not work with IPv6, see https://github.com/golang/go/issues/65521 Apply TLS fingerprinter on GetConfigForClient instead of GetCertificate Cleanup go-away cookies before passing to backend Code action for specifically replying with an HTTP code
172 lines
4.5 KiB
Go
172 lines
4.5 KiB
Go
package challenge
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"git.gammaspectra.live/git/go-away/lib/condition"
|
|
"git.gammaspectra.live/git/go-away/utils"
|
|
"github.com/google/cel-go/cel"
|
|
"github.com/google/cel-go/common/types"
|
|
"github.com/google/cel-go/common/types/traits"
|
|
"net"
|
|
"net/http"
|
|
"net/textproto"
|
|
"time"
|
|
)
|
|
|
|
type requestDataContextKey struct {
|
|
}
|
|
|
|
func RequestDataFromContext(ctx context.Context) *RequestData {
|
|
return ctx.Value(requestDataContextKey{}).(*RequestData)
|
|
}
|
|
|
|
type RequestId [16]byte
|
|
|
|
func (id RequestId) String() string {
|
|
return hex.EncodeToString(id[:])
|
|
}
|
|
|
|
type RequestData struct {
|
|
Id RequestId
|
|
Time time.Time
|
|
ChallengeVerify map[Id]VerifyResult
|
|
ChallengeState map[Id]VerifyState
|
|
RemoteAddress net.IP
|
|
State StateInterface
|
|
|
|
r *http.Request
|
|
|
|
fp map[string]string
|
|
header traits.Mapper
|
|
query traits.Mapper
|
|
}
|
|
|
|
func CreateRequestData(r *http.Request, state StateInterface) (*http.Request, *RequestData) {
|
|
|
|
var data RequestData
|
|
// generate random id, todo: is this fast?
|
|
_, _ = rand.Read(data.Id[:])
|
|
data.RemoteAddress = utils.GetRequestAddress(r, state.Settings().ClientIpHeader)
|
|
data.ChallengeVerify = make(map[Id]VerifyResult, len(state.GetChallenges()))
|
|
data.ChallengeState = make(map[Id]VerifyState, len(state.GetChallenges()))
|
|
data.Time = time.Now().UTC()
|
|
data.State = state
|
|
data.r = r
|
|
|
|
data.fp = make(map[string]string, 2)
|
|
|
|
if fp := utils.GetTLSFingerprint(r); fp != nil {
|
|
if ja3nPtr := fp.JA3N(); ja3nPtr != nil {
|
|
ja3n := ja3nPtr.String()
|
|
data.fp["ja3n"] = ja3n
|
|
r.Header.Set("X-TLS-Fingerprint-JA3N", ja3n)
|
|
}
|
|
if ja4Ptr := fp.JA4(); ja4Ptr != nil {
|
|
ja4 := ja4Ptr.String()
|
|
data.fp["ja4"] = ja4
|
|
r.Header.Set("X-TLS-Fingerprint-JA4", ja4)
|
|
}
|
|
}
|
|
|
|
data.query = condition.NewValuesMap(r.URL.Query())
|
|
data.header = condition.NewMIMEMap(textproto.MIMEHeader(r.Header))
|
|
|
|
r = r.WithContext(context.WithValue(r.Context(), requestDataContextKey{}, &data))
|
|
|
|
return r, &data
|
|
}
|
|
|
|
func (d *RequestData) ResolveName(name string) (any, bool) {
|
|
switch name {
|
|
case "host":
|
|
return d.r.Host, true
|
|
case "method":
|
|
return d.r.Method, true
|
|
case "remoteAddress":
|
|
return d.RemoteAddress, true
|
|
case "userAgent":
|
|
return d.r.UserAgent(), true
|
|
case "path":
|
|
return d.r.URL.Path, true
|
|
case "query":
|
|
return d.query, true
|
|
case "headers":
|
|
return d.header, true
|
|
case "fp":
|
|
return d.fp, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
func (d *RequestData) Parent() cel.Activation {
|
|
return nil
|
|
}
|
|
|
|
func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request) {
|
|
for _, reg := range d.State.GetChallenges() {
|
|
key := GetChallengeKeyForRequest(d.State, reg, d.Expiration(reg.Duration), r)
|
|
verifyResult, verifyState, err := reg.VerifyChallengeToken(d.State.PublicKey(), key, r)
|
|
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
|
// clear invalid cookie
|
|
utils.ClearCookie(utils.CookiePrefix+reg.Name, w, r)
|
|
}
|
|
|
|
// prevent evaluating the challenge if not solved
|
|
if !verifyResult.Ok() && reg.Condition != nil {
|
|
out, _, err := reg.Condition.Eval(d)
|
|
// verify eligibility
|
|
if err != nil {
|
|
d.State.Logger(r).Error(err.Error(), "challenge", reg.Name)
|
|
} else if out != nil && out.Type() == types.BoolType {
|
|
if out.Equal(types.True) != types.True {
|
|
// skip challenge match due to precondition!
|
|
verifyResult = VerifyResultSkip
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
d.ChallengeVerify[reg.Id()] = verifyResult
|
|
d.ChallengeState[reg.Id()] = verifyState
|
|
}
|
|
|
|
if d.State.Settings().BackendIpHeader != "" {
|
|
if d.State.Settings().ClientIpHeader != "" {
|
|
r.Header.Del(d.State.Settings().ClientIpHeader)
|
|
}
|
|
r.Header.Set(d.State.Settings().BackendIpHeader, d.RemoteAddress.String())
|
|
}
|
|
|
|
// send these to client so we consistently get the headers
|
|
//w.Header().Set("Accept-CH", "Sec-CH-UA, Sec-CH-UA-Platform")
|
|
//w.Header().Set("Critical-CH", "Sec-CH-UA, Sec-CH-UA-Platform")
|
|
}
|
|
|
|
func (d *RequestData) Expiration(duration time.Duration) time.Time {
|
|
return d.Time.Add(duration).Round(duration)
|
|
}
|
|
|
|
func (d *RequestData) HasValidChallenge(id Id) bool {
|
|
return d.ChallengeVerify[id].Ok()
|
|
}
|
|
|
|
func (d *RequestData) Headers(headers http.Header) {
|
|
headers.Set("X-Away-Id", d.Id.String())
|
|
|
|
for id, result := range d.ChallengeVerify {
|
|
if result.Ok() {
|
|
c, ok := d.State.GetChallenge(id)
|
|
if !ok {
|
|
panic("challenge not found")
|
|
}
|
|
|
|
headers.Set(fmt.Sprintf("X-Away-Challenge-%s-Result", c.Name), result.String())
|
|
headers.Set(fmt.Sprintf("X-Away-Challenge-%s-State", c.Name), d.ChallengeState[id].String())
|
|
}
|
|
}
|
|
}
|