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
@@ -0,0 +1,120 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http
|
6
|
+
class Session < Client::Session
|
7
|
+
include Nokogiri::XML
|
8
|
+
|
9
|
+
attr_accessor :content_type, :hold, :inactivity, :wait
|
10
|
+
|
11
|
+
CONTENT_TYPE = 'text/xml; charset=utf-8'.freeze
|
12
|
+
|
13
|
+
def initialize(stream)
|
14
|
+
super
|
15
|
+
@state = Http::Start.new(stream)
|
16
|
+
@inactivity, @wait, @hold = 20, 60, 1
|
17
|
+
@replied = Time.now
|
18
|
+
@requests, @responses = [], []
|
19
|
+
@content_type = CONTENT_TYPE
|
20
|
+
end
|
21
|
+
|
22
|
+
def close
|
23
|
+
Sessions.delete(@id)
|
24
|
+
router.delete(self)
|
25
|
+
delete_from_cluster
|
26
|
+
unsubscribe_pubsub
|
27
|
+
@requests.each {|req| req.stream.close_connection }
|
28
|
+
@requests.clear
|
29
|
+
@responses.clear
|
30
|
+
@state = Client::Closed.new(nil)
|
31
|
+
@unbound = true
|
32
|
+
@available = false
|
33
|
+
broadcast_unavailable
|
34
|
+
end
|
35
|
+
|
36
|
+
def ready?
|
37
|
+
@state.class == Http::Ready
|
38
|
+
end
|
39
|
+
|
40
|
+
def requests
|
41
|
+
@requests.clone
|
42
|
+
end
|
43
|
+
|
44
|
+
def expired?
|
45
|
+
respond_to_expired_requests
|
46
|
+
@requests.empty? && (Time.now - @replied > @inactivity)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Resume this session from its most recent state with a new client
|
50
|
+
# stream and incoming node.
|
51
|
+
def resume(stream, node)
|
52
|
+
stream.session.requests.each do |req|
|
53
|
+
request(req)
|
54
|
+
end
|
55
|
+
stream.session = self
|
56
|
+
@state.stream = stream
|
57
|
+
@state.node(node)
|
58
|
+
end
|
59
|
+
|
60
|
+
def request(request)
|
61
|
+
if @responses.any?
|
62
|
+
request.reply(wrap_body(@responses.join), @content_type)
|
63
|
+
@replied = Time.now
|
64
|
+
@responses.clear
|
65
|
+
else
|
66
|
+
while @requests.size >= @hold
|
67
|
+
@requests.shift.reply(wrap_body(''), @content_type)
|
68
|
+
@replied = Time.now
|
69
|
+
end
|
70
|
+
@requests << request
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Send an HTTP 200 OK response wrapping the XMPP node content back
|
75
|
+
# to the client.
|
76
|
+
def reply(node)
|
77
|
+
if request = @requests.shift
|
78
|
+
request.reply(node, @content_type)
|
79
|
+
@replied = Time.now
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Write the XMPP node to the client stream after wrapping it in a BOSH
|
84
|
+
# body tag. If there's a waiting request, the node is written
|
85
|
+
# immediately. If not, it's queued until the next request arrives.
|
86
|
+
def write(node)
|
87
|
+
if request = @requests.shift
|
88
|
+
request.reply(wrap_body(node), @content_type)
|
89
|
+
@replied = Time.now
|
90
|
+
else
|
91
|
+
@responses << node.to_s
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def unbind!(stream)
|
96
|
+
@requests.reject! {|req| req.stream == stream }
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def respond_to_expired_requests
|
102
|
+
expired = @requests.select {|req| req.age > @wait }
|
103
|
+
expired.each do |request|
|
104
|
+
request.reply(wrap_body(''), @content_type)
|
105
|
+
@requests.delete(request)
|
106
|
+
@replied = Time.now
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def wrap_body(data)
|
111
|
+
doc = Document.new
|
112
|
+
doc.create_element('body') do |node|
|
113
|
+
node.add_namespace(nil, NAMESPACES[:http_bind])
|
114
|
+
node.inner_html = data.to_s
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http
|
6
|
+
# Sessions is a cache of Http::Session objects for transient HTTP
|
7
|
+
# connections. The cache is monitored for expired client connections.
|
8
|
+
class Sessions
|
9
|
+
include Vines::Log
|
10
|
+
|
11
|
+
@@instance = nil
|
12
|
+
def self.instance
|
13
|
+
@@instance ||= self.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.[](sid)
|
17
|
+
instance[sid]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.[]=(sid, session)
|
21
|
+
instance[sid] = session
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.delete(sid)
|
25
|
+
instance.delete(sid)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@sessions = {}
|
30
|
+
start_timer
|
31
|
+
end
|
32
|
+
|
33
|
+
def []=(sid, session)
|
34
|
+
@sessions[sid] = session
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](sid)
|
38
|
+
@sessions[sid]
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(sid)
|
42
|
+
@sessions.delete(sid)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Check for expired clients to cleanup every second.
|
48
|
+
def start_timer
|
49
|
+
@timer ||= EventMachine::PeriodicTimer.new(1) { cleanup }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Remove cached information for all expired connections. An expired
|
53
|
+
# HTTP client is one that has no queued requests and has had no activity
|
54
|
+
# for over 20 seconds.
|
55
|
+
def cleanup
|
56
|
+
@sessions.each_value do |session|
|
57
|
+
session.close if session.expired?
|
58
|
+
end
|
59
|
+
rescue => e
|
60
|
+
log.error("Expired session cleanup failed: #{e}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Http
|
6
|
+
class Start < State
|
7
|
+
def initialize(stream, success=Auth)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def node(node)
|
12
|
+
raise StreamErrors::NotAuthorized unless body?(node)
|
13
|
+
if session = Sessions[node['sid']]
|
14
|
+
session.resume(stream, node)
|
15
|
+
else
|
16
|
+
stream.start(node)
|
17
|
+
advance
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
class Parser < Nokogiri::XML::SAX::Document
|
6
|
+
include Nokogiri::XML
|
7
|
+
STREAM_NAME = 'stream'.freeze
|
8
|
+
STREAM_URI = 'http://etherx.jabber.org/streams'.freeze
|
9
|
+
IGNORE = NAMESPACES.values_at(:client, :component, :server)
|
10
|
+
|
11
|
+
def initialize(&block)
|
12
|
+
@listeners, @node = Hash.new {|h, k| h[k] = []}, nil
|
13
|
+
@parser = Nokogiri::XML::SAX::PushParser.new(self)
|
14
|
+
instance_eval(&block) if block
|
15
|
+
end
|
16
|
+
|
17
|
+
[:stream_open, :stream_close, :stanza].each do |name|
|
18
|
+
define_method(name) do |&block|
|
19
|
+
@listeners[name] << block
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def <<(data)
|
24
|
+
@parser << data
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_element_namespace(name, attrs=[], prefix=nil, uri=nil, ns=[])
|
29
|
+
el = node(name, attrs, prefix, uri, ns)
|
30
|
+
if stream?(name, uri)
|
31
|
+
notify(:stream_open, el)
|
32
|
+
else
|
33
|
+
@node << el if @node
|
34
|
+
@node = el
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def end_element_namespace(name, prefix=nil, uri=nil)
|
39
|
+
if stream?(name, uri)
|
40
|
+
notify(:stream_close)
|
41
|
+
elsif @node.parent != @node.document
|
42
|
+
@node = @node.parent
|
43
|
+
else
|
44
|
+
notify(:stanza, @node)
|
45
|
+
@node = nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def characters(chars)
|
50
|
+
@node << Text.new(chars, @node.document) if @node
|
51
|
+
end
|
52
|
+
alias :cdata_block :characters
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def notify(msg, node=nil)
|
57
|
+
@listeners[msg].each do |b|
|
58
|
+
(node ? b.call(node) : b.call) rescue nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def stream?(name, uri)
|
63
|
+
name == STREAM_NAME && uri == STREAM_URI
|
64
|
+
end
|
65
|
+
|
66
|
+
def node(name, attrs=[], prefix=nil, uri=nil, ns=[])
|
67
|
+
ignore = stream?(name, uri) ? [] : IGNORE
|
68
|
+
doc = @node ? @node.document : Document.new
|
69
|
+
doc.create_element(name) do |node|
|
70
|
+
attrs.each {|attr| node[attr.localname] = attr.value }
|
71
|
+
ns.each {|prefix, uri| node.add_namespace(prefix, uri) unless ignore.include?(uri) }
|
72
|
+
node.namespace = node.add_namespace(prefix, uri) unless ignore.include?(uri)
|
73
|
+
doc << node unless @node
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
# Provides plain (username/password) and external (TLS certificate) SASL
|
6
|
+
# authentication to client and server streams.
|
7
|
+
class SASL
|
8
|
+
include Vines::Log
|
9
|
+
EMPTY = '='.freeze
|
10
|
+
|
11
|
+
def initialize(stream)
|
12
|
+
@stream = stream
|
13
|
+
end
|
14
|
+
|
15
|
+
# Authenticate s2s streams, comparing their domain to their SSL certificate.
|
16
|
+
# Return +true+ if the base64 encoded domain matches the TLS certificate
|
17
|
+
# presented earlier in stream negotiation. Raise a +SaslError+ if
|
18
|
+
# authentication failed.
|
19
|
+
# http://xmpp.org/extensions/xep-0178.html#s2s
|
20
|
+
def external_auth(encoded)
|
21
|
+
unless encoded == EMPTY
|
22
|
+
authzid = decode64(encoded)
|
23
|
+
matches_from = (authzid == @stream.remote_domain)
|
24
|
+
raise SaslErrors::InvalidAuthzid unless matches_from
|
25
|
+
end
|
26
|
+
matches_from = @stream.cert_domain_matches?(@stream.remote_domain)
|
27
|
+
matches_from or raise SaslErrors::NotAuthorized
|
28
|
+
end
|
29
|
+
|
30
|
+
# Authenticate c2s streams using a username and password. Return the
|
31
|
+
# authenticated +User+ or raise a +SaslError+ if authentication failed.
|
32
|
+
def plain_auth(encoded)
|
33
|
+
jid, password = decode_credentials(encoded)
|
34
|
+
user = authenticate(jid, password)
|
35
|
+
user or raise SaslErrors::NotAuthorized
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Storage backends should not raise errors, but if an unexpected error
|
41
|
+
# occurs during authentication, convert it to a temporary-auth-failure.
|
42
|
+
# Return the authenticated +User+ or +nil+ if authentication failed.
|
43
|
+
def authenticate(jid, password)
|
44
|
+
log.info("Authenticating user: %s" % jid)
|
45
|
+
@stream.storage.authenticate(jid, password).tap do |user|
|
46
|
+
log.info("Authentication succeeded: %s" % user.jid) if user
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
log.error("Failed to authenticate: #{e.to_s}")
|
50
|
+
raise SaslErrors::TemporaryAuthFailure
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the +JID+ and password decoded from the base64 encoded SASL PLAIN
|
54
|
+
# credentials formatted as authzid\0authcid\0password.
|
55
|
+
# http://tools.ietf.org/html/rfc6120#section-6.3.8
|
56
|
+
# http://tools.ietf.org/html/rfc4616
|
57
|
+
def decode_credentials(encoded)
|
58
|
+
authzid, node, password = decode64(encoded).split("\x00")
|
59
|
+
raise SaslErrors::NotAuthorized if node.nil? || node.empty? || password.nil? || password.empty?
|
60
|
+
jid = JID.new(node, @stream.domain) rescue (raise SaslErrors::NotAuthorized)
|
61
|
+
validate_authzid!(authzid, jid)
|
62
|
+
[jid, password]
|
63
|
+
end
|
64
|
+
|
65
|
+
# An optional SASL authzid allows a user to authenticate with one
|
66
|
+
# user name and password and then have their connection authorized as a
|
67
|
+
# different ID (the authzid). We don't support that, so raise an error if
|
68
|
+
# the authzid is provided and different than the authcid.
|
69
|
+
#
|
70
|
+
# Most clients don't send an authzid at all because it's optional and not
|
71
|
+
# widely supported. However, Strophe and Blather send a bare JID, in
|
72
|
+
# compliance with RFC 6120, but Smack sends just the user name as the
|
73
|
+
# authzid. So, take care to handle non-compliant clients here.
|
74
|
+
# http://tools.ietf.org/html/rfc6120#section-6.3.8
|
75
|
+
def validate_authzid!(authzid, jid)
|
76
|
+
return if authzid.nil? || authzid.empty?
|
77
|
+
authzid.downcase!
|
78
|
+
smack = authzid == jid.node
|
79
|
+
compliant = authzid == jid.to_s
|
80
|
+
raise SaslErrors::InvalidAuthzid unless compliant || smack
|
81
|
+
end
|
82
|
+
|
83
|
+
# Decode the base64 encoded string, raising an error for invalid data.
|
84
|
+
# http://tools.ietf.org/html/rfc6120#section-13.9.1
|
85
|
+
def decode64(encoded)
|
86
|
+
Base64.strict_decode64(encoded)
|
87
|
+
rescue
|
88
|
+
raise SaslErrors::IncorrectEncoding
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stream
|
5
|
+
# Implements the XMPP protocol for server-to-server (s2s) streams. This
|
6
|
+
# serves connected streams using the jabber:server namespace. This handles
|
7
|
+
# both accepting incoming s2s streams and initiating outbound s2s streams
|
8
|
+
# to other servers.
|
9
|
+
class Server < Stream
|
10
|
+
MECHANISMS = %w[EXTERNAL].freeze
|
11
|
+
|
12
|
+
# Starts the connection to the remote server. When the stream is
|
13
|
+
# connected and ready to send stanzas it will yield to the callback
|
14
|
+
# block. The callback is run on the EventMachine reactor thread. The
|
15
|
+
# yielded stream will be nil if the remote connection failed. We need to
|
16
|
+
# use a background thread to avoid blocking the server on DNS SRV
|
17
|
+
# lookups.
|
18
|
+
def self.start(config, to, from, &callback)
|
19
|
+
op = proc do
|
20
|
+
Resolv::DNS.open do |dns|
|
21
|
+
dns.getresources("_xmpp-server._tcp.#{to}", Resolv::DNS::Resource::IN::SRV)
|
22
|
+
end.sort! {|a,b| a.priority == b.priority ? b.weight <=> a.weight : a.priority <=> b.priority }
|
23
|
+
end
|
24
|
+
cb = proc do |srv|
|
25
|
+
if srv.empty?
|
26
|
+
srv << {target: to, port: 5269}
|
27
|
+
class << srv.first
|
28
|
+
def method_missing(name); self[name]; end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
Server.connect(config, to, from, srv, callback)
|
32
|
+
end
|
33
|
+
EM.defer(proc { op.call rescue [] }, cb)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.connect(config, to, from, srv, callback)
|
37
|
+
if srv.empty?
|
38
|
+
# fiber so storage calls work properly
|
39
|
+
Fiber.new { callback.call(nil) }.resume
|
40
|
+
else
|
41
|
+
begin
|
42
|
+
rr = srv.shift
|
43
|
+
opts = {to: to, from: from, srv: srv, callback: callback}
|
44
|
+
EM.connect(rr.target.to_s, rr.port, Server, config, opts)
|
45
|
+
rescue => e
|
46
|
+
connect(config, to, from, srv, callback)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :domain
|
52
|
+
attr_accessor :remote_domain
|
53
|
+
|
54
|
+
def initialize(config, options={})
|
55
|
+
super(config)
|
56
|
+
@connected = false
|
57
|
+
@remote_domain = options[:to]
|
58
|
+
@domain = options[:from]
|
59
|
+
@srv = options[:srv]
|
60
|
+
@callback = options[:callback]
|
61
|
+
@outbound = @remote_domain && @domain
|
62
|
+
start = @outbound ? Outbound::Start.new(self) : Start.new(self)
|
63
|
+
advance(start)
|
64
|
+
end
|
65
|
+
|
66
|
+
def post_init
|
67
|
+
super
|
68
|
+
send_stream_header if @outbound
|
69
|
+
end
|
70
|
+
|
71
|
+
def max_stanza_size
|
72
|
+
config[:server].max_stanza_size
|
73
|
+
end
|
74
|
+
|
75
|
+
def ssl_handshake_completed
|
76
|
+
close_connection unless cert_domain_matches?(@remote_domain)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return an array of allowed authentication mechanisms advertised as
|
80
|
+
# server stream features.
|
81
|
+
def authentication_mechanisms
|
82
|
+
MECHANISMS
|
83
|
+
end
|
84
|
+
|
85
|
+
def stream_type
|
86
|
+
:server
|
87
|
+
end
|
88
|
+
|
89
|
+
def unbind
|
90
|
+
super
|
91
|
+
if @outbound && !@connected
|
92
|
+
Server.connect(config, @remote_domain, @domain, @srv, @callback)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def vhost?(domain)
|
97
|
+
config.vhost?(domain)
|
98
|
+
end
|
99
|
+
|
100
|
+
def notify_connected
|
101
|
+
@connected = true
|
102
|
+
if @callback
|
103
|
+
@callback.call(self)
|
104
|
+
@callback = nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def ready?
|
109
|
+
state.class == Server::Ready
|
110
|
+
end
|
111
|
+
|
112
|
+
def start(node)
|
113
|
+
if @outbound then send_stream_header; return end
|
114
|
+
to, from = %w[to from].map {|a| node[a] }
|
115
|
+
@domain, @remote_domain = to, from unless @domain
|
116
|
+
send_stream_header
|
117
|
+
raise StreamErrors::NotAuthorized if domain_change?(to, from)
|
118
|
+
raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0'
|
119
|
+
raise StreamErrors::ImproperAddressing unless valid_address?(@domain) && valid_address?(@remote_domain)
|
120
|
+
raise StreamErrors::HostUnknown unless config.vhost?(@domain) || config.pubsub?(@domain) || config.component?(@domain)
|
121
|
+
raise StreamErrors::NotAuthorized unless config.s2s?(@remote_domain) && config.allowed?(@domain, @remote_domain)
|
122
|
+
raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:server]
|
123
|
+
raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# The +to+ and +from+ domain addresses set on the initial stream header
|
129
|
+
# must not change during stream restarts. This prevents a server from
|
130
|
+
# authenticating as one domain, then sending stanzas from users in a
|
131
|
+
# different domain.
|
132
|
+
def domain_change?(to, from)
|
133
|
+
to != @domain || from != @remote_domain
|
134
|
+
end
|
135
|
+
|
136
|
+
def send_stream_header
|
137
|
+
attrs = {
|
138
|
+
'xmlns' => NAMESPACES[:server],
|
139
|
+
'xmlns:stream' => NAMESPACES[:stream],
|
140
|
+
'xml:lang' => 'en',
|
141
|
+
'id' => Kit.uuid,
|
142
|
+
'from' => @domain,
|
143
|
+
'to' => @remote_domain,
|
144
|
+
'version' => '1.0'
|
145
|
+
}
|
146
|
+
write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|