vines 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README +34 -0
- data/Rakefile +55 -0
- data/bin/vines +95 -0
- data/conf/certs/README +32 -0
- data/conf/certs/ca-bundle.crt +3987 -0
- data/conf/config.rb +114 -0
- data/lib/vines.rb +155 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +49 -0
- data/lib/vines/command/init.rb +58 -0
- data/lib/vines/command/ldap.rb +35 -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.rb +191 -0
- data/lib/vines/contact.rb +99 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/jid.rb +56 -0
- data/lib/vines/kit.rb +23 -0
- data/lib/vines/router.rb +125 -0
- data/lib/vines/stanza.rb +55 -0
- data/lib/vines/stanza/iq.rb +50 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +25 -0
- data/lib/vines/stanza/iq/disco_items.rb +23 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -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 +153 -0
- data/lib/vines/stanza/iq/session.rb +22 -0
- data/lib/vines/stanza/iq/vcard.rb +58 -0
- data/lib/vines/stanza/message.rb +41 -0
- data/lib/vines/stanza/presence.rb +119 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +38 -0
- data/lib/vines/stanza/presence/subscribe.rb +66 -0
- data/lib/vines/stanza/presence/subscribed.rb +64 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +57 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +50 -0
- data/lib/vines/storage.rb +216 -0
- data/lib/vines/storage/couchdb.rb +119 -0
- data/lib/vines/storage/ldap.rb +59 -0
- data/lib/vines/storage/local.rb +66 -0
- data/lib/vines/storage/redis.rb +108 -0
- data/lib/vines/storage/sql.rb +174 -0
- data/lib/vines/store.rb +51 -0
- data/lib/vines/stream.rb +198 -0
- data/lib/vines/stream/client.rb +131 -0
- data/lib/vines/stream/client/auth.rb +94 -0
- data/lib/vines/stream/client/auth_restart.rb +33 -0
- data/lib/vines/stream/client/bind.rb +58 -0
- data/lib/vines/stream/client/bind_restart.rb +25 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +15 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +37 -0
- data/lib/vines/stream/component.rb +53 -0
- data/lib/vines/stream/component/handshake.rb +25 -0
- data/lib/vines/stream/component/ready.rb +24 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/http.rb +111 -0
- data/lib/vines/stream/http/http_request.rb +22 -0
- data/lib/vines/stream/http/http_state.rb +139 -0
- data/lib/vines/stream/http/http_states.rb +53 -0
- data/lib/vines/stream/parser.rb +78 -0
- data/lib/vines/stream/server.rb +126 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +19 -0
- data/lib/vines/stream/server/final_restart.rb +20 -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 +28 -0
- data/lib/vines/stream/server/outbound/final_features.rb +27 -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 +31 -0
- data/lib/vines/stream/server/ready.rb +20 -0
- data/lib/vines/stream/server/start.rb +13 -0
- data/lib/vines/stream/server/tls.rb +13 -0
- data/lib/vines/stream/state.rb +55 -0
- data/lib/vines/token_bucket.rb +46 -0
- data/lib/vines/user.rb +124 -0
- data/lib/vines/version.rb +5 -0
- data/lib/vines/xmpp_server.rb +25 -0
- data/test/config_test.rb +396 -0
- data/test/error_test.rb +59 -0
- data/test/ext/nokogiri.rb +14 -0
- data/test/jid_test.rb +71 -0
- data/test/kit_test.rb +21 -0
- data/test/router_test.rb +60 -0
- data/test/stanza/iq/roster_test.rb +198 -0
- data/test/stanza/iq/session_test.rb +30 -0
- data/test/stanza/iq/vcard_test.rb +159 -0
- data/test/stanza/message_test.rb +124 -0
- data/test/stanza/presence/subscribe_test.rb +75 -0
- data/test/storage/couchdb_test.rb +102 -0
- data/test/storage/ldap_test.rb +207 -0
- data/test/storage/local_test.rb +54 -0
- data/test/storage/redis_test.rb +75 -0
- data/test/storage/sql_test.rb +55 -0
- data/test/storage/storage_tests.rb +134 -0
- data/test/storage_test.rb +90 -0
- data/test/stream/client/auth_test.rb +127 -0
- data/test/stream/client/ready_test.rb +47 -0
- data/test/stream/component/handshake_test.rb +46 -0
- data/test/stream/component/ready_test.rb +105 -0
- data/test/stream/component/start_test.rb +41 -0
- data/test/stream/parser_test.rb +121 -0
- data/test/stream/server/outbound/auth_test.rb +77 -0
- data/test/stream/server/ready_test.rb +100 -0
- data/test/token_bucket_test.rb +24 -0
- data/test/user_test.rb +64 -0
- metadata +318 -0
data/lib/vines/store.rb
ADDED
@@ -0,0 +1,51 @@
|
|
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
|
+
@@certs = nil
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@store = OpenSSL::X509::Store.new
|
13
|
+
certs.each {|c| @store.add_cert(c) }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return true if the certificate is signed by a CA certificate in the
|
17
|
+
# store. If the certificate can be trusted, it's added to the store so
|
18
|
+
# it can be used to trust other certs.
|
19
|
+
def trusted?(pem)
|
20
|
+
if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
|
21
|
+
@store.verify(cert).tap do |trusted|
|
22
|
+
@store.add_cert(cert) if trusted rescue nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return true if the domain name matches one of the names in the
|
28
|
+
# certificate. In other words, is the certificate provided to us really
|
29
|
+
# for the domain to which we think we're connected?
|
30
|
+
def domain?(pem, domain)
|
31
|
+
if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
|
32
|
+
OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return the trusted root CA certificates installed in conf/certs. These
|
37
|
+
# certificates are used to start the trust chain needed to validate certs
|
38
|
+
# we receive from clients and servers.
|
39
|
+
def certs
|
40
|
+
unless @@certs
|
41
|
+
pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
|
42
|
+
dir = File.join(VINES_ROOT, 'conf', 'certs')
|
43
|
+
certs = Dir[File.join(dir, '*.crt')].map {|f| File.read(f) }
|
44
|
+
certs = certs.map {|c| c.scan(pattern) }.flatten
|
45
|
+
certs.map! {|c| OpenSSL::X509::Certificate.new(c) }
|
46
|
+
@@certs = certs.reject {|c| c.not_after < Time.now }
|
47
|
+
end
|
48
|
+
@@certs
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/vines/stream.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
# The base class for various XMPP streams (c2s, s2s, component, http),
|
5
|
+
# containing behavior common to all streams like rate limiting, stanza
|
6
|
+
# parsing, and stream error handling.
|
7
|
+
class Stream < EventMachine::Connection
|
8
|
+
include Vines::Log
|
9
|
+
|
10
|
+
ERROR = 'error'.freeze
|
11
|
+
PAD = 20
|
12
|
+
|
13
|
+
attr_accessor :user
|
14
|
+
|
15
|
+
def post_init
|
16
|
+
router << self
|
17
|
+
@remote_addr, @local_addr = [get_peername, get_sockname].map do |addr|
|
18
|
+
addr ? Socket.unpack_sockaddr_in(addr)[0, 2].reverse.join(':') : 'unknown'
|
19
|
+
end
|
20
|
+
@user, @closed, @stanza_size = nil, false, 0
|
21
|
+
@bucket = TokenBucket.new(100, 10)
|
22
|
+
@store = Store.new
|
23
|
+
|
24
|
+
@nodes = EM::Queue.new
|
25
|
+
process_node_queue
|
26
|
+
|
27
|
+
@parser = Parser.new.tap do |p|
|
28
|
+
p.stream_open {|node| @nodes.push(node) }
|
29
|
+
p.stream_close { close_connection }
|
30
|
+
p.stanza {|node| @nodes.push(node) }
|
31
|
+
end
|
32
|
+
log.info { "%s %21s -> %s" %
|
33
|
+
['Stream connected:'.ljust(PAD), @remote_addr, @local_addr] }
|
34
|
+
end
|
35
|
+
|
36
|
+
def close_connection(after_writing=false)
|
37
|
+
super
|
38
|
+
@closed = true
|
39
|
+
advance(Client::Closed.new(self))
|
40
|
+
end
|
41
|
+
|
42
|
+
def receive_data(data)
|
43
|
+
return if @closed
|
44
|
+
@stanza_size += data.bytesize
|
45
|
+
if @stanza_size < max_stanza_size
|
46
|
+
@parser << data rescue error(StreamErrors::NotWellFormed.new)
|
47
|
+
else
|
48
|
+
error(StreamErrors::PolicyViolation.new('max stanza size reached'))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Send the stanza to all recipients, stamping it with from and
|
53
|
+
# to addresses first.
|
54
|
+
def broadcast(stanza, recipients)
|
55
|
+
stanza['from'] = @user.jid.to_s
|
56
|
+
recipients.each do |recipient|
|
57
|
+
stanza['to'] = recipient.user.jid.to_s
|
58
|
+
recipient.write(stanza)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the storage system for the domain. If no domain is given,
|
63
|
+
# the stream's storage mechanism is returned.
|
64
|
+
def storage(domain=@domain)
|
65
|
+
@config.vhosts[domain]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Reload the user's information into their active connections. Call this
|
69
|
+
# after storage.save_user() to sync the new user state with their other
|
70
|
+
# connections.
|
71
|
+
def update_user_streams(user)
|
72
|
+
router.connected_resources(user.jid.bare).each do |stream|
|
73
|
+
stream.user.update_from(user)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def ssl_verify_peer(pem)
|
78
|
+
# EM is supposed to close the connection when this returns false,
|
79
|
+
# but it only does that for inbound connections, not when we
|
80
|
+
# make a connection to another server.
|
81
|
+
@store.trusted?(pem).tap do |trusted|
|
82
|
+
close_connection unless trusted
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def cert_domain_matches?(domain)
|
87
|
+
@store.domain?(get_peer_cert, domain)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Send the data over the wire to this client.
|
91
|
+
def write(data)
|
92
|
+
log_node(data, :out)
|
93
|
+
if data.respond_to?(:to_xml)
|
94
|
+
data = data.to_xml(:indent => 0)
|
95
|
+
end
|
96
|
+
send_data(data)
|
97
|
+
end
|
98
|
+
|
99
|
+
def encrypt
|
100
|
+
cert, key = tls_files
|
101
|
+
start_tls(:private_key_file => key, :cert_chain_file => cert, :verify_peer => true)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns true if the TLS certificate and private key files for this domain
|
105
|
+
# exist and can be used to encrypt this stream.
|
106
|
+
def encrypt?
|
107
|
+
tls_files.all? {|f| File.exists?(f) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def unbind
|
111
|
+
router.delete(self)
|
112
|
+
log.info { "%s %21s -> %s" %
|
113
|
+
['Stream disconnected:'.ljust(PAD), @remote_addr, @local_addr] }
|
114
|
+
log.info { "Streams connected: #{router.size}" }
|
115
|
+
end
|
116
|
+
|
117
|
+
def advance(state)
|
118
|
+
@state = state
|
119
|
+
end
|
120
|
+
|
121
|
+
# Stream level errors close the stream while stanza and SASL errors are
|
122
|
+
# written to the client and leave the stream open. All exceptions should
|
123
|
+
# pass through this method for consistent handling.
|
124
|
+
def error(e)
|
125
|
+
case e
|
126
|
+
when SaslError, StanzaError
|
127
|
+
write(e.to_xml)
|
128
|
+
when StreamError
|
129
|
+
write(e.to_xml)
|
130
|
+
close_stream
|
131
|
+
else
|
132
|
+
log.error(e)
|
133
|
+
write(StreamErrors::InternalServerError.new.to_xml)
|
134
|
+
close_stream
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def router
|
139
|
+
Router.instance
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def close_stream
|
145
|
+
write('</stream:stream>')
|
146
|
+
close_connection_after_writing
|
147
|
+
end
|
148
|
+
|
149
|
+
def error?(node)
|
150
|
+
ns = node.namespace ? node.namespace.href : nil
|
151
|
+
node.name == ERROR && ns == NAMESPACES[:stream]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Schedule a queue pop on the EM thread to handle the next element.
|
155
|
+
# This provides the in-order stanza processing guarantee required by
|
156
|
+
# RFC 6120 section 10.1.
|
157
|
+
def process_node_queue
|
158
|
+
@nodes.pop do |node|
|
159
|
+
Fiber.new do
|
160
|
+
process_node(node)
|
161
|
+
process_node_queue
|
162
|
+
end.resume unless @closed
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def process_node(node)
|
167
|
+
log_node(node, :in)
|
168
|
+
@stanza_size = 0
|
169
|
+
enforce_rate_limit
|
170
|
+
if error?(node)
|
171
|
+
close_stream
|
172
|
+
else
|
173
|
+
@state.node(node)
|
174
|
+
end
|
175
|
+
rescue Exception => e
|
176
|
+
error(e)
|
177
|
+
end
|
178
|
+
|
179
|
+
def enforce_rate_limit
|
180
|
+
unless @bucket.take(1)
|
181
|
+
raise StreamErrors::PolicyViolation.new('rate limit exceeded')
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def log_node(node, direction)
|
186
|
+
return unless log.debug?
|
187
|
+
from, to = @remote_addr, @local_addr
|
188
|
+
from, to = to, from if direction == :out
|
189
|
+
label = (direction == :out) ? 'Sent' : 'Received'
|
190
|
+
log.debug("%s %21s -> %s\n%s\n" %
|
191
|
+
["#{label} stanza:".ljust(PAD), from, to, node])
|
192
|
+
end
|
193
|
+
|
194
|
+
def tls_files
|
195
|
+
%w[crt key].map {|ext| File.join(VINES_ROOT, 'conf', 'certs', "#{@domain}.#{ext}") }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
|
6
|
+
# Implements the XMPP protocol for client-to-server (c2s) streams. This
|
7
|
+
# serves connected streams using the jabber:client namespace.
|
8
|
+
class Client < Stream
|
9
|
+
attr_reader :config, :domain
|
10
|
+
attr_accessor :last_broadcast_presence
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
@config = config
|
14
|
+
@domain = nil
|
15
|
+
@requested_roster = false
|
16
|
+
@available = false
|
17
|
+
@unbound = false
|
18
|
+
@last_broadcast_presence = nil
|
19
|
+
@state = Start.new(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def ssl_handshake_completed
|
23
|
+
if get_peer_cert
|
24
|
+
close_connection unless cert_domain_matches?(@domain)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def max_stanza_size
|
29
|
+
@config[:client].max_stanza_size
|
30
|
+
end
|
31
|
+
|
32
|
+
def max_resources_per_account
|
33
|
+
@config[:client].max_resources_per_account
|
34
|
+
end
|
35
|
+
|
36
|
+
def unbind
|
37
|
+
@unbound = true
|
38
|
+
@available = false
|
39
|
+
if authenticated?
|
40
|
+
doc = Nokogiri::XML::Document.new
|
41
|
+
el = doc.create_element('presence', 'type' => 'unavailable')
|
42
|
+
Stanza::Presence::Unavailable.new(el, self).outbound_broadcast_presence
|
43
|
+
end
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns true if this client has properly authenticated with
|
48
|
+
# the server.
|
49
|
+
def authenticated?
|
50
|
+
!@user.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
# A connected resource has authenticated and bound a resource
|
54
|
+
# identifier.
|
55
|
+
def connected?
|
56
|
+
!@unbound && authenticated? && !@user.jid.bare?
|
57
|
+
end
|
58
|
+
|
59
|
+
# An available resource has sent initial presence and can
|
60
|
+
# receive presence subscription requests.
|
61
|
+
def available?
|
62
|
+
@available && connected?
|
63
|
+
end
|
64
|
+
|
65
|
+
# An interested resource has requested its roster and can
|
66
|
+
# receive roster pushes.
|
67
|
+
def interested?
|
68
|
+
@requested_roster && connected?
|
69
|
+
end
|
70
|
+
|
71
|
+
def available!
|
72
|
+
@available = true
|
73
|
+
end
|
74
|
+
|
75
|
+
def requested_roster!
|
76
|
+
@requested_roster = true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns streams for available resources to which this user
|
80
|
+
# has successfully subscribed.
|
81
|
+
def available_subscribed_to_resources
|
82
|
+
subscribed = @user.subscribed_to_contacts.map {|c| c.jid }
|
83
|
+
router.available_resources(subscribed)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns streams for available resources that are subscribed
|
87
|
+
# to this user's presence updates.
|
88
|
+
def available_subscribers
|
89
|
+
subscribed = @user.subscribed_from_contacts.map {|c| c.jid }
|
90
|
+
router.available_resources(subscribed)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns contacts hosted at remote servers that are subscribed
|
94
|
+
# to this user's presence updates.
|
95
|
+
def remote_subscribers(to=nil)
|
96
|
+
jid = (to.nil? || to.empty?) ? nil : JID.new(to).bare
|
97
|
+
@user.subscribed_from_contacts.reject do |c|
|
98
|
+
router.local_jid?(c.jid) || (jid && c.jid.bare != jid)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def ready?
|
103
|
+
@state.class == Client::Ready
|
104
|
+
end
|
105
|
+
|
106
|
+
def start(node)
|
107
|
+
@domain, from = %w[to from].map {|a| node[a] }
|
108
|
+
send_stream_header(from)
|
109
|
+
raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0'
|
110
|
+
raise StreamErrors::HostUnknown unless @config.vhost?(@domain)
|
111
|
+
raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:client]
|
112
|
+
raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def send_stream_header(to)
|
118
|
+
attrs = {
|
119
|
+
'xmlns' => NAMESPACES[:client],
|
120
|
+
'xmlns:stream' => NAMESPACES[:stream],
|
121
|
+
'xml:lang' => 'en',
|
122
|
+
'id' => Kit.uuid,
|
123
|
+
'from' => @domain,
|
124
|
+
'version' => '1.0'
|
125
|
+
}
|
126
|
+
attrs['to'] = to if to
|
127
|
+
write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Client
|
6
|
+
class Auth < State
|
7
|
+
NS = NAMESPACES[:sasl]
|
8
|
+
AUTH = 'auth'.freeze
|
9
|
+
SUCCESS = %Q{<success xmlns="#{NS}"/>}.freeze
|
10
|
+
MAX_AUTH_ATTEMPTS = 3
|
11
|
+
AUTH_MECHANISMS = {'PLAIN' => :plain_auth, 'EXTERNAL' => :external_auth}.freeze
|
12
|
+
|
13
|
+
def initialize(stream, success=BindRestart)
|
14
|
+
super
|
15
|
+
@attempts, @outstanding = 0, false
|
16
|
+
end
|
17
|
+
|
18
|
+
def node(node)
|
19
|
+
raise StreamErrors::NotAuthorized unless auth?(node)
|
20
|
+
unless node.text.empty?
|
21
|
+
(name = AUTH_MECHANISMS[node['mechanism']]) ?
|
22
|
+
method(name).call(node) :
|
23
|
+
send_auth_fail(SaslErrors::InvalidMechanism.new)
|
24
|
+
else
|
25
|
+
send_auth_fail(SaslErrors::MalformedRequest.new)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def auth?(node)
|
32
|
+
node.name == AUTH && namespace(node) == NS && !@outstanding
|
33
|
+
end
|
34
|
+
|
35
|
+
# Authenticate s2s streams by comparing their domain to
|
36
|
+
# their SSL certificate.
|
37
|
+
def external_auth(stanza)
|
38
|
+
domain = Base64.decode64(stanza.text)
|
39
|
+
cert = OpenSSL::X509::Certificate.new(stream.get_peer_cert) rescue nil
|
40
|
+
if (!OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false)
|
41
|
+
send_auth_fail(SaslErrors::NotAuthorized.new)
|
42
|
+
stream.write('</stream:stream>')
|
43
|
+
stream.close_connection_after_writing
|
44
|
+
else
|
45
|
+
stream.remote_domain = domain
|
46
|
+
send_auth_success
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Authenticate c2s streams using a username and password. Call the
|
51
|
+
# authentication module in a separate thread to avoid blocking stanza
|
52
|
+
# processing for other users.
|
53
|
+
def plain_auth(stanza)
|
54
|
+
jid, node, password = Base64.decode64(stanza.text).split("\000")
|
55
|
+
jid = [node, stream.domain].join('@') if jid.nil? || jid.empty?
|
56
|
+
log.info("Authenticating user: %s" % jid)
|
57
|
+
@outstanding = true
|
58
|
+
begin
|
59
|
+
user = stream.storage.authenticate(jid, password)
|
60
|
+
finish(user || SaslErrors::NotAuthorized.new)
|
61
|
+
rescue Exception => e
|
62
|
+
log.error("Failed to authenticate: #{e.to_s}")
|
63
|
+
finish(SaslErrors::TemporaryAuthFailure.new)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def finish(result)
|
68
|
+
@outstanding = false
|
69
|
+
if result.kind_of?(Exception)
|
70
|
+
send_auth_fail(result)
|
71
|
+
else
|
72
|
+
stream.user = result
|
73
|
+
log.info("Authentication succeeded: %s" % stream.user.jid)
|
74
|
+
send_auth_success
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def send_auth_success
|
79
|
+
stream.write(SUCCESS)
|
80
|
+
advance
|
81
|
+
end
|
82
|
+
|
83
|
+
def send_auth_fail(condition)
|
84
|
+
@attempts += 1
|
85
|
+
if @attempts >= MAX_AUTH_ATTEMPTS
|
86
|
+
stream.error(StreamErrors::PolicyViolation.new("max authentication attempts exceeded"))
|
87
|
+
else
|
88
|
+
stream.error(condition)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|