vinesmod 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. data/Gemfile +3 -0
  2. data/LICENSE +19 -0
  3. data/README.md +43 -0
  4. data/Rakefile +57 -0
  5. data/bin/vines +93 -0
  6. data/conf/certs/README +39 -0
  7. data/conf/certs/ca-bundle.crt +3366 -0
  8. data/conf/config.rb +149 -0
  9. data/lib/vines.rb +197 -0
  10. data/lib/vines/cluster.rb +246 -0
  11. data/lib/vines/cluster/connection.rb +26 -0
  12. data/lib/vines/cluster/publisher.rb +55 -0
  13. data/lib/vines/cluster/pubsub.rb +92 -0
  14. data/lib/vines/cluster/sessions.rb +125 -0
  15. data/lib/vines/cluster/subscriber.rb +108 -0
  16. data/lib/vines/command/bcrypt.rb +12 -0
  17. data/lib/vines/command/cert.rb +50 -0
  18. data/lib/vines/command/init.rb +68 -0
  19. data/lib/vines/command/register.rb +27 -0
  20. data/lib/vines/command/restart.rb +12 -0
  21. data/lib/vines/command/schema.rb +24 -0
  22. data/lib/vines/command/start.rb +28 -0
  23. data/lib/vines/command/stop.rb +18 -0
  24. data/lib/vines/command/unregister.rb +27 -0
  25. data/lib/vines/config.rb +213 -0
  26. data/lib/vines/config/host.rb +119 -0
  27. data/lib/vines/config/port.rb +132 -0
  28. data/lib/vines/config/pubsub.rb +108 -0
  29. data/lib/vines/contact.rb +111 -0
  30. data/lib/vines/daemon.rb +78 -0
  31. data/lib/vines/error.rb +150 -0
  32. data/lib/vines/jid.rb +95 -0
  33. data/lib/vines/kit.rb +35 -0
  34. data/lib/vines/log.rb +24 -0
  35. data/lib/vines/router.rb +179 -0
  36. data/lib/vines/stanza.rb +175 -0
  37. data/lib/vines/stanza/iq.rb +48 -0
  38. data/lib/vines/stanza/iq/auth.rb +18 -0
  39. data/lib/vines/stanza/iq/disco_info.rb +45 -0
  40. data/lib/vines/stanza/iq/disco_items.rb +29 -0
  41. data/lib/vines/stanza/iq/error.rb +16 -0
  42. data/lib/vines/stanza/iq/ping.rb +16 -0
  43. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  44. data/lib/vines/stanza/iq/query.rb +10 -0
  45. data/lib/vines/stanza/iq/register.rb +42 -0
  46. data/lib/vines/stanza/iq/result.rb +16 -0
  47. data/lib/vines/stanza/iq/roster.rb +140 -0
  48. data/lib/vines/stanza/iq/session.rb +17 -0
  49. data/lib/vines/stanza/iq/vcard.rb +56 -0
  50. data/lib/vines/stanza/iq/version.rb +25 -0
  51. data/lib/vines/stanza/message.rb +43 -0
  52. data/lib/vines/stanza/presence.rb +156 -0
  53. data/lib/vines/stanza/presence/error.rb +23 -0
  54. data/lib/vines/stanza/presence/probe.rb +37 -0
  55. data/lib/vines/stanza/presence/subscribe.rb +42 -0
  56. data/lib/vines/stanza/presence/subscribed.rb +51 -0
  57. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  58. data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
  59. data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
  60. data/lib/vines/stanza/pubsub.rb +22 -0
  61. data/lib/vines/stanza/pubsub/create.rb +39 -0
  62. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  63. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  64. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  65. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  66. data/lib/vines/storage.rb +188 -0
  67. data/lib/vines/storage/local.rb +165 -0
  68. data/lib/vines/storage/null.rb +39 -0
  69. data/lib/vines/storage/sql.rb +260 -0
  70. data/lib/vines/store.rb +94 -0
  71. data/lib/vines/stream.rb +247 -0
  72. data/lib/vines/stream/client.rb +84 -0
  73. data/lib/vines/stream/client/auth.rb +74 -0
  74. data/lib/vines/stream/client/auth_restart.rb +29 -0
  75. data/lib/vines/stream/client/bind.rb +72 -0
  76. data/lib/vines/stream/client/bind_restart.rb +24 -0
  77. data/lib/vines/stream/client/closed.rb +13 -0
  78. data/lib/vines/stream/client/ready.rb +17 -0
  79. data/lib/vines/stream/client/session.rb +210 -0
  80. data/lib/vines/stream/client/start.rb +27 -0
  81. data/lib/vines/stream/client/tls.rb +38 -0
  82. data/lib/vines/stream/component.rb +58 -0
  83. data/lib/vines/stream/component/handshake.rb +26 -0
  84. data/lib/vines/stream/component/ready.rb +23 -0
  85. data/lib/vines/stream/component/start.rb +19 -0
  86. data/lib/vines/stream/http.rb +157 -0
  87. data/lib/vines/stream/http/auth.rb +22 -0
  88. data/lib/vines/stream/http/bind.rb +32 -0
  89. data/lib/vines/stream/http/bind_restart.rb +37 -0
  90. data/lib/vines/stream/http/ready.rb +29 -0
  91. data/lib/vines/stream/http/request.rb +172 -0
  92. data/lib/vines/stream/http/session.rb +120 -0
  93. data/lib/vines/stream/http/sessions.rb +65 -0
  94. data/lib/vines/stream/http/start.rb +23 -0
  95. data/lib/vines/stream/parser.rb +78 -0
  96. data/lib/vines/stream/sasl.rb +92 -0
  97. data/lib/vines/stream/server.rb +150 -0
  98. data/lib/vines/stream/server/auth.rb +13 -0
  99. data/lib/vines/stream/server/auth_restart.rb +13 -0
  100. data/lib/vines/stream/server/final_restart.rb +21 -0
  101. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  102. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  103. data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
  104. data/lib/vines/stream/server/outbound/final_features.rb +28 -0
  105. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  106. data/lib/vines/stream/server/outbound/start.rb +20 -0
  107. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  108. data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
  109. data/lib/vines/stream/server/ready.rb +24 -0
  110. data/lib/vines/stream/server/start.rb +13 -0
  111. data/lib/vines/stream/server/tls.rb +13 -0
  112. data/lib/vines/stream/state.rb +60 -0
  113. data/lib/vines/token_bucket.rb +55 -0
  114. data/lib/vines/user.rb +123 -0
  115. data/lib/vines/version.rb +5 -0
  116. data/lib/vines/xmpp_server.rb +43 -0
  117. data/vines.gemspec +36 -0
  118. data/web/404.html +51 -0
  119. data/web/apple-touch-icon.png +0 -0
  120. data/web/chat/coffeescripts/chat.coffee +362 -0
  121. data/web/chat/coffeescripts/init.coffee +15 -0
  122. data/web/chat/index.html +16 -0
  123. data/web/chat/javascripts/app.js +1 -0
  124. data/web/chat/stylesheets/chat.css +144 -0
  125. data/web/favicon.png +0 -0
  126. data/web/lib/coffeescripts/button.coffee +25 -0
  127. data/web/lib/coffeescripts/contact.coffee +32 -0
  128. data/web/lib/coffeescripts/filter.coffee +49 -0
  129. data/web/lib/coffeescripts/layout.coffee +30 -0
  130. data/web/lib/coffeescripts/login.coffee +68 -0
  131. data/web/lib/coffeescripts/logout.coffee +5 -0
  132. data/web/lib/coffeescripts/navbar.coffee +84 -0
  133. data/web/lib/coffeescripts/notification.coffee +14 -0
  134. data/web/lib/coffeescripts/router.coffee +40 -0
  135. data/web/lib/coffeescripts/session.coffee +229 -0
  136. data/web/lib/coffeescripts/transfer.coffee +106 -0
  137. data/web/lib/images/dark-gray.png +0 -0
  138. data/web/lib/images/default-user.png +0 -0
  139. data/web/lib/images/light-gray.png +0 -0
  140. data/web/lib/images/logo-large.png +0 -0
  141. data/web/lib/images/logo-small.png +0 -0
  142. data/web/lib/images/white.png +0 -0
  143. data/web/lib/javascripts/base.js +12 -0
  144. data/web/lib/javascripts/icons.js +110 -0
  145. data/web/lib/javascripts/jquery.cookie.js +91 -0
  146. data/web/lib/javascripts/jquery.js +4 -0
  147. data/web/lib/javascripts/raphael.js +6 -0
  148. data/web/lib/javascripts/strophe.js +1 -0
  149. data/web/lib/stylesheets/base.css +385 -0
  150. data/web/lib/stylesheets/login.css +68 -0
  151. metadata +423 -0
