161 lines
4.1 KiB
Go
161 lines
4.1 KiB
Go
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, ¶ms)
|
|
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)
|
|
|
|
data := challenge.RequestDataFromContext(r.Context())
|
|
|
|
if response.StatusCode != params.HttpCode {
|
|
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, false)
|
|
if err != nil {
|
|
return challenge.VerifyResultFail
|
|
}
|
|
utils.SetCookie(data.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(data.CookiePrefix+reg.Name, token, expiry, w, r)
|
|
return challenge.VerifyResultOK
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|