package config import ( "fmt" "net" "regexp" "sort" "strconv" "strings" ) // templateName matches a route template: a valid route name ending in "-*". var templateName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*-\*$`) // routeName matches a route (or template) name: the pattern a concrete or template route key must // match, so a name with '*' or an invalid prefix never resolves. var routeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // Route is an ordered list of hosts to try, with an optional default model, the peers allowed to // reach it, how its requests are placed (affinity), and whether crossbar queues them. type Route struct { Hosts []string `toml:"hosts"` DefaultModel string `toml:"default_model"` 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 // lease per model, so the route lives on one host. func (r Route) PerRoute() bool { return r.Affinity == "route" } // Queues reports whether the route's requests wait in (and can be refused by) crossbar's per-(host, // model) queue; false only for queue = false, which leaves queueing to the client's own slot. func (r Route) Queues() bool { return r.Queue == nil || *r.Queue } // Route resolves a request route name: an exact entry wins; else the longest template // "-*" whose prefix (including the dash) starts name with a non-empty remainder; // else ok is false. key is the config key that matched (the template's name for a template). // A name that is not a valid route name (the pattern below) or contains '*' never matches. func (c *Config) Route(name string) (r Route, key string, ok bool) { if !routeName.MatchString(name) { return Route{}, "", false } if rt, found := c.Routes[name]; found { return rt, name, true } var ( best Route bestKey string bestLen int ) for tmpl, rt := range c.Routes { if !templateName.MatchString(tmpl) { continue } prefix := tmpl[:len(tmpl)-1] // drop the trailing '*', keeping the dash if len(prefix) > bestLen && strings.HasPrefix(name, prefix) && len(name) > len(prefix) { best, bestKey, bestLen = rt, tmpl, len(prefix) } } if bestKey == "" { return Route{}, "", false } return best, bestKey, true } // checkRoutes validates and defaults one route's hosts, model, affinity and peers in a fixed order. // A name that is neither a valid route nor a template, a missing or unknown host, a default model no // host serves, an unrecognised affinity, or a peers list that breaks the identity contract each // wins as the first error. func (c *Config) checkRoutes(peersDefined map[string]bool, identityDefined bool) *Error { if len(c.Routes) == 0 { return &Error{Field: "routes", Msg: "at least one required"} } names := make([]string, 0, len(c.Routes)) for name := range c.Routes { 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] if !routeName.MatchString(name) && !templateName.MatchString(name) { return &Error{Field: fmt.Sprintf("routes.%s", name), Msg: "must match [a-z0-9][a-z0-9-]*"} } hostsField := fmt.Sprintf("routes.%s.hosts", name) if len(r.Hosts) == 0 { return &Error{Field: hostsField, Msg: "at least one required"} } seen := make(map[string]bool, len(r.Hosts)) for _, h := range r.Hosts { if seen[h] { return &Error{Field: hostsField, Msg: "host listed twice"} } seen[h] = true if _, ok := c.Hosts[h]; !ok { return &Error{Field: hostsField, Msg: "unknown host"} } } if r.DefaultModel != "" { served := false for _, h := range r.Hosts { if _, ok := c.Hosts[h].Models[r.DefaultModel]; ok { served = true break } } if !served { return &Error{Field: fmt.Sprintf("routes.%s.default_model", name), Msg: "not served by any host in route"} } } switch r.Affinity { case "", "conversation", "route": default: return &Error{Field: fmt.Sprintf("routes.%s.affinity", name), Msg: `must be "conversation" or "route"`} } 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..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 }