mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-15 21:04:56 +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 (
|
import (
|
||||||
_ "github.com/TecharoHQ/anubis/lib/checker/headerexists"
|
_ "github.com/TecharoHQ/anubis/lib/checker/headerexists"
|
||||||
_ "github.com/TecharoHQ/anubis/lib/checker/headermatches"
|
_ "github.com/TecharoHQ/anubis/lib/checker/headermatches"
|
||||||
|
_ "github.com/TecharoHQ/anubis/lib/checker/path"
|
||||||
_ "github.com/TecharoHQ/anubis/lib/checker/remoteaddress"
|
_ "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