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.
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ class PingHandler
5
+ def initialize(idle_timeout:, ping_timeout:)
6
+ @timer_notify = Async::Queue.new
7
+ @notifications = Async::Queue.new
8
+
9
+ @idle_timer = ResettableTimer.new(idle_timeout, @timer_notify, tag: :idle)
10
+ @response_timer = ResettableTimer.new(ping_timeout, @timer_notify, tag: :response)
11
+
12
+ @waiting_response = false
13
+ @waiting_cookie = nil
14
+ end
15
+
16
+ def recv = @notifications.dequeue
17
+
18
+ def start(task)
19
+ task.async { @idle_timer.start(task) }
20
+ task.async { @response_timer.start(task) }
21
+ task.async do
22
+ loop do
23
+ ev, tag = @timer_notify.dequeue
24
+ if ev.nil? && tag.nil?
25
+ break
26
+ elsif tag == :response && ev == :timeout
27
+ @notifications.enqueue({ kind: :timeout })
28
+ stop
29
+ break
30
+ elsif tag == :idle && ev == :timeout
31
+ next if @waiting_response
32
+
33
+ @waiting_response = true
34
+ @waiting_cookie = SecureRandom.bytes(8).unpack1("Q>")
35
+ @notifications.enqueue({
36
+ kind: :request,
37
+ cookie: @waiting_cookie,
38
+ })
39
+ @response_timer.reset
40
+ else
41
+ raise "Unexpected ev/tag pair: #{ev}, #{tag}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def notify_received
48
+ return if @waiting_response
49
+
50
+ @idle_timer.reset
51
+ @response_timer.reset
52
+ end
53
+
54
+ def notify_pong(cookie)
55
+ return if !@waiting_response || cookie != @waiting_cookie
56
+
57
+ @waiting_response = false
58
+ @waiting_cookie = nil
59
+ @idle_timer.reset
60
+ @response_timer.reset
61
+ end
62
+
63
+ def stop
64
+ @idle_timer.stop
65
+ @response_timer.stop
66
+ @timer_notify.close
67
+ @notifications.close
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ module Proto
5
+ # Error is the payload of a MessageKindError frame. It signals a problem on
6
+ # either a stream or the connection. The enclosing frame's StreamID
7
+ # disambiguate the scope: zero for connection-level errors, non-zero for
8
+ # stream-level errors.
9
+ class Error
10
+ attr_accessor :code, :terminal, :debug
11
+
12
+ def initialize(code, terminal: false, debug: nil)
13
+ @code = code
14
+ @terminal = terminal
15
+ @debug = debug
16
+ end
17
+
18
+ def encode(stream_id)
19
+ buf = Writer.new
20
+ buf.write_u8(@code)
21
+ if (size = @debug&.bytesize || 0).positive?
22
+ buf.write_u32(size)
23
+ buf.write(@debug)
24
+ else
25
+ buf.write_u32(0)
26
+ end
27
+
28
+ Frame.new(
29
+ call_sign: nil,
30
+ stream_id:,
31
+ kind: MessageKind::ERROR,
32
+ flags: @terminal ? 1 : 0,
33
+ length: buf.length,
34
+ payload: buf.string,
35
+ )
36
+ end
37
+
38
+ def self.read(fr)
39
+ raise ParserError.new(ErrorKind::FRAME_SIZE_ERROR) if fr.length < 5
40
+
41
+ reader = Reader.from_bytes(fr.payload)
42
+ terminal = fr.flags.allbits?(0x01)
43
+ code = reader.read_byte
44
+ debug_len = reader.read_u32
45
+ debug = nil
46
+ debug = reader.read_n(debug_len) if debug_len.positive?
47
+
48
+ new(code, terminal:, debug:)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ module Proto
5
+ # Frame is the fundamental unit of the MFP wire protocol. Every message
6
+ # exchanged between endpoints is wrapped in a Frame.
7
+ class Frame
8
+ attr_accessor :call_sign, :stream_id, :kind, :flags, :length, :payload
9
+
10
+ def initialize(stream_id:, kind:, flags:, length:, payload:, call_sign: "")
11
+ @call_sign = call_sign
12
+ @stream_id = stream_id
13
+ @kind = kind
14
+ @flags = flags
15
+ @length = length
16
+ @payload = payload
17
+ end
18
+
19
+ def encode
20
+ w = Writer.new
21
+ w.write(@call_sign)
22
+ w.write_u32(@stream_id || 0)
23
+ w.write_u8(@kind)
24
+ w.write_u8(@flags || 0)
25
+ w.write_u32(@length || 0)
26
+ w.write(@payload || "")
27
+ w.string
28
+ end
29
+
30
+ def self.read(io, max_len, call_sign)
31
+ r = Reader.new(io)
32
+ cs = r.read_n(4)
33
+ if cs.nil?
34
+ # This is a clean disconnect before reading the callsign.
35
+ # Raise ConnectionClosedError instead of an spurious
36
+ # InvalidCallSignError
37
+ raise ConnectionClosedError
38
+ end
39
+ raise InvalidCallSignError, cs.inspect if cs != call_sign
40
+
41
+ stream_id = r.read_u32
42
+ kind = r.read_byte
43
+ kind = MessageKind::UNKNOWN if kind > MessageKind.max
44
+ flags = r.read_byte
45
+ length = r.read_u32
46
+ raise PayloadTooLargeError if max_len.positive? && length > max_len
47
+
48
+ payload = length.positive? ? r.read_n(length) : ""
49
+ new(call_sign:, stream_id:, kind:, flags:, length:, payload:)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ module Proto
5
+ # Hello is the payload of a MessageKindHello frame. It must be the first
6
+ # frame sent by the client on every connection. The server responds with
7
+ # either a HelloResponse or an Error.
8
+ class Hello
9
+ attr_accessor :version_major, :version_minor, :capabilities, :application, :host
10
+
11
+ def initialize(version_major:, version_minor:, capabilities: 0, application: "", host: "")
12
+ @version_major = version_major
13
+ @version_minor = version_minor
14
+ @capabilities = capabilities
15
+ @application = application
16
+ @host = host
17
+ end
18
+
19
+ def encode(stream_id)
20
+ buf = Writer.new
21
+ buf.write_u8(@version_major)
22
+ buf.write_u8(@version_minor)
23
+ buf.write_u16(@capabilities)
24
+ buf.write_u16_string(@application)
25
+ buf.write_u16_string(@host)
26
+
27
+ Frame.new(
28
+ stream_id:,
29
+ kind: MessageKind::HELLO,
30
+ flags: 0,
31
+ length: buf.length,
32
+ payload: buf.string,
33
+ )
34
+ end
35
+
36
+ def self.read(fr)
37
+ raise ParserError.new(ErrorKind::FRAME_SIZE_ERROR) if fr.length < 8
38
+
39
+ r = Reader.new(StringIO.new(fr.payload))
40
+ version_major = r.read_byte
41
+ version_minor = r.read_byte
42
+ capabilities = r.read_u16
43
+ application = r.read_u16_string
44
+ host = r.read_u16_string
45
+ new(version_major:, version_minor:, capabilities:, application:, host:)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ module Proto
5
+ # HelloResponse is the payload of a MessageKindHelloResponse frame, sent by
6
+ # the server upon successfully accepting a Hello. It confirms the negotiated
7
+ # capabilities and advertises connection limits.
8
+ class HelloResponse
9
+ attr_accessor :capabilities, :max_streams, :max_payload
10
+
11
+ def initialize(capabilities:, max_streams:, max_payload:)
12
+ @capabilities = capabilities
13
+ @max_streams = max_streams
14
+ @max_payload = max_payload
15
+ end
16
+
17
+ def encode(stream_id)
18
+ buf = Writer.new
19
+ buf.write_u16(@capabilities)
20
+ buf.write_u32(@max_streams)
21
+ buf.write_u32(@max_payload)
22
+ Frame.new(
23
+ stream_id:,
24
+ kind: MessageKind::HELLO_RESPONSE,
25
+ flags: 0,
26
+ length: buf.length,
27
+ payload: buf.string,
28
+ )
29
+ end
30
+
31
+ def self.read(fr)
32
+ raise ParserError.new(ErrorKind::FRAME_SIZE_ERROR) if fr.length != 10
33
+
34
+ r = Reader.new(StringIO.new(fr.payload))
35
+ capabilities = r.read_u16
36
+ max_streams = r.read_u32
37
+ max_payload = r.read_u32
38
+
39
+ new(capabilities:, max_streams:, max_payload:)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ module Proto
5
+ class Ping
6
+ attr_accessor :ack, :payload
7
+
8
+ def initialize(ack:, payload:)
9
+ @ack = ack
10
+ @payload = payload
11
+ end
12
+
13
+ def encode(*)
14
+ buf = Writer.new
15
+ buf.write_u64(@payload)
16
+ Frame.new(
17
+ stream_id: 0,
18
+ kind: MessageKind::PING,
19
+ flags: @ack ? 0x01 : 0x00,
20
+ length: 8,
21
+ payload: buf.string,
22
+ )
23
+ end
24
+
25
+ def self.read(fr)
26
+ new(
27
+ ack: fr.flags.allbits?(0x01),
28
+ payload: fr.payload.unpack1("Q>"),
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ module Proto
5
+ # Message Kind -------------------------------------------------------------
6
+
7
+ class MessageKind < Enum
8
+ # UNKNOWN is the zero value and represents an unrecognised or
9
+ # uninitialised frame kind. Frames with this kind MUST be rejected as a
10
+ # protocol violation.
11
+ define :UNKNOWN, 0
12
+
13
+ # HELLO is sent by the client as the first frame of every
14
+ # connection. No other frame may be sent before the handshake is complete
15
+ define :HELLO, 1
16
+
17
+ # HELLO_RESPONSE is sent by the server in response to a Hello
18
+ # frame, confirming negotiated capabilities and connection limits
19
+ define :HELLO_RESPONSE, 2
20
+
21
+ # PING may be sent by either endpoint to verify that the
22
+ # connection is still alive. The receiving endpoint must echo the payload
23
+ # with the ACK flag set.
24
+ define :PING, 3
25
+
26
+ # ERROR signals a problem on a stream or the connection itself.
27
+ # The frame's StreamID disambiguates scope: zero for connection-level
28
+ # errors, non-zero for stream-level errors.
29
+ define :ERROR, 4
30
+
31
+ # STREAM carries an opaque payload for a given stream. The
32
+ # frame's StreamID must be non-zero. Payload semantics are defined by
33
+ # the upper-layer protocol.
34
+ define :STREAM, 5
35
+
36
+ # CONTROL carries an opaque, out-of-band signal scoped to
37
+ # either a stream (non-zero StreamID) or the connection (zero StreamID).
38
+ # Control frames are exempt from compression. Payload semantics are
39
+ # defined by the upper-layer protocol.
40
+ define :CONTROL, 6
41
+ end
42
+
43
+ # Capabilities -------------------------------------------------------------
44
+
45
+ class Capability < Enum
46
+ # COMPRESSION_GZIP indicates support for gzip payload
47
+ # compression on STREAM frames.
48
+ define :COMPRESSION_GZIP, 1 << 0
49
+ # COMPRESSION_BROTLI indicates support for Brotli payload
50
+ # compression on STREAM frames.
51
+ define :COMPRESSION_BROTLI, 1 << 1
52
+ end
53
+
54
+ # Error Kinds --------------------------------------------------------------
55
+
56
+ class ErrorKind < Enum
57
+ # PROTOCOL_ERROR signals that an invalid frame was received at this point
58
+ # in the stream's or connection's lifetime. Applicable to both streams
59
+ # and connections.
60
+ define :PROTOCOL_ERROR, 0x01
61
+
62
+ # INTERNAL_ERROR signals that an unexpected internal error occurred.
63
+ # Applicable to both streams and connections.
64
+ define :INTERNAL_ERROR, 0x02
65
+
66
+ # STREAM_CLOSED_ERROR signals that the targeted stream has been terminated
67
+ # or was never established. Applicable to streams only.
68
+ define :STREAM_CLOSED_ERROR, 0x03
69
+
70
+ # REFUSED_STREAM_ERROR signals that the server refused to open the stream.
71
+ # Applicable to streams only.
72
+ define :REFUSED_STREAM_ERROR, 0x04
73
+
74
+ # FRAME_SIZE_ERROR signals that a frame exceeded the negotiated Max. Payload
75
+ # or had an otherwise invalid size (e.g. a PING whose Length ≠ 8).
76
+ # Applicable to both streams and connections.
77
+ define :FRAME_SIZE_ERROR, 0x05
78
+
79
+ # COMPRESSION_ERROR signals that a payload could not be decompressed.
80
+ # Applicable to connections only.
81
+ define :COMPRESSION_ERROR, 0x06
82
+
83
+ # ENHANCE_YOUR_CALM_ERROR signals that the peer is generating excessive load.
84
+ # Applicable to connections only.
85
+ define :ENHANCE_YOUR_CALM_ERROR, 0x07
86
+
87
+ # INADEQUATE_SECURITY_ERROR signals that the underlying transport does not
88
+ # meet minimum security requirements. Applicable to connections only.
89
+ define :INADEQUATE_SECURITY_ERROR, 0x08
90
+
91
+ # INSUFFICIENT_CAPACITY_ERROR signals that the server lacks capacity to
92
+ # handle the request. Applicable to connections only.
93
+ define :INSUFFICIENT_CAPACITY_ERROR, 0x09
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ module Proto
5
+ class Reader
6
+ def self.from_bytes(value) = new(StringIO.new(value))
7
+
8
+ def initialize(io)
9
+ @io = io
10
+ end
11
+
12
+ def read_n(n) = @io.read(n)
13
+ def read_u16 = @io.read(2).unpack1("S>")
14
+ def read_u32 = @io.read(4).unpack1("L>")
15
+ def read_u64 = @io.read(8).unpack1("Q>")
16
+ def read_byte = @io.read(1).unpack1("C")
17
+
18
+ def read_u16_string
19
+ size = read_u16
20
+ return "" if size.zero?
21
+
22
+ read_n(size).encode("utf-8")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ module Proto
5
+ class Writer
6
+ def initialize
7
+ @buf = StringIO.new
8
+ end
9
+
10
+ def write(value) = @buf.write(value)
11
+ def write_u8(value) = write([value].pack("C"))
12
+ def write_u16(value) = write([value].pack("S>"))
13
+ def write_u32(value) = write([value].pack("L>"))
14
+ def write_u64(value) = write([value].pack("Q>"))
15
+
16
+ def write_u16_string(value)
17
+ l = value&.bytesize || 0
18
+ write_u16(l)
19
+ return if l.zero?
20
+
21
+ write(value.encode("utf-8"))
22
+ end
23
+
24
+ def string = @buf.string
25
+ def length = @buf.length
26
+ end
27
+ end
28
+ end
data/lib/mfp/proto.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "proto/protocol"
4
+ require_relative "proto/writer"
5
+ require_relative "proto/reader"
6
+ require_relative "proto/frame"
7
+ require_relative "proto/error"
8
+ require_relative "proto/hello"
9
+ require_relative "proto/hello_response"
10
+ require_relative "proto/ping"
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mfp
4
+ class ResettableTimer
5
+ def initialize(timeout, notify_queue, tag: nil)
6
+ @timeout = timeout
7
+ @reset_condition = Async::Condition.new
8
+ @running = true
9
+ @notify_queue = notify_queue
10
+ @tag = tag
11
+ @itask = nil
12
+ end
13
+
14
+ def start(task)
15
+ @itask = task.async do |task|
16
+ while @running
17
+ result = Async::Queue.new
18
+
19
+ sleeper = task.async do |s|
20
+ s.sleep(@timeout)
21
+ result.enqueue(:timeout)
22
+ end
23
+
24
+ waiter = task.async do
25
+ @reset_condition.wait
26
+ result.enqueue(:reset)
27
+ end
28
+
29
+ msg = result.dequeue
30
+ sleeper.stop unless sleeper.stopped?
31
+ waiter.stop unless waiter.stopped?
32
+ next if msg == :reset
33
+
34
+ @notify_queue.enqueue [msg, @tag]
35
+ end
36
+ end
37
+ end
38
+
39
+ def reset = @reset_condition.signal
40
+
41
+ def stop
42
+ @itask&.stop
43
+ @running = false
44
+ @reset_condition.signal
45
+ end
46
+ end
47
+ end