lygneo-vines 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (174) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +19 -0
  4. data/README.md +7 -0
  5. data/Rakefile +23 -0
  6. data/bin/vines +4 -0
  7. data/conf/certs/README +39 -0
  8. data/conf/certs/ca-bundle.crt +3895 -0
  9. data/conf/config.rb +42 -0
  10. data/lib/vines/cli.rb +132 -0
  11. data/lib/vines/cluster/connection.rb +26 -0
  12. data/lib/vines/cluster/publisher.rb +55 -0
  13. data/lib/vines/cluster/pubsub.rb +92 -0
  14. data/lib/vines/cluster/sessions.rb +125 -0
  15. data/lib/vines/cluster/subscriber.rb +108 -0
  16. data/lib/vines/cluster.rb +246 -0
  17. data/lib/vines/command/bcrypt.rb +12 -0
  18. data/lib/vines/command/cert.rb +50 -0
  19. data/lib/vines/command/init.rb +68 -0
  20. data/lib/vines/command/ldap.rb +38 -0
  21. data/lib/vines/command/restart.rb +12 -0
  22. data/lib/vines/command/schema.rb +24 -0
  23. data/lib/vines/command/start.rb +28 -0
  24. data/lib/vines/command/stop.rb +18 -0
  25. data/lib/vines/config/host.rb +125 -0
  26. data/lib/vines/config/port.rb +132 -0
  27. data/lib/vines/config/pubsub.rb +108 -0
  28. data/lib/vines/config.rb +223 -0
  29. data/lib/vines/daemon.rb +78 -0
  30. data/lib/vines/error.rb +150 -0
  31. data/lib/vines/follower.rb +111 -0
  32. data/lib/vines/jid.rb +95 -0
  33. data/lib/vines/kit.rb +23 -0
  34. data/lib/vines/log.rb +24 -0
  35. data/lib/vines/router.rb +179 -0
  36. data/lib/vines/stanza/iq/auth.rb +18 -0
  37. data/lib/vines/stanza/iq/disco_info.rb +45 -0
  38. data/lib/vines/stanza/iq/disco_items.rb +29 -0
  39. data/lib/vines/stanza/iq/error.rb +16 -0
  40. data/lib/vines/stanza/iq/ping.rb +16 -0
  41. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  42. data/lib/vines/stanza/iq/query.rb +10 -0
  43. data/lib/vines/stanza/iq/result.rb +16 -0
  44. data/lib/vines/stanza/iq/roster.rb +140 -0
  45. data/lib/vines/stanza/iq/session.rb +17 -0
  46. data/lib/vines/stanza/iq/vcard.rb +56 -0
  47. data/lib/vines/stanza/iq/version.rb +25 -0
  48. data/lib/vines/stanza/iq.rb +48 -0
  49. data/lib/vines/stanza/message.rb +40 -0
  50. data/lib/vines/stanza/presence/error.rb +23 -0
  51. data/lib/vines/stanza/presence/probe.rb +37 -0
  52. data/lib/vines/stanza/presence/subscribe.rb +42 -0
  53. data/lib/vines/stanza/presence/subscribed.rb +51 -0
  54. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  55. data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
  56. data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
  57. data/lib/vines/stanza/presence.rb +141 -0
  58. data/lib/vines/stanza/pubsub/create.rb +39 -0
  59. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  60. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  61. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  62. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  63. data/lib/vines/stanza/pubsub.rb +22 -0
  64. data/lib/vines/stanza.rb +175 -0
  65. data/lib/vines/storage/ldap.rb +71 -0
  66. data/lib/vines/storage/local.rb +139 -0
  67. data/lib/vines/storage/null.rb +39 -0
  68. data/lib/vines/storage/sql.rb +138 -0
  69. data/lib/vines/storage.rb +239 -0
  70. data/lib/vines/store.rb +110 -0
  71. data/lib/vines/stream/client/auth.rb +74 -0
  72. data/lib/vines/stream/client/auth_restart.rb +29 -0
  73. data/lib/vines/stream/client/bind.rb +72 -0
  74. data/lib/vines/stream/client/bind_restart.rb +24 -0
  75. data/lib/vines/stream/client/closed.rb +13 -0
  76. data/lib/vines/stream/client/ready.rb +17 -0
  77. data/lib/vines/stream/client/session.rb +210 -0
  78. data/lib/vines/stream/client/start.rb +27 -0
  79. data/lib/vines/stream/client/tls.rb +38 -0
  80. data/lib/vines/stream/client.rb +84 -0
  81. data/lib/vines/stream/component/handshake.rb +26 -0
  82. data/lib/vines/stream/component/ready.rb +23 -0
  83. data/lib/vines/stream/component/start.rb +19 -0
  84. data/lib/vines/stream/component.rb +58 -0
  85. data/lib/vines/stream/http/auth.rb +22 -0
  86. data/lib/vines/stream/http/bind.rb +32 -0
  87. data/lib/vines/stream/http/bind_restart.rb +37 -0
  88. data/lib/vines/stream/http/ready.rb +29 -0
  89. data/lib/vines/stream/http/request.rb +172 -0
  90. data/lib/vines/stream/http/session.rb +120 -0
  91. data/lib/vines/stream/http/sessions.rb +65 -0
  92. data/lib/vines/stream/http/start.rb +23 -0
  93. data/lib/vines/stream/http.rb +157 -0
  94. data/lib/vines/stream/parser.rb +79 -0
  95. data/lib/vines/stream/sasl.rb +128 -0
  96. data/lib/vines/stream/server/auth.rb +13 -0
  97. data/lib/vines/stream/server/auth_restart.rb +13 -0
  98. data/lib/vines/stream/server/final_restart.rb +21 -0
  99. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  100. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  101. data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
  102. data/lib/vines/stream/server/outbound/final_features.rb +28 -0
  103. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  104. data/lib/vines/stream/server/outbound/start.rb +20 -0
  105. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  106. data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
  107. data/lib/vines/stream/server/ready.rb +24 -0
  108. data/lib/vines/stream/server/start.rb +13 -0
  109. data/lib/vines/stream/server/tls.rb +13 -0
  110. data/lib/vines/stream/server.rb +150 -0
  111. data/lib/vines/stream/state.rb +60 -0
  112. data/lib/vines/stream.rb +247 -0
  113. data/lib/vines/token_bucket.rb +55 -0
  114. data/lib/vines/user.rb +123 -0
  115. data/lib/vines/version.rb +6 -0
  116. data/lib/vines/xmpp_server.rb +25 -0
  117. data/lib/vines.rb +203 -0
  118. data/test/cluster/publisher_test.rb +57 -0
  119. data/test/cluster/sessions_test.rb +47 -0
  120. data/test/cluster/subscriber_test.rb +109 -0
  121. data/test/config/host_test.rb +369 -0
  122. data/test/config/pubsub_test.rb +187 -0
  123. data/test/config_test.rb +732 -0
  124. data/test/error_test.rb +58 -0
  125. data/test/ext/nokogiri.rb +14 -0
  126. data/test/follower_test.rb +102 -0
  127. data/test/jid_test.rb +147 -0
  128. data/test/kit_test.rb +31 -0
  129. data/test/router_test.rb +243 -0
  130. data/test/stanza/iq/disco_info_test.rb +78 -0
  131. data/test/stanza/iq/disco_items_test.rb +49 -0
  132. data/test/stanza/iq/private_storage_test.rb +184 -0
  133. data/test/stanza/iq/roster_test.rb +229 -0
  134. data/test/stanza/iq/session_test.rb +25 -0
  135. data/test/stanza/iq/vcard_test.rb +146 -0
  136. data/test/stanza/iq/version_test.rb +64 -0
  137. data/test/stanza/iq_test.rb +70 -0
  138. data/test/stanza/message_test.rb +126 -0
  139. data/test/stanza/presence/probe_test.rb +50 -0
  140. data/test/stanza/presence/subscribe_test.rb +83 -0
  141. data/test/stanza/pubsub/create_test.rb +116 -0
  142. data/test/stanza/pubsub/delete_test.rb +169 -0
  143. data/test/stanza/pubsub/publish_test.rb +309 -0
  144. data/test/stanza/pubsub/subscribe_test.rb +205 -0
  145. data/test/stanza/pubsub/unsubscribe_test.rb +148 -0
  146. data/test/stanza_test.rb +85 -0
  147. data/test/storage/ldap_test.rb +201 -0
  148. data/test/storage/local_test.rb +59 -0
  149. data/test/storage/mock_redis.rb +97 -0
  150. data/test/storage/null_test.rb +29 -0
  151. data/test/storage/storage_tests.rb +182 -0
  152. data/test/storage_test.rb +85 -0
  153. data/test/store_test.rb +130 -0
  154. data/test/stream/client/auth_test.rb +137 -0
  155. data/test/stream/client/ready_test.rb +47 -0
  156. data/test/stream/client/session_test.rb +27 -0
  157. data/test/stream/component/handshake_test.rb +52 -0
  158. data/test/stream/component/ready_test.rb +103 -0
  159. data/test/stream/component/start_test.rb +39 -0
  160. data/test/stream/http/auth_test.rb +70 -0
  161. data/test/stream/http/ready_test.rb +86 -0
  162. data/test/stream/http/request_test.rb +209 -0
  163. data/test/stream/http/sessions_test.rb +49 -0
  164. data/test/stream/http/start_test.rb +50 -0
  165. data/test/stream/parser_test.rb +122 -0
  166. data/test/stream/sasl_test.rb +195 -0
  167. data/test/stream/server/auth_test.rb +61 -0
  168. data/test/stream/server/outbound/auth_test.rb +75 -0
  169. data/test/stream/server/ready_test.rb +98 -0
  170. data/test/test_helper.rb +42 -0
  171. data/test/token_bucket_test.rb +44 -0
  172. data/test/user_test.rb +96 -0
  173. data/vines.gemspec +30 -0
  174. metadata +387 -0
