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,66 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Subscribe < Presence
7
+ register "/presence[@type='subscribe']"
8
+
9
+ def process
10
+ inbound? ? process_inbound : process_outbound
11
+ end
12
+
13
+ def process_outbound
14
+ self['from'] = stream.user.jid.bare.to_s
15
+ to = stamp_to
16
+ route unless local?
17
+
18
+ stream.user.request_subscription(to)
19
+ storage.save_user(stream.user)
20
+ stream.update_user_streams(stream.user)
21
+
22
+ process_inbound if local?
23
+
24
+ router.interested_resources(stream.user.jid).each do |recipient|
25
+ send_subscribe_roster_push(recipient, stream.user.contact(to))
26
+ end
27
+ end
28
+
29
+ def process_inbound
30
+ self['from'] = stream.user.jid.bare.to_s
31
+ to = stamp_to
32
+
33
+ contact = storage(to.domain).find_user(to)
34
+ if contact.nil?
35
+ auto_reply_to_subscription_request(to, 'unsubscribed')
36
+ elsif contact.subscribed_from?(stream.user.jid)
37
+ auto_reply_to_subscription_request(to, 'subscribed')
38
+ else
39
+ recipients = router.available_resources(to)
40
+ if recipients.empty?
41
+ # TODO store subscription request per RFC 6121 3.1.3 #4
42
+ else
43
+ recipients.each {|stream| stream.write(@node) }
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def send_subscribe_roster_push(recipient, contact)
51
+ doc = Document.new
52
+ node = doc.create_element('iq') do |el|
53
+ el['id'] = Kit.uuid
54
+ el['to'] = recipient.user.jid.to_s
55
+ el['type'] = 'set'
56
+ el << doc.create_element('query') do |query|
57
+ query.default_namespace = NAMESPACES[:roster]
58
+ query << contact.to_roster_xml
59
+ end
60
+ end
61
+ recipient.write(node)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Subscribed < Presence
7
+ register "/presence[@type='subscribed']"
8
+
9
+ def process
10
+ inbound? ? process_inbound : process_outbound
11
+ end
12
+
13
+ def process_outbound
14
+ self['from'] = stream.user.jid.bare.to_s
15
+ to = stamp_to
16
+ route unless local?
17
+
18
+ stream.user.add_subscription_from(to)
19
+ storage.save_user(stream.user)
20
+ stream.update_user_streams(stream.user)
21
+
22
+ router.interested_resources(stream.user.jid).each do |recipient|
23
+ send_subscribed_roster_push(recipient, to, stream.user.contact(to).subscription)
24
+ end
25
+
26
+ presences = router.available_resources(stream.user.jid).map do |c|
27
+ doc = Document.new
28
+ doc.create_element('presence',
29
+ 'from' => c.user.jid.to_s,
30
+ 'id' => Kit.uuid,
31
+ 'to' => to.to_s)
32
+ end
33
+
34
+ if local?
35
+ router.available_resources(to).each do |recipient|
36
+ presences.each {|el| recipient.write(el) }
37
+ end
38
+ else
39
+ presences.each {|el| router.route(el) }
40
+ end
41
+
42
+ process_inbound if local?
43
+ end
44
+
45
+ def process_inbound
46
+ self['from'] = stream.user.jid.bare.to_s
47
+ to = stamp_to
48
+
49
+ user = storage(to.domain).find_user(to)
50
+ contact = user.contact(stream.user.jid) if user
51
+ return unless contact && contact.can_subscribe?
52
+ contact.subscribe_to
53
+ storage(to.domain).save_user(user)
54
+ stream.update_user_streams(user)
55
+
56
+ router.interested_resources(to).each do |recipient|
57
+ recipient.write(@node)
58
+ send_subscribed_roster_push(recipient, stream.user.jid.bare, contact.subscription)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Unavailable < Presence
7
+ register "/presence[@type='unavailable']"
8
+
9
+ def process
10
+ inbound? ? inbound_broadcast_presence : outbound_broadcast_presence
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Unsubscribe < Presence
7
+ register "/presence[@type='unsubscribe']"
8
+
9
+ def process
10
+ inbound? ? process_inbound : process_outbound
11
+ end
12
+
13
+ def process_outbound
14
+ self['from'] = stream.user.jid.bare.to_s
15
+ to = stamp_to
16
+ route unless local?
17
+
18
+ stream.user.remove_subscription_to(to)
19
+ storage.save_user(stream.user)
20
+ stream.update_user_streams(stream.user)
21
+
22
+ process_inbound if local?
23
+
24
+ if contact = stream.user.contact(to)
25
+ router.interested_resources(stream.user.jid).each do |recipient|
26
+ send_subscribed_roster_push(recipient, to, contact.subscription)
27
+ end
28
+ end
29
+ end
30
+
31
+ def process_inbound
32
+ self['from'] = stream.user.jid.bare.to_s
33
+ to = stamp_to
34
+
35
+ user = storage(to.domain).find_user(to)
36
+ return unless user && user.subscribed_from?(stream.user.jid)
37
+ contact = user.contact(stream.user.jid)
38
+ contact.unsubscribe_from
39
+ storage(to.domain).save_user(user)
40
+ stream.update_user_streams(user)
41
+
42
+ router.interested_resources(to).each do |recipient|
43
+ recipient.write(@node)
44
+ send_subscribed_roster_push(recipient, stream.user.jid.bare, contact.subscription)
45
+ doc = Document.new
46
+ el = doc.create_element('presence',
47
+ 'from' => stream.user.jid.bare.to_s,
48
+ 'id' => Kit.uuid,
49
+ 'to' => recipient.user.jid.to_s,
50
+ 'type' => 'unavailable')
51
+ recipient.write(el)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Unsubscribed < Presence
7
+ register "/presence[@type='unsubscribed']"
8
+
9
+ def process
10
+ inbound? ? process_inbound : process_outbound
11
+ end
12
+
13
+ def process_outbound
14
+ self['from'] = stream.user.jid.bare.to_s
15
+ to = stamp_to
16
+ route unless local?
17
+
18
+ stream.user.remove_subscription_from(to)
19
+ storage.save_user(stream.user)
20
+ stream.update_user_streams(stream.user)
21
+
22
+ if contact = stream.user.contact(to)
23
+ router.interested_resources(stream.user.jid).each do |recipient|
24
+ send_subscribed_roster_push(recipient, to, contact.subscription)
25
+ end
26
+ end
27
+
28
+ process_inbound if local?
29
+ end
30
+
31
+ def process_inbound
32
+ self['from'] = stream.user.jid.bare.to_s
33
+ to = stamp_to
34
+
35
+ user = storage(to.domain).find_user(to)
36
+ return unless user && user.subscribed_to?(stream.user.jid)
37
+ contact = user.contact(stream.user.jid)
38
+ contact.unsubscribe_to
39
+ storage(to.domain).save_user(user)
40
+ stream.update_user_streams(user)
41
+
42
+ router.interested_resources(to).each do |recipient|
43
+ recipient.write(@node)
44
+ send_subscribed_roster_push(recipient, stream.user.jid.bare, contact.subscription)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,216 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+ include Vines::Log
6
+
7
+ attr_accessor :ldap
8
+
9
+ @@nicks = {}
10
+
11
+ # Register a nickname that can be used in the config file to specify this
12
+ # storage implementation.
13
+ def self.register(name)
14
+ @@nicks[name.to_sym] = self
15
+ end
16
+
17
+ def self.from_name(name, &block)
18
+ klass = @@nicks[name.to_sym]
19
+ raise "#{name} storage class not found" unless klass
20
+ klass.new(&block)
21
+ end
22
+
23
+ # Wrap a blocking IO method in a new method that pushes the original method
24
+ # onto EventMachine's thread pool using EM#defer. Storage classes implemented
25
+ # with blocking IO don't need to worry about threading or blocking the the
26
+ # EventMachine reactor thread if they wrap their methods with this one.
27
+ #
28
+ # For example:
29
+ # def find_user(jid)
30
+ # some_blocking_lookup(jid)
31
+ # end
32
+ # defer :find_user
33
+ #
34
+ # Storage classes that use asynchronous IO (through an EventMachine
35
+ # enabled library like em-http-request or em-redis) don't need any special
36
+ # consideration and must not use this method.
37
+ def self.defer(method)
38
+ old = "_deferred_#{method}"
39
+ alias_method old, method
40
+ define_method method do |*args|
41
+ fiber = Fiber.current
42
+ op = proc do
43
+ begin
44
+ method(old).call(*args)
45
+ rescue Exception => e
46
+ log.error("Thread pool operation failed: #{e.message}")
47
+ nil
48
+ end
49
+ end
50
+ cb = proc {|result| fiber.resume(result) }
51
+ EM.defer(op, cb)
52
+ Fiber.yield
53
+ end
54
+ end
55
+
56
+ # Wrap an authenticate method with a new method that uses LDAP if it's
57
+ # enabled in the config file. If LDAP is not enabled, invoke the original
58
+ # authenticate method as usual. This allows storage classes to implement
59
+ # their native authentication logic and not worry about handling LDAP.
60
+ #
61
+ # For example:
62
+ # def authenticate(username, password)
63
+ # some_user_lookup_by_password(username, password)
64
+ # end
65
+ # wrap_ldap :authenticate
66
+ def self.wrap_ldap(method)
67
+ old = "_ldap_#{method}"
68
+ alias_method old, method
69
+ define_method method do |*args|
70
+ ldap? ? authenticate_with_ldap(*args) : method(old).call(*args)
71
+ end
72
+ end
73
+
74
+ # Wrap a method with Fiber yield and resume logic. The method must yield
75
+ # its result to a block. This makes it easier to write asynchronous
76
+ # implementations of +authenticate+, +find_user+, and +save_user+ that
77
+ # block and return a result rather than yielding.
78
+ #
79
+ # For example:
80
+ # def find_user(jid)
81
+ # http = EM::HttpRequest.new(url).get
82
+ # http.callback { yield build_user_from_http_response(http) }
83
+ # end
84
+ # fiber :find_user
85
+ #
86
+ # Because +find_user+ has been wrapped in Fiber logic, we can call it
87
+ # synchronously even though it uses asynchronous EventMachine calls.
88
+ #
89
+ # user = storage.find_user('alice@wonderland.lit')
90
+ # puts user.nil?
91
+ def self.fiber(method)
92
+ old = "_fiber_#{method}"
93
+ alias_method old, method
94
+ define_method method do |*args|
95
+ fiber, yielding = Fiber.current, true
96
+ method(old).call(*args) do |user|
97
+ fiber.resume(user) rescue yielding = false
98
+ end
99
+ Fiber.yield if yielding
100
+ end
101
+ end
102
+
103
+ # Return true if users are authenticated against an LDAP directory.
104
+ def ldap?
105
+ !!ldap
106
+ end
107
+
108
+ # Validate the username and password pair and return a Vines::User object
109
+ # on success. Return nil on failure.
110
+ #
111
+ # For example:
112
+ # user = storage.authenticate('alice@wonderland.lit', 'secr3t')
113
+ # puts user.nil?
114
+ #
115
+ # This default implementation validates the password against a bcrypt hash
116
+ # of the password stored in the database. Sub-classes not using bcrypt
117
+ # passwords must override this method.
118
+ def authenticate(username, password)
119
+ user = find_user(username)
120
+ hash = BCrypt::Password.new(user.password) rescue nil
121
+ (hash && hash == password) ? user : nil
122
+ end
123
+ wrap_ldap :authenticate
124
+
125
+ # Return the Vines::User associated with the JID. Return nil if the user
126
+ # could not be found. JID may be +nil+, a +String+, or a +Vines::JID+
127
+ # object. It may be a bare JID or a full JID. Implementations of this method
128
+ # must convert the JID to a bare JID before searching for the user in the
129
+ # database.
130
+ #
131
+ # user = storage.find_user('alice@wonderland.lit')
132
+ # puts user.nil?
133
+ def find_user(jid)
134
+ raise 'subclass must implement'
135
+ end
136
+
137
+ # Persist the Vines::User object to the database and return when the save
138
+ # is complete.
139
+ #
140
+ # alice = Vines::User.new(:jid => 'alice@wonderland.lit')
141
+ # storage.save_user(alice)
142
+ # puts 'saved'
143
+ def save_user(user)
144
+ raise 'subclass must implement'
145
+ end
146
+
147
+ # Return the Nokogiri::XML::Node for the vcard stored for this JID. Return
148
+ # nil if the vcard could not be found. JID may be +nil+, a +String+, or a
149
+ # +Vines::JID+ object. It may be a bare JID or a full JID. Implementations
150
+ # of this method must convert the JID to a bare JID before searching for the
151
+ # vcard in the database.
152
+ #
153
+ # card = storage.find_vcard('alice@wonderland.lit')
154
+ # puts card.nil?
155
+ def find_vcard(jid)
156
+ raise 'subclass must implement'
157
+ end
158
+
159
+ # Save the vcard to the database and return when the save is complete. JID
160
+ # may be a +String+ or a +Vines::JID+ object. It may be a bare JID or a
161
+ # full JID. Implementations of this method must convert the JID to a bare
162
+ # JID before saving the vcard. Card is a +Nokogiri::XML::Node+ object.
163
+ #
164
+ # card = Nokogiri::XML('<vCard>...</vCard>').root
165
+ # storage.save_vcard('alice@wonderland.lit', card)
166
+ # puts 'saved'
167
+ def save_vcard(jid, card)
168
+ raise 'subclass must implement'
169
+ end
170
+
171
+ private
172
+
173
+ # Return true if any of the arguments are nil or empty strings.
174
+ # For example:
175
+ # username, password = 'alice@wonderland.lit', ''
176
+ # empty?(username, password) #=> true
177
+ def empty?(*args)
178
+ args.flatten.any? {|arg| (arg || '').strip.empty? }
179
+ end
180
+
181
+ # Return a Vines::User object if we are able to bind to the LDAP server
182
+ # using the username and password. Return nil if authentication failed. If
183
+ # authentication succeeds, but the user is not yet stored in our database,
184
+ # save the user to the database.
185
+ def authenticate_with_ldap(username, password, &block)
186
+ if empty?(username, password)
187
+ block.call; return
188
+ end
189
+
190
+ op = proc do
191
+ begin
192
+ ldap.authenticate(username, password)
193
+ rescue Exception => e
194
+ log.error("LDAP authentication failed: #{e.message}")
195
+ nil
196
+ end
197
+ end
198
+ cb = proc {|user| save_ldap_user(user, &block) }
199
+ EM.defer(op, cb)
200
+ end
201
+ fiber :authenticate_with_ldap
202
+
203
+ def save_ldap_user(user, &block)
204
+ Fiber.new do
205
+ if user.nil?
206
+ block.call
207
+ elsif found = find_user(user.jid)
208
+ block.call(found)
209
+ else
210
+ save_user(user)
211
+ block.call(user)
212
+ end
213
+ end.resume
214
+ end
215
+ end
216
+ end