vinesmod 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. data/Gemfile +3 -0
  2. data/LICENSE +19 -0
  3. data/README.md +43 -0
  4. data/Rakefile +57 -0
  5. data/bin/vines +93 -0
  6. data/conf/certs/README +39 -0
  7. data/conf/certs/ca-bundle.crt +3366 -0
  8. data/conf/config.rb +149 -0
  9. data/lib/vines.rb +197 -0
  10. data/lib/vines/cluster.rb +246 -0
  11. data/lib/vines/cluster/connection.rb +26 -0
  12. data/lib/vines/cluster/publisher.rb +55 -0
  13. data/lib/vines/cluster/pubsub.rb +92 -0
  14. data/lib/vines/cluster/sessions.rb +125 -0
  15. data/lib/vines/cluster/subscriber.rb +108 -0
  16. data/lib/vines/command/bcrypt.rb +12 -0
  17. data/lib/vines/command/cert.rb +50 -0
  18. data/lib/vines/command/init.rb +68 -0
  19. data/lib/vines/command/register.rb +27 -0
  20. data/lib/vines/command/restart.rb +12 -0
  21. data/lib/vines/command/schema.rb +24 -0
  22. data/lib/vines/command/start.rb +28 -0
  23. data/lib/vines/command/stop.rb +18 -0
  24. data/lib/vines/command/unregister.rb +27 -0
  25. data/lib/vines/config.rb +213 -0
  26. data/lib/vines/config/host.rb +119 -0
  27. data/lib/vines/config/port.rb +132 -0
  28. data/lib/vines/config/pubsub.rb +108 -0
  29. data/lib/vines/contact.rb +111 -0
  30. data/lib/vines/daemon.rb +78 -0
  31. data/lib/vines/error.rb +150 -0
  32. data/lib/vines/jid.rb +95 -0
  33. data/lib/vines/kit.rb +35 -0
  34. data/lib/vines/log.rb +24 -0
  35. data/lib/vines/router.rb +179 -0
  36. data/lib/vines/stanza.rb +175 -0
  37. data/lib/vines/stanza/iq.rb +48 -0
  38. data/lib/vines/stanza/iq/auth.rb +18 -0
  39. data/lib/vines/stanza/iq/disco_info.rb +45 -0
  40. data/lib/vines/stanza/iq/disco_items.rb +29 -0
  41. data/lib/vines/stanza/iq/error.rb +16 -0
  42. data/lib/vines/stanza/iq/ping.rb +16 -0
  43. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  44. data/lib/vines/stanza/iq/query.rb +10 -0
  45. data/lib/vines/stanza/iq/register.rb +42 -0
  46. data/lib/vines/stanza/iq/result.rb +16 -0
  47. data/lib/vines/stanza/iq/roster.rb +140 -0
  48. data/lib/vines/stanza/iq/session.rb +17 -0
  49. data/lib/vines/stanza/iq/vcard.rb +56 -0
  50. data/lib/vines/stanza/iq/version.rb +25 -0
  51. data/lib/vines/stanza/message.rb +43 -0
  52. data/lib/vines/stanza/presence.rb +156 -0
  53. data/lib/vines/stanza/presence/error.rb +23 -0
  54. data/lib/vines/stanza/presence/probe.rb +37 -0
  55. data/lib/vines/stanza/presence/subscribe.rb +42 -0
  56. data/lib/vines/stanza/presence/subscribed.rb +51 -0
  57. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  58. data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
  59. data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
  60. data/lib/vines/stanza/pubsub.rb +22 -0
  61. data/lib/vines/stanza/pubsub/create.rb +39 -0
  62. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  63. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  64. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  65. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  66. data/lib/vines/storage.rb +188 -0
  67. data/lib/vines/storage/local.rb +165 -0
  68. data/lib/vines/storage/null.rb +39 -0
  69. data/lib/vines/storage/sql.rb +260 -0
  70. data/lib/vines/store.rb +94 -0
  71. data/lib/vines/stream.rb +247 -0
  72. data/lib/vines/stream/client.rb +84 -0
  73. data/lib/vines/stream/client/auth.rb +74 -0
  74. data/lib/vines/stream/client/auth_restart.rb +29 -0
  75. data/lib/vines/stream/client/bind.rb +72 -0
  76. data/lib/vines/stream/client/bind_restart.rb +24 -0
  77. data/lib/vines/stream/client/closed.rb +13 -0
  78. data/lib/vines/stream/client/ready.rb +17 -0
  79. data/lib/vines/stream/client/session.rb +210 -0
  80. data/lib/vines/stream/client/start.rb +27 -0
  81. data/lib/vines/stream/client/tls.rb +38 -0
  82. data/lib/vines/stream/component.rb +58 -0
  83. data/lib/vines/stream/component/handshake.rb +26 -0
  84. data/lib/vines/stream/component/ready.rb +23 -0
  85. data/lib/vines/stream/component/start.rb +19 -0
  86. data/lib/vines/stream/http.rb +157 -0
  87. data/lib/vines/stream/http/auth.rb +22 -0
  88. data/lib/vines/stream/http/bind.rb +32 -0
  89. data/lib/vines/stream/http/bind_restart.rb +37 -0
  90. data/lib/vines/stream/http/ready.rb +29 -0
  91. data/lib/vines/stream/http/request.rb +172 -0
  92. data/lib/vines/stream/http/session.rb +120 -0
  93. data/lib/vines/stream/http/sessions.rb +65 -0
  94. data/lib/vines/stream/http/start.rb +23 -0
  95. data/lib/vines/stream/parser.rb +78 -0
  96. data/lib/vines/stream/sasl.rb +92 -0
  97. data/lib/vines/stream/server.rb +150 -0
  98. data/lib/vines/stream/server/auth.rb +13 -0
  99. data/lib/vines/stream/server/auth_restart.rb +13 -0
  100. data/lib/vines/stream/server/final_restart.rb +21 -0
  101. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  102. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  103. data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
  104. data/lib/vines/stream/server/outbound/final_features.rb +28 -0
  105. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  106. data/lib/vines/stream/server/outbound/start.rb +20 -0
  107. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  108. data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
  109. data/lib/vines/stream/server/ready.rb +24 -0
  110. data/lib/vines/stream/server/start.rb +13 -0
  111. data/lib/vines/stream/server/tls.rb +13 -0
  112. data/lib/vines/stream/state.rb +60 -0
  113. data/lib/vines/token_bucket.rb +55 -0
  114. data/lib/vines/user.rb +123 -0
  115. data/lib/vines/version.rb +5 -0
  116. data/lib/vines/xmpp_server.rb +43 -0
  117. data/vines.gemspec +36 -0
  118. data/web/404.html +51 -0
  119. data/web/apple-touch-icon.png +0 -0
  120. data/web/chat/coffeescripts/chat.coffee +362 -0
  121. data/web/chat/coffeescripts/init.coffee +15 -0
  122. data/web/chat/index.html +16 -0
  123. data/web/chat/javascripts/app.js +1 -0
  124. data/web/chat/stylesheets/chat.css +144 -0
  125. data/web/favicon.png +0 -0
  126. data/web/lib/coffeescripts/button.coffee +25 -0
  127. data/web/lib/coffeescripts/contact.coffee +32 -0
  128. data/web/lib/coffeescripts/filter.coffee +49 -0
  129. data/web/lib/coffeescripts/layout.coffee +30 -0
  130. data/web/lib/coffeescripts/login.coffee +68 -0
  131. data/web/lib/coffeescripts/logout.coffee +5 -0
  132. data/web/lib/coffeescripts/navbar.coffee +84 -0
  133. data/web/lib/coffeescripts/notification.coffee +14 -0
  134. data/web/lib/coffeescripts/router.coffee +40 -0
  135. data/web/lib/coffeescripts/session.coffee +229 -0
  136. data/web/lib/coffeescripts/transfer.coffee +106 -0
  137. data/web/lib/images/dark-gray.png +0 -0
  138. data/web/lib/images/default-user.png +0 -0
  139. data/web/lib/images/light-gray.png +0 -0
  140. data/web/lib/images/logo-large.png +0 -0
  141. data/web/lib/images/logo-small.png +0 -0
  142. data/web/lib/images/white.png +0 -0
  143. data/web/lib/javascripts/base.js +12 -0
  144. data/web/lib/javascripts/icons.js +110 -0
  145. data/web/lib/javascripts/jquery.cookie.js +91 -0
  146. data/web/lib/javascripts/jquery.js +4 -0
  147. data/web/lib/javascripts/raphael.js +6 -0
  148. data/web/lib/javascripts/strophe.js +1 -0
  149. data/web/lib/stylesheets/base.css +385 -0
  150. data/web/lib/stylesheets/login.css +68 -0
  151. metadata +423 -0
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
@@ -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
@@ -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