vines 0.4.5 → 0.4.6

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 (89) hide show
  1. data/Gemfile +3 -0
  2. data/README.md +48 -0
  3. data/Rakefile +6 -58
  4. data/bin/vines +12 -2
  5. data/conf/certs/ca-bundle.crt +568 -39
  6. data/conf/config.rb +9 -42
  7. data/lib/vines.rb +1 -8
  8. data/lib/vines/command/cert.rb +4 -4
  9. data/lib/vines/command/init.rb +3 -3
  10. data/lib/vines/config.rb +9 -0
  11. data/lib/vines/kit.rb +2 -7
  12. data/lib/vines/storage/local.rb +50 -17
  13. data/lib/vines/store.rb +6 -4
  14. data/lib/vines/stream.rb +1 -1
  15. data/lib/vines/stream/http/session.rb +0 -0
  16. data/lib/vines/stream/http/sessions.rb +0 -0
  17. data/lib/vines/stream/parser.rb +3 -2
  18. data/lib/vines/token_bucket.rb +19 -10
  19. data/lib/vines/version.rb +1 -1
  20. data/test/cluster/publisher_test.rb +45 -33
  21. data/test/cluster/sessions_test.rb +32 -39
  22. data/test/cluster/subscriber_test.rb +93 -78
  23. data/test/config/host_test.rb +2 -4
  24. data/test/config/pubsub_test.rb +132 -126
  25. data/test/config_test.rb +2 -4
  26. data/test/contact_test.rb +80 -66
  27. data/test/error_test.rb +54 -55
  28. data/test/jid_test.rb +1 -2
  29. data/test/kit_test.rb +22 -17
  30. data/test/router_test.rb +187 -146
  31. data/test/stanza/iq/disco_info_test.rb +59 -59
  32. data/test/stanza/iq/disco_items_test.rb +36 -34
  33. data/test/stanza/iq/private_storage_test.rb +138 -143
  34. data/test/stanza/iq/roster_test.rb +198 -175
  35. data/test/stanza/iq/session_test.rb +17 -18
  36. data/test/stanza/iq/vcard_test.rb +117 -116
  37. data/test/stanza/iq/version_test.rb +47 -46
  38. data/test/stanza/iq_test.rb +53 -49
  39. data/test/stanza/message_test.rb +92 -89
  40. data/test/stanza/presence/probe_test.rb +2 -5
  41. data/test/stanza/presence/subscribe_test.rb +67 -54
  42. data/test/stanza/pubsub/create_test.rb +86 -108
  43. data/test/stanza/pubsub/delete_test.rb +141 -114
  44. data/test/stanza/pubsub/publish_test.rb +256 -320
  45. data/test/stanza/pubsub/subscribe_test.rb +169 -150
  46. data/test/stanza/pubsub/unsubscribe_test.rb +111 -142
  47. data/test/stanza_test.rb +61 -54
  48. data/test/storage/ldap_test.rb +1 -2
  49. data/test/storage/local_test.rb +3 -5
  50. data/test/storage/null_test.rb +3 -4
  51. data/test/storage/storage_tests.rb +1 -3
  52. data/test/storage_test.rb +1 -2
  53. data/test/store_test.rb +1 -2
  54. data/test/stream/client/auth_test.rb +61 -63
  55. data/test/stream/client/ready_test.rb +7 -8
  56. data/test/stream/client/session_test.rb +19 -18
  57. data/test/stream/component/handshake_test.rb +40 -37
  58. data/test/stream/component/ready_test.rb +76 -61
  59. data/test/stream/component/start_test.rb +7 -8
  60. data/test/stream/http/auth_test.rb +3 -4
  61. data/test/stream/http/ready_test.rb +52 -60
  62. data/test/stream/http/request_test.rb +1 -3
  63. data/test/stream/http/sessions_test.rb +2 -3
  64. data/test/stream/http/start_test.rb +3 -4
  65. data/test/stream/parser_test.rb +3 -4
  66. data/test/stream/sasl_test.rb +105 -86
  67. data/test/stream/server/auth_test.rb +40 -36
  68. data/test/stream/server/outbound/auth_test.rb +3 -4
  69. data/test/stream/server/ready_test.rb +51 -51
  70. data/test/test_helper.rb +42 -0
  71. data/test/token_bucket_test.rb +38 -18
  72. data/test/user_test.rb +79 -49
  73. data/vines.gemspec +33 -0
  74. data/web/chat/javascripts/app.js +1 -1
  75. data/web/lib/coffeescripts/layout.coffee +1 -1
  76. data/web/lib/javascripts/base.js +10 -10
  77. data/web/lib/javascripts/jquery.js +4 -4
  78. metadata +31 -128
  79. data/README +0 -35
  80. data/lib/vines/storage/couchdb.rb +0 -129
  81. data/lib/vines/storage/mongodb.rb +0 -132
  82. data/lib/vines/storage/redis.rb +0 -127
  83. data/lib/vines/storage/sql.rb +0 -220
  84. data/test/rake_test_loader.rb +0 -17
  85. data/test/storage/couchdb_test.rb +0 -107
  86. data/test/storage/mock_mongo.rb +0 -40
  87. data/test/storage/mongodb_test.rb +0 -81
  88. data/test/storage/redis_test.rb +0 -51
  89. data/test/storage/sql_test.rb +0 -62
