vines 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.
Files changed (119) hide show
  1. data/LICENSE +19 -0
  2. data/README +34 -0
  3. data/Rakefile +55 -0
  4. data/bin/vines +95 -0
  5. data/conf/certs/README +32 -0
  6. data/conf/certs/ca-bundle.crt +3987 -0
  7. data/conf/config.rb +114 -0
  8. data/lib/vines.rb +155 -0
  9. data/lib/vines/command/bcrypt.rb +12 -0
  10. data/lib/vines/command/cert.rb +49 -0
  11. data/lib/vines/command/init.rb +58 -0
  12. data/lib/vines/command/ldap.rb +35 -0
  13. data/lib/vines/command/restart.rb +12 -0
  14. data/lib/vines/command/schema.rb +24 -0
  15. data/lib/vines/command/start.rb +28 -0
  16. data/lib/vines/command/stop.rb +18 -0
  17. data/lib/vines/config.rb +191 -0
  18. data/lib/vines/contact.rb +99 -0
  19. data/lib/vines/daemon.rb +78 -0
  20. data/lib/vines/error.rb +150 -0
  21. data/lib/vines/jid.rb +56 -0
  22. data/lib/vines/kit.rb +23 -0
  23. data/lib/vines/router.rb +125 -0
  24. data/lib/vines/stanza.rb +55 -0
  25. data/lib/vines/stanza/iq.rb +50 -0
  26. data/lib/vines/stanza/iq/auth.rb +18 -0
  27. data/lib/vines/stanza/iq/disco_info.rb +25 -0
  28. data/lib/vines/stanza/iq/disco_items.rb +23 -0
  29. data/lib/vines/stanza/iq/error.rb +16 -0
  30. data/lib/vines/stanza/iq/ping.rb +16 -0
  31. data/lib/vines/stanza/iq/query.rb +10 -0
  32. data/lib/vines/stanza/iq/result.rb +16 -0
  33. data/lib/vines/stanza/iq/roster.rb +153 -0
  34. data/lib/vines/stanza/iq/session.rb +22 -0
  35. data/lib/vines/stanza/iq/vcard.rb +58 -0
  36. data/lib/vines/stanza/message.rb +41 -0
  37. data/lib/vines/stanza/presence.rb +119 -0
  38. data/lib/vines/stanza/presence/error.rb +23 -0
  39. data/lib/vines/stanza/presence/probe.rb +38 -0
  40. data/lib/vines/stanza/presence/subscribe.rb +66 -0
  41. data/lib/vines/stanza/presence/subscribed.rb +64 -0
  42. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  43. data/lib/vines/stanza/presence/unsubscribe.rb +57 -0
  44. data/lib/vines/stanza/presence/unsubscribed.rb +50 -0
  45. data/lib/vines/storage.rb +216 -0
  46. data/lib/vines/storage/couchdb.rb +119 -0
  47. data/lib/vines/storage/ldap.rb +59 -0
  48. data/lib/vines/storage/local.rb +66 -0
  49. data/lib/vines/storage/redis.rb +108 -0
  50. data/lib/vines/storage/sql.rb +174 -0
  51. data/lib/vines/store.rb +51 -0
  52. data/lib/vines/stream.rb +198 -0
  53. data/lib/vines/stream/client.rb +131 -0
  54. data/lib/vines/stream/client/auth.rb +94 -0
  55. data/lib/vines/stream/client/auth_restart.rb +33 -0
  56. data/lib/vines/stream/client/bind.rb +58 -0
  57. data/lib/vines/stream/client/bind_restart.rb +25 -0
  58. data/lib/vines/stream/client/closed.rb +13 -0
  59. data/lib/vines/stream/client/ready.rb +15 -0
  60. data/lib/vines/stream/client/start.rb +27 -0
  61. data/lib/vines/stream/client/tls.rb +37 -0
  62. data/lib/vines/stream/component.rb +53 -0
  63. data/lib/vines/stream/component/handshake.rb +25 -0
  64. data/lib/vines/stream/component/ready.rb +24 -0
  65. data/lib/vines/stream/component/start.rb +19 -0
  66. data/lib/vines/stream/http.rb +111 -0
  67. data/lib/vines/stream/http/http_request.rb +22 -0
  68. data/lib/vines/stream/http/http_state.rb +139 -0
  69. data/lib/vines/stream/http/http_states.rb +53 -0
  70. data/lib/vines/stream/parser.rb +78 -0
  71. data/lib/vines/stream/server.rb +126 -0
  72. data/lib/vines/stream/server/auth.rb +13 -0
  73. data/lib/vines/stream/server/auth_restart.rb +19 -0
  74. data/lib/vines/stream/server/final_restart.rb +20 -0
  75. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  76. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  77. data/lib/vines/stream/server/outbound/auth_result.rb +28 -0
  78. data/lib/vines/stream/server/outbound/final_features.rb +27 -0
  79. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  80. data/lib/vines/stream/server/outbound/start.rb +20 -0
  81. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  82. data/lib/vines/stream/server/outbound/tls_result.rb +31 -0
  83. data/lib/vines/stream/server/ready.rb +20 -0
  84. data/lib/vines/stream/server/start.rb +13 -0
  85. data/lib/vines/stream/server/tls.rb +13 -0
  86. data/lib/vines/stream/state.rb +55 -0
  87. data/lib/vines/token_bucket.rb +46 -0
  88. data/lib/vines/user.rb +124 -0
  89. data/lib/vines/version.rb +5 -0
  90. data/lib/vines/xmpp_server.rb +25 -0
  91. data/test/config_test.rb +396 -0
  92. data/test/error_test.rb +59 -0
  93. data/test/ext/nokogiri.rb +14 -0
  94. data/test/jid_test.rb +71 -0
  95. data/test/kit_test.rb +21 -0
  96. data/test/router_test.rb +60 -0
  97. data/test/stanza/iq/roster_test.rb +198 -0
  98. data/test/stanza/iq/session_test.rb +30 -0
  99. data/test/stanza/iq/vcard_test.rb +159 -0
  100. data/test/stanza/message_test.rb +124 -0
  101. data/test/stanza/presence/subscribe_test.rb +75 -0
  102. data/test/storage/couchdb_test.rb +102 -0
  103. data/test/storage/ldap_test.rb +207 -0
  104. data/test/storage/local_test.rb +54 -0
  105. data/test/storage/redis_test.rb +75 -0
  106. data/test/storage/sql_test.rb +55 -0
  107. data/test/storage/storage_tests.rb +134 -0
  108. data/test/storage_test.rb +90 -0
  109. data/test/stream/client/auth_test.rb +127 -0
  110. data/test/stream/client/ready_test.rb +47 -0
  111. data/test/stream/component/handshake_test.rb +46 -0
  112. data/test/stream/component/ready_test.rb +105 -0
  113. data/test/stream/component/start_test.rb +41 -0
  114. data/test/stream/parser_test.rb +121 -0
  115. data/test/stream/server/outbound/auth_test.rb +77 -0
  116. data/test/stream/server/ready_test.rb +100 -0
  117. data/test/token_bucket_test.rb +24 -0
  118. data/test/user_test.rb +64 -0
  119. metadata +318 -0
