Some checks failed
Docker image builds / build (push) Waiting to run
Asset Build Verification / asset_verification (push) Has been cancelled
Docs deploy / build (push) Has been cancelled
Go Mod Tidy Check / go_mod_tidy_check (push) Has been cancelled
Go / go_tests (push) Has been cancelled
Package builds (unstable) / package_builds (push) Has been cancelled
Smoke tests / smoke-test (default-config-macro) (push) Has been cancelled
Smoke tests / smoke-test (docker-registry) (push) Has been cancelled
Smoke tests / smoke-test (double_slash) (push) Has been cancelled
Smoke tests / smoke-test (forced-language) (push) Has been cancelled
Smoke tests / smoke-test (git-clone) (push) Has been cancelled
Smoke tests / smoke-test (git-push) (push) Has been cancelled
Smoke tests / smoke-test (healthcheck) (push) Has been cancelled
Smoke tests / smoke-test (i18n) (push) Has been cancelled
Smoke tests / smoke-test (log-file) (push) Has been cancelled
Smoke tests / smoke-test (nginx) (push) Has been cancelled
Smoke tests / smoke-test (palemoon/amd64) (push) Has been cancelled
Smoke tests / smoke-test (robots_txt) (push) Has been cancelled
Check Spelling / Check Spelling (push) Has been cancelled
SSH CI / ssh (aarch64-16k) (push) Has been cancelled
SSH CI / ssh (aarch64-4k) (push) Has been cancelled
SSH CI / ssh (ppc64le) (push) Has been cancelled
SSH CI / ssh (riscv64) (push) Has been cancelled
zizmor / zizmor latest via PyPI (push) Has been cancelled
172 lines
3.8 KiB
Go
172 lines
3.8 KiB
Go
package policy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/netip"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"git.sad.ovh/sophie/nuke/internal"
|
|
"git.sad.ovh/sophie/nuke/lib/policy/checker"
|
|
"github.com/gaissmai/bart"
|
|
)
|
|
|
|
var (
|
|
ErrMisconfiguration = errors.New("[unexpected] policy: administrator misconfiguration")
|
|
)
|
|
|
|
type RemoteAddrChecker struct {
|
|
prefixTable *bart.Lite
|
|
hash string
|
|
}
|
|
|
|
func NewRemoteAddrChecker(cidrs []string) (checker.Impl, error) {
|
|
table := new(bart.Lite)
|
|
|
|
for _, cidr := range cidrs {
|
|
prefix, err := netip.ParsePrefix(cidr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: range %s not parsing: %w", ErrMisconfiguration, cidr, err)
|
|
}
|
|
|
|
table.Insert(prefix)
|
|
}
|
|
|
|
return &RemoteAddrChecker{
|
|
prefixTable: table,
|
|
hash: internal.FastHash(strings.Join(cidrs, ",")),
|
|
}, nil
|
|
}
|
|
|
|
func (rac *RemoteAddrChecker) Check(r *http.Request) (bool, error) {
|
|
host := r.Header.Get("X-Real-Ip")
|
|
if host == "" {
|
|
return false, fmt.Errorf("%w: header X-Real-Ip is not set", ErrMisconfiguration)
|
|
}
|
|
|
|
addr, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%w: %s is not an IP address: %w", ErrMisconfiguration, host, err)
|
|
}
|
|
|
|
// Convert IPv4-mapped IPv6 addresses to IPv4
|
|
if addr.Is6() && addr.Is4In6() {
|
|
addr = addr.Unmap()
|
|
}
|
|
|
|
return rac.prefixTable.Contains(addr), nil
|
|
}
|
|
|
|
func (rac *RemoteAddrChecker) Hash() string {
|
|
return rac.hash
|
|
}
|
|
|
|
type HeaderMatchesChecker struct {
|
|
header string
|
|
regexp *regexp.Regexp
|
|
hash string
|
|
}
|
|
|
|
func NewUserAgentChecker(rexStr string) (checker.Impl, error) {
|
|
return NewHeaderMatchesChecker("User-Agent", rexStr)
|
|
}
|
|
|
|
func NewHeaderMatchesChecker(header, rexStr string) (checker.Impl, error) {
|
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
|
}
|
|
return &HeaderMatchesChecker{strings.TrimSpace(header), rex, internal.FastHash(header + ": " + rexStr)}, nil
|
|
}
|
|
|
|
func (hmc *HeaderMatchesChecker) Check(r *http.Request) (bool, error) {
|
|
if hmc.regexp.MatchString(r.Header.Get(hmc.header)) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (hmc *HeaderMatchesChecker) Hash() string {
|
|
return hmc.hash
|
|
}
|
|
|
|
type PathChecker struct {
|
|
regexp *regexp.Regexp
|
|
hash string
|
|
}
|
|
|
|
func NewPathChecker(rexStr string) (checker.Impl, error) {
|
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
|
}
|
|
return &PathChecker{rex, internal.FastHash(rexStr)}, nil
|
|
}
|
|
|
|
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
|
originalUrl := r.Header.Get("X-Original-URI")
|
|
if originalUrl != "" {
|
|
if pc.regexp.MatchString(originalUrl) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
if pc.regexp.MatchString(r.URL.Path) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (pc *PathChecker) Hash() string {
|
|
return pc.hash
|
|
}
|
|
|
|
func NewHeaderExistsChecker(key string) checker.Impl {
|
|
return headerExistsChecker{strings.TrimSpace(key)}
|
|
}
|
|
|
|
type headerExistsChecker struct {
|
|
header string
|
|
}
|
|
|
|
func (hec headerExistsChecker) Check(r *http.Request) (bool, error) {
|
|
if r.Header.Get(hec.header) != "" {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (hec headerExistsChecker) Hash() string {
|
|
return internal.FastHash(hec.header)
|
|
}
|
|
|
|
func NewHeadersChecker(headermap map[string]string) (checker.Impl, error) {
|
|
var result checker.List
|
|
var errs []error
|
|
|
|
for key, rexStr := range headermap {
|
|
if rexStr == ".*" {
|
|
result = append(result, headerExistsChecker{strings.TrimSpace(key)})
|
|
continue
|
|
}
|
|
|
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("while compiling header %s regex %s: %w", key, rexStr, err))
|
|
continue
|
|
}
|
|
|
|
result = append(result, &HeaderMatchesChecker{key, rex, internal.FastHash(key + ": " + rexStr)})
|
|
}
|
|
|
|
if len(errs) != 0 {
|
|
return nil, errors.Join(errs...)
|
|
}
|
|
|
|
return result, nil
|
|
}
|