vines 0.2.1 → 0.3.0

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 (96) hide show
  1. data/README +1 -1
  2. data/Rakefile +10 -10
  3. data/conf/certs/ca-bundle.crt +112 -378
  4. data/conf/config.rb +18 -9
  5. data/lib/vines.rb +8 -1
  6. data/lib/vines/command/cert.rb +2 -1
  7. data/lib/vines/command/init.rb +11 -0
  8. data/lib/vines/command/ldap.rb +6 -3
  9. data/lib/vines/command/schema.rb +1 -1
  10. data/lib/vines/config.rb +57 -146
  11. data/lib/vines/config/host.rb +85 -0
  12. data/lib/vines/config/port.rb +111 -0
  13. data/lib/vines/contact.rb +1 -1
  14. data/lib/vines/jid.rb +26 -4
  15. data/lib/vines/kit.rb +6 -0
  16. data/lib/vines/log.rb +24 -0
  17. data/lib/vines/router.rb +70 -38
  18. data/lib/vines/stanza.rb +45 -8
  19. data/lib/vines/stanza/iq.rb +3 -3
  20. data/lib/vines/stanza/iq/disco_info.rb +5 -1
  21. data/lib/vines/stanza/iq/disco_items.rb +3 -0
  22. data/lib/vines/stanza/iq/private_storage.rb +9 -5
  23. data/lib/vines/stanza/iq/roster.rb +11 -12
  24. data/lib/vines/stanza/iq/vcard.rb +4 -4
  25. data/lib/vines/stanza/iq/version.rb +25 -0
  26. data/lib/vines/stanza/message.rb +4 -5
  27. data/lib/vines/stanza/presence.rb +20 -18
  28. data/lib/vines/stanza/presence/probe.rb +3 -4
  29. data/lib/vines/stanza/presence/subscribe.rb +4 -3
  30. data/lib/vines/stanza/presence/subscribed.rb +6 -5
  31. data/lib/vines/stanza/presence/unsubscribe.rb +4 -4
  32. data/lib/vines/stanza/presence/unsubscribed.rb +4 -3
  33. data/lib/vines/storage/couchdb.rb +3 -3
  34. data/lib/vines/storage/ldap.rb +19 -8
  35. data/lib/vines/storage/local.rb +23 -12
  36. data/lib/vines/storage/redis.rb +3 -3
  37. data/lib/vines/storage/sql.rb +5 -5
  38. data/lib/vines/stream.rb +40 -6
  39. data/lib/vines/stream/client.rb +5 -6
  40. data/lib/vines/stream/client/auth.rb +3 -2
  41. data/lib/vines/stream/client/bind.rb +2 -2
  42. data/lib/vines/stream/client/bind_restart.rb +1 -2
  43. data/lib/vines/stream/client/ready.rb +2 -0
  44. data/lib/vines/stream/client/session.rb +13 -4
  45. data/lib/vines/stream/client/tls.rb +1 -0
  46. data/lib/vines/stream/component.rb +6 -5
  47. data/lib/vines/stream/component/ready.rb +5 -6
  48. data/lib/vines/stream/http.rb +10 -4
  49. data/lib/vines/stream/http/request.rb +23 -2
  50. data/lib/vines/stream/server.rb +13 -11
  51. data/lib/vines/stream/server/outbound/auth_result.rb +1 -0
  52. data/lib/vines/stream/server/outbound/tls_result.rb +1 -0
  53. data/lib/vines/stream/server/ready.rb +2 -2
  54. data/lib/vines/user.rb +2 -1
  55. data/lib/vines/version.rb +1 -1
  56. data/test/config/host_test.rb +292 -0
  57. data/test/config_test.rb +244 -103
  58. data/test/contact_test.rb +7 -1
  59. data/test/jid_test.rb +48 -0
  60. data/test/router_test.rb +16 -47
  61. data/test/stanza/iq/disco_info_test.rb +76 -0
  62. data/test/stanza/iq/disco_items_test.rb +47 -0
  63. data/test/stanza/iq/private_storage_test.rb +33 -10
  64. data/test/stanza/iq/roster_test.rb +15 -5
  65. data/test/stanza/iq/vcard_test.rb +8 -25
  66. data/test/stanza/iq/version_test.rb +62 -0
  67. data/test/stanza/iq_test.rb +13 -10
  68. data/test/stanza/message_test.rb +16 -24
  69. data/test/stanza/presence/probe_test.rb +52 -0
  70. data/test/stanza/presence/subscribe_test.rb +1 -5
  71. data/test/stanza_test.rb +77 -0
  72. data/test/stream/client/auth_test.rb +1 -0
  73. data/test/stream/client/ready_test.rb +2 -0
  74. data/test/stream/client/session_test.rb +7 -2
  75. data/test/stream/component/ready_test.rb +19 -36
  76. data/test/stream/http/request_test.rb +22 -2
  77. data/test/stream/server/ready_test.rb +14 -21
  78. data/web/404.html +9 -3
  79. data/web/chat/index.html +2 -2
  80. data/web/chat/javascripts/app.js +1 -1
  81. data/web/chat/stylesheets/chat.css +4 -9
  82. data/web/lib/coffeescripts/layout.coffee +2 -2
  83. data/web/{chat → lib}/coffeescripts/logout.coffee +0 -0
  84. data/web/lib/coffeescripts/notification.coffee +14 -0
  85. data/web/lib/coffeescripts/session.coffee +28 -24
  86. data/web/lib/coffeescripts/transfer.coffee +37 -34
  87. data/web/lib/javascripts/base.js +8 -8
  88. data/web/lib/javascripts/icons.js +3 -0
  89. data/web/lib/javascripts/jquery.js +4 -18
  90. data/web/lib/javascripts/layout.js +2 -2
  91. data/web/{chat → lib}/javascripts/logout.js +0 -0
  92. data/web/lib/javascripts/notification.js +26 -0
  93. data/web/lib/javascripts/session.js +20 -16
  94. data/web/lib/javascripts/transfer.js +45 -55
  95. data/web/lib/stylesheets/base.css +45 -9
  96. metadata +31 -15