@@ -8,6 +8,11 @@ Vines::Config.configure do
8
8
  # level logs all XML sent and received by the server.
9
9
  log :info
10
10
 
11
+ # Set the directory in which to look for virtual hosts' TLS certificates.
12
+ # This is optional and defaults to the conf/certs directory created during
13
+ # `vines init`.
14
+ certs 'conf/certs'
15
+
11
16
  # Each host element below is a virtual host domain name that this server will
12
17
  # service. Hosts can share storage configurations or use separate databases.
13
18
  # TLS encryption is mandatory so each host must have a <domain>.crt and
@@ -16,6 +21,10 @@ Vines::Config.configure do
16
21
  # command. Change the example, 'wonderland.lit', domain name to your actual
17
22
  # domain.
18
23
  #
24
+ # The vines gem is distributed with a single 'fs' filesystem storage backend.
25
+ # Additional database support is provided by the vines-sql, vines-redis,
26
+ # vines-couchdb, and vines-mongodb gems.
27
+ #
19
28
  # The private_storage attribute allows clients to store XML fragments
20
29
  # on the server, using the XEP-0049 Private XML Storage feature.
21
30
  #
@@ -125,45 +134,3 @@ Vines::Config.configure do
125
134
  # password ''
126
135
  #end
127
136
  end
128
-
129
- # Available storage implementations:
130
-
131
- #storage 'fs' do
132
- # dir 'data'
133
- #end
134
-
135
- #storage 'couchdb' do
136
- # host 'localhost'
137
- # port 6984
138
- # database 'xmpp'
139
- # tls true
140
- # username ''
141
- # password ''
142
- #end
143
-
144
- #storage 'mongodb' do
145
- # host 'localhost', 27017
146
- # host 'localhost', 27018 # optional, connects to replica set
147
- # database 'xmpp'
148
- # tls true
149
- # username ''
150
- # password ''
151
- # pool 5
152
- #end
153
-
154
- #storage 'redis' do
155
- # host 'localhost'
156
- # port 6379
157
- # database 0
158
- # password ''
159
- #end
160
-
161
- #storage 'sql' do
162
- # adapter 'postgresql'
163
- # host 'localhost'
164
- # port 5432
165
- # database 'xmpp'
166
- # username ''
167
- # password ''
168
- # pool 5
169
- #end
@@ -55,25 +55,22 @@ module Vines
55
55
  end
56
56
 
