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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +39 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE +22 -0
- data/README.md +35 -0
- data/Rakefile +12 -0
- data/Steepfile +33 -0
- data/lib/wampproto/acceptor/authenticator.rb +14 -0
- data/lib/wampproto/acceptor/request.rb +58 -0
- data/lib/wampproto/acceptor/response.rb +19 -0
- data/lib/wampproto/acceptor.rb +183 -0
- data/lib/wampproto/auth/anonymous.rb +14 -0
- data/lib/wampproto/auth/base.rb +20 -0
- data/lib/wampproto/auth/cra.rb +84 -0
- data/lib/wampproto/auth/cryptosign.rb +86 -0
- data/lib/wampproto/auth/helpers.rb +25 -0
- data/lib/wampproto/auth/ticket.rb +27 -0
- data/lib/wampproto/auth.rb +14 -0
- data/lib/wampproto/broker.rb +155 -0
- data/lib/wampproto/dealer.rb +143 -0
- data/lib/wampproto/id_generator.rb +23 -0
- data/lib/wampproto/joiner.rb +78 -0
- data/lib/wampproto/message/abort.rb +32 -0
- data/lib/wampproto/message/authenticate.rb +25 -0
- data/lib/wampproto/message/base.rb +30 -0
- data/lib/wampproto/message/call.rb +33 -0
- data/lib/wampproto/message/challenge.rb +25 -0
- data/lib/wampproto/message/error.rb +27 -0
- data/lib/wampproto/message/event.rb +33 -0
- data/lib/wampproto/message/goodbye.rb +25 -0
- data/lib/wampproto/message/hello.rb +64 -0
- data/lib/wampproto/message/invocation.rb +33 -0
- data/lib/wampproto/message/publish.rb +33 -0
- data/lib/wampproto/message/published.rb +25 -0
- data/lib/wampproto/message/register.rb +26 -0
- data/lib/wampproto/message/registered.rb +25 -0
- data/lib/wampproto/message/result.rb +32 -0
- data/lib/wampproto/message/subscribe.rb +26 -0
- data/lib/wampproto/message/subscribed.rb +25 -0
- data/lib/wampproto/message/unregister.rb +25 -0
- data/lib/wampproto/message/unregistered.rb +24 -0
- data/lib/wampproto/message/unsubscribe.rb +25 -0
- data/lib/wampproto/message/unsubscribed.rb +24 -0
- data/lib/wampproto/message/welcome.rb +41 -0
- data/lib/wampproto/message/yield.rb +32 -0
- data/lib/wampproto/message.rb +108 -0
- data/lib/wampproto/message_with_recipient.rb +13 -0
- data/lib/wampproto/serializer/cbor.rb +18 -0
- data/lib/wampproto/serializer/json.rb +18 -0
- data/lib/wampproto/serializer/msgpack.rb +18 -0
- data/lib/wampproto/serializer.rb +5 -0
- data/lib/wampproto/session.rb +144 -0
- data/lib/wampproto/session_details.rb +24 -0
- data/lib/wampproto/validate.rb +52 -0
- data/lib/wampproto/version.rb +5 -0
- data/lib/wampproto.rb +32 -0
- data/sig/cbor.rbs +5 -0
- data/sig/ed25519/signing_key.rbs +7 -0
- data/sig/ed25519/verify_key.rbs +7 -0
- data/sig/message_pack.rbs +5 -0
- data/sig/wampproto/acceptor/authenticator.rbs +9 -0
- data/sig/wampproto/acceptor/request.rbs +46 -0
- data/sig/wampproto/acceptor/response.rbs +25 -0
- data/sig/wampproto/acceptor.rbs +86 -0
- data/sig/wampproto/auth/anonymous.rbs +13 -0
- data/sig/wampproto/auth/base.rbs +25 -0
- data/sig/wampproto/auth/cra.rbs +34 -0
- data/sig/wampproto/auth/cryptosign.rbs +38 -0
- data/sig/wampproto/auth/helpers.rbs +16 -0
- data/sig/wampproto/auth/ticket.rbs +21 -0
- data/sig/wampproto/broker.rbs +48 -0
- data/sig/wampproto/dealer.rbs +49 -0
- data/sig/wampproto/id_generator.rbs +17 -0
- data/sig/wampproto/joiner.rbs +43 -0
- data/sig/wampproto/message/abort.rbs +31 -0
- data/sig/wampproto/message/authenticate.rbs +20 -0
- data/sig/wampproto/message/base.rbs +14 -0
- data/sig/wampproto/message/call.rbs +34 -0
- data/sig/wampproto/message/challenge.rbs +20 -0
- data/sig/wampproto/message/error.rbs +28 -0
- data/sig/wampproto/message/event.rbs +34 -0
- data/sig/wampproto/message/goodbye.rbs +21 -0
- data/sig/wampproto/message/hello.rbs +39 -0
- data/sig/wampproto/message/invocation.rbs +34 -0
- data/sig/wampproto/message/publish.rbs +32 -0
- data/sig/wampproto/message/published.rbs +20 -0
- data/sig/wampproto/message/register.rbs +24 -0
- data/sig/wampproto/message/registered.rbs +20 -0
- data/sig/wampproto/message/result.rbs +32 -0
- data/sig/wampproto/message/subscribe.rbs +24 -0
- data/sig/wampproto/message/subscribed.rbs +20 -0
- data/sig/wampproto/message/unregister.rbs +20 -0
- data/sig/wampproto/message/unregistered.rbs +16 -0
- data/sig/wampproto/message/unsubscribe.rbs +20 -0
- data/sig/wampproto/message/unsubscribed.rbs +16 -0
- data/sig/wampproto/message/welcome.rbs +36 -0
- data/sig/wampproto/message/yield.rbs +30 -0
- data/sig/wampproto/message.rbs +59 -0
- data/sig/wampproto/message_with_recipient.rbs +15 -0
- data/sig/wampproto/serializer/cbor.rbs +10 -0
- data/sig/wampproto/serializer/json.rbs +11 -0
- data/sig/wampproto/serializer/msgpack.rbs +11 -0
- data/sig/wampproto/session.rbs +43 -0
- data/sig/wampproto/session_details.rbs +25 -0
- data/sig/wampproto/validate.rbs +18 -0
- data/sig/wampproto.rbs +17 -0
- data/wampproto.gemspec +39 -0
- 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
|