sping 1.0.2 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5ab53904c197ff35bab0d5fc29f26b5ada7c1ded24548b30d42df12d3df6837
4
- data.tar.gz: 8bd57a7d33c5f39f1eb76ad2551550fafecc5836f373b3c3675648e620afebe1
3
+ metadata.gz: 9f7679b3fb31d07d034182114e576f3e272ef8223954f592bb30a98378b98fd5
4
+ data.tar.gz: b8d21d6a58ffb3c57481cc421d61f8fc25da32646fb8d5ae160829017d01fb14
5
5
  SHA512:
6
- metadata.gz: 90e021acc0d54dace745238c35566efacdd5be77d8db2f9ecd6e6aa1ed8d71ee89c552fb4674be23e1adcf244987154e1975364731eff78e76ad5dd7006ec3df
7
- data.tar.gz: 336910fef1e87548f060a4a70547b272d78362828274dd5b21ed011a821a66ed71c7ece447045ca717a169676fa872594cbbd9e649e701731c63cd812c7f7bba
6
+ metadata.gz: a6a2c6897ead8f7371b2f1a83719c1a8f2c07d44957a9c302bc548855d25671858435fe0d6e8162e14737e065f6bbcede37defd2ea21e23561d19e7f8d5ef63f
7
+ data.tar.gz: 2618e70abea52acc38364f7fb431dafa12a4d48ad8f07665f93ac76e316784e6a2f8f5d2b463a55053da88b10f46dc23adf16846abb1a4b1d4677eb9eef09e7f
data/bin/sping CHANGED
@@ -7,10 +7,10 @@ require 'msgpack'
7
7
  require_relative '../lib/session_manager'
8
8
 
9
9
  MessagePack::DefaultFactory.register_type(
10
- MessagePack::Timestamp::TYPE,
11
- Time,
12
- packer: MessagePack::Time::Packer,
13
- unpacker: MessagePack::Time::Unpacker
10
+ MessagePack::Timestamp::TYPE,
11
+ Time,
12
+ packer: MessagePack::Time::Packer,
13
+ unpacker: MessagePack::Time::Unpacker
14
14
  )
15
15
 
16
16
  $logger = Logger.new $stdout
@@ -21,18 +21,18 @@ sess_mgr.run
21
21
  sess_mgr.run_server
22
22
 
23
23
  ARGV.each do |host|
24
- sess_mgr.new_session(host)
24
+ sess_mgr.new_session(host)
25
25
  end
26
26
 
27
27
  Signal.trap('INT') do
28
- puts "Stop server..."
29
- sess_mgr.stop_server
28
+ puts 'Stop server...'
29
+ sess_mgr.stop_server
30
30
 
31
- puts "Stop sessions..."
32
- sess_mgr.stop_sessions
31
+ puts 'Stop sessions...'
32
+ sess_mgr.stop_sessions
33
33
 
34
- puts "Stop client..."
35
- sess_mgr.stop
34
+ puts 'Stop client...'
35
+ sess_mgr.stop
36
36
  end
37
37
 
38
38
  sess_mgr.join
data/lib/errors.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # sharable_constant_value: literal
3
+
4
+ module SPing
5
+ class SPingError < StandardError; end
6
+
7
+ class SessionManagerError < SPingError; end
8
+ class OutOfSessions < SessionManagerError; end
9
+
10
+ class SessionError < SPingError; end
11
+ class TCPHandshakeError < SessionError; end
12
+
13
+ class PeerError < SPingError; end
14
+ class InvalidPacketError < PeerError; end
15
+ class InvalidMessageError < InvalidPacketError; end
16
+ class UnknownPacketError < InvalidPacketError; end
17
+ class SignalizedError < PeerError; end
18
+ class UnknownVersionError < PeerError; end
19
+ class UnknownSessionError < PeerError; end
20
+ end
data/lib/last_acks.rb CHANGED
@@ -1,21 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
  # sharable_constant_value: literal
3
3
 
4
- class LastAcks
5
-
6
- attr_reader :acks, :size
4
+ module SPing
5
+ class LastAcks
6
+ attr_reader :size
7
7
 
8
8
  def initialize(size = 32)
9
- @size = 32
10
- @acks = []
9
+ @size = size
10
+ @acks = []
11
+ @acks_mutex = Mutex.new
12
+
13
+ @size.times do |index|
14
+ @acks[index] = {
15
+ 'R' => 0,
16
+ 'U' => Time.at(0),
17
+ 'X' => Time.at(0)
18
+ }
19
+ end
20
+ end
21
+
22
+ def acks
23
+ @acks_mutex.synchronize do
24
+ return @acks.dup
25
+ end
11
26
  end
12
27
 
13
28
  def add_ack(ack)
29
+ @acks_mutex.synchronize do
14
30
  @acks << ack
15
31
 
16
- if @acks.length > @size
17
- @acks.shift
18
- end
32
+ @acks.shift if @acks.length > @size
33
+ end
19
34
  end
