wampproto 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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