vinesmod 0.4.5

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