20
-
35
+ end
21
36
  end
data/lib/session.rb CHANGED
@@ -2,228 +2,242 @@
2
2
  # sharable_constant_value: literal
3
3
 
4
4
  module SPing
5
+ require_relative 'errors'
5
6
 
6
- class SessionError < StandardError; end
7
- class TCPHandshakeError < SessionError; end
7
+ class Session
8
+ attr_reader :created, :last_rx, :madebyme
9
+ attr_accessor :session_id, :tcp_handshake_complete, :udp_handshake_complete
8
10
 
9
- class Session
11
+ require 'socket'
12
+ require 'timeout'
13
+ require 'msgpack'
14
+ require_relative 'last_acks'
10
15
 
11
- attr_reader :created, :last_rx, :madebyme
12
- attr_accessor :session_id, :tcp_handshake_complete, :udp_handshake_complete
16
+ def initialize(host, port, socket, madebyme)
17
+ @host = host
18
+ @port = port
19
+ @socket = socket
20
+ @created = Time.now
21
+ @madebyme = madebyme
13
22
 
14
- require 'socket'
15
- require 'timeout'
16
- require 'msgpack'
17
- require_relative 'last_acks'
23
+ @tcp_handshake_complete = false
24
+ @udp_handshake_complete = false
18
25
 
19
- def initialize(host, port, socket, madebyme)
20
- @host = host
21
- @port = port
22
- @socket = socket
23
- @created = Time.now
24
- @madebyme = madebyme
26
+ @last_acks = SPing::LastAcks.new
27
+ end
25
28
 
26
- @tcp_handshake_complete = false
27
- @udp_handshake_complete = false
29
+ def do_tcp_handshake1(socket, session_id)
30
+ socket.write "sping-0.3-https://codeberg.org/mark22k/sping\r\n"
31
+ socket.flush
28
32
 
29
- @last_acks = LastAcks.new
30
- end
33
+ invite = socket.readpartial 6
31
34
 
32
- def do_tcp_handshake1(socket, session_id)
33
- socket.write "sping-0.3-https://codeberg.org/mark22k/sping\r\n"
34
- socket.flush
35
+ raise TCPHandshakeError, 'Peer didn\'t invite us.' unless invite.chomp == 'INVITE'
35
36
 
36
- invite = socket.readpartial 6
37
+ @session_id = session_id
37
38
 
38
- if invite.chomp != "INVITE"
39
- raise TCPHandshakeError, 'Peer didn\'t invite us.'
40
- end
39
+ socket.write "#{session_id}\r\n"
40
+ socket.flush
41
41
 
42
- @session_id = session_id
42
+ socket.close
43
43
 
44
- socket.write "#{session_id}\r\n"
45
- socket.flush
44
+ @tcp_handshake_complete = true
45
+ end
46
46
 
47
- socket.close
47
+ def do_tcp_handshake2
48
+ socket = TCPSocket.new @host, @port
49
+ banner = socket.readpartial 9001
50
+ if banner.length > 9000
51
+ socket.close
52
+ raise TCPHandshakeError, 'Host banner too big'
53
+ end
48
54
 
49
- @tcp_handshake_complete = true
50
- end
55
+ unless banner.start_with? 'sping-0.3-'
56
+ socket.close
57
+ raise TCPHandshakeError, 'Host banner not sping'
58
+ end
51
59
 
52
- def do_tcp_handshake2
53
- socket = TCPSocket.new @host, @port
54
- banner = socket.readpartial 9001
55
- if banner.length > 9000
56
- socket.close
57
- raise TCPHandshakeError, 'Host banner too big'
58
- end
60
+ @remote_version = banner.chomp
61
+ $logger.info "Peer uses the following program version: #{@remote_version.dump}"
59
62
 
60
- if ! banner.start_with? 'sping-0.3-'
61
- socket.close
62
- raise TCPHandshakeError, 'Host banner not sping'
63
- end
63
+ socket.write "INVITE\r\n"
64
+ socket.flush
64
65
 
65
- @remote_version = banner.chomp
66
- $logger.info "Peer uses the following program version: #{@remote_version.dump}"
66
+ invite_buf = socket.readpartial 32
67
+ if invite_buf.length > 31 || invite_buf.empty?
68
+ socket.close
69
+ raise TCPHandshakeError, 'Invite banner wrong size'
70
+ end
67
71
 
68
- socket.write "INVITE\r\n"
69
- socket.flush
72
+ @session_id = invite_buf.chomp.to_i
70
73
 
71
- inviteBuf = socket.readpartial 32
72
- if inviteBuf.length > 31 || inviteBuf.length.zero?
73
- socket.close
74
- raise TCPHandshakeError, 'Invite banner wrong size'
75
- end
74
+ socket.close
76
75
 
77
- @session_id = inviteBuf.chomp.to_i
76
+ @tcp_handshake_complete = true
77
+ end
78
78
 
