Merge MessageHdr into Message and make Ipv6Address a BasePrimitive
The former makes setting payload_length sensibly much easier, with no drawbacks that I can see, while the latter lets us override assign to make comparisons between instances sane. All good.
This commit is contained in:
@@ -4,41 +4,58 @@ require 'ipaddress'
|
|||||||
# Wraps the IPAddress::IPv6 class found in the ipaddress gem to provide easier
|
# Wraps the IPAddress::IPv6 class found in the ipaddress gem to provide easier
|
||||||
# handling of binary-format IP addresses
|
# handling of binary-format IP addresses
|
||||||
# @author Nick Thomas <nick@lupine.me.uk>
|
# @author Nick Thomas <nick@lupine.me.uk>
|
||||||
class Ipv6Address < BinData::Primitive
|
class Ipv6Address < BinData::BasePrimitive
|
||||||
endian :big
|
register_self if BinData::VERSION < "1.3.2"
|
||||||
|
|
||||||
uint128 :address
|
|
||||||
|
|
||||||
def get
|
def value_to_binary_string(v)
|
||||||
if address
|
ip = any_to_ipaddress(v)
|
||||||
if address >= 0xffff00000000 && address <= 0xffffffffffff # v6-mapped v4
|
raise ArgumentError.new("Can't convert #{v.class} to Ipv6Address") unless ip
|
||||||
IPAddress::IPv6::Mapped.parse_u128(address)
|
ip.data
|
||||||
else # v6
|
end
|
||||||
IPAddress::IPv6.parse_u128(address)
|
|
||||||
end
|
def read_and_return_value(io)
|
||||||
else
|
address = read_uint128(io)
|
||||||
nil # Nothing set
|
if address >= 0xffff00000000 && address <= 0xffffffffffff # v6-mapped v4
|
||||||
end
|
IPAddress::IPv6::Mapped.parse_u128(address)
|
||||||
|
else # v6
|
||||||
|
IPAddress::IPv6.parse_u128(address)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sensible_default
|
||||||
|
IPAddress("::")
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign(v)
|
||||||
|
super(any_to_ipaddress(v))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set(v)
|
protected
|
||||||
|
|
||||||
|
def any_to_ipaddress(v)
|
||||||
v = IPAddress(v) if v.is_a?(String)
|
v = IPAddress(v) if v.is_a?(String)
|
||||||
v = IPAddress::IPv6::Mapped.new(v.to_s) if v.is_a?(IPAddress::IPv4)
|
|
||||||
if v.is_a?(Fixnum) || v.is_a?(Bignum)
|
|
||||||
v = if v < 2**32
|
|
||||||
IPAddress::IPv6::Mapped.parse_u128(v)
|
|
||||||
else
|
|
||||||
IPAddress::IPv6::parse_u128(v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
if v.respond_to?(:to_u128)
|
if v.is_a?(IPAddress::IPv4)
|
||||||
self.address = v.to_u128
|
IPAddress::IPv6::Mapped.new("::ffff:#{v.to_s}")
|
||||||
|
elsif v.is_a?(IPAddress::IPv6)
|
||||||
|
v
|
||||||
|
elsif v.is_a?(Fixnum) || v.is_a?(Bignum) || v.respond_to?(:to_u128)
|
||||||
|
v = v.to_u128 if v.respond_to?(:to_u128)
|
||||||
|
if v < 2**32
|
||||||
|
IPAddress::IPv6::Mapped.new("::ffff:#{IPAddress::IPv4.parse_u32(v).to_s}")
|
||||||
|
else
|
||||||
|
IPAddress::IPv6.parse_u128(v)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
raise ArgumentError.new("Can't set #{v.class} to an IPv6Address")
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def read_uint128(io)
|
||||||
|
top, middle1, middle2, bottom = io.readbytes(16).unpack("NNNN")
|
||||||
|
(top << 96) | (middle1 << 64) | (middle2 << 32) | (bottom)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Implementation of the BitCoin wire protocol, written using bindata.
|
# Implementation of the BitCoin wire protocol, written using bindata.
|
||||||
@@ -395,30 +412,6 @@ module BtcWireProto
|
|||||||
|
|
||||||
## Top-level message format ##
|
## Top-level message format ##
|
||||||
|
|
||||||
# Found at the start of all Bitcoin messages.
|
|
||||||
# @author Nick Thomas <nick@lupine.me.uk>
|
|
||||||
class MessageHdr < BinData::Record
|
|
||||||
endian :little
|
|
||||||
uint32 :magic
|
|
||||||
string :command, :length => 12
|
|
||||||
uint32 :payload_len
|
|
||||||
uint32 :checksum, :onlyif => :has_checksum?
|
|
||||||
|
|
||||||
def cmd_sym
|
|
||||||
c = command.strip
|
|
||||||
c == "" ? nil : c.to_sym
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
# version and verack messages don't have a checksum. The rest do.
|
|
||||||
# @return[Boolean] does this message header have a checksum field or not?
|
|
||||||
def has_checksum?
|
|
||||||
!%w|version verack|.include?(command.strip)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Everything on the wire is a Message.
|
# Everything on the wire is a Message.
|
||||||
# @author Nick Thomas <nick@lupine.me.uk>
|
# @author Nick Thomas <nick@lupine.me.uk>
|
||||||
class Message < BinData::Record
|
class Message < BinData::Record
|
||||||
@@ -430,11 +423,11 @@ module BtcWireProto
|
|||||||
@version = v || ::BtcWireProto::CURRENT_VERSION(:main)
|
@version = v || ::BtcWireProto::CURRENT_VERSION(:main)
|
||||||
end
|
end
|
||||||
|
|
||||||
message_hdr :header
|
endian :little
|
||||||
|
uint32 :magic
|
||||||
def cmd_sym
|
string :command, :length => 12
|
||||||
header ? header.cmd_sym : nil
|
uint32 :payload_len, :value => lambda { payload.num_bytes }
|
||||||
end
|
uint32 :checksum, :onlyif => :has_checksum?
|
||||||
|
|
||||||
choice :payload, :selection => :payload_choice do
|
choice :payload, :selection => :payload_choice do
|
||||||
version "version"
|
version "version"
|
||||||
@@ -453,10 +446,20 @@ module BtcWireProto
|
|||||||
null_payload "null"
|
null_payload "null"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cmd_sym
|
||||||
|
c = command.strip
|
||||||
|
c == "" ? nil : c.to_sym
|
||||||
|
end
|
||||||
|
# version and verack messages don't have a checksum. The rest do.
|
||||||
|
# @return[Boolean] does this message header have a checksum field or not?
|
||||||
|
def has_checksum?
|
||||||
|
!%w|version verack|.include?(command.to_s.strip)
|
||||||
|
end
|
||||||
|
|
||||||
# Works out what the payload looks like based on the MessageHdr struct
|
# Works out what the payload looks like based on the MessageHdr struct
|
||||||
# and (potentially) the version
|
# and (potentially) the version
|
||||||
def payload_choice
|
def payload_choice
|
||||||
cmd = header.command.to_s.strip
|
cmd = command.to_s.strip
|
||||||
return cmd if %w{
|
return cmd if %w{
|
||||||
version inv getdata getblocks getheaders tx block headers alert
|
version inv getdata getblocks getheaders tx block headers alert
|
||||||
}.include?(cmd)
|
}.include?(cmd)
|
||||||
|
@@ -6,58 +6,50 @@ require 'btc_wire_proto'
|
|||||||
include ::BtcWireProto
|
include ::BtcWireProto
|
||||||
|
|
||||||
describe Ipv6Address do
|
describe Ipv6Address do
|
||||||
describe "set" do
|
|
||||||
before(:each) { @ip = Ipv6Address.new }
|
|
||||||
|
|
||||||
it "should parse IPv4 strings into IPAddress::IPv6::Mapped objects" do
|
it "should read IPv6 strings into IPv6 addresses" do
|
||||||
@ip.set("127.0.0.1")
|
ex_obj = IPAddress("fea1:fea1:fea1:fea1:fea1:fea1:fea1:fea1")
|
||||||
@ip.get.should be_a(IPAddress::IPv6::Mapped)
|
ex = ex_obj.data
|
||||||
@ip.get.to_s.should == "::ffff:127.0.0.1"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should parse IPv6 strings into IPAddress::IPv6 objects" do
|
ip = Ipv6Address::read(ex)
|
||||||
@ip.set("fe80::1")
|
ip.should == ex_obj
|
||||||
@ip.get.should be_a(IPAddress::IPv6)
|
ip.to_binary_s.should == ex
|
||||||
@ip.get.to_s.should == "fe80::1"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should accept IPAddress::IPv(4|6) objects" do
|
|
||||||
@ip.set(IPAddress("127.0.0.1"))
|
|
||||||
@ip.get.should be_a(IPAddress::IPv6::Mapped)
|
|
||||||
@ip.get.to_s.should == "::ffff:127.0.0.1"
|
|
||||||
|
|
||||||
@ip.set(IPAddress("fe80::1"))
|
|
||||||
@ip.get.should be_a(IPAddress::IPv6)
|
|
||||||
@ip.get.to_s.should == "fe80::1"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should accept arbitrary objects with #to_u128" do
|
ip = Ipv6Address.new(ex_obj)
|
||||||
o = Object.new
|
ip.to_binary_s.should == ex
|
||||||
class << o
|
end
|
||||||
def to_u128 ; 666 ; end
|
|
||||||
end
|
it "should read IPv4 strings into IPv6-mapped addresses" do
|
||||||
|
ip = Ipv6Address.new("127.0.0.1")
|
||||||
|
ip.should == IPAddress("::ffff:127.0.0.1")
|
||||||
|
end
|
||||||
|
|
||||||
@ip.set(o)
|
it "should read IPAddress objects into IPv6/mapped addresses" do
|
||||||
@ip.address.should == 666
|
ip = Ipv6Address.new(IPAddress("127.0.0.1"))
|
||||||
|
ip.should == (ip_m = IPAddress("::ffff:127.0.0.1"))
|
||||||
|
ip.to_binary_s.should == ip_m.data
|
||||||
|
end
|
||||||
|
|
||||||
end
|
it "should read arbitrary objects with #to_u128 into IPv6/mapped addresses" do
|
||||||
|
o = Object.new
|
||||||
|
class << o ; def to_u128 ; 0xffffffffffff ; end ; end
|
||||||
|
ip = Ipv6Address.new(o)
|
||||||
|
ip.should == IPAddress("::ffff:ffff:ffff")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should read Fixnums and Bignums into IPv6/mapped addresses" do
|
||||||
|
ipv4 = IPAddress("127.0.0.1") # Should still be mapped
|
||||||
|
ipv6 = IPAddress("fe80::1")
|
||||||
|
|
||||||
it "should treat Fixnums and Bignums as decimal IPs" do
|
ip = Ipv6Address.new(ipv4.to_u32)
|
||||||
ipv4 = IPAddress("127.0.0.1") # Should still be mapped
|
ip.should == IPAddress("::ffff:127.0.0.1")
|
||||||
ipv6 = IPAddress("fe80::1")
|
|
||||||
|
ip = Ipv6Address.new(ipv6.to_u128)
|
||||||
@ip.set(ipv4.to_u32)
|
ip.should == ipv6
|
||||||
@ip.get.should be_a(IPAddress::IPv6::Mapped)
|
end
|
||||||
@ip.get.to_s.should == "::ffff:127.0.0.1"
|
|
||||||
|
|
||||||
@ip.set(ipv6.to_u128)
|
|
||||||
@ip.get.should be_a(IPAddress::IPv6)
|
|
||||||
@ip.get.to_s.should == "fe80::1"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should raise an ArgumentError when it can't parse objects" do
|
it "should raise ArgumentError for unparseable objects" do
|
||||||
lambda { @ip.set("bad IP") }.should raise_error(ArgumentError)
|
lambda { Ipv6Address.new("bad IP") }.should raise_error(ArgumentError)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -281,34 +273,36 @@ describe ::BtcWireProto do
|
|||||||
|
|
||||||
|
|
||||||
# Messages
|
# Messages
|
||||||
describe MessageHdr do
|
GOOD_VERSION_DATA = binary(%w{
|
||||||
end
|
F9 BE B4 D9 76 65 72 73 69 6F 6E 00 00 00 00 00 55 00 00
|
||||||
|
00 9C 7C 00 00 01 00 00 00 00 00 00 00 E6 15 10 4D 00 00
|
||||||
|
00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||||
|
00 FF FF 0A 00 00 01 DA F6 01 00 00 00 00 00 00 00 00 00
|
||||||
|
00 00 00 00 00 00 00 00 FF FF 0A 00 00 02 20 8D DD 9D 20
|
||||||
|
2C 3A B4 57 13 00 55 81 01 00
|
||||||
|
})
|
||||||
|
|
||||||
|
SRV = { :node_network => 1 }
|
||||||
|
|
||||||
describe Message do
|
describe Message do
|
||||||
context "Version message" do
|
context "Version message" do
|
||||||
it "should have a Version payload" do
|
it "should have a Version payload" do
|
||||||
m = Message::new(:header => {:command => 'version'})
|
m = Message::new(:command => 'version')
|
||||||
m.payload.selection.should == "version"
|
m.payload.selection.should == "version"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should have a :version command symbol" do
|
it "should have a :version command symbol" do
|
||||||
m = Message::new(:header => {:command => 'version'})
|
m = Message::new(:command => 'version')
|
||||||
m.cmd_sym.should == :version
|
m.cmd_sym.should == :version
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should parse binary data correctly" do
|
it "should parse binary data correctly" do
|
||||||
m = Message::read(binary(%w{
|
m = Message::read(GOOD_VERSION_DATA)
|
||||||
F9 BE B4 D9 76 65 72 73 69 6F 6E 00 00 00 00 00 55 00 00
|
|
||||||
00 9C 7C 00 00 01 00 00 00 00 00 00 00 E6 15 10 4D 00 00
|
m.magic.should == BtcWireProto::NETWORKS[:main]
|
||||||
00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
m.command.should == "version\x00\x00\x00\x00\x00"
|
||||||
00 FF FF 0A 00 00 01 DA F6 01 00 00 00 00 00 00 00 00 00
|
m.payload_len.should == 85
|
||||||
00 00 00 00 00 00 00 00 FF FF 0A 00 00 02 20 8D DD 9D 20
|
m.has_parameter?(:checksum).should be_false
|
||||||
2C 3A B4 57 13 00 55 81 01 00
|
|
||||||
}))
|
|
||||||
m.header.magic.should == BtcWireProto::NETWORKS[:main]
|
|
||||||
m.header.command.should == "version\x00\x00\x00\x00\x00"
|
|
||||||
m.header.payload_len.should == 85
|
|
||||||
m.header.has_parameter?(:checksum).should be_false
|
|
||||||
m.payload.version.should == 31900
|
m.payload.version.should == 31900
|
||||||
m.payload.services.node_network.should == 1
|
m.payload.services.node_network.should == 1
|
||||||
m.payload.timestamp.should == 1292899814
|
m.payload.timestamp.should == 1292899814
|
||||||
@@ -317,29 +311,50 @@ describe ::BtcWireProto do
|
|||||||
m.payload.nonce.should == 0x1357B43A2C209DDD
|
m.payload.nonce.should == 0x1357B43A2C209DDD
|
||||||
m.payload.sub_version.should == ""
|
m.payload.sub_version.should == ""
|
||||||
m.payload.start_height.should == 98645
|
m.payload.start_height.should == 98645
|
||||||
|
m.to_binary_s.should == GOOD_VERSION_DATA
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should generate binary data correctly" do
|
||||||
|
m = Message::read(GOOD_VERSION_DATA)
|
||||||
|
ex = Message::new(
|
||||||
|
:magic => BtcWireProto::NETWORKS[:main], :command => 'version',
|
||||||
|
:payload => {
|
||||||
|
:version => 31900, :services => {:node_network => 1},
|
||||||
|
:timestamp => 1292899814, :nonce => 0x1357B43A2C209DDD,
|
||||||
|
:sub_version => "", :start_height => 98645,
|
||||||
|
:addr_me => { :services => SRV, :ip => "10.0.0.1", :port => 56054 },
|
||||||
|
:addr_you => { :services => SRV, :ip => "10.0.0.2", :port => 8333 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
m.should == ex
|
||||||
|
ex.to_binary_s.should == GOOD_VERSION_DATA
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
GOOD_VERACK_DATA = binary(%w{
|
||||||
|
F9 BE B4 D9 76 65 72 61 63 6B 00 00 00 00 00 00 00 00 00 00
|
||||||
|
})
|
||||||
|
|
||||||
context "Verack message" do
|
context "Verack message" do
|
||||||
|
|
||||||
it "should have no payload" do
|
it "should have no payload" do
|
||||||
m = Message.new(:header => {:command => "verack"})
|
m = Message.new(:command => "verack")
|
||||||
m.payload.selection.should == "null"
|
m.payload.selection.should == "null"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should have a :verack command symbol" do
|
it "should have a :verack command symbol" do
|
||||||
m = Message.new(:header => {:command => "verack"})
|
m = Message.new(:command => "verack")
|
||||||
m.cmd_sym.should == :verack
|
m.cmd_sym.should == :verack
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should parse the binary data correctly" do
|
it "should parse the binary data correctly" do
|
||||||
m = Message::read(binary(%w{
|
m = Message::read(GOOD_VERACK_DATA)
|
||||||
F9 BE B4 D9 76 65 72 61 63 6B 00 00 00 00 00 00 00 00 00 00
|
m.magic.should == BtcWireProto::NETWORKS[:main]
|
||||||
}))
|
m.command.should == "verack\x00\x00\x00\x00\x00\x00"
|
||||||
m.header.magic.should == BtcWireProto::NETWORKS[:main]
|
m.payload_len.should == 0
|
||||||
m.header.command.should == "verack\x00\x00\x00\x00\x00\x00"
|
m.has_parameter?(:chcksum).should be_false
|
||||||
m.header.payload_len.should == 0
|
m.to_binary_s.should == GOOD_VERACK_DATA
|
||||||
m.header.has_parameter?(:chcksum).should be_false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user