diaspora-vines 0.1.2

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 (174) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +19 -0
  4. data/README.md +7 -0
  5. data/Rakefile +23 -0
  6. data/bin/vines +4 -0
  7. data/conf/certs/README +39 -0
  8. data/conf/certs/ca-bundle.crt +3895 -0
  9. data/conf/config.rb +42 -0
  10. data/lib/vines/cli.rb +132 -0
  11. data/lib/vines/cluster/connection.rb +26 -0
  12. data/lib/vines/cluster/publisher.rb +55 -0
  13. data/lib/vines/cluster/pubsub.rb +92 -0
  14. data/lib/vines/cluster/sessions.rb +125 -0
  15. data/lib/vines/cluster/subscriber.rb +108 -0
  16. data/lib/vines/cluster.rb +246 -0
  17. data/lib/vines/command/bcrypt.rb +12 -0
  18. data/lib/vines/command/cert.rb +50 -0
  19. data/lib/vines/command/init.rb +68 -0
  20. data/lib/vines/command/ldap.rb +38 -0
  21. data/lib/vines/command/restart.rb +12 -0
  22. data/lib/vines/command/schema.rb +24 -0
  23. data/lib/vines/command/start.rb +28 -0
  24. data/lib/vines/command/stop.rb +18 -0
  25. data/lib/vines/config/host.rb +125 -0
  26. data/lib/vines/config/port.rb +132 -0
  27. data/lib/vines/config/pubsub.rb +108 -0
  28. data/lib/vines/config.rb +223 -0
  29. data/lib/vines/contact.rb +111 -0
  30. data/lib/vines/daemon.rb +78 -0
  31. data/lib/vines/error.rb +150 -0
  32. data/lib/vines/jid.rb +95 -0
  33. data/lib/vines/kit.rb +23 -0
  34. data/lib/vines/log.rb +24 -0
  35. data/lib/vines/router.rb +179 -0
  36. data/lib/vines/stanza/iq/auth.rb +18 -0
  37. data/lib/vines/stanza/iq/disco_info.rb +45 -0
  38. data/lib/vines/stanza/iq/disco_items.rb +29 -0
  39. data/lib/vines/stanza/iq/error.rb +16 -0
  40. data/lib/vines/stanza/iq/ping.rb +16 -0
  41. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  42. data/lib/vines/stanza/iq/query.rb +10 -0
  43. data/lib/vines/stanza/iq/result.rb +16 -0
  44. data/lib/vines/stanza/iq/roster.rb +140 -0
  45. data/lib/vines/stanza/iq/session.rb +17 -0
  46. data/lib/vines/stanza/iq/vcard.rb +56 -0
  47. data/lib/vines/stanza/iq/version.rb +25 -0
  48. data/lib/vines/stanza/iq.rb +48 -0
  49. data/lib/vines/stanza/message.rb +40 -0
  50. data/lib/vines/stanza/presence/error.rb +23 -0
  51. data/lib/vines/stanza/presence/probe.rb +37 -0
  52. data/lib/vines/stanza/presence/subscribe.rb +42 -0
  53. data/lib/vines/stanza/presence/subscribed.rb +51 -0
  54. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  55. data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
  56. data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
  57. data/lib/vines/stanza/presence.rb +141 -0
  58. data/lib/vines/stanza/pubsub/create.rb +39 -0
  59. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  60. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  61. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  62. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  63. data/lib/vines/stanza/pubsub.rb +22 -0
  64. data/lib/vines/stanza.rb +175 -0
  65. data/lib/vines/storage/ldap.rb +71 -0
  66. data/lib/vines/storage/local.rb +139 -0
  67. data/lib/vines/storage/null.rb +39 -0
  68. data/lib/vines/storage/sql.rb +138 -0
  69. data/lib/vines/storage.rb +239 -0
  70. data/lib/vines/store.rb +110 -0
  71. data/lib/vines/stream/client/auth.rb +74 -0
  72. data/lib/vines/stream/client/auth_restart.rb +29 -0
  73. data/lib/vines/stream/client/bind.rb +72 -0
  74. data/lib/vines/stream/client/bind_restart.rb +24 -0
  75. data/lib/vines/stream/client/closed.rb +13 -0
  76. data/lib/vines/stream/client/ready.rb +17 -0
  77. data/lib/vines/stream/client/session.rb +210 -0
  78. data/lib/vines/stream/client/start.rb +27 -0
  79. data/lib/vines/stream/client/tls.rb +38 -0
  80. data/lib/vines/stream/client.rb +84 -0
  81. data/lib/vines/stream/component/handshake.rb +26 -0
  82. data/lib/vines/stream/component/ready.rb +23 -0
  83. data/lib/vines/stream/component/start.rb +19 -0
  84. data/lib/vines/stream/component.rb +58 -0
  85. data/lib/vines/stream/http/auth.rb +22 -0
  86. data/lib/vines/stream/http/bind.rb +32 -0
  87. data/lib/vines/stream/http/bind_restart.rb +37 -0
  88. data/lib/vines/stream/http/ready.rb +29 -0
  89. data/lib/vines/stream/http/request.rb +172 -0
  90. data/lib/vines/stream/http/session.rb +120 -0
  91. data/lib/vines/stream/http/sessions.rb +65 -0
  92. data/lib/vines/stream/http/start.rb +23 -0
  93. data/lib/vines/stream/http.rb +157 -0
  94. data/lib/vines/stream/parser.rb +79 -0
  95. data/lib/vines/stream/sasl.rb +128 -0
  96. data/lib/vines/stream/server/auth.rb +13 -0
  97. data/lib/vines/stream/server/auth_restart.rb +13 -0
  98. data/lib/vines/stream/server/final_restart.rb +21 -0
  99. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  100. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  101. data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
  102. data/lib/vines/stream/server/outbound/final_features.rb +28 -0
  103. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  104. data/lib/vines/stream/server/outbound/start.rb +20 -0
  105. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  106. data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
  107. data/lib/vines/stream/server/ready.rb +24 -0
  108. data/lib/vines/stream/server/start.rb +13 -0
  109. data/lib/vines/stream/server/tls.rb +13 -0
  110. data/lib/vines/stream/server.rb +150 -0
  111. data/lib/vines/stream/state.rb +60 -0
  112. data/lib/vines/stream.rb +247 -0
  113. data/lib/vines/token_bucket.rb +55 -0
  114. data/lib/vines/user.rb +123 -0
  115. data/lib/vines/version.rb +6 -0
  116. data/lib/vines/xmpp_server.rb +25 -0
  117. data/lib/vines.rb +203 -0
  118. data/test/cluster/publisher_test.rb +57 -0
  119. data/test/cluster/sessions_test.rb +47 -0
  120. data/test/cluster/subscriber_test.rb +109 -0
  121. data/test/config/host_test.rb +369 -0
  122. data/test/config/pubsub_test.rb +187 -0
  123. data/test/config_test.rb +732 -0
  124. data/test/contact_test.rb +102 -0
  125. data/test/error_test.rb +58 -0
  126. data/test/ext/nokogiri.rb +14 -0
  127. data/test/jid_test.rb +147 -0
  128. data/test/kit_test.rb +31 -0
  129. data/test/router_test.rb +243 -0
  130. data/test/stanza/iq/disco_info_test.rb +78 -0
  131. data/test/stanza/iq/disco_items_test.rb +49 -0
  132. data/test/stanza/iq/private_storage_test.rb +184 -0
  133. data/test/stanza/iq/roster_test.rb +229 -0
  134. data/test/stanza/iq/session_test.rb +25 -0
  135. data/test/stanza/iq/vcard_test.rb +146 -0
  136. data/test/stanza/iq/version_test.rb +64 -0
  137. data/test/stanza/iq_test.rb +70 -0
  138. data/test/stanza/message_test.rb +126 -0
  139. data/test/stanza/presence/probe_test.rb +50 -0
  140. data/test/stanza/presence/subscribe_test.rb +83 -0
  141. data/test/stanza/pubsub/create_test.rb +116 -0
  142. data/test/stanza/pubsub/delete_test.rb +169 -0
  143. data/test/stanza/pubsub/publish_test.rb +309 -0
  144. data/test/stanza/pubsub/subscribe_test.rb +205 -0
  145. data/test/stanza/pubsub/unsubscribe_test.rb +148 -0
  146. data/test/stanza_test.rb +85 -0
  147. data/test/storage/ldap_test.rb +201 -0
  148. data/test/storage/local_test.rb +59 -0
  149. data/test/storage/mock_redis.rb +97 -0
  150. data/test/storage/null_test.rb +29 -0
  151. data/test/storage/storage_tests.rb +182 -0
  152. data/test/storage_test.rb +85 -0
  153. data/test/store_test.rb +130 -0
  154. data/test/stream/client/auth_test.rb +137 -0
  155. data/test/stream/client/ready_test.rb +47 -0
  156. data/test/stream/client/session_test.rb +27 -0
  157. data/test/stream/component/handshake_test.rb +52 -0
  158. data/test/stream/component/ready_test.rb +103 -0
  159. data/test/stream/component/start_test.rb +39 -0
  160. data/test/stream/http/auth_test.rb +70 -0
  161. data/test/stream/http/ready_test.rb +86 -0
  162. data/test/stream/http/request_test.rb +209 -0
  163. data/test/stream/http/sessions_test.rb +49 -0
  164. data/test/stream/http/start_test.rb +50 -0
  165. data/test/stream/parser_test.rb +122 -0
  166. data/test/stream/sasl_test.rb +195 -0
  167. data/test/stream/server/auth_test.rb +61 -0
  168. data/test/stream/server/outbound/auth_test.rb +75 -0
  169. data/test/stream/server/ready_test.rb +98 -0
  170. data/test/test_helper.rb +42 -0
  171. data/test/token_bucket_test.rb +44 -0
  172. data/test/user_test.rb +96 -0
  173. data/vines.gemspec +30 -0
  174. metadata +387 -0