79
- socket.close
79
+ def set_endpoint(host, port)
80
+ @host = host
81
+ @port = port
82
+ end
80
83
 
81
- @tcp_handshake_complete = true
82
- end
84
+ def start_udp_handshake_sender(send_interval = 5)
85
+ raise 'UDP handshake sender is already running.' if @udp_handshake_sender
83
86
 
84
- def set_endpoint(host, port)
85
- @host = host
86
- @port = port
87
+ @udp_handshake_sender = Thread.new(send_interval) do |th_send_interval|
88
+ loop do
89
+ send_udp_handshake
90
+ sleep th_send_interval
87
91
  end
92
+ end
93
+ end
88
94
 
89
- def start_udp_handshake_sender(send_interval = 5)
90
- raise 'UDP handshake sender is already running.' if @udp_handshake_sender
95
+ def send_udp_handshake
96
+ packet = {
97
+ 'Y' => 'h'.ord,
98
+ 'M' => 11_181,
99
+ 'V' => 3,
100
+ 'S' => @session_id
101
+ }.to_msgpack
91
102
 
92
- @udp_handshake_sender = Thread.new(send_interval) do |send_interval|
93
- loop do
94
- self.send_udp_handshake
95
- sleep send_interval
96
- end
97
- end
98
- end
103
+ $logger.debug "Send UDP handshake to #{@host} port #{@port}."
104
+ @socket.send packet, 0, @host, @port
105
+ end
99
106
 
100
- def send_udp_handshake
101
- packet = {
102
- 'Y' => 'h'.ord,
103
- 'M' => 11181,
104
- 'V' => 3,
105
- 'S' => @session_id
106
- }.to_msgpack
107
+ def stop_udp_handshake_sender
108
+ raise 'UDP handshake sender is not running and therefore cannot be terminated.' unless @udp_handshake_sender
107
109
 
108
- $logger.debug "Send UDP handshake to #{@host} port #{@port}."
109
- @socket.send packet, 0, @host, @port
110
- end
110
+ @udp_handshake_sender.kill
111
+ @udp_handshake_sender = nil
112
+ end
111
113
 
112
- def stop_udp_handshake_sender
113
- raise 'UDP handshake sender is not running and therefore cannot be terminated.' unless @udp_handshake_sender
114
- @udp_handshake_sender.kill
115
- @udp_handshake_sender = nil
116
- end
114
+ def udp_handshake_recived
115
+ stop_udp_handshake_sender if @madebyme
116
+ @udp_handshake_complete = true
117
+ end
117
118
 
118
- def udp_handshake_recived
119
- stop_udp_handshake_sender if @madebyme
120
- @udp_handshake_complete = true
119
+ def start_pinger
120
+ raise 'Pinger is already running.' if @pinger
121
+
122
+ @pinger = Thread.new do
123
+ loop do
124
+ ping
125
+ sleep 1
121
126
  end
127
+ end
128
+ end
122
129
 
123
- def start_pinger
124
- raise 'Pinger is already running.' if @pinger
130
+ def stop_pinger
131
+ raise 'Pinger sender is not running and therefore cannot be terminated.' unless @pinger
125
132
 
126
- @pinger = Thread.new do
127
- loop do
128
- self.ping
129
- sleep 1
130
- end
131
- end
132
- end
133
+ @pinger.kill
134
+ @pinger = nil
135
+ end
133
136
 
134
- def stop_pinger
135
- raise 'Pinger sender is not running and therefore cannot be terminated.' unless @pinger
136
- @pinger.kill
137
- @pinger = nil
138
- end
137
+ def ping
138
+ current_id = (Time.now.to_i % 255) + 1
139
+ data = {
140
+ 'Y' => 't'.ord,
141
+ 'M' => 11_181,
142
+ 'S' => @session_id,
143
+ 'I' => current_id,
144
+ 'T' => Time.now,
145
+ 'E' => 0,
146
+ 'A' => @last_acks.acks
147
+ }.to_msgpack
148
+
149
+ $logger.debug "Send ping to #{@host} port #{@port}."
150
+ @socket.send data, 0, @host, @port
151
+ end
139
152
 
