lygneo-vines 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/follower.rb +111 -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/error_test.rb +58 -0
- data/test/ext/nokogiri.rb +14 -0
- data/test/follower_test.rb +102 -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 << 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
|
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
|
+
# 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
|