vines 0.1.0
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.
- 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
|