-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This renames `PeerAddress` to `NodeAddress`, moves it and `NodeID` into a separate file `address.go`, adds tests for them, and fixes a bunch of bugs and inconsistencies.
- Loading branch information
1 parent
1f39f80
commit fc71882
Showing
11 changed files
with
661 additions
and
258 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
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}$`) | ||
|
||
// reHasScheme tries to detect URLs with schemes. It looks for a : before a / (if any). | ||
reHasScheme = regexp.MustCompile(`^[^/]+:`) | ||
|
||
// 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 == "") && | ||
(!reHasScheme.MatchString(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, also used as path. | ||
if url.Opaque != "" { | ||
address.NodeID = NodeID(url.Opaque) | ||
address.Path = 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 | ||
} |
Oops, something went wrong.