Move most code under lib
This commit is contained in:
207
lib/http.go
Normal file
207
lib/http.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"codeberg.org/meta/gzipped/v2"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
go_away "git.gammaspectra.live/git/go-away"
|
||||
"git.gammaspectra.live/git/go-away/lib/policy"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var templates map[string]*template.Template
|
||||
|
||||
var cacheBust string
|
||||
|
||||
// DefaultValidity TODO: adjust
|
||||
const DefaultValidity = time.Hour * 24 * 7
|
||||
|
||||
func init() {
|
||||
|
||||
buf := make([]byte, 16)
|
||||
_, _ = rand.Read(buf)
|
||||
cacheBust = base64.RawURLEncoding.EncodeToString(buf)
|
||||
|
||||
templates = make(map[string]*template.Template)
|
||||
|
||||
dir, err := go_away.TemplatesFs.ReadDir("templates")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, e := range dir {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
data, err := go_away.TemplatesFs.ReadFile(filepath.Join("templates", e.Name()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tpl := template.New(e.Name())
|
||||
_, err = tpl.Parse(string(data))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
templates[e.Name()] = tpl
|
||||
}
|
||||
}
|
||||
|
||||
func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//TODO better matcher! combo ast?
|
||||
env := map[string]any{
|
||||
"remoteAddress": state.GetRequestAddress(r),
|
||||
"userAgent": r.UserAgent(),
|
||||
"path": r.URL.Path,
|
||||
"query": func() map[string]string {
|
||||
result := make(map[string]string)
|
||||
for k, v := range r.URL.Query() {
|
||||
result[k] = strings.Join(v, ",")
|
||||
}
|
||||
return result
|
||||
}(),
|
||||
"headers": func() map[string]string {
|
||||
result := make(map[string]string)
|
||||
for k, v := range r.Header {
|
||||
result[k] = strings.Join(v, ",")
|
||||
}
|
||||
return result
|
||||
}(),
|
||||
}
|
||||
|
||||
for _, rule := range state.Rules {
|
||||
if out, _, err := rule.Program.Eval(env); err != nil {
|
||||
//TODO error
|
||||
panic(err)
|
||||
} else if out != nil && out.Type() == types.BoolType {
|
||||
if out.Equal(types.True) == types.True {
|
||||
switch rule.Action {
|
||||
default:
|
||||
panic(fmt.Errorf("unknown action %s", rule.Action))
|
||||
case policy.RuleActionPASS:
|
||||
state.Backend.ServeHTTP(w, r)
|
||||
return
|
||||
case policy.RuleActionCHALLENGE, policy.RuleActionCHECK:
|
||||
expiry := time.Now().UTC().Add(DefaultValidity).Round(DefaultValidity)
|
||||
|
||||
for _, challengeName := range rule.Challenges {
|
||||
key := state.GetChallengeKeyForRequest(challengeName, expiry, r)
|
||||
ok, err := state.VerifyChallengeToken(challengeName, key, r)
|
||||
if !ok || err != nil {
|
||||
if !errors.Is(err, http.ErrNoCookie) {
|
||||
ClearCookie(CookiePrefix+challengeName, w)
|
||||
}
|
||||
} else {
|
||||
if rule.Action == policy.RuleActionCHECK {
|
||||
goto nextRule
|
||||
}
|
||||
// we passed the challenge!
|
||||
//TODO log?
|
||||
state.Backend.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
switch result {
|
||||
case ChallengeResultStop:
|
||||
return
|
||||
case ChallengeResultContinue:
|
||||
continue
|
||||
case ChallengeResultPass:
|
||||
if rule.Action == policy.RuleActionCHECK {
|
||||
goto nextRule
|
||||
}
|
||||
// we pass the challenge early!
|
||||
state.Backend.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
panic("challenge not found")
|
||||
}
|
||||
}
|
||||
case policy.RuleActionDENY:
|
||||
//TODO: config error code
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
case policy.RuleActionBLOCK:
|
||||
//TODO: config error code
|
||||
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextRule:
|
||||
}
|
||||
|
||||
state.Backend.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func (state *State) setupRoutes() error {
|
||||
|
||||
state.Mux.HandleFunc("/", state.handleRequest)
|
||||
|
||||
state.Mux.Handle("GET "+state.UrlPath+"/assets/", http.StripPrefix(state.UrlPath, gzipped.FileServer(gzipped.FS(go_away.AssetsFs))))
|
||||
|
||||
for challengeName, c := range state.Challenges {
|
||||
if c.Static != nil {
|
||||
state.Mux.Handle("GET "+c.Path+"/static/", c.Static)
|
||||
}
|
||||
|
||||
if c.ChallengeScript != nil {
|
||||
state.Mux.Handle("GET "+c.ChallengeScriptPath, c.ChallengeScript)
|
||||
}
|
||||
|
||||
if c.MakeChallenge != nil {
|
||||
state.Mux.Handle(fmt.Sprintf("POST %s/make-challenge", c.Path), c.MakeChallenge)
|
||||
}
|
||||
|
||||
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) {
|
||||
expiry := time.Now().UTC().Add(DefaultValidity).Round(DefaultValidity)
|
||||
key := state.GetChallengeKeyForRequest(challengeName, expiry, r)
|
||||
result := r.FormValue("result")
|
||||
|
||||
if ok, err := c.Verify(key, result); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
ClearCookie(CookiePrefix+challengeName, w)
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return nil
|
||||
}
|
||||
|
||||
token, err := state.IssueChallengeToken(challengeName, key, []byte(result), expiry)
|
||||
if err != nil {
|
||||
ClearCookie(CookiePrefix+challengeName, w)
|
||||
} else {
|
||||
SetCookie(CookiePrefix+challengeName, token, expiry, w)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, r.FormValue("redirect"), http.StatusTemporaryRedirect)
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
ClearCookie(CookiePrefix+challengeName, w)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user