Routes Java Edition connections by the hostname in the handshake, with wildcard domains, backend failover, PROXY protocol v2, cached server-list status from health probes, hot-reloaded config, and an admin page and metrics endpoint. Includes CI, tagged releases and a container image. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e640c5bf02
commit
b53af0e196
29 files changed
+2640
No files matched your search
@@ -0,0 +1,60 @@
|
||||
// Package router maps the hostname a client typed to a configured server.
|
||||
package router
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.veillax.com/Veillax/spinner/internal/config"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
exact map[string]*config.Server
|
||||
wild []wildcard
|
||||
}
|
||||
|
||||
type wildcard struct {
|
||||
suffix string // ".example.com"
|
||||
srv *config.Server
|
||||
}
|
||||
|
||||
// Normalize lowercases a handshake host and strips the trailing dot and any
|
||||
// Forge "\0FML\0" suffix.
|
||||
func Normalize(host string) string {
|
||||
if i := strings.IndexByte(host, 0); i >= 0 {
|
||||
host = host[:i]
|
||||
}
|
||||
return strings.ToLower(strings.TrimRight(host, "."))
|
||||
}
|
||||
|
||||
// New builds a router. Exact domains win; wildcards (*.example.com) match any
|
||||
// depth of subdomain but not the apex, and longer suffixes win over shorter.
|
||||
func New(cfg *config.Config) *Router {
|
||||
r := &Router{exact: map[string]*config.Server{}}
|
||||
for i := range cfg.Servers {
|
||||
s := &cfg.Servers[i]
|
||||
for _, d := range s.Domains {
|
||||
d = strings.ToLower(strings.TrimRight(d, "."))
|
||||
if rest, ok := strings.CutPrefix(d, "*."); ok {
|
||||
r.wild = append(r.wild, wildcard{"." + rest, s})
|
||||
} else if _, dup := r.exact[d]; !dup {
|
||||
r.exact[d] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.SliceStable(r.wild, func(i, j int) bool { return len(r.wild[i].suffix) > len(r.wild[j].suffix) })
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Router) Match(host string) *config.Server {
|
||||
host = Normalize(host)
|
||||
if s, ok := r.exact[host]; ok {
|
||||
return s
|
||||
}
|
||||
for _, w := range r.wild {
|
||||
if strings.HasSuffix(host, w.suffix) && len(host) > len(w.suffix) {
|
||||
return w.srv
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.veillax.com/Veillax/spinner/internal/config"
|
||||
)
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
cfg, err := config.Parse([]byte(`{"servers":[
|
||||
{"name":"a","domains":["a.example.com","*.deep.example.com"],"addresses":["127.0.0.1:1"]},
|
||||
{"name":"b","domains":["*.example.com"],"addresses":["127.0.0.1:2"]},
|
||||
{"name":"c","domains":["*.x.deep.example.com"],"addresses":["127.0.0.1:3"]}
|
||||
]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := New(cfg)
|
||||
cases := map[string]string{
|
||||
"a.example.com": "a",
|
||||
"A.Example.COM.": "a",
|
||||
"a.example.com\x00FML\x00": "a",
|
||||
"q.deep.example.com": "a",
|
||||
"other.example.com": "b",
|
||||
"s.x.deep.example.com": "c",
|
||||
"example.com": "",
|
||||
"evil.com": "",
|
||||
"notexample.com": "",
|
||||
"": "",
|
||||
}
|
||||
for host, want := range cases {
|
||||
got := ""
|
||||
if s := r.Match(host); s != nil {
|
||||
got = s.Name
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("Match(%q) = %q, want %q", host, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
bad := []string{
|
||||
`{"servers":[{"name":"","domains":["a"],"addresses":["h:1"]}]}`,
|
||||
`{"servers":[{"name":"a","domains":[],"addresses":["h:1"]}]}`,
|
||||
`{"servers":[{"name":"a","domains":["a"],"addresses":["nope"]}]}`,
|
||||
`{"servers":[{"name":"a","domains":["a"],"addresses":["h:1"]},{"name":"a","domains":["b"],"addresses":["h:1"]}]}`,
|
||||
`{"unknown_field":1}`,
|
||||
}
|
||||
for _, b := range bad {
|
||||
if _, err := config.Parse([]byte(b)); err == nil {
|
||||
t.Errorf("expected error for %s", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user