REBASE!
This commit is contained in:
208
protocol/PROTOCOL.md
Normal file
208
protocol/PROTOCOL.md
Normal file
@@ -0,0 +1,208 @@
|
||||
Block Exchange Protocol v1.0
|
||||
============================
|
||||
|
||||
Introduction and Definitions
|
||||
----------------------------
|
||||
|
||||
The BEP is used between two or more _nodes_ thus forming a _cluster_.
|
||||
Each node has a _repository_ of files described by the _local model_,
|
||||
containing modifications times and block hashes. The local model is sent
|
||||
to the other nodes in the cluster. The union of all files in the local
|
||||
models, with files selected for most recent modification time, forms the
|
||||
_global model_. Each node strives to get it's repository in synch with
|
||||
the global model by requesting missing blocks from the other nodes.
|
||||
|
||||
Transport and Authentication
|
||||
----------------------------
|
||||
|
||||
The BEP itself does not provide retransmissions, compression, encryption
|
||||
nor authentication. It is expected that this is performed at lower
|
||||
layers of the networking stack. A typical deployment stack should be
|
||||
similar to the following:
|
||||
|
||||
|-----------------------------|
|
||||
| Block Exchange Protocol |
|
||||
|-----------------------------|
|
||||
| Compression (RFC 1951) |
|
||||
|-----------------------------|
|
||||
| Encryption & Auth (TLS 1.0) |
|
||||
|-----------------------------|
|
||||
| TCP |
|
||||
|-----------------------------|
|
||||
v v
|
||||
|
||||
The exact nature of the authentication is up to the application.
|
||||
Possibilities include certificates signed by a common trusted CA,
|
||||
preshared certificates, preshared certificate fingerprints or
|
||||
certificate pinning combined with some out of band first verification.
|
||||
|
||||
There is no required order or synchronization among BEP messages - any
|
||||
message type may be sent at any time and the sender need not await a
|
||||
response to one message before sending another. Responses must however
|
||||
be sent in the same order as the requests are received.
|
||||
|
||||
Compression is started directly after a successfull TLS handshake,
|
||||
before the first message is sent. The compression is flushed at each
|
||||
message boundary.
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
||||
Every message starts with one 32 bit word indicating the message version
|
||||
and type. For BEP v1.0 the Version field is set to zero. Future versions
|
||||
with incompatible message formats will increment the Version field. The
|
||||
reserved bits must be set to zero.
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Ver=0 | Message ID | Type | Reserved |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
All data following the message header is in XDR (RFC 1014) encoding.
|
||||
The actual data types in use by BEP, in XDR naming convention, are:
|
||||
|
||||
- unsigned int -- unsigned 32 bit integer
|
||||
- hyper -- signed 64 bit integer
|
||||
- unsigned hyper -- signed 64 bit integer
|
||||
- opaque<> -- variable length opaque data
|
||||
- string<> -- variable length string
|
||||
|
||||
The encoding of opaque<> and string<> are identical, the distinction is
|
||||
solely in interpretation. Opaque data should not be interpreted as such,
|
||||
but can be compared bytewise to other opaque data. All strings use the
|
||||
UTF-8 encoding.
|
||||
|
||||
### Index (Type = 1)
|
||||
|
||||
The Index message defines the contents of the senders repository. A Index
|
||||
message is sent by each peer immediately upon connection and whenever the
|
||||
local repository contents changes. However, if a peer has no data to
|
||||
advertise (the repository is empty, or it is set to only import data) it
|
||||
is allowed but not required to send an empty Index message (a file list of
|
||||
zero length). If the repository contents change from non-empty to empty,
|
||||
an empty Index message must be sent. There is no response to the Index
|
||||
message.
|
||||
|
||||
struct IndexMessage {
|
||||
FileInfo Files<>;
|
||||
}
|
||||
|
||||
struct FileInfo {
|
||||
string Name<>;
|
||||
unsigned int Flags;
|
||||
hyper Modified;
|
||||
BlockInfo Blocks<>;
|
||||
}
|
||||
|
||||
struct BlockInfo {
|
||||
unsigned int Length;
|
||||
opaque Hash<>
|
||||
}
|
||||
|
||||
The file name is the part relative to the repository root. The
|
||||
modification time is expressed as the number of seconds since the Unix
|
||||
Epoch. The hash algorithm is implied by the hash length. Currently, the
|
||||
hash must be 32 bytes long and computed by SHA256.
|
||||
|
||||
The flags field is made up of the following single bit flags:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Reserved |D| Unix Perm. & Mode |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
- The lower 12 bits hold the common Unix permission and mode bits.
|
||||
|
||||
- Bit 19 ("D") is set when the file has been deleted. The block list
|
||||
shall contain zero blocks and the modification time indicates the
|
||||
time of deletion or, if deletion time is not reliably determinable,
|
||||
one second past the last know modification time.
|
||||
|
||||
- Bit 0 through 18 are reserved for future use and shall be set to
|
||||
zero.
|
||||
|
||||
### Request (Type = 2)
|
||||
|
||||
The Request message expresses the desire to receive a data block
|
||||
corresponding to a part of a certain file in the peer's repository.
|
||||
|
||||
The requested block must correspond exactly to one block seen in the
|
||||
peer's Index message. The hash field must be set to the expected value by
|
||||
the sender. The receiver may validate that this is actually the case
|
||||
before transmitting data. Each Request message must be met with a Response
|
||||
message.
|
||||
|
||||
struct RequestMessage {
|
||||
string Name<>;
|
||||
unsigned hyper Offset;
|
||||
unsigned int Length;
|
||||
opaque Hash<>;
|
||||
}
|
||||
|
||||
The hash algorithm is implied by the hash length. Currently, the hash
|
||||
must be 32 bytes long and computed by SHA256.
|
||||
|
||||
The Message ID in the header must set to a unique value to be able to
|
||||
correlate the request with the response message.
|
||||
|
||||
### Response (Type = 3)
|
||||
|
||||
The Response message is sent in response to a Request message. In case the
|
||||
requested data was not available (an outdated block was requested, or
|
||||
the file has been deleted), the Data field is empty.
|
||||
|
||||
struct ResponseMessage {
|
||||
opaque Data<>
|
||||
}
|
||||
|
||||
The Message ID in the header is used to correlate requests and
|
||||
responses.
|
||||
|
||||
### Ping (Type = 4)
|
||||
|
||||
The Ping message is used to determine that a connection is alive, and to
|
||||
keep connections alive through state tracking network elements such as
|
||||
firewalls and NAT gateways. The Ping message has no contents.
|
||||
|
||||
struct PingMessage {
|
||||
}
|
||||
|
||||
### Pong (Type = 5)
|
||||
|
||||
The Pong message is sent in response to a Ping. The Pong message has no
|
||||
contents, but copies the Message ID from the Ping.
|
||||
|
||||
struct PongMessage {
|
||||
}
|
||||
|
||||
Example Exchange
|
||||
----------------
|
||||
|
||||
A B
|
||||
1. Index-> <-Index
|
||||
2. Request->
|
||||
3. Request->
|
||||
4. Request->
|
||||
5. Request->
|
||||
6. <-Response
|
||||
7. <-Response
|
||||
8. <-Response
|
||||
9. <-Response
|
||||
10. Index->
|
||||
...
|
||||
11. Ping->
|
||||
12. <-Pong
|
||||
|
||||
The connection is established and at 1. both peers send Index records.
|
||||
The Index records are received and both peers recompute their knowledge
|
||||
of the data in the cluster. In this example, peer A has four missing or
|
||||
outdated blocks. At 2 through 5 peer A sends requests for these blocks.
|
||||
The requests are received by peer B, who retrieves the data from the
|
||||
repository and transmits Response records (6 through 9). Node A updates
|
||||
their repository contents and transmits an updated Index message (10).
|
||||
Both peers enter idle state after 10. At some later time 11, peer A
|
||||
determines that it has not seen data from B for some time and sends a
|
||||
Ping request. A response is sent at 12.
|
||||
|
||||
119
protocol/marshal.go
Normal file
119
protocol/marshal.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/calmh/syncthing/buffers"
|
||||
)
|
||||
|
||||
func pad(l int) int {
|
||||
d := l % 4
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return 4 - d
|
||||
}
|
||||
|
||||
var padBytes = []byte{0, 0, 0}
|
||||
|
||||
type marshalWriter struct {
|
||||
w io.Writer
|
||||
tot int
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *marshalWriter) writeString(s string) {
|
||||
w.writeBytes([]byte(s))
|
||||
}
|
||||
|
||||
func (w *marshalWriter) writeBytes(bs []byte) {
|
||||
if w.err != nil {
|
||||
return
|
||||
}
|
||||
w.writeUint32(uint32(len(bs)))
|
||||
if w.err != nil {
|
||||
return
|
||||
}
|
||||
_, w.err = w.w.Write(bs)
|
||||
if p := pad(len(bs)); p > 0 {
|
||||
w.w.Write(padBytes[:p])
|
||||
}
|
||||
w.tot += len(bs) + pad(len(bs))
|
||||
}
|
||||
|
||||
func (w *marshalWriter) writeUint32(v uint32) {
|
||||
if w.err != nil {
|
||||
return
|
||||
}
|
||||
var b [4]byte
|
||||
b[0] = byte(v >> 24)
|
||||
b[1] = byte(v >> 16)
|
||||
b[2] = byte(v >> 8)
|
||||
b[3] = byte(v)
|
||||
_, w.err = w.w.Write(b[:])
|
||||
w.tot += 4
|
||||
}
|
||||
|
||||
func (w *marshalWriter) writeUint64(v uint64) {
|
||||
if w.err != nil {
|
||||
return
|
||||
}
|
||||
var b [8]byte
|
||||
b[0] = byte(v >> 56)
|
||||
b[1] = byte(v >> 48)
|
||||
b[2] = byte(v >> 40)
|
||||
b[3] = byte(v >> 32)
|
||||
b[4] = byte(v >> 24)
|
||||
b[5] = byte(v >> 16)
|
||||
b[6] = byte(v >> 8)
|
||||
b[7] = byte(v)
|
||||
_, w.err = w.w.Write(b[:])
|
||||
w.tot += 8
|
||||
}
|
||||
|
||||
type marshalReader struct {
|
||||
r io.Reader
|
||||
tot int
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *marshalReader) readString() string {
|
||||
bs := r.readBytes()
|
||||
defer buffers.Put(bs)
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func (r *marshalReader) readBytes() []byte {
|
||||
if r.err != nil {
|
||||
return nil
|
||||
}
|
||||
l := int(r.readUint32())
|
||||
if r.err != nil {
|
||||
return nil
|
||||
}
|
||||
b := buffers.Get(l + pad(l))
|
||||
_, r.err = io.ReadFull(r.r, b)
|
||||
r.tot += int(l + pad(l))
|
||||
return b[:l]
|
||||
}
|
||||
|
||||
func (r *marshalReader) readUint32() uint32 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
var b [4]byte
|
||||
_, r.err = io.ReadFull(r.r, b[:])
|
||||
r.tot += 4
|
||||
return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
|
||||
}
|
||||
|
||||
func (r *marshalReader) readUint64() uint64 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
var b [8]byte
|
||||
_, r.err = io.ReadFull(r.r, b[:])
|
||||
r.tot += 8
|
||||
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
|
||||
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
|
||||
}
|
||||
106
protocol/messages.go
Normal file
106
protocol/messages.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package protocol
|
||||
|
||||
import "io"
|
||||
|
||||
type request struct {
|
||||
name string
|
||||
offset uint64
|
||||
size uint32
|
||||
hash []byte
|
||||
}
|
||||
|
||||
type header struct {
|
||||
version int
|
||||
msgID int
|
||||
msgType int
|
||||
}
|
||||
|
||||
func encodeHeader(h header) uint32 {
|
||||
return uint32(h.version&0xf)<<28 +
|
||||
uint32(h.msgID&0xfff)<<16 +
|
||||
uint32(h.msgType&0xff)<<8
|
||||
}
|
||||
|
||||
func decodeHeader(u uint32) header {
|
||||
return header{
|
||||
version: int(u>>28) & 0xf,
|
||||
msgID: int(u>>16) & 0xfff,
|
||||
msgType: int(u>>8) & 0xff,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *marshalWriter) writeHeader(h header) {
|
||||
w.writeUint32(encodeHeader(h))
|
||||
}
|
||||
|
||||
func (w *marshalWriter) writeIndex(idx []FileInfo) {
|
||||
w.writeUint32(uint32(len(idx)))
|
||||
for _, f := range idx {
|
||||
w.writeString(f.Name)
|
||||
w.writeUint32(f.Flags)
|
||||
w.writeUint64(uint64(f.Modified))
|
||||
w.writeUint32(uint32(len(f.Blocks)))
|
||||
for _, b := range f.Blocks {
|
||||
w.writeUint32(b.Length)
|
||||
w.writeBytes(b.Hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WriteIndex(w io.Writer, idx []FileInfo) (int, error) {
|
||||
mw := marshalWriter{w, 0, nil}
|
||||
mw.writeIndex(idx)
|
||||
return mw.tot, mw.err
|
||||
}
|
||||
|
||||
func (w *marshalWriter) writeRequest(r request) {
|
||||
w.writeString(r.name)
|
||||
w.writeUint64(r.offset)
|
||||
w.writeUint32(r.size)
|
||||
w.writeBytes(r.hash)
|
||||
}
|
||||
|
||||
func (w *marshalWriter) writeResponse(data []byte) {
|
||||
w.writeBytes(data)
|
||||
}
|
||||
|
||||
func (r *marshalReader) readHeader() header {
|
||||
return decodeHeader(r.readUint32())
|
||||
}
|
||||
|
||||
func (r *marshalReader) readIndex() []FileInfo {
|
||||
nfiles := r.readUint32()
|
||||
files := make([]FileInfo, nfiles)
|
||||
for i := range files {
|
||||
files[i].Name = r.readString()
|
||||
files[i].Flags = r.readUint32()
|
||||
files[i].Modified = int64(r.readUint64())
|
||||
nblocks := r.readUint32()
|
||||
blocks := make([]BlockInfo, nblocks)
|
||||
for j := range blocks {
|
||||
blocks[j].Length = r.readUint32()
|
||||
blocks[j].Hash = r.readBytes()
|
||||
}
|
||||
files[i].Blocks = blocks
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func ReadIndex(r io.Reader) ([]FileInfo, error) {
|
||||
mr := marshalReader{r, 0, nil}
|
||||
idx := mr.readIndex()
|
||||
return idx, mr.err
|
||||
}
|
||||
|
||||
func (r *marshalReader) readRequest() request {
|
||||
var req request
|
||||
req.name = r.readString()
|
||||
req.offset = r.readUint64()
|
||||
req.size = r.readUint32()
|
||||
req.hash = r.readBytes()
|
||||
return req
|
||||
}
|
||||
|
||||
func (r *marshalReader) readResponse() []byte {
|
||||
return r.readBytes()
|
||||
}
|
||||
115
protocol/messages_test.go
Normal file
115
protocol/messages_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
idx := []FileInfo{
|
||||
{
|
||||
"Foo",
|
||||
0755,
|
||||
1234567890,
|
||||
[]BlockInfo{
|
||||
{12345678, []byte("hash hash hash")},
|
||||
{23456781, []byte("ash hash hashh")},
|
||||
{34567812, []byte("sh hash hashha")},
|
||||
},
|
||||
}, {
|
||||
"Quux/Quux",
|
||||
0644,
|
||||
2345678901,
|
||||
[]BlockInfo{
|
||||
{45678123, []byte("4321 hash hash hash")},
|
||||
{56781234, []byte("3214 ash hash hashh")},
|
||||
{67812345, []byte("2143 sh hash hashha")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var buf = new(bytes.Buffer)
|
||||
var wr = marshalWriter{buf, 0, nil}
|
||||
wr.writeIndex(idx)
|
||||
|
||||
var rd = marshalReader{buf, 0, nil}
|
||||
var idx2 = rd.readIndex()
|
||||
|
||||
if !reflect.DeepEqual(idx, idx2) {
|
||||
t.Errorf("Index marshal error:\n%#v\n%#v\n", idx, idx2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequest(t *testing.T) {
|
||||
f := func(name string, offset uint64, size uint32, hash []byte) bool {
|
||||
var buf = new(bytes.Buffer)
|
||||
var req = request{name, offset, size, hash}
|
||||
var wr = marshalWriter{buf, 0, nil}
|
||||
wr.writeRequest(req)
|
||||
var rd = marshalReader{buf, 0, nil}
|
||||
var req2 = rd.readRequest()
|
||||
return req.name == req2.name &&
|
||||
req.offset == req2.offset &&
|
||||
req.size == req2.size &&
|
||||
bytes.Compare(req.hash, req2.hash) == 0
|
||||
}
|
||||
if err := quick.Check(f, nil); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponse(t *testing.T) {
|
||||
f := func(data []byte) bool {
|
||||
var buf = new(bytes.Buffer)
|
||||
var wr = marshalWriter{buf, 0, nil}
|
||||
wr.writeResponse(data)
|
||||
var rd = marshalReader{buf, 0, nil}
|
||||
var read = rd.readResponse()
|
||||
return bytes.Compare(read, data) == 0
|
||||
}
|
||||
if err := quick.Check(f, nil); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteIndex(b *testing.B) {
|
||||
idx := []FileInfo{
|
||||
{
|
||||
"Foo",
|
||||
0777,
|
||||
1234567890,
|
||||
[]BlockInfo{
|
||||
{12345678, []byte("hash hash hash")},
|
||||
{23456781, []byte("ash hash hashh")},
|
||||
{34567812, []byte("sh hash hashha")},
|
||||
},
|
||||
}, {
|
||||
"Quux/Quux",
|
||||
0644,
|
||||
2345678901,
|
||||
[]BlockInfo{
|
||||
{45678123, []byte("4321 hash hash hash")},
|
||||
{56781234, []byte("3214 ash hash hashh")},
|
||||
{67812345, []byte("2143 sh hash hashha")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var wr = marshalWriter{ioutil.Discard, 0, nil}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wr.writeIndex(idx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteRequest(b *testing.B) {
|
||||
var req = request{"blah blah", 1231323, 13123123, []byte("hash hash hash")}
|
||||
var wr = marshalWriter{ioutil.Discard, 0, nil}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wr.writeRequest(req)
|
||||
}
|
||||
}
|
||||
239
protocol/protocol.go
Normal file
239
protocol/protocol.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/calmh/syncthing/buffers"
|
||||
)
|
||||
|
||||
const (
|
||||
messageTypeReserved = iota
|
||||
messageTypeIndex
|
||||
messageTypeRequest
|
||||
messageTypeResponse
|
||||
messageTypePing
|
||||
messageTypePong
|
||||
)
|
||||
|
||||
var ErrClosed = errors.New("Connection closed")
|
||||
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Flags uint32
|
||||
Modified int64
|
||||
Blocks []BlockInfo
|
||||
}
|
||||
|
||||
type BlockInfo struct {
|
||||
Length uint32
|
||||
Hash []byte
|
||||
}
|
||||
|
||||
type Model interface {
|
||||
// An index was received from the peer node
|
||||
Index(nodeID string, files []FileInfo)
|
||||
// A request was made by the peer node
|
||||
Request(nodeID, name string, offset uint64, size uint32, hash []byte) ([]byte, error)
|
||||
// The peer node closed the connection
|
||||
Close(nodeID string)
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
receiver Model
|
||||
reader io.Reader
|
||||
mreader *marshalReader
|
||||
writer io.Writer
|
||||
mwriter *marshalWriter
|
||||
wLock sync.RWMutex
|
||||
closed bool
|
||||
closedLock sync.RWMutex
|
||||
awaiting map[int]chan interface{}
|
||||
nextId int
|
||||
ID string
|
||||
}
|
||||
|
||||
func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model) *Connection {
|
||||
flrd := flate.NewReader(reader)
|
||||
flwr, err := flate.NewWriter(writer, flate.BestSpeed)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c := Connection{
|
||||
receiver: receiver,
|
||||
reader: flrd,
|
||||
mreader: &marshalReader{flrd, 0, nil},
|
||||
writer: flwr,
|
||||
mwriter: &marshalWriter{flwr, 0, nil},
|
||||
awaiting: make(map[int]chan interface{}),
|
||||
ID: nodeID,
|
||||
}
|
||||
|
||||
go c.readerLoop()
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
// Index writes the list of file information to the connected peer node
|
||||
func (c *Connection) Index(idx []FileInfo) {
|
||||
c.wLock.Lock()
|
||||
defer c.wLock.Unlock()
|
||||
|
||||
c.mwriter.writeHeader(header{0, c.nextId, messageTypeIndex})
|
||||
c.nextId = (c.nextId + 1) & 0xfff
|
||||
c.mwriter.writeIndex(idx)
|
||||
c.flush()
|
||||
}
|
||||
|
||||
// Request returns the bytes for the specified block after fetching them from the connected peer.
|
||||
func (c *Connection) Request(name string, offset uint64, size uint32, hash []byte) ([]byte, error) {
|
||||
c.wLock.Lock()
|
||||
rc := make(chan interface{})
|
||||
c.awaiting[c.nextId] = rc
|
||||
c.mwriter.writeHeader(header{0, c.nextId, messageTypeRequest})
|
||||
c.mwriter.writeRequest(request{name, offset, size, hash})
|
||||
c.flush()
|
||||
c.nextId = (c.nextId + 1) & 0xfff
|
||||
c.wLock.Unlock()
|
||||
|
||||
// Reading something that might be nil from a possibly closed channel...
|
||||
// r0<~
|
||||
|
||||
var data []byte
|
||||
i, ok := <-rc
|
||||
if ok {
|
||||
if d, ok := i.([]byte); ok {
|
||||
data = d
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
i, ok = <-rc
|
||||
if ok {
|
||||
if e, ok := i.(error); ok {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (c *Connection) Ping() bool {
|
||||
c.wLock.Lock()
|
||||
rc := make(chan interface{})
|
||||
c.awaiting[c.nextId] = rc
|
||||
c.mwriter.writeHeader(header{0, c.nextId, messageTypePing})
|
||||
c.flush()
|
||||
c.nextId = (c.nextId + 1) & 0xfff
|
||||
c.wLock.Unlock()
|
||||
|
||||
_, ok := <-rc
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *Connection) Stop() {
|
||||
}
|
||||
|
||||
type flusher interface {
|
||||
Flush() error
|
||||
}
|
||||
|
||||
func (c *Connection) flush() {
|
||||
if f, ok := c.writer.(flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connection) close() {
|
||||
c.closedLock.Lock()
|
||||
c.closed = true
|
||||
c.closedLock.Unlock()
|
||||
c.wLock.Lock()
|
||||
for _, ch := range c.awaiting {
|
||||
close(ch)
|
||||
}
|
||||
c.awaiting = nil
|
||||
c.wLock.Unlock()
|
||||
c.receiver.Close(c.ID)
|
||||
}
|
||||
|
||||
func (c *Connection) isClosed() bool {
|
||||
c.closedLock.RLock()
|
||||
defer c.closedLock.RUnlock()
|
||||
return c.closed
|
||||
}
|
||||
|
||||
func (c *Connection) readerLoop() {
|
||||
for !c.isClosed() {
|
||||
hdr := c.mreader.readHeader()
|
||||
if c.mreader.err != nil {
|
||||
c.close()
|
||||
break
|
||||
}
|
||||
|
||||
switch hdr.msgType {
|
||||
case messageTypeIndex:
|
||||
files := c.mreader.readIndex()
|
||||
if c.mreader.err != nil {
|
||||
c.close()
|
||||
} else {
|
||||
c.receiver.Index(c.ID, files)
|
||||
}
|
||||
|
||||
case messageTypeRequest:
|
||||
c.processRequest(hdr.msgID)
|
||||
|
||||
case messageTypeResponse:
|
||||
data := c.mreader.readResponse()
|
||||
|
||||
if c.mreader.err != nil {
|
||||
c.close()
|
||||
} else {
|
||||
c.wLock.RLock()
|
||||
rc, ok := c.awaiting[hdr.msgID]
|
||||
c.wLock.RUnlock()
|
||||
|
||||
if ok {
|
||||
rc <- data
|
||||
rc <- c.mreader.err
|
||||
delete(c.awaiting, hdr.msgID)
|
||||
close(rc)
|
||||
}
|
||||
}
|
||||
|
||||
case messageTypePing:
|
||||
c.wLock.Lock()
|
||||
c.mwriter.writeUint32(encodeHeader(header{0, hdr.msgID, messageTypePong}))
|
||||
c.flush()
|
||||
c.wLock.Unlock()
|
||||
|
||||
case messageTypePong:
|
||||
c.wLock.Lock()
|
||||
if rc, ok := c.awaiting[hdr.msgID]; ok {
|
||||
rc <- true
|
||||
close(rc)
|
||||
delete(c.awaiting, hdr.msgID)
|
||||
}
|
||||
c.wLock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connection) processRequest(msgID int) {
|
||||
req := c.mreader.readRequest()
|
||||
if c.mreader.err != nil {
|
||||
c.close()
|
||||
} else {
|
||||
go func() {
|
||||
data, _ := c.receiver.Request(c.ID, req.name, req.offset, req.size, req.hash)
|
||||
c.wLock.Lock()
|
||||
c.mwriter.writeUint32(encodeHeader(header{0, msgID, messageTypeResponse}))
|
||||
c.mwriter.writeResponse(data)
|
||||
buffers.Put(data)
|
||||
c.flush()
|
||||
c.wLock.Unlock()
|
||||
}()
|
||||
}
|
||||
}
|
||||
37
protocol/protocol_test.go
Normal file
37
protocol/protocol_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
func TestHeaderFunctions(t *testing.T) {
|
||||
f := func(ver, id, typ int) bool {
|
||||
ver = int(uint(ver) % 16)
|
||||
id = int(uint(id) % 4096)
|
||||
typ = int(uint(typ) % 256)
|
||||
h0 := header{ver, id, typ}
|
||||
h1 := decodeHeader(encodeHeader(h0))
|
||||
return h0 == h1
|
||||
}
|
||||
if err := quick.Check(f, nil); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPad(t *testing.T) {
|
||||
tests := [][]int{
|
||||
{0, 0},
|
||||
{1, 3},
|
||||
{2, 2},
|
||||
{3, 1},
|
||||
{4, 0},
|
||||
{32, 0},
|
||||
{33, 3},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if p := pad(tc[0]); p != tc[1] {
|
||||
t.Errorf("Incorrect padding for %d bytes, %d != %d", tc[0], p, tc[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user