140
- def ping
141
- current_id = Time.now.to_i % 255 + 1
142
- data = {
143
- 'Y' => 't'.ord,
144
- 'M' => 11181,
145
- 'S' => @session_id,
146
- 'I' => current_id,
147
- 'T' => Time.now,
148
- 'E' => 0,
149
- 'A' => @last_acks.acks
150
- }.to_msgpack
151
-
152
- $logger.debug "Send ping to #{@host} port #{@port}."
153
- @socket.send data, 0, @host, @port
153
+ def stop
154
+ @pinger&.kill
155
+ @udp_handshake_sender&.kill
156
+ end
157
+
158
+ def handle_ping(packet, rxtime, peeraddr)
159
+ if !(packet.keys - %w[M Y E I T A S]).empty? ||
160
+ # M, Y are already checked in handle_packet from session manager
161
+ !packet['E'].is_a?(Integer) ||
162
+ !packet['I'].is_a?(Integer) ||
163
+ !packet['T'].is_a?(Time) ||
164
+ !packet['A'].is_a?(Array)
165
+ raise InvalidPacketError, 'The peer has sent an invalid message. The ping packet is incorrectly coded.'
166
+ end
167
+
168
+ raise SignalizedError, "Package displays an error message: #{packet['E']} Processing aborted." if packet['E'] != 0
169
+
170
+ id = packet['I']
171
+ txtime = packet['T']
172
+ remote_last_acks = packet['A']
173
+
174
+ if remote_last_acks.length != 32
175
+ raise InvalidMessageError, 'The peer has sent an invalid message. It does not contain any 32-Acks.'
176
+ end
177
+
178
+ remote_last_acks.each do |block_ack|
179
+ if !(block_ack.keys - %w[R U X]).empty? ||
180
+ !block_ack['R'].is_a?(Integer) ||
181
+ !block_ack['U'].is_a?(Time) ||
182
+ !block_ack['X'].is_a?(Time)
183
+ raise InvalidMessageError, 'The peer has sent an invalid message. An Ack is formatted invalid.'
154
184
  end
185
+ end
186
+
187
+ ack = {
188
+ 'R' => id,
189
+ 'U' => txtime,
190
+ 'X' => rxtime
191
+ }
192
+
193
+ @last_acks.add_ack(ack)
194
+ @last_rx = rxtime
195
+
196
+ # This is the A
197
+ # The peer is B
198
+ # TX => Time from A to B
199
+ # RX => Time from B to A
200
+
201
+ # Remove zero acks
202
+ remote_last_acks.reject! { |block_ack| block_ack['R'].zero? }
203
+
204
+ # Sort by TX time
205
+ remote_last_acks.sort_by! { |block_ack| block_ack['U'] }
206
+
207
+ newest_remote_ack = remote_last_acks.last.to_h
208
+
209
+ # Calculate loss
210
+ tx_loss = 0
211
+ rx_loss = 0
212
+ exchanges = 0
213
+
214
+ if remote_last_acks.length == 32
215
+ # We have enough data
216
+ exchanges = 32
217
+ remote_acks = remote_last_acks.map { |ack| ack['R'] }
218
+ local_acks = @last_acks.acks.map { |ack| ack['R'] }
219
+
220
+ tip_id = (Time.now.to_i % 255) + 1
221
+ starting = tip_id - 32
222
+
223
+ last_ids = if tip_id > 32
224
+ (starting..tip_id).to_a
225
+ else
226
+ ((255 + starting)..255).to_a + (1..tip_id).to_a
227
+ end
155
228
 
156
- def stop
157
- @pinger&.kill
158
- @udp_handshake_sender&.kill
229
+ last_ids[0...-1].each do |id|
230
+ tx_loss += 1 unless remote_acks.include? id
159
231
  end
160
232
 
161
- def handle_ping packet, rxtime, peeraddr
162
- if packet['E'] != 0
163
- $logger.error "Package displays an error message: #{packet['E']} Processing aborted."
164
- return
165
- end
166
-
167
- id = packet['I']
168
- txtime = packet['T']
169
- lastAcks = packet['A']
170
-
171
- ack = {
172
- 'R' => id,
173
- 'U' => txtime,
174
- 'X' => rxtime
175
- }
176
-
177
- @last_acks.add_ack(ack)
178
- @last_rx = rxtime
179
-
180
- # This is the A
181
- # The peer is B
182
- # TX => Time from A to B
183
- # RX => Time from B to A
184
-
185
- # Remove zero acks
186
- lastAcks.reject! { |ack| ack['R'] == 0 }
187
-
188
- # Sort by TX time
189
- lastAcks.sort_by! { |ack| ack['U'] }
190
-
191
- newest_remote_ack = lastAcks.last.to_h
192
-
193
- # Calculate loss
194
- tx_loss = 0
195
- rx_loss = 0
196
- exchanges = 0
197
-
198
- if lastAcks.length == 32
199
- # We have enough data
200
- exchanges = 32
201
- remote_acks = lastAcks.map { |ack| ack['R'] }
202
- local_acks = @last_acks.acks.map { |ack| ack['R'] }
203
-
204
- tip_id = Time.now.to_i % 255 + 1
205
- starting = tip_id - 32
206
-
207
- last_ids = nil
208
- if tip_id > 32
209
- last_ids = (starting..tip_id).to_a
210
- else
211
- last_ids = ((255 + starting)..255).to_a + (1..tip_id).to_a
212
- end
213
-
214
- last_ids[0...-1].each do |id|
215
- tx_loss += 1 unless remote_acks.include? id
216
- end
217
-
218
- last_ids[1..-1].each do |id|
219
- rx_loss += 1 unless local_acks.include? id
220
- end
221
- end
222
-
223
- tx_latency = ((newest_remote_ack['X'].to_f - newest_remote_ack['U'].to_f) * 1000.0).round(6)
224
- rx_latency = ((rxtime - txtime) * 1000.0).round(6)
225
- puts "[#{peeraddr}] RX: #{rx_latency}ms TX: #{tx_latency}ms [Loss RX: #{rx_loss}/#{exchanges} | Loss TX: #{tx_loss}/#{exchanges}]"
233
+ last_ids[1..].each do |id|
234
+ rx_loss += 1 unless local_acks.include? id
226
235
  end
