vines 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
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.1"
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.1"
39
- s.add_dependency "em-hiredis", "~> 0.1.0"
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.11.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.3"
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
 
@@ -142,6 +142,7 @@ end
142
142
  vines/cluster/subscriber
143
143
 
144
144
  vines/stream
145
+ vines/stream/sasl
145
146
  vines/stream/state
146
147
  vines/stream/parser
147
148
 
@@ -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
- # This provides the in-order stanza processing guarantee required by
194
- # RFC 6120 section 10.1.
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 state of the stream's state machine. Provided as a
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
@@ -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 = NAMESPACES[:sasl]
8
- AUTH = 'auth'.freeze
9
- SUCCESS = %Q{<success xmlns="#{NS}"/>}.freeze
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, @outstanding = 0, false
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
- unless node.text.empty?
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 && !@outstanding
38
+ node.name == AUTH && namespace(node) == NS
33
39
  end
34
40
 
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
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
- # 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 => 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
- mechanisms.each {|name| parent << doc.create_element('mechanism', name) }
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
@@ -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
- @domain, @remote_domain = %w[to from].map {|a| node[a] }
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],
@@ -7,12 +7,6 @@ module Vines
7
7
  def initialize(stream, success=Auth)
8
8
  super
9
9
  end
10
-
11
- private
12
-
13
- def mechanisms
14
- ['EXTERNAL']
15
- end
16
10
  end
17
11
  end
18
12
  end
@@ -13,8 +13,8 @@ module Vines
13
13
 
14
14
  def node(node)
15
15
  raise StreamErrors::NotAuthorized unless external?(node)
16
- authz = Base64.encode64(stream.domain).chomp
17
- stream.write(%Q{<auth xmlns="#{NS}" mechanism="EXTERNAL">#{authz}</auth>})
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
- when 'success'
16
- stream.start(node)
17
- stream.reset
18
- advance
19
- when 'failure'
20
- stream.close_connection
21
- else
22
- raise StreamErrors::NotAuthorized
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 = NAMESPACES[:tls]
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
- when 'proceed'
18
- stream.encrypt
19
- stream.start(node)
20
- stream.reset
21
- advance
22
- when 'failure'
23
- stream.close_connection
24
- else
25
- raise StreamErrors::NotAuthorized
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