p2p/enode: implement base64 text format

This commit is contained in:
Felix Lange 2019-06-04 18:52:10 +02:00
parent e527144fe3
commit fd41587237
3 changed files with 120 additions and 47 deletions

View file

@ -18,6 +18,7 @@ package enode
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"encoding/base64"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@ -27,8 +28,11 @@ import (
"strings" "strings"
"github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/rlp"
) )
var errMissingPrefix = errors.New("missing 'enr:' prefix for base64-encoded record")
// Node represents a host on the network. // Node represents a host on the network.
type Node struct { type Node struct {
r enr.Record r enr.Record
@ -48,6 +52,34 @@ func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) {
return node, nil return node, nil
} }
// MustParse parses a node record or enode:// URL. It panics if the input is invalid.
func MustParse(rawurl string) *Node {
n, err := Parse(ValidSchemes, rawurl)
if err != nil {
panic("invalid node: " + err.Error())
}
return n
}
// Parse decodes and verifies a base64-encoded node record.
func Parse(validSchemes enr.IdentityScheme, input string) (*Node, error) {
if strings.HasPrefix(input, "enode://") {
return ParseV4(input)
}
if !strings.HasPrefix(input, "enr:") {
return nil, errMissingPrefix
}
bin, err := base64.RawURLEncoding.DecodeString(input[4:])
if err != nil {
return nil, err
}
var r enr.Record
if err := rlp.DecodeBytes(bin, &r); err != nil {
return nil, err
}
return New(validSchemes, &r)
}
// ID returns the node identifier. // ID returns the node identifier.
func (n *Node) ID() ID { func (n *Node) ID() ID {
return n.id return n.id
@ -113,10 +145,11 @@ func (n *Node) Record() *enr.Record {
return &cpy return &cpy
} }
// checks whether n is a valid complete node. // ValidateComplete checks whether n has a valid IP and UDP port.
// Deprecated: don't use this method.
func (n *Node) ValidateComplete() error { func (n *Node) ValidateComplete() error {
if n.Incomplete() { if n.Incomplete() {
return errors.New("incomplete node") return errors.New("missing IP address")
} }
if n.UDP() == 0 { if n.UDP() == 0 {
return errors.New("missing UDP port") return errors.New("missing UDP port")
@ -130,20 +163,24 @@ func (n *Node) ValidateComplete() error {
return n.Load(&key) return n.Load(&key)
} }
// The string representation of a Node is a URL. // String returns the text representation of the record.
// Please see ParseNode for a description of the format.
func (n *Node) String() string { func (n *Node) String() string {
return n.v4URL() if isNewV4(n) {
return n.URLv4() // backwards-compatibility glue for NewV4 nodes
}
enc, _ := rlp.EncodeToBytes(&n.r) // always succeeds because record is valid
b64 := base64.RawURLEncoding.EncodeToString(enc)
return "enr:" + b64
} }
// MarshalText implements encoding.TextMarshaler. // MarshalText implements encoding.TextMarshaler.
func (n *Node) MarshalText() ([]byte, error) { func (n *Node) MarshalText() ([]byte, error) {
return []byte(n.v4URL()), nil return []byte(n.String()), nil
} }
// UnmarshalText implements encoding.TextUnmarshaler. // UnmarshalText implements encoding.TextUnmarshaler.
func (n *Node) UnmarshalText(text []byte) error { func (n *Node) UnmarshalText(text []byte) error {
dec, err := ParseV4(string(text)) dec, err := Parse(ValidSchemes, string(text))
if err == nil { if err == nil {
*n = *dec *n = *dec
} }

View file

@ -70,7 +70,7 @@ func ParseV4(rawurl string) (*Node, error) {
if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil { if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil {
id, err := parsePubkey(m[1]) id, err := parsePubkey(m[1])
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid node ID (%v)", err) return nil, fmt.Errorf("invalid public key (%v)", err)
} }
return NewV4(id, nil, 0, 0), nil return NewV4(id, nil, 0, 0), nil
} }
@ -98,6 +98,12 @@ func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node {
return n return n
} }
// isNewV4 returns true for nodes created by NewV4.
func isNewV4(n *Node) bool {
var k s256raw
return n.r.IdentityScheme() == "" && n.r.Load(&k) == nil && len(n.r.Signature()) == 0
}
func parseComplete(rawurl string) (*Node, error) { func parseComplete(rawurl string) (*Node, error) {
var ( var (
id *ecdsa.PublicKey id *ecdsa.PublicKey
@ -116,7 +122,7 @@ func parseComplete(rawurl string) (*Node, error) {
return nil, errors.New("does not contain node ID") return nil, errors.New("does not contain node ID")
} }
if id, err = parsePubkey(u.User.String()); err != nil { if id, err = parsePubkey(u.User.String()); err != nil {
return nil, fmt.Errorf("invalid node ID (%v)", err) return nil, fmt.Errorf("invalid public key (%v)", err)
} }
// Parse the IP address. // Parse the IP address.
host, port, err := net.SplitHostPort(u.Host) host, port, err := net.SplitHostPort(u.Host)
@ -153,7 +159,7 @@ func parsePubkey(in string) (*ecdsa.PublicKey, error) {
return crypto.UnmarshalPubkey(b) return crypto.UnmarshalPubkey(b)
} }
func (n *Node) v4URL() string { func (n *Node) URLv4() string {
var ( var (
scheme enr.ID scheme enr.ID
nodeid string nodeid string

View file

@ -22,36 +22,58 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enr"
) )
var parseNodeTests = []struct { var parseNodeTests = []struct {
rawurl string input string
wantError string wantError string
wantResult *Node wantResult *Node
}{ }{
// Records
{ {
rawurl: "http://foobar", input: "enr:-IS4QGrdq0ugARp5T2BZ41TrZOqLc_oKvZoPuZP5--anqWE_J-Tucc1xgkOL7qXl0puJgT7qc2KSvcupc4NCb0nr4tdjgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQM6UUF2Rm-oFe1IH_rQkRCi00T2ybeMHRSvw1HDpRvjPYN1ZHCCdl8",
wantError: `invalid URL scheme, want "enode"`, wantResult: func() *Node {
testKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
var r enr.Record
r.Set(enr.IP{127, 0, 0, 1})
r.Set(enr.UDP(30303))
r.SetSeq(99)
SignV4(&r, testKey)
n, _ := New(ValidSchemes, &r)
return n
}(),
},
// Invalid Records
{
input: "enr:",
wantError: "EOF", // could be nicer
}, },
{ {
rawurl: "enode://01010101@123.124.125.126:3", input: "enr:x",
wantError: `invalid node ID (wrong length, want 128 hex chars)`, wantError: "illegal base64 data at input byte 0",
}, },
// Complete nodes with IP address.
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3", input: "enr:-EmGZm9vYmFyY4JpZIJ2NIJpcIR_AAABiXNlY3AyNTZrMaEDOlFBdkZvqBXtSB_60JEQotNE9sm3jB0Ur8NRw6Ub4z2DdWRwgnZf",
wantError: enr.ErrInvalidSig.Error(),
},
// Complete node URLs with IP address and ports
{
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3",
wantError: `invalid IP address`, wantError: `invalid IP address`,
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo",
wantError: `invalid port`, wantError: `invalid port`,
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo",
wantError: `invalid discport in query`, wantError: `invalid discport in query`,
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150",
wantResult: NewV4( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{0x7f, 0x0, 0x0, 0x1}, net.IP{0x7f, 0x0, 0x0, 0x1},
@ -60,7 +82,7 @@ var parseNodeTests = []struct {
), ),
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150",
wantResult: NewV4( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.ParseIP("::"), net.ParseIP("::"),
@ -69,7 +91,7 @@ var parseNodeTests = []struct {
), ),
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150",
wantResult: NewV4( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), net.ParseIP("2001:db8:3c4d:15::abcd:ef12"),
@ -78,7 +100,7 @@ var parseNodeTests = []struct {
), ),
}, },
{ {
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334",
wantResult: NewV4( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
net.IP{0x7f, 0x0, 0x0, 0x1}, net.IP{0x7f, 0x0, 0x0, 0x1},
@ -86,16 +108,9 @@ var parseNodeTests = []struct {
22334, 22334,
), ),
}, },
// Incomplete nodes with no address. // Incomplete node URLs with no address
{ {
rawurl: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
nil, 0, 0,
),
},
{
rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
wantResult: NewV4( wantResult: NewV4(
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
nil, 0, 0, nil, 0, 0,
@ -103,17 +118,32 @@ var parseNodeTests = []struct {
}, },
// Invalid URLs // Invalid URLs
{ {
rawurl: "01010101", input: "",
wantError: `invalid node ID (wrong length, want 128 hex chars)`, wantError: errMissingPrefix.Error(),
}, },
{ {
rawurl: "enode://01010101", input: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
wantError: `invalid node ID (wrong length, want 128 hex chars)`, wantError: errMissingPrefix.Error(),
}, },
{ {
// This test checks that errors from url.Parse are handled. input: "01010101",
rawurl: "://foo", wantError: errMissingPrefix.Error(),
wantError: `parse ://foo: missing protocol scheme`, },
{
input: "enode://01010101@123.124.125.126:3",
wantError: `invalid public key (wrong length, want 128 hex chars)`,
},
{
input: "enode://01010101",
wantError: `invalid public key (wrong length, want 128 hex chars)`,
},
{
input: "http://foobar",
wantError: errMissingPrefix.Error(),
},
{
input: "://foo",
wantError: errMissingPrefix.Error(),
}, },
} }
@ -127,22 +157,22 @@ func hexPubkey(h string) *ecdsa.PublicKey {
func TestParseNode(t *testing.T) { func TestParseNode(t *testing.T) {
for _, test := range parseNodeTests { for _, test := range parseNodeTests {
n, err := ParseV4(test.rawurl) n, err := Parse(ValidSchemes, test.input)
if test.wantError != "" { if test.wantError != "" {
if err == nil { if err == nil {
t.Errorf("test %q:\n got nil error, expected %#q", test.rawurl, test.wantError) t.Errorf("test %q:\n got nil error, expected %#q", test.input, test.wantError)
continue continue
} else if err.Error() != test.wantError { } else if err.Error() != test.wantError {
t.Errorf("test %q:\n got error %#q, expected %#q", test.rawurl, err.Error(), test.wantError) t.Errorf("test %q:\n got error %#q, expected %#q", test.input, err.Error(), test.wantError)
continue continue
} }
} else { } else {
if err != nil { if err != nil {
t.Errorf("test %q:\n unexpected error: %v", test.rawurl, err) t.Errorf("test %q:\n unexpected error: %v", test.input, err)
continue continue
} }
if !reflect.DeepEqual(n, test.wantResult) { if !reflect.DeepEqual(n, test.wantResult) {
t.Errorf("test %q:\n result mismatch:\ngot: %#v\nwant: %#v", test.rawurl, n, test.wantResult) t.Errorf("test %q:\n result mismatch:\ngot: %#v\nwant: %#v", test.input, n, test.wantResult)
} }
} }
} }
@ -150,10 +180,10 @@ func TestParseNode(t *testing.T) {
func TestNodeString(t *testing.T) { func TestNodeString(t *testing.T) {
for i, test := range parseNodeTests { for i, test := range parseNodeTests {
if test.wantError == "" && strings.HasPrefix(test.rawurl, "enode://") { if test.wantError == "" && strings.HasPrefix(test.input, "enode://") {
str := test.wantResult.String() str := test.wantResult.String()
if str != test.rawurl { if str != test.input {
t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl) t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.input)
} }
} }
} }