236
+ end
227
237
 
238
+ tx_latency = ((newest_remote_ack['X'].to_f - newest_remote_ack['U'].to_f) * 1000.0).round(6)
239
+ rx_latency = ((rxtime - txtime) * 1000.0).round(6)
240
+ puts "[#{peeraddr}] RX: #{rx_latency}ms TX: #{tx_latency}ms [Loss RX: #{rx_loss}/#{exchanges} | Loss TX: #{tx_loss}/#{exchanges}]"
228
241
  end
242
+ end
229
243
  end
@@ -2,261 +2,285 @@
2
2
  # sharable_constant_value: literal
3
3
 
4
4
  module SPing
5
+ require_relative 'errors'
5
6
 
6
- class SessionManagerError < StandardError; end
7
- class OutOfSessions < SessionManagerError; end
7
+ class SessionManager
8
+ require 'socket'
9
+ require 'timeout'
10
+ require_relative 'session'
8
11
 
9
- class SessionManager
12
+ def initialize(host = '::', port = 6924)
13
+ @host = host
14
+ @port = port
10
15
 
11
- require 'socket'
12
- require 'timeout'
13
- require_relative 'session'
16
+ @sessions = {}
17
+ @sessions_mutex = Mutex.new
14
18
 
15
- def initialize(port = 6924)
16
- @port = port
19
+ @socket = UDPSocket.new(Socket::AF_INET6)
20
+ @socket.bind @host, @port
21
+ end
17
22
 
18
- @sessions = {}
19
- @pingers = {}
23
+ def new_session(host, port = 6924)
24
+ $logger.info "Add new session for host #{host} port #{port}."
25
+ session = SPing::Session.new host, port, @socket, true
20
26
 
21
- @socket = UDPSocket.new(Socket::AF_INET6)
22
- @socket.bind "::", @port
23
- end
27
+ counter = 0
28
+ loop do
29
+ session_id = request_session session
30
+ break if session_id
24
31
 
25
- def new_session(host, port = 6924)
26
- $logger.info "Add new session for host #{host} port #{port}."
27
- session = SPing::Session.new host, port, @socket, true
28
-
29
- begin
30
- counter = 0
31
- loop do
32
- begin
33
- session.do_tcp_handshake2
34
- rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
35
- $logger.warn "TCP handshake failed: #{e.message}"
36
- return
37
- end
38
- if ! @sessions.key? session.session_id
39
- break
40
- end
41
- if counter > 5
42
- raise OutOfSessions, 'The peer could not name a session ID that had not yet been assigned.'
43
- end
44
- counter += 1
45
- end
46
- rescue OutOfSessions => e
47
- $logger.error "Out of session: #{e.message}"
48
- return
49
- end
50
-
51
- $logger.info "TCP handshake for host #{host} port #{port} successful. Session ID is #{session.session_id}."
52
-
53
- $logger.info "Perform UDP handshake for session ID #{session.session_id}."
54
- session.start_udp_handshake_sender
55
-
56
- @sessions[session.session_id] = session
32
+ # rubocop:disable Style/IfUnlessModifier
33
+ if counter > 5
34
+ raise OutOfSessions, 'The peer could not name a session ID that had not yet been assigned.'
57
35
  end
36
+ # rubocop:enable Style/IfUnlessModifier
58
37
 
59
- def del_session(session_id)
60
- $logger.debug "Delete session with session id #{session_id}"
38
+ counter += 1
39
+ end
61
40
 
62
- @sessions[session_id].stop
63
- @sessions.delete(session_id)
64
- end
41
+ $logger.info "TCP handshake for host #{host} port #{port} successful. Session ID is #{session.session_id}."
65
42
 
66
- def stop_sessions
67
- @sessions.each do |session_id, session|
68
- session.stop
69
- end
70
- end
43
+ $logger.info "Perform UDP handshake for session ID #{session.session_id}."
44
+ session.start_udp_handshake_sender
45
+ rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
46
+ $logger.warn "TCP handshake failed: #{e.message}"
47
+ rescue OutOfSessions => e
48
+ $logger.error "Out of session: #{e.message}"
49
+ end
71
50
 
72
- def join
73
- @runner&.join
74
- @gc_thread&.join
75
- @server_thread&.join
76
- end
51
+ def del_session(session_id)
52
+ $logger.debug "Delete session with session id #{session_id}"
77
53
 