data/conf/config.rb CHANGED
@@ -16,17 +16,28 @@ Vines::Config.configure do
16
16
  # command. Change the example, 'wonderland.lit', domain name to your actual
17
17
  # domain.
18
18
  #
19
+ # The private_storage attribute allows clients to store XML fragments
20
+ # on the server, using the XEP-0049 Private XML Storage feature.
21
+ #
19
22
  # Shared storage example:
20
23
  # host 'verona.lit', 'wonderland.lit' do
24
+ # private_storage false
25
+ # cross_domain_messages false
21
26
  # storage 'fs' do
22
27
  # dir 'data/users'
23
28
  # end
29
+ # components 'tea' => 'secr3t',
30
+ # 'cake' => 'passw0rd'
24
31
  # end
25
32
 
26
33
  host 'wonderland.lit' do
34
+ cross_domain_messages false
35
+ private_storage false
27
36
  storage 'fs' do
28
37
  dir 'data/users'
29
38
  end
39
+ # components 'tea' => 'secr3t',
40
+ # 'cake' => 'passw0rd'
30
41
  end
31
42
 
32
43
  # Hosts can use LDAP authentication that overrides the authentication
@@ -35,6 +46,8 @@ Vines::Config.configure do
35
46
  # information, like rosters, is still saved in the storage database.
36
47
  #
37
48
  # host 'wonderland.lit' do
49
+ # cross_domain_messages false
50
+ # private_storage false
38
51
  # storage 'fs' do
39
52
  # dir 'data/users'
40
53
  # end
@@ -42,19 +55,19 @@ Vines::Config.configure do
42
55
  # dn 'cn=Directory Manager'
43
56
  # password 'secr3t'
44
57
  # basedn 'dc=wonderland,dc=lit'
58
+ # groupdn 'cn=chatters,dc=wonderland,dc=lit' # optional
45
59
  # object_class 'person'
46
60
  # user_attr 'uid'
47
61
  # name_attr 'cn'
48
62
  # tls true
49
63
  # end
64
+ # components 'tea' => 'secr3t',
65
+ # 'cake' => 'passw0rd'
50
66
  # end
51
67
 
52
68
  # Configure the client-to-server port. The max_resources_per_account attribute
53
69
  # limits how many concurrent connections one user can have to the server.
