wampproto 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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +39 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/LICENSE +22 -0
  7. data/README.md +35 -0
  8. data/Rakefile +12 -0
  9. data/Steepfile +33 -0
  10. data/lib/wampproto/acceptor/authenticator.rb +14 -0
  11. data/lib/wampproto/acceptor/request.rb +58 -0
  12. data/lib/wampproto/acceptor/response.rb +19 -0
  13. data/lib/wampproto/acceptor.rb +183 -0
  14. data/lib/wampproto/auth/anonymous.rb +14 -0
  15. data/lib/wampproto/auth/base.rb +20 -0
  16. data/lib/wampproto/auth/cra.rb +84 -0
  17. data/lib/wampproto/auth/cryptosign.rb +86 -0
  18. data/lib/wampproto/auth/helpers.rb +25 -0
  19. data/lib/wampproto/auth/ticket.rb +27 -0
  20. data/lib/wampproto/auth.rb +14 -0
  21. data/lib/wampproto/broker.rb +155 -0
  22. data/lib/wampproto/dealer.rb +143 -0
  23. data/lib/wampproto/id_generator.rb +23 -0
  24. data/lib/wampproto/joiner.rb +78 -0
  25. data/lib/wampproto/message/abort.rb +32 -0
  26. data/lib/wampproto/message/authenticate.rb +25 -0
  27. data/lib/wampproto/message/base.rb +30 -0
  28. data/lib/wampproto/message/call.rb +33 -0
  29. data/lib/wampproto/message/challenge.rb +25 -0
  30. data/lib/wampproto/message/error.rb +27 -0
  31. data/lib/wampproto/message/event.rb +33 -0
  32. data/lib/wampproto/message/goodbye.rb +25 -0
  33. data/lib/wampproto/message/hello.rb +64 -0
  34. data/lib/wampproto/message/invocation.rb +33 -0
  35. data/lib/wampproto/message/publish.rb +33 -0
  36. data/lib/wampproto/message/published.rb +25 -0
  37. data/lib/wampproto/message/register.rb +26 -0
  38. data/lib/wampproto/message/registered.rb +25 -0
  39. data/lib/wampproto/message/result.rb +32 -0
  40. data/lib/wampproto/message/subscribe.rb +26 -0
  41. data/lib/wampproto/message/subscribed.rb +25 -0
  42. data/lib/wampproto/message/unregister.rb +25 -0
  43. data/lib/wampproto/message/unregistered.rb +24 -0
  44. data/lib/wampproto/message/unsubscribe.rb +25 -0
  45. data/lib/wampproto/message/unsubscribed.rb +24 -0
  46. data/lib/wampproto/message/welcome.rb +41 -0
  47. data/lib/wampproto/message/yield.rb +32 -0
  48. data/lib/wampproto/message.rb +108 -0
  49. data/lib/wampproto/message_with_recipient.rb +13 -0
  50. data/lib/wampproto/serializer/cbor.rb +18 -0
  51. data/lib/wampproto/serializer/json.rb +18 -0
  52. data/lib/wampproto/serializer/msgpack.rb +18 -0
  53. data/lib/wampproto/serializer.rb +5 -0
  54. data/lib/wampproto/session.rb +144 -0
  55. data/lib/wampproto/session_details.rb +24 -0
  56. data/lib/wampproto/validate.rb +52 -0
  57. data/lib/wampproto/version.rb +5 -0
  58. data/lib/wampproto.rb +32 -0
  59. data/sig/cbor.rbs +5 -0
  60. data/sig/ed25519/signing_key.rbs +7 -0
  61. data/sig/ed25519/verify_key.rbs +7 -0
  62. data/sig/message_pack.rbs +5 -0
  63. data/sig/wampproto/acceptor/authenticator.rbs +9 -0
  64. data/sig/wampproto/acceptor/request.rbs +46 -0
  65. data/sig/wampproto/acceptor/response.rbs +25 -0
  66. data/sig/wampproto/acceptor.rbs +86 -0
  67. data/sig/wampproto/auth/anonymous.rbs +13 -0
  68. data/sig/wampproto/auth/base.rbs +25 -0
  69. data/sig/wampproto/auth/cra.rbs +34 -0
  70. data/sig/wampproto/auth/cryptosign.rbs +38 -0
  71. data/sig/wampproto/auth/helpers.rbs +16 -0
  72. data/sig/wampproto/auth/ticket.rbs +21 -0
  73. data/sig/wampproto/broker.rbs +48 -0
  74. data/sig/wampproto/dealer.rbs +49 -0
  75. data/sig/wampproto/id_generator.rbs +17 -0
  76. data/sig/wampproto/joiner.rbs +43 -0
  77. data/sig/wampproto/message/abort.rbs +31 -0
  78. data/sig/wampproto/message/authenticate.rbs +20 -0
  79. data/sig/wampproto/message/base.rbs +14 -0
  80. data/sig/wampproto/message/call.rbs +34 -0
  81. data/sig/wampproto/message/challenge.rbs +20 -0
  82. data/sig/wampproto/message/error.rbs +28 -0
  83. data/sig/wampproto/message/event.rbs +34 -0
  84. data/sig/wampproto/message/goodbye.rbs +21 -0
  85. data/sig/wampproto/message/hello.rbs +39 -0
  86. data/sig/wampproto/message/invocation.rbs +34 -0
  87. data/sig/wampproto/message/publish.rbs +32 -0
  88. data/sig/wampproto/message/published.rbs +20 -0
  89. data/sig/wampproto/message/register.rbs +24 -0
  90. data/sig/wampproto/message/registered.rbs +20 -0
  91. data/sig/wampproto/message/result.rbs +32 -0
  92. data/sig/wampproto/message/subscribe.rbs +24 -0
  93. data/sig/wampproto/message/subscribed.rbs +20 -0
  94. data/sig/wampproto/message/unregister.rbs +20 -0
  95. data/sig/wampproto/message/unregistered.rbs +16 -0
  96. data/sig/wampproto/message/unsubscribe.rbs +20 -0
  97. data/sig/wampproto/message/unsubscribed.rbs +16 -0
  98. data/sig/wampproto/message/welcome.rbs +36 -0
  99. data/sig/wampproto/message/yield.rbs +30 -0
  100. data/sig/wampproto/message.rbs +59 -0
  101. data/sig/wampproto/message_with_recipient.rbs +15 -0
  102. data/sig/wampproto/serializer/cbor.rbs +10 -0
  103. data/sig/wampproto/serializer/json.rbs +11 -0
  104. data/sig/wampproto/serializer/msgpack.rbs +11 -0
  105. data/sig/wampproto/session.rbs +43 -0
  106. data/sig/wampproto/session_details.rbs +25 -0
  107. data/sig/wampproto/validate.rbs +18 -0
  108. data/sig/wampproto.rbs +17 -0
  109. data/wampproto.gemspec +39 -0
  110. metadata +196 -0
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ed25519"
4
+
5
+ module Wampproto
6
+ module Auth
7
+ # generates wampcra authentication signature
8
+ class Cryptosign < Base
9
+ include Helpers
10
+
11
+ attr_reader :private_key
12
+ attr_accessor :channel_id
13
+
14
+ AUTH_METHOD = "cryptosign"
15
+
16
+ def initialize(private_key, authid, authextra = {})
17
+ @private_key = Validate.string!("Private Key", private_key)
18
+ super(AUTH_METHOD, authid, authextra)
19
+ end
20
+
21
+ def authenticate(challenge)
22
+ signature = self.class.create_signature(private_key, challenge.extra[:challenge], channel_id)
23
+ Message::Authenticate.new(signature)
24
+ end
25
+
26
+ class << self
27
+ def create_challenge
28
+ binary_challenge = SecureRandom.random_bytes(32)
29
+ binary_to_hex(binary_challenge)
30
+ end
31
+
32
+ def verify_challenge(signature, msg, public_key)
33
+ verify_key = Ed25519::VerifyKey.new(hex_to_binary(public_key))
34
+
35
+ binary_signature = hex_to_binary(signature)
36
+ signature = binary_signature[0, 64].to_s
37
+ message = binary_signature[64, 32].to_s
38
+ return false if message.empty? || signature.empty?
39
+ return false if msg != binary_to_hex(message)
40
+
41
+ verify_key.verify(signature, message)
42
+ end
43
+
44
+ def sign_challenge(private_key, challenge, channel_id = nil)
45
+ create_signature(private_key, challenge, channel_id)
46
+ end
47
+
48
+ def create_signature(private_key, challenge, channel_id = nil)
49
+ return handle_channel_binding(private_key, challenge, channel_id) if channel_id
50
+
51
+ handle_without_channel_binding(private_key, challenge)
52
+ end
53
+
54
+ def handle_without_channel_binding(private_key, hex_challenge)
55
+ key_pair = Ed25519::SigningKey.new(hex_to_binary(private_key))
56
+
57
+ binary_challenge = hex_to_binary(hex_challenge)
58
+ binary_signature = key_pair.sign(binary_challenge)
59
+ signature = binary_to_hex(binary_signature)
60
+
61
+ "#{signature}#{hex_challenge}"
62
+ end
63
+
64
+ def handle_channel_binding(private_key, hex_challenge, channel_id)
65
+ key_pair = Ed25519::SigningKey.new(hex_to_binary(private_key))
66
+
67
+ channel_id = hex_to_binary(channel_id)
68
+ challenge = hex_to_binary(hex_challenge)
69
+ xored_challenge = xored_strings(channel_id, challenge)
70
+ binary_signed_challenge = key_pair.sign(xored_challenge)
71
+ signature = binary_to_hex(binary_signed_challenge)
72
+ hex_xored_challenge = binary_to_hex(xored_challenge)
73
+ "#{signature}#{hex_xored_challenge}"
74
+ end
75
+
76
+ def xored_strings(channel_id, challenge_str)
77
+ channel_id_bytes = channel_id.bytes
78
+ challenge_bytes = challenge_str.bytes
79
+ # Added || 0 like (byte1 || 0) to make steep check happy
80
+ xored = channel_id_bytes.zip(challenge_bytes).map { |byte1, byte2| (byte1 || 0) ^ (byte2 || 0) }
81
+ xored.pack("C*")
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ # Auth classes
5
+ module Auth
6
+ # Auth Helpers
7
+ module Helpers
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ base.include(ClassMethods)
11
+ end
12
+
13
+ # class methods
14
+ module ClassMethods
15
+ def hex_to_binary(hex_string)
16
+ [hex_string].pack("H*")
17
+ end
18
+
19
+ def binary_to_hex(binary_string)
20
+ binary_string.unpack1("H*")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ module Auth
5
+ # generates ticket authentication signature
6
+ class Ticket < Base
7
+ AUTH_METHOD = "ticket"
8
+ attr_reader :secret
9
+
10
+ def initialize(secret, authid, authextra = {})
11
+ @secret = Validate.string!("Secret", secret)
12
+ super(AUTH_METHOD, authid, authextra)
13
+ end
14
+
15
+ def authenticate(challenge)
16
+ signature = create_signature(challenge)
17
+ Message::Authenticate.new(signature)
18
+ end
19
+
20
+ private
21
+
22
+ def create_signature(_challenge)
23
+ secret
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth/helpers"
4
+ require_relative "auth/base"
5
+ require_relative "auth/anonymous"
6
+ require_relative "auth/ticket"
7
+ require_relative "auth/cra"
8
+ require_relative "auth/cryptosign"
9
+
10
+ module Wampproto
11
+ # Auth classes
12
+ module Auth
13
+ end
14
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ # Wampproto broker implementation
5
+ class Broker # rubocop:disable Metrics/ClassLength
6
+ attr_reader :subscriptions_by_session, :subscriptions_by_topic, :id_gen, :sessions
7
+
8
+ def initialize(id_gen = IdGenerator.new)
9
+ @id_gen = id_gen
10
+ @subscriptions_by_session = {}
11
+ @subscriptions_by_topic = {}
12
+ @sessions = {}
13
+ end
14
+
15
+ def add_session(details)
16
+ session_id = details.session_id
17
+
18
+ error_message = "cannot add session twice"
19
+ raise KeyError, error_message if subscriptions_by_session.include?(session_id)
20
+
21
+ subscriptions_by_session[session_id] = {}
22
+ sessions[session_id] = details
23
+ end
24
+
25
+ def remove_session(session_id)
26
+ error_message = "cannot remove non-existing session"
27
+ raise KeyError, error_message unless subscriptions_by_session.include?(session_id)
28
+
29
+ subscriptions = subscriptions_by_session.delete(session_id) || {}
30
+ subscriptions.each do |subscription_id, topic|
31
+ remove_topic_subscriber(topic, subscription_id, session_id)
32
+ end
33
+ sessions.delete(session_id)
34
+ end
35
+
36
+ def subscription?(topic)
37
+ subscriptions = subscriptions_by_topic[topic]
38
+ return false unless subscriptions
39
+
40
+ subscriptions.any?
41
+ end
42
+
43
+ def receive_message(session_id, message)
44
+ case message
45
+ when Message::Subscribe then handle_subscribe(session_id, message)
46
+ when Message::Unsubscribe then handle_unsubscribe(session_id, message)
47
+ when Message::Publish then handle_publish(session_id, message)
48
+ else
49
+ raise ValueError, "message type not supported"
50
+ end
51
+ end
52
+
53
+ def handle_publish(session_id, message) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
54
+ error_message = "cannot publish, session #{session_id} doesn't exist"
55
+ raise ValueError, error_message unless subscriptions_by_session.include?(session_id)
56
+
57
+ subscriptions = subscriptions_by_topic.fetch(message.topic, {})
58
+ return if subscriptions.empty?
59
+
60
+ publication_id = id_gen.next
61
+
62
+ messages = []
63
+ if message.options[:acknowledge]
64
+ published = Message::Published.new(message.request_id, publication_id)
65
+ messages << MessageWithRecipient.new(published, session_id)
66
+ end
67
+ subscription_id, session_ids = subscriptions.first
68
+
69
+ event_options = event_details_for(session_id, message)
70
+ event = Message::Event.new(subscription_id, publication_id, event_options, *message.args, **message.kwargs)
71
+
72
+ session_ids.each_with_object(messages) do |recipient_id, list|
73
+ list << MessageWithRecipient.new(event, recipient_id) unless exclude?(message, session_id, recipient_id)
74
+ end
75
+ end
76
+
77
+ def handle_subscribe(session_id, message)
78
+ error_message = "cannot subscribe, session #{session_id} doesn't exist"
79
+ raise ValueError, error_message unless subscriptions_by_session.include?(session_id)
80
+
81
+ subscription_id = find_subscription_id_from(message.topic)
82
+ add_topic_subscriber(message.topic, subscription_id, session_id)
83
+ subscriptions_by_session[session_id][subscription_id] = message.topic
84
+
85
+ subscribed = Message::Subscribed.new(message.request_id, subscription_id)
86
+ MessageWithRecipient.new(subscribed, session_id)
87
+ end
88
+
89
+ def handle_unsubscribe(session_id, message) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
90
+ error_message = "cannot unsubscribe, session #{session_id} doesn't exist"
91
+ raise ValueError, error_message unless subscriptions_by_session.include?(session_id)
92
+
93
+ subscriptions = subscriptions_by_session.fetch(session_id)
94
+
95
+ unless subscriptions.include?(message.subscription_id)
96
+ error = Message::Error.new(Message::Type::UNSUBSCRIBE, message.request_id, {},
97
+ "wamp.error.no_such_subscription")
98
+ return MessageWithRecipient.new(error, session_id)
99
+ end
100
+
101
+ topic = subscriptions.fetch(message.subscription_id)
102
+
103
+ remove_topic_subscriber(topic, message.subscription_id, session_id)
104
+ subscriptions_by_session[session_id].delete(message.subscription_id)
105
+
106
+ unsubscribed = Message::Unsubscribed.new(message.request_id)
107
+ MessageWithRecipient.new(unsubscribed, session_id)
108
+ end
109
+
110
+ private
111
+
112
+ def find_subscription_id_from(topic)
113
+ subscription_id, = subscriptions_by_topic.fetch(topic, {}).first
114
+ return subscription_id if subscription_id
115
+
116
+ id_gen.next
117
+ end
118
+
119
+ def remove_topic_subscriber(topic, subscription_id, session_id)
120
+ subscriptions = subscriptions_by_topic.fetch(topic, {})
121
+ return if subscriptions.empty?
122
+
123
+ if subscriptions.one? && subscriptions[subscription_id].include?(session_id)
124
+ return subscriptions_by_topic.delete(topic)
125
+ end
126
+
127
+ subscriptions_by_topic[topic][subscription_id].delete(session_id)
128
+ end
129
+
130
+ def add_topic_subscriber(topic, subscription_id, session_id)
131
+ subscriptions = subscriptions_by_topic.fetch(topic, {})
132
+ if subscriptions.empty?
133
+ subscriptions[subscription_id] = [session_id]
134
+ else
135
+ sessions = subscriptions.fetch(subscription_id, [])
136
+ sessions << session_id unless sessions.include?(session_id)
137
+ subscriptions[subscription_id] = sessions
138
+ end
139
+ subscriptions_by_topic[topic] = subscriptions
140
+ end
141
+
142
+ def exclude?(message, session_id, recipient_id)
143
+ return false if session_id != recipient_id
144
+
145
+ message.options.fetch(:exclude_me, true)
146
+ end
147
+
148
+ def event_details_for(session_id, message)
149
+ return {} unless message.options.include?(:disclose_me)
150
+
151
+ session = sessions[session_id]
152
+ { publisher: session_id, publisher_authid: session.authid, publisher_authrole: session.authrole }
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ # Wamprpoto Dealer handler
5
+ class Dealer
6
+ attr_reader :registrations_by_procedure, :registrations_by_session, :pending_calls, :pending_invocations, :id_gen,
7
+ :sessions
8
+
9
+ def initialize(id_gen = IdGenerator.new)
10
+ @registrations_by_session = {}
11
+ @registrations_by_procedure = Hash.new { |h, k| h[k] = {} }
12
+ @pending_calls = {}
13
+ @pending_invocations = {}
14
+ @id_gen = id_gen
15
+ @sessions = {}
16
+ end
17
+
18
+ def add_session(details)
19
+ session_id = details.session_id
20
+
21
+ error_message = "cannot add session twice"
22
+ raise KeyError, error_message if registrations_by_session.include?(session_id)
23
+
24
+ registrations_by_session[session_id] = {}
25
+ sessions[session_id] = details
26
+ end
27
+
28
+ def remove_session(session_id)
29
+ error_message = "cannot remove non-existing session"
30
+ raise KeyError, error_message unless registrations_by_session.include?(session_id)
31
+
32
+ registrations = registrations_by_session.delete(session_id) || {}
33
+ registrations.each do |registration_id, procedure|
34
+ registrations_by_procedure[procedure].delete(registration_id)
35
+ end
36
+ sessions.delete(session_id)
37
+ end
38
+
39
+ def registration?(procedure)
40
+ registrations = registrations_by_procedure[procedure]
41
+ return false unless registrations
42
+
43
+ registrations.any?
44
+ end
45
+
46
+ def receive_message(session_id, message)
47
+ case message
48
+ when Wampproto::Message::Call then handle_call(session_id, message)
49
+ when Message::Yield then handle_yield(session_id, message)
50
+ when Message::Register then handle_register(session_id, message)
51
+ when Message::Unregister then handle_unregister(session_id, message)
52
+ else
53
+ raise ValueError, "message type not supported"
54
+ end
55
+ end
56
+
57
+ def handle_call(session_id, message) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
58
+ registrations = registrations_by_procedure.fetch(message.procedure, {})
59
+ if registrations.empty?
60
+ error = Message::Error.new(Message::Type::CALL, message.request_id, {}, "wamp.error.no_such_procedure")
61
+ return MessageWithRecipient.new(error, session_id)
62
+ end
63
+
64
+ registration_id, callee_id = registrations.first
65
+
66
+ pending_calls[callee_id] = {} unless pending_calls.include?(callee_id)
67
+ pending_invocations[callee_id] = {} unless pending_invocations.include?(callee_id)
68
+
69
+ # we received call from the "caller" lets call that request_id "1"
70
+ # we need to send invocation message to "callee" let call that request_id "10"
71
+ # we need "caller" id after we have received yield so that request_id will be "10"
72
+ # we need to send request to "caller" to the original request_id 1
73
+ request_id = id_gen.next
74
+
75
+ pending_invocations[callee_id][request_id] = session_id
76
+
77
+ pending_calls[callee_id][session_id] = message.request_id
78
+
79
+ invocation = Message::Invocation.new(
80
+ request_id,
81
+ registration_id,
82
+ invocation_details_for(session_id, message),
83
+ *message.args,
84
+ **message.kwargs
85
+ )
86
+
87
+ MessageWithRecipient.new(invocation, callee_id)
88
+ end
89
+
90
+ def invocation_details_for(session_id, message)
91
+ return {} unless message.options.include?(:disclose_me)
92
+
93
+ session = sessions[session_id]
94
+ { caller: session_id, caller_authid: session.authid, caller_authrole: session.authrole }
95
+ end
96
+
97
+ def handle_yield(session_id, message)
98
+ calls = pending_calls.fetch(session_id, {})
99
+ error_message = "no pending calls for session #{session_id}"
100
+ raise ValueError, error_message if calls.empty?
101
+
102
+ invocations = pending_invocations[session_id]
103
+ caller_id = invocations.delete(message.request_id).to_i # make steep happy
104
+
105
+ request_id = calls.delete(caller_id)
106
+
107
+ result = Message::Result.new(request_id, {}, *message.args, **message.kwargs)
108
+ MessageWithRecipient.new(result, caller_id)
109
+ end
110
+
111
+ def handle_register(session_id, message)
112
+ error_message = "cannot register, session #{session_id} doesn't exist"
113
+ raise ValueError, error_message unless registrations_by_session.include?(session_id)
114
+
115
+ registration_id = id_gen.next
116
+ registrations_by_procedure[message.procedure][registration_id] = session_id
117
+ registrations_by_session[session_id][registration_id] = message.procedure
118
+
119
+ registered = Message::Registered.new(message.request_id, registration_id)
120
+ MessageWithRecipient.new(registered, session_id)
121
+ end
122
+
123
+ def handle_unregister(session_id, message) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength)
124
+ error_message = "cannot unregister, session #{session_id} doesn't exist"
125
+ raise ValueError, error_message unless registrations_by_session.include?(session_id)
126
+
127
+ registrations = registrations_by_session.fetch(session_id)
128
+
129
+ unless registrations.include?(message.registration_id)
130
+ error = Message::Error.new(Message::Type::UNREGISTER, message.request_id, {}, "wamp.error.no_such_registration")
131
+ return MessageWithRecipient.new(error, session_id)
132
+ end
133
+
134
+ procedure = registrations.fetch(message.registration_id)
135
+
136
+ registrations_by_procedure[procedure].delete(message.registration_id)
137
+ registrations_by_session[session_id].delete(message.registration_id)
138
+
139
+ unregistered = Message::Unregistered.new(message.request_id)
140
+ MessageWithRecipient.new(unregistered, session_id)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ # ID Generator
5
+ class IdGenerator
6
+ MAX_ID = 1 << 53
7
+
8
+ class << self
9
+ def generate_session_id
10
+ rand(1..MAX_ID)
11
+ end
12
+ end
13
+
14
+ def initialize
15
+ @id = 0
16
+ end
17
+
18
+ def next
19
+ @id = 0 if @id == MAX_ID
20
+ @id += 1
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ CLIENT_ROLES = {
5
+ caller: { features: {} },
6
+ callee: { features: {} },
7
+ publisher: { features: {} },
8
+ subscriber: { features: {} }
9
+ }.freeze
10
+
11
+ # Handle Joining part of wamp protocol
12
+ class Joiner
13
+ STATE_NONE = 0
14
+ STATE_HELLO_SENT = 1
15
+ STATE_AUTHENTICATE_SENT = 2
16
+ STATE_JOINED = 3
17
+
18
+ attr_reader :realm, :serializer, :authenticator
19
+ attr_accessor :state
20
+
21
+ def initialize(realm, serializer = Serializer::JSON, authenticator = Auth::Anonymous.new)
22
+ @realm = realm
23
+ @serializer = serializer
24
+ @authenticator = authenticator
25
+ @state = STATE_NONE
26
+ end
27
+
28
+ def joined?
29
+ @state == STATE_JOINED
30
+ end
31
+
32
+ def send_hello # rubocop:disable Metrics/MethodLength
33
+ hello = Message::Hello.new(
34
+ realm,
35
+ {
36
+ roles: CLIENT_ROLES,
37
+ authid: authenticator.authid,
38
+ authmethods: [authenticator.authmethod],
39
+ authextra: authenticator.authextra
40
+ }
41
+ )
42
+
43
+ @state = STATE_HELLO_SENT
44
+ serializer.serialize(hello)
45
+ end
46
+
47
+ def receive(data)
48
+ message = serializer.deserialize(data)
49
+ to_send = receive_message(message)
50
+
51
+ return unless to_send.instance_of?(Message::Authenticate)
52
+
53
+ serializer.serialize(to_send)
54
+ end
55
+
56
+ def receive_message(msg) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
57
+ case msg
58
+ when Message::Welcome
59
+ if state != STATE_HELLO_SENT && state != STATE_AUTHENTICATE_SENT
60
+ raise ProtocolViolation, "Received WELCOME message after session was established"
61
+ end
62
+
63
+ @session_details = SessionDetails.new(msg.session_id, realm, msg.authid, msg.authrole)
64
+ self.state = STATE_JOINED
65
+ when Message::Challenge
66
+ raise ProtocolViolation, "Received CHALLENGE message before HELLO message was sent" if state != STATE_HELLO_SENT
67
+
68
+ authenticate = authenticator.authenticate(msg)
69
+ self.state = STATE_AUTHENTICATE_SENT
70
+ authenticate
71
+ when Message::Abort
72
+ raise ValueError, "received abort"
73
+ else
74
+ raise ValueError, "received unknown message"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ module Message
5
+ # abort message
6
+ class Abort < Base
7
+ attr_reader :details, :reason, :args, :kwargs
8
+
9
+ def initialize(details, reason, *args, **kwargs)
10
+ super()
11
+ @details = Validate.hash!("Details", details)
12
+ @reason = Validate.string!("Reason", reason)
13
+ @args = Validate.array!("Arguments", args)
14
+ @kwargs = Validate.hash!("Keyword Arguments", kwargs)
15
+ end
16
+
17
+ def marshal
18
+ @marshal = [Type::ABORT, details, reason]
19
+ @marshal << args if kwargs.any? || args.any?
20
+ @marshal << kwargs if kwargs.any?
21
+ @marshal
22
+ end
23
+
24
+ def self.parse(wamp_message)
25
+ _type, details, reason, args, kwargs = wamp_message
26
+ args ||= []
27
+ kwargs ||= {}
28
+ new(details, reason, *args, **kwargs)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ module Message
5
+ # Wamp Authenticate message
6
+ class Authenticate < Base
7
+ attr_reader :signature, :extra
8
+
9
+ def initialize(signature, extra = {})
10
+ super()
11
+ @signature = Validate.string!("Signature", signature)
12
+ @extra = Validate.hash!("Extra", extra)
13
+ end
14
+
15
+ def marshal
16
+ [Type::AUTHENTICATE, signature, extra]
17
+ end
18
+
19
+ def self.parse(wamp_message)
20
+ _type, signature, extra = wamp_message
21
+ new(signature, extra)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ module Message
5
+ # Base Interface for the Message(s)
6
+ class Base
7
+ class << self
8
+ def parse(msg)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def type
13
+ # to_s is added to satisfy RBS else they are not required
14
+ const = name.to_s.split("::").last.to_s.upcase
15
+ Wampproto::Message::Type.const_get(const)
16
+ end
17
+ end
18
+
19
+ def marshal
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def type
24
+ # to_s is added to satisfy RBS else they are not required
25
+ const = self.class.name.to_s.split("::").last.to_s.upcase
26
+ Wampproto::Message::Type.const_get(const)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wampproto
4
+ module Message
5
+ # wamp call message
6
+ class Call < Base
7
+ attr_reader :request_id, :options, :procedure, :args, :kwargs
8
+
9
+ def initialize(request_id, options, procedure, *args, **kwargs)
10
+ super()
11
+ @request_id = Validate.int!("Request Id", request_id)
12
+ @options = Validate.hash!("Options", options)
13
+ @procedure = Validate.string!("Procedure", procedure)
14
+ @args = Validate.array!("Arguments", args)
15
+ @kwargs = Validate.hash!("Keyword Arguments", kwargs)
16
+ end
17
+
18
+ def marshal
19
+ @payload = [Type::CALL, request_id, options, procedure]
20
+ @payload << args if kwargs.any? || args.any?
21
+ @payload << kwargs if kwargs.any?
22
+ @payload
23
+ end
24
+
25
+ def self.parse(wamp_message)
26
+ _type, request_id, options, procedure, args, kwargs = wamp_message
27
+ args ||= []
28
+ kwargs ||= {}
29
+ new(request_id, options, procedure, *args, **kwargs)
30
+ end
31
+ end
32
+ end
33
+ end