package envfile import ( "fmt" "net" "os" "regexp" "strconv" "strings" ) // The port a machine put something on, said beside the value that names it. // // **An address written down when a port was decided does not follow the port** (novox/hq // 04-ISSUES/102). The control plane's own store and broker connections are written at genesis — // full connection strings, password and all — and sealed, so the mesh cannot open them to move the // port inside. When the node's settings move that port, every consumer's binding follows and the // control plane's own connection does not: it goes on dialling the number genesis wrote, and the // mesh is headless. // // So a setting has a third twin. `NAME_FILE` says where the value is; `NAME_PORT` says which port // this machine put the thing at, composed into the control plane's environment from the node's // settings exactly as a consumer's binding is. The value's own port is what stands when the twin // says nothing — a mesh whose foundation is still where genesis raised it needs no override, and // an empty answer is the mesh saying it has nothing to add, not the mesh saying zero. // // Only the port. The host inside the value is the machine's own loopback, or the broker's public // name — a fact of the genesis, not of any node's settings — and the mesh has no better opinion // of it. What moves under ADR 0100 is the port. // PortVar is the twin saying which port this machine put the named thing at. func PortVar(name string) string { return name + "_PORT" } // Port is what NAME_PORT says, checked to be a port, or "" when it says nothing. // // Blank counts as unset, as it does for every other variable here: a container given an empty // string was given nothing, and refusing it as ambiguous would turn an omission into a puzzle. func Port(name string) (string, error) { raw := strings.TrimSpace(os.Getenv(PortVar(name))) if raw == "" { return "", nil } n, err := strconv.Atoi(raw) if err != nil || n < 1 || n > 65535 { return "", fmt.Errorf("%s is %q, which is not a port", PortVar(name), raw) } return strconv.Itoa(n), nil } // Placed is Value, with its port moved to where NAME_PORT says this machine put it. func Placed(name string) (string, error) { value, err := Value(name) if err != nil { return "", err } port, err := Port(name) if err != nil || port == "" { return value, err } placed, err := WithPort(value, port) if err != nil { // Where it came from is said; what it says is not. The value is a connection string with // a password in it, and every error here is written on the assumption it will be logged. return "", fmt.Errorf("%s names a port to move %s to, and %s could not be read as "+ "something with a port in it: %w", PortVar(name), name, name, err) } return placed, nil } // keywordPort is `port=…` in a `key=value` connection string. var keywordPort = regexp.MustCompile(`(^|\s)port=\S*`) // WithPort is the value with its port replaced by `port`. // // Three shapes, because the control plane's settings come in three: a URL // (`scheme://[user:password@]host[:port][/…]`), a bare `host[:port]` (the broker's address) and // a `key=value` connection string (`host=… port=…`), which is what the store's driver accepts // beside a URL. An IPv6 host stays in its brackets. Nothing here parses the URL properly — a // password with a character a URL parser dislikes is still a working connection string, and // refusing it would refuse a value that has been dialling fine since genesis. func WithPort(value, port string) (string, error) { value = strings.TrimSpace(value) if value == "" { return "", fmt.Errorf("the value is empty") } if scheme, rest, isURL := strings.Cut(value, "://"); isURL { end := strings.IndexAny(rest, "/?#") authority, tail := rest, "" if end >= 0 { authority, tail = rest[:end], rest[end:] } userinfo := "" if at := strings.LastIndex(authority, "@"); at >= 0 { userinfo, authority = authority[:at+1], authority[at+1:] } host, err := hostWithPort(authority, port) if err != nil { return "", err } return scheme + "://" + userinfo + host + tail, nil } if strings.Contains(value, "=") && !strings.ContainsAny(value, "/") { if keywordPort.MatchString(value) { return keywordPort.ReplaceAllString(value, "${1}port="+port), nil } return value + " port=" + port, nil } return hostWithPort(value, port) } // hostWithPort is `host[:port]` with the port set, brackets kept around an IPv6 host. func hostWithPort(authority, port string) (string, error) { if authority == "" { return "", fmt.Errorf("there is no host to put a port on") } host, _, err := net.SplitHostPort(authority) if err != nil { // No port yet. A bracketed IPv6 host without a port is a shape SplitHostPort refuses, and // a bare one carries colons of its own — both are a host, not an error. host = strings.TrimSuffix(strings.TrimPrefix(authority, "["), "]") } return net.JoinHostPort(host, port), nil }