ciri-p2p 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +15 -0
- data/.vscode/launch.json +90 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/bin/bundle +105 -0
- data/bin/console +14 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/ciri-p2p.gemspec +37 -0
- data/lib/ciri/p2p.rb +7 -0
- data/lib/ciri/p2p/address.rb +51 -0
- data/lib/ciri/p2p/dial_scheduler.rb +73 -0
- data/lib/ciri/p2p/dialer.rb +55 -0
- data/lib/ciri/p2p/discovery/protocol.rb +237 -0
- data/lib/ciri/p2p/discovery/service.rb +255 -0
- data/lib/ciri/p2p/errors.rb +36 -0
- data/lib/ciri/p2p/kad.rb +301 -0
- data/lib/ciri/p2p/network_state.rb +223 -0
- data/lib/ciri/p2p/node.rb +96 -0
- data/lib/ciri/p2p/peer.rb +151 -0
- data/lib/ciri/p2p/peer_store.rb +183 -0
- data/lib/ciri/p2p/protocol.rb +62 -0
- data/lib/ciri/p2p/protocol_context.rb +54 -0
- data/lib/ciri/p2p/protocol_io.rb +65 -0
- data/lib/ciri/p2p/rlpx.rb +29 -0
- data/lib/ciri/p2p/rlpx/connection.rb +182 -0
- data/lib/ciri/p2p/rlpx/encryption_handshake.rb +143 -0
- data/lib/ciri/p2p/rlpx/errors.rb +34 -0
- data/lib/ciri/p2p/rlpx/frame_io.rb +229 -0
- data/lib/ciri/p2p/rlpx/message.rb +45 -0
- data/lib/ciri/p2p/rlpx/protocol_handshake.rb +56 -0
- data/lib/ciri/p2p/rlpx/protocol_messages.rb +71 -0
- data/lib/ciri/p2p/rlpx/secrets.rb +49 -0
- data/lib/ciri/p2p/server.rb +159 -0
- data/lib/ciri/p2p/version.rb +5 -0
- metadata +229 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
# Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
|
24
|
+
|
25
|
+
require 'ciri/utils/logger'
|
26
|
+
|
27
|
+
module Ciri
|
28
|
+
module P2P
|
29
|
+
|
30
|
+
# PeerStore store information of all peers we have seen
|
31
|
+
#TODO rewrite with a database(sqlite)
|
32
|
+
# Support score peers
|
33
|
+
class PeerStore
|
34
|
+
PEER_LAST_SEEN_VALID = 12 * 3600 # consider peer is valid if we seen it within 12 hours
|
35
|
+
PING_EXPIRATION_IN = 10 * 60 # allow ping within 10 minutes
|
36
|
+
|
37
|
+
# report peer behaviours
|
38
|
+
module Behaviours
|
39
|
+
INVALID_DATA = :invalid_data
|
40
|
+
CONNECT = :connect
|
41
|
+
PING = :ping
|
42
|
+
FAILED_TO_CONNECT = :failed_to_connect
|
43
|
+
FAILED_TO_PING = :failed_to_ping
|
44
|
+
UNEXPECT_DISCONNECT = :unexpect_disconnect
|
45
|
+
end
|
46
|
+
|
47
|
+
include Behaviours
|
48
|
+
|
49
|
+
# peer status
|
50
|
+
module Status
|
51
|
+
CONNECTED = :connected
|
52
|
+
DISCONNECTED = :disconnected
|
53
|
+
UNKNOWN = :unknown
|
54
|
+
end
|
55
|
+
|
56
|
+
include Status
|
57
|
+
|
58
|
+
PEER_INITIAL_SCORE = 100
|
59
|
+
DEFAULT_SCORE_SCHEMA = {
|
60
|
+
INVALID_DATA => -50,
|
61
|
+
CONNECT => 10,
|
62
|
+
PING => 5,
|
63
|
+
FAILED_TO_PING => -10,
|
64
|
+
FAILED_TO_CONNECT => -10,
|
65
|
+
UNEXPECT_DISCONNECT => -20,
|
66
|
+
}
|
67
|
+
|
68
|
+
def initialize(score_schema:{})
|
69
|
+
@peers_ping_records = {}
|
70
|
+
@peers_seen_records = {}
|
71
|
+
@peers = {}
|
72
|
+
@bootnodes = []
|
73
|
+
@ban_peers = {}
|
74
|
+
@score_schema = DEFAULT_SCORE_SCHEMA.merge(score_schema)
|
75
|
+
end
|
76
|
+
|
77
|
+
def has_ping?(raw_node_id, ping_hash, expires_in: PING_EXPIRATION_IN)
|
78
|
+
return false if has_ban?(raw_node_id)
|
79
|
+
record = @peers_ping_records[raw_node_id]
|
80
|
+
if record && record[:ping_hash] == ping_hash && (record[:ping_at] + expires_in) > Time.now.to_i
|
81
|
+
return true
|
82
|
+
elsif record
|
83
|
+
@peers_ping_records.delete(raw_node_id)
|
84
|
+
end
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
# record ping message
|
89
|
+
def update_ping(raw_node_id, ping_hash, ping_at: Time.now.to_i)
|
90
|
+
@peers_ping_records[raw_node_id] = {ping_hash: ping_hash, ping_at: ping_at}
|
91
|
+
end
|
92
|
+
|
93
|
+
def update_last_seen(raw_node_id, at: Time.now.to_i)
|
94
|
+
@peers_seen_records[raw_node_id] = at
|
95
|
+
end
|
96
|
+
|
97
|
+
def has_seen?(raw_node_id, expires_in: PEER_LAST_SEEN_VALID)
|
98
|
+
return false if has_ban?(raw_node_id)
|
99
|
+
seen = (last_seen_at = @peers_seen_records[raw_node_id]) && (last_seen_at + expires_in > Time.now.to_i)
|
100
|
+
# convert to bool
|
101
|
+
!!seen
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_bootnode(node)
|
105
|
+
@bootnodes << node
|
106
|
+
add_node(node)
|
107
|
+
end
|
108
|
+
|
109
|
+
def has_ban?(raw_node_id, now: Time.now)
|
110
|
+
record = @ban_peers[raw_node_id]
|
111
|
+
if record && (record[:ban_at].to_i + record[:timeout_secs]) > now.to_i
|
112
|
+
true
|
113
|
+
else
|
114
|
+
@ban_peers.delete(raw_node_id)
|
115
|
+
false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def ban_peer(raw_node_id, now: Time.now, timeout_secs:600)
|
120
|
+
@ban_peers[raw_node_id] = {ban_at: now, timeout_secs: timeout_secs}
|
121
|
+
end
|
122
|
+
|
123
|
+
def report_peer(raw_node_id, behaviour)
|
124
|
+
score = @score_schema[behaviour]
|
125
|
+
raise ValueError.new("unsupport report behaviour: #{behaviour}") if score.nil?
|
126
|
+
if (node_info = @peers[raw_node_id])
|
127
|
+
node_info[:score] += score
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# TODO find high scoring peers, use bootnodes as fallback
|
132
|
+
def find_bootnodes(count)
|
133
|
+
nodes = @bootnodes.sample(count)
|
134
|
+
nodes + find_attempt_peers(count - nodes.size)
|
135
|
+
end
|
136
|
+
|
137
|
+
# TODO find high scoring peers
|
138
|
+
def find_attempt_peers(count)
|
139
|
+
@peers.values.reject do |peer_info|
|
140
|
+
# reject already connected peers and bootnodes
|
141
|
+
@bootnodes.include?(peer_info[:node]) || peer_status(peer_info[:node].raw_node_id) == Status::CONNECTED
|
142
|
+
end.sort_by do |peer_info|
|
143
|
+
-peer_info[:score]
|
144
|
+
end.map do |peer_info|
|
145
|
+
peer_info[:node]
|
146
|
+
end.take(count)
|
147
|
+
end
|
148
|
+
|
149
|
+
def add_node_addresses(raw_node_id, addresses)
|
150
|
+
node_info = @peers[raw_node_id]
|
151
|
+
node = node_info && node_info[:node]
|
152
|
+
if node
|
153
|
+
node.addresses = (node.addresses + addresses).uniq
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def get_node_addresses(raw_node_id)
|
158
|
+
peer_info = @peers[raw_node_id]
|
159
|
+
peer_info && peer_info[:node].addresses
|
160
|
+
end
|
161
|
+
|
162
|
+
def add_node(node)
|
163
|
+
@peers[node.raw_node_id] = {node: node, score: PEER_INITIAL_SCORE, status: Status::UNKNOWN}
|
164
|
+
end
|
165
|
+
|
166
|
+
def peer_status(raw_node_id)
|
167
|
+
if (peer_info = @peers[raw_node_id])
|
168
|
+
peer_info[:status]
|
169
|
+
else
|
170
|
+
Status::UNKNOWN
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def update_peer_status(raw_node_id, status)
|
175
|
+
if (peer_info = @peers[raw_node_id])
|
176
|
+
peer_info[:status] = status
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
# Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
|
24
|
+
|
25
|
+
require 'ciri/utils/logger'
|
26
|
+
|
27
|
+
module Ciri
|
28
|
+
module P2P
|
29
|
+
|
30
|
+
# protocol represent P2P sub protocols
|
31
|
+
class Protocol
|
32
|
+
|
33
|
+
include Utils::Logger
|
34
|
+
|
35
|
+
attr_reader :name, :version, :length
|
36
|
+
|
37
|
+
def initialize(name:, version:, length:)
|
38
|
+
@name = name
|
39
|
+
@version = version
|
40
|
+
@length = length
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialized(context)
|
44
|
+
debug("not implemented Protocol#initialized callback")
|
45
|
+
end
|
46
|
+
|
47
|
+
def received(context, data)
|
48
|
+
debug("not implemented Protocol#received callback")
|
49
|
+
end
|
50
|
+
|
51
|
+
def connected(context)
|
52
|
+
debug("not implemented Protocol#connected callback")
|
53
|
+
end
|
54
|
+
|
55
|
+
def disconnected(context)
|
56
|
+
debug("not implemented Protocol#disconnected callback")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
# Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
|
24
|
+
|
25
|
+
require 'forwardable'
|
26
|
+
|
27
|
+
module Ciri
|
28
|
+
module P2P
|
29
|
+
|
30
|
+
# ProtocolContext is used to manuaplate
|
31
|
+
class ProtocolContext
|
32
|
+
|
33
|
+
extend Forwardable
|
34
|
+
|
35
|
+
attr_reader :peer, :protocol, :protocol_io
|
36
|
+
|
37
|
+
def_delegators :protocol_io, :send_data
|
38
|
+
def_delegators :@network_state, :local_node_id
|
39
|
+
|
40
|
+
def initialize(network_state, peer: nil, protocol: nil, protocol_io: nil)
|
41
|
+
@network_state = network_state
|
42
|
+
@peer = peer
|
43
|
+
@protocol = protocol
|
44
|
+
@protocol_io = protocol_io
|
45
|
+
end
|
46
|
+
|
47
|
+
def raw_local_node_id
|
48
|
+
@raw_local_node_id ||= local_node_id.to_bytes
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
|
23
|
+
|
24
|
+
require 'forwardable'
|
25
|
+
require 'async/semaphore'
|
26
|
+
require_relative 'rlpx/message'
|
27
|
+
|
28
|
+
module Ciri
|
29
|
+
module P2P
|
30
|
+
|
31
|
+
# send/read sub protocol msg
|
32
|
+
class ProtocolIO
|
33
|
+
|
34
|
+
class Error < StandardError
|
35
|
+
end
|
36
|
+
class InvalidMessageCode < Error
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :protocol, :offset
|
40
|
+
|
41
|
+
def initialize(protocol, offset, frame_io)
|
42
|
+
@protocol = protocol
|
43
|
+
@offset = offset
|
44
|
+
@frame_io = frame_io
|
45
|
+
@semaphore = Async::Semaphore.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_data(code, data)
|
49
|
+
@semaphore.acquire do
|
50
|
+
msg = RLPX::Message.new(code: code, size: data.size, payload: data)
|
51
|
+
write_msg(msg)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def write_msg(msg)
|
56
|
+
raise InvalidMessageCode, "code #{msg.code} must less than length #{protocol.length}" if msg.code > protocol.length
|
57
|
+
msg.code += offset
|
58
|
+
@frame_io.write_msg(msg)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
|
4
|
+
# Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
|
24
|
+
require_relative 'rlpx/message'
|
25
|
+
require_relative 'rlpx/frame_io'
|
26
|
+
require_relative 'rlpx/protocol_messages'
|
27
|
+
require_relative 'rlpx/protocol_handshake'
|
28
|
+
require_relative 'rlpx/encryption_handshake'
|
29
|
+
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
|
23
|
+
|
24
|
+
require 'ciri/rlp'
|
25
|
+
require 'socket'
|
26
|
+
require 'forwardable'
|
27
|
+
require_relative 'frame_io'
|
28
|
+
require_relative 'protocol_messages'
|
29
|
+
require_relative 'errors'
|
30
|
+
require_relative 'encryption_handshake'
|
31
|
+
|
32
|
+
module Ciri
|
33
|
+
module P2P
|
34
|
+
module RLPX
|
35
|
+
|
36
|
+
# RLPX::Connection implement RLPX protocol operations
|
37
|
+
# all operations end with bang(!)
|
38
|
+
class Connection
|
39
|
+
extend Forwardable
|
40
|
+
|
41
|
+
def_delegators :@frame_io, :read_msg, :write_msg, :send_data, :closed?, :close
|
42
|
+
|
43
|
+
class Error < RLPX::Error
|
44
|
+
end
|
45
|
+
|
46
|
+
class MessageOverflowError < Error
|
47
|
+
end
|
48
|
+
|
49
|
+
class UnexpectedMessageError < Error
|
50
|
+
end
|
51
|
+
|
52
|
+
class FormatError < Error
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(io)
|
56
|
+
set_timeout(io)
|
57
|
+
@io = io
|
58
|
+
@frame_io = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Encryption handshake, exchange keys with node, must been invoked before other operations
|
62
|
+
def encryption_handshake!(private_key:, remote_node_id: nil)
|
63
|
+
enc_handshake = EncryptionHandshake.new(private_key: private_key, remote_id: remote_node_id)
|
64
|
+
secrets = remote_node_id.nil? ? receiver_enc_handshake(enc_handshake) : initiator_enc_handshake(enc_handshake)
|
65
|
+
@frame_io = FrameIO.new(@io, secrets)
|
66
|
+
end
|
67
|
+
|
68
|
+
# protocol handshake
|
69
|
+
def protocol_handshake!(our_hs)
|
70
|
+
@frame_io.send_data(Code::HANDSHAKE, our_hs.rlp_encode)
|
71
|
+
remote_hs = read_protocol_handshake
|
72
|
+
# enable snappy compress if remote peer support
|
73
|
+
@frame_io.snappy = remote_hs.version >= SNAPPY_PROTOCOL_VERSION
|
74
|
+
remote_hs
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def receiver_enc_handshake(receiver)
|
80
|
+
auth_msg_binary, auth_packet = read_enc_handshake_msg(ENC_AUTH_MSG_LENGTH, receiver.private_key)
|
81
|
+
auth_msg = AuthMsgV4.rlp_decode(auth_msg_binary)
|
82
|
+
receiver.handle_auth_msg(auth_msg)
|
83
|
+
|
84
|
+
auth_ack_msg = receiver.auth_ack_msg
|
85
|
+
auth_ack_msg_plain_text = auth_ack_msg.rlp_encode
|
86
|
+
auth_ack_packet = if auth_msg.got_plain
|
87
|
+
raise NotImplementedError.new('not support pre eip8 plain text seal')
|
88
|
+
else
|
89
|
+
seal_eip8(auth_ack_msg_plain_text, receiver)
|
90
|
+
end
|
91
|
+
@io.write(auth_ack_packet)
|
92
|
+
@io.flush
|
93
|
+
|
94
|
+
receiver.extract_secrets(auth_packet, auth_ack_packet, initiator: false)
|
95
|
+
end
|
96
|
+
|
97
|
+
def initiator_enc_handshake(initiator)
|
98
|
+
initiator_auth_msg = initiator.auth_msg
|
99
|
+
auth_msg_plain_text = initiator_auth_msg.rlp_encode
|
100
|
+
# seal eip8
|
101
|
+
auth_packet = seal_eip8(auth_msg_plain_text, initiator)
|
102
|
+
@io.write(auth_packet)
|
103
|
+
@io.flush
|
104
|
+
|
105
|
+
auth_ack_mgs_binary, auth_ack_packet = read_enc_handshake_msg(ENC_AUTH_RESP_MSG_LENGTH, initiator.private_key)
|
106
|
+
auth_ack_msg = AuthRespV4.rlp_decode auth_ack_mgs_binary
|
107
|
+
initiator.handle_auth_ack_msg(auth_ack_msg)
|
108
|
+
|
109
|
+
initiator.extract_secrets(auth_packet, auth_ack_packet, initiator: true)
|
110
|
+
end
|
111
|
+
|
112
|
+
def read_enc_handshake_msg(plain_size, private_key)
|
113
|
+
packet = @io.read(plain_size)
|
114
|
+
|
115
|
+
decrypt_binary_msg = begin
|
116
|
+
private_key.ecies_decrypt(packet)
|
117
|
+
rescue Crypto::ECIESDecryptionError => e
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
# pre eip old plain format
|
122
|
+
return decrypt_binary_msg if decrypt_binary_msg
|
123
|
+
|
124
|
+
# try decode eip8 format
|
125
|
+
prefix = packet[0...2]
|
126
|
+
size = Ciri::Utils.big_endian_decode(prefix)
|
127
|
+
raise FormatError.new("EIP8 format message size #{size} less than plain_size #{plain_size}") if size < plain_size
|
128
|
+
|
129
|
+
# continue read remain bytes
|
130
|
+
packet << @io.read(size - plain_size + 2)
|
131
|
+
# decrypt message
|
132
|
+
[private_key.ecies_decrypt(packet[2..-1], prefix), packet]
|
133
|
+
end
|
134
|
+
|
135
|
+
def read_protocol_handshake
|
136
|
+
msg = @frame_io.read_msg
|
137
|
+
|
138
|
+
if msg.size > BASE_PROTOCOL_MAX_MSG_SIZE
|
139
|
+
raise MessageOverflowError.new("message size #{msg.size} is too big")
|
140
|
+
end
|
141
|
+
if msg.code == Code::DISCONNECT
|
142
|
+
payload = RLP.decode(msg.payload)
|
143
|
+
raise UnexpectedMessageError.new("expected handshake, get disconnect, reason: #{payload}")
|
144
|
+
end
|
145
|
+
if msg.code != Code::HANDSHAKE
|
146
|
+
raise UnexpectedMessageError.new("expected handshake, get #{msg.code}")
|
147
|
+
end
|
148
|
+
ProtocolHandshake.rlp_decode(msg.payload)
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_timeout(io)
|
152
|
+
timeout = HANDSHAKE_TIMEOUT
|
153
|
+
|
154
|
+
if io.is_a?(BasicSocket)
|
155
|
+
secs = Integer(timeout)
|
156
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
157
|
+
optval = [secs, usecs].pack("l_2")
|
158
|
+
io.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
159
|
+
io.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def seal_eip8(encoded_msg, handshake)
|
164
|
+
# padding encoded message, make message distinguished from pre eip8
|
165
|
+
encoded_msg += "\x00".b * rand(100..300)
|
166
|
+
prefix = encoded_prefix(encoded_msg.size + ECIES_OVERHEAD)
|
167
|
+
|
168
|
+
enc = handshake.remote_key.ecies_encrypt(encoded_msg, prefix)
|
169
|
+
prefix + enc
|
170
|
+
end
|
171
|
+
|
172
|
+
# encode 16 uint prefix
|
173
|
+
def encoded_prefix(n)
|
174
|
+
prefix = Utils.big_endian_encode(n)
|
175
|
+
# pad to 2 bytes
|
176
|
+
prefix.ljust(2, "\x00".b)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|