78
- def run
79
- raise 'Client is already running.' if @runner
80
- raise 'GC is already running.' if @gc
81
-
82
- @runner = Thread.new do
83
- loop do
84
- Thread.new(@socket.recvfrom(10000)) do |buf|
85
- handle_packet buf
86
- end
87
- end
88
- end
89
- @gc_thread = Thread.new do
90
- loop do
91
- do_gc
92
- sleep 10
93
- end
94
- end
95
- end
54
+ @sessions_mutex.synchronize do
55
+ @sessions[session_id].stop
56
+ @sessions.delete(session_id)
57
+ end
58
+ end
96
59
 
97
- def run_server
98
- raise 'Server is already running.' if @server_thread
99
- raise 'Server already exist.' if @server
60
+ def stop_sessions
61
+ @sessions.each do |_session_id, session|
62
+ session.stop
63
+ end
64
+ end
100
65
 
101
- @server = TCPServer.new '::', @port
66
+ def join
67
+ @runner&.join
68
+ @gc_thread&.join
69
+ @server_thread&.join
70
+ end
102
71
 
103
- @server_thread = Thread.new do
104
- loop do
105
- Thread.new(@server.accept) do |client|
106
- handle_client client
107
- end
108
- end
109
- end
72
+ def run
73
+ raise 'Client is already running.' if @runner
74
+ raise 'GC is already running.' if @gc
110
75
 
111
- return @server_thread
76
+ @runner = Thread.new do
77
+ loop do
78
+ Thread.new(@socket.recvfrom(10_000)) do |buf|
79
+ handle_packet buf
80
+ end
112
81
  end
82
+ end
83
+ @gc_thread = Thread.new do
84
+ loop do
85
+ do_gc
86
+ sleep 10
87
+ end
88
+ end
89
+ end
113
90
 
114
- def stop_server
115
- raise 'Server thread is not running.' unless @server_thread
116
-
117
- @server_thread.kill
118
- @server_thread = nil
91
+ def run_server
92
+ raise 'Server is already running.' if @server_thread
93
+ raise 'Server already exist.' if @server
119
94
 
120
- raise 'Server is not running.' unless @server
95
+ @server = TCPServer.new @host, @port
121
96
 
122
- @server.close
123
- @server = nil
97
+ @server_thread = Thread.new do
98
+ loop do
99
+ Thread.new(@server.accept) do |client|
100
+ handle_client client
101
+ end
124
102
  end
103
+ end
125
104
 
126
- def stop
127
- raise 'Session manager is not running.' unless @runner
105
+ return @server_thread
106
+ end
128
107
 
129
- @runner.kill
130
- @runner = nil
108
+ def stop_server
109
+ raise 'Server thread is not running.' unless @server_thread
131
110
 
132
- raise 'Session GC is not running.' unless @gc_thread
111
+ @server_thread.kill
112
+ @server_thread = nil
133
113
 
134
- @gc_thread.kill
135
- @gc_thread = nil
136
- end
114
+ raise 'Server is not running.' unless @server
137
115
 
138
- def generate_session_id
139
- Timeout::timeout(30, OutOfSessions, 'No session ID could be generated which has not yet been used.') do
140
- session_id = rand(2 ** 32 - 1)
141
- while @sessions.key? session_id
142
- session_id = rand(2 ** 32 - 1)
143
- end
116
+ @server.close
117
+ @server = nil
118
+ end
144
119
 
145
- return session_id
146
- end
147
- end
120
+ def stop
121
+ raise 'Session manager is not running.' unless @runner
148
122
 
149
- private
150
-
151
- def do_gc
152
- $logger.debug 'Remove outdated sessions.'
153
- @sessions.keys.each do |session_id|
154
- session = @sessions[session_id]
155
- # It is possible that sessions no longer exist here, as we may have session IDs that have already been deleted.
156
- # However, we can ignore this aspect here, as we are the only function that deletes sessions.
157
- if ! (session.tcp_handshake_complete && session.udp_handshake_complete)
158
- # Handshake incomplete
159
- if (Time.now.to_i - session.created.to_i) > 60
160
- # TCP and/or UDP take more than 60 seconds.
161
- $logger.debug "UDP handshake for session id #{session_id} timed out."
162
- del_session session_id
163
- end
164
- elsif (Time.now.to_i - session.last_rx.to_i) > 30
165
- # 30 seconds have elapsed since the last ping from the peer was received. The peer is probably dead.
166
- $logger.debug "Session id #{session_id} without activity for over thirty seconds."
167
- del_session session_id
168
- end
169
- end
170
- end
123
+ @runner.kill
124
+ @runner = nil
171
125
 