@@ -0,0 +1,141 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence < Stanza
6
+ register "/presence"
7
+
8
+ VALID_TYPES = %w[subscribe subscribed unsubscribe unsubscribed unavailable probe error].freeze
9
+
10
+ VALID_TYPES.each do |type|
11
+ define_method "#{type}?" do
12
+ self['type'] == type
13
+ end
14
+ end
15
+
16
+ def process
17
+ stream.last_broadcast_presence = @node.clone unless validate_to
18
+ unless self['type'].nil?
19
+ raise StanzaErrors::BadRequest.new(self, 'modify')
20
+ end
21
+ dir = outbound? ? 'outbound' : 'inbound'
22
+ method("#{dir}_broadcast_presence").call
23
+ end
24
+
25
+ def outbound?
26
+ !inbound?
27
+ end
28
+
29
+ def inbound?
30
+ stream.class == Vines::Stream::Server ||
31
+ stream.class == Vines::Stream::Component
32
+ end
33
+
34
+ def outbound_broadcast_presence
35
+ self['from'] = stream.user.jid.to_s
36
+ to = validate_to
37
+ type = (self['type'] || '').strip
38
+ initial = to.nil? && type.empty? && !stream.available?
39
+
40
+ recipients = if to.nil?
41
+ stream.available_subscribers
42
+ else
43
+ stream.user.subscribed_from?(to) ? stream.available_resources(to) : []
44
+ end
45
+
46
+ broadcast(recipients)
47
+ broadcast(stream.available_resources(stream.user.jid))
48
+
49
+ if initial
50
+ stream.available_subscribed_to_resources.each do |recipient|
51
+ if recipient.last_broadcast_presence
52
+ el = recipient.last_broadcast_presence.clone
53
+ el['to'] = stream.user.jid.to_s
54
+ el['from'] = recipient.user.jid.to_s
55
+ stream.write(el)
56
+ end
57
+ end
58
+ stream.remote_subscribed_to_contacts.each do |contact|
59
+ send_probe(contact.jid.bare)
60
+ end
61
+ stream.available!
62
+ end
63
+
64
+ stream.remote_subscribers(to).each do |contact|
65
+ node = @node.clone
66
+ node['to'] = contact.jid.bare.to_s
67
+ router.route(node) rescue nil # ignore RemoteServerNotFound
68
+ end
69
+ end
70
+
71
+ def inbound_broadcast_presence
72
+ broadcast(stream.available_resources(validate_to))
73
+ end
74
+
75
+ private
76
+
77
+ def send_probe(to)
78
+ to = JID.new(to)
79
+ doc = Document.new
80
+ probe = doc.create_element('presence',
81
+ 'from' => stream.user.jid.bare.to_s,
82
+ 'id' => Kit.uuid,
83
+ 'to' => to.bare.to_s,
84
+ 'type' => 'probe')
85
+ router.route(probe) rescue nil # ignore RemoteServerNotFound
86
+ end
87
+
88
+ def auto_reply_to_subscription_request(from, type)
89
+ doc = Document.new
90
+ node = doc.create_element('presence') do |el|
91
+ el['from'] = from.to_s
92
+ el['id'] = self['id'] if self['id']
93
+ el['to'] = stream.user.jid.bare.to_s
94
+ el['type'] = type
95
+ end
96
+ stream.write(node)
97
+ end
98
+
99
+ # Send the contact's roster item to the current user's interested streams.
100
+ # Roster pushes are required, following presence subscription updates, to
101
+ # notify the user's clients of the contact's current state.
102
+ def send_roster_push(to)
103
+ contact = stream.user.contact(to)
104
+ stream.interested_resources(stream.user.jid).each do |recipient|
105
+ contact.send_roster_push(recipient)
106
+ end
107
+ end
108
+
109
+ # Notify the current user's interested streams of a contact's subscription
110
+ # state change as a result of receiving a subscribed, unsubscribe, or
111
+ # unsubscribed presence stanza.
112
+ def broadcast_subscription_change(contact)
113
+ stamp_from
114
+ stream.interested_resources(stamp_to).each do |recipient|
115
+ @node['to'] = recipient.user.jid.to_s
116
+ recipient.write(@node)
117
+ contact.send_roster_push(recipient)
118
+ end
119
+ end
120
+
121
+ # Validate that the incoming stanza has a 'to' attribute and strip any
122
+ # resource part from it so it's a bare jid. Return the bare JID object
123
+ # that was stamped.
124
+ def stamp_to
125
+ to = validate_to
126
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless to
127
+ to.bare.tap do |bare|
128
+ self['to'] = bare.to_s
129
+ end
130
+ end
131
+
132
+ # Presence subscription stanzas must be addressed from the user's bare
133
+ # JID. Return the user's bare JID object that was stamped.
134
+ def stamp_from
135
+ stream.user.jid.bare.tap do |bare|
136
+ self['from'] = bare.to_s
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Create < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:create", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:create', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+
19
+ id = (node['node'] || '').strip
20
+ id = Kit.uuid if id.empty?
21
+ raise StanzaErrors::Conflict.new(self, 'cancel') if pubsub.node?(id)
22
+ pubsub.add_node(id)
23
+ send_result_iq(id)
24
+ end
25
+
26
+ private
27
+
28
+ def send_result_iq(id)
29
+ el = to_result
30
+ el << el.document.create_element('pubsub') do |node|
31
+ node.default_namespace = NS
32
+ node << el.document.create_element('create', 'node' => id)
33
+ end
34
+ stream.write(el)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Delete < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:delete", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:delete', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+
19
+ id = node['node']
20
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
21
+
22
+ pubsub.publish(id, message(id))
23
+ pubsub.delete_node(id)
24
+ stream.write(to_result)
25
+ end
26
+
27
+ private
28
+
29
+ def message(id)
30
+ doc = Document.new
31
+ doc.create_element('message') do |node|
32
+ node << node.document.create_element('event') do |event|
33
+ event.default_namespace = NAMESPACES[:pubsub_event]
34
+ event << node.document.create_element('delete', 'node' => id)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Publish < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:publish", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:publish', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+ id = node['node']
19
+
20
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
21
+
22
+ item = node.xpath('ns:item', 'ns' => NS)
23
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless item.size == 1
24
+ item = item.first
25
+ unless item['id']
26
+ item['id'] = Kit.uuid
27
+ include_item = true
28
+ end
29
+
30
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless item.elements.size == 1
31
+ pubsub.publish(id, message(id, item))
32
+ send_result_iq(id, include_item ? item : nil)
33
+ end
34
+
35
+ private
36
+
37
+ def message(node, item)
38
+ doc = Document.new
39
+ doc.create_element('message') do |message|
40
+ message << doc.create_element('event') do |event|
41
+ event.default_namespace = NAMESPACES[:pubsub_event]
42
+ event << doc.create_element('items', 'node' => node) do |items|
43
+ items << doc.create_element('item', 'id' => item['id'], 'publisher' => stream.user.jid.to_s) do |el|
44
+ el << item.elements.first
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def send_result_iq(node, item)
52
+ result = to_result
53
+ if item
54
+ result << result.document.create_element('pubsub') do |pubsub|
55
+ pubsub.default_namespace = NS
56
+ pubsub << result.document.create_element('publish', 'node' => node) do |publish|
57
+ publish << result.document.create_element('item', 'id' => item['id'])
58
+ end
59
+ end
60
+ end
61
+ stream.write(result)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,44 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Subscribe < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:subscribe", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:subscribe', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+ id, jid = node['node'], JID.new(node['jid'])
19
+
20
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless stream.user.jid.bare == jid.bare
21
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
22
+ raise StanzaErrors::PolicyViolation.new(self, 'wait') if pubsub.subscribed?(id, jid)
23
+
24
+ pubsub.subscribe(id, jid)
25
+ send_result_iq(id, jid)
26
+ end
27
+
28
+ private
29
+
30
+ def send_result_iq(id, jid)
31
+ result = to_result
32
+ result << result.document.create_element('pubsub') do |node|
33
+ node.default_namespace = NS
34
+ node << result.document.create_element('subscription',
35
+ 'node' => id,
36
+ 'jid' => jid.to_s,
37
+ 'subscription' => 'subscribed')
38
+ end
39
+ stream.write(result)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Unsubscribe < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:unsubscribe", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:unsubscribe', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+ id, jid = node['node'], JID.new(node['jid'])
19
+
20
+ raise StanzaErrors::Forbidden.new(self, 'auth') unless stream.user.jid.bare == jid.bare
21
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
22
+ raise StanzaErrors::UnexpectedRequest.new(self, 'cancel') unless pubsub.subscribed?(id, jid)
23
+
24
+ pubsub.unsubscribe(id, jid)
25
+ stream.write(to_result)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub < Iq
6
+
7
+ private
8
+
9
+ # Return the Config::PubSub system for the domain to which this stanza is
10
+ # addressed or nil if it's not to a pubsub subdomain.
11
+ def pubsub
12
+ stream.config.pubsub(validate_to)
13
+ end
14
+
15
+ # Raise feature-not-implemented if this stanza is addressed to the chat
16
+ # server itself, rather than a pubsub subdomain.
17
+ def validate_to_address
18
+ raise StanzaErrors::FeatureNotImplemented.new(self, 'cancel') unless pubsub
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,175 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ include Nokogiri::XML
6
+
7
+ attr_reader :stream
8
+
9
+ EMPTY = ''.freeze
10
+ FROM, MESSAGE, TO = %w[from message to].map {|s| s.freeze }
11
+ ROUTABLE_STANZAS = %w[message iq presence].freeze
12
+
13
+ @@types = {}
14
+
15
+ def self.register(xpath, ns={})
16
+ @@types[[xpath, ns]] = self
17
+ end
18
+
19
+ def self.from_node(node, stream)
20
+ # optimize common case
21
+ return Message.new(node, stream) if node.name == MESSAGE
22
+ found = @@types.select {|pair, v| node.xpath(*pair).any? }
23
+ .sort {|a, b| b[0][0].length - a[0][0].length }.first
24
+ found ? found[1].new(node, stream) : nil
25
+ end
26
+
27
+ def initialize(node, stream)
28
+ @node, @stream = node, stream
29
+ end
30
+
31
+ # Send the stanza to all recipients, stamping it with from and
32
+ # to addresses first.
33
+ def broadcast(recipients)
34
+ @node[FROM] = stream.user.jid.to_s
35
+ recipients.each do |recipient|
36
+ @node[TO] = recipient.user.jid.to_s
37
+ recipient.write(@node)
38
+ end
39
+ end
40
+
41
+ # Returns true if this stanza should be processed locally. Returns false
42
+ # if it's destined for a remote domain or external component.
43
+ def local?
44
+ return true unless ROUTABLE_STANZAS.include?(@node.name)
45
+ to = JID.new(@node[TO])
46
+ to.empty? || local_jid?(to)
47
+ end
48
+
49
+ def local_jid?(*jids)
50
+ stream.config.local_jid?(*jids)
51
+ end
52
+
53
+ # Return true if this stanza is addressed to a pubsub subdomain hosted
54
+ # at this server. This helps differentiate between IQ stanzas addressed
55
+ # to the server and stanzas addressed to pubsub domains, both of which must
56
+ # be handled locally and not routed.
57
+ def to_pubsub_domain?
58
+ stream.config.pubsub?(validate_to)
59
+ end
60
+
61
+ def route
62
+ stream.router.route(@node)
63
+ end
64
+
65
+ def router
66
+ stream.router
67
+ end
68
+
69
+ def storage(domain=stream.domain)
70
+ stream.storage(domain)
71
+ end
72
+
73
+ def process
74
+ raise 'subclass must implement'
75
+ end
76
+
77
+ # Broadcast unavailable presence from the user's available resources to the
78
+ # recipient's available resources. Route the stanza to a remote server if
79
+ # the recipient isn't hosted locally.
80
+ def send_unavailable(from, to)
81
+ available = router.available_resources(from, to)
82
+ stanzas = available.map {|stream| unavailable(stream.user.jid) }
83
+ broadcast_to_available_resources(stanzas, to)
84
+ end
85
+
86
+ # Return an unavailable presence stanza addressed from the given JID.
87
+ def unavailable(from)
88
+ doc = Document.new
89
+ doc.create_element('presence',
90
+ 'from' => from.to_s,
91
+ 'id' => Kit.uuid,
92
+ 'type' => 'unavailable')
93
+ end
94
+
95
+ # Return nil if this stanza has no 'to' attribute. Return a Vines::JID
96
+ # if it contains a valid 'to' attribute. Raise a JidMalformed error if
97
+ # the JID is invalid.
98
+ def validate_to
99
+ validate_address(TO)
100
+ end
101
+
102
+ # Return nil if this stanza has no 'from' attribute. Return a Vines::JID
103
+ # if it contains a valid 'from' attribute. Raise a JidMalformed error if
104
+ # the JID is invalid.
105
+ def validate_from
106
+ validate_address(FROM)
107
+ end
108
+
109
+ def method_missing(method, *args, &block)
110
+ @node.send(method, *args, &block)
111
+ end
112
+
113
+ private
114
+
115
+ # Send the stanzas to the destination JID, routing to a s2s stream
116
+ # if the address is remote. This method properly stamps the to address
117
+ # on each stanza before it's sent. The caller must set the from address.
118
+ def broadcast_to_available_resources(stanzas, to)
119
+ return if send_to_remote(stanzas, to)
120
+ send_to_recipients(stanzas, stream.available_resources(to))
121
+ end
122
+
123
+ # Send the stanzas to the destination JID, routing to a s2s stream
124
+ # if the address is remote. This method properly stamps the to address
125
+ # on each stanza before it's sent. The caller must set the from address.
126
+ def broadcast_to_interested_resources(stanzas, to)
127
+ return if send_to_remote(stanzas, to)
128
+ send_to_recipients(stanzas, stream.interested_resources(to))
129
+ end
130
+
131
+ # Route the stanzas to a remote server, stamping a bare JID as the
132
+ # to address. Bare JIDs are required for presence subscription stanzas
133
+ # sent to the remote contact's server. Return true if the stanzas were
134
+ # routed, false if they must be delivered locally.
135
+ def send_to_remote(stanzas, to)
136
+ return false if local_jid?(to)
137
+ to = JID.new(to)
138
+ stanzas.each do |el|
139
+ el[TO] = to.bare.to_s
140
+ router.route(el)
141
+ end
142
+ true
143
+ end
144
+
145
+ # Send the stanzas to the local recipient streams, stamping a full JID as
146
+ # the to address. It's important to use full JIDs, even when sending to
147
+ # local clients, because the stanzas may be routed to other cluster nodes
148
+ # for delivery. We need the receiving cluster node to send the stanza just
149
+ # to this full JID, not to lookup all JIDs for this user.
150
+ def send_to_recipients(stanzas, recipients)
151
+ recipients.each do |recipient|
152
+ stanzas.each do |el|
153
+ el[TO] = recipient.user.jid.to_s
154
+ recipient.write(el)
155
+ end
156
+ end
157
+ end
158
+
159
+ # Return true if the to and from JIDs are allowed to communicate with one
160
+ # another based on the cross_domain_messages setting in conf/config.rb. If
161
+ # a domain's users are isolated to sending messages only within their own
162
+ # domain, pubsub stanzas must not be processed from remote JIDs.
163
+ def allowed?
164
+ stream.config.allowed?(validate_to || stream.domain, stream.user.jid)
165
+ end
166
+
167
+ def validate_address(attr)
168
+ jid = (self[attr] || EMPTY)
169
+ return if jid.empty?
170
+ JID.new(jid)
171
+ rescue
172
+ raise StanzaErrors::JidMalformed.new(self, 'modify')
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,71 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+
6
+ # Authenticates usernames and passwords against an LDAP directory. This can
7
+ # provide authentication logic for the other, full-featured Storage
8
+ # implementations while they store and retrieve the rest of the user
9
+ # information.
10
+ class Ldap
11
+ @@required = [:host, :port]
12
+ %w[tls dn password basedn object_class user_attr name_attr groupdn].each do |name|
13
+ @@required << name.to_sym unless name == 'groupdn'
14
+ define_method name do |*args|
15
+ @config[name.to_sym] = args.first
16
+ end
17
+ end
18
+
19
+ def initialize(host='localhost', port=636, &block)
20
+ @config = {host: host, port: port}
21
+ instance_eval(&block)
22
+ @@required.each {|key| raise "Must provide #{key}" if @config[key].nil? }
23
+ end
24
+
25
+ # Validates a username and password by binding to the LDAP instance with
26
+ # those credentials. If the bind succeeds, the user's attributes are
27
+ # retrieved.
28
+ def authenticate(username, password)
29
+ username = JID.new(username).to_s rescue nil
30
+ return if [username, password].any? {|arg| (arg || '').strip.empty? }
31
+
32
+ ldap = connect(@config[:dn], @config[:password])
33
+ entries = ldap.search(
34
+ attributes: [@config[:name_attr], 'mail'],
35
+ filter: filter(username))
36
+ return unless entries && entries.size == 1
37
+
38
+ user = if connect(entries.first.dn, password).bind
39
+ name = entries.first[@config[:name_attr]].first
40
+ User.new(jid: username, name: name.to_s, roster: [])
41
+ end
42
+ user
43
+ end
44
+
45
+ # Return an LDAP search filter for a user optionally belonging to the
46
+ # group defined by the groupdn config attribute.
47
+ def filter(username)
48
+ clas = Net::LDAP::Filter.eq('objectClass', @config[:object_class])
49
+ uid = Net::LDAP::Filter.eq(@config[:user_attr], username)
50
+ filter = clas & uid
51
+ if group = @config[:groupdn]
52
+ memberOf = Net::LDAP::Filter.eq('memberOf', group)
53
+ isMemberOf = Net::LDAP::Filter.eq('isMemberOf', group)
54
+ filter = filter & (memberOf | isMemberOf)
55
+ end
56
+ filter
57
+ end
58
+
59
+ private
60
+
61
+ def connect(dn, password)
62
+ options = [:host, :port, :base].zip(
63
+ @config.values_at(:host, :port, :basedn))
64
+ Net::LDAP.new(Hash[options]).tap do |ldap|
65
+ ldap.encryption(:simple_tls) if @config[:tls]
66
+ ldap.auth(dn, password)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end