package p2p import ( "context" "encoding/hex" "errors" "fmt" "net" "net/url" "regexp" "strconv" "strings" "github.com/tendermint/tendermint/crypto" ) const ( // NodeIDByteLength is the length of a crypto.Address. Currently only 20. // FIXME: support other length addresses? NodeIDByteLength = crypto.AddressSize ) var ( // reNodeID is a regexp for valid node IDs. reNodeID = regexp.MustCompile(`^[0-9a-f]{40}$`) // stringHasScheme tries to detect URLs with schemes. It looks for a : before a / (if any). stringHasScheme = func(str string) bool { return strings.Contains(str, "://") } // reSchemeIsHost tries to detect URLs where the scheme part is instead a // hostname, i.e. of the form "host:80/path" where host: is a hostname. reSchemeIsHost = regexp.MustCompile(`^[^/:]+:\d+(/|$)`) ) // NodeID is a hex-encoded crypto.Address. It must be lowercased // (for uniqueness) and of length 2*NodeIDByteLength. type NodeID string // NewNodeID returns a lowercased (normalized) NodeID, or errors if the // node ID is invalid. func NewNodeID(nodeID string) (NodeID, error) { n := NodeID(strings.ToLower(nodeID)) return n, n.Validate() } // NodeIDFromPubKey creates a node ID from a given PubKey address. func NodeIDFromPubKey(pubKey crypto.PubKey) NodeID { return NodeID(hex.EncodeToString(pubKey.Address())) } // Bytes converts the node ID to its binary byte representation. func (id NodeID) Bytes() ([]byte, error) { bz, err := hex.DecodeString(string(id)) if err != nil { return nil, fmt.Errorf("invalid node ID encoding: %w", err) } return bz, nil } // Validate validates the NodeID. func (id NodeID) Validate() error { switch { case len(id) == 0: return errors.New("empty node ID") case len(id) != 2*NodeIDByteLength: return fmt.Errorf("invalid node ID length %d, expected %d", len(id), 2*NodeIDByteLength) case !reNodeID.MatchString(string(id)): return fmt.Errorf("node ID can only contain lowercased hex digits") default: return nil } } // NodeAddress is a node address URL. It differs from a transport Endpoint in // that it contains the node's ID, and that the address hostname may be resolved // into multiple IP addresses (and thus multiple endpoints). // // If the URL is opaque, i.e. of the form "scheme:opaque", then the opaque part // is expected to contain a node ID. type NodeAddress struct { NodeID NodeID Protocol Protocol Hostname string Port uint16 Path string } // ParseNodeAddress parses a node address URL into a NodeAddress, normalizing // and validating it. func ParseNodeAddress(urlString string) (NodeAddress, error) { // url.Parse requires a scheme, so if it fails to parse a scheme-less URL // we try to apply a default scheme. url, err := url.Parse(urlString) if (err != nil || url.Scheme == "") && (!stringHasScheme(urlString) || reSchemeIsHost.MatchString(urlString)) { url, err = url.Parse(string(defaultProtocol) + "://" + urlString) } if err != nil { return NodeAddress{}, fmt.Errorf("invalid node address %q: %w", urlString, err) } address := NodeAddress{ Protocol: Protocol(strings.ToLower(url.Scheme)), } // Opaque URLs are expected to contain only a node ID. if url.Opaque != "" { address.NodeID = NodeID(url.Opaque) return address, address.Validate() } // Otherwise, just parse a normal networked URL. if url.User != nil { address.NodeID = NodeID(strings.ToLower(url.User.Username())) } address.Hostname = strings.ToLower(url.Hostname()) if portString := url.Port(); portString != "" { port64, err := strconv.ParseUint(portString, 10, 16) if err != nil { return NodeAddress{}, fmt.Errorf("invalid port %q: %w", portString, err) } address.Port = uint16(port64) } address.Path = url.Path if url.RawQuery != "" { address.Path += "?" + url.RawQuery } if url.Fragment != "" { address.Path += "#" + url.Fragment } if address.Path != "" { switch address.Path[0] { case '/', '#', '?': default: address.Path = "/" + address.Path } } return address, address.Validate() } // Resolve resolves a NodeAddress into a set of Endpoints, by expanding // out a DNS hostname to IP addresses. func (a NodeAddress) Resolve(ctx context.Context) ([]Endpoint, error) { if a.Protocol == "" { return nil, errors.New("address has no protocol") } // If there is no hostname, this is an opaque URL in the form // "scheme:opaque", and the opaque part is assumed to be node ID used as // Path. if a.Hostname == "" { if a.NodeID == "" { return nil, errors.New("local address has no node ID") } return []Endpoint{{ Protocol: a.Protocol, Path: string(a.NodeID), }}, nil } ips, err := net.DefaultResolver.LookupIP(ctx, "ip", a.Hostname) if err != nil { return nil, err } endpoints := make([]Endpoint, len(ips)) for i, ip := range ips { endpoints[i] = Endpoint{ Protocol: a.Protocol, IP: ip, Port: a.Port, Path: a.Path, } } return endpoints, nil } // String formats the address as a URL string. func (a NodeAddress) String() string { u := url.URL{Scheme: string(a.Protocol)} if a.NodeID != "" { u.User = url.User(string(a.NodeID)) } switch { case a.Hostname != "": if a.Port > 0 { u.Host = net.JoinHostPort(a.Hostname, strconv.Itoa(int(a.Port))) } else { u.Host = a.Hostname } u.Path = a.Path case a.Protocol != "" && (a.Path == "" || a.Path == string(a.NodeID)): u.User = nil u.Opaque = string(a.NodeID) // e.g. memory:id case a.Path != "" && a.Path[0] != '/': u.Path = "/" + a.Path // e.g. some/path default: u.Path = a.Path // e.g. /some/path } return strings.TrimPrefix(u.String(), "//") } // Validate validates a NodeAddress. func (a NodeAddress) Validate() error { if a.Protocol == "" { return errors.New("no protocol") } if a.NodeID == "" { return errors.New("no peer ID") } else if err := a.NodeID.Validate(); err != nil { return fmt.Errorf("invalid peer ID: %w", err) } if a.Port > 0 && a.Hostname == "" { return errors.New("cannot specify port without hostname") } return nil }