172
- def handle_packet buf
173
- rxtime = Time.now
174
-
175
- peeraddr = buf[1][2]
176
- peerport = buf[1][1]
177
- $logger.debug "Packet received from #{peeraddr} port #{peerport}."
178
-
179
- packet = MessagePack.unpack(buf[0])
180
-
181
- # Check whether the Magic Number is the correct one.
182
- if packet['M'] != 11181
183
- $logger.warn 'Package contains incorrect magic number. Processing is canceled.'
184
- return
185
- end
186
-
187
- type = packet['Y'].chr
188
- case type
189
- when 'h'
190
- handle_udp_handshake packet, peeraddr, peerport
191
- when 't'
192
- handle_ping packet, rxtime, peeraddr, peerport
193
- else
194
- $logger.warn "Package is of unknown type: #{type}"
195
- end
196
- end
126
+ raise 'Session GC is not running.' unless @gc_thread
197
127
 
198
- def handle_udp_handshake packet, peeraddr, peerport
199
- session_id = packet['S']
200
- session = @sessions[session_id]
201
- if session
202
- if packet['V'] != 3
203
- $logger.warn "UDP handshake uses an unsupported version: #{packet['V']}"
204
- return
205
- end
206
-
207
- session.set_endpoint peeraddr, peerport
208
-
209
- if ! session.madebyme
210
- # If the session was not started by me, the other peer expects a
211
- # confirmation of the handshake in which the same is sent back.
212
- $logger.debug "Send UDP Handshake back for session id #{session_id}."
213
- session.send_udp_handshake
214
- end
215
-
216
- if session.udp_handshake_complete
217
- $logger.warn 'UDP handshake is received, although a previous one was already successful.'
218
- return
219
- end
220
-
221
- session.udp_handshake_recived
222
- $logger.info "UDP handshake for session ID #{session_id} was successful."
223
-
224
- session.start_pinger
225
- else
226
- $logger.warn 'UDP handshake received for uninitiated session.'
227
- return
228
- end
128
+ @gc_thread.kill
129
+ @gc_thread = nil
130
+ end
131
+
132
+ def generate_session_id
133
+ Timeout.timeout(30, OutOfSessions, 'No session ID could be generated which has not yet been used.') do
134
+ loop do
135
+ session_id = rand (2**32) - 1
136
+ return session_id unless @sessions.key? session_id
229
137
  end
138
+ end
139
+ end
230
140
 
231
- def handle_ping packet, rxtime, peeraddr, peerport
232
- session_id = packet['S']
233
- session = @sessions[session_id]
234
- if session && session.tcp_handshake_complete && session.udp_handshake_complete
235
- session.set_endpoint peeraddr, peerport
236
- session.handle_ping packet, rxtime, peeraddr
237
- else
238
- $logger.warn "Ping packet received for non-initiated session id #{session_id}."
239
- end
141
+ private
142
+
143
+ def do_gc
144
+ $logger.debug 'Remove outdated sessions.'
145
+ # rubocop:disable Style/HashEachMethods
146
+ # You cannot run through a map and edit it at the same time. Since this is necessary due to the threading,
147
+ # a snapshot of the key variable is run through.
148
+ @sessions.keys.each do |session_id|
149
+ # rubocop:enable Style/HashEachMethods
150
+
151
+ session = @sessions[session_id]
152
+ # It is possible that sessions no longer exist here, as we may have session IDs that have already been deleted.
153
+ # However, we can ignore this aspect here, as we are the only function that deletes sessions.
154
+ if !(session.tcp_handshake_complete && session.udp_handshake_complete)
155
+ # Handshake incomplete
156
+ if (Time.now.to_i - session.created.to_i) > 60
157
+ # TCP and/or UDP take more than 60 seconds.
158
+ $logger.debug "UDP handshake for session id #{session_id} timed out."
159
+ del_session session_id
160
+ end
161
+ elsif (Time.now.to_i - session.last_rx.to_i) > 30
162
+ # 30 seconds have elapsed since the last ping from the peer was received. The peer is probably dead.
163
+ $logger.debug "Session id #{session_id} without activity for over thirty seconds."
164
+ del_session session_id
240
165
  end
166
+ end
167
+ end
168
+
169
+ def request_session(session)
170
+ session.do_tcp_handshake2
241
171
 
242
- def handle_client(client)
243
- host = client.peeraddr[3]
244
- port = client.peeraddr[1]
245
- session = SPing::Session.new host, port, @socket, false
246
-
247
- $logger.debug "Session request from host #{host} port #{port}."
248
-
249
- begin
250
- session.do_tcp_handshake1 client, self.generate_session_id
251
- rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
252
- $logger.warn "Could not establish a new session with Peer: #{e.message}"
253
- return
254
- rescue OutOfSessions => e
255
- $logger.error "Could not establish a new session with Peer: #{e.message}"
256
- return
257
- end
258
-
259
- @sessions[session.session_id] = session
172
+ @sessions_mutex.synchronize do
173
+ unless @sessions.key? session.session_id
174
+ @sessions[session.session_id] = session
175
+ return session.session_id
260
176
  end
