async-rabbitmq 0.4.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,123 @@
1
+ module AsyncRabbitMQ
2
+ module SASL
3
+ class Base
4
+ # The AMQP mechanism name sent in connection.start-ok.
5
+ def mechanism_name
6
+ raise NotImplementedError
7
+ end
8
+
9
+ # Initial response sent with connection.start-ok.
10
+ def initial_response
11
+ raise NotImplementedError
12
+ end
13
+
14
+ # Handle a connection.secure challenge from the broker.
15
+ # Returns the response string for connection.secure-ok.
16
+ def challenge_response(_challenge)
17
+ raise NotImplementedError
18
+ end
19
+ end
20
+
21
+ class Plain < Base
22
+ def initialize(username, password)
23
+ @username = username
24
+ @password = password
25
+ end
26
+
27
+ def mechanism_name
28
+ "PLAIN"
29
+ end
30
+
31
+ def initial_response
32
+ "\x00#{@username}\x00#{@password}"
33
+ end
34
+
35
+ def challenge_response(_challenge)
36
+ initial_response
37
+ end
38
+ end
39
+
40
+ class External < Base
41
+ def mechanism_name
42
+ "EXTERNAL"
43
+ end
44
+
45
+ # EXTERNAL relies on the transport layer (TLS client cert) for identity.
46
+ # The initial response is empty — the broker extracts the identity from the cert.
47
+ def initial_response
48
+ ""
49
+ end
50
+
51
+ def challenge_response(_challenge)
52
+ ""
53
+ end
54
+ end
55
+
56
+ # Challenge-response mechanism for testing SASL multi-step flows.
57
+ # The broker sends a challenge; we echo it back as the response.
58
+ class CRRabbitMQDemo < Base
59
+ def mechanism_name
60
+ "RABBIT-CR-DEMO"
61
+ end
62
+
63
+ def initialize(username, password)
64
+ @username = username
65
+ @password = password
66
+ end
67
+
68
+ # Initial response is just the username — password comes in challenge_response.
69
+ def initial_response
70
+ @username
71
+ end
72
+
73
+ # The broker sends "Please tell me your password" (or similar);
74
+ # we respond with the password.
75
+ def challenge_response(_challenge)
76
+ @password
77
+ end
78
+ end
79
+
80
+ # Registry of built-in mechanism classes keyed by AMQP mechanism name.
81
+ REGISTRY = {
82
+ "PLAIN" => Plain,
83
+ "EXTERNAL" => External,
84
+ "RABBIT-CR-DEMO" => CRRabbitMQDemo,
85
+ }.freeze
86
+
87
+ # Select the best mechanism the broker supports, in preference order.
88
+ # Returns an instantiated mechanism object.
89
+ # Raises AuthenticationError if no common mechanism is found.
90
+ def self.negotiate(broker_mechanisms_string, preferred: nil, username: nil, password: nil)
91
+ broker_list = broker_mechanisms_string.split(/\s+/)
92
+
93
+ if preferred
94
+ name = preferred.upcase
95
+ unless broker_list.include?(name)
96
+ raise AuthenticationError, "Requested SASL mechanism #{name} not offered by broker (available: #{broker_list.join(', ')})"
97
+ end
98
+ return instantiate(name, username: username, password: password)
99
+ end
100
+
101
+ # Preference order: EXTERNAL (strongest, no credentials on wire), PLAIN (universal)
102
+ %w[EXTERNAL PLAIN].each do |name|
103
+ if broker_list.include?(name)
104
+ return instantiate(name, username: username, password: password)
105
+ end
106
+ end
107
+
108
+ raise AuthenticationError, "No supported SASL mechanism (broker offers: #{broker_list.join(', ')})"
109
+ end
110
+
111
+ def self.instantiate(name, username: nil, password: nil)
112
+ klass = REGISTRY[name]
113
+ raise AuthenticationError, "Unknown SASL mechanism: #{name}" unless klass
114
+
115
+ if klass.instance_method(:initialize).arity == 0
116
+ klass.new
117
+ else
118
+ klass.new(username, password)
119
+ end
120
+ end
121
+ private_class_method :instantiate
122
+ end
123
+ end