Condition, rules, state and action refactor / rewrite

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
This commit is contained in:
WeebDataHoarder
2025-04-19 00:42:18 +02:00
committed by WeebDataHoarder
parent 1c7fe1bed9
commit ead41055ca
58 changed files with 3258 additions and 2048 deletions

158
lib/challenge/http/http.go Normal file
View File

@@ -0,0 +1,158 @@
package http
import (
"crypto/sha256"
"crypto/subtle"
"errors"
"git.gammaspectra.live/git/go-away/lib/challenge"
"git.gammaspectra.live/git/go-away/utils"
"github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/ast"
"io"
"net/http"
"slices"
"time"
)
func init() {
challenge.Runtimes[Key] = FillRegistration
}
const Key = "http"
type Parameters struct {
VerifyProbability float64 `yaml:"verify-probability"`
HttpMethod string `yaml:"http-method"`
HttpCode int `yaml:"http-code"`
HttpCookie string `yaml:"http-cookie"`
Url string `yaml:"http-url"`
}
var DefaultParameters = Parameters{
VerifyProbability: 0.20,
HttpMethod: http.MethodGet,
HttpCode: http.StatusOK,
}
func FillRegistration(state challenge.StateInterface, reg *challenge.Registration, parameters ast.Node) error {
params := DefaultParameters
if parameters != nil {
ymlData, err := parameters.MarshalYAML()
if err != nil {
return err
}
err = yaml.Unmarshal(ymlData, &params)
if err != nil {
return err
}
}
if params.Url == "" {
return errors.New("empty url")
}
reg.Class = challenge.ClassTransparent
bindAuthValue := func(key challenge.Key, r *http.Request) ([]byte, error) {
var cookieValue string
if cookie, err := r.Cookie(params.HttpCookie); err != nil || cookie == nil {
// skip check if we don't have cookie or it's expired
return nil, http.ErrNoCookie
} else {
cookieValue = cookie.Value
}
// bind hash of cookie contents
sum := sha256.New()
sum.Write([]byte(cookieValue))
sum.Write([]byte{0})
sum.Write(key[:])
return sum.Sum(nil), nil
}
if params.VerifyProbability <= 0 {
//20% default
params.VerifyProbability = 0.20
} else if params.VerifyProbability > 1.0 {
params.VerifyProbability = 1.0
}
reg.VerifyProbability = params.VerifyProbability
if params.HttpCookie != "" {
// re-verify the cookie value
// TODO: configure to verify with backend
reg.Verify = func(key challenge.Key, token []byte, r *http.Request) (challenge.VerifyResult, error) {
sum, err := bindAuthValue(key, r)
if err != nil {
return challenge.VerifyResultFail, err
}
if subtle.ConstantTimeCompare(sum, token) == 1 {
return challenge.VerifyResultOK, nil
}
return challenge.VerifyResultFail, errors.New("invalid cookie value")
}
}
reg.IssueChallenge = func(w http.ResponseWriter, r *http.Request, key challenge.Key, expiry time.Time) challenge.VerifyResult {
var sum []byte
if params.HttpCookie != "" {
if c, err := r.Cookie(params.HttpCookie); err != nil || c == nil {
// skip check if we don't have cookie or it's expired
return challenge.VerifyResultSkip
} else {
sum, err = bindAuthValue(key, r)
if err != nil {
return challenge.VerifyResultFail
}
}
}
request, err := http.NewRequest(params.HttpMethod, params.Url, nil)
if err != nil {
return challenge.VerifyResultFail
}
var excludeHeaders = []string{"Host", "Content-Length"}
for k, v := range r.Header {
if slices.Contains(excludeHeaders, k) {
// skip these parameters
continue
}
request.Header[k] = v
}
// set id
request.Header.Set("X-Away-Id", challenge.RequestDataFromContext(r.Context()).Id.String())
// set request info in X headers
request.Header.Set("X-Away-Host", r.Host)
request.Header.Set("X-Away-Path", r.URL.Path)
request.Header.Set("X-Away-Query", r.URL.RawQuery)
response, err := state.Client().Do(request)
if err != nil {
return challenge.VerifyResultFail
}
defer response.Body.Close()
defer io.Copy(io.Discard, response.Body)
if response.StatusCode != params.HttpCode {
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, false)
if err != nil {
return challenge.VerifyResultFail
}
utils.SetCookie(utils.CookiePrefix+reg.Name, token, expiry, w, r)
return challenge.VerifyResultNotOK
} else {
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, true)
if err != nil {
return challenge.VerifyResultFail
}
utils.SetCookie(utils.CookiePrefix+reg.Name, token, expiry, w, r)
return challenge.VerifyResultOK
}
}
return nil
}