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.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +7 -0
- data/Rakefile +23 -0
- data/bin/vines +4 -0
- data/conf/certs/README +39 -0
- data/conf/certs/ca-bundle.crt +3895 -0
- data/conf/config.rb +42 -0
- data/lib/vines/cli.rb +132 -0
- data/lib/vines/cluster/connection.rb +26 -0
- data/lib/vines/cluster/publisher.rb +55 -0
- data/lib/vines/cluster/pubsub.rb +92 -0
- data/lib/vines/cluster/sessions.rb +125 -0
- data/lib/vines/cluster/subscriber.rb +108 -0
- data/lib/vines/cluster.rb +246 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +50 -0
- data/lib/vines/command/init.rb +68 -0
- data/lib/vines/command/ldap.rb +38 -0
- data/lib/vines/command/restart.rb +12 -0
- data/lib/vines/command/schema.rb +24 -0
- data/lib/vines/command/start.rb +28 -0
- data/lib/vines/command/stop.rb +18 -0
- data/lib/vines/config/host.rb +125 -0
- data/lib/vines/config/port.rb +132 -0
- data/lib/vines/config/pubsub.rb +108 -0
- data/lib/vines/config.rb +223 -0
- data/lib/vines/contact.rb +111 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/jid.rb +95 -0
- data/lib/vines/kit.rb +23 -0
- data/lib/vines/log.rb +24 -0
- data/lib/vines/router.rb +179 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +45 -0
- data/lib/vines/stanza/iq/disco_items.rb +29 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/private_storage.rb +83 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +140 -0
- data/lib/vines/stanza/iq/session.rb +17 -0
- data/lib/vines/stanza/iq/vcard.rb +56 -0
- data/lib/vines/stanza/iq/version.rb +25 -0
- data/lib/vines/stanza/iq.rb +48 -0
- data/lib/vines/stanza/message.rb +40 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +37 -0
- data/lib/vines/stanza/presence/subscribe.rb +42 -0
- data/lib/vines/stanza/presence/subscribed.rb +51 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
- data/lib/vines/stanza/presence.rb +141 -0
- data/lib/vines/stanza/pubsub/create.rb +39 -0
- data/lib/vines/stanza/pubsub/delete.rb +41 -0
- data/lib/vines/stanza/pubsub/publish.rb +66 -0
- data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
- data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
- data/lib/vines/stanza/pubsub.rb +22 -0
- data/lib/vines/stanza.rb +175 -0
- data/lib/vines/storage/ldap.rb +71 -0
- data/lib/vines/storage/local.rb +139 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/sql.rb +138 -0
- data/lib/vines/storage.rb +239 -0
- data/lib/vines/store.rb +110 -0
- data/lib/vines/stream/client/auth.rb +74 -0
- data/lib/vines/stream/client/auth_restart.rb +29 -0
- data/lib/vines/stream/client/bind.rb +72 -0
- data/lib/vines/stream/client/bind_restart.rb +24 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +17 -0
- data/lib/vines/stream/client/session.rb +210 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +38 -0
- data/lib/vines/stream/client.rb +84 -0
- data/lib/vines/stream/component/handshake.rb +26 -0
- data/lib/vines/stream/component/ready.rb +23 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/component.rb +58 -0
- data/lib/vines/stream/http/auth.rb +22 -0
- data/lib/vines/stream/http/bind.rb +32 -0
- data/lib/vines/stream/http/bind_restart.rb +37 -0
- data/lib/vines/stream/http/ready.rb +29 -0
- data/lib/vines/stream/http/request.rb +172 -0
- data/lib/vines/stream/http/session.rb +120 -0
- data/lib/vines/stream/http/sessions.rb +65 -0
- data/lib/vines/stream/http/start.rb +23 -0
- data/lib/vines/stream/http.rb +157 -0
- data/lib/vines/stream/parser.rb +79 -0
- data/lib/vines/stream/sasl.rb +128 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +13 -0
- data/lib/vines/stream/server/final_restart.rb +21 -0
- data/lib/vines/stream/server/outbound/auth.rb +31 -0
- data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
- data/lib/vines/stream/server/outbound/final_features.rb +28 -0
- data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/start.rb +20 -0
- data/lib/vines/stream/server/outbound/tls.rb +30 -0
- data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
- data/lib/vines/stream/server/ready.rb +24 -0
- data/lib/vines/stream/server/start.rb +13 -0
- data/lib/vines/stream/server/tls.rb +13 -0
- data/lib/vines/stream/server.rb +150 -0
- data/lib/vines/stream/state.rb +60 -0
- data/lib/vines/stream.rb +247 -0
- data/lib/vines/token_bucket.rb +55 -0
- data/lib/vines/user.rb +123 -0
- data/lib/vines/version.rb +6 -0
- data/lib/vines/xmpp_server.rb +25 -0
- data/lib/vines.rb +203 -0
- data/test/cluster/publisher_test.rb +57 -0
- data/test/cluster/sessions_test.rb +47 -0
- data/test/cluster/subscriber_test.rb +109 -0
- data/test/config/host_test.rb +369 -0
- data/test/config/pubsub_test.rb +187 -0
- data/test/config_test.rb +732 -0
- data/test/contact_test.rb +102 -0
- data/test/error_test.rb +58 -0
- data/test/ext/nokogiri.rb +14 -0
- data/test/jid_test.rb +147 -0
- data/test/kit_test.rb +31 -0
- data/test/router_test.rb +243 -0
- data/test/stanza/iq/disco_info_test.rb +78 -0
- data/test/stanza/iq/disco_items_test.rb +49 -0
- data/test/stanza/iq/private_storage_test.rb +184 -0
- data/test/stanza/iq/roster_test.rb +229 -0
- data/test/stanza/iq/session_test.rb +25 -0
- data/test/stanza/iq/vcard_test.rb +146 -0
- data/test/stanza/iq/version_test.rb +64 -0
- data/test/stanza/iq_test.rb +70 -0
- data/test/stanza/message_test.rb +126 -0
- data/test/stanza/presence/probe_test.rb +50 -0
- data/test/stanza/presence/subscribe_test.rb +83 -0
- data/test/stanza/pubsub/create_test.rb +116 -0
- data/test/stanza/pubsub/delete_test.rb +169 -0
- data/test/stanza/pubsub/publish_test.rb +309 -0
- data/test/stanza/pubsub/subscribe_test.rb +205 -0
- data/test/stanza/pubsub/unsubscribe_test.rb +148 -0
- data/test/stanza_test.rb +85 -0
- data/test/storage/ldap_test.rb +201 -0
- data/test/storage/local_test.rb +59 -0
- data/test/storage/mock_redis.rb +97 -0
- data/test/storage/null_test.rb +29 -0
- data/test/storage/storage_tests.rb +182 -0
- data/test/storage_test.rb +85 -0
- data/test/store_test.rb +130 -0
- data/test/stream/client/auth_test.rb +137 -0
- data/test/stream/client/ready_test.rb +47 -0
- data/test/stream/client/session_test.rb +27 -0
- data/test/stream/component/handshake_test.rb +52 -0
- data/test/stream/component/ready_test.rb +103 -0
- data/test/stream/component/start_test.rb +39 -0
- data/test/stream/http/auth_test.rb +70 -0
- data/test/stream/http/ready_test.rb +86 -0
- data/test/stream/http/request_test.rb +209 -0
- data/test/stream/http/sessions_test.rb +49 -0
- data/test/stream/http/start_test.rb +50 -0
- data/test/stream/parser_test.rb +122 -0
- data/test/stream/sasl_test.rb +195 -0
- data/test/stream/server/auth_test.rb +61 -0
- data/test/stream/server/outbound/auth_test.rb +75 -0
- data/test/stream/server/ready_test.rb +98 -0
- data/test/test_helper.rb +42 -0
- data/test/token_bucket_test.rb +44 -0
- data/test/user_test.rb +96 -0
- data/vines.gemspec +30 -0
- 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
|
data/lib/vines/store.rb
ADDED
@@ -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
|