diaspora-vines 0.1.2

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 (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/contact.rb +111 -0
  30. data/lib/vines/daemon.rb +78 -0
  31. data/lib/vines/error.rb +150 -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/contact_test.rb +102 -0
  125. data/test/error_test.rb +58 -0
  126. data/test/ext/nokogiri.rb +14 -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 << Contact.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 |contact|
48
+ record['roster'][contact.jid.bare.to_s] = contact.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 Contact < ActiveRecord::Base
10
+ belongs_to :user
11
+ belongs_to :person
12
+ end
13
+
14
+ class User < ActiveRecord::Base
15
+ has_many :contacts#, through: :user_id
16
+ has_many :contact_people, :through => :contacts, :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 diaspora-sql adapter without Diaspora" 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.contacts.each do |contact|
61
+ handle = contact.person.diaspora_handle
62
+ ask = 'none'
63
+ subscription = 'none'
64
+
65
+ if contact.sharing && contact.receiving
66
+ subscription = 'both'
67
+ elsif contact.sharing && !contact.receiving
68
+ ask = 'suscribe'
69
+ subscription = 'from'
70
+ elsif !contact.sharing && contact.receiving
71
+ subscription = 'to'
72
+ else
73
+ ask = 'suscribe'
74
+ end
75
+ # finally build the roster entry
76
+ user.roster << Vines::Contact.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
+ # diaspora 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