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,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