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