ciri 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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 'logger'
25
+
26
+ module Ciri
27
+ module DevP2P
28
+
29
+ # simple actor model implementation
30
+ # Example:
31
+ #
32
+ # class Hello
33
+ # include Actor
34
+ #
35
+ # def say_hello
36
+ # puts 'hello world'
37
+ # 'hello world'
38
+ # end
39
+ # end
40
+ #
41
+ # actor = Hello.new
42
+ # # start actor loop
43
+ # actor.start
44
+ # # push message to actor inbox
45
+ # actor << :say_hello
46
+ # # push message and wait until get response
47
+ # actor.call(:say_hello).value
48
+ #
49
+ # # raise error
50
+ # actor.call(:hello).value # NoMethodError
51
+ #
52
+ # # stop actor
53
+ # actor.send_stop
54
+ # actor.wait
55
+ #
56
+ module Actor
57
+
58
+ LOGGER = Logger.new(STDERR, datetime_format: '%Y-%m-%d %H:%M:%S', level: Logger::INFO)
59
+
60
+ # future, use this to wait actor msg respond
61
+ class Future
62
+ def initialize
63
+ @value = nil
64
+ @done = false
65
+ end
66
+
67
+ def value=(val)
68
+ if @done
69
+ raise RuntimeError.new('future value duplicated set')
70
+ end
71
+ @done = true
72
+ @queue << :done if @queue
73
+ @value = val
74
+ end
75
+
76
+ def value
77
+ loop do
78
+ if @done
79
+ return @value
80
+ elsif @error
81
+ raise @error
82
+ else
83
+ queue.pop
84
+ end
85
+ end
86
+ end
87
+
88
+ def raise_error(error)
89
+ error.set_backtrace(caller) if error.backtrace.nil?
90
+ @error = error
91
+ @queue << :error if @queue
92
+ end
93
+
94
+ private
95
+ def queue
96
+ @queue ||= Queue.new
97
+ end
98
+ end
99
+
100
+ class Error < StandardError
101
+ end
102
+
103
+ # stop actor
104
+ class StopError < Error
105
+ end
106
+
107
+ class StateError < Error
108
+ end
109
+
110
+ attr_accessor :executor
111
+
112
+ def initialize(executor: nil)
113
+ @inbox = Queue.new
114
+ @executor = executor
115
+ @future = Future.new
116
+ @running = false
117
+ end
118
+
119
+ # async call
120
+ def enqueue(method, *args)
121
+ self << [method, *args]
122
+ end
123
+
124
+ def <<(args)
125
+ @inbox << args
126
+ end
127
+
128
+ # sync call, push msg to inbox, and return future
129
+ #
130
+ # Example:
131
+ # future = actor.call(:result) # future
132
+ # future.value # blocking and wait for result
133
+ #
134
+ def call(method, *args)
135
+ future = Future.new
136
+ self << [future, method, *args]
137
+ future
138
+ end
139
+
140
+ # start actor
141
+ def start
142
+ raise Error.new("must set executor before start") unless executor
143
+
144
+ @running = true
145
+ executor.post do
146
+ start_loop
147
+ end
148
+ end
149
+
150
+ # send stop to actor
151
+ #
152
+ # Example:
153
+ # actor.send_stop
154
+ # # wait for actor actually stopped
155
+ # actor.wait
156
+ #
157
+ def send_stop
158
+ self << [:raise_error, StopError.new]
159
+ end
160
+
161
+ # wait until an error occurs
162
+ def wait
163
+ raise StateError.new('actor not running!') unless @running
164
+ @future.value
165
+ end
166
+
167
+ # start loop
168
+ def start_loop
169
+ loop_callback do |wait_message: true|
170
+ # check inbox
171
+ next Thread.pass if @inbox.empty? && !wait_message
172
+ msg = @inbox.pop
173
+
174
+ # extract sync or async call
175
+ future = nil
176
+ method, *args = msg
177
+ if method.is_a?(Future)
178
+ future = method
179
+ method, *args = args
180
+ end
181
+ begin
182
+ val = send(method, *args)
183
+ rescue StandardError => e
184
+ future.raise_error(e) if future
185
+ raise
186
+ end
187
+ # if future not nil, set value
188
+ future.value = val if future
189
+ end while true
190
+
191
+ rescue StopError
192
+ # actor stop
193
+ @future.value = nil
194
+ rescue StandardError => e
195
+ @future.raise_error e
196
+ LOGGER.error("Actor #{self}") {"#{e}\n#{e.backtrace.join("\n")}"}
197
+ ensure
198
+ @running = false
199
+ end
200
+
201
+ # allow inject callback into actor loop
202
+ # Example:
203
+ #
204
+ # class A
205
+ # include Actor
206
+ #
207
+ # def loop_callback
208
+ # # before handle msg
209
+ # yield
210
+ # # after handle msg
211
+ # end
212
+ # end
213
+ #
214
+ def loop_callback
215
+ yield
216
+ end
217
+
218
+ def raise_error(e)
219
+ raise e
220
+ end
221
+ end
222
+
223
+ end
224
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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_relative 'rlpx'
26
+ require_relative 'actor'
27
+ require_relative 'protocol_io'
28
+
29
+ module Ciri
30
+ module DevP2P
31
+
32
+ # represent a connected remote node
33
+ class Peer
34
+
35
+ class DiscoverError < StandardError
36
+ end
37
+ class UnknownMessageCodeError < StandardError
38
+ end
39
+
40
+ include Actor
41
+
42
+ attr_reader :connection
43
+
44
+ def initialize(connection, handshake, protocols)
45
+ @connection = connection
46
+ @handshake = handshake
47
+ @protocols = protocols
48
+ @protocol_io_hash = make_protocol_io_hash(protocols, handshake.caps, connection)
49
+ super()
50
+ end
51
+
52
+ def node_id
53
+ @node_id ||= RLPX::NodeID.from_raw_id(@handshake.id)
54
+ end
55
+
56
+ # start peer
57
+ # handle msg, handle sub protocols
58
+ def start
59
+ executor.post {read_loop}
60
+ start_protocols
61
+ super
62
+ end
63
+
64
+ # read and handle msg
65
+ def read_loop
66
+ loop do
67
+ msg = connection.read_msg
68
+ msg.received_at = Time.now
69
+ handle(msg)
70
+ end
71
+ rescue StandardError => e
72
+ self << [:raise_error, e]
73
+ end
74
+
75
+ def start_protocols
76
+ @protocols.each do |protocol|
77
+ protocol.start.(self, @protocol_io_hash[protocol.name])
78
+ end
79
+ end
80
+
81
+ def handle(msg)
82
+ if msg.code == RLPX::MESSAGES[:ping]
83
+ #TODO send pong
84
+ elsif msg.code == RLPX::MESSAGES[:discover]
85
+ reason = RLP.decode_with_type(msg.payload, Integer)
86
+ raise DiscoverError.new("receive error discovery message, reason: #{reason}")
87
+ else
88
+ # send msg to sub protocol
89
+ if (protocol_io = find_protocol_io_by_msg_code(msg.code)).nil?
90
+ raise UnknownMessageCodeError.new("can't find protocol with msg code #{msg.code}")
91
+ end
92
+ protocol_io.msg_queue << msg
93
+ end
94
+ end
95
+
96
+ private
97
+ def find_protocol_io_by_msg_code(code)
98
+ @protocol_io_hash.values.find do |protocol_io|
99
+ offset = protocol_io.offset
100
+ protocol = protocol_io.protocol
101
+ code >= offset && code < offset + protocol.length
102
+ end
103
+ end
104
+
105
+ # return protocol_io_hash
106
+ # handle multiple sub protocols upon one io
107
+ def make_protocol_io_hash(protocols, caps, io)
108
+ # sub protocol offset
109
+ offset = RLPX::BASE_PROTOCOL_LENGTH
110
+ result = {}
111
+ # [name, version] as key
112
+ protocols_hash = protocols.map {|protocol| [[protocol.name, protocol.version], protocol]}.to_h
113
+ sorted_caps = caps.sort_by {|c| [c.name, c.version]}
114
+
115
+ sorted_caps.each do |cap|
116
+ protocol = protocols_hash[[cap.name, cap.version]]
117
+ next unless protocol
118
+ # ignore same name old protocols
119
+ if (old = result[cap.name])
120
+ result.delete(cap.name)
121
+ offset -= old.protocol.length
122
+ end
123
+ result[cap.name] = ProtocolIO.new(protocol, offset, io)
124
+ # move offset, to support next protocol
125
+ offset += protocol.length
126
+ end
127
+ result
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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_relative 'actor'
26
+
27
+ module Ciri
28
+ module DevP2P
29
+
30
+ # protocol represent DevP2P sub protocols
31
+ class Protocol
32
+
33
+ attr_reader :name, :version, :length
34
+ attr_accessor :node_info, :peer_info, :start
35
+
36
+ def initialize(name:, version:, length:)
37
+ @name = name
38
+ @version = version
39
+ @length = length
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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_relative 'actor'
25
+ require_relative 'rlpx/message'
26
+
27
+ module Ciri
28
+ module DevP2P
29
+
30
+ # send/read sub protocol msg
31
+ class ProtocolIO
32
+
33
+ class Error < StandardError
34
+ end
35
+ class InvalidMessageCode < Error
36
+ end
37
+
38
+ attr_reader :protocol, :offset, :msg_queue
39
+
40
+ def initialize(protocol, offset, frame_io)
41
+ @protocol = protocol
42
+ @offset = offset
43
+ @frame_io = frame_io
44
+ @msg_queue = Queue.new
45
+ end
46
+
47
+ def send_data(code, data)
48
+ msg = RLPX::Message.new(code: code, size: data.size, payload: data)
49
+ write_msg(msg)
50
+ end
51
+
52
+ def write_msg(msg)
53
+ raise InvalidMessageCode, "code #{code} must less than length #{protocol.length}" if msg.code > protocol.length
54
+ msg.code += offset
55
+ @frame_io.write_msg(msg)
56
+ end
57
+
58
+ def read_msg
59
+ msg = msg_queue.pop
60
+ msg.code -= offset
61
+ msg
62
+ end
63
+ end
64
+
65
+ end
66
+ end