vines 0.4.5 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
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