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
|
|
}
|