177
+ end
178
+
179
+ return nil
180
+ end
181
+
182
+ def handle_packet(buf)
183
+ rxtime = Time.now
184
+
185
+ peeraddr = buf[1][2]
186
+ peerport = buf[1][1]
187
+ $logger.debug "Packet received from #{peeraddr} port #{peerport}."
188
+
189
+ packet = MessagePack.unpack(buf[0]).to_h
190
+
191
+ if !packet.key?('M') ||
192
+ !packet.key?('Y') ||
193
+ !packet.key?('S') ||
194
+ !packet['M'].is_a?(Integer) ||
195
+ !packet['Y'].is_a?(Integer) ||
196
+ !packet['S'].is_a?(Integer)
197
+ raise InvalidMessageError, 'The peer has sent an invalid message. The packet is incorrectly coded.'
198
+ end
199
+
200
+ # Check whether the Magic Number is the correct one.
201
+ if packet['M'] != 11_181
202
+ raise UnknownPacketError, 'Package contains incorrect magic number. Processing is canceled.'
203
+ end
204
+
205
+ type = packet['Y'].chr
206
+ case type
207
+ when 'h'
208
+ handle_udp_handshake packet, peeraddr, peerport
209
+ when 't'
210
+ handle_ping packet, rxtime, peeraddr, peerport
211
+ else
212
+ raise UnknownPacketError, "Package is of unknown type: #{type}"
213
+ end
214
+ rescue MessagePack::MalformedFormatError => e
215
+ $logger.error "Packet cannot be decoded: #{e.message}"
216
+ rescue PeerError => e
217
+ $logger.error "Perr error: #{e.message}"
218
+ end
219
+
220
+ def handle_udp_handshake(packet, peeraddr, peerport)
221
+ if !(packet.keys - %w[M Y V S]).empty? ||
222
+ # M, Y are already checked in handle_packet from session manager
223
+ !packet['V'].is_a?(Integer)
224
+ raise InvalidMessageError, 'The peer has sent an invalid message. The handshake packet is incorrectly coded.'
225
+ end
226
+
227
+ session_id = packet['S']
228
+ session = @sessions[session_id]
229
+ raise UnknownSessionError, 'UDP handshake received for uninitiated session.' unless session
230
+ raise UnknownVersionError, "UDP handshake uses an unsupported version: #{packet['V']}" if packet['V'] != 3
231
+
232
+ session.set_endpoint peeraddr, peerport
233
+
234
+ unless session.madebyme
235
+ # If the session was not started by me, the other peer expects a
236
+ # confirmation of the handshake in which the same is sent back.
237
+ $logger.debug "Send UDP Handshake back for session id #{session_id}."
238
+ session.send_udp_handshake
239
+ end
240
+
241
+ if session.udp_handshake_complete
242
+ $logger.warn 'UDP handshake is received, although a previous one was already successful.'
243
+ return
244
+ end
245
+
246
+ session.udp_handshake_recived
247
+ $logger.info "UDP handshake for session ID #{session_id} was successful."
248
+
249
+ session.start_pinger
250
+ end
251
+
252
+ def handle_ping(packet, rxtime, peeraddr, peerport)
253
+ session_id = packet['S']
254
+ session = @sessions[session_id]
255
+ unless session&.tcp_handshake_complete && session&.udp_handshake_complete
256
+ raise UnknownSessionError, "Ping packet received for non-initiated session id #{session_id}."
257
+ end
258
+
259
+ session.set_endpoint peeraddr, peerport
260
+ session.handle_ping packet, rxtime, peeraddr
261
+ end
262
+
263
+ def handle_client(client)
264
+ host = client.peeraddr[3]
265
+ port = client.peeraddr[1]
266
+ session = SPing::Session.new host, port, @socket, false
267
+
268
+ $logger.debug "Session request from host #{host} port #{port}."
269
+
270
+ session_id = nil
271
+
272
+ @sessions_mutex.synchronize do
273
+ session_id = generate_session_id
274
+ @sessions[session_id] = session
275
+ end
276
+
277
+ session.do_tcp_handshake1 client, session_id
278
+ rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
279
+ $logger.warn "Could not establish a new session with Peer: #{e.message}"
280
+ del_session session_id
281
+ rescue OutOfSessions => e
282
+ $logger.error "Could not establish a new session with Peer: #{e.message}"
283
+ del_session session_id
261
284
  end
285
+ end
262
286
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sping
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marek Küthe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-07 00:00:00.000000000 Z
11
+ date: 2023-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -43,6 +43,7 @@ files:
43
43
  - LICENSE
44
44
  - README.md
45
45
  - bin/sping
46
+ - lib/errors.rb
46
47
  - lib/last_acks.rb
47
48
  - lib/session.rb
48
49
  - lib/session_manager.rb
@@ -68,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
69
  - !ruby/object:Gem::Version
69
70
  version: '0'
70
71
  requirements: []
71
- rubygems_version: 3.4.21
72
+ rubygems_version: 3.4.22
72
73
  signing_key:
73
74
  specification_version: 4
74
75
  summary: Reimplementation of sping in Ruby.