diff --git a/lib/challenge/state.go b/lib/challenge/state.go new file mode 100644 index 0000000..9ece04d --- /dev/null +++ b/lib/challenge/state.go @@ -0,0 +1,34 @@ +package challenge + +import ( + "net/http" + "time" +) + +type Result int + +const ( + // ResultStop Stop testing other challenges and return + ResultStop = Result(iota) + // ResultContinue Test next + ResultContinue + // ResultPass passed, return and proxy + ResultPass +) + +type Challenge struct { + Path string + + Verify func(key []byte, result string) (bool, error) + VerifyProbability float64 + + ServeStatic http.Handler + + ServeChallenge func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) Result + + ServeScript http.Handler + ServeScriptPath string + + ServeMakeChallenge http.Handler + ServeVerifyChallenge http.Handler +} diff --git a/lib/http.go b/lib/http.go index 2fc9cf2..7bec8be 100644 --- a/lib/http.go +++ b/lib/http.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "git.gammaspectra.live/git/go-away/embed" + "git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/policy" "github.com/google/cel-go/common/types" "html/template" @@ -251,15 +252,15 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) { // none matched, issue first challenge in priority for _, challengeName := range rule.Challenges { c := state.Challenges[challengeName] - if c.Challenge != nil { - result := c.Challenge(w, r, state.GetChallengeKeyForRequest(challengeName, expiry, r), expiry) + if c.ServeChallenge != nil { + result := c.ServeChallenge(w, r, state.GetChallengeKeyForRequest(challengeName, expiry, r), expiry) switch result { - case ChallengeResultStop: + case challenge.ResultStop: lg.Info("request challenged", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", challengeName) return - case ChallengeResultContinue: + case challenge.ResultContinue: continue - case ChallengeResultPass: + case challenge.ResultPass: if rule.Action == policy.RuleActionCHECK { goto nextRule } @@ -347,20 +348,20 @@ func (state *State) setupRoutes() error { state.Mux.Handle("GET "+state.UrlPath+"/assets/", http.StripPrefix(state.UrlPath, gzipped.FileServer(gzipped.FS(embed.AssetsFs)))) for challengeName, c := range state.Challenges { - if c.Static != nil { - state.Mux.Handle("GET "+c.Path+"/static/", c.Static) + if c.ServeStatic != nil { + state.Mux.Handle("GET "+c.Path+"/static/", c.ServeStatic) } - if c.ChallengeScript != nil { - state.Mux.Handle("GET "+c.ChallengeScriptPath, c.ChallengeScript) + if c.ServeScript != nil { + state.Mux.Handle("GET "+c.ServeScriptPath, c.ServeScript) } - if c.MakeChallenge != nil { - state.Mux.Handle(fmt.Sprintf("POST %s/make-challenge", c.Path), c.MakeChallenge) + if c.ServeMakeChallenge != nil { + state.Mux.Handle(fmt.Sprintf("POST %s/make-challenge", c.Path), c.ServeMakeChallenge) } - if c.VerifyChallenge != nil { - state.Mux.Handle(fmt.Sprintf("GET %s/verify-challenge", c.Path), c.VerifyChallenge) + if c.ServeVerifyChallenge != nil { + state.Mux.Handle(fmt.Sprintf("GET %s/verify-challenge", c.Path), c.ServeVerifyChallenge) } else if c.Verify != nil { state.Mux.HandleFunc(fmt.Sprintf("GET %s/verify-challenge", c.Path), func(w http.ResponseWriter, r *http.Request) { err := func() (err error) { diff --git a/lib/state.go b/lib/state.go index 72818a7..d0a56fe 100644 --- a/lib/state.go +++ b/lib/state.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "git.gammaspectra.live/git/go-away/embed" + "git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/challenge/wasm" "git.gammaspectra.live/git/go-away/lib/challenge/wasm/interface" "git.gammaspectra.live/git/go-away/lib/condition" @@ -47,7 +48,7 @@ type State struct { Wasm *wasm.Runner - Challenges map[string]ChallengeState + Challenges map[string]challenge.Challenge RulesEnv *cel.Env @@ -70,31 +71,6 @@ type RuleState struct { Challenges []string } -type ChallengeResult int - -const ( - // ChallengeResultStop Stop testing challenges and return - ChallengeResultStop = ChallengeResult(iota) - // ChallengeResultContinue Test next challenge - ChallengeResultContinue - // ChallengeResultPass Challenge passed, return and proxy - ChallengeResultPass -) - -type ChallengeState struct { - Path string - - Static http.Handler - Challenge func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) ChallengeResult - ChallengeScriptPath string - ChallengeScript http.Handler - MakeChallenge http.Handler - VerifyChallenge http.Handler - Verify func(key []byte, result string) (bool, error) - - VerifyProbability float64 -} - type StateSettings struct { Backends map[string]http.Handler PrivateKeySeed []byte @@ -190,10 +166,10 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) state.Wasm = wasm.NewRunner(true) - state.Challenges = make(map[string]ChallengeState) + state.Challenges = make(map[string]challenge.Challenge) for challengeName, p := range p.Challenges { - c := ChallengeState{ + c := challenge.Challenge{ Path: fmt.Sprintf("%s/challenge/%s", state.UrlPath, challengeName), VerifyProbability: p.Runtime.Probability, } @@ -208,7 +184,7 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) assetPath := c.Path + "/static/" subFs, err := fs.Sub(embed.ChallengeFs, fmt.Sprintf("challenge/%s/static", challengeName)) if err == nil { - c.Static = http.StripPrefix( + c.ServeStatic = http.StripPrefix( assetPath, gzipped.FileServer(gzipped.FS(subFs)), ) @@ -233,23 +209,23 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) expectedCookie := p.Parameters["http-cookie"] - c.Challenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) ChallengeResult { + c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result { 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 ChallengeResultContinue + return challenge.ResultContinue } } request, err := http.NewRequest(method, *p.Url, nil) if err != nil { - return ChallengeResultContinue + return challenge.ResultContinue } request.Header = r.Header response, err := state.Client.Do(request) if err != nil { - return ChallengeResultContinue + return challenge.ResultContinue } defer response.Body.Close() defer io.Copy(io.Discard, response.Body) @@ -257,7 +233,7 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) if response.StatusCode != httpCode { ClearCookie(CookiePrefix+challengeName, w) // continue other challenges! - return ChallengeResultContinue + return challenge.ResultContinue } else { token, err := state.IssueChallengeToken(challengeName, key, nil, expiry) if err != nil { @@ -267,12 +243,12 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) } // we passed it! - return ChallengeResultPass + return challenge.ResultPass } } case "cookie": - c.Challenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) ChallengeResult { + c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result { token, err := state.IssueChallengeToken(challengeName, key, nil, expiry) if err != nil { ClearCookie(CookiePrefix+challengeName, w) @@ -282,10 +258,10 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) // self redirect! //TODO: add redirect loop detect parameter http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect) - return ChallengeResultStop + return challenge.ResultStop } case "meta-refresh": - c.Challenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) ChallengeResult { + c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result { redirectUri := new(url.URL) redirectUri.Path = c.Path + "/verify-challenge" @@ -302,10 +278,10 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) }, }) - return ChallengeResultStop + return challenge.ResultStop } case "header-refresh": - c.Challenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) ChallengeResult { + c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result { redirectUri := new(url.URL) redirectUri.Path = c.Path + "/verify-challenge" @@ -321,10 +297,10 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) _ = state.challengePage(w, r.Header.Get("X-Away-Id"), http.StatusTeapot, "", nil) - return ChallengeResultStop + return challenge.ResultStop } case "resource-load": - c.Challenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) ChallengeResult { + c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result { redirectUri := new(url.URL) redirectUri.Path = c.Path + "/verify-challenge" @@ -343,16 +319,16 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) }, }) - return ChallengeResultStop + return challenge.ResultStop } case "js": - c.Challenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) ChallengeResult { + c.ServeChallenge = func(w http.ResponseWriter, r *http.Request, key []byte, expiry time.Time) challenge.Result { _ = state.challengePage(w, r.Header.Get("X-Away-Id"), http.StatusTeapot, challengeName, nil) - return ChallengeResultStop + return challenge.ResultStop } - c.ChallengeScriptPath = c.Path + "/challenge.mjs" - c.ChallengeScript = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.ServeScriptPath = c.Path + "/challenge.mjs" + c.ServeScript = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { params, _ := json.Marshal(p.Parameters) //TODO: move this to http.go as a template @@ -415,7 +391,7 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) return true, nil } - c.VerifyChallenge = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.ServeVerifyChallenge = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := func() (err error) { expiry := time.Now().UTC().Add(DefaultValidity).Round(DefaultValidity) @@ -475,7 +451,7 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error) return nil, fmt.Errorf("c %s: compiling runtime: %w", challengeName, err) } - c.MakeChallenge = 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) { in := _interface.MakeChallengeInput{