57
57
  %w[
58
- active_record
59
58
  base64
60
59
  bcrypt
61
60
  digest/sha1
62
- em-http
63
61
  em-hiredis
64
62
  eventmachine
65
63
  fiber
66
64
  fileutils
67
65
  http/parser
66
+ json
68
67
  logger
69
- mongo
70
68
  net/ldap
71
69
  nokogiri
72
70
  openssl
73
71
  resolv
74
72
  set
75
73
  socket
76
- uri
77
74
  yaml
78
75
 
79
76
  vines/log
@@ -110,13 +107,9 @@ end
110
107
  vines/stanza/pubsub/unsubscribe
111
108
 
112
109
  vines/storage
113
- vines/storage/couchdb
114
110
  vines/storage/ldap
115
111
  vines/storage/local
116
- vines/storage/mongodb
117
112
  vines/storage/null
118
- vines/storage/redis
119
- vines/storage/sql
120
113
 
121
114
  vines/config
122
115
  vines/config/host
@@ -5,8 +5,8 @@ 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('../certs', opts[:config])
9
- create_cert(opts[:args].first, dir)
8
+ require opts[:config]
9
+ create_cert(opts[:args].first, Config.instance.certs)
10
10
  end
11
11
 
12
12
  def create_cert(domain, dir)
@@ -33,9 +33,9 @@ module Vines
33
33
 
34
34
  cert.sign(key, OpenSSL::Digest::SHA1.new)
35
35
 
36
- {'key' => key, 'crt' => cert}.each_pair do |ext, o|
36
+ {'key' => key, 'crt' => cert}.each_pair do |ext, o|
37
37
  name = File.join(dir, "#{domain}.#{ext}")
38
- File.open(name, "w") {|f| f.write(o.to_pem) }
38
+ File.open(name, 'w:utf-8') {|f| f.write(o.to_pem) }
39
39
  File.chmod(0600, name) if ext == 'key'
40
40
  end
41
41
  end
@@ -36,8 +36,8 @@ module Vines
36
36
 
37
37
  def update_config(domain, dir)
38
38
  config = File.expand_path('conf/config.rb', dir)
39
- text = File.read(config)
40
- File.open(config, 'w') do |f|
39
+ text = File.read(config, encoding: 'utf-8')
40
+ File.open(config, 'w:utf-8') do |f|
41
41
  f.write(text.gsub('wonderland.lit', domain))
42
42
  end
43
43
  end
@@ -65,4 +65,4 @@ module Vines
65
65
  end
66
66
  end
67
67
  end
68
- end
68
+ end
@@ -21,11 +21,20 @@ module Vines
21
21
  end
22
22
 
23
23
  def initialize(&block)
24
+ @certs = File.expand_path('conf/certs')
24
25
  @vhosts, @ports, @cluster = {}, {}, nil
25
26
  @null = Storage::Null.new
26
27
  @router = Router.new(self)
27
28
  instance_eval(&block)
28
29
  raise "must define at least one virtual host" if @vhosts.empty?
30
+
31
+ unless @certs && File.directory?(@certs) && File.readable?(@certs)
32
+ raise 'Must provide a readable certs directory'
33
+ end
34
+ end
35
+
36
+ def certs(dir=nil)
37
+ dir ? @certs = File.expand_path(dir) : @certs
29
38
  end
30
39
 
31
40
  def host(*names, &block)
@@ -12,17 +12,12 @@ module Vines
12
12
  # Generates a random uuid per rfc 4122 that's useful for including in
13
13
  # stream, iq, and other xmpp stanzas.
14
14
  def self.uuid
15
- hex = (0...16).map { "%02x" % rand(256) }.join
16
- hex[12] = '4'
17
- hex[16] = %w[8 9 a b][rand(4)]
18
- hex.scan(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/).first.join('-')
15
+ SecureRandom.uuid
19
16
  end
20
17
 
21
18
  # Generates a random 128 character authentication token.
22
19
  def self.auth_token
23
- hash = Digest::SHA512.new
24
- 1024.times { hash << rand.to_s }
25
- hash.hexdigest
20
+ SecureRandom.hex(64)
26
21
  end
27
22
  end
28
23
  end
@@ -27,8 +27,8 @@ module Vines
27
27
 
28
28
  def find_user(jid)
29
29
  jid = JID.new(jid).bare.to_s
30
- file = absolute_path("user/#{jid}") unless jid.empty?
31
- record = YAML.load_file(file) rescue nil
30
+ file = "user/#{jid}" unless jid.empty?
31
+ record = YAML.load(read(file)) rescue nil
32
32
  return User.new(jid: jid).tap do |user|
33
33
  user.name, user.password = record.values_at('name', 'password')
34
34
  (record['roster'] || {}).each_pair do |jid, props|
@@ -47,43 +47,48 @@ module Vines
47
47
  user.roster.each do |contact|
48
48
  record['roster'][contact.jid.bare.to_s] = contact.to_h
49
49
  end
50
- save("user/#{user.jid.bare}") do |f|
51
- YAML.dump(record, f)
52
- end
50
+ save("user/#{user.jid.bare}", YAML.dump(record))
53
51
  end
54
52
 
55
53
  def find_vcard(jid)
56
54
  jid = JID.new(jid).bare.to_s
57
55
  return if jid.empty?
58
- file = absolute_path("vcard/#{jid}")
59
- Nokogiri::XML(File.read(file)).root rescue nil
56
+ file = "vcard/#{jid}"
57
+ Nokogiri::XML(read(file)).root rescue nil
60
58
  end
61
59
 
62
60
  def save_vcard(jid, card)
63
61
  jid = JID.new(jid).bare.to_s
64
62
  return if jid.empty?
65
- save("vcard/#{jid}") do |f|
66
- f.write(card.to_xml)
67
- end
63
+ save("vcard/#{jid}", card.to_xml)
68
64
  end
69
65
 
70
66
  def find_fragment(jid, node)
71
67
  jid = JID.new(jid).bare.to_s
72
68
  return if jid.empty?
73
- file = absolute_path("fragment/#{fragment_id(jid, node)}")
74
- Nokogiri::XML(File.read(file)).root rescue nil
69
+ file = 'fragment/%s' % fragment_id(jid, node)
70
+ Nokogiri::XML(read(file)).root rescue nil
75
71
  end
76
72
 
77
73
  def save_fragment(jid, node)
78
74
  jid = JID.new(jid).bare.to_s
79
75
  return if jid.empty?
80
- save("fragment/#{fragment_id(jid, node)}") do |f|
81
- f.write(node.to_xml)
82
- end
76
+ file = 'fragment/%s' % fragment_id(jid, node)
77
+ save(file, node.to_xml)
83
78
  end
84
79
 
85
80
  private
86
81
 
82
+ # Resolves a relative file name into an absolute path inside the
83
+ # storage directory.
84
+ #
85
+ # file - A fully-qualified or relative file name String.
86
+ #
87
+ # Returns the fully-qualified file path String.
88
+ #
89
+ # Raises RuntimeError if the resolved path is outside of the storage
90
+ # directory. This prevents directory path traversals with maliciously
91
+ # crafted JIDs.
87
92
  def absolute_path(file)
88
93
  File.expand_path(file, @dir).tap do |absolute|
89
94
  parent = File.dirname(File.dirname(absolute))
@@ -91,12 +96,40 @@ module Vines
91
96
  end
92
97
  end
93
98
 
94
- def save(file)
99
+ # Read the file from the filesystem and return its contents as a String.
100
+ # All files are assumed to be encoded as UTF-8.
101
+ #
102
+ # file - A fully-qualified or relative file name String.
103
+ #
104
+ # Returns the file content as a UTF-8 encoded String.
105
+ def read(file)
106
+ file = absolute_path(file)
107
+ File.read(file, encoding: 'utf-8')
108
+ end
109
+
110
+ # Write the content to the file. Make sure to consistently encode files
111
+ # we read and write as UTF-8.
112
+ #
113
+ # file - A fully-qualified or relative file name String.
114
+ # content - The String to write.
115
+ #
116
+ # Returns nothing.
117
+ def save(file, content)
95
118
  file = absolute_path(file)
96
- File.open(file, 'w') {|f| yield f }
119
+ File.open(file, 'w:utf-8') {|f| f.write(content) }
97
120
  File.chmod(0600, file)
98
121
  end
99
122
 
123
+ # Generates a unique file id for the user's private XML fragment.
124
+ #
125
+ # Private XML fragment storage needs to uniquely identify fragment files
126
+ # on disk. We combine the user's JID with a SHA-1 hash of the element's
127
+ # name and namespace to avoid special characters in the file name.
128
+ #
129
+ # jid - A bare JID identifying the user who owns this fragment.
130
+ # node - A Nokogiri::XML::Node for the XML to be stored.
131
+ #
132
+ # Returns an id String suitable for use in a file name.
100
133
  def fragment_id(jid, node)
101
134
  id = Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
102
135
  "#{jid}-#{id}"
@@ -43,10 +43,12 @@ module Vines
43
43
  unless @@sources
44
44
  pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
45
45
  pairs = Dir[File.join(@dir, '*.crt')].map do |name|
46
- pems = File.read(name).scan(pattern)
47
- certs = pems.map {|pem| OpenSSL::X509::Certificate.new(pem) }
48
- certs.reject! {|cert| cert.not_after < Time.now }
49
- [name, certs]
46
+ File.open(name, "r:UTF-8") do |f|
47
+ pems = f.read.scan(pattern)
48
+ certs = pems.map {|pem| OpenSSL::X509::Certificate.new(pem) }
49
+ certs.reject! {|cert| cert.not_after < Time.now }
50
+ [name, certs]
51
+ end
50
52
  end
51
53
  @@sources = Hash[pairs]
52
54
  end
@@ -21,7 +21,7 @@ module Vines
21
21
  @remote_addr, @local_addr = addresses
22
22
  @user, @closed, @stanza_size = nil, false, 0
23
23
  @bucket = TokenBucket.new(100, 10)
24
- @store = Store.new(File.join(VINES_ROOT, 'conf', 'certs'))
24
+ @store = Store.new(@config.certs)
25
25
  @nodes = EM::Queue.new
26
26
  process_node_queue
27
27
  create_parser
File without changes
File without changes
@@ -66,12 +66,13 @@ module Vines
66
66
  def node(name, attrs=[], prefix=nil, uri=nil, ns=[])
67
67
  ignore = stream?(name, uri) ? [] : IGNORE
68
68
  doc = @node ? @node.document : Document.new
69
- doc.create_element(name) do |node|
69
+ node = doc.create_element(name) do |node|
70
70
  attrs.each {|attr| node[attr.localname] = attr.value }
71
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
72
  doc << node unless @node
74
73
  end
74
+ node.namespace = node.add_namespace(prefix, uri) unless ignore.include?(uri)
75
+ node
75
76
  end
76
77
  end
77
78
  end
@@ -9,8 +9,12 @@ module Vines
9
9
  # of operations.
10
10
  class TokenBucket
11
11
 
12
- # Create a full bucket with capacity number of tokens to be filled
12
+ # Create a full bucket with `capacity` number of tokens to be filled
13
13
  # at the given rate of tokens/second.
14
+ #
15
+ # capacity - The Fixnum maximum number of tokens the bucket can hold.
16
+ # rate - The Fixnum number of tokens per second at which the bucket is
17
+ # refilled.
14
18
  def initialize(capacity, rate)
15
19
  raise ArgumentError.new('capacity must be > 0') unless capacity > 0
16
20
  raise ArgumentError.new('rate must be > 0') unless rate > 0
@@ -20,24 +24,29 @@ module Vines
20
24
  @timestamp = Time.new
21
25
  end
22
26
 
23
- # Returns true if tokens can be taken from the bucket.
27
+ # Remove tokens from the bucket if it's full enough. There's no way, or
28
+ # need, to add tokens to the bucket. It refills over time.
29
+ #
30
+ # tokens - The Fixnum number of tokens to attempt to take from the bucket.
31
+ #
32
+ # Returns true if the bucket contains enough tokens to take, false if the
33
+ # bucket isn't full enough to satisy the request.
24
34
  def take(tokens)
25
35
  raise ArgumentError.new('tokens must be > 0') unless tokens > 0
26
- if tokens <= fill
27
- @tokens -= tokens
28
- true
29
- else
30
- false
31
- end
36
+ tokens <= fill ? @tokens -= tokens : false
32
37
  end
33
38
 
34
39
  private
35
40
 
41
+ # Add tokens to the bucket at the `rate` provided in the constructor. This
42
+ # fills the bucket slowly over time.
43
+ #
44
+ # Returns the Fixnum number of tokens left in the bucket.
36
45
  def fill
37
46
  if @tokens < @capacity
38
47
  now = Time.new
39
- delta = (@rate * (now - @timestamp)).round
40
- @tokens = [@capacity, @tokens + delta].min
48
+ @tokens += (@rate * (now - @timestamp)).round
49
+ @tokens = @capacity if @tokens > @capacity
41
50
  @timestamp = now
42
51
  end
43
52
  @tokens
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Vines
4
- VERSION = '0.4.5'
4
+ VERSION = '0.4.6'
5
5
  end
@@ -1,45 +1,57 @@
1
1
  # encoding: UTF-8
2
2
 
3
- require 'vines'
4
- require 'minitest/autorun'
5
-
6
- class ClusterPublisherTest < MiniTest::Unit::TestCase
7
- def setup
8
- @connection = MiniTest::Mock.new
9
- @cluster = MiniTest::Mock.new
10
- @cluster.expect(:id, 'abc')
11
- @cluster.expect(:connection, @connection)
12
- end
3
+ require 'test_helper'
4
+
5
+ describe Vines::Cluster::Publisher do
6
+ subject { Vines::Cluster::Publisher.new(cluster) }
7
+ let(:connection) { MiniTest::Mock.new }
8
+ let(:cluster) { MiniTest::Mock.new }
13
9
 
14
- def test_broadcast
15
- msg = {from: 'abc', type: 'online', time: Time.now.to_i}.to_json
16
- @connection.expect(:publish, nil, ["cluster:nodes:all", msg])
10
+ before do
11
+ cluster.expect :id, 'abc'
12
+ cluster.expect :connection, connection
13
+ end
17
14
 
18
- publisher = Vines::Cluster::Publisher.new(@cluster)
19
- publisher.broadcast(:online)
20
- assert @connection.verify
21
- assert @cluster.verify
15
+ describe '#broadcast' do
16
+ before do
17
+ msg = {from: 'abc', type: 'online', time: Time.now.to_i}.to_json
18
+ connection.expect :publish, nil, ["cluster:nodes:all", msg]
19
+ end
20
+
21
+ it 'publishes the message to every cluster node' do
22
+ subject.broadcast(:online)
23
+ connection.verify
24
+ cluster.verify
25
+ end
22
26
  end
23
27
 
24
- def test_route
25
- stanza = "<message>hello</message>"
26
- msg = {from: 'abc', type: 'stanza', stanza: stanza}.to_json
27
- @connection.expect(:publish, nil, ["cluster:nodes:node-42", msg])
28
+ describe '#route' do
29
+ let(:stanza) { "<message>hello</message>" }
28
30
 
29
- publisher = Vines::Cluster::Publisher.new(@cluster)
30
- publisher.route(stanza, "node-42")
31
- assert @connection.verify
32
- assert @cluster.verify
31
+ before do
32
+ msg = {from: 'abc', type: 'stanza', stanza: stanza}.to_json
33
+ connection.expect :publish, nil, ["cluster:nodes:node-42", msg]
34
+ end
35
+
36
+ it 'publishes the message to just one cluster node' do
37
+ subject.route(stanza, "node-42")
38
+ connection.verify
39
+ cluster.verify
40
+ end
33
41
  end
34
42
 
35
- def test_update_user
36
- jid = Vines::JID.new('alice@wonderland.lit')
37
- msg = {from: 'abc', type: 'user', jid: jid.to_s}.to_json
38
- @connection.expect(:publish, nil, ["cluster:nodes:node-42", msg])
43
+ describe '#update_user' do
44
+ let(:jid) { Vines::JID.new('alice@wonderland.lit') }
45
+
46
+ before do
47
+ msg = {from: 'abc', type: 'user', jid: jid.to_s}.to_json
48
+ connection.expect :publish, nil, ["cluster:nodes:node-42", msg]
49
+ end
39
50
 
40
- publisher = Vines::Cluster::Publisher.new(@cluster)
41
- publisher.update_user(jid, "node-42")
42
- assert @connection.verify
43
- assert @cluster.verify
51
+ it 'publishes the new user to just one cluster node' do
52
+ subject.update_user(jid, "node-42")
53
+ connection.verify
54
+ cluster.verify
55
+ end
44
56
  end
45
57
  end