vines 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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