A route may have its own listener: every request there is that route, paths unprefixed
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package config_test
|
||||
|
||||
// v2.3 task 03: a concrete route may own a dedicated listener. Every request that arrives on it is
|
||||
// that route, with the upstream path unprefixed, for clients that cannot put a route in the path
|
||||
// or a header (Boxmaker's inferproxy rewrites nothing).
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.wntrmute.dev/kyle/crossbar/internal/config"
|
||||
)
|
||||
|
||||
const listenBase = `
|
||||
listen = "127.0.0.1:7777"
|
||||
[hosts.a]
|
||||
base_url = "http://a:1"
|
||||
models = { "m" = { } }
|
||||
[routes.bm-a]
|
||||
hosts = ["a"]
|
||||
listen = "127.0.0.1:7801"
|
||||
[routes.bm-b]
|
||||
hosts = ["a"]
|
||||
listen = "127.0.0.1:7802"
|
||||
[routes.plain]
|
||||
hosts = ["a"]
|
||||
`
|
||||
|
||||
func TestRouteListen(t *testing.T) {
|
||||
c, err := config.Parse(strings.NewReader(listenBase))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for route, want := range map[string]string{"bm-a": "127.0.0.1:7801", "bm-b": "127.0.0.1:7802", "plain": ""} {
|
||||
if got := c.Routes[route].Listen; got != want {
|
||||
t.Errorf("%s listen = %q, want %q", route, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteListenRejected(t *testing.T) {
|
||||
for _, tc := range []struct{ name, text, want string }{
|
||||
{"not host:port", strings.Replace(listenBase, `"127.0.0.1:7801"`, `"7801"`, 1), "routes.bm-a.listen"},
|
||||
{"bad port", strings.Replace(listenBase, `"127.0.0.1:7801"`, `"127.0.0.1:http"`, 1), "routes.bm-a.listen"},
|
||||
{"port zero", strings.Replace(listenBase, `"127.0.0.1:7801"`, `"127.0.0.1:0"`, 1), "routes.bm-a.listen"},
|
||||
{"same as another route", strings.Replace(listenBase, `"127.0.0.1:7802"`, `"127.0.0.1:7801"`, 1), "listen"},
|
||||
{"same as the main listener", strings.Replace(listenBase, `"127.0.0.1:7801"`, `"127.0.0.1:7777"`, 1), "routes.bm-a.listen"},
|
||||
{"on a template", listenBase + "[routes.\"t-*\"]\nhosts = [\"a\"]\nlisten = \"127.0.0.1:7803\"\n", "t-*"},
|
||||
} {
|
||||
if _, err := config.Parse(strings.NewReader(tc.text)); err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Errorf("%s: err %v, want one containing %q", tc.name, err, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -22,6 +24,7 @@ type Route struct {
|
||||
Peers []string `toml:"peers"`
|
||||
Affinity string `toml:"affinity"` // "" or "conversation" (the default), or "route"
|
||||
Queue *bool `toml:"queue"` // nil means true
|
||||
Listen string `toml:"listen"` // "" = none; else host:port of the route's own listener
|
||||
}
|
||||
|
||||
// PerRoute reports affinity = "route": every request on the route (chat or control) shares one
|
||||
@@ -80,6 +83,9 @@ func (c *Config) checkRoutes(peersDefined map[string]bool, identityDefined bool)
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
// A route's own listener address, keyed for the uniqueness check: the value is the
|
||||
// route that first claimed it, so the second route in sorted order reports the miss.
|
||||
seenListen := make(map[string]string, len(c.Routes))
|
||||
for _, name := range names {
|
||||
r := c.Routes[name]
|
||||
|
||||
@@ -124,6 +130,39 @@ func (c *Config) checkRoutes(peersDefined map[string]bool, identityDefined bool)
|
||||
if e := checkPeers(name, r.Peers, peersDefined[name], identityDefined, c.Identity); e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if e := checkListen(name, r.Listen, c.Listen, seenListen); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkListen validates one route's own listener. The rules, in order, each naming the key
|
||||
// routes.<name>.listen: the value must split into host and a numeric port 1-65535, it must not be on
|
||||
// a template route, it must not be the top-level listen, and it must be unique across routes (the
|
||||
// second route in sorted name order reports the clash and the other route's name).
|
||||
func checkListen(name, listen, mainListen string, seenListen map[string]string) *Error {
|
||||
if listen == "" {
|
||||
return nil
|
||||
}
|
||||
_, port, err := net.SplitHostPort(listen)
|
||||
if err != nil {
|
||||
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "must be host:port"}
|
||||
}
|
||||
n, err := strconv.Atoi(port)
|
||||
if err != nil || n < 1 || n > 65535 {
|
||||
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "port must be 1-65535"}
|
||||
}
|
||||
if templateName.MatchString(name) {
|
||||
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "a template route cannot have its own listener"}
|
||||
}
|
||||
if listen == mainListen {
|
||||
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "cannot be the main listen address"}
|
||||
}
|
||||
if other, dup := seenListen[listen]; dup {
|
||||
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: fmt.Sprintf("already used by route %s", other)}
|
||||
}
|
||||
seenListen[listen] = name
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user