@@ -0,0 +1,153 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Roster < Query
7
+ NS = NAMESPACES[:roster]
8
+
9
+ register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS
10
+
11
+ def process
12
+ get? ? roster_query : update_roster
13
+ end
14
+
15
+ private
16
+
17
+ # Send an iq result stanza containing roster items to the user in
18
+ # response to their roster get request. Requesting the roster makes
19
+ # this stream an "interested resource" that can now receive roster
20
+ # updates.
21
+ def roster_query
22
+ stream.requested_roster!
23
+ stream.write(stream.user.to_roster_xml(self['id']))
24
+ end
25
+
26
+ # Roster sets must have no 'to' address or be addressed to the same
27
+ # JID that sent the stanza. RFC 6121 sections 2.1.5 and 2.3.3.
28
+ def validate_to_address
29
+ to = (self['to'] || '').strip
30
+ unless to.empty? || JID.new(to).bare == stream.user.jid.bare
31
+ raise StanzaErrors::Forbidden.new(self, 'auth')
32
+ end
33
+ end
34
+
35
+ # Add, update, or delete the roster item contained in the iq set
36
+ # stanza received from the client. RFC 6121 sections 2.3, 2.4, 2.5.
37
+ def update_roster
38
+ validate_to_address
39
+
40
+ items = self.xpath('ns:query/ns:item', 'ns' => NS)
41
+ raise StanzaErrors::BadRequest.new(self, 'modify') if items.size != 1
42
+ item = items.first
43
+
44
+ jid = (item['jid'] || '').strip.empty? ? nil : JID.new(item['jid'].strip)
45
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless jid && jid.bare?
46
+
47
+ if item['subscription'] == 'remove'
48
+ remove_contact(jid)
49
+ return
50
+ end
51
+
52
+ raise StanzaErrors::NotAllowed.new(self, 'modify') if jid == stream.user.jid.bare
53
+ groups = item.xpath('ns:group', 'ns' => NS).map {|g| g.text.strip }
54
+ raise StanzaErrors::BadRequest.new(self, 'modify') if groups.uniq!
55
+ raise StanzaErrors::NotAcceptable.new(self, 'modify') if groups.include?('')
56
+
57
+ contact = stream.user.contact(jid)
58
+ unless contact
59
+ contact = Contact.new(:jid => jid)
60
+ stream.user.roster << contact
61
+ end
62
+ contact.name = item['name']
63
+ contact.groups = groups
64
+ storage.save_user(stream.user)
65
+ stream.update_user_streams(stream.user)
66
+ send_result_iq
67
+ push_roster_updates(stream.user.jid, contact)
68
+ end
69
+
70
+ # Remove the contact with this JID from the user's roster and send
71
+ # roster pushes to the user's interested resources. This is triggered
72
+ # by receiving an iq set with an item element like
73
+ # <item jid="alice@wonderland.lit" subscription="remove"/>. RFC 6121
74
+ # section 2.5.
75
+ def remove_contact(jid)
76
+ contact = stream.user.contact(jid)
77
+ raise StanzaErrors::ItemNotFound.new(self, 'modify') unless contact
78
+ if router.local_jid?(contact.jid)
79
+ user = storage(contact.jid.domain).find_user(contact.jid)
80
+ remove(contact, user)
81
+ else
82
+ remove(contact, nil)
83
+ end
84
+ end
85
+
86
+ def remove(contact, user=nil)
87
+ if user && user.contact(stream.user.jid)
88
+ user.contact(stream.user.jid).subscription = 'none'
89
+ user.contact(stream.user.jid).ask = nil
90
+ end
91
+ stream.user.remove_contact(contact.jid)
92
+ [user, stream.user].compact.each do |save|
93
+ storage(save.jid.domain).save_user(save)
94
+ stream.update_user_streams(save)
95
+ end
96
+ send_result_iq
97
+ push_roster_updates(stream.user.jid, Contact.new(
98
+ :jid => contact.jid,
99
+ :subscription => 'remove'))
100
+
101
+ presence = [%w[to unsubscribe], %w[from unsubscribed]].map do |meth, type|
102
+ if contact.send("subscribed_#{meth}?")
103
+ doc = Document.new
104
+ doc.create_element('presence',
105
+ 'from' => stream.user.jid.bare.to_s,
106
+ 'to' => contact.jid.to_s,
107
+ 'type' => type)
108
+ end
109
+ end.compact
110
+
111
+ if router.local_jid?(contact.jid)
112
+ router.interested_resources(contact.jid).each do |recipient|
113
+ presence.each {|el| recipient.write(el) }
114
+ doc = Document.new
115
+ el = doc.create_element('presence',
116
+ 'from' => stream.user.jid.bare.to_s,
117
+ 'to' => recipient.user.jid.to_s,
118
+ 'type' => 'unavailable')
119
+ recipient.write(el)
120
+ end
121
+ push_roster_updates(contact.jid, Contact.new(
122
+ :jid => stream.user.jid,
123
+ :subscription => 'none'))
124
+ else
125
+ presence.each {|el| router.route(el) }
126
+ end
127
+ end
128
+
129
+ # Send an iq set stanza to the user's interested resources, letting them
130
+ # know their roster has been updated.
131
+ def push_roster_updates(to, contact)
132
+ doc = Document.new
133
+ el = doc.create_element('iq', 'type' => 'set') do |node|
134
+ node << doc.create_element('query', 'xmlns' => NAMESPACES[:roster]) do |query|
135
+ query << contact.to_roster_xml
136
+ end
137
+ end
138
+ router.interested_resources(to).each do |recipient|
139
+ el['id'] = Kit.uuid
140
+ el['to'] = recipient.user.jid.to_s
141
+ recipient.write(el)
142
+ end
143
+ end
144
+
145
+ def send_result_iq
146
+ doc = Document.new
147
+ node = doc.create_element('iq', 'id' => self['id'], 'type' => 'result')
148
+ stream.write(node)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ # Session support is deprecated, but Adium requires it so reply with an
7
+ # iq result stanza.
8
+ class Session < Iq
9
+ register "/iq[@id and @type='set']/ns:session", 'ns' => NAMESPACES[:session]
10
+
11
+ def process
12
+ doc = Document.new
13
+ result = doc.create_element('iq',
14
+ 'from' => stream.domain,
15
+ 'id' => self['id'],
16
+ 'type' => 'result')
17
+ stream.write(result)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Vcard < Iq
7
+ NS = NAMESPACES[:vcard]
8
+
9
+ register "/iq[@id and @type='get' or @type='set']/ns:vCard", 'ns' => NS
10
+
11
+ def process
12
+ if local?
13
+ get? ? vcard_query : vcard_update
14
+ else
15
+ self['from'] = stream.user.jid.to_s
16
+ route
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def vcard_query
23
+ jid = (self['to'] || '').strip
24
+ jid = jid.empty? ? stream.user.jid.bare : JID.new(jid).bare
25
+ card = storage.find_vcard(jid)
26
+
27
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless card
28
+
29
+ doc = Document.new
30
+ result = doc.create_element('iq') do |node|
31
+ node['from'] = jid.to_s unless jid == stream.user.jid.bare
32
+ node['id'] = self['id']
33
+ node['to'] = stream.user.jid.to_s
34
+ node['type'] = 'result'
35
+ node << card
36
+ end
37
+ stream.write(result)
38
+ end
39
+
40
+ def vcard_update
41
+ to = (self['to'] || '').strip
42
+ unless to.empty? || to == stream.user.jid.bare.to_s
43
+ raise StanzaErrors::Forbidden.new(self, 'auth')
44
+ end
45
+
46
+ storage.save_vcard(stream.user.jid, elements.first)
47
+
48
+ doc = Document.new
49
+ result = doc.create_element('iq',
50
+ 'id' => self['id'],
51
+ 'to' => stream.user.jid.to_s,
52
+ 'type' => 'result')
53
+ stream.write(result)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Message < Stanza
6
+ register "/message"
7
+
8
+ TYPE, TO, FROM = %w[type to from].map {|s| s.freeze }
9
+ VALID_TYPES = %w[chat error groupchat headline normal].freeze
10
+
11
+ VALID_TYPES.each do |type|
12
+ define_method "#{type}?" do
13
+ self[TYPE] == type
14
+ end
15
+ end
16
+
17
+ def process
18
+ unless self[TYPE].nil? || VALID_TYPES.include?(self[TYPE])
19
+ raise StanzaErrors::BadRequest.new(self, 'modify')
20
+ end
21
+
22
+ if local?
23
+ to = (self[TO] || '').strip
24
+ to = to.empty? ? stream.user.jid.bare : JID.new(to)
25
+ recipients = router.connected_resources(to)
26
+ if recipients.empty?
27
+ if user = storage(to.domain).find_user(to)
28
+ # TODO Implement offline messaging storage
29
+ raise StanzaErrors::ServiceUnavailable.new(self, 'cancel')
30
+ end
31
+ else
32
+ broadcast(recipients)
33
+ end
34
+ else
35
+ self[FROM] = stream.user.jid.to_s
36
+ route
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,119 @@
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 self['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
+ stream.class != Vines::Stream::Server
27
+ end
28
+
29
+ def inbound?
30
+ stream.class == Vines::Stream::Server
31
+ end
32
+
33
+ def outbound_broadcast_presence
34
+ self['from'] = stream.user.jid.to_s
35
+ to, type = %w[to type].map {|a| (self[a] || '').strip }
36
+ initial = to.empty? && type.empty? && !stream.available?
37
+
38
+ recipients = if to.empty?
39
+ stream.available_subscribers
40
+ else
41
+ stream.user.subscribed_from?(to) ? router.available_resources(to) : []
42
+ end
43
+ broadcast(recipients + router.available_resources(stream.user.jid))
44
+
45
+ if initial
46
+ stream.available_subscribed_to_resources.each do |recipient|
47
+ if recipient.last_broadcast_presence
48
+ el = recipient.last_broadcast_presence.clone
49
+ el['to'] = stream.user.jid.to_s
50
+ el['from'] = recipient.user.jid.to_s
51
+ stream.write(el)
52
+ end
53
+ end
54
+ stream.available!
55
+ end
56
+
57
+ stream.remote_subscribers(to).each do |contact|
58
+ node = @node.clone
59
+ node['to'] = contact.jid.bare.to_s
60
+ router.route(node) rescue nil # ignore RemoteServerNotFound
61
+ send_probe(contact.jid.bare) if initial
62
+ end
63
+ end
64
+
65
+ def inbound_broadcast_presence
66
+ broadcast(router.available_resources(self['to']))
67
+ end
68
+
69
+ private
70
+
71
+ def send_probe(to)
72
+ to = JID.new(to)
73
+ doc = Document.new
74
+ probe = doc.create_element('presence',
75
+ 'from' => stream.user.jid.bare.to_s,
76
+ 'id' => Kit.uuid,
77
+ 'to' => to.bare.to_s,
78
+ 'type' => 'probe')
79
+ router.route(probe)
80
+ end
81
+
82
+ def send_subscribed_roster_push(recipient, jid, state)
83
+ doc = Document.new
84
+ node = doc.create_element('iq',
85
+ 'id' => Kit.uuid,
86
+ 'to' => recipient.user.jid.to_s,
87
+ 'type' => 'set')
88
+ node << doc.create_element('query', 'xmlns' => NAMESPACES[:roster]) do |query|
89
+ query << doc.create_element('item', 'jid' => jid.to_s, 'subscription' => state)
90
+ end
91
+ recipient.write(node)
92
+ end
93
+
94
+ def auto_reply_to_subscription_request(from, type)
95
+ doc = Document.new
96
+ node = doc.create_element('presence') do |el|
97
+ el['from'] = from.to_s
98
+ el['id'] = self['id'] if self['id']
99
+ el['to'] = stream.user.jid.bare.to_s
100
+ el['type'] = type
101
+ end
102
+ stream.write(node)
103
+ end
104
+
105
+ # Validate that the incoming stanza has a 'to' attribute and strip any
106
+ # resource part from it so it's a bare jid. Return the bare JID object
107
+ # that was stamped.
108
+ def stamp_to
109
+ to = (self['to'] || '').strip
110
+ raise StanzaErrors::BadRequest.new(self, 'modify') if to.empty?
111
+ to = JID.new(to).bare
112
+ malformed = [to.node, to.domain].any? {|part| (part || '').strip.empty? }
113
+ raise StanzaErrors::JidMalformed.new(self, 'modify') if malformed
114
+ self['to'] = to.to_s
115
+ to
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Error < Presence
7
+ register "/presence[@type='error']"
8
+
9
+ def process
10
+ inbound? ? process_inbound : process_outbound
11
+ end
12
+
13
+ def process_outbound
14
+ # FIXME Implement error handling
15
+ end
16
+
17
+ def process_inbound
18
+ # FIXME Implement error handling
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Probe < Presence
7
+ register "/presence[@type='probe']"
8
+
9
+ def process
10
+ inbound? ? process_inbound : process_outbound
11
+ end
12
+
13
+ def process_outbound
14
+ self['from'] = stream.user.jid.to_s
15
+ local? ? process_inbound : route
16
+ end
17
+
18
+ def process_inbound
19
+ to = (self['to'] || '').strip
20
+ raise StanzaErrors::BadRequest.new(self, 'modify') if to.empty?
21
+ to = JID.new(to)
22
+
23
+ user = storage(to.domain).find_user(to)
24
+ unless user && user.subscribed_from?(stream.user.jid)
25
+ auto_reply_to_subscription_request(to.bare, 'unsubscribed')
26
+ else
27
+ router.available_resources(to).each do |recipient|
28
+ el = recipient.last_broadcast_presence.clone
29
+ el['from'] = recipient.user.jid.to_s
30
+ el['to'] = stream.user.jid.to_s
31
+ stream.write(el)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end