Refactor node ID handling, use check digits (fixes #269)
New node ID:s contain four Luhn check digits and are grouped differently. Code uses NodeID type instead of string, so it's formatted homogenously everywhere.
This commit is contained in:
@@ -24,13 +24,13 @@ func newTestModel() *TestModel {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TestModel) Index(nodeID string, repo string, files []FileInfo) {
|
||||
func (t *TestModel) Index(nodeID NodeID, repo string, files []FileInfo) {
|
||||
}
|
||||
|
||||
func (t *TestModel) IndexUpdate(nodeID string, repo string, files []FileInfo) {
|
||||
func (t *TestModel) IndexUpdate(nodeID NodeID, repo string, files []FileInfo) {
|
||||
}
|
||||
|
||||
func (t *TestModel) Request(nodeID, repo, name string, offset int64, size int) ([]byte, error) {
|
||||
func (t *TestModel) Request(nodeID NodeID, repo, name string, offset int64, size int) ([]byte, error) {
|
||||
t.repo = repo
|
||||
t.name = name
|
||||
t.offset = offset
|
||||
@@ -38,11 +38,11 @@ func (t *TestModel) Request(nodeID, repo, name string, offset int64, size int) (
|
||||
return t.data, nil
|
||||
}
|
||||
|
||||
func (t *TestModel) Close(nodeID string, err error) {
|
||||
func (t *TestModel) Close(nodeID NodeID, err error) {
|
||||
close(t.closedCh)
|
||||
}
|
||||
|
||||
func (t *TestModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
|
||||
func (t *TestModel) ClusterConfig(nodeID NodeID, config ClusterConfigMessage) {
|
||||
}
|
||||
|
||||
func (t *TestModel) isClosed() bool {
|
||||
|
||||
@@ -42,7 +42,7 @@ type Repository struct {
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID string // max:64
|
||||
ID []byte // max:32
|
||||
Flags uint32
|
||||
MaxVersion uint64
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
@@ -337,10 +333,10 @@ func (o Node) MarshalXDR() []byte {
|
||||
}
|
||||
|
||||
func (o Node) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
if len(o.ID) > 64 {
|
||||
if len(o.ID) > 32 {
|
||||
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||
}
|
||||
xw.WriteString(o.ID)
|
||||
xw.WriteBytes(o.ID)
|
||||
xw.WriteUint32(o.Flags)
|
||||
xw.WriteUint64(o.MaxVersion)
|
||||
return xw.Tot(), xw.Error()
|
||||
@@ -358,7 +354,7 @@ func (o *Node) UnmarshalXDR(bs []byte) error {
|
||||
}
|
||||
|
||||
func (o *Node) decodeXDR(xr *xdr.Reader) error {
|
||||
o.ID = xr.ReadStringMax(64)
|
||||
o.ID = xr.ReadBytesMax(32)
|
||||
o.Flags = xr.ReadUint32()
|
||||
o.MaxVersion = xr.ReadUint64()
|
||||
return xr.Error()
|
||||
|
||||
@@ -14,29 +14,29 @@ type nativeModel struct {
|
||||
next Model
|
||||
}
|
||||
|
||||
func (m nativeModel) Index(nodeID string, repo string, files []FileInfo) {
|
||||
func (m nativeModel) Index(nodeID NodeID, repo string, files []FileInfo) {
|
||||
for i := range files {
|
||||
files[i].Name = norm.NFD.String(files[i].Name)
|
||||
}
|
||||
m.next.Index(nodeID, repo, files)
|
||||
}
|
||||
|
||||
func (m nativeModel) IndexUpdate(nodeID string, repo string, files []FileInfo) {
|
||||
func (m nativeModel) IndexUpdate(nodeID NodeID, repo string, files []FileInfo) {
|
||||
for i := range files {
|
||||
files[i].Name = norm.NFD.String(files[i].Name)
|
||||
}
|
||||
m.next.IndexUpdate(nodeID, repo, files)
|
||||
}
|
||||
|
||||
func (m nativeModel) Request(nodeID, repo string, name string, offset int64, size int) ([]byte, error) {
|
||||
func (m nativeModel) Request(nodeID NodeID, repo string, name string, offset int64, size int) ([]byte, error) {
|
||||
name = norm.NFD.String(name)
|
||||
return m.next.Request(nodeID, repo, name, offset, size)
|
||||
}
|
||||
|
||||
func (m nativeModel) ClusterConfig(nodeID string, config ClusterConfigMessage) {
|
||||
func (m nativeModel) ClusterConfig(nodeID NodeID, config ClusterConfigMessage) {
|
||||
m.next.ClusterConfig(nodeID, config)
|
||||
}
|
||||
|
||||
func (m nativeModel) Close(nodeID string, err error) {
|
||||
func (m nativeModel) Close(nodeID NodeID, err error) {
|
||||
m.next.Close(nodeID, err)
|
||||
}
|
||||
|
||||
136
protocol/nodeid.go
Normal file
136
protocol/nodeid.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base32"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/calmh/syncthing/luhn"
|
||||
)
|
||||
|
||||
type NodeID [32]byte
|
||||
|
||||
// NewNodeID generates a new node ID from the raw bytes of a certificate
|
||||
func NewNodeID(rawCert []byte) NodeID {
|
||||
var n NodeID
|
||||
hf := sha256.New()
|
||||
hf.Write(rawCert)
|
||||
hf.Sum(n[:0])
|
||||
return n
|
||||
}
|
||||
|
||||
func NodeIDFromString(s string) (NodeID, error) {
|
||||
var n NodeID
|
||||
err := n.UnmarshalText([]byte(s))
|
||||
return n, err
|
||||
}
|
||||
|
||||
// String returns the canonical string representation of the node ID
|
||||
func (n NodeID) String() string {
|
||||
id := base32.StdEncoding.EncodeToString(n[:])
|
||||
id = strings.Trim(id, "=")
|
||||
id = luhnify(id)
|
||||
id = chunkify(id)
|
||||
return id
|
||||
}
|
||||
|
||||
func (n NodeID) GoString() string {
|
||||
return n.String()
|
||||
}
|
||||
|
||||
func (n NodeID) Compare(other NodeID) int {
|
||||
return bytes.Compare(n[:], other[:])
|
||||
}
|
||||
|
||||
func (n NodeID) Equals(other NodeID) bool {
|
||||
return bytes.Compare(n[:], other[:]) == 0
|
||||
}
|
||||
|
||||
func (n *NodeID) MarshalText() ([]byte, error) {
|
||||
return []byte(n.String()), nil
|
||||
}
|
||||
|
||||
func (n *NodeID) UnmarshalText(bs []byte) error {
|
||||
id := string(bs)
|
||||
id = strings.Trim(id, "=")
|
||||
id = strings.ToUpper(id)
|
||||
id = untypeoify(id)
|
||||
id = unchunkify(id)
|
||||
|
||||
var err error
|
||||
switch len(id) {
|
||||
case 56:
|
||||
// New style, with check digits
|
||||
id, err = unluhnify(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case 52:
|
||||
// Old style, no check digits
|
||||
dec, err := base32.StdEncoding.DecodeString(id + "====")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copy(n[:], dec)
|
||||
return nil
|
||||
default:
|
||||
return errors.New("node ID invalid: incorrect length")
|
||||
}
|
||||
}
|
||||
|
||||
func luhnify(s string) string {
|
||||
if len(s) != 52 {
|
||||
panic("unsupported string length")
|
||||
}
|
||||
|
||||
res := make([]string, 0, 4)
|
||||
for i := 0; i < 4; i++ {
|
||||
p := s[i*13 : (i+1)*13]
|
||||
l := luhn.Base32Trimmed.Generate(p)
|
||||
res = append(res, fmt.Sprintf("%s%c", p, l))
|
||||
}
|
||||
return res[0] + res[1] + res[2] + res[3]
|
||||
}
|
||||
|
||||
func unluhnify(s string) (string, error) {
|
||||
if len(s) != 56 {
|
||||
return "", fmt.Errorf("unsupported string length %d", len(s))
|
||||
}
|
||||
|
||||
res := make([]string, 0, 4)
|
||||
for i := 0; i < 4; i++ {
|
||||
p := s[i*14 : (i+1)*14-1]
|
||||
l := luhn.Base32Trimmed.Generate(p)
|
||||
if g := fmt.Sprintf("%s%c", p, l); g != s[i*14:(i+1)*14] {
|
||||
log.Printf("%q; %q", g, s[i*14:(i+1)*14])
|
||||
return "", errors.New("check digit incorrect")
|
||||
}
|
||||
res = append(res, p)
|
||||
}
|
||||
return res[0] + res[1] + res[2] + res[3], nil
|
||||
}
|
||||
|
||||
func chunkify(s string) string {
|
||||
s = regexp.MustCompile("(.{7})").ReplaceAllString(s, "$1-")
|
||||
s = strings.Trim(s, "-")
|
||||
return s
|
||||
}
|
||||
|
||||
func unchunkify(s string) string {
|
||||
s = strings.Replace(s, "-", "", -1)
|
||||
s = strings.Replace(s, " ", "", -1)
|
||||
return s
|
||||
}
|
||||
|
||||
func untypeoify(s string) string {
|
||||
s = strings.Replace(s, "0", "O", -1)
|
||||
s = strings.Replace(s, "1", "I", -1)
|
||||
s = strings.Replace(s, "8", "B", -1)
|
||||
return s
|
||||
}
|
||||
74
protocol/nodeid_test.go
Normal file
74
protocol/nodeid_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package protocol
|
||||
|
||||
import "testing"
|
||||
|
||||
var formatted = "P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"
|
||||
var formatCases = []string{
|
||||
"P56IOI-7MZJNU-2IQGDR-EYDM2M-GTMGL3-BXNPQ6-W5BTBB-Z4TJXZ-WICQ",
|
||||
"P56IOI-7MZJNU2Y-IQGDR-EYDM2M-GTI-MGL3-BXNPQ6-W5BM-TBB-Z4TJXZ-WICQ2",
|
||||
"P56IOI7 MZJNU2I QGDREYD M2MGTMGL 3BXNPQ6W 5BTB BZ4T JXZWICQ",
|
||||
"P56IOI7 MZJNU2Y IQGDREY DM2MGTI MGL3BXN PQ6W5BM TBBZ4TJ XZWICQ2",
|
||||
"P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ",
|
||||
"p56ioi7mzjnu2iqgdreydm2mgtmgl3bxnpq6w5btbbz4tjxzwicq",
|
||||
"P56IOI7MZJNU2YIQGDREYDM2MGTIMGL3BXNPQ6W5BMTBBZ4TJXZWICQ2",
|
||||
"P561017MZJNU2YIQGDREYDM2MGTIMGL3BXNPQ6W5BMT88Z4TJXZWICQ2",
|
||||
"p56ioi7mzjnu2yiqgdreydm2mgtimgl3bxnpq6w5bmtbbz4tjxzwicq2",
|
||||
"p561017mzjnu2yiqgdreydm2mgtimgl3bxnpq6w5bmt88z4tjxzwicq2",
|
||||
}
|
||||
|
||||
func TestFormatNodeID(t *testing.T) {
|
||||
for i, tc := range formatCases {
|
||||
var id NodeID
|
||||
err := id.UnmarshalText([]byte(tc))
|
||||
if err != nil {
|
||||
t.Errorf("#%d UnmarshalText(%q); %v", i, tc, err)
|
||||
} else if f := id.String(); f != formatted {
|
||||
t.Errorf("#%d FormatNodeID(%q)\n\t%q !=\n\t%q", i, tc, f, formatted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var validateCases = []struct {
|
||||
s string
|
||||
ok bool
|
||||
}{
|
||||
{"", false},
|
||||
{"P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2", true},
|
||||
{"P56IOI7-MZJNU2-IQGDREY-DM2MGT-MGL3BXN-PQ6W5B-TBBZ4TJ-XZWICQ", true},
|
||||
{"P56IOI7 MZJNU2I QGDREYD M2MGTMGL 3BXNPQ6W 5BTB BZ4T JXZWICQ", true},
|
||||
{"P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ", true},
|
||||
{"P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQCCCC", false},
|
||||
{"p56ioi7mzjnu2iqgdreydm2mgtmgl3bxnpq6w5btbbz4tjxzwicq", true},
|
||||
{"p56ioi7mzjnu2iqgdreydm2mgtmgl3bxnpq6w5btbbz4tjxzwicqCCCC", false},
|
||||
}
|
||||
|
||||
func TestValidateNodeID(t *testing.T) {
|
||||
for _, tc := range validateCases {
|
||||
var id NodeID
|
||||
err := id.UnmarshalText([]byte(tc.s))
|
||||
if (err == nil && !tc.ok) || (err != nil && tc.ok) {
|
||||
t.Errorf("ValidateNodeID(%q); %v != %v", tc.s, err, tc.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshallingNodeID(t *testing.T) {
|
||||
n0 := NodeID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 10, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}
|
||||
n1 := NodeID{}
|
||||
n2 := NodeID{}
|
||||
|
||||
bs, _ := n0.MarshalText()
|
||||
n1.UnmarshalText(bs)
|
||||
bs, _ = n1.MarshalText()
|
||||
n2.UnmarshalText(bs)
|
||||
|
||||
if n2.String() != n0.String() {
|
||||
t.Errorf("String marshalling error; %q != %q", n2.String(), n0.String())
|
||||
}
|
||||
if !n2.Equals(n0) {
|
||||
t.Error("Equals error")
|
||||
}
|
||||
if n2.Compare(n0) != 0 {
|
||||
t.Error("Compare error")
|
||||
}
|
||||
}
|
||||
@@ -48,19 +48,19 @@ var (
|
||||
|
||||
type Model interface {
|
||||
// An index was received from the peer node
|
||||
Index(nodeID string, repo string, files []FileInfo)
|
||||
Index(nodeID NodeID, repo string, files []FileInfo)
|
||||
// An index update was received from the peer node
|
||||
IndexUpdate(nodeID string, repo string, files []FileInfo)
|
||||
IndexUpdate(nodeID NodeID, repo string, files []FileInfo)
|
||||
// A request was made by the peer node
|
||||
Request(nodeID string, repo string, name string, offset int64, size int) ([]byte, error)
|
||||
Request(nodeID NodeID, repo string, name string, offset int64, size int) ([]byte, error)
|
||||
// A cluster configuration message was received
|
||||
ClusterConfig(nodeID string, config ClusterConfigMessage)
|
||||
ClusterConfig(nodeID NodeID, config ClusterConfigMessage)
|
||||
// The peer node closed the connection
|
||||
Close(nodeID string, err error)
|
||||
Close(nodeID NodeID, err error)
|
||||
}
|
||||
|
||||
type Connection interface {
|
||||
ID() string
|
||||
ID() NodeID
|
||||
Index(repo string, files []FileInfo)
|
||||
Request(repo string, name string, offset int64, size int) ([]byte, error)
|
||||
ClusterConfig(config ClusterConfigMessage)
|
||||
@@ -68,7 +68,7 @@ type Connection interface {
|
||||
}
|
||||
|
||||
type rawConnection struct {
|
||||
id string
|
||||
id NodeID
|
||||
receiver Model
|
||||
|
||||
reader io.ReadCloser
|
||||
@@ -102,7 +102,7 @@ const (
|
||||
pingIdleTime = 60 * time.Second
|
||||
)
|
||||
|
||||
func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model) Connection {
|
||||
func NewConnection(nodeID NodeID, reader io.Reader, writer io.Writer, receiver Model) Connection {
|
||||
cr := &countingReader{Reader: reader}
|
||||
cw := &countingWriter{Writer: writer}
|
||||
|
||||
@@ -139,7 +139,7 @@ func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver M
|
||||
return wireFormatConnection{&c}
|
||||
}
|
||||
|
||||
func (c *rawConnection) ID() string {
|
||||
func (c *rawConnection) ID() NodeID {
|
||||
return c.id
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
|
||||
type incomingIndex struct {
|
||||
update bool
|
||||
id string
|
||||
id NodeID
|
||||
repo string
|
||||
files []FileInfo
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import (
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
var (
|
||||
c0ID = NewNodeID([]byte{1})
|
||||
c1ID = NewNodeID([]byte{2})
|
||||
)
|
||||
|
||||
func TestHeaderFunctions(t *testing.T) {
|
||||
f := func(ver, id, typ int) bool {
|
||||
ver = int(uint(ver) % 16)
|
||||
@@ -54,8 +59,8 @@ func TestPing(t *testing.T) {
|
||||
ar, aw := io.Pipe()
|
||||
br, bw := io.Pipe()
|
||||
|
||||
c0 := NewConnection("c0", ar, bw, nil).(wireFormatConnection).next.(*rawConnection)
|
||||
c1 := NewConnection("c1", br, aw, nil).(wireFormatConnection).next.(*rawConnection)
|
||||
c0 := NewConnection(c0ID, ar, bw, nil).(wireFormatConnection).next.(*rawConnection)
|
||||
c1 := NewConnection(c1ID, br, aw, nil).(wireFormatConnection).next.(*rawConnection)
|
||||
|
||||
if ok := c0.ping(); !ok {
|
||||
t.Error("c0 ping failed")
|
||||
@@ -78,8 +83,8 @@ func TestPingErr(t *testing.T) {
|
||||
eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
|
||||
ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
|
||||
|
||||
c0 := NewConnection("c0", ar, ebw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||
NewConnection("c1", br, eaw, m1)
|
||||
c0 := NewConnection(c0ID, ar, ebw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||
NewConnection(c1ID, br, eaw, m1)
|
||||
|
||||
res := c0.ping()
|
||||
if (i < 4 || j < 4) && res {
|
||||
@@ -106,8 +111,8 @@ func TestPingErr(t *testing.T) {
|
||||
// eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
|
||||
// ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
|
||||
|
||||
// NewConnection("c0", ar, ebw, m0, nil)
|
||||
// c1 := NewConnection("c1", br, eaw, m1, nil).(wireFormatConnection).next.(*rawConnection)
|
||||
// NewConnection(c0ID, ar, ebw, m0, nil)
|
||||
// c1 := NewConnection(c1ID, br, eaw, m1, nil).(wireFormatConnection).next.(*rawConnection)
|
||||
|
||||
// d, err := c1.Request("default", "tn", 1234, 5678)
|
||||
// if err == e || err == ErrClosed {
|
||||
@@ -154,8 +159,8 @@ func TestVersionErr(t *testing.T) {
|
||||
ar, aw := io.Pipe()
|
||||
br, bw := io.Pipe()
|
||||
|
||||
c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||
NewConnection("c1", br, aw, m1)
|
||||
c0 := NewConnection(c0ID, ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||
NewConnection(c1ID, br, aw, m1)
|
||||
|
||||
c0.xw.WriteUint32(encodeHeader(header{
|
||||
version: 2,
|
||||
@@ -176,8 +181,8 @@ func TestTypeErr(t *testing.T) {
|
||||
ar, aw := io.Pipe()
|
||||
br, bw := io.Pipe()
|
||||
|
||||
c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||
NewConnection("c1", br, aw, m1)
|
||||
c0 := NewConnection(c0ID, ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||
NewConnection(c1ID, br, aw, m1)
|
||||
|
||||
c0.xw.WriteUint32(encodeHeader(header{
|
||||
version: 0,
|
||||
@@ -198,8 +203,8 @@ func TestClose(t *testing.T) {
|
||||
ar, aw := io.Pipe()
|
||||
br, bw := io.Pipe()
|
||||
|
||||
c0 := NewConnection("c0", ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||
NewConnection("c1", br, aw, m1)
|
||||
c0 := NewConnection(c0ID, ar, bw, m0).(wireFormatConnection).next.(*rawConnection)
|
||||
NewConnection(c1ID, br, aw, m1)
|
||||
|
||||
c0.close(nil)
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ type wireFormatConnection struct {
|
||||
next Connection
|
||||
}
|
||||
|
||||
func (c wireFormatConnection) ID() string {
|
||||
func (c wireFormatConnection) ID() NodeID {
|
||||
return c.next.ID()
|
||||
}
|
||||
|
||||
func (c wireFormatConnection) Index(node string, fs []FileInfo) {
|
||||
func (c wireFormatConnection) Index(repo string, fs []FileInfo) {
|
||||
var myFs = make([]FileInfo, len(fs))
|
||||
copy(myFs, fs)
|
||||
|
||||
@@ -26,7 +26,7 @@ func (c wireFormatConnection) Index(node string, fs []FileInfo) {
|
||||
myFs[i].Name = norm.NFC.String(filepath.ToSlash(myFs[i].Name))
|
||||
}
|
||||
|
||||
c.next.Index(node, myFs)
|
||||
c.next.Index(repo, myFs)
|
||||
}
|
||||
|
||||
func (c wireFormatConnection) Request(repo, name string, offset int64, size int) ([]byte, error) {
|
||||
|
||||
Reference in New Issue
Block a user