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