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/jid.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class JID
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
PATTERN = /\A(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?\Z/.freeze
|
8
|
+
|
9
|
+
# http://tools.ietf.org/html/rfc6122#appendix-A
|
10
|
+
NODE_PREP = /[[:cntrl:] "&'\/:<>@]/.freeze
|
11
|
+
|
12
|
+
# http://tools.ietf.org/html/rfc3454#appendix-C
|
13
|
+
NAME_PREP = /[[:cntrl:] ]/.freeze
|
14
|
+
|
15
|
+
# http://tools.ietf.org/html/rfc6122#appendix-B
|
16
|
+
RESOURCE_PREP = /[[:cntrl:]]/.freeze
|
17
|
+
|
18
|
+
attr_reader :node, :domain, :resource
|
19
|
+
attr_writer :resource
|
20
|
+
|
21
|
+
def self.new(node, domain=nil, resource=nil)
|
22
|
+
node.is_a?(JID) ? node : super
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(node, domain=nil, resource=nil)
|
26
|
+
@node, @domain, @resource = node, domain, resource
|
27
|
+
|
28
|
+
if @domain.nil? && @resource.nil?
|
29
|
+
@node, @domain, @resource = @node.to_s.scan(PATTERN).first
|
30
|
+
end
|
31
|
+
[@node, @domain].each {|part| part.downcase! if part }
|
32
|
+
|
33
|
+
validate
|
34
|
+
end
|
35
|
+
|
36
|
+
# Strip the resource part from this JID and return it as a new
|
37
|
+
# JID object. The new JID contains only the optional node part
|
38
|
+
# and the required domain part from the original. This JID remains
|
39
|
+
# unchanged.
|
40
|
+
def bare
|
41
|
+
JID.new(@node, @domain)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return true if this is a bare JID without a resource part.
|
45
|
+
def bare?
|
46
|
+
@resource.nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return true if this is a domain-only JID without a node or resource part.
|
50
|
+
def domain?
|
51
|
+
!empty? && to_s == @domain
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return true if this JID is equal to the empty string ''. That is, it's
|
55
|
+
# missing the node, domain, and resource parts that form a valid JID. It
|
56
|
+
# makes for easier error handling to be able to create JID objects from
|
57
|
+
# strings and then check if they're empty rather than nil.
|
58
|
+
def empty?
|
59
|
+
to_s == ''
|
60
|
+
end
|
61
|
+
|
62
|
+
def <=>(jid)
|
63
|
+
self.to_s <=> jid.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def eql?(jid)
|
67
|
+
jid.is_a?(JID) && self == jid
|
68
|
+
end
|
69
|
+
|
70
|
+
def hash
|
71
|
+
self.to_s.hash
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
s = @domain
|
76
|
+
s = "#{@node}@#{s}" if @node
|
77
|
+
s = "#{s}/#{@resource}" if @resource
|
78
|
+
s
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def validate
|
84
|
+
[@node, @domain, @resource].each do |part|
|
85
|
+
raise ArgumentError, 'jid too long' if (part || '').size > 1023
|
86
|
+
end
|
87
|
+
raise ArgumentError, 'empty node' if @node && @node.strip.empty?
|
88
|
+
raise ArgumentError, 'node contains invalid characters' if @node && @node =~ NODE_PREP
|
89
|
+
raise ArgumentError, 'empty resource' if @resource && @resource.strip.empty?
|
90
|
+
raise ArgumentError, 'resource contains invalid characters' if @resource && @resource =~ RESOURCE_PREP
|
91
|
+
raise ArgumentError, 'empty domain' if @domain == '' && (@node || @resource)
|
92
|
+
raise ArgumentError, 'domain contains invalid characters' if @domain && @domain =~ NAME_PREP
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/vines/kit.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
# A module for utility methods with no better home.
|
5
|
+
module Kit
|
6
|
+
# Create a hex-encoded, SHA-512 HMAC of the data, using the secret key.
|
7
|
+
def self.hmac(key, data)
|
8
|
+
digest = OpenSSL::Digest::Digest.new("sha512")
|
9
|
+
OpenSSL::HMAC.hexdigest(digest, key, data)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Generates a random uuid per rfc 4122 that's useful for including in
|
13
|
+
# stream, iq, and other xmpp stanzas.
|
14
|
+
def self.uuid
|
15
|
+
SecureRandom.uuid
|
16
|
+
end
|
17
|
+
|
18
|
+
# Generates a random 128 character authentication token.
|
19
|
+
def self.auth_token
|
20
|
+
SecureRandom.hex(64)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.local_ip
|
24
|
+
orig = Socket.do_not_reverse_lookup
|
25
|
+
Socket.do_not_reverse_lookup = true
|
26
|
+
UDPSocket.open do |s|
|
27
|
+
s.connect '64.223.187.99', 1
|
28
|
+
s.addr.last
|
29
|
+
end
|
30
|
+
|
31
|
+
ensure
|
32
|
+
Socket.do_not_reverse_lookup = orig
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/vines/log.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Log
|
5
|
+
@@logger = nil
|
6
|
+
def log
|
7
|
+
unless @@logger
|
8
|
+
@@logger = Logger.new(STDOUT)
|
9
|
+
@@logger.level = Logger::INFO
|
10
|
+
@@logger.progname = 'vines'
|
11
|
+
@@logger.formatter = Class.new(Logger::Formatter) do
|
12
|
+
def initialize
|
13
|
+
@time = "%Y-%m-%d %H:%M:%S".freeze
|
14
|
+
@fmt = "[%s] %5s -- %s: %s\n".freeze
|
15
|
+
end
|
16
|
+
def call(severity, time, program, msg)
|
17
|
+
@fmt % [time.utc.strftime(@time), severity, program, msg2str(msg)]
|
18
|
+
end
|
19
|
+
end.new
|
20
|
+
end
|
21
|
+
@@logger
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/vines/router.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
# The router tracks all stream connections to the server for all clients,
|
5
|
+
# servers, and components. It sends stanzas to the correct stream based on
|
6
|
+
# the 'to' attribute. Router is a singleton, shared by all streams, that must
|
7
|
+
# be accessed with +Config#router+.
|
8
|
+
class Router
|
9
|
+
EMPTY = [].freeze
|
10
|
+
|
11
|
+
STREAM_TYPES = [:client, :server, :component].freeze
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
@clients, @servers, @components = {}, [], []
|
16
|
+
@pending = Hash.new {|h,k| h[k] = [] }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns streams for all connected resources for this JID. A resource is
|
20
|
+
# considered connected after it has completed authentication and resource
|
21
|
+
# binding.
|
22
|
+
def connected_resources(jid, from, proxies=true)
|
23
|
+
jid, from = JID.new(jid), JID.new(from)
|
24
|
+
return [] unless @config.allowed?(jid, from)
|
25
|
+
|
26
|
+
local = @clients[jid.bare] || EMPTY
|
27
|
+
local = local.select {|stream| stream.user.jid == jid } unless jid.bare?
|
28
|
+
remote = proxies ? proxies(jid) : EMPTY
|
29
|
+
[local, remote].flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns streams for all available resources for this JID. A resource is
|
33
|
+
# marked available after it sends initial presence.
|
34
|
+
def available_resources(*jids, from)
|
35
|
+
clients(jids, from) do |stream|
|
36
|
+
stream.available?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns streams for all interested resources for this JID. A resource is
|
41
|
+
# marked interested after it requests the roster.
|
42
|
+
def interested_resources(*jids, from)
|
43
|
+
clients(jids, from) do |stream|
|
44
|
+
stream.interested?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Add the connection to the routing table. The connection must return
|
49
|
+
# :client, :server, or :component from its +stream_type+ method so the
|
50
|
+
# router can properly route stanzas to the stream.
|
51
|
+
def <<(stream)
|
52
|
+
case stream_type(stream)
|
53
|
+
when :client then
|
54
|
+
return unless stream.connected?
|
55
|
+
jid = stream.user.jid.bare
|
56
|
+
@clients[jid] ||= []
|
57
|
+
@clients[jid] << stream
|
58
|
+
when :server then @servers << stream
|
59
|
+
when :component then @components << stream
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Remove the connection from the routing table.
|
64
|
+
def delete(stream)
|
65
|
+
case stream_type(stream)
|
66
|
+
when :client then
|
67
|
+
return unless stream.connected?
|
68
|
+
jid = stream.user.jid.bare
|
69
|
+
streams = @clients[jid] || []
|
70
|
+
streams.delete(stream)
|
71
|
+
@clients.delete(jid) if streams.empty?
|
72
|
+
when :server then @servers.delete(stream)
|
73
|
+
when :component then @components.delete(stream)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Send the stanza to the appropriate remote server-to-server stream
|
78
|
+
# or an external component stream.
|
79
|
+
def route(stanza)
|
80
|
+
to, from = %w[to from].map {|attr| JID.new(stanza[attr]) }
|
81
|
+
return unless @config.allowed?(to, from)
|
82
|
+
key = [to.domain, from.domain]
|
83
|
+
|
84
|
+
if stream = connection_to(to, from)
|
85
|
+
stream.write(stanza)
|
86
|
+
elsif @pending.key?(key)
|
87
|
+
@pending[key] << stanza
|
88
|
+
elsif @config.s2s?(to.domain)
|
89
|
+
@pending[key] << stanza
|
90
|
+
Vines::Stream::Server.start(@config, to.domain, from.domain) do |stream|
|
91
|
+
stream ? send_pending(key, stream) : return_pending(key)
|
92
|
+
@pending.delete(key)
|
93
|
+
end
|
94
|
+
else
|
95
|
+
raise StanzaErrors::RemoteServerNotFound.new(stanza, 'cancel')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the total number of streams connected to the server.
|
100
|
+
def size
|
101
|
+
clients = @clients.values.inject(0) {|sum, arr| sum + arr.size }
|
102
|
+
clients + @servers.size + @components.size
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Write all pending stanzas for this domain to the stream. Called after a
|
108
|
+
# s2s stream has successfully connected and we need to dequeue all stanzas
|
109
|
+
# we received while waiting for the connection to finish.
|
110
|
+
def send_pending(key, stream)
|
111
|
+
@pending[key].each do |stanza|
|
112
|
+
stream.write(stanza)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return all pending stanzas to their senders as remote-server-not-found
|
117
|
+
# errors. Called after a s2s stream has failed to connect.
|
118
|
+
def return_pending(key)
|
119
|
+
@pending[key].each do |stanza|
|
120
|
+
to, from = JID.new(stanza['to']), JID.new(stanza['from'])
|
121
|
+
xml = StanzaErrors::RemoteServerNotFound.new(stanza, 'cancel').to_xml
|
122
|
+
if @config.component?(from)
|
123
|
+
connection_to(from, to).write(xml) rescue nil
|
124
|
+
else
|
125
|
+
connected_resources(from, to).each {|c| c.write(xml) }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Return the client streams to which the from address is allowed to
|
131
|
+
# contact. Apply the filter block to each stream to narrow the results
|
132
|
+
# before returning the streams.
|
133
|
+
def clients(jids, from, &filter)
|
134
|
+
jids = filter_allowed(jids, from)
|
135
|
+
local = @clients.values_at(*jids).compact.flatten.select(&filter)
|
136
|
+
proxies = proxies(*jids).select(&filter)
|
137
|
+
[local, proxies].flatten
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return the bare JIDs from the list that are allowed to talk to
|
141
|
+
# the +from+ JID.
|
142
|
+
def filter_allowed(jids, from)
|
143
|
+
from = JID.new(from)
|
144
|
+
jids.flatten.map {|jid| JID.new(jid).bare }
|
145
|
+
.select {|jid| @config.allowed?(jid, from) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def proxies(*jids)
|
149
|
+
return EMPTY unless @config.cluster?
|
150
|
+
@config.cluster.remote_sessions(*jids)
|
151
|
+
end
|
152
|
+
|
153
|
+
def connection_to(to, from)
|
154
|
+
component_stream(to) || server_stream(to, from)
|
155
|
+
end
|
156
|
+
|
157
|
+
def component_stream(to)
|
158
|
+
@components.select do |stream|
|
159
|
+
stream.ready? && stream.remote_domain == to.domain
|
160
|
+
end.sample
|
161
|
+
end
|
162
|
+
|
163
|
+
def server_stream(to, from)
|
164
|
+
@servers.select do |stream|
|
165
|
+
stream.ready? &&
|
166
|
+
stream.remote_domain == to.domain &&
|
167
|
+
stream.domain == from.domain
|
168
|
+
end.sample
|
169
|
+
end
|
170
|
+
|
171
|
+
def stream_type(connection)
|
172
|
+
connection.stream_type.tap do |type|
|
173
|
+
unless STREAM_TYPES.include?(type)
|
174
|
+
raise ArgumentError, "unexpected stream type: #{type}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/lib/vines/stanza.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class Stanza
|
5
|
+
include Nokogiri::XML
|
6
|
+
|
7
|
+
attr_reader :stream
|
8
|
+
|
9
|
+
EMPTY = ''.freeze
|
10
|
+
FROM, MESSAGE, TO = %w[from message to].map {|s| s.freeze }
|
11
|
+
ROUTABLE_STANZAS = %w[message iq presence].freeze
|
12
|
+
|
13
|
+
@@types = {}
|
14
|
+
|
15
|
+
def self.register(xpath, ns={})
|
16
|
+
@@types[[xpath, ns]] = self
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_node(node, stream)
|
20
|
+
# optimize common case
|
21
|
+
return Message.new(node, stream) if node.name == MESSAGE
|
22
|
+
found = @@types.select {|pair, v| node.xpath(*pair).any? }
|
23
|
+
.sort {|a, b| b[0][0].length - a[0][0].length }.first
|
24
|
+
found ? found[1].new(node, stream) : nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(node, stream)
|
28
|
+
@node, @stream = node, stream
|
29
|
+
end
|
30
|
+
|
31
|
+
# Send the stanza to all recipients, stamping it with from and
|
32
|
+
# to addresses first.
|
33
|
+
def broadcast(recipients)
|
34
|
+
@node[FROM] = stream.user.jid.to_s
|
35
|
+
recipients.each do |recipient|
|
36
|
+
@node[TO] = recipient.user.jid.to_s
|
37
|
+
recipient.write(@node)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns true if this stanza should be processed locally. Returns false
|
42
|
+
# if it's destined for a remote domain or external component.
|
43
|
+
def local?
|
44
|
+
return true unless ROUTABLE_STANZAS.include?(@node.name)
|
45
|
+
to = JID.new(@node[TO])
|
46
|
+
to.empty? || local_jid?(to)
|
47
|
+
end
|
48
|
+
|
49
|
+
def local_jid?(*jids)
|
50
|
+
stream.config.local_jid?(*jids)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return true if this stanza is addressed to a pubsub subdomain hosted
|
54
|
+
# at this server. This helps differentiate between IQ stanzas addressed
|
55
|
+
# to the server and stanzas addressed to pubsub domains, both of which must
|
56
|
+
# be handled locally and not routed.
|
57
|
+
def to_pubsub_domain?
|
58
|
+
stream.config.pubsub?(validate_to)
|
59
|
+
end
|
60
|
+
|
61
|
+
def route
|
62
|
+
stream.router.route(@node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def router
|
66
|
+
stream.router
|
67
|
+
end
|
68
|
+
|
69
|
+
def storage(domain=stream.domain)
|
70
|
+
stream.storage(domain)
|
71
|
+
end
|
72
|
+
|
73
|
+
def process
|
74
|
+
raise 'subclass must implement'
|
75
|
+
end
|
76
|
+
|
77
|
+
# Broadcast unavailable presence from the user's available resources to the
|
78
|
+
# recipient's available resources. Route the stanza to a remote server if
|
79
|
+
# the recipient isn't hosted locally.
|
80
|
+
def send_unavailable(from, to)
|
81
|
+
available = router.available_resources(from, to)
|
82
|
+
stanzas = available.map {|stream| unavailable(stream.user.jid) }
|
83
|
+
broadcast_to_available_resources(stanzas, to)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return an unavailable presence stanza addressed from the given JID.
|
87
|
+
def unavailable(from)
|
88
|
+
doc = Document.new
|
89
|
+
doc.create_element('presence',
|
90
|
+
'from' => from.to_s,
|
91
|
+
'id' => Kit.uuid,
|
92
|
+
'type' => 'unavailable')
|
93
|
+
end
|
94
|
+
|
95
|
+
# Return nil if this stanza has no 'to' attribute. Return a Vines::JID
|
96
|
+
# if it contains a valid 'to' attribute. Raise a JidMalformed error if
|
97
|
+
# the JID is invalid.
|
98
|
+
def validate_to
|
99
|
+
validate_address(TO)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return nil if this stanza has no 'from' attribute. Return a Vines::JID
|
103
|
+
# if it contains a valid 'from' attribute. Raise a JidMalformed error if
|
104
|
+
# the JID is invalid.
|
105
|
+
def validate_from
|
106
|
+
validate_address(FROM)
|
107
|
+
end
|
108
|
+
|
109
|
+
def method_missing(method, *args, &block)
|
110
|
+
@node.send(method, *args, &block)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Send the stanzas to the destination JID, routing to a s2s stream
|
116
|
+
# if the address is remote. This method properly stamps the to address
|
117
|
+
# on each stanza before it's sent. The caller must set the from address.
|
118
|
+
def broadcast_to_available_resources(stanzas, to)
|
119
|
+
return if send_to_remote(stanzas, to)
|
120
|
+
send_to_recipients(stanzas, stream.available_resources(to))
|
121
|
+
end
|
122
|
+
|
123
|
+
# Send the stanzas to the destination JID, routing to a s2s stream
|
124
|
+
# if the address is remote. This method properly stamps the to address
|
125
|
+
# on each stanza before it's sent. The caller must set the from address.
|
126
|
+
def broadcast_to_interested_resources(stanzas, to)
|
127
|
+
return if send_to_remote(stanzas, to)
|
128
|
+
send_to_recipients(stanzas, stream.interested_resources(to))
|
129
|
+
end
|
130
|
+
|
131
|
+
# Route the stanzas to a remote server, stamping a bare JID as the
|
132
|
+
# to address. Bare JIDs are required for presence subscription stanzas
|
133
|
+
# sent to the remote contact's server. Return true if the stanzas were
|
134
|
+
# routed, false if they must be delivered locally.
|
135
|
+
def send_to_remote(stanzas, to)
|
136
|
+
return false if local_jid?(to)
|
137
|
+
to = JID.new(to)
|
138
|
+
stanzas.each do |el|
|
139
|
+
el[TO] = to.bare.to_s
|
140
|
+
router.route(el)
|
141
|
+
end
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
# Send the stanzas to the local recipient streams, stamping a full JID as
|
146
|
+
# the to address. It's important to use full JIDs, even when sending to
|
147
|
+
# local clients, because the stanzas may be routed to other cluster nodes
|
148
|
+
# for delivery. We need the receiving cluster node to send the stanza just
|
149
|
+
# to this full JID, not to lookup all JIDs for this user.
|
150
|
+
def send_to_recipients(stanzas, recipients)
|
151
|
+
recipients.each do |recipient|
|
152
|
+
stanzas.each do |el|
|
153
|
+
el[TO] = recipient.user.jid.to_s
|
154
|
+
recipient.write(el)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Return true if the to and from JIDs are allowed to communicate with one
|
160
|
+
# another based on the cross_domain_messages setting in conf/config.rb. If
|
161
|
+
# a domain's users are isolated to sending messages only within their own
|
162
|
+
# domain, pubsub stanzas must not be processed from remote JIDs.
|
163
|
+
def allowed?
|
164
|
+
stream.config.allowed?(validate_to || stream.domain, stream.user.jid)
|
165
|
+
end
|
166
|
+
|
167
|
+
def validate_address(attr)
|
168
|
+
jid = (self[attr] || EMPTY)
|
169
|
+
return if jid.empty?
|
170
|
+
JID.new(jid)
|
171
|
+
rescue
|
172
|
+
raise StanzaErrors::JidMalformed.new(self, 'modify')
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|