@@ -0,0 +1,94 @@
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
+ @@sources = nil
10
+
11
+ # Create a certificate store to read certificate files from the given
12
+ # directory.
13
+ def initialize(dir)
14
+ @dir = File.expand_path(dir)
15
+ @store = OpenSSL::X509::Store.new
16
+ certs.each {|c| @store.add_cert(c) }
17
+ end
18
+
19
+ # Return true if the certificate is signed by a CA certificate in the
20
+ # store. If the certificate can be trusted, it's added to the store so
21
+ # it can be used to trust other certs.
22
+ def trusted?(pem)
23
+ if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
24
+ @store.verify(cert).tap do |trusted|
25
+ @store.add_cert(cert) if trusted rescue nil
26
+ end
27
+ end
28
+ end
29
+
30
+ # Return true if the domain name matches one of the names in the
31
+ # certificate. In other words, is the certificate provided to us really
32
+ # for the domain to which we think we're connected?
33
+ def domain?(pem, domain)
34
+ if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
35
+ OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false
36
+ end
37
+ end
38
+
39
+ # Return the trusted root CA certificates installed in conf/certs. These
40
+ # certificates are used to start the trust chain needed to validate certs
41
+ # we receive from clients and servers.
42
+ def certs
43
+ unless @@sources
44
+ pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
45
+ pairs = Dir[File.join(@dir, '*.crt')].map do |name|
46
+ File.open(name, "r:UTF-8") do |f|
47
+ pems = f.read.scan(pattern)
48
+ certs = pems.map {|pem| OpenSSL::X509::Certificate.new(pem) }
49
+ certs.reject! {|cert| cert.not_after < Time.now }
50
+ [name, certs]
51
+ end
52
+ end
53
+ @@sources = Hash[pairs]
54
+ end
55
+ @@sources.values.flatten
56
+ end
57
+
58
+ # Returns a pair of file names containing the public key certificate
59
+ # and matching private key for the given domain. This supports using
60
+ # wildcard certificate files to serve several subdomains.
61
+ #
62
+ # Finding the certificate and private key file for a domain follows these steps:
63
+ # - look for <domain>.crt and <domain>.key files in the conf/certs directory.
64
+ # if found, return those file names, else
65
+ # - inspect all conf/certs/*.crt files for certificates that contain the
66
+ # domain name either as the subject common name (CN) or as a DNS
67
+ # subjectAltName. The corresponding private key must be in a file of the
68
+ # same name as the certificate's, but with a .key extension.
69
+ #
70
+ # So in the simplest configuration, the tea.wonderland.lit encryption files would
71
+ # be named conf/certs/tea.wonderland.lit.crt and conf/certs/tea.wonderland.lit.key.
72
+ #
73
+ # However, in the case of a wildcard certificate for *.wonderland.lit, the
74
+ # files would be conf/certs/wonderland.lit.crt and conf/certs/wonderland.lit.key.
75
+ # These same two files would be returned for the subdomains of tea.wonderland.lit,
76
+ # crumpets.wonderland.lit, etc.
77
+ def files_for_domain(domain)
78
+ crt = File.expand_path("#{domain}.crt", @dir)
79
+ key = File.expand_path("#{domain}.key", @dir)
80
+ return [crt, key] if File.exists?(crt) && File.exists?(key)
81
+
82
+ # might be a wildcard cert file
83
+ @@sources.each do |file, certs|
84
+ certs.each do |cert|
85
+ if OpenSSL::SSL.verify_certificate_identity(cert, domain)
86
+ key = file.chomp(File.extname(file)) + '.key'
87
+ return [file, key] if File.exists?(file) && File.exists?(key)
88
+ end
89
+ end
90
+ end
91
+ nil
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,247 @@
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_reader :config, :domain
14
+ attr_accessor :user
15
+
16
+ def initialize(config)
17
+ @config = config
18
+ end
19
+
20
+ def post_init
21
+ @remote_addr, @local_addr = addresses
22
+ @user, @closed, @stanza_size = nil, false, 0
23
+ @bucket = TokenBucket.new(100, 10)
24
+ @store = Store.new(@config.certs)
25
+ @nodes = EM::Queue.new
26
+ process_node_queue
27
+ create_parser
28
+ log.info { "%s %21s -> %s" %
29
+ ['Stream connected:'.ljust(PAD), @remote_addr, @local_addr] }
30
+ end
31
+
32
+ # Initialize a new XML parser for this connection. This is called when the
33
+ # stream is first connected as well as for stream restarts during
34
+ # negotiation. Subclasses can override this method to provide a different
35
+ # type of parser (e.g. HTTP).
36
+ def create_parser
37
+ @parser = Parser.new.tap do |p|
38
+ p.stream_open {|node| @nodes.push(node) }
39
+ p.stream_close { close_connection }
40
+ p.stanza {|node| @nodes.push(node) }
41
+ end
42
+ end
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.
47
+ def close_connection(after_writing=false)
48
+ super
49
+ @closed = true
50
+ advance(Client::Closed.new(self))
51
+ end
52
+
53
+ def receive_data(data)
54
+ return if @closed
55
+ @stanza_size += data.bytesize
56
+ if @stanza_size < max_stanza_size
57
+ @parser << data rescue error(StreamErrors::NotWellFormed.new)
58
+ else
59
+ error(StreamErrors::PolicyViolation.new('max stanza size reached'))
60
+ end
61
+ end
62
+
63
+ # Reset the connection's XML parser when a new <stream:stream> header
64
+ # is received.
65
+ def reset
66
+ create_parser
67
+ end
68
+
69
+ # Returns the storage system for the domain. If no domain is given,
70
+ # the stream's storage mechanism is returned.
71
+ def storage(domain=nil)
72
+ @config.storage(domain || self.domain)
73
+ end
74
+
75
+ # Returns the Vines::Config::Host virtual host for the stream's domain.
76
+ def vhost
77
+ @config.vhost(domain)
78
+ end
79
+
80
+ # Reload the user's information into their active connections. Call this
81
+ # after storage.save_user() to sync the new user state with their other
82
+ # connections.
83
+ def update_user_streams(user)
84
+ connected_resources(user.jid.bare).each do |stream|
85
+ stream.user.update_from(user)
86
+ end
87
+ end
88
+
89
+ def connected_resources(jid)
90
+ router.connected_resources(jid, user.jid)
91
+ end
92
+
93
+ def available_resources(*jid)
94
+ router.available_resources(*jid, user.jid)
95
+ end
96
+
97
+ def interested_resources(*jid)
98
+ router.interested_resources(*jid, user.jid)
99
+ end
100
+
101
+ def ssl_verify_peer(pem)
102
+ # EM is supposed to close the connection when this returns false,
103
+ # but it only does that for inbound connections, not when we
104
+ # make a connection to another server.
105
+ @store.trusted?(pem).tap do |trusted|
106
+ close_connection unless trusted
107
+ end
108
+ end
109
+
110
+ def cert_domain_matches?(domain)
111
+ @store.domain?(get_peer_cert, domain)
112
+ end
113
+
114
+ # Send the data over the wire to this client.
115
+ def write(data)
116
+ log_node(data, :out)
117
+ if data.respond_to?(:to_xml)
118
+ data = data.to_xml(:indent => 0)
119
+ end
120
+ send_data(data)
121
+ end
122
+
123
+ def encrypt
124
+ cert, key = @store.files_for_domain(domain)
125
+ start_tls(cert_chain_file: cert, private_key_file: key, verify_peer: true)
126
+ end
127
+
128
+ # Returns true if the TLS certificate and private key files for this domain
129
+ # exist and can be used to encrypt this stream.
130
+ def encrypt?
131
+ !@store.files_for_domain(domain).nil?
132
+ end
133
+
134
+ def unbind
135
+ router.delete(self)
136
+ log.info { "%s %21s -> %s" %
137
+ ['Stream disconnected:'.ljust(PAD), @remote_addr, @local_addr] }
138
+ log.info { "Streams connected: #{router.size}" }
139
+ end
140
+
141
+ # Advance the stream's state machine to the new state. XML nodes received
142
+ # by the stream will be passed to this state's +node+ method.
143
+ def advance(state)
144
+ @state = state
145
+ end
146
+
147
+ # Stream level errors close the stream while stanza and SASL errors are
148
+ # written to the client and leave the stream open. All exceptions should
149
+ # pass through this method for consistent handling.
150
+ def error(e)
151
+ case e
152
+ when SaslError, StanzaError
153
+ write(e.to_xml)
154
+ when StreamError
155
+ send_stream_error(e)
156
+ close_stream
157
+ else
158
+ log.error(e)
159
+ send_stream_error(StreamErrors::InternalServerError.new)
160
+ close_stream
161
+ end
162
+ end
163
+
164
+ def router
165
+ @config.router
166
+ end
167
+
168
+ private
169
+
170
+ # Return the remote and local socket addresses used by this connection.
171
+ def addresses
172
+ [get_peername, get_sockname].map do |addr|
173
+ addr ? Socket.unpack_sockaddr_in(addr)[0, 2].reverse.join(':') : 'unknown'
174
+ end
175
+ end
176
+
177
+ # Write the StreamError's xml to the stream. Subclasses can override
178
+ # this method with custom error writing behavior.
179
+ def send_stream_error(e)
180
+ write(e.to_xml)
181
+ end
182
+
183
+ # Write a closing stream tag to the stream then close the stream. Subclasses
184
+ # can override this method for custom close behavior.
185
+ def close_stream
186
+ write('</stream:stream>')
187
+ close_connection_after_writing
188
+ end
189
+
190
+ def error?(node)
191
+ ns = node.namespace ? node.namespace.href : nil
192
+ node.name == ERROR && ns == NAMESPACES[:stream]
193
+ end
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
198
+ def process_node_queue
199
+ @nodes.pop do |node|
200
+ Fiber.new do
201
+ process_node(node)
202
+ process_node_queue
203
+ end.resume unless @closed
204
+ end
205
+ end
206
+
207
+ def process_node(node)
208
+ log_node(node, :in)
209
+ @stanza_size = 0
210
+ enforce_rate_limit
211
+ if error?(node)
212
+ close_stream
213
+ else
214
+ state.node(node)
215
+ end
216
+ rescue => e
217
+ error(e)
218
+ end
219
+
220
+ def enforce_rate_limit
221
+ unless @bucket.take(1)
222
+ raise StreamErrors::PolicyViolation.new('rate limit exceeded')
223
+ end
224
+ end
225
+
226
+ def log_node(node, direction)
227
+ return unless log.debug?
228
+ from, to = @remote_addr, @local_addr
229
+ from, to = to, from if direction == :out
230
+ label = (direction == :out) ? 'Sent' : 'Received'
231
+ log.debug("%s %21s -> %s\n%s\n" %
232
+ ["#{label} stanza:".ljust(PAD), from, to, node])
233
+ end
234
+
235
+ # Returns the current +State+ of the stream's state machine. Provided as a
236
+ # method so subclasses can override the behavior.
237
+ def state
238
+ @state
239
+ end
240
+
241
+ # Return +true+ if this is a valid domain-only JID that can be used in
242
+ # stream initiation stanza headers.
243
+ def valid_address?(jid)
244
+ JID.new(jid).domain? rescue false
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,84 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ # Implements the XMPP protocol for client-to-server (c2s) streams. This
6
+ # serves connected streams using the jabber:client namespace.
7
+ class Client < Stream
8
+ MECHANISMS = %w[PLAIN].freeze
9
+
10
+ def initialize(config)
11
+ super
12
+ @session = Client::Session.new(self)
13
+ end
14
+
15
+ # Delegate behavior to the session that's storing our stream state.
16
+ def method_missing(name, *args)
17
+ @session.send(name, *args)
18
+ end
19
+
20
+ %w[advance domain state user user=].each do |name|
21
+ define_method name do |*args|
22
+ @session.send(name, *args)
23
+ end
24
+ end
25
+
26
+ %w[max_stanza_size max_resources_per_account].each do |name|
27
+ define_method name do |*args|
28
+ config[:client].send(name, *args)
29
+ end
30
+ end
31
+
32
+ # Return an array of allowed authentication mechanisms advertised as
33
+ # client stream features.
34
+ def authentication_mechanisms
35
+ MECHANISMS
36
+ end
37
+
38
+ def ssl_handshake_completed
39
+ if get_peer_cert
40
+ close_connection unless cert_domain_matches?(@session.domain)
41
+ end
42
+ end
43
+
44
+ def unbind
45
+ @session.unbind!(self)
46
+ super
47
+ end
48
+
49
+ def start(node)
50
+ to, from = %w[to from].map {|a| node[a] }
51
+ @session.domain = to unless @session.domain
52
+ send_stream_header(from)
53
+ raise StreamErrors::NotAuthorized if domain_change?(to)
54
+ raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0'
55
+ raise StreamErrors::ImproperAddressing unless valid_address?(@session.domain)
56
+ raise StreamErrors::HostUnknown unless config.vhost?(@session.domain)
57
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:client]
58
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
59
+ end
60
+
61
+ private
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
+
70
+ def send_stream_header(to)
71
+ attrs = {
72
+ 'xmlns' => NAMESPACES[:client],
73
+ 'xmlns:stream' => NAMESPACES[:stream],
74
+ 'xml:lang' => 'en',
75
+ 'id' => Kit.uuid,
76
+ 'from' => @session.domain,
77
+ 'version' => '1.0'
78
+ }
79
+ attrs['to'] = to if to
80
+ write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class Auth < State
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
13
+ MAX_AUTH_ATTEMPTS = 3
14
+
15
+ def initialize(stream, success=BindRestart)
16
+ super
17
+ @attempts = 0
18
+ @sasl = SASL.new(stream)
19
+ end
20
+
21
+ def node(node)
22
+ raise StreamErrors::NotAuthorized unless auth?(node)
23
+ if node.text.empty?
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)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def auth?(node)
38
+ node.name == AUTH && namespace(node) == NS
39
+ end
40
+
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)
46
+ end
47
+
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
55
+ end
56
+
57
+ def send_auth_success
58
+ stream.write(SUCCESS)
59
+ stream.reset
60
+ advance
61
+ end
62
+
63
+ def send_auth_fail(condition)
64
+ @attempts += 1
65
+ if @attempts >= MAX_AUTH_ATTEMPTS
66
+ stream.error(StreamErrors::PolicyViolation.new("max authentication attempts exceeded"))
67
+ else
68
+ stream.error(condition)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end