54
- # The private_storage attribute allows clients to store XML fragments
55
- # on the server, using the XEP-0049 Private XML Storage feature.
56
70
  client '0.0.0.0', 5222 do
57
- private_storage true
58
71
  max_stanza_size 65536
59
72
  max_resources_per_account 5
60
73
  end
@@ -75,19 +88,15 @@ Vines::Config.configure do
75
88
  # the URL to which BOSH clients must POST their XMPP stanza requests.
76
89
  http '0.0.0.0', 5280 do
77
90
  bind '/xmpp'
78
- private_storage true
79
91
  max_stanza_size 65536
80
92
  max_resources_per_account 5
81
93
  root 'web'
82
94
  end
83
95
 
84
- # Configure the XEP-0114 external component port. Add entries for each
85
- # component sub-domain allowed to connect to this server. Components must
86
- # authenticate with a password.
96
+ # Configure the XEP-0114 external component port. Component sub-domains and
97
+ # their passwords are defined with their virtual host entries above.
87
98
  component '0.0.0.0', 5347 do
88
99
  max_stanza_size 131072
89
- #components 'tea.wonderland.lit' => 'secr3t',
90
- # 'cake.wonderland.lit' => 'passw0rd'
91
100
  end
92
101
  end
93
102
 
data/lib/vines.rb CHANGED
@@ -9,6 +9,7 @@ module Vines
9
9
  :roster => 'jabber:iq:roster'.freeze,
10
10
  :non_sasl => 'jabber:iq:auth'.freeze,
11
11
  :storage => 'jabber:iq:private'.freeze,
12
+ :version => 'jabber:iq:version'.freeze,
12
13
  :sasl => 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze,
13
14
  :tls => 'urn:ietf:params:xml:ns:xmpp-tls'.freeze,
14
15
  :bind => 'urn:ietf:params:xml:ns:xmpp-bind'.freeze,
@@ -65,7 +66,9 @@ end
65
66
  uri
66
67
  yaml
67
68
 
69
+ vines/log
68
70
  vines/jid
71
+
69
72
  vines/stanza
70
73
  vines/stanza/iq
71
74
  vines/stanza/iq/query
@@ -79,6 +82,7 @@ end
79
82
  vines/stanza/iq/roster
80
83
  vines/stanza/iq/session
81
84
  vines/stanza/iq/vcard
85
+ vines/stanza/iq/version
82
86
  vines/stanza/message
83
87
  vines/stanza/presence
84
88
  vines/stanza/presence/error
@@ -96,9 +100,12 @@ end
96
100
  vines/storage/redis
97
101
  vines/storage/sql
98
102
 
103
+ vines/config
104
+ vines/config/host
105
+ vines/config/port
106
+
99
107
  vines/store
100
108
  vines/contact
101
- vines/config
102
109
  vines/daemon
103
110
  vines/error
104
111
  vines/kit
@@ -5,7 +5,7 @@ module Vines
5
5
  class Cert
6
6
  def run(opts)
7
7
  raise 'vines cert <domain>' unless opts[:args].size == 1
8
- dir = File.expand_path(File.join(opts[:config], '../certs'))
8
+ dir = File.expand_path('../certs', opts[:config])
9
9
  create_cert(opts[:args].first, dir)
10
10
  end
11
11
 
@@ -36,6 +36,7 @@ module Vines
36
36
  {'key' => key, 'crt' => cert}.each_pair do |ext, o|
37
37
  name = File.join(dir, "#{domain}.#{ext}")
38
38
  File.open(name, "w") {|f| f.write(o.to_pem) }
39
+ File.chmod(0600, name) if ext == 'key'
39
40
  end
40
41
  end
41
42
 
@@ -19,6 +19,7 @@ module Vines
19
19
 
20
20
  create_users(domain, users)
21
21
  update_config(domain, File.join(dir, 'conf', 'config.rb'))
22
+ fix_perms(dir)
22
23
  Command::Cert.new.create_cert(domain, File.join(dir, 'conf/certs'))
23
24
 
24
25
  puts "Initialized server directory: #{domain}"
@@ -27,6 +28,16 @@ module Vines
27
28
 
28
29
  private
29
30
 