@@ -0,0 +1,139 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+
6
+ # A storage implementation that persists data to YAML files on the
7
+ # local file system.
8
+ class Local < Storage
9
+ register :fs
10
+
11
+ def initialize(&block)
12
+ @dir = nil
13
+ instance_eval(&block)
14
+ unless @dir && File.directory?(@dir) && File.writable?(@dir)
15
+ raise 'Must provide a writable storage directory'
16
+ end
17
+
18
+ %w[user vcard fragment].each do |sub|
19
+ sub = File.expand_path(sub, @dir)
20
+ Dir.mkdir(sub, 0700) unless File.exists?(sub)
21
+ end
22
+ end
23
+
24
+ def dir(dir=nil)
25
+ dir ? @dir = File.expand_path(dir) : @dir
26
+ end
27
+
28
+ def find_user(jid)
29
+ jid = JID.new(jid).bare.to_s
30
+ file = "user/#{jid}" unless jid.empty?
31
+ record = YAML.load(read(file)) rescue nil
32
+ return User.new(jid: jid).tap do |user|
33
+ user.name, user.password = record.values_at('name', 'password')
34
+ (record['roster'] || {}).each_pair do |jid, props|
35
+ user.roster << Follower.new(
36
+ jid: jid,
37
+ name: props['name'],
38
+ subscription: props['subscription'],
39
+ ask: props['ask'],
40
+ groups: props['groups'] || [])
41
+ end
42
+ end if record
43
+ end
44
+
45
+ def save_user(user)
46
+ record = {'name' => user.name, 'password' => user.password, 'roster' => {}}
47
+ user.roster.each do |follower|
48
+ record['roster'][follower.jid.bare.to_s] = follower.to_h
49
+ end
50
+ save("user/#{user.jid.bare}", YAML.dump(record))
51
+ end
52
+
53
+ def find_vcard(jid)
54
+ jid = JID.new(jid).bare.to_s
55
+ return if jid.empty?
56
+ file = "vcard/#{jid}"
57
+ Nokogiri::XML(read(file)).root rescue nil
58
+ end
59
+
60
+ def save_vcard(jid, card)
61
+ jid = JID.new(jid).bare.to_s
62
+ return if jid.empty?
63
+ save("vcard/#{jid}", card.to_xml)
64
+ end
65
+
66
+ def find_fragment(jid, node)
67
+ jid = JID.new(jid).bare.to_s
68
+ return if jid.empty?
69
+ file = 'fragment/%s' % fragment_id(jid, node)
70
+ Nokogiri::XML(read(file)).root rescue nil
71
+ end
72
+
73
+ def save_fragment(jid, node)
74
+ jid = JID.new(jid).bare.to_s
75
+ return if jid.empty?
76
+ file = 'fragment/%s' % fragment_id(jid, node)
77
+ save(file, node.to_xml)
78
+ end
79
+
80
+ private
81
+
82
+ # Resolves a relative file name into an absolute path inside the
83
+ # storage directory.
84
+ #
85
+ # file - A fully-qualified or relative file name String.
86
+ #
87
+ # Returns the fully-qualified file path String.
88
+ #
89
+ # Raises RuntimeError if the resolved path is outside of the storage
90
+ # directory. This prevents directory path traversals with maliciously
91
+ # crafted JIDs.
92
+ def absolute_path(file)
93
+ File.expand_path(file, @dir).tap do |absolute|
94
+ parent = File.dirname(File.dirname(absolute))
95
+ raise 'path traversal' unless parent == @dir
96
+ end
97
+ end
98
+
99
+ # Read the file from the filesystem and return its contents as a String.
100
+ # All files are assumed to be encoded as UTF-8.
101
+ #
102
+ # file - A fully-qualified or relative file name String.
103
+ #
104
+ # Returns the file content as a UTF-8 encoded String.
105
+ def read(file)
106
+ file = absolute_path(file)
107
+ File.read(file, encoding: 'utf-8')
108
+ end
109
+
110
+ # Write the content to the file. Make sure to consistently encode files
111
+ # we read and write as UTF-8.
112
+ #
113
+ # file - A fully-qualified or relative file name String.
114
+ # content - The String to write.
115
+ #
116
+ # Returns nothing.
117
+ def save(file, content)
118
+ file = absolute_path(file)
119
+ File.open(file, 'w:utf-8') {|f| f.write(content) }
120
+ File.chmod(0600, file)
121
+ end
122
+
123
+ # Generates a unique file id for the user's private XML fragment.
124
+ #
125
+ # Private XML fragment storage needs to uniquely identify fragment files
126
+ # on disk. We combine the user's JID with a SHA-1 hash of the element's
127
+ # name and namespace to avoid special characters in the file name.
128
+ #
129
+ # jid - A bare JID identifying the user who owns this fragment.
130
+ # node - A Nokogiri::XML::Node for the XML to be stored.
131
+ #
132
+ # Returns an id String suitable for use in a file name.
133
+ def fragment_id(jid, node)
134
+ id = Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
135
+ "#{jid}-#{id}"
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+ # A storage implementation that does not persist data to any form of storage.
6
+ # When looking up the storage object for a domain, it's easier to treat a
7
+ # missing domain with a Null storage than checking for nil.
8
+ #
9
+ # For example, presence subscription stanzas sent to a pubsub subdomain
10
+ # have no storage. Rather than checking for nil storage or pubsub addresses,
11
+ # it's easier to treat stanzas to pubsub domains as Null storage that never
12
+ # finds or saves users and their rosters.
13
+ class Null < Storage
14
+ def find_user(jid)
15
+ nil
16
+ end
17
+
18
+ def save_user(user)
19
+ # do nothing
20
+ end
21
+
22
+ def find_vcard(jid)
23
+ nil
24
+ end
25
+
26
+ def save_vcard(jid, card)
27
+ # do nothing
28
+ end
29
+
30
+ def find_fragment(jid, node)
31
+ nil
32
+ end
33
+
34
+ def save_fragment(jid, node)
35
+ # do nothing
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,138 @@
1
+ require 'active_record'
2
+
3
+ module Vines
4
+ class Storage
5
+ class Sql < Storage
6
+ register :sql
7
+
8
+ class Person < ActiveRecord::Base; end
9
+ class Follower < ActiveRecord::Base
10
+ belongs_to :user
11
+ belongs_to :person
12
+ end
13
+
14
+ class User < ActiveRecord::Base
15
+ has_many :followers#, through: :user_id
16
+ has_many :follower_people, :through => :followers, :source => :person
17
+ has_one :person, :foreign_key => :owner_id
18
+ end
19
+
20
+ # Wrap the method with ActiveRecord connection pool logic, so we properly
21
+ # return connections to the pool when we're finished with them. This also
22
+ # defers the original method by pushing it onto the EM thread pool because
23
+ # ActiveRecord uses blocking IO.
24
+ def self.with_connection(method, args={})
25
+ deferrable = args.key?(:defer) ? args[:defer] : true
26
+ old = instance_method(method)
27
+ define_method method do |*args|
28
+ ActiveRecord::Base.connection_pool.with_connection do
29
+ old.bind(self).call(*args)
30
+ end
31
+ end
32
+ defer(method) if deferrable
33
+ end
34
+
35
+ def initialize(&block)
36
+ raise "You configured lygneo-sql adapter without Lygneo" unless defined? AppConfig
37
+ @config = {
38
+ :adapter => AppConfig.adapter.to_s,
39
+ :database => AppConfig.database.to_s,
40
+ :host => AppConfig.host.to_s,
41
+ :port => AppConfig.port.to_i,
42
+ :username => AppConfig.username.to_s,
43
+ :password => AppConfig.password.to_s
44
+ }
45
+
46
+ required = [:adapter, :database]
47
+ required << [:host, :port] unless @config[:adapter] == 'sqlite3'
48
+ required.flatten.each {|key| raise "Must provide #{key}" unless @config[key] }
49
+ [:username, :password].each {|key| @config.delete(key) if empty?(@config[key]) }
50
+ establish_connection
51
+ end
52
+
53
+ def find_user(jid)
54
+ jid = JID.new(jid).bare.to_s
55
+ return if jid.empty?
56
+ xuser = user_by_jid(jid)
57
+ return Vines::User.new(jid: jid).tap do |user|
58
+ user.name, user.password = xuser.username, xuser.authentication_token
59
+
60
+ xuser.followers.each do |follower|
61
+ handle = follower.person.lygneo_handle
62
+ ask = 'none'
63
+ subscription = 'none'
64
+
65
+ if follower.sharing && follower.receiving
66
+ subscription = 'both'
67
+ elsif follower.sharing && !follower.receiving
68
+ ask = 'suscribe'
69
+ subscription = 'from'
70
+ elsif !follower.sharing && follower.receiving
71
+ subscription = 'to'
72
+ else
73
+ ask = 'suscribe'
74
+ end
75
+ # finally build the roster entry
76
+ user.roster << Vines::Follower.new(
77
+ jid: handle,
78
+ name: handle.gsub(/\@.*?$/, ''),
79
+ subscription: subscription,
80
+ ask: ask
81
+ ) if handle
82
+ end
83
+ end if xuser
84
+ end
85
+ with_connection :find_user
86
+
87
+ def authenticate(username, password)
88
+ user = find_user(username)
89
+
90
+ dbhash = BCrypt::Password.new(user.password) rescue nil
91
+ hash = BCrypt::Engine.hash_secret("#{password}#{Config.instance.pepper}", dbhash.salt) rescue nil
92
+
93
+ userAuth = ((hash && dbhash) && hash == dbhash)
94
+ tokenAuth = ((password && user.password) && password == user.password)
95
+ (tokenAuth || userAuth)? user : nil
96
+ end
97
+
98
+ def save_user(user)
99
+ # do nothing
100
+ #log.error("You cannot save a user via XMPP server!")
101
+ end
102
+ with_connection :save_user
103
+
104
+ def find_vcard(jid)
105
+ # do nothing
106
+ nil
107
+ end
108
+ with_connection :find_vcard
109
+
110
+ def save_vcard(jid, card)
111
+ # do nothing
112
+ end
113
+ with_connection :save_vcard
114
+
115
+ def find_fragment(jid, node)
116
+ # do nothing
117
+ nil
118
+ end
119
+ with_connection :find_fragment
120
+
121
+ def save_fragment(jid, node)
122
+ # do nothing
123
+ end
124
+ with_connection :save_fragment
125
+
126
+ private
127
+ def establish_connection
128
+ ActiveRecord::Base.logger = Logger.new('/dev/null')
129
+ ActiveRecord::Base.establish_connection(@config)
130
+ end
131
+
132
+ def user_by_jid(jid)
133
+ name = JID.new(jid).node
134
+ Sql::User.find_by_username(name)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,239 @@
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
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 = instance_method(method)
39
+ define_method method do |*args|
40
+ fiber = Fiber.current
41
+ op = operation { old.bind(self).call(*args) }
42
+ cb = proc {|result| fiber.resume(result) }
43
+ EM.defer(op, cb)
44
+ Fiber.yield
45
+ end
46
+ end
47
+
48
+ # Wrap an authenticate method with a new method that uses LDAP if it's
49
+ # enabled in the config file. If LDAP is not enabled, invoke the original
50
+ # authenticate method as usual. This allows storage classes to implement
51
+ # their native authentication logic and not worry about handling LDAP.
52
+ #
53
+ # For example:
54
+ # def authenticate(username, password)
55
+ # some_user_lookup_by_password(username, password)
56
+ # end
57
+ # wrap_ldap :authenticate
58
+ def self.wrap_ldap(method)
59
+ old = instance_method(method)
60
+ define_method method do |*args|
61
+ ldap? ? authenticate_with_ldap(*args) : old.bind(self).call(*args)
62
+ end
63
+ end
64
+
65
+ # Wrap a method with Fiber yield and resume logic. The method must yield
66
+ # its result to a block. This makes it easier to write asynchronous
67
+ # implementations of +authenticate+, +find_user+, and +save_user+ that
68
+ # block and return a result rather than yielding.
69
+ #
70
+ # For example:
71
+ # def find_user(jid)
72
+ # http = EM::HttpRequest.new(url).get
73
+ # http.callback { yield build_user_from_http_response(http) }
74
+ # end
75
+ # fiber :find_user
76
+ #
77
+ # Because +find_user+ has been wrapped in Fiber logic, we can call it
78
+ # synchronously even though it uses asynchronous EventMachine calls.
79
+ #
80
+ # user = storage.find_user('alice@wonderland.lit')
81
+ # puts user.nil?
82
+ def self.fiber(method)
83
+ old = instance_method(method)
84
+ define_method method do |*args|
85
+ fiber, yielding = Fiber.current, true
86
+ old.bind(self).call(*args) do |user|
87
+ fiber.resume(user) rescue yielding = false
88
+ end
89
+ Fiber.yield if yielding
90
+ end
91
+ end
92
+
93
+ # Return +true+ if users are authenticated against an LDAP directory.
94
+ def ldap?
95
+ !!ldap
96
+ end
97
+
98
+ # Validate the username and password pair and return a +Vines::User+ object
99
+ # on success. Return +nil+ on failure.
100
+ #
101
+ # For example:
102
+ # user = storage.authenticate('alice@wonderland.lit', 'secr3t')
103
+ # puts user.nil?
104
+ #
105
+ # This default implementation validates the password against a bcrypt hash
106
+ # of the password stored in the database. Sub-classes not using bcrypt
107
+ # passwords must override this method.
108
+ def authenticate(username, password)
109
+ user = find_user(username)
110
+ hash = BCrypt::Password.new(user.password) rescue nil
111
+ (hash && hash == password) ? user : nil
112
+ end
113
+ wrap_ldap :authenticate
114
+
115
+ # Return the +Vines::User+ associated with the JID. Return +nil+ if the user
116
+ # could not be found. JID may be +nil+, a +String+, or a +Vines::JID+
117
+ # object. It may be a bare JID or a full JID. Implementations of this method
118
+ # must convert the JID to a bare JID before searching for the user in the
119
+ # database.
120
+ #
121
+ # user = storage.find_user('alice@wonderland.lit')
122
+ # puts user.nil?
123
+ def find_user(jid)
124
+ raise 'subclass must implement'
125
+ end
126
+
127
+ # Persist the +Vines::User+ object to the database and return when the save
128
+ # is complete.
129
+ #
130
+ # alice = Vines::User.new(:jid => 'alice@wonderland.lit')
131
+ # storage.save_user(alice)
132
+ # puts 'saved'
133
+ def save_user(user)
134
+ raise 'subclass must implement'
135
+ end
136
+
137
+ # Return the Nokogiri::XML::Node for the vcard stored for this JID. Return
138
+ # nil if the vcard could not be found. JID may be +nil+, a +String+, or a
139
+ # +Vines::JID+ object. It may be a bare JID or a full JID. Implementations
140
+ # of this method must convert the JID to a bare JID before searching for the
141
+ # vcard in the database.
142
+ #
143
+ # card = storage.find_vcard('alice@wonderland.lit')
144
+ # puts card.nil?
145
+ def find_vcard(jid)
146
+ raise 'subclass must implement'
147
+ end
148
+
149
+ # Save the vcard to the database and return when the save is complete. JID
150
+ # may be a +String+ or a +Vines::JID+ object. It may be a bare JID or a
151
+ # full JID. Implementations of this method must convert the JID to a bare
152
+ # JID before saving the vcard. Card is a +Nokogiri::XML::Node+ object.
153
+ #
154
+ # card = Nokogiri::XML('<vCard>...</vCard>').root
155
+ # storage.save_vcard('alice@wonderland.lit', card)
156
+ # puts 'saved'
157
+ def save_vcard(jid, card)
158
+ raise 'subclass must implement'
159
+ end
160
+
161
+ # Return the Nokogiri::XML::Node for the XML fragment stored for this JID.
162
+ # Return nil if the fragment could not be found. JID may be +nil+, a
163
+ # +String+, or a +Vines::JID+ object. It may be a bare JID or a full JID.
164
+ # Implementations of this method must convert the JID to a bare JID before
165
+ # searching for the fragment in the database.
166
+ #
167
+ # Private XML storage uniquely identifies fragments by JID, root element name,
168
+ # and root element namespace.
169
+ #
170
+ # root = Nokogiri::XML('<custom xmlns="urn:custom:ns"/>').root
171
+ # fragment = storage.find_fragment('alice@wonderland.lit', root)
172
+ # puts fragment.nil?
173
+ def find_fragment(jid, node)
174
+ raise 'subclass must implement'
175
+ end
176
+
177
+ # Save the XML fragment to the database and return when the save is complete.
178
+ # JID may be a +String+ or a +Vines::JID+ object. It may be a bare JID or a
179
+ # full JID. Implementations of this method must convert the JID to a bare
180
+ # JID before saving the fragment. Fragment is a +Nokogiri::XML::Node+ object.
181
+ #
182
+ # fragment = Nokogiri::XML('<custom xmlns="urn:custom:ns">some data</custom>').root
183
+ # storage.save_fragment('alice@wonderland.lit', fragment)
184
+ # puts 'saved'
185
+ def save_fragment(jid, fragment)
186
+ raise 'subclass must implement'
187
+ end
188
+
189
+ private
190
+
191
+ # Return true if any of the arguments are nil or empty strings.
192
+ # For example:
193
+ # username, password = 'alice@wonderland.lit', ''
194
+ # empty?(username, password) #=> true
195
+ def empty?(*args)
196
+ args.flatten.any? {|arg| (arg || '').strip.empty? }
197
+ end
198
+
199
+ # Return a +proc+ suitable for running on the +EM.defer+ thread pool that traps
200
+ # and logs any errors thrown by the provided block.
201
+ def operation
202
+ proc do
203
+ begin
204
+ yield
205
+ rescue => e
206
+ log.error("Thread pool operation failed: #{e.message}")
207
+ nil
208
+ end
209
+ end
210
+ end
211
+
212
+ # Return a +Vines::User+ object if we are able to bind to the LDAP server
213
+ # using the username and password. Return +nil+ if authentication failed. If
214
+ # authentication succeeds, but the user is not yet stored in our database,
215
+ # save the user to the database.
216
+ def authenticate_with_ldap(username, password, &block)
217
+ op = operation { ldap.authenticate(username, password) }
218
+ cb = proc {|user| save_ldap_user(user, &block) }
219
+ EM.defer(op, cb)
220
+ end
221
+ fiber :authenticate_with_ldap
222
+
223
+ # Save missing users to the storage database after they're authenticated with
224
+ # LDAP. This allows admins to define users once in LDAP and have them sync
225
+ # to the chat database the first time they successfully sign in.
226
+ def save_ldap_user(user, &block)
227
+ Fiber.new do
228
+ if user.nil?
229
+ block.call
230
+ elsif found = find_user(user.jid)
231
+ block.call(found)
232
+ else
233
+ save_user(user)
234
+ block.call(user)
235
+ end
236
+ end.resume
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,110 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+
5
+ # An X509 certificate store that validates certificate trust chains.
6
+ # This uses the conf/certs/*.crt files as the list of trusted root
7
+ # CA certificates.
8
+ class Store
9
+ include Vines::Log
10
+
11
+ @@sources = nil
12
+
13
+ # Create a certificate store to read certificate files from the given
14
+ # directory.
15
+ def initialize(dir)
16
+ @dir = File.expand_path(dir)
17
+ @store = OpenSSL::X509::Store.new
18
+ certs.each {|c|
19
+ begin
20
+ @store.add_cert(c)
21
+ rescue
22
+ # do nothing cert is already known
23
+ log.warn("WARNING! There are duplicate certificates")
24
+ end
25
+ }
26
+ end
27
+
28
+ # Return true if the certificate is signed by a CA certificate in the
29
+ # store. If the certificate can be trusted, it's added to the store so
30
+ # it can be used to trust other certs.
31
+ def trusted?(pem)
32
+ if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
33
+ @store.verify(cert).tap do |trusted|
34
+ @store.add_cert(cert) if trusted rescue nil
35
+ end
36
+ end
37
+ end
38
+
39
+ # Return true if the domain name matches one of the names in the
40
+ # certificate. In other words, is the certificate provided to us really
41
+ # for the domain to which we think we're connected?
42
+ def domain?(pem, domain)
43
+ if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
44
+ OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false
45
+ end
46
+ end
47
+
48
+ # Return the trusted root CA certificates installed in conf/certs. These
49
+ # certificates are used to start the trust chain needed to validate certs
50
+ # we receive from clients and servers.
51
+ def certs
52
+ unless @@sources
53
+ pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
54
+ files = Dir[File.join(@dir, '*.crt')]
55
+ files << AppConfig.environment.certificate_authorities if defined?(AppConfig)
56
+ pairs = files.map do |name|
57
+ File.open(name, "r:UTF-8") do |f|
58
+ pems = f.read.scan(pattern)
59
+ certs = pems.map {|pem| OpenSSL::X509::Certificate.new(pem) }
60
+ certs.reject! {|cert| cert.not_after < Time.now }
61
+ [name, certs]
62
+ end
63
+ end
64
+ @@sources = Hash[pairs]
65
+ end
66
+ @@sources.values.flatten
67
+ end
68
+
69
+ # Returns a pair of file names containing the public key certificate
70
+ # and matching private key for the given domain. This supports using
71
+ # wildcard certificate files to serve several subdomains.
72
+ #
73
+ # Finding the certificate and private key file for a domain follows these steps:
74
+ # - look for <domain>.crt and <domain>.key files in the conf/certs directory.
75
+ # if found, return those file names, else
76
+ # - inspect all conf/certs/*.crt files for certificates that contain the
77
+ # domain name either as the subject common name (CN) or as a DNS
78
+ # subjectAltName. The corresponding private key must be in a file of the
79
+ # same name as the certificate's, but with a .key extension.
80
+ #
81
+ # So in the simplest configuration, the tea.wonderland.lit encryption files would
82
+ # be named conf/certs/tea.wonderland.lit.crt and conf/certs/tea.wonderland.lit.key.
83
+ #
84
+ # However, in the case of a wildcard certificate for *.wonderland.lit, the
85
+ # files would be conf/certs/wonderland.lit.crt and conf/certs/wonderland.lit.key.
86
+ # These same two files would be returned for the subdomains of tea.wonderland.lit,
87
+ # crumpets.wonderland.lit, etc.
88
+ def files_for_domain(domain)
89
+ crt = File.expand_path("#{domain}.crt", @dir)
90
+ key = File.expand_path("#{domain}.key", @dir)
91
+ # lygneo keys will be prioritized
92
+ if defined?(AppConfig)
93
+ crt = AppConfig.server.chat.certificate
94
+ key = AppConfig.server.chat.key
95
+ end
96
+ return [crt, key] if File.exists?(crt) && File.exists?(key)
97
+
98
+ # might be a wildcard cert file
99
+ @@sources.each do |file, certs|
100
+ certs.each do |cert|
101
+ if OpenSSL::SSL.verify_certificate_identity(cert, domain)
102
+ key = file.chomp(File.extname(file)) + '.key'
103
+ return [file, key] if File.exists?(file) && File.exists?(key)
104
+ end
105
+ end
106
+ end
107
+ nil
108
+ end
109
+ end
110
+ end