mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-10 02:28:45 +00:00
feat(checker): port path checker
Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
@@ -4,5 +4,6 @@ package all
|
||||
import (
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/headerexists"
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/headermatches"
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/path"
|
||||
_ "github.com/TecharoHQ/anubis/lib/checker/remoteaddress"
|
||||
)
|
||||
|
||||
37
lib/checker/path/checker.go
Normal file
37
lib/checker/path/checker.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
func New(rexStr string) (checker.Interface, error) {
|
||||
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: regex %s failed parse: %w", anubis.ErrMisconfiguration, rexStr, err)
|
||||
}
|
||||
return &Checker{rex, internal.FastHash(rexStr)}, nil
|
||||
}
|
||||
|
||||
type Checker struct {
|
||||
regexp *regexp.Regexp
|
||||
hash string
|
||||
}
|
||||
|
||||
func (c *Checker) Check(r *http.Request) (bool, error) {
|
||||
if c.regexp.MatchString(r.URL.Path) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *Checker) Hash() string {
|
||||
return c.hash
|
||||
}
|
||||
90
lib/checker/path/checker_test.go
Normal file
90
lib/checker/path/checker_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChecker(t *testing.T) {
|
||||
fac := Factory{}
|
||||
|
||||
for _, tt := range []struct {
|
||||
err error
|
||||
name string
|
||||
rexStr string
|
||||
reqPath string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
name: "match",
|
||||
rexStr: "^/api/.*",
|
||||
reqPath: "/api/v1/users",
|
||||
ok: true,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "not_match",
|
||||
rexStr: "^/api/.*",
|
||||
reqPath: "/static/index.html",
|
||||
ok: false,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "wildcard_match",
|
||||
rexStr: ".*\\.json$",
|
||||
reqPath: "/data/config.json",
|
||||
ok: true,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "wildcard_not_match",
|
||||
rexStr: ".*\\.json$",
|
||||
reqPath: "/data/config.yaml",
|
||||
ok: false,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid_regex",
|
||||
rexStr: "a(b",
|
||||
err: ErrInvalidRegex,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fc := fileConfig{
|
||||
Regex: tt.rexStr,
|
||||
}
|
||||
data, err := json.Marshal(fc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pc, err := fac.Build(t.Context(), json.RawMessage(data))
|
||||
if err != nil && !errors.Is(err, tt.err) {
|
||||
t.Fatalf("creating PathChecker failed")
|
||||
}
|
||||
|
||||
if tt.err != nil && pc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.Log(pc.Hash())
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, tt.reqPath, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
ok, err := pc.Check(r)
|
||||
|
||||
if tt.ok != ok {
|
||||
t.Errorf("ok: %v, wanted: %v", ok, tt.ok)
|
||||
}
|
||||
|
||||
if err != nil && tt.err != nil && !errors.Is(err, tt.err) {
|
||||
t.Errorf("err: %v, wanted: %v", err, tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
38
lib/checker/path/config.go
Normal file
38
lib/checker/path/config.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoRegex = errors.New("path: no regex is configured")
|
||||
ErrInvalidRegex = errors.New("path: regex is invalid")
|
||||
)
|
||||
|
||||
type fileConfig struct {
|
||||
Regex string `json:"regex" yaml:"regex"`
|
||||
}
|
||||
|
||||
func (fc fileConfig) String() string {
|
||||
return fmt.Sprintf("regex=%q", fc.Regex)
|
||||
}
|
||||
|
||||
func (fc fileConfig) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if fc.Regex == "" {
|
||||
errs = append(errs, ErrNoRegex)
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(fc.Regex); err != nil {
|
||||
errs = append(errs, ErrInvalidRegex, err)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
50
lib/checker/path/config_test.go
Normal file
50
lib/checker/path/config_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileConfigValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name, description string
|
||||
in fileConfig
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "simple happy",
|
||||
description: "the most common usecase",
|
||||
in: fileConfig{
|
||||
Regex: "^/api/.*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard match",
|
||||
description: "match files with specific extension",
|
||||
in: fileConfig{
|
||||
Regex: ".*[.]json$",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no regex",
|
||||
description: "Regex must be set, it is not",
|
||||
in: fileConfig{},
|
||||
err: ErrNoRegex,
|
||||
},
|
||||
{
|
||||
name: "invalid regex",
|
||||
description: "the user wrote an invalid regular expression",
|
||||
in: fileConfig{
|
||||
Regex: "[a-z",
|
||||
},
|
||||
err: ErrInvalidRegex,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.in.Valid(); !errors.Is(err, tt.err) {
|
||||
t.Log(tt.description)
|
||||
t.Fatalf("got %v, wanted %v", err, tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
lib/checker/path/factory.go
Normal file
58
lib/checker/path/factory.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/checker"
|
||||
)
|
||||
|
||||
func init() {
|
||||
checker.Register("path", Factory{})
|
||||
}
|
||||
|
||||
type Factory struct{}
|
||||
|
||||
func (f Factory) Build(ctx context.Context, data json.RawMessage) (checker.Interface, error) {
|
||||
var fc fileConfig
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &fc); err != nil {
|
||||
return nil, errors.Join(checker.ErrUnparseableConfig, err)
|
||||
}
|
||||
|
||||
if err := fc.Valid(); err != nil {
|
||||
return nil, errors.Join(checker.ErrInvalidConfig, err)
|
||||
}
|
||||
|
||||
pathRex, err := regexp.Compile(strings.TrimSpace(fc.Regex))
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrInvalidRegex, err)
|
||||
}
|
||||
|
||||
return &Checker{
|
||||
regexp: pathRex,
|
||||
hash: internal.FastHash(fc.String()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f Factory) Valid(ctx context.Context, data json.RawMessage) error {
|
||||
var fc fileConfig
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &fc); err != nil {
|
||||
return errors.Join(checker.ErrUnparseableConfig, err)
|
||||
}
|
||||
|
||||
return fc.Valid()
|
||||
}
|
||||
|
||||
func Valid(pathRex string) error {
|
||||
fc := fileConfig{
|
||||
Regex: pathRex,
|
||||
}
|
||||
|
||||
return fc.Valid()
|
||||
}
|
||||
52
lib/checker/path/factory_test.go
Normal file
52
lib/checker/path/factory_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFactoryGood(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/good")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fac := Factory{}
|
||||
|
||||
for _, fname := range files {
|
||||
t.Run(fname.Name(), func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", "good", fname.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := fac.Valid(t.Context(), json.RawMessage(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFactoryBad(t *testing.T) {
|
||||
files, err := os.ReadDir("./testdata/bad")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fac := Factory{}
|
||||
|
||||
for _, fname := range files {
|
||||
t.Run(fname.Name(), func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", "bad", fname.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := fac.Valid(t.Context(), json.RawMessage(data)); err == nil {
|
||||
t.Fatal("expected validation to fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
3
lib/checker/path/testdata/bad/invalid_regex.json
vendored
Normal file
3
lib/checker/path/testdata/bad/invalid_regex.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"regex": "a(b"
|
||||
}
|
||||
1
lib/checker/path/testdata/bad/nothing.json
vendored
Normal file
1
lib/checker/path/testdata/bad/nothing.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
3
lib/checker/path/testdata/good/simple.json
vendored
Normal file
3
lib/checker/path/testdata/good/simple.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"regex": "^/api/.*"
|
||||
}
|
||||
3
lib/checker/path/testdata/good/wildcard.json
vendored
Normal file
3
lib/checker/path/testdata/good/wildcard.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"regex": ".*\\.json$"
|
||||
}
|
||||
Reference in New Issue
Block a user