mfp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/lib/mfp/client.rb +390 -0
- data/lib/mfp/compressor.rb +35 -0
- data/lib/mfp/enum.rb +83 -0
- data/lib/mfp/error.rb +20 -0
- data/lib/mfp/event.rb +25 -0
- data/lib/mfp/outgoing_message.rb +15 -0
- data/lib/mfp/ping_handler.rb +70 -0
- data/lib/mfp/proto/error.rb +52 -0
- data/lib/mfp/proto/frame.rb +53 -0
- data/lib/mfp/proto/hello.rb +49 -0
- data/lib/mfp/proto/hello_response.rb +43 -0
- data/lib/mfp/proto/ping.rb +33 -0
- data/lib/mfp/proto/protocol.rb +96 -0
- data/lib/mfp/proto/reader.rb +26 -0
- data/lib/mfp/proto/writer.rb +28 -0
- data/lib/mfp/proto.rb +10 -0
- data/lib/mfp/resettable_timer.rb +47 -0
- data/lib/mfp/server/conn.rb +362 -0
- data/lib/mfp/server/server.rb +118 -0
- data/lib/mfp/server.rb +4 -0
- data/lib/mfp/tls_config.rb +12 -0
- data/lib/mfp/version.rb +5 -0
- data/lib/mfp.rb +39 -0
- metadata +143 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Server
|
|
5
|
+
class Conn
|
|
6
|
+
attr_reader :id, :application, :hostname, :max_streams, :max_payload, :closed
|
|
7
|
+
|
|
8
|
+
def initialize(server, id, peer_socket, call_sign, settings)
|
|
9
|
+
@logger = settings.logger
|
|
10
|
+
@server = server
|
|
11
|
+
@sock = peer_socket
|
|
12
|
+
@call_sign = call_sign
|
|
13
|
+
@max_streams = settings.max_streams
|
|
14
|
+
@max_payload = settings.max_payload
|
|
15
|
+
@id = id
|
|
16
|
+
@first_frame_timeout = settings.first_frame_timeout_secs
|
|
17
|
+
|
|
18
|
+
@to_write = Async::Queue.new
|
|
19
|
+
@to_read = Async::Queue.new
|
|
20
|
+
@compression = Compressor.new(:identity)
|
|
21
|
+
@ping_handler = PingHandler.new(
|
|
22
|
+
idle_timeout: settings.idle_timeout_secs,
|
|
23
|
+
ping_timeout: settings.ping_timeout_secs,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@application = nil
|
|
27
|
+
@hostname = nil
|
|
28
|
+
@closed = false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def start(task)
|
|
32
|
+
@task = task.async do |t|
|
|
33
|
+
@write_task = t.async { handle_writes }
|
|
34
|
+
begin
|
|
35
|
+
status, err = t.with_timeout(@first_frame_timeout) { perform_handshake }
|
|
36
|
+
rescue Async::TimeoutError => e
|
|
37
|
+
@logger.error("Timeout waiting for full handshake", e)
|
|
38
|
+
status = :failed
|
|
39
|
+
err = e
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
@logger.error("Failed performing handshake", e)
|
|
42
|
+
status = :failed
|
|
43
|
+
err = e
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if status == :failed
|
|
47
|
+
@logger.error("Failed completing handshake", err)
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@server.connect(self)
|
|
52
|
+
@ping_handler.start(t)
|
|
53
|
+
t.async { handle_pings }
|
|
54
|
+
|
|
55
|
+
run_loop
|
|
56
|
+
ensure
|
|
57
|
+
close
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def close
|
|
62
|
+
return if @closed
|
|
63
|
+
|
|
64
|
+
@logger.debug("Closing connection")
|
|
65
|
+
|
|
66
|
+
@closed = true
|
|
67
|
+
|
|
68
|
+
@ping_handler&.stop
|
|
69
|
+
@sock&.close
|
|
70
|
+
@server.disconnect(self)
|
|
71
|
+
@to_read.close
|
|
72
|
+
@task&.cancel
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def recv = @to_read.dequeue
|
|
76
|
+
|
|
77
|
+
def send_stream(stream_id, msg, flags: 0)
|
|
78
|
+
raise ArgumentError, "msg must be a String or nil" unless msg.nil? || msg.is_a?(String)
|
|
79
|
+
|
|
80
|
+
send_conn_frame(Proto::Frame.new(
|
|
81
|
+
stream_id:,
|
|
82
|
+
kind: Proto::MessageKind::STREAM,
|
|
83
|
+
flags:,
|
|
84
|
+
length: msg&.bytesize || 0,
|
|
85
|
+
payload: msg,
|
|
86
|
+
))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_control(stream_id, msg, flags: 0)
|
|
90
|
+
raise ArgumentError, "msg must be a String or nil" unless msg.nil? || msg.is_a?(String)
|
|
91
|
+
|
|
92
|
+
send_conn_frame(Proto::Frame.new(
|
|
93
|
+
stream_id:,
|
|
94
|
+
kind: Proto::MessageKind::CONTROL,
|
|
95
|
+
flags:,
|
|
96
|
+
length: msg&.bytesize || 0,
|
|
97
|
+
payload: msg,
|
|
98
|
+
))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def send_error(stream_id, terminal:, code:, debug:)
|
|
102
|
+
debug = debug.to_s unless debug.nil?
|
|
103
|
+
msg = Proto::Error.new(code, terminal:, debug:)
|
|
104
|
+
send_conn_frame(msg.encode(stream_id))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def run_loop
|
|
110
|
+
@logger.debug("Client entering runloop")
|
|
111
|
+
loop do
|
|
112
|
+
begin
|
|
113
|
+
fr = read_frame
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
unless @closed
|
|
116
|
+
@logger.error("Failed reading frame", e)
|
|
117
|
+
@to_read.enqueue ConnectionEvent.new(CONNECTION_EVENT_CODE_BROKEN)
|
|
118
|
+
close
|
|
119
|
+
end
|
|
120
|
+
break
|
|
121
|
+
end
|
|
122
|
+
@ping_handler.notify_received
|
|
123
|
+
handle_frame(fr)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def failure(msg) = StandardError.new(msg)
|
|
128
|
+
|
|
129
|
+
def read_frame = Proto::Frame.read(@sock, @max_payload, @call_sign)
|
|
130
|
+
|
|
131
|
+
def perform_handshake
|
|
132
|
+
fr = read_frame
|
|
133
|
+
if fr.kind != Proto::MessageKind::HELLO
|
|
134
|
+
@logger.debug("Rejecting client: Invalid frame kind for handshake")
|
|
135
|
+
return [:failed, failure("invalid frame kind")]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
hello = Proto::Hello.read(fr)
|
|
139
|
+
if [hello.version_major, hello.version_minor] != [1, 0]
|
|
140
|
+
@logger.debug("Rejecting client: Invalid version", vMaj: hello.version_major, vMin: hello.version_minor)
|
|
141
|
+
return [:failed, failure("invalid version")]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
@application = hello.application
|
|
145
|
+
@hostname = hello.host
|
|
146
|
+
|
|
147
|
+
answer = Proto::HelloResponse.new(
|
|
148
|
+
capabilities: handle_caps(hello.capabilities),
|
|
149
|
+
max_streams: @max_streams,
|
|
150
|
+
max_payload: @max_payload,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
send_conn(answer)
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
@logger.error("Failed replying to hello", e)
|
|
157
|
+
terminate_error(Proto::ErrorKind::INTERNAL_ERROR)
|
|
158
|
+
return
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
case answer.capabilities
|
|
162
|
+
when Proto::Capability::COMPRESSION_BROTLI
|
|
163
|
+
@compression = Compressor.new(:brotli)
|
|
164
|
+
when Proto::Capability::COMPRESSION_GZIP
|
|
165
|
+
@compression = Compressor.new(:gzip)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
[:ok, nil]
|
|
169
|
+
rescue Mfp::Error => e
|
|
170
|
+
[:failed, e]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def handle_writes
|
|
174
|
+
until @closed
|
|
175
|
+
msg = @to_write.dequeue
|
|
176
|
+
frm = msg.frame
|
|
177
|
+
frm.call_sign = @call_sign
|
|
178
|
+
|
|
179
|
+
begin
|
|
180
|
+
@sock.write(frm.encode)
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
msg.complete(e)
|
|
183
|
+
next
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
msg.complete(nil)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def send_conn(msg)
|
|
191
|
+
raise ConnectionClosedError if @closed
|
|
192
|
+
|
|
193
|
+
fr = msg.encode(0)
|
|
194
|
+
send_conn_frame(fr)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def send_conn_frame(fr)
|
|
198
|
+
raise ConnectionClosedError if @closed
|
|
199
|
+
|
|
200
|
+
@logger.debug("Sending frame", frame: fr.inspect)
|
|
201
|
+
|
|
202
|
+
fr.call_sign = @call_sign
|
|
203
|
+
if fr.kind == Proto::MessageKind::STREAM && fr.length.positive? && @compression.kind != :identity
|
|
204
|
+
fr.payload = @compression.compress(fr.payload)
|
|
205
|
+
fr.length = fr.payload.bytesize
|
|
206
|
+
end
|
|
207
|
+
out = OutgoingMessage.new(fr)
|
|
208
|
+
err = Async::Task.current.defer_cancel do
|
|
209
|
+
@to_write.enqueue out
|
|
210
|
+
out.wait
|
|
211
|
+
end
|
|
212
|
+
raise err if err
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def handle_pings
|
|
216
|
+
until @closed
|
|
217
|
+
msg = @ping_handler.recv
|
|
218
|
+
break if msg.nil?
|
|
219
|
+
|
|
220
|
+
case msg[:kind]
|
|
221
|
+
when :request
|
|
222
|
+
begin
|
|
223
|
+
send_conn(Proto::Ping.new(ack: false, payload: msg[:cookie]))
|
|
224
|
+
rescue StandardError => e
|
|
225
|
+
@logger.error("Failed sending ping", e)
|
|
226
|
+
@to_read.enqueue(ConnectionEvent.new(code: CONNECTION_EVENT_CODE_BROKEN))
|
|
227
|
+
close
|
|
228
|
+
return
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
when :timeout
|
|
232
|
+
@logger.warn("Timed out waiting for ping acknowledgement. Dropping connection...")
|
|
233
|
+
@to_read.enqueue(ConnectionEvent.new(code: CONNECTION_EVENT_CODE_TIMED_OUT))
|
|
234
|
+
close
|
|
235
|
+
return
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def handle_caps(r)
|
|
241
|
+
if r.anybits?(Proto::Capability::COMPRESSION_BROTLI)
|
|
242
|
+
Proto::Capability::COMPRESSION_BROTLI
|
|
243
|
+
elsif r.anybits?(Proto::Capability::COMPRESSION_GZIP)
|
|
244
|
+
Proto::Capability::COMPRESSION_GZIP
|
|
245
|
+
else
|
|
246
|
+
0
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def handle_connection_error(fr)
|
|
251
|
+
err = nil
|
|
252
|
+
failed = false
|
|
253
|
+
begin
|
|
254
|
+
err = Proto::Error.read(fr)
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
@logger.error("Received connection-level error message from client, but decoding failed", e)
|
|
257
|
+
failed = true
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
unless failed
|
|
261
|
+
kvs = {
|
|
262
|
+
code: err.code,
|
|
263
|
+
debug_len: err.debug.respond_to?(:bytesize) ? err.debug.bytesize : 0,
|
|
264
|
+
}
|
|
265
|
+
kvs[:debug] = err.debug if !err.debug.nil? && err.debug.is_a?(String)
|
|
266
|
+
|
|
267
|
+
@logger.error("Received connection-level error message from client", **kvs)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
@to_read.enqueue(ConnectionEvent.new(code: CONNECTION_EVENT_CODE_BROKEN))
|
|
271
|
+
close
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def handle_stream_error(fr)
|
|
275
|
+
err = Proto::Error.read(fr)
|
|
276
|
+
@to_read.enqueue(ErrorEvent.new(
|
|
277
|
+
stream_id: fr.stream_id,
|
|
278
|
+
code: err.code,
|
|
279
|
+
terminal: err.terminal,
|
|
280
|
+
debug: err.debug,
|
|
281
|
+
))
|
|
282
|
+
rescue StandardError => e
|
|
283
|
+
@logger.error("Failed handling stream error", e)
|
|
284
|
+
terminate_error(Proto::ErrorKind::PROTOCOL_ERROR)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def handle_ping(fr)
|
|
288
|
+
png = Proto::Ping.read(fr)
|
|
289
|
+
return @ping_handler.notify_pong(png.payload) if png.ack
|
|
290
|
+
|
|
291
|
+
send_conn(Proto::Ping.new(
|
|
292
|
+
ack: true,
|
|
293
|
+
payload: png.payload,
|
|
294
|
+
))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def handle_frame(fr)
|
|
298
|
+
@logger.debug("Read frame", frame: fr.inspect)
|
|
299
|
+
case fr.kind
|
|
300
|
+
when Proto::MessageKind::HELLO, Proto::MessageKind::HELLO_RESPONSE, Proto::MessageKind::UNKNOWN
|
|
301
|
+
terminate_error(Proto::ErrorKind::PROTOCOL_ERROR)
|
|
302
|
+
|
|
303
|
+
when Proto::MessageKind::ERROR
|
|
304
|
+
return handle_connection_error(fr) if fr.stream_id.zero?
|
|
305
|
+
|
|
306
|
+
handle_stream_error(fr)
|
|
307
|
+
|
|
308
|
+
when Proto::MessageKind::CONTROL
|
|
309
|
+
@to_read.enqueue(ControlEvent.new(
|
|
310
|
+
stream_id: fr.stream_id,
|
|
311
|
+
payload: fr.payload,
|
|
312
|
+
))
|
|
313
|
+
|
|
314
|
+
when Proto::MessageKind::PING
|
|
315
|
+
return terminate_error(Proto::ErrorKind::PROTOCOL_ERROR) unless fr.stream_id.zero?
|
|
316
|
+
return terminate_error(Proto::ErrorKind::PROTOCOL_ERROR) if fr.length != 8
|
|
317
|
+
|
|
318
|
+
begin
|
|
319
|
+
handle_ping(fr)
|
|
320
|
+
rescue StandardError => e
|
|
321
|
+
@logger.error("Failed handling ping", e)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
when Proto::MessageKind::STREAM
|
|
325
|
+
return terminate_error(Proto::ErrorKind::PROTOCOL_ERROR) if fr.stream_id.zero?
|
|
326
|
+
|
|
327
|
+
if @compression.kind != :identity && fr.length.positive?
|
|
328
|
+
begin
|
|
329
|
+
out = @compression.decompress(fr.payload)
|
|
330
|
+
fr.payload = out
|
|
331
|
+
fr.length = out.bytesize
|
|
332
|
+
rescue StandardError => e
|
|
333
|
+
@logger.error("Failed decompressing stream", e)
|
|
334
|
+
terminate_error(Proto::ErrorKind::COMPRESSION_ERROR)
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
@to_read.enqueue(StreamEvent.new(
|
|
340
|
+
stream_id: fr.stream_id,
|
|
341
|
+
payload: fr.payload,
|
|
342
|
+
))
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def terminate_error(code)
|
|
347
|
+
@to_read.enqueue(ConnectionEvent.new(
|
|
348
|
+
code: CONNECTION_EVENT_CODE_BROKEN,
|
|
349
|
+
))
|
|
350
|
+
begin
|
|
351
|
+
send_conn(Proto::Error.new(
|
|
352
|
+
code,
|
|
353
|
+
terminal: true,
|
|
354
|
+
))
|
|
355
|
+
rescue StandardError
|
|
356
|
+
# noop
|
|
357
|
+
end
|
|
358
|
+
close
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mfp
|
|
4
|
+
module Server
|
|
5
|
+
Settings = Data.define(
|
|
6
|
+
:max_streams,
|
|
7
|
+
:max_payload,
|
|
8
|
+
:idle_timeout_secs,
|
|
9
|
+
:ping_timeout_secs,
|
|
10
|
+
:first_frame_timeout_secs,
|
|
11
|
+
:logger,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
class Server
|
|
15
|
+
DEFAULT_OPTIONS = {
|
|
16
|
+
max_streams: 0,
|
|
17
|
+
max_payload: 1 << 24,
|
|
18
|
+
idle_timeout_secs: 30,
|
|
19
|
+
ping_timeout_secs: 10,
|
|
20
|
+
cert: nil,
|
|
21
|
+
key: nil,
|
|
22
|
+
ca_certs: [],
|
|
23
|
+
client_auth: nil,
|
|
24
|
+
first_frame_timeout_secs: 5,
|
|
25
|
+
logger: Logrb.noop,
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :port
|
|
29
|
+
|
|
30
|
+
def initialize(host, port, call_sign, **opts)
|
|
31
|
+
raise ArgumentError, "call_sign must have 4 bytes" if call_sign.bytesize != 4
|
|
32
|
+
|
|
33
|
+
opts = DEFAULT_OPTIONS.merge(opts)
|
|
34
|
+
@client_id = 0
|
|
35
|
+
@clients = {}
|
|
36
|
+
@client_queue = Async::Queue.new
|
|
37
|
+
@call_sign = call_sign
|
|
38
|
+
|
|
39
|
+
@logger = opts[:logger]
|
|
40
|
+
|
|
41
|
+
@listener = if opts[:cert] && opts[:key]
|
|
42
|
+
@logger.debug("Starting TLS server", host:, port:)
|
|
43
|
+
ctx = Mfp.default_tls_context
|
|
44
|
+
ctx.cert = OpenSSL::X509::Certificate.new(opts[:cert])
|
|
45
|
+
ctx.key = OpenSSL::PKey.read(opts[:key])
|
|
46
|
+
store = OpenSSL::X509::Store.new
|
|
47
|
+
store.set_default_paths
|
|
48
|
+
opts[:ca_certs].each do |raw|
|
|
49
|
+
store.add_cert(OpenSSL::X509::Certificate.new(raw))
|
|
50
|
+
end
|
|
51
|
+
ctx.cert_store = store
|
|
52
|
+
ctx.verify_mode = opts[:client_auth] if opts[:client_auth]
|
|
53
|
+
IO::Endpoint.ssl(host, port, ssl_context: ctx)
|
|
54
|
+
else
|
|
55
|
+
@logger.debug("Starting plain server", host:, port:)
|
|
56
|
+
IO::Endpoint.tcp(host, port)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@settings = Settings.new(
|
|
60
|
+
opts[:max_streams],
|
|
61
|
+
opts[:max_payload],
|
|
62
|
+
opts[:idle_timeout_secs],
|
|
63
|
+
opts[:ping_timeout_secs],
|
|
64
|
+
opts[:first_frame_timeout_secs],
|
|
65
|
+
opts[:logger],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@sockets = @listener.bind
|
|
69
|
+
@port = @sockets.first.local_address.ip_port
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def start(task)
|
|
73
|
+
return if @running
|
|
74
|
+
|
|
75
|
+
@running = true
|
|
76
|
+
@accept_tasks = @sockets.map do |sock|
|
|
77
|
+
task.async do |t|
|
|
78
|
+
loop do
|
|
79
|
+
peer = nil
|
|
80
|
+
begin
|
|
81
|
+
peer, = sock.accept
|
|
82
|
+
peer.accept if peer.respond_to?(:accept) && peer.is_a?(OpenSSL::SSL::SSLSocket)
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
@logger.debug("Error accepting connection", err: e.to_s)
|
|
85
|
+
peer&.close
|
|
86
|
+
next
|
|
87
|
+
end
|
|
88
|
+
client_addr = peer.remote_address.inspect_sockaddr
|
|
89
|
+
@logger.debug("Accepting connection", addr: client_addr)
|
|
90
|
+
id = @client_id
|
|
91
|
+
@client_id += 1
|
|
92
|
+
settings = Settings.new(**@settings.to_h, logger: @logger.with_fields(id:))
|
|
93
|
+
conn = Conn.new(self, id, peer, @call_sign, settings)
|
|
94
|
+
@logger.debug("Created client", addr: client_addr, id:)
|
|
95
|
+
@clients[id] = conn
|
|
96
|
+
@logger.debug("Starting client in background", id:)
|
|
97
|
+
conn.start(t)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def close
|
|
104
|
+
@accept_tasks&.each(&:stop)
|
|
105
|
+
@sockets.each(&:close)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def connect(client) = @client_queue.enqueue(client)
|
|
109
|
+
|
|
110
|
+
def disconnect(client)
|
|
111
|
+
@clients.delete(client.id)
|
|
112
|
+
@logger.debug("Disconnected client", id: client.id, connected_clients: @clients.length)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def accept = @client_queue.dequeue
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/mfp/server.rb
ADDED
data/lib/mfp/version.rb
ADDED
data/lib/mfp.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "io/endpoint"
|
|
5
|
+
require "io/endpoint/host_endpoint"
|
|
6
|
+
require "io/endpoint/ssl_endpoint"
|
|
7
|
+
require "brotli"
|
|
8
|
+
require "zlib"
|
|
9
|
+
require "openssl"
|
|
10
|
+
require "logrb"
|
|
11
|
+
|
|
12
|
+
require_relative "mfp/version"
|
|
13
|
+
require_relative "mfp/enum"
|
|
14
|
+
require_relative "mfp/tls_config"
|
|
15
|
+
require_relative "mfp/error"
|
|
16
|
+
require_relative "mfp/compressor"
|
|
17
|
+
require_relative "mfp/outgoing_message"
|
|
18
|
+
require_relative "mfp/resettable_timer"
|
|
19
|
+
require_relative "mfp/ping_handler"
|
|
20
|
+
require_relative "mfp/proto"
|
|
21
|
+
require_relative "mfp/event"
|
|
22
|
+
require_relative "mfp/server"
|
|
23
|
+
require_relative "mfp/client"
|
|
24
|
+
|
|
25
|
+
module Mfp
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def listen(host, port, call_sign, task, **)
|
|
29
|
+
srv = Server::Server.new(host, port, call_sign, **)
|
|
30
|
+
srv.start(task)
|
|
31
|
+
srv
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def dial(host, port, call_sign, task, **)
|
|
35
|
+
cli = Client.new(host, port, call_sign, **)
|
|
36
|
+
cli.start(task)
|
|
37
|
+
cli
|
|
38
|
+
end
|
|
39
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mfp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vito Sartori
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.10'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.10'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: brotli
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.8'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.8'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: io-endpoint
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.17'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.17'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: io-stream
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.14'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.14'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: logrb
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 0.2.0
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 0.2.0
|
|
82
|
+
description: General-purpose framing protocol for organising data across multiplexed
|
|
83
|
+
streams
|
|
84
|
+
email:
|
|
85
|
+
- hey@vito.io
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- ".editorconfig"
|
|
91
|
+
- ".ruby-version"
|
|
92
|
+
- CODE_OF_CONDUCT.md
|
|
93
|
+
- LICENSE.txt
|
|
94
|
+
- README.md
|
|
95
|
+
- Rakefile
|
|
96
|
+
- lib/mfp.rb
|
|
97
|
+
- lib/mfp/client.rb
|
|
98
|
+
- lib/mfp/compressor.rb
|
|
99
|
+
- lib/mfp/enum.rb
|
|
100
|
+
- lib/mfp/error.rb
|
|
101
|
+
- lib/mfp/event.rb
|
|
102
|
+
- lib/mfp/outgoing_message.rb
|
|
103
|
+
- lib/mfp/ping_handler.rb
|
|
104
|
+
- lib/mfp/proto.rb
|
|
105
|
+
- lib/mfp/proto/error.rb
|
|
106
|
+
- lib/mfp/proto/frame.rb
|
|
107
|
+
- lib/mfp/proto/hello.rb
|
|
108
|
+
- lib/mfp/proto/hello_response.rb
|
|
109
|
+
- lib/mfp/proto/ping.rb
|
|
110
|
+
- lib/mfp/proto/protocol.rb
|
|
111
|
+
- lib/mfp/proto/reader.rb
|
|
112
|
+
- lib/mfp/proto/writer.rb
|
|
113
|
+
- lib/mfp/resettable_timer.rb
|
|
114
|
+
- lib/mfp/server.rb
|
|
115
|
+
- lib/mfp/server/conn.rb
|
|
116
|
+
- lib/mfp/server/server.rb
|
|
117
|
+
- lib/mfp/tls_config.rb
|
|
118
|
+
- lib/mfp/version.rb
|
|
119
|
+
homepage: https://vito.io
|
|
120
|
+
licenses:
|
|
121
|
+
- MIT
|
|
122
|
+
metadata:
|
|
123
|
+
allowed_push_host: https://rubygems.org
|
|
124
|
+
homepage_uri: https://vito.io
|
|
125
|
+
rubygems_mfa_required: 'true'
|
|
126
|
+
rdoc_options: []
|
|
127
|
+
require_paths:
|
|
128
|
+
- lib
|
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: '4.0'
|
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
requirements: []
|
|
140
|
+
rubygems_version: 4.0.16
|
|
141
|
+
specification_version: 4
|
|
142
|
+
summary: mfp stands for Multiplexed Framing Protocol
|
|
143
|
+
test_files: []
|