31
+ # Limit file system database directory access so the server is the only
32
+ # process managing the data. The config.rb file contains component and
33
+ # database passwords, so restrict access to just the server user as well.
34
+ def fix_perms(dir)
35
+ %w[data data/users].each do |f|
36
+ File.chmod(0700, File.join(dir, f))
37
+ end
38
+ File.chmod(0600, File.join(dir, 'conf/config.rb'))
39
+ end
40
+
30
41
  def update_config(domain, config)
31
42
  text = File.read(config)
32
43
  File.open(config, 'w') do |f|
@@ -7,7 +7,7 @@ module Vines
7
7
  raise 'vines ldap <domain>' unless opts[:args].size == 1
8
8
  require opts[:config]
9
9
  domain = opts[:args].first
10
- unless storage = Config.instance.vhosts[domain]
10
+ unless storage = Config.instance.vhosts[domain].storage rescue nil
11
11
  raise "#{domain} virtual host not found in conf/config.rb"
12
12
  end
13
13
  unless storage.ldap?
@@ -27,8 +27,11 @@ module Vines
27
27
  rescue Exception => e
28
28
  raise "LDAP connection failed: #{e.message}"
29
29
  end
30
- raise "User not found" unless user
31
- puts "Found #{user.jid} with name: #{user.name}"
30
+
31
+ filter = storage.ldap.filter(jid)
32
+ raise "User not found with filter:\n #{filter}" unless user
33
+ name = user.name.empty? ? '<name missing>' : user.name
34
+ puts "Found user #{name} with filter:\n #{filter}"
32
35
  end
33
36
  end
34
37
  end
@@ -7,7 +7,7 @@ module Vines
7
7
  raise 'vines schema <domain>' unless opts[:args].size == 1
8
8
  require opts[:config]
9
9
  domain = opts[:args].first
10
- unless storage = Config.instance.vhosts[domain]
10
+ unless storage = Config.instance.vhosts[domain].storage rescue nil
11
11
  raise "#{domain} virtual host not found in conf/config.rb"
12
12
  end
13
13
  unless storage.respond_to?(:create_schema)
data/lib/vines/config.rb CHANGED
@@ -31,7 +31,7 @@ module Vines
31
31
  dupes = names.uniq.size != names.size || (@vhosts.keys & names).any?
32
32
  raise "one host definition per domain allowed" if dupes
33
33
  names.each do |name|
34
- @vhosts.merge! Host.new(name, &block).to_hash
34
+ @vhosts[name] = Host.new(name, &block)
35
35
  end
36
36
  end
37
37
 
@@ -57,10 +57,36 @@ module Vines
57
57
  @ports.values
58
58
  end
59
59
 
60
+ # Return true if the domain is virtual hosted by this server.
60
61
  def vhost?(domain)
61
62
  @vhosts.key?(domain)
62
63
  end
63
64
 
65
+ # Return true if all JID's belong to components hosted by this server.
66
+ def component?(*jids)
67
+ !jids.flatten.index do |jid|
68
+ !component_password(JID.new(jid).domain)
69
+ end
70
+ end
71
+
72
+ # Return the password for the component or nil if it's not hosted here.
73
+ def component_password(domain)
74
+ host = @vhosts.values.find {|host| host.component?(domain) }
75
+ host.password(domain) if host
76
+ end
77
+
78
+ # Return true if all of the JID's are hosted by this server.
79
+ def local_jid?(*jids)
80
+ !jids.flatten.index do |jid|
81
+ !vhost?(JID.new(jid).domain)
82
+ end
83
+ end
84
+
85
+ # Return true if private XML fragment storage is enabled for this domain.
86
+ def private_storage?(domain)
87
+ @vhosts[domain].private_storage?
88
+ end
89
+
64
90
  # Returns true if server-to-server connections are allowed with the
65
91
  # given domain.
66
92
  def s2s?(domain)
@@ -73,160 +99,45 @@ module Vines
73
99
  @ports[name] or raise ArgumentError.new("no port named #{name}")
74
100
  end
75
101
 
