vinesmod 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +43 -0
- data/Rakefile +57 -0
- data/bin/vines +93 -0
- data/conf/certs/README +39 -0
- data/conf/certs/ca-bundle.crt +3366 -0
- data/conf/config.rb +149 -0
- data/lib/vines.rb +197 -0
- data/lib/vines/cluster.rb +246 -0
- data/lib/vines/cluster/connection.rb +26 -0
- data/lib/vines/cluster/publisher.rb +55 -0
- data/lib/vines/cluster/pubsub.rb +92 -0
- data/lib/vines/cluster/sessions.rb +125 -0
- data/lib/vines/cluster/subscriber.rb +108 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +50 -0
- data/lib/vines/command/init.rb +68 -0
- data/lib/vines/command/register.rb +27 -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/command/unregister.rb +27 -0
- data/lib/vines/config.rb +213 -0
- data/lib/vines/config/host.rb +119 -0
- data/lib/vines/config/port.rb +132 -0
- data/lib/vines/config/pubsub.rb +108 -0
- data/lib/vines/contact.rb +111 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/jid.rb +95 -0
- data/lib/vines/kit.rb +35 -0
- data/lib/vines/log.rb +24 -0
- data/lib/vines/router.rb +179 -0
- data/lib/vines/stanza.rb +175 -0
- data/lib/vines/stanza/iq.rb +48 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +45 -0
- data/lib/vines/stanza/iq/disco_items.rb +29 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/private_storage.rb +83 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/register.rb +42 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +140 -0
- data/lib/vines/stanza/iq/session.rb +17 -0
- data/lib/vines/stanza/iq/vcard.rb +56 -0
- data/lib/vines/stanza/iq/version.rb +25 -0
- data/lib/vines/stanza/message.rb +43 -0
- data/lib/vines/stanza/presence.rb +156 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +37 -0
- data/lib/vines/stanza/presence/subscribe.rb +42 -0
- data/lib/vines/stanza/presence/subscribed.rb +51 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
- data/lib/vines/stanza/pubsub.rb +22 -0
- data/lib/vines/stanza/pubsub/create.rb +39 -0
- data/lib/vines/stanza/pubsub/delete.rb +41 -0
- data/lib/vines/stanza/pubsub/publish.rb +66 -0
- data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
- data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
- data/lib/vines/storage.rb +188 -0
- data/lib/vines/storage/local.rb +165 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/sql.rb +260 -0
- data/lib/vines/store.rb +94 -0
- data/lib/vines/stream.rb +247 -0
- data/lib/vines/stream/client.rb +84 -0
- data/lib/vines/stream/client/auth.rb +74 -0
- data/lib/vines/stream/client/auth_restart.rb +29 -0
- data/lib/vines/stream/client/bind.rb +72 -0
- data/lib/vines/stream/client/bind_restart.rb +24 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +17 -0
- data/lib/vines/stream/client/session.rb +210 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +38 -0
- data/lib/vines/stream/component.rb +58 -0
- data/lib/vines/stream/component/handshake.rb +26 -0
- data/lib/vines/stream/component/ready.rb +23 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/http.rb +157 -0
- data/lib/vines/stream/http/auth.rb +22 -0
- data/lib/vines/stream/http/bind.rb +32 -0
- data/lib/vines/stream/http/bind_restart.rb +37 -0
- data/lib/vines/stream/http/ready.rb +29 -0
- data/lib/vines/stream/http/request.rb +172 -0
- data/lib/vines/stream/http/session.rb +120 -0
- data/lib/vines/stream/http/sessions.rb +65 -0
- data/lib/vines/stream/http/start.rb +23 -0
- data/lib/vines/stream/parser.rb +78 -0
- data/lib/vines/stream/sasl.rb +92 -0
- data/lib/vines/stream/server.rb +150 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +13 -0
- data/lib/vines/stream/server/final_restart.rb +21 -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 +32 -0
- data/lib/vines/stream/server/outbound/final_features.rb +28 -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 +34 -0
- data/lib/vines/stream/server/ready.rb +24 -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 +60 -0
- data/lib/vines/token_bucket.rb +55 -0
- data/lib/vines/user.rb +123 -0
- data/lib/vines/version.rb +5 -0
- data/lib/vines/xmpp_server.rb +43 -0
- data/vines.gemspec +36 -0
- data/web/404.html +51 -0
- data/web/apple-touch-icon.png +0 -0
- data/web/chat/coffeescripts/chat.coffee +362 -0
- data/web/chat/coffeescripts/init.coffee +15 -0
- data/web/chat/index.html +16 -0
- data/web/chat/javascripts/app.js +1 -0
- data/web/chat/stylesheets/chat.css +144 -0
- data/web/favicon.png +0 -0
- data/web/lib/coffeescripts/button.coffee +25 -0
- data/web/lib/coffeescripts/contact.coffee +32 -0
- data/web/lib/coffeescripts/filter.coffee +49 -0
- data/web/lib/coffeescripts/layout.coffee +30 -0
- data/web/lib/coffeescripts/login.coffee +68 -0
- data/web/lib/coffeescripts/logout.coffee +5 -0
- data/web/lib/coffeescripts/navbar.coffee +84 -0
- data/web/lib/coffeescripts/notification.coffee +14 -0
- data/web/lib/coffeescripts/router.coffee +40 -0
- data/web/lib/coffeescripts/session.coffee +229 -0
- data/web/lib/coffeescripts/transfer.coffee +106 -0
- data/web/lib/images/dark-gray.png +0 -0
- data/web/lib/images/default-user.png +0 -0
- data/web/lib/images/light-gray.png +0 -0
- data/web/lib/images/logo-large.png +0 -0
- data/web/lib/images/logo-small.png +0 -0
- data/web/lib/images/white.png +0 -0
- data/web/lib/javascripts/base.js +12 -0
- data/web/lib/javascripts/icons.js +110 -0
- data/web/lib/javascripts/jquery.cookie.js +91 -0
- data/web/lib/javascripts/jquery.js +4 -0
- data/web/lib/javascripts/raphael.js +6 -0
- data/web/lib/javascripts/strophe.js +1 -0
- data/web/lib/stylesheets/base.css +385 -0
- data/web/lib/stylesheets/login.css +68 -0
- metadata +423 -0
data/lib/vines/store.rb
ADDED
@@ -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
|
data/lib/vines/stream.rb
ADDED
@@ -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
|