194 lines
5.6 KiB
Go
194 lines
5.6 KiB
Go
package action
|
|
|
|
import (
|
|
"fmt"
|
|
"git.gammaspectra.live/git/go-away/lib/challenge"
|
|
"git.gammaspectra.live/git/go-away/lib/policy"
|
|
"github.com/goccy/go-yaml"
|
|
"github.com/goccy/go-yaml/ast"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
func init() {
|
|
i := func(state challenge.StateInterface, ruleName, ruleHash string, settings ast.Node, cont bool) (Handler, error) {
|
|
params := ChallengeDefaultSettings
|
|
|
|
if settings != nil {
|
|
ymlData, err := settings.MarshalYAML()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = yaml.Unmarshal(ymlData, ¶ms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if params.Code == 0 {
|
|
params.Code = state.Settings().ChallengeResponseCode
|
|
}
|
|
|
|
var regs []*challenge.Registration
|
|
for _, regName := range params.Challenges {
|
|
if reg, ok := state.GetChallengeByName(regName); ok {
|
|
regs = append(regs, reg)
|
|
} else {
|
|
return nil, fmt.Errorf("challenge %s not found", regName)
|
|
}
|
|
}
|
|
|
|
if len(regs) == 0 {
|
|
return nil, fmt.Errorf("no registered challenges found in rule %s", ruleName)
|
|
}
|
|
|
|
passAction := policy.RuleAction(strings.ToUpper(params.PassAction))
|
|
passHandler, ok := Register[passAction]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown pass action %s", params.PassAction)
|
|
}
|
|
|
|
passActionHandler, err := passHandler(state, ruleName, ruleHash, params.PassSettings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
failAction := policy.RuleAction(strings.ToUpper(params.FailAction))
|
|
failHandler, ok := Register[failAction]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown pass action %s", params.FailAction)
|
|
}
|
|
|
|
failActionHandler, err := failHandler(state, ruleName, ruleHash, params.FailSettings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return Challenge{
|
|
RuleHash: ruleHash,
|
|
Code: params.Code,
|
|
Continue: cont,
|
|
Challenges: regs,
|
|
|
|
PassAction: passAction,
|
|
PassActionHandler: passActionHandler,
|
|
FailAction: failAction,
|
|
FailActionHandler: failActionHandler,
|
|
}, nil
|
|
}
|
|
Register[policy.RuleActionCHALLENGE] = func(state challenge.StateInterface, ruleName, ruleHash string, settings ast.Node) (Handler, error) {
|
|
return i(state, ruleName, ruleHash, settings, false)
|
|
}
|
|
Register[policy.RuleActionCHECK] = func(state challenge.StateInterface, ruleName, ruleHash string, settings ast.Node) (Handler, error) {
|
|
return i(state, ruleName, ruleHash, settings, true)
|
|
}
|
|
}
|
|
|
|
var ChallengeDefaultSettings = ChallengeSettings{
|
|
PassAction: string(policy.RuleActionPASS),
|
|
FailAction: string(policy.RuleActionDENY),
|
|
}
|
|
|
|
type ChallengeSettings struct {
|
|
Code int `yaml:"http-code"`
|
|
Challenges []string `yaml:"challenges"`
|
|
|
|
PassAction string `yaml:"pass"`
|
|
PassSettings ast.Node `yaml:"pass-settings"`
|
|
|
|
// FailAction Executed in case no challenges match or
|
|
FailAction string `yaml:"fail"`
|
|
FailSettings ast.Node `yaml:"fail-settings"`
|
|
}
|
|
|
|
type Challenge struct {
|
|
RuleHash string
|
|
Code int
|
|
Continue bool
|
|
Challenges []*challenge.Registration
|
|
|
|
PassAction policy.RuleAction
|
|
PassActionHandler Handler
|
|
FailAction policy.RuleAction
|
|
FailActionHandler Handler
|
|
}
|
|
|
|
func (a Challenge) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request, done func() (backend http.Handler)) (next bool, err error) {
|
|
data := challenge.RequestDataFromContext(r.Context())
|
|
for _, reg := range a.Challenges {
|
|
if data.HasValidChallenge(reg.Id()) {
|
|
|
|
data.State.ChallengeChecked(r, reg, r.URL.String(), logger)
|
|
|
|
if a.Continue {
|
|
return true, nil
|
|
}
|
|
|
|
// we passed!
|
|
data.State.ActionHit(r, a.PassAction, logger)
|
|
return a.PassActionHandler.Handle(logger.With("challenge", reg.Name), w, r, done)
|
|
}
|
|
}
|
|
// none matched, issue challenges in sequential priority
|
|
for _, reg := range a.Challenges {
|
|
result := data.ChallengeVerify[reg.Id()]
|
|
state := data.ChallengeState[reg.Id()]
|
|
if result.Ok() || result == challenge.VerifyResultSkip || state == challenge.VerifyStatePass {
|
|
// skip already ok'd challenges for some reason (TODO: why)
|
|
// also skip skipped challenges due to preconditions
|
|
continue
|
|
}
|
|
|
|
expiry := data.Expiration(reg.Duration)
|
|
key := challenge.GetChallengeKeyForRequest(data.State, reg, expiry, r)
|
|
result = reg.IssueChallenge(w, r, key, expiry)
|
|
if result != challenge.VerifyResultSkip {
|
|
data.State.ChallengeIssued(r, reg, r.URL.String(), logger)
|
|
}
|
|
data.ChallengeVerify[reg.Id()] = result
|
|
data.ChallengeState[reg.Id()] = challenge.VerifyStatePass
|
|
switch result {
|
|
case challenge.VerifyResultOK:
|
|
data.State.ChallengePassed(r, reg, r.URL.String(), logger)
|
|
if a.Continue {
|
|
return true, nil
|
|
}
|
|
|
|
data.State.ActionHit(r, a.PassAction, logger)
|
|
return a.PassActionHandler.Handle(logger.With("challenge", reg.Name), w, r, done)
|
|
case challenge.VerifyResultNotOK:
|
|
// we have had the challenge checked, but it's not ok!
|
|
// safe to continue
|
|
continue
|
|
case challenge.VerifyResultFail:
|
|
err := fmt.Errorf("challenge %s failed on issuance", reg.Name)
|
|
data.State.ChallengeFailed(r, reg, err, r.URL.String(), logger)
|
|
|
|
if reg.Class == challenge.ClassTransparent {
|
|
// allow continuing transparent challenges
|
|
continue
|
|
}
|
|
|
|
data.State.ActionHit(r, a.FailAction, logger)
|
|
return a.FailActionHandler.Handle(logger, w, r, done)
|
|
case challenge.VerifyResultNone:
|
|
// challenge was issued
|
|
if reg.Class == challenge.ClassTransparent {
|
|
// allow continuing transparent challenges
|
|
continue
|
|
}
|
|
// we cannot continue after issuance
|
|
return false, nil
|
|
|
|
case challenge.VerifyResultSkip:
|
|
// continue onto next one due to precondition
|
|
continue
|
|
}
|
|
}
|
|
|
|
// nothing matched, execute default action
|
|
data.State.ActionHit(r, a.FailAction, logger)
|
|
return a.FailActionHandler.Handle(logger, w, r, done)
|
|
}
|