Minor cleanup of challenges code, bind session http cookies to issued tokens
This commit is contained in:
@@ -382,6 +382,7 @@ rules:
|
|||||||
conditions:
|
conditions:
|
||||||
- 'path == "/"'
|
- 'path == "/"'
|
||||||
# generic /*/*/ match gave too many options for scrapers to trigger random endpoints
|
# generic /*/*/ match gave too many options for scrapers to trigger random endpoints
|
||||||
|
# edit this with preferential users/orgs for now
|
||||||
# todo: create negative match?
|
# todo: create negative match?
|
||||||
- 'path.matches("(?i)^/(WeebDataHoarder|P2Pool|mirror|git|S\\.O\\.N\\.G|FM10K|Sillycom|pwgen2155|kaitou|metonym)/[^/]+$")'
|
- 'path.matches("(?i)^/(WeebDataHoarder|P2Pool|mirror|git|S\\.O\\.N\\.G|FM10K|Sillycom|pwgen2155|kaitou|metonym)/[^/]+$")'
|
||||||
action: pass
|
action: pass
|
||||||
|
111
lib/challenge.go
111
lib/challenge.go
@@ -1,14 +1,9 @@
|
|||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/go-jose/go-jose/v4"
|
|
||||||
"github.com/go-jose/go-jose/v4/jwt"
|
"github.com/go-jose/go-jose/v4/jwt"
|
||||||
"math/rand/v2"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -44,10 +39,10 @@ func getRequestAddress(r *http.Request, clientHeader string) net.IP {
|
|||||||
return net.ParseIP(ipStr)
|
return net.ParseIP(ipStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *State) GetChallengeKeyForRequest(name string, until time.Time, r *http.Request) []byte {
|
func (state *State) GetChallengeKeyForRequest(challengeName string, until time.Time, r *http.Request) []byte {
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
hasher.Write([]byte("challenge\x00"))
|
hasher.Write([]byte("challenge\x00"))
|
||||||
hasher.Write([]byte(name))
|
hasher.Write([]byte(challengeName))
|
||||||
hasher.Write([]byte{0})
|
hasher.Write([]byte{0})
|
||||||
hasher.Write(getRequestAddress(r, state.Settings.ClientIpHeader).To16())
|
hasher.Write(getRequestAddress(r, state.Settings.ClientIpHeader).To16())
|
||||||
hasher.Write([]byte{0})
|
hasher.Write([]byte{0})
|
||||||
@@ -71,105 +66,3 @@ func (state *State) GetChallengeKeyForRequest(name string, until time.Time, r *h
|
|||||||
|
|
||||||
return hasher.Sum(nil)
|
return hasher.Sum(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *State) IssueChallengeToken(name string, key, result []byte, until time.Time) (token string, err error) {
|
|
||||||
signer, err := jose.NewSigner(jose.SigningKey{
|
|
||||||
Algorithm: jose.EdDSA,
|
|
||||||
Key: state.privateKey,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
expiry := jwt.NumericDate(until.Unix())
|
|
||||||
notBefore := jwt.NumericDate(time.Now().UTC().AddDate(0, 0, -1).Unix())
|
|
||||||
issuedAt := jwt.NumericDate(time.Now().UTC().Unix())
|
|
||||||
|
|
||||||
token, err = jwt.Signed(signer).Claims(ChallengeInformation{
|
|
||||||
Name: name,
|
|
||||||
Key: key,
|
|
||||||
Result: result,
|
|
||||||
Expiry: &expiry,
|
|
||||||
NotBefore: ¬Before,
|
|
||||||
IssuedAt: &issuedAt,
|
|
||||||
}).Serialize()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (state *State) VerifyChallengeToken(name string, expectedKey []byte, w http.ResponseWriter, r *http.Request) (ok bool, err error) {
|
|
||||||
c, ok := state.Challenges[name]
|
|
||||||
if !ok {
|
|
||||||
return false, errors.New("challenge not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie, err := r.Cookie(CookiePrefix + name)
|
|
||||||
if err != nil {
|
|
||||||
// fallback: fetch cookie from response
|
|
||||||
// TODO: implemented in go1.23, port back
|
|
||||||
/*
|
|
||||||
if setCookies, ok := w.Header()["Set-Cookie"]; ok {
|
|
||||||
for _, setCookie := range setCookies {
|
|
||||||
newCookie, err := http.ParseSetCookie(setCookie)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// keep processing to find last set cookie
|
|
||||||
if newCookie.Name == name {
|
|
||||||
cookie = newCookie
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if cookie == nil {
|
|
||||||
return false, http.ErrNoCookie
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := jwt.ParseSigned(cookie.Value, []jose.SignatureAlgorithm{jose.EdDSA})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var i ChallengeInformation
|
|
||||||
err = token.Claims(state.publicKey, &i)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if i.Name != name {
|
|
||||||
return false, errors.New("token invalid name")
|
|
||||||
}
|
|
||||||
if i.Expiry == nil && i.Expiry.Time().Compare(time.Now()) < 0 {
|
|
||||||
return false, errors.New("token expired")
|
|
||||||
}
|
|
||||||
if i.NotBefore == nil && i.NotBefore.Time().Compare(time.Now()) > 0 {
|
|
||||||
return false, errors.New("token not valid yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.Compare(expectedKey, i.Key) != 0 {
|
|
||||||
return false, errors.New("key mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Verify != nil {
|
|
||||||
if rand.Float64() < c.VerifyProbability {
|
|
||||||
// random spot check
|
|
||||||
if ok, err := c.Verify(expectedKey, string(i.Result)); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else if !ok {
|
|
||||||
return false, errors.New("failed challenge verification")
|
|
||||||
}
|
|
||||||
r.Header.Set(fmt.Sprintf("X-Away-Challenge-%s-Verify", name), "FULL")
|
|
||||||
} else {
|
|
||||||
r.Header.Set(fmt.Sprintf("X-Away-Challenge-%s-Verify", name), "BRIEF")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.Header.Set(fmt.Sprintf("X-Away-Challenge-%s-Verify", name), "CHECK")
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
package challenge
|
package challenge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"errors"
|
||||||
|
"git.gammaspectra.live/git/go-away/utils"
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
|
"github.com/go-jose/go-jose/v4/jwt"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -16,10 +23,14 @@ const (
|
|||||||
ResultPass
|
ResultPass
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Id int
|
||||||
|
|
||||||
type Challenge struct {
|
type Challenge struct {
|
||||||
|
Id Id
|
||||||
|
Name string
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
Verify func(key []byte, result string) (bool, error)
|
Verify func(key []byte, result string, r *http.Request) (bool, error)
|
||||||
VerifyProbability float64
|
VerifyProbability float64
|
||||||
|
|
||||||
ServeStatic http.Handler
|
ServeStatic http.Handler
|
||||||
@@ -32,3 +43,129 @@ type Challenge struct {
|
|||||||
ServeMakeChallenge http.Handler
|
ServeMakeChallenge http.Handler
|
||||||
ServeVerifyChallenge http.Handler
|
ServeVerifyChallenge http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Key []byte `json:"key"`
|
||||||
|
Result []byte `json:"result,omitempty"`
|
||||||
|
|
||||||
|
Expiry *jwt.NumericDate `json:"exp,omitempty"`
|
||||||
|
NotBefore *jwt.NumericDate `json:"nbf,omitempty"`
|
||||||
|
IssuedAt *jwt.NumericDate `json:"iat,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Challenge) IssueChallengeToken(privateKey ed25519.PrivateKey, key, result []byte, until time.Time) (token string, err error) {
|
||||||
|
signer, err := jose.NewSigner(jose.SigningKey{
|
||||||
|
Algorithm: jose.EdDSA,
|
||||||
|
Key: privateKey,
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
expiry := jwt.NumericDate(until.Unix())
|
||||||
|
notBefore := jwt.NumericDate(time.Now().UTC().AddDate(0, 0, -1).Unix())
|
||||||
|
issuedAt := jwt.NumericDate(time.Now().UTC().Unix())
|
||||||
|
|
||||||
|
token, err = jwt.Signed(signer).Claims(Token{
|
||||||
|
Name: c.Name,
|
||||||
|
Key: key,
|
||||||
|
Result: result,
|
||||||
|
Expiry: &expiry,
|
||||||
|
NotBefore: ¬Before,
|
||||||
|
IssuedAt: &issuedAt,
|
||||||
|
}).Serialize()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifyResult int
|
||||||
|
|
||||||
|
const (
|
||||||
|
VerifyResultNONE = VerifyResult(iota)
|
||||||
|
VerifyResultFAIL
|
||||||
|
|
||||||
|
// VerifyResultPASS Client just passed this challenge
|
||||||
|
VerifyResultPASS
|
||||||
|
VerifyResultOK
|
||||||
|
VerifyResultBRIEF
|
||||||
|
VerifyResultFULL
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r VerifyResult) Ok() bool {
|
||||||
|
return r > VerifyResultFAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r VerifyResult) String() string {
|
||||||
|
switch r {
|
||||||
|
case VerifyResultNONE:
|
||||||
|
return "NONE"
|
||||||
|
case VerifyResultFAIL:
|
||||||
|
return "FAIL"
|
||||||
|
case VerifyResultPASS:
|
||||||
|
return "PASS"
|
||||||
|
case VerifyResultOK:
|
||||||
|
return "OK"
|
||||||
|
case VerifyResultBRIEF:
|
||||||
|
return "BRIEF"
|
||||||
|
case VerifyResultFULL:
|
||||||
|
return "FULL"
|
||||||
|
default:
|
||||||
|
panic("unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrVerifyKeyMismatch = errors.New("verify: key mismatch")
|
||||||
|
var ErrVerifyVerifyMismatch = errors.New("verify: verification mismatch")
|
||||||
|
|
||||||
|
func (c Challenge) VerifyChallengeToken(publicKey ed25519.PublicKey, expectedKey []byte, r *http.Request) (VerifyResult, error) {
|
||||||
|
cookie, err := r.Cookie(utils.CookiePrefix + c.Name)
|
||||||
|
if err != nil {
|
||||||
|
return VerifyResultNONE, err
|
||||||
|
}
|
||||||
|
if cookie == nil {
|
||||||
|
return VerifyResultNONE, http.ErrNoCookie
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.ParseSigned(cookie.Value, []jose.SignatureAlgorithm{jose.EdDSA})
|
||||||
|
if err != nil {
|
||||||
|
return VerifyResultFAIL, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var i Token
|
||||||
|
err = token.Claims(publicKey, &i)
|
||||||
|
if err != nil {
|
||||||
|
return VerifyResultFAIL, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Name != c.Name {
|
||||||
|
return VerifyResultFAIL, errors.New("token invalid name")
|
||||||
|
}
|
||||||
|
if i.Expiry == nil && i.Expiry.Time().Compare(time.Now()) < 0 {
|
||||||
|
return VerifyResultFAIL, errors.New("token expired")
|
||||||
|
}
|
||||||
|
if i.NotBefore == nil && i.NotBefore.Time().Compare(time.Now()) > 0 {
|
||||||
|
return VerifyResultFAIL, errors.New("token not valid yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Compare(expectedKey, i.Key) != 0 {
|
||||||
|
return VerifyResultFAIL, ErrVerifyKeyMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Verify != nil {
|
||||||
|
if rand.Float64() < c.VerifyProbability {
|
||||||
|
// random spot check
|
||||||
|
if ok, err := c.Verify(expectedKey, string(i.Result), r); err != nil {
|
||||||
|
return VerifyResultFAIL, err
|
||||||
|
} else if !ok {
|
||||||
|
return VerifyResultFAIL, ErrVerifyVerifyMismatch
|
||||||
|
}
|
||||||
|
return VerifyResultFULL, nil
|
||||||
|
} else {
|
||||||
|
return VerifyResultBRIEF, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return VerifyResultOK, nil
|
||||||
|
}
|
||||||
|
129
lib/http.go
129
lib/http.go
@@ -3,6 +3,7 @@ package lib
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"codeberg.org/meta/gzipped/v2"
|
"codeberg.org/meta/gzipped/v2"
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"git.gammaspectra.live/git/go-away/embed"
|
"git.gammaspectra.live/git/go-away/embed"
|
||||||
"git.gammaspectra.live/git/go-away/lib/challenge"
|
"git.gammaspectra.live/git/go-away/lib/challenge"
|
||||||
"git.gammaspectra.live/git/go-away/lib/policy"
|
"git.gammaspectra.live/git/go-away/lib/policy"
|
||||||
|
"git.gammaspectra.live/git/go-away/utils"
|
||||||
"github.com/google/cel-go/common/types"
|
"github.com/google/cel-go/common/types"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
@@ -88,7 +90,7 @@ func (state *State) challengePage(w http.ResponseWriter, id string, status int,
|
|||||||
|
|
||||||
err := templates["challenge-"+state.Settings.ChallengeTemplate+".gohtml"].Execute(buf, input)
|
err := templates["challenge-"+state.Settings.ChallengeTemplate+".gohtml"].Execute(buf, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = state.errorPage(w, id, http.StatusInternalServerError, err)
|
_ = state.errorPage(w, id, http.StatusInternalServerError, err, "")
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
_, _ = w.Write(buf.Bytes())
|
_, _ = w.Write(buf.Bytes())
|
||||||
@@ -96,7 +98,7 @@ func (state *State) challengePage(w http.ResponseWriter, id string, status int,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *State) errorPage(w http.ResponseWriter, id string, status int, err error) error {
|
func (state *State) errorPage(w http.ResponseWriter, id string, status int, err error, redirect string) error {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 8192))
|
buf := bytes.NewBuffer(make([]byte, 0, 8192))
|
||||||
@@ -110,6 +112,7 @@ func (state *State) errorPage(w http.ResponseWriter, id string, status int, err
|
|||||||
"Title": "Oh no! " + http.StatusText(status),
|
"Title": "Oh no! " + http.StatusText(status),
|
||||||
"HideSpinner": true,
|
"HideSpinner": true,
|
||||||
"Challenge": "",
|
"Challenge": "",
|
||||||
|
"Redirect": redirect,
|
||||||
})
|
})
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
panic(err2)
|
panic(err2)
|
||||||
@@ -144,6 +147,8 @@ func (state *State) logger(r *http.Request) *slog.Logger {
|
|||||||
func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
|
func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
host := r.Host
|
host := r.Host
|
||||||
|
|
||||||
|
data := RequestDataFromContext(r.Context())
|
||||||
|
|
||||||
backend, ok := state.Settings.Backends[host]
|
backend, ok := state.Settings.Backends[host]
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
|
http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
|
||||||
@@ -190,13 +195,14 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
fail := func(code int, err error) {
|
fail := func(code int, err error) {
|
||||||
state.addTiming(w, "rule-eval", "Evaluate access rules", ruleEvalDuration)
|
state.addTiming(w, "rule-eval", "Evaluate access rules", ruleEvalDuration)
|
||||||
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), code, err)
|
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), code, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
setAwayState := func(rule RuleState) {
|
setAwayState := func(rule RuleState) {
|
||||||
r.Header.Set("X-Away-Rule", rule.Name)
|
r.Header.Set("X-Away-Rule", rule.Name)
|
||||||
r.Header.Set("X-Away-Hash", rule.Hash)
|
r.Header.Set("X-Away-Hash", rule.Hash)
|
||||||
r.Header.Set("X-Away-Action", string(rule.Action))
|
r.Header.Set("X-Away-Action", string(rule.Action))
|
||||||
|
data.Headers(state, r.Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rule := range state.Rules {
|
for _, rule := range state.Rules {
|
||||||
@@ -224,39 +230,31 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
serve()
|
serve()
|
||||||
return
|
return
|
||||||
case policy.RuleActionCHALLENGE, policy.RuleActionCHECK:
|
case policy.RuleActionCHALLENGE, policy.RuleActionCHECK:
|
||||||
start = time.Now()
|
|
||||||
|
|
||||||
expiry := time.Now().UTC().Add(DefaultValidity).Round(DefaultValidity)
|
for _, challengeId := range rule.Challenges {
|
||||||
|
if result := data.Challenges[challengeId]; !result.Ok() {
|
||||||
for _, challengeName := range rule.Challenges {
|
continue
|
||||||
key := state.GetChallengeKeyForRequest(challengeName, expiry, r)
|
|
||||||
ok, err := state.VerifyChallengeToken(challengeName, key, w, r)
|
|
||||||
if !ok || err != nil {
|
|
||||||
if !errors.Is(err, http.ErrNoCookie) {
|
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if rule.Action == policy.RuleActionCHECK {
|
if rule.Action == policy.RuleActionCHECK {
|
||||||
goto nextRule
|
goto nextRule
|
||||||
}
|
}
|
||||||
// we passed the challenge!
|
|
||||||
|
|
||||||
lg.Debug("request passed", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", challengeName)
|
// we passed the challenge!
|
||||||
|
lg.Debug("request passed", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", state.Challenges[challengeId].Name)
|
||||||
setAwayState(rule)
|
setAwayState(rule)
|
||||||
serve()
|
serve()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.addTiming(w, "challenge-token-check", "Verify client challenge tokens", time.Since(start))
|
|
||||||
|
|
||||||
// none matched, issue first challenge in priority
|
// none matched, issue first challenge in priority
|
||||||
for _, challengeName := range rule.Challenges {
|
for _, challengeId := range rule.Challenges {
|
||||||
c := state.Challenges[challengeName]
|
c := state.Challenges[challengeId]
|
||||||
if c.ServeChallenge != nil {
|
if c.ServeChallenge != nil {
|
||||||
result := c.ServeChallenge(w, r, state.GetChallengeKeyForRequest(challengeName, expiry, r), expiry)
|
result := c.ServeChallenge(w, r, state.GetChallengeKeyForRequest(c.Name, data.Expires, r), data.Expires)
|
||||||
switch result {
|
switch result {
|
||||||
case challenge.ResultStop:
|
case challenge.ResultStop:
|
||||||
lg.Info("request challenged", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", challengeName)
|
lg.Info("request challenged", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", c.Name)
|
||||||
return
|
return
|
||||||
case challenge.ResultContinue:
|
case challenge.ResultContinue:
|
||||||
continue
|
continue
|
||||||
@@ -264,12 +262,12 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
if rule.Action == policy.RuleActionCHECK {
|
if rule.Action == policy.RuleActionCHECK {
|
||||||
goto nextRule
|
goto nextRule
|
||||||
}
|
}
|
||||||
state.logger(r).Warn("challenge passed", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", challengeName)
|
state.logger(r).Warn("challenge passed", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", c.Name)
|
||||||
|
|
||||||
|
data.Challenges[c.Id] = challenge.VerifyResultOK
|
||||||
|
|
||||||
// we pass the challenge early!
|
// we pass the challenge early!
|
||||||
r.Header.Set(fmt.Sprintf("X-Away-Challenge-%s-Verify", challengeName), "PASS")
|
lg.Debug("request passed", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", c.Name)
|
||||||
|
|
||||||
lg.Debug("request passed", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", challengeName)
|
|
||||||
setAwayState(rule)
|
setAwayState(rule)
|
||||||
serve()
|
serve()
|
||||||
return
|
return
|
||||||
@@ -347,7 +345,7 @@ func (state *State) setupRoutes() error {
|
|||||||
|
|
||||||
state.Mux.Handle("GET "+state.UrlPath+"/assets/", http.StripPrefix(state.UrlPath, gzipped.FileServer(gzipped.FS(embed.AssetsFs))))
|
state.Mux.Handle("GET "+state.UrlPath+"/assets/", http.StripPrefix(state.UrlPath, gzipped.FileServer(gzipped.FS(embed.AssetsFs))))
|
||||||
|
|
||||||
for challengeName, c := range state.Challenges {
|
for _, c := range state.Challenges {
|
||||||
if c.ServeStatic != nil {
|
if c.ServeStatic != nil {
|
||||||
state.Mux.Handle("GET "+c.Path+"/static/", c.ServeStatic)
|
state.Mux.Handle("GET "+c.Path+"/static/", c.ServeStatic)
|
||||||
}
|
}
|
||||||
@@ -365,43 +363,47 @@ func (state *State) setupRoutes() error {
|
|||||||
} else if c.Verify != nil {
|
} else if c.Verify != nil {
|
||||||
state.Mux.HandleFunc(fmt.Sprintf("GET %s/verify-challenge", c.Path), func(w http.ResponseWriter, r *http.Request) {
|
state.Mux.HandleFunc(fmt.Sprintf("GET %s/verify-challenge", c.Path), func(w http.ResponseWriter, r *http.Request) {
|
||||||
err := func() (err error) {
|
err := func() (err error) {
|
||||||
expiry := time.Now().UTC().Add(DefaultValidity).Round(DefaultValidity)
|
|
||||||
key := state.GetChallengeKeyForRequest(challengeName, expiry, r)
|
data := RequestDataFromContext(r.Context())
|
||||||
|
|
||||||
|
key := state.GetChallengeKeyForRequest(c.Name, data.Expires, r)
|
||||||
result := r.FormValue("result")
|
result := r.FormValue("result")
|
||||||
|
|
||||||
requestId, err := hex.DecodeString(r.FormValue("requestId"))
|
requestId, err := hex.DecodeString(r.FormValue("requestId"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
// override
|
||||||
r.Header.Set("X-Away-Id", hex.EncodeToString(requestId))
|
r.Header.Set("X-Away-Id", hex.EncodeToString(requestId))
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
ok, err := c.Verify(key, result)
|
ok, err := c.Verify(key, result, r)
|
||||||
state.addTiming(w, "challenge-verify", "Verify client challenge", time.Since(start))
|
state.addTiming(w, "challenge-verify", "Verify client challenge", time.Since(start))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.logger(r).Error(fmt.Errorf("challenge error: %w", err).Error(), "challenge", challengeName, "redirect", r.FormValue("redirect"))
|
state.logger(r).Error(fmt.Errorf("challenge error: %w", err).Error(), "challenge", c.Name, "redirect", r.FormValue("redirect"))
|
||||||
return err
|
return err
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
state.logger(r).Warn("challenge failed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
|
state.logger(r).Warn("challenge failed", "challenge", c.Name, "redirect", r.FormValue("redirect"))
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+c.Name, w)
|
||||||
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusForbidden, fmt.Errorf("access denied: failed challenge %s", challengeName))
|
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusForbidden, fmt.Errorf("access denied: failed challenge %s", c.Name), r.FormValue("redirect"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
state.logger(r).Info("challenge passed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
|
state.logger(r).Info("challenge passed", "challenge", c.Name, "redirect", r.FormValue("redirect"))
|
||||||
|
|
||||||
token, err := state.IssueChallengeToken(challengeName, key, []byte(result), expiry)
|
token, err := c.IssueChallengeToken(state.privateKey, key, []byte(result), data.Expires)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+c.Name, w)
|
||||||
} else {
|
} else {
|
||||||
SetCookie(CookiePrefix+challengeName, token, expiry, w)
|
utils.SetCookie(utils.CookiePrefix+c.Name, token, data.Expires, w)
|
||||||
}
|
}
|
||||||
|
data.Challenges[c.Id] = challenge.VerifyResultPASS
|
||||||
|
|
||||||
http.Redirect(w, r, r.FormValue("redirect"), http.StatusTemporaryRedirect)
|
http.Redirect(w, r, r.FormValue("redirect"), http.StatusTemporaryRedirect)
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+c.Name, w)
|
||||||
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusInternalServerError, err)
|
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusInternalServerError, err, r.FormValue("redirect"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -410,3 +412,54 @@ func (state *State) setupRoutes() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (state *State) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var data RequestData
|
||||||
|
// generate random id, todo: is this fast?
|
||||||
|
_, _ = rand.Read(data.Id[:])
|
||||||
|
data.Challenges = make(map[challenge.Id]challenge.VerifyResult, len(state.Challenges))
|
||||||
|
data.Expires = time.Now().UTC().Add(DefaultValidity).Round(DefaultValidity)
|
||||||
|
|
||||||
|
for _, c := range state.Challenges {
|
||||||
|
key := state.GetChallengeKeyForRequest(c.Name, data.Expires, r)
|
||||||
|
result, err := c.VerifyChallengeToken(state.publicKey, key, r)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
||||||
|
// clear invalid cookie
|
||||||
|
utils.ClearCookie(utils.CookiePrefix+c.Name, w)
|
||||||
|
}
|
||||||
|
data.Challenges[c.Id] = result
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("X-Away-Id", hex.EncodeToString(data.Id[:]))
|
||||||
|
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), "_goaway_data", &data))
|
||||||
|
|
||||||
|
state.Mux.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestDataFromContext(ctx context.Context) *RequestData {
|
||||||
|
return ctx.Value("_goaway_data").(*RequestData)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestData struct {
|
||||||
|
Id [16]byte
|
||||||
|
Expires time.Time
|
||||||
|
Challenges map[challenge.Id]challenge.VerifyResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RequestData) HasValidChallenge(id challenge.Id) bool {
|
||||||
|
return d.Challenges[id].Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RequestData) Headers(state *State, headers http.Header) {
|
||||||
|
for id, result := range d.Challenges {
|
||||||
|
if result.Ok() {
|
||||||
|
c, ok := state.Challenges[id]
|
||||||
|
if !ok {
|
||||||
|
panic("challenge not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.Set(fmt.Sprintf("X-Away-Challenge-%s-Result", c.Name), result.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
124
lib/state.go
124
lib/state.go
@@ -17,6 +17,7 @@ import (
|
|||||||
"git.gammaspectra.live/git/go-away/lib/challenge/wasm/interface"
|
"git.gammaspectra.live/git/go-away/lib/challenge/wasm/interface"
|
||||||
"git.gammaspectra.live/git/go-away/lib/condition"
|
"git.gammaspectra.live/git/go-away/lib/condition"
|
||||||
"git.gammaspectra.live/git/go-away/lib/policy"
|
"git.gammaspectra.live/git/go-away/lib/policy"
|
||||||
|
"git.gammaspectra.live/git/go-away/utils"
|
||||||
"git.gammaspectra.live/git/go-away/utils/inline"
|
"git.gammaspectra.live/git/go-away/utils/inline"
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/common/types"
|
"github.com/google/cel-go/common/types"
|
||||||
@@ -48,7 +49,7 @@ type State struct {
|
|||||||
|
|
||||||
Wasm *wasm.Runner
|
Wasm *wasm.Runner
|
||||||
|
|
||||||
Challenges map[string]challenge.Challenge
|
Challenges map[challenge.Id]challenge.Challenge
|
||||||
|
|
||||||
RulesEnv *cel.Env
|
RulesEnv *cel.Env
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ type RuleState struct {
|
|||||||
|
|
||||||
Program cel.Program
|
Program cel.Program
|
||||||
Action policy.RuleAction
|
Action policy.RuleAction
|
||||||
Challenges []string
|
Challenges []challenge.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateSettings struct {
|
type StateSettings struct {
|
||||||
@@ -97,7 +98,7 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
if proxy.ErrorHandler == nil {
|
if proxy.ErrorHandler == nil {
|
||||||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
state.logger(r).Error(err.Error())
|
state.logger(r).Error(err.Error())
|
||||||
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusBadGateway, err)
|
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusBadGateway, err, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,13 +167,18 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
|
|
||||||
state.Wasm = wasm.NewRunner(true)
|
state.Wasm = wasm.NewRunner(true)
|
||||||
|
|
||||||
state.Challenges = make(map[string]challenge.Challenge)
|
state.Challenges = make(map[challenge.Id]challenge.Challenge)
|
||||||
|
|
||||||
|
idCounter := challenge.Id(1)
|
||||||
|
|
||||||
for challengeName, p := range p.Challenges {
|
for challengeName, p := range p.Challenges {
|
||||||
c := challenge.Challenge{
|
c := challenge.Challenge{
|
||||||
|
Id: idCounter,
|
||||||
|
Name: challengeName,
|
||||||
Path: fmt.Sprintf("%s/challenge/%s", state.UrlPath, challengeName),
|
Path: fmt.Sprintf("%s/challenge/%s", state.UrlPath, challengeName),
|
||||||
VerifyProbability: p.Runtime.Probability,
|
VerifyProbability: p.Runtime.Probability,
|
||||||
}
|
}
|
||||||
|
idCounter++
|
||||||
|
|
||||||
if c.VerifyProbability <= 0 {
|
if c.VerifyProbability <= 0 {
|
||||||
//10% default
|
//10% default
|
||||||
@@ -209,11 +215,38 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
|
|
||||||
expectedCookie := p.Parameters["http-cookie"]
|
expectedCookie := p.Parameters["http-cookie"]
|
||||||
|
|
||||||
|
c.Verify = func(key []byte, result string, r *http.Request) (bool, error) {
|
||||||
|
var cookieValue string
|
||||||
|
if expectedCookie != "" {
|
||||||
|
if cookie, err := r.Cookie(expectedCookie); err != nil || cookie == nil {
|
||||||
|
// skip check if we don't have cookie or it's expired
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
cookieValue = cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// bind hash of cookie contents
|
||||||
|
sum := sha256.New()
|
||||||
|
sum.Write([]byte(cookieValue))
|
||||||
|
sum.Write([]byte{0})
|
||||||
|
sum.Write(key)
|
||||||
|
sum.Write([]byte{0})
|
||||||
|
sum.Write(state.publicKey)
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(sum.Sum(nil), []byte(result)) == 1 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result {
|
c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result {
|
||||||
|
var cookieValue string
|
||||||
if expectedCookie != "" {
|
if expectedCookie != "" {
|
||||||
if cookie, err := r.Cookie(expectedCookie); err != nil || cookie == nil {
|
if cookie, err := r.Cookie(expectedCookie); err != nil || cookie == nil {
|
||||||
// skip check if we don't have cookie or it's expired
|
// skip check if we don't have cookie or it's expired
|
||||||
return challenge.ResultContinue
|
return challenge.ResultContinue
|
||||||
|
} else {
|
||||||
|
cookieValue = cookie.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,15 +264,23 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
defer io.Copy(io.Discard, response.Body)
|
defer io.Copy(io.Discard, response.Body)
|
||||||
|
|
||||||
if response.StatusCode != httpCode {
|
if response.StatusCode != httpCode {
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+c.Name, w)
|
||||||
// continue other challenges!
|
// continue other challenges!
|
||||||
return challenge.ResultContinue
|
return challenge.ResultContinue
|
||||||
} else {
|
} else {
|
||||||
token, err := state.IssueChallengeToken(challengeName, key, nil, expiry)
|
// bind hash of cookie contents
|
||||||
|
sum := sha256.New()
|
||||||
|
sum.Write([]byte(cookieValue))
|
||||||
|
sum.Write([]byte{0})
|
||||||
|
sum.Write(key)
|
||||||
|
sum.Write([]byte{0})
|
||||||
|
sum.Write(state.publicKey)
|
||||||
|
|
||||||
|
token, err := c.IssueChallengeToken(state.privateKey, key, sum.Sum(nil), expiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+c.Name, w)
|
||||||
} else {
|
} else {
|
||||||
SetCookie(CookiePrefix+challengeName, token, expiry, w)
|
utils.SetCookie(utils.CookiePrefix+challengeName, token, expiry, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// we passed it!
|
// we passed it!
|
||||||
@@ -249,11 +290,12 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
|
|
||||||
case "cookie":
|
case "cookie":
|
||||||
c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result {
|
c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result {
|
||||||
token, err := state.IssueChallengeToken(challengeName, key, nil, expiry)
|
|
||||||
|
token, err := c.IssueChallengeToken(state.privateKey, key, nil, expiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+challengeName, w)
|
||||||
} else {
|
} else {
|
||||||
SetCookie(CookiePrefix+challengeName, token, expiry, w)
|
utils.SetCookie(utils.CookiePrefix+challengeName, token, expiry, w)
|
||||||
}
|
}
|
||||||
// self redirect!
|
// self redirect!
|
||||||
//TODO: add redirect loop detect parameter
|
//TODO: add redirect loop detect parameter
|
||||||
@@ -379,7 +421,7 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
content = []byte(data)
|
content = []byte(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Verify = func(key []byte, result string) (bool, error) {
|
c.Verify = func(key []byte, result string, r *http.Request) (bool, error) {
|
||||||
resultBytes, err := hex.DecodeString(result)
|
resultBytes, err := hex.DecodeString(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -394,9 +436,9 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
c.ServeVerifyChallenge = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
c.ServeVerifyChallenge = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
err := func() (err error) {
|
err := func() (err error) {
|
||||||
expiry := time.Now().UTC().Add(DefaultValidity).Round(DefaultValidity)
|
data := RequestDataFromContext(r.Context())
|
||||||
|
|
||||||
key := state.GetChallengeKeyForRequest(challengeName, expiry, r)
|
key := state.GetChallengeKeyForRequest(challengeName, data.Expires, r)
|
||||||
result := r.FormValue("result")
|
result := r.FormValue("result")
|
||||||
|
|
||||||
requestId, err := hex.DecodeString(r.FormValue("requestId"))
|
requestId, err := hex.DecodeString(r.FormValue("requestId"))
|
||||||
@@ -404,22 +446,24 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
r.Header.Set("X-Away-Id", hex.EncodeToString(requestId))
|
r.Header.Set("X-Away-Id", hex.EncodeToString(requestId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := c.Verify(key, result); err != nil {
|
if ok, err := c.Verify(key, result, r); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
state.logger(r).Warn("challenge failed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
|
state.logger(r).Warn("challenge failed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+challengeName, w)
|
||||||
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusForbidden, fmt.Errorf("access denied: failed challenge %s", challengeName))
|
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusForbidden, fmt.Errorf("access denied: failed challenge %s", challengeName), r.FormValue("redirect"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
state.logger(r).Warn("challenge passed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
|
state.logger(r).Warn("challenge passed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
|
||||||
|
|
||||||
token, err := state.IssueChallengeToken(challengeName, key, []byte(result), expiry)
|
token, err := c.IssueChallengeToken(state.privateKey, key, []byte(result), data.Expires)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+challengeName, w)
|
||||||
} else {
|
} else {
|
||||||
SetCookie(CookiePrefix+challengeName, token, expiry, w)
|
utils.SetCookie(utils.CookiePrefix+challengeName, token, data.Expires, w)
|
||||||
}
|
}
|
||||||
|
data.Challenges[c.Id] = challenge.VerifyResultPASS
|
||||||
|
|
||||||
switch httpCode {
|
switch httpCode {
|
||||||
case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
|
case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
|
||||||
@@ -435,8 +479,8 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ClearCookie(CookiePrefix+challengeName, w)
|
utils.ClearCookie(utils.CookiePrefix+challengeName, w)
|
||||||
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusInternalServerError, err)
|
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusInternalServerError, err, r.FormValue("redirect"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -454,8 +498,10 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
c.ServeMakeChallenge = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
c.ServeMakeChallenge = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
err := state.Wasm.Instantiate(challengeName, func(ctx context.Context, mod api.Module) (err error) {
|
err := state.Wasm.Instantiate(challengeName, func(ctx context.Context, mod api.Module) (err error) {
|
||||||
|
|
||||||
|
data := RequestDataFromContext(r.Context())
|
||||||
|
|
||||||
in := _interface.MakeChallengeInput{
|
in := _interface.MakeChallengeInput{
|
||||||
Key: state.GetChallengeKeyForRequest(challengeName, time.Now().UTC().Add(DefaultValidity).Round(DefaultValidity), r),
|
Key: state.GetChallengeKeyForRequest(challengeName, data.Expires, r),
|
||||||
Parameters: p.Parameters,
|
Parameters: p.Parameters,
|
||||||
Headers: inline.MIMEHeader(r.Header),
|
Headers: inline.MIMEHeader(r.Header),
|
||||||
}
|
}
|
||||||
@@ -479,12 +525,12 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusInternalServerError, err)
|
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusInternalServerError, err, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
c.Verify = func(key []byte, result string) (ok bool, err error) {
|
c.Verify = func(key []byte, result string, r *http.Request) (ok bool, err error) {
|
||||||
err = state.Wasm.Instantiate(challengeName, func(ctx context.Context, mod api.Module) (err error) {
|
err = state.Wasm.Instantiate(challengeName, func(ctx context.Context, mod api.Module) (err error) {
|
||||||
in := _interface.VerifyChallengeInput{
|
in := _interface.VerifyChallengeInput{
|
||||||
Key: key,
|
Key: key,
|
||||||
@@ -510,7 +556,7 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Challenges[challengeName] = c
|
state.Challenges[c.Id] = c
|
||||||
}
|
}
|
||||||
|
|
||||||
state.RulesEnv, err = cel.NewEnv(
|
state.RulesEnv, err = cel.NewEnv(
|
||||||
@@ -598,12 +644,22 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
hasher.Write(privateKeyFingerprint[:])
|
hasher.Write(privateKeyFingerprint[:])
|
||||||
sum := hasher.Sum(nil)
|
sum := hasher.Sum(nil)
|
||||||
|
|
||||||
|
challenges := make([]challenge.Id, 0, len(rule.Challenges))
|
||||||
|
|
||||||
|
for _, challengeName := range rule.Challenges {
|
||||||
|
c, ok := state.GetChallengeByName(challengeName)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("challenge %s not found", challengeName)
|
||||||
|
}
|
||||||
|
challenges = append(challenges, c.Id)
|
||||||
|
}
|
||||||
|
|
||||||
r := RuleState{
|
r := RuleState{
|
||||||
Name: rule.Name,
|
Name: rule.Name,
|
||||||
Hash: hex.EncodeToString(sum[:8]),
|
Hash: hex.EncodeToString(sum[:8]),
|
||||||
Host: rule.Host,
|
Host: rule.Host,
|
||||||
Action: policy.RuleAction(strings.ToUpper(rule.Action)),
|
Action: policy.RuleAction(strings.ToUpper(rule.Action)),
|
||||||
Challenges: rule.Challenges,
|
Challenges: challenges,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.Action == policy.RuleActionCHALLENGE || r.Action == policy.RuleActionCHECK) && len(r.Challenges) == 0 {
|
if (r.Action == policy.RuleActionCHALLENGE || r.Action == policy.RuleActionCHECK) && len(r.Challenges) == 0 {
|
||||||
@@ -641,11 +697,11 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
|
|||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *State) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (state *State) GetChallengeByName(name string) (challenge.Challenge, bool) {
|
||||||
requestId := make([]byte, 16)
|
for _, c := range state.Challenges {
|
||||||
_, _ = rand.Read(requestId)
|
if c.Name == name {
|
||||||
|
return c, true
|
||||||
r.Header.Set("X-Away-Id", hex.EncodeToString(requestId))
|
}
|
||||||
|
}
|
||||||
state.Mux.ServeHTTP(w, r)
|
return challenge.Challenge{}, false
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package lib
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
Reference in New Issue
Block a user