Initial vibeproxy implementation
Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// Package access evaluates reusable access lists: CIDR allow/deny rules
|
||||
// plus an optional HTTP Basic Auth user set.
|
||||
package access
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
"github.com/owen/vibeproxy/internal/models"
|
||||
)
|
||||
|
||||
type rule struct {
|
||||
allow bool
|
||||
net *net.IPNet
|
||||
prio int
|
||||
}
|
||||
|
||||
// List is the runtime-evaluatable form of a models.AccessList.
|
||||
type List struct {
|
||||
ID int64
|
||||
Name string
|
||||
DefaultAllow bool
|
||||
rules []rule
|
||||
Users map[string]string // username -> argon2id hash
|
||||
}
|
||||
|
||||
func Compile(m models.AccessList) *List {
|
||||
l := &List{ID: m.ID, Name: m.Name, DefaultAllow: m.DefaultAllow, Users: map[string]string{}}
|
||||
for _, r := range m.Rules {
|
||||
_, ipnet, err := net.ParseCIDR(r.CIDR)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
l.rules = append(l.rules, rule{allow: r.Action == models.AccessAllow, net: ipnet, prio: r.Priority})
|
||||
}
|
||||
sort.SliceStable(l.rules, func(i, j int) bool { return l.rules[i].prio < l.rules[j].prio })
|
||||
for _, u := range m.BasicAuthUsers {
|
||||
l.Users[u.Username] = u.PasswordHash
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// Allowed reports whether ip is permitted by this list's CIDR rules. The
|
||||
// first matching rule (in priority order) wins; otherwise the default
|
||||
// action applies.
|
||||
func (l *List) Allowed(ip net.IP) bool {
|
||||
for _, r := range l.rules {
|
||||
if r.net.Contains(ip) {
|
||||
return r.allow
|
||||
}
|
||||
}
|
||||
return l.DefaultAllow
|
||||
}
|
||||
|
||||
// RequiresAuth reports whether this list has HTTP Basic Auth users
|
||||
// configured, meaning IP-allowed requests must still authenticate.
|
||||
func (l *List) RequiresAuth() bool { return len(l.Users) > 0 }
|
||||
@@ -0,0 +1,49 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/owen/vibeproxy/internal/models"
|
||||
)
|
||||
|
||||
func TestAccessListAllowDeny(t *testing.T) {
|
||||
m := models.AccessList{
|
||||
DefaultAllow: false,
|
||||
Rules: []models.AccessRule{
|
||||
{Action: models.AccessAllow, CIDR: "10.0.0.0/8", Priority: 0},
|
||||
{Action: models.AccessDeny, CIDR: "10.1.0.0/16", Priority: 1},
|
||||
},
|
||||
}
|
||||
l := Compile(m)
|
||||
|
||||
cases := []struct {
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"10.2.3.4", true}, // matches allow 10.0.0.0/8 first
|
||||
{"192.168.1.1", false}, // no match, default deny
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := l.Allowed(net.ParseIP(c.ip))
|
||||
if got != c.want {
|
||||
t.Errorf("Allowed(%s) = %v, want %v", c.ip, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessListDefaultAllow(t *testing.T) {
|
||||
m := models.AccessList{DefaultAllow: true}
|
||||
l := Compile(m)
|
||||
if !l.Allowed(net.ParseIP("8.8.8.8")) {
|
||||
t.Fatal("expected default allow to permit unmatched IP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessListRequiresAuth(t *testing.T) {
|
||||
m := models.AccessList{BasicAuthUsers: []models.AccessListUser{{Username: "bob", PasswordHash: "x"}}}
|
||||
l := Compile(m)
|
||||
if !l.RequiresAuth() {
|
||||
t.Fatal("expected RequiresAuth to be true when users are configured")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user