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.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +7 -0
- data/Rakefile +23 -0
- data/bin/vines +4 -0
- data/conf/certs/README +39 -0
- data/conf/certs/ca-bundle.crt +3895 -0
- data/conf/config.rb +42 -0
- data/lib/vines/cli.rb +132 -0
- data/lib/vines/cluster/connection.rb +26 -0
- data/lib/vines/cluster/publisher.rb +55 -0
- data/lib/vines/cluster/pubsub.rb +92 -0
- data/lib/vines/cluster/sessions.rb +125 -0
- data/lib/vines/cluster/subscriber.rb +108 -0
- data/lib/vines/cluster.rb +246 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +50 -0
- data/lib/vines/command/init.rb +68 -0
- data/lib/vines/command/ldap.rb +38 -0
- data/lib/vines/command/restart.rb +12 -0
- data/lib/vines/command/schema.rb +24 -0
- data/lib/vines/command/start.rb +28 -0
- data/lib/vines/command/stop.rb +18 -0
- data/lib/vines/config/host.rb +125 -0
- data/lib/vines/config/port.rb +132 -0
- data/lib/vines/config/pubsub.rb +108 -0
- data/lib/vines/config.rb +223 -0
- data/lib/vines/contact.rb +111 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/jid.rb +95 -0
- data/lib/vines/kit.rb +23 -0
- data/lib/vines/log.rb +24 -0
- data/lib/vines/router.rb +179 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +45 -0
- data/lib/vines/stanza/iq/disco_items.rb +29 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/private_storage.rb +83 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +140 -0
- data/lib/vines/stanza/iq/session.rb +17 -0
- data/lib/vines/stanza/iq/vcard.rb +56 -0
- data/lib/vines/stanza/iq/version.rb +25 -0
- data/lib/vines/stanza/iq.rb +48 -0
- data/lib/vines/stanza/message.rb +40 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +37 -0
- data/lib/vines/stanza/presence/subscribe.rb +42 -0
- data/lib/vines/stanza/presence/subscribed.rb +51 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
- data/lib/vines/stanza/presence.rb +141 -0
- data/lib/vines/stanza/pubsub/create.rb +39 -0
- data/lib/vines/stanza/pubsub/delete.rb +41 -0
- data/lib/vines/stanza/pubsub/publish.rb +66 -0
- data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
- data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
- data/lib/vines/stanza/pubsub.rb +22 -0
- data/lib/vines/stanza.rb +175 -0
- data/lib/vines/storage/ldap.rb +71 -0
- data/lib/vines/storage/local.rb +139 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/sql.rb +138 -0
- data/lib/vines/storage.rb +239 -0
- data/lib/vines/store.rb +110 -0
- data/lib/vines/stream/client/auth.rb +74 -0
- data/lib/vines/stream/client/auth_restart.rb +29 -0
- data/lib/vines/stream/client/bind.rb +72 -0
- data/lib/vines/stream/client/bind_restart.rb +24 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +17 -0
- data/lib/vines/stream/client/session.rb +210 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +38 -0
- data/lib/vines/stream/client.rb +84 -0
- data/lib/vines/stream/component/handshake.rb +26 -0
- data/lib/vines/stream/component/ready.rb +23 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/component.rb +58 -0
- data/lib/vines/stream/http/auth.rb +22 -0
- data/lib/vines/stream/http/bind.rb +32 -0
- data/lib/vines/stream/http/bind_restart.rb +37 -0
- data/lib/vines/stream/http/ready.rb +29 -0
- data/lib/vines/stream/http/request.rb +172 -0
- data/lib/vines/stream/http/session.rb +120 -0
- data/lib/vines/stream/http/sessions.rb +65 -0
- data/lib/vines/stream/http/start.rb +23 -0
- data/lib/vines/stream/http.rb +157 -0
- data/lib/vines/stream/parser.rb +79 -0
- data/lib/vines/stream/sasl.rb +128 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +13 -0
- data/lib/vines/stream/server/final_restart.rb +21 -0
- data/lib/vines/stream/server/outbound/auth.rb +31 -0
- data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
- data/lib/vines/stream/server/outbound/final_features.rb +28 -0
- data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/start.rb +20 -0
- data/lib/vines/stream/server/outbound/tls.rb +30 -0
- data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
- data/lib/vines/stream/server/ready.rb +24 -0
- data/lib/vines/stream/server/start.rb +13 -0
- data/lib/vines/stream/server/tls.rb +13 -0
- data/lib/vines/stream/server.rb +150 -0
- data/lib/vines/stream/state.rb +60 -0
- data/lib/vines/stream.rb +247 -0
- data/lib/vines/token_bucket.rb +55 -0
- data/lib/vines/user.rb +123 -0
- data/lib/vines/version.rb +6 -0
- data/lib/vines/xmpp_server.rb +25 -0
- data/lib/vines.rb +203 -0
- data/test/cluster/publisher_test.rb +57 -0
- data/test/cluster/sessions_test.rb +47 -0
- data/test/cluster/subscriber_test.rb +109 -0
- data/test/config/host_test.rb +369 -0
- data/test/config/pubsub_test.rb +187 -0
- data/test/config_test.rb +732 -0
- data/test/contact_test.rb +102 -0
- data/test/error_test.rb +58 -0
- data/test/ext/nokogiri.rb +14 -0
- data/test/jid_test.rb +147 -0
- data/test/kit_test.rb +31 -0
- data/test/router_test.rb +243 -0
- data/test/stanza/iq/disco_info_test.rb +78 -0
- data/test/stanza/iq/disco_items_test.rb +49 -0
- data/test/stanza/iq/private_storage_test.rb +184 -0
- data/test/stanza/iq/roster_test.rb +229 -0
- data/test/stanza/iq/session_test.rb +25 -0
- data/test/stanza/iq/vcard_test.rb +146 -0
- data/test/stanza/iq/version_test.rb +64 -0
- data/test/stanza/iq_test.rb +70 -0
- data/test/stanza/message_test.rb +126 -0
- data/test/stanza/presence/probe_test.rb +50 -0
- data/test/stanza/presence/subscribe_test.rb +83 -0
- data/test/stanza/pubsub/create_test.rb +116 -0
- data/test/stanza/pubsub/delete_test.rb +169 -0
- data/test/stanza/pubsub/publish_test.rb +309 -0
- data/test/stanza/pubsub/subscribe_test.rb +205 -0
- data/test/stanza/pubsub/unsubscribe_test.rb +148 -0
- data/test/stanza_test.rb +85 -0
- data/test/storage/ldap_test.rb +201 -0
- data/test/storage/local_test.rb +59 -0
- data/test/storage/mock_redis.rb +97 -0
- data/test/storage/null_test.rb +29 -0
- data/test/storage/storage_tests.rb +182 -0
- data/test/storage_test.rb +85 -0
- data/test/store_test.rb +130 -0
- data/test/stream/client/auth_test.rb +137 -0
- data/test/stream/client/ready_test.rb +47 -0
- data/test/stream/client/session_test.rb +27 -0
- data/test/stream/component/handshake_test.rb +52 -0
- data/test/stream/component/ready_test.rb +103 -0
- data/test/stream/component/start_test.rb +39 -0
- data/test/stream/http/auth_test.rb +70 -0
- data/test/stream/http/ready_test.rb +86 -0
- data/test/stream/http/request_test.rb +209 -0
- data/test/stream/http/sessions_test.rb +49 -0
- data/test/stream/http/start_test.rb +50 -0
- data/test/stream/parser_test.rb +122 -0
- data/test/stream/sasl_test.rb +195 -0
- data/test/stream/server/auth_test.rb +61 -0
- data/test/stream/server/outbound/auth_test.rb +75 -0
- data/test/stream/server/ready_test.rb +98 -0
- data/test/test_helper.rb +42 -0
- data/test/token_bucket_test.rb +44 -0
- data/test/user_test.rb +96 -0
- data/vines.gemspec +30 -0
- 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
|
data/lib/vines/stanza.rb
ADDED
@@ -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
|