76
- class Host
77
- def initialize(name, &block)
78
- @name, @storage, @ldap = name, nil, nil
79
- instance_eval(&block)
80
- end
81
-
82
- def storage(name, &block)
83
- raise "one storage mechanism per host allowed" if @storage
84
- @storage = Storage.from_name(name, &block)
85
- @storage.ldap = @ldap
86
- end
87
-
88
- def ldap(host='localhost', port=636, &block)
89
- @ldap = Storage::Ldap.new(host, port, &block)
90
- @storage.ldap = @ldap if @storage
91
- end
92
-
93
- def to_hash
94
- raise "storage required for #{@name}" unless @storage
95
- {@name => @storage}
96
- end
97
- end
98
-
99
- class Port
100
- include Vines::Log
101
-
102
- attr_reader :config, :stream
103
-
104
- %w[host port].each do |name|
105
- define_method(name) do
106
- @settings[name.to_sym]
107
- end
108
- end
109
-
110
- def initialize(config, host, port, &block)
111
- @config, @settings = config, {}
112
- instance_eval(&block) if block
113
- defaults = {:host => host, :port => port,
114
- :max_resources_per_account => 5, :max_stanza_size => 128 * 1024}
115
- @settings = defaults.merge(@settings)
116
- end
117
-
118
- def max_stanza_size(max=nil)
119
- if max
120
- # rfc 6120 section 13.12
121
- @settings[:max_stanza_size] = [10000, max].max
122
- else
123
- @settings[:max_stanza_size]
124
- end
125
- end
126
-
127
- def start
128
- type = stream.name.split('::').last.downcase
129
- log.info("Accepting #{type} connections on #{host}:#{port}")
130
- EventMachine::start_server(host, port, stream, config)
131
- end
102
+ # Return true if the two JID's are allowed to send messages to each other.
103
+ # Both domains must have enabled cross_domain_messages in their config files.
104
+ def allowed?(to, from)
105
+ to, from = JID.new(to), JID.new(from)
106
+ return false if to.empty? || from.empty?
107
+ return true if to.domain == from.domain # same domain always allowed
108
+ return cross_domain?(to, from) if local_jid?(to, from) # both virtual hosted here
109
+ return check_components(to, from) if component?(to, from) # component to component
110
+ return check_component(to, from) if component?(to) # to component
111
+ return check_component(from, to) if component?(from) # from component
112
+ return cross_domain?(to) if local_jid?(to) # from is remote
113
+ return cross_domain?(from) if local_jid?(from) # to is remote
114
+ return false
132
115
  end
133
116
 
134
- class ClientPort < Port
135
- def initialize(config, host='0.0.0.0', port=5222, &block)
136
- @stream = Vines::Stream::Client
137
- super(config, host, port, &block)
138
- end
139
-
140
- def max_resources_per_account(max=nil)
141
- if max
142
- @settings[:max_resources_per_account] = max
143
- else
144
- @settings[:max_resources_per_account]
145
- end
146
- end
117
+ private
147
118
 
148
- def private_storage(enabled)
149
- @settings[:private_storage] = !!enabled
150
- end
151
-
152
- def private_storage?
153
- @settings[:private_storage]
154
- end
119
+ def check_components(to, from)
120
+ comp1, comp2 = strip_domain(to), strip_domain(from)
121
+ (comp1 == comp2) || cross_domain?(comp1, comp2)
155
122
  end
156
123
 
157
- class ServerPort < Port
158
- def initialize(config, host='0.0.0.0', port=5269, &block)
159
- @hosts, @stream = [], Vines::Stream::Server
160
- super(config, host, port, &block)
161
- end
162
-
163
- def hosts(*hosts)
164
- if hosts.any?
165
- @hosts << hosts
166
- @hosts.flatten!
167
- else
168
- @hosts
169
- end
170
- end
124
+ def check_component(component_jid, jid)
125
+ comp = strip_domain(component_jid)
126
+ return true if comp.domain == jid.domain
127
+ local_jid?(jid) ? cross_domain?(comp, jid) : cross_domain?(comp)
171
128
  end
172
129
 
