vines 0.4.2 → 0.4.3
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/Rakefile +5 -5
- data/lib/vines.rb +1 -0
- data/lib/vines/stream.rb +8 -5
- data/lib/vines/stream/client.rb +17 -2
- data/lib/vines/stream/client/auth.rb +29 -50
- data/lib/vines/stream/client/auth_restart.rb +3 -7
- data/lib/vines/stream/sasl.rb +74 -0
- data/lib/vines/stream/server.rb +18 -2
- data/lib/vines/stream/server/auth_restart.rb +0 -6
- data/lib/vines/stream/server/outbound/auth.rb +2 -2
- data/lib/vines/stream/server/outbound/auth_result.rb +11 -8
- data/lib/vines/stream/server/outbound/tls_result.rb +12 -10
- data/lib/vines/version.rb +1 -1
- data/test/rake_test_loader.rb +1 -1
- data/test/stream/client/auth_test.rb +87 -75
- data/test/stream/sasl_test.rb +151 -0
- data/test/stream/server/auth_test.rb +57 -0
- data/web/chat/javascripts/app.js +1 -1
- data/web/lib/javascripts/base.js +7 -7
- metadata +44 -39
data/Rakefile
CHANGED
@@ -33,10 +33,10 @@ is mandatory on all client and server connections."
|
|
33
33
|
s.executables = %w[vines]
|
34
34
|
s.require_path = "lib"
|
35
35
|
|
36
|
-
s.add_dependency "activerecord", "~> 3.2.
|
36
|
+
s.add_dependency "activerecord", "~> 3.2.3"
|
37
37
|
s.add_dependency "bcrypt-ruby", "~> 3.0.1"
|
38
|
-
s.add_dependency "em-http-request", "~> 1.0.
|
39
|
-
s.add_dependency "em-hiredis", "~> 0.1.
|
38
|
+
s.add_dependency "em-http-request", "~> 1.0.2"
|
39
|
+
s.add_dependency "em-hiredis", "~> 0.1.1"
|
40
40
|
s.add_dependency "eventmachine", ">= 0.12.10"
|
41
41
|
s.add_dependency "http_parser.rb", "~> 0.5.3"
|
42
42
|
s.add_dependency "mongo", "~> 1.5.2"
|
@@ -44,10 +44,10 @@ is mandatory on all client and server connections."
|
|
44
44
|
s.add_dependency "net-ldap", "~> 0.2.2"
|
45
45
|
s.add_dependency "nokogiri", "~> 1.4.7"
|
46
46
|
|
47
|
-
s.add_development_dependency "minitest", "~> 2.
|
47
|
+
s.add_development_dependency "minitest", "~> 2.12.1"
|
48
48
|
s.add_development_dependency "coffee-script", "~> 2.2.0"
|
49
49
|
s.add_development_dependency "coffee-script-source", "~> 1.2.0"
|
50
|
-
s.add_development_dependency "uglifier", "~> 1.2.
|
50
|
+
s.add_development_dependency "uglifier", "~> 1.2.4"
|
51
51
|
s.add_development_dependency "rake"
|
52
52
|
s.add_development_dependency "sqlite3"
|
53
53
|
|
data/lib/vines.rb
CHANGED
data/lib/vines/stream.rb
CHANGED
@@ -41,6 +41,9 @@ module Vines
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
# Advance the state machine into the +Closed+ state so any remaining queued
|
45
|
+
# nodes are not processed while we're waiting for EM to actually close the
|
46
|
+
# connection.
|
44
47
|
def close_connection(after_writing=false)
|
45
48
|
super
|
46
49
|
@closed = true
|
@@ -189,9 +192,9 @@ module Vines
|
|
189
192
|
node.name == ERROR && ns == NAMESPACES[:stream]
|
190
193
|
end
|
191
194
|
|
192
|
-
# Schedule a queue pop on the EM thread to handle the next element.
|
193
|
-
#
|
194
|
-
#
|
195
|
+
# Schedule a queue pop on the EM thread to handle the next element. This
|
196
|
+
# guarantees all stanzas received on this stream are processed in order.
|
197
|
+
# http://tools.ietf.org/html/rfc6120#section-10.1
|
195
198
|
def process_node_queue
|
196
199
|
@nodes.pop do |node|
|
197
200
|
Fiber.new do
|
@@ -229,13 +232,13 @@ module Vines
|
|
229
232
|
["#{label} stanza:".ljust(PAD), from, to, node])
|
230
233
|
end
|
231
234
|
|
232
|
-
# Returns the current
|
235
|
+
# Returns the current +State+ of the stream's state machine. Provided as a
|
233
236
|
# method so subclasses can override the behavior.
|
234
237
|
def state
|
235
238
|
@state
|
236
239
|
end
|
237
240
|
|
238
|
-
# Return true if this is a valid domain-only JID that can be used in
|
241
|
+
# Return +true+ if this is a valid domain-only JID that can be used in
|
239
242
|
# stream initiation stanza headers.
|
240
243
|
def valid_address?(jid)
|
241
244
|
JID.new(jid).domain? rescue false
|
data/lib/vines/stream/client.rb
CHANGED
@@ -2,10 +2,11 @@
|
|
2
2
|
|
3
3
|
module Vines
|
4
4
|
class Stream
|
5
|
-
|
6
5
|
# Implements the XMPP protocol for client-to-server (c2s) streams. This
|
7
6
|
# serves connected streams using the jabber:client namespace.
|
8
7
|
class Client < Stream
|
8
|
+
MECHANISMS = %w[PLAIN].freeze
|
9
|
+
|
9
10
|
def initialize(config)
|
10
11
|
super
|
11
12
|
@session = Client::Session.new(self)
|
@@ -28,6 +29,12 @@ module Vines
|
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
32
|
+
# Return an array of allowed authentication mechanisms advertised as
|
33
|
+
# client stream features.
|
34
|
+
def authentication_mechanisms
|
35
|
+
MECHANISMS
|
36
|
+
end
|
37
|
+
|
31
38
|
def ssl_handshake_completed
|
32
39
|
if get_peer_cert
|
33
40
|
close_connection unless cert_domain_matches?(@session.domain)
|
@@ -41,8 +48,9 @@ module Vines
|
|
41
48
|
|
42
49
|
def start(node)
|
43
50
|
to, from = %w[to from].map {|a| node[a] }
|
44
|
-
@session.domain = to
|
51
|
+
@session.domain = to unless @session.domain
|
45
52
|
send_stream_header(from)
|
53
|
+
raise StreamErrors::NotAuthorized if domain_change?(to)
|
46
54
|
raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0'
|
47
55
|
raise StreamErrors::ImproperAddressing unless valid_address?(@session.domain)
|
48
56
|
raise StreamErrors::HostUnknown unless config.vhost?(@session.domain)
|
@@ -52,6 +60,13 @@ module Vines
|
|
52
60
|
|
53
61
|
private
|
54
62
|
|
63
|
+
# The +to+ domain address set on the initial stream header must not change
|
64
|
+
# during stream restarts. This prevents a user from authenticating in one
|
65
|
+
# domain, then using a stream in a different domain.
|
66
|
+
def domain_change?(to)
|
67
|
+
to != @session.domain
|
68
|
+
end
|
69
|
+
|
55
70
|
def send_stream_header(to)
|
56
71
|
attrs = {
|
57
72
|
'xmlns' => NAMESPACES[:client],
|
@@ -4,75 +4,54 @@ module Vines
|
|
4
4
|
class Stream
|
5
5
|
class Client
|
6
6
|
class Auth < State
|
7
|
-
NS
|
8
|
-
|
9
|
-
|
7
|
+
NS = NAMESPACES[:sasl]
|
8
|
+
MECHANISM = 'mechanism'.freeze
|
9
|
+
AUTH = 'auth'.freeze
|
10
|
+
PLAIN = 'PLAIN'.freeze
|
11
|
+
EXTERNAL = 'EXTERNAL'.freeze
|
12
|
+
SUCCESS = %Q{<success xmlns="#{NS}"/>}.freeze
|
10
13
|
MAX_AUTH_ATTEMPTS = 3
|
11
|
-
AUTH_MECHANISMS = {'PLAIN' => :plain_auth, 'EXTERNAL' => :external_auth}.freeze
|
12
14
|
|
13
15
|
def initialize(stream, success=BindRestart)
|
14
16
|
super
|
15
|
-
@attempts
|
17
|
+
@attempts = 0
|
18
|
+
@sasl = SASL.new(stream)
|
16
19
|
end
|
17
20
|
|
18
21
|
def node(node)
|
19
22
|
raise StreamErrors::NotAuthorized unless auth?(node)
|
20
|
-
|
21
|
-
(name = AUTH_MECHANISMS[node['mechanism']]) ?
|
22
|
-
method(name).call(node) :
|
23
|
-
send_auth_fail(SaslErrors::InvalidMechanism.new)
|
24
|
-
else
|
23
|
+
if node.text.empty?
|
25
24
|
send_auth_fail(SaslErrors::MalformedRequest.new)
|
25
|
+
elsif stream.authentication_mechanisms.include?(node[MECHANISM])
|
26
|
+
case node[MECHANISM]
|
27
|
+
when PLAIN then plain_auth(node)
|
28
|
+
when EXTERNAL then external_auth(node)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
send_auth_fail(SaslErrors::InvalidMechanism.new)
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
29
35
|
private
|
30
36
|
|
31
37
|
def auth?(node)
|
32
|
-
node.name == AUTH && namespace(node) == NS
|
38
|
+
node.name == AUTH && namespace(node) == NS
|
33
39
|
end
|
34
40
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
41
|
+
def plain_auth(node)
|
42
|
+
stream.user = @sasl.plain_auth(node.text)
|
43
|
+
send_auth_success
|
44
|
+
rescue => e
|
45
|
+
send_auth_fail(e)
|
48
46
|
end
|
49
47
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
@outstanding = true
|
58
|
-
begin
|
59
|
-
user = stream.storage.authenticate(jid, password)
|
60
|
-
finish(user || SaslErrors::NotAuthorized.new)
|
61
|
-
rescue => 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
|
48
|
+
def external_auth(node)
|
49
|
+
@sasl.external_auth(node.text)
|
50
|
+
send_auth_success
|
51
|
+
rescue => e
|
52
|
+
send_auth_fail(e)
|
53
|
+
stream.write('</stream:stream>')
|
54
|
+
stream.close_connection_after_writing
|
76
55
|
end
|
77
56
|
|
78
57
|
def send_auth_success
|
@@ -15,18 +15,14 @@ module Vines
|
|
15
15
|
features = doc.create_element('stream:features') do |el|
|
16
16
|
el << doc.create_element('mechanisms') do |parent|
|
17
17
|
parent.default_namespace = NAMESPACES[:sasl]
|
18
|
-
|
18
|
+
stream.authentication_mechanisms.each do |name|
|
19
|
+
parent << doc.create_element('mechanism', name)
|
20
|
+
end
|
19
21
|
end
|
20
22
|
end
|
21
23
|
stream.write(features)
|
22
24
|
advance
|
23
25
|
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def mechanisms
|
28
|
-
['EXTERNAL', 'PLAIN']
|
29
|
-
end
|
30
26
|
end
|
31
27
|
end
|
32
28
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
# Provides plain (username/password) and external (TLS certificate) SASL
|
6
|
+
# authentication to client and server streams.
|
7
|
+
class SASL
|
8
|
+
include Vines::Log
|
9
|
+
EMPTY = '='.freeze
|
10
|
+
|
11
|
+
def initialize(stream)
|
12
|
+
@stream = stream
|
13
|
+
end
|
14
|
+
|
15
|
+
# Authenticate s2s streams, comparing their domain to their SSL certificate.
|
16
|
+
# Return +true+ if the base64 encoded domain matches the TLS certificate
|
17
|
+
# presented earlier in stream negotiation. Raise a +SaslError+ if
|
18
|
+
# authentication failed.
|
19
|
+
# http://xmpp.org/extensions/xep-0178.html#s2s
|
20
|
+
def external_auth(encoded)
|
21
|
+
unless encoded == EMPTY
|
22
|
+
authzid = decode64(encoded)
|
23
|
+
matches_from = (authzid == @stream.remote_domain)
|
24
|
+
raise SaslErrors::InvalidAuthzid unless matches_from
|
25
|
+
end
|
26
|
+
matches_from = @stream.cert_domain_matches?(@stream.remote_domain)
|
27
|
+
matches_from or raise SaslErrors::NotAuthorized
|
28
|
+
end
|
29
|
+
|
30
|
+
# Authenticate c2s streams using a username and password. Return the
|
31
|
+
# authenticated +User+ or raise a +SaslError+ if authentication failed.
|
32
|
+
def plain_auth(encoded)
|
33
|
+
jid, password = decode_credentials(encoded)
|
34
|
+
user = authenticate(jid, password)
|
35
|
+
user or raise SaslErrors::NotAuthorized
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Storage backends should not raise errors, but if an unexpected error
|
41
|
+
# occurs during authentication, convert it to a temporary-auth-failure.
|
42
|
+
# Return the authenticated +User+ or +nil+ if authentication failed.
|
43
|
+
def authenticate(jid, password)
|
44
|
+
log.info("Authenticating user: %s" % jid)
|
45
|
+
@stream.storage.authenticate(jid, password).tap do |user|
|
46
|
+
log.info("Authentication succeeded: %s" % user.jid) if user
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
log.error("Failed to authenticate: #{e.to_s}")
|
50
|
+
raise SaslErrors::TemporaryAuthFailure
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the +JID+ and password decoded from the base64 encoded SASL PLAIN
|
54
|
+
# credentials formatted as authzid\0authcid\0password.
|
55
|
+
# http://tools.ietf.org/html/rfc6120#section-6.3.8
|
56
|
+
# http://tools.ietf.org/html/rfc4616
|
57
|
+
def decode_credentials(encoded)
|
58
|
+
authzid, node, password = decode64(encoded).split("\x00")
|
59
|
+
raise SaslErrors::NotAuthorized if node.nil? || node.empty? || password.nil? || password.empty?
|
60
|
+
jid = JID.new(node, @stream.domain) rescue (raise SaslErrors::NotAuthorized)
|
61
|
+
raise SaslErrors::InvalidAuthzid unless authzid.nil? || authzid.empty? || authzid.downcase == jid.to_s
|
62
|
+
[jid, password]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Decode the base64 encoded string, raising an error for invalid data.
|
66
|
+
# http://tools.ietf.org/html/rfc6120#section-13.9.1
|
67
|
+
def decode64(encoded)
|
68
|
+
Base64.strict_decode64(encoded)
|
69
|
+
rescue
|
70
|
+
raise SaslErrors::IncorrectEncoding
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/vines/stream/server.rb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
module Vines
|
4
4
|
class Stream
|
5
|
-
|
6
5
|
# Implements the XMPP protocol for server-to-server (s2s) streams. This
|
7
6
|
# serves connected streams using the jabber:server namespace. This handles
|
8
7
|
# both accepting incoming s2s streams and initiating outbound s2s streams
|
9
8
|
# to other servers.
|
10
9
|
class Server < Stream
|
10
|
+
MECHANISMS = %w[EXTERNAL].freeze
|
11
11
|
|
12
12
|
# Starts the connection to the remote server. When the stream is
|
13
13
|
# connected and ready to send stanzas it will yield to the callback
|
@@ -76,6 +76,12 @@ module Vines
|
|
76
76
|
close_connection unless cert_domain_matches?(@remote_domain)
|
77
77
|
end
|
78
78
|
|
79
|
+
# Return an array of allowed authentication mechanisms advertised as
|
80
|
+
# server stream features.
|
81
|
+
def authentication_mechanisms
|
82
|
+
MECHANISMS
|
83
|
+
end
|
84
|
+
|
79
85
|
def stream_type
|
80
86
|
:server
|
81
87
|
end
|
@@ -105,8 +111,10 @@ module Vines
|
|
105
111
|
|
106
112
|
def start(node)
|
107
113
|
if @outbound then send_stream_header; return end
|
108
|
-
|
114
|
+
to, from = %w[to from].map {|a| node[a] }
|
115
|
+
@domain, @remote_domain = to, from unless @domain
|
109
116
|
send_stream_header
|
117
|
+
raise StreamErrors::NotAuthorized if domain_change?(to, from)
|
110
118
|
raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0'
|
111
119
|
raise StreamErrors::ImproperAddressing unless valid_address?(@domain) && valid_address?(@remote_domain)
|
112
120
|
raise StreamErrors::HostUnknown unless config.vhost?(@domain) || config.pubsub?(@domain) || config.component?(@domain)
|
@@ -117,6 +125,14 @@ module Vines
|
|
117
125
|
|
118
126
|
private
|
119
127
|
|
128
|
+
# The +to+ and +from+ domain addresses set on the initial stream header
|
129
|
+
# must not change during stream restarts. This prevents a server from
|
130
|
+
# authenticating as one domain, then sending stanzas from users in a
|
131
|
+
# different domain.
|
132
|
+
def domain_change?(to, from)
|
133
|
+
to != @domain || from != @remote_domain
|
134
|
+
end
|
135
|
+
|
120
136
|
def send_stream_header
|
121
137
|
attrs = {
|
122
138
|
'xmlns' => NAMESPACES[:server],
|
@@ -13,8 +13,8 @@ module Vines
|
|
13
13
|
|
14
14
|
def node(node)
|
15
15
|
raise StreamErrors::NotAuthorized unless external?(node)
|
16
|
-
|
17
|
-
stream.write(%Q{<auth xmlns="#{NS}" mechanism="EXTERNAL">#{
|
16
|
+
authzid = Base64.strict_encode64(stream.domain)
|
17
|
+
stream.write(%Q{<auth xmlns="#{NS}" mechanism="EXTERNAL">#{authzid}</auth>})
|
18
18
|
advance
|
19
19
|
end
|
20
20
|
|
@@ -5,6 +5,9 @@ module Vines
|
|
5
5
|
class Server
|
6
6
|
class Outbound
|
7
7
|
class AuthResult < State
|
8
|
+
SUCCESS = 'success'.freeze
|
9
|
+
FAILURE = 'failure'.freeze
|
10
|
+
|
8
11
|
def initialize(stream, success=FinalRestart)
|
9
12
|
super
|
10
13
|
end
|
@@ -12,14 +15,14 @@ module Vines
|
|
12
15
|
def node(node)
|
13
16
|
raise StreamErrors::NotAuthorized unless namespace(node) == NAMESPACES[:sasl]
|
14
17
|
case node.name
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
when SUCCESS
|
19
|
+
stream.start(node)
|
20
|
+
stream.reset
|
21
|
+
advance
|
22
|
+
when FAILURE
|
23
|
+
stream.close_connection
|
24
|
+
else
|
25
|
+
raise StreamErrors::NotAuthorized
|
23
26
|
end
|
24
27
|
end
|
25
28
|
end
|
@@ -5,7 +5,9 @@ module Vines
|
|
5
5
|
class Server
|
6
6
|
class Outbound
|
7
7
|
class TLSResult < State
|
8
|
-
NS
|
8
|
+
NS = NAMESPACES[:tls]
|
9
|
+
PROCEED = 'proceed'.freeze
|
10
|
+
FAILURE = 'failure'.freeze
|
9
11
|
|
10
12
|
def initialize(stream, success=AuthRestart)
|
11
13
|
super
|
@@ -14,15 +16,15 @@ module Vines
|
|
14
16
|
def node(node)
|
15
17
|
raise StreamErrors::NotAuthorized unless namespace(node) == NS
|
16
18
|
case node.name
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
19
|
+
when PROCEED
|
20
|
+
stream.encrypt
|
21
|
+
stream.start(node)
|
22
|
+
stream.reset
|
23
|
+
advance
|
24
|
+
when FAILURE
|
25
|
+
stream.close_connection
|
26
|
+
else
|
27
|
+
raise StreamErrors::NotAuthorized
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|