173
- class HttpPort < Port
174
- def initialize(config, host='0.0.0.0', port=5280, &block)
175
- @stream = Vines::Stream::Http
176
- super(config, host, port, &block)
177
- defaults = {:root => File.expand_path('web'), :bind => '/xmpp'}
178
- @settings = defaults.merge(@settings)
179
- end
180
-
181
- def max_resources_per_account(max=nil)
182
- if max
183
- @settings[:max_resources_per_account] = max
184
- else
185
- @settings[:max_resources_per_account]
186
- end
187
- end
188
-
189
- def private_storage(enabled)
190
- @settings[:private_storage] = !!enabled
191
- end
192
-
193
- def private_storage?
194
- @settings[:private_storage]
195
- end
196
-
197
- def root(dir=nil)
198
- if dir
199
- @settings[:root] = File.expand_path(dir)
200
- else
201
- @settings[:root]
202
- end
203
- end
204
-
205
- def bind(url=nil)
206
- if url
207
- @settings[:bind] = url
208
- else
209
- @settings[:bind]
210
- end
211
- end
130
+ # Return the JID's domain with the first subdomain stripped off. For example,
131
+ # alice@tea.wonderland.lit returns wonderland.lit.
132
+ def strip_domain(jid)
133
+ domain = jid.domain.split('.').drop(1).join('.')
134
+ JID.new(domain)
212
135
  end
213
136
 
214
- class ComponentPort < Port
215
- def initialize(config, host='0.0.0.0', port=5347, &block)
216
- @components, @stream = {}, Vines::Stream::Component
217
- super(config, host, port, &block)
218
- end
219
-
220
- def components(options=nil)
221
- if options
222
- @components = options
223
- else
224
- @components
225
- end
226
- end
227
-
228
- def password(component)
229
- @components[component]
137
+ # Return true if all JID's are allowed to exchange cross domain messages.
138
+ def cross_domain?(*jids)
139
+ !jids.flatten.index do |jid|
140
+ !@vhosts[jid.domain].cross_domain_messages?
230
141
  end
231
142
  end
232
143
  end
@@ -0,0 +1,85 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Config
5
+
6
+ # Provides the DSL methods for the virtual host definitions in the
7
+ # conf/config.rb file. Host instances can be accessed at runtime through
8
+ # the +Config#vhosts+ method.
9
+ class Host
10
+ def initialize(name, &block)
11
+ @name, @storage, @ldap = name.downcase, nil, nil
12
+ @cross_domain_messages = false
13
+ @private_storage = false
14
+ @components = {}
15
+ validate_domain(@name)
16
+ instance_eval(&block)
17
+ raise "storage required for #{@name}" unless @storage
18
+ end
19
+
20
+ def storage(name=nil, &block)
21
+ if name
22
+ raise "one storage mechanism per host allowed" if @storage
23
+ @storage = Storage.from_name(name, &block)
24
+ @storage.ldap = @ldap
25
+ else
26
+ @storage
27
+ end
28
+ end
29
+
30
+ def ldap(host='localhost', port=636, &block)
31
+ @ldap = Storage::Ldap.new(host, port, &block)
32
+ @storage.ldap = @ldap if @storage
33
+ end
34
+
35
+ def cross_domain_messages(enabled)
36
+ @cross_domain_messages = !!enabled
37
+ end
38
+
39
+ def cross_domain_messages?
40
+ @cross_domain_messages
41
+ end
42
+
43
+ def components(options=nil)
44
+ return @components unless options
45
+
46
+ names = options.keys.map {|domain| "#{domain}.#{@name}".downcase }
47
+ dupes = names.uniq.size != names.size || (@components.keys & names).any?
48
+ raise "duplicate component domains not allowed" if dupes
49
+
50
+ options.each do |domain, password|
51
+ raise 'component domain required' if (domain || '').to_s.strip.empty?
52
+ raise 'component password required' if (password || '').strip.empty?
53
+ name = "#{domain}.#{@name}".downcase
54
+ raise "components must be one level below their host: #{name}" if domain.to_s.include?('.')
55
+ validate_domain(name)
56
+ @components[name] = password
57
+ end
58
+ end
59
+
60
+ def component?(domain)
61
+ !!@components[domain]
62
+ end
63
+
64
+ def password(domain)
65
+ @components[domain]
66
+ end
67
+
68
+ def private_storage(enabled)
69
+ @private_storage = !!enabled
70
+ end
71
+
72
+ def private_storage?
73
+ @private_storage
74
+ end
75
+
76
+ private
77
+
78
+ # Prevent domains in config files that won't form valid JID's.
79
+ def validate_domain(name)
80
+ jid = JID.new(name)
81
+ raise "incorrect domain: #{name}" if jid.node || jid.resource
82
+ end
83
+ end
84
+ end
85
+ end