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
@@ -0,0 +1,111 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Config
5
+ class Port
6
+ include Vines::Log
7
+
8
+ attr_reader :config, :stream
9
+
10
+ %w[host port].each do |name|
11
+ define_method(name) do
12
+ @settings[name.to_sym]
13
+ end
14
+ end
15
+
16
+ def initialize(config, host, port, &block)
17
+ @config, @settings = config, {}
18
+ instance_eval(&block) if block
19
+ defaults = {:host => host, :port => port,
20
+ :max_resources_per_account => 5, :max_stanza_size => 128 * 1024}
21
+ @settings = defaults.merge(@settings)
22
+ end
23
+
24
+ def max_stanza_size(max=nil)
25
+ if max
26
+ # rfc 6120 section 13.12
27
+ @settings[:max_stanza_size] = [10000, max].max
28
+ else
29
+ @settings[:max_stanza_size]
30
+ end
31
+ end
32
+
33
+ def start
34
+ type = stream.name.split('::').last.downcase
35
+ log.info("Accepting #{type} connections on #{host}:#{port}")
36
+ EventMachine::start_server(host, port, stream, config)
37
+ end
38
+ end
39
+
40
+ class ClientPort < Port
41
+ def initialize(config, host='0.0.0.0', port=5222, &block)
42
+ @stream = Vines::Stream::Client
43
+ super(config, host, port, &block)
44
+ end
45
+
46
+ def max_resources_per_account(max=nil)
47
+ if max
48
+ @settings[:max_resources_per_account] = max
49
+ else
50
+ @settings[:max_resources_per_account]
51
+ end
52
+ end
53
+ end
54
+
55
+ class ServerPort < Port
56
+ def initialize(config, host='0.0.0.0', port=5269, &block)
57
+ @hosts, @stream = [], Vines::Stream::Server
58
+ super(config, host, port, &block)
59
+ end
60
+
61
+ def hosts(*hosts)
62
+ if hosts.any?
63
+ @hosts << hosts
64
+ @hosts.flatten!
65
+ else
66
+ @hosts
67
+ end
68
+ end
69
+ end
70
+
71
+ class HttpPort < Port
72
+ def initialize(config, host='0.0.0.0', port=5280, &block)
73
+ @stream = Vines::Stream::Http
74
+ super(config, host, port, &block)
75
+ defaults = {:root => File.expand_path('web'), :bind => '/xmpp'}
76
+ @settings = defaults.merge(@settings)
77
+ end
78
+
79
+ def max_resources_per_account(max=nil)
80
+ if max
81
+ @settings[:max_resources_per_account] = max
82
+ else
83
+ @settings[:max_resources_per_account]
84
+ end
85
+ end
86
+
87
+ def root(dir=nil)
88
+ if dir
89
+ @settings[:root] = File.expand_path(dir)
90
+ else
91
+ @settings[:root]
92
+ end
93
+ end
94
+
95
+ def bind(url=nil)
96
+ if url
97
+ @settings[:bind] = url
98
+ else
99
+ @settings[:bind]
100
+ end
101
+ end
102
+ end
103
+
104
+ class ComponentPort < Port
105
+ def initialize(config, host='0.0.0.0', port=5347, &block)
106
+ @stream = Vines::Stream::Component
107
+ super(config, host, port, &block)
108
+ end
109
+ end
110
+ end
111
+ end
data/lib/vines/contact.rb CHANGED
@@ -9,7 +9,7 @@ module Vines
9
9
 
10
10
  def initialize(args={})
11
11
  @jid = JID.new(args[:jid]).bare
12
- raise ArgumentError, 'invalid jid' unless @jid.node && !@jid.domain.empty?
12
+ raise ArgumentError, 'invalid jid' if @jid.empty?
13
13
  @name = args[:name]
14
14
  @subscription = args[:subscription] || 'none'
15
15
  @ask = args[:ask]
data/lib/vines/jid.rb CHANGED
@@ -6,6 +6,12 @@ module Vines
6
6
 
7
7
  PATTERN = /^(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?$/.freeze
8
8
 
9
+ # http://tools.ietf.org/html/rfc6122#appendix-A
10
+ NODE_PREP = /[[:space:][:cntrl:]"&'\/:<>@]/.freeze
11
+
12
+ # http://tools.ietf.org/html/rfc3454#appendix-C
13
+ NAME_PREP = /[[:space:][:cntrl:]]/.freeze
14
+
9
15
  attr_reader :node, :domain, :resource
10
16
  attr_writer :resource
11
17
 
@@ -19,11 +25,9 @@ module Vines
19
25
  if @domain.nil? && @resource.nil?
20
26
  @node, @domain, @resource = @node.to_s.scan(PATTERN).first
21
27
  end
22
- [@node, @domain].each {|piece| piece.downcase! if piece }
28
+ [@node, @domain].each {|part| part.downcase! if part }
23
29
 
24
- [@node, @domain, @resource].each do |piece|
25
- raise ArgumentError, 'jid too long' if (piece || '').size > 1023
26
- end
30
+ validate
27
31
  end
28
32
 
29
33
  def bare
@@ -34,6 +38,10 @@ module Vines
34
38
  @resource.nil?
35
39
  end
36
40
 
41
+ def empty?
42
+ to_s == ''
43
+ end
44
+
37
45
  def <=>(jid)
38
46
  self.to_s <=> jid.to_s
39
47
  end
@@ -52,5 +60,19 @@ module Vines
52
60
  s = "#{s}/#{@resource}" if @resource
53
61
  s
54
62
  end
63
+
64
+ private
65
+
66
+ def validate
67
+ [@node, @domain, @resource].each do |part|
68
+ raise ArgumentError, 'jid too long' if (part || '').size > 1023
69
+ end
70
+ raise ArgumentError, 'empty node' if @node && @node.strip.empty?
71
+ raise ArgumentError, 'node contains invalid characters' if @node && @node =~ NODE_PREP
72
+ raise ArgumentError, 'empty resource' if @resource && @resource.strip.empty?
73
+ raise ArgumentError, 'resource contains invalid characters' if @resource && @resource =~ NAME_PREP
74
+ raise ArgumentError, 'empty domain' if @domain == '' && (@node || @resource)
75
+ raise ArgumentError, 'domain contains invalid characters' if @domain && @domain =~ NAME_PREP
76
+ end
55
77
  end
56
78
  end
data/lib/vines/kit.rb CHANGED
@@ -19,5 +19,11 @@ module Vines
19
19
  hex[16] = %w[8 9 a b][rand(4)]
20
20
  hex.scan(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/).first.join('-')
21
21
  end
22
+
23
+ def self.generate_password
24
+ hash = Digest::SHA512.new
25
+ 1024.times { hash << rand.to_s }
26
+ hash.hexdigest
27
+ end
22
28
  end
23
29
  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-%dT%H:%M:%SZ".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 CHANGED
@@ -6,7 +6,6 @@ module Vines
6
6
  # the 'to' attribute. Router is a singleton, shared by all streams, that must
7
7
  # be accessed with +Router.instance+, not +Router.new+.
8
8
  class Router
9
- ROUTABLE_STANZAS = %w[message iq presence].freeze
10
9
 
11
10
  STREAM_TYPES = [:client, :server, :component].freeze
12
11
  STREAM_TYPES.each do |name|
@@ -29,30 +28,32 @@ module Vines
29
28
  # Returns streams for all connected resources for this JID. A
30
29
  # resource is considered connected after it has completed authentication
31
30
  # and resource binding.
32
- def connected_resources(jid)
33
- jid = JID.new(jid)
31
+ def connected_resources(jid, from)
32
+ jid, from = JID.new(jid), JID.new(from)
34
33
  clients.select do |stream|
35
- stream.connected? && jid == (jid.bare? ? stream.user.jid.bare : stream.user.jid)
34
+ stream.connected? &&
35
+ jid == (jid.bare? ? stream.user.jid.bare : stream.user.jid) &&
36
+ @config.allowed?(jid, from)
36
37
  end
37
38
  end
38
39
 
39
40
  # Returns streams for all available resources for this JID. A
40
41
  # resource is marked available after it sends initial presence.
41
42
  # This method accepts a single JID or a list of JIDs.
42
- def available_resources(*jid)
43
- ids = jid.flatten.map {|jid| JID.new(jid).bare }
43
+ def available_resources(*jids, from)
44
+ jids = filter_allowed(jids, from)
44
45
  clients.select do |stream|
45
- stream.available? && ids.include?(stream.user.jid.bare)
46
+ stream.available? && jids.include?(stream.user.jid.bare)
46
47
  end
47
48
  end
48
49
 
49
50
  # Returns streams for all interested resources for this JID. A
50
51
  # resource is marked interested after it requests the roster.
51
52
  # This method accepts a single JID or a list of JIDs.
52
- def interested_resources(*jid)
53
- ids = jid.flatten.map {|jid| JID.new(jid).bare }
53
+ def interested_resources(*jids, from)
54
+ jids = filter_allowed(jids, from)
54
55
  clients.select do |stream|
55
- stream.interested? && ids.include?(stream.user.jid.bare)
56
+ stream.interested? && jids.include?(stream.user.jid.bare)
56
57
  end
57
58
  end
58
59
 
@@ -75,40 +76,24 @@ module Vines
75
76
  # or an external component stream.
76
77
  def route(stanza)
77
78
  to, from = %w[to from].map {|attr| JID.new(stanza[attr]) }
78
- if stream = connection_to(to.domain)
79
+ return unless @config.allowed?(to, from)
80
+ key = [to.domain, from.domain]
81
+
82
+ if stream = connection_to(to, from)
79
83
  stream.write(stanza)
80
- elsif @pending.key?(to.domain)
81
- @pending[to.domain] << stanza
84
+ elsif @pending.key?(key)
85
+ @pending[key] << stanza
82
86
  elsif @config.s2s?(to.domain)
83
- @pending[to.domain] << stanza
87
+ @pending[key] << stanza
84
88
  Vines::Stream::Server.start(@config, to.domain, from.domain) do |stream|
85
- if stream
86
- @pending[to.domain].each {|s| stream.write(s) }
87
- else
88
- @pending[to.domain].each do |s|
89
- xml = StanzaErrors::RemoteServerNotFound.new(s, 'cancel').to_xml
90
- connected_resources(s['from']).each {|c| c.write(xml) }
91
- end
92
- end
93
- @pending.delete(to.domain)
89
+ stream ? send_pending(key, stream) : return_pending(key)
90
+ @pending.delete(key)
94
91
  end
95
92
  else
96
93
  raise StanzaErrors::RemoteServerNotFound.new(stanza, 'cancel')
97
94
  end
98
95
  end
99
96
 
100
- # Returns true if this stanza should be processed locally. Returns false
101
- # if it's destined for a remote domain or external component.
102
- def local?(stanza)
103
- return true unless ROUTABLE_STANZAS.include?(stanza.name)
104
- to = (stanza['to'] || '').strip
105
- to.empty? || local_jid?(to)
106
- end
107
-
108
- def local_jid?(jid)
109
- @config.vhost?(JID.new(jid).domain)
110
- end
111
-
112
97
  # Returns the total number of streams connected to the server.
113
98
  def size
114
99
  @streams.values.inject(0) {|sum, arr| sum + arr.size }
@@ -116,9 +101,56 @@ module Vines
116
101
 
117
102
  private
118
103
 
119
- def connection_to(domain)
120
- (components + servers).find do |stream|
121
- stream.ready? && stream.remote_domain == domain
104
+ # Write all pending stanzas for this domain to the stream. Called after a
105
+ # s2s stream has successfully connected and we need to dequeue all stanzas
106
+ # we received while waiting for the connection to finish.
107
+ def send_pending(key, stream)
108
+ @pending[key].each do |stanza|
109
+ stream.write(stanza)
110
+ end
111
+ end
112
+
113
+ # Return all pending stanzas to their senders as remote-server-not-found
114
+ # errors. Called after a s2s stream has failed to connect.
115
+ def return_pending(key)
116
+ @pending[key].each do |stanza|
117
+ to, from = JID.new(stanza['to']), JID.new(stanza['from'])
118
+ xml = StanzaErrors::RemoteServerNotFound.new(stanza, 'cancel').to_xml
119
+ if @config.component?(from)
120
+ connection_to(from, to).write(xml) rescue nil
121
+ else
122
+ connected_resources(from, to).each {|c| c.write(xml) }
123
+ end
124
+ end
125
+ end
126
+
127
+ # Return the bare JID's from the list that are allowed to talk to
128
+ # the +from+ JID. Store them in a Hash for fast +include?+ checks.
129
+ def filter_allowed(jids, from)
130
+ from = JID.new(from)
131
+ {}.tap do |ids|
132
+ jids.flatten.each do |jid|
133
+ jid = JID.new(jid).bare
134
+ ids[jid] = nil if @config.allowed?(jid, from)
135
+ end
136
+ end
137
+ end
138
+
139
+ def connection_to(to, from)
140
+ component_stream(to) || server_stream(to, from)
141
+ end
142
+
143
+ def component_stream(to)
144
+ components.find do |stream|
145
+ stream.ready? && stream.remote_domain == to.domain
146
+ end
147
+ end
148
+
149
+ def server_stream(to, from)
150
+ servers.find do |stream|
151
+ stream.ready? &&
152
+ stream.remote_domain == to.domain &&
153
+ stream.domain == from.domain
122
154
  end
123
155
  end
124
156
 
data/lib/vines/stanza.rb CHANGED
@@ -6,7 +6,13 @@ module Vines
6
6
 
7
7
  attr_reader :stream
8
8
 
9
+ EMPTY = ''.freeze
10
+ FROM = 'from'.freeze
9
11
  MESSAGE = 'message'.freeze
12
+ TO = 'to'.freeze
13
+
14
+ ROUTABLE_STANZAS = %w[message iq presence].freeze
15
+
10
16
  @@types = {}
11
17
 
12
18
  def self.register(xpath, ns={})
@@ -28,15 +34,23 @@ module Vines
28
34
  # Send the stanza to all recipients, stamping it with from and
29
35
  # to addresses first.
30
36
  def broadcast(recipients)
31
- @node['from'] = stream.user.jid.to_s
37
+ @node[FROM] = stream.user.jid.to_s
32
38
  recipients.each do |recipient|
33
- @node['to'] = recipient.user.jid.to_s
39
+ @node[TO] = recipient.user.jid.to_s
34
40
  recipient.write(@node)
35
41
  end
36
42
  end
37
43
 
44
+ # Returns true if this stanza should be processed locally. Returns false
45
+ # if it's destined for a remote domain or external component.
38
46
  def local?
39
- stream.router.local?(@node)
47
+ return true unless ROUTABLE_STANZAS.include?(@node.name)
48
+ to = JID.new(@node['to'])
49
+ to.empty? || local_jid?(to)
50
+ end
51
+
52
+ def local_jid?(*jids)
53
+ stream.config.local_jid?(*jids)
40
54
  end
41
55
 
42
56
  def route
@@ -59,12 +73,12 @@ module Vines
59
73
  # recipient's available resources. Route the stanza to a remote server if
60
74
  # the recipient isn't hosted locally.
61
75
  def send_unavailable(from, to)
62
- router.available_resources(from).each do |stream|
76
+ recipients = router.available_resources(to, from) if local_jid?(to)
77
+
78
+ router.available_resources(from, to).each do |stream|
63
79
  el = unavailable(stream.user.jid, to)
64
- if router.local_jid?(to)
65
- router.available_resources(to).each do |recipient|
66
- recipient.write(el)
67
- end
80
+ if local_jid?(to)
81
+ recipients.each {|recipient| recipient.write(el) }
68
82
  else
69
83
  router.route(el)
70
84
  end
@@ -81,8 +95,31 @@ module Vines
81
95
  'type' => 'unavailable')
82
96
  end
83
97
 
98
+ # Return nil if this stanza has no 'to' attribute. Return a Vines::JID
99
+ # if it contains a valid 'to' attribute. Raise a JidMalformed error if
100
+ # the JID is invalid.
101
+ def validate_to
102
+ validate_address(TO)
103
+ end
104
+
105
+ # Return nil if this stanza has no 'from' attribute. Return a Vines::JID
106
+ # if it contains a valid 'from' attribute. Raise a JidMalformed error if
107
+ # the JID is invalid.
108
+ def validate_from
109
+ validate_address(FROM)
110
+ end
111
+
84
112
  def method_missing(method, *args, &block)
85
113
  @node.send(method, *args, &block)
86
114
  end
115
+
116
+ private
117
+
118
+ def validate_address(attr)
119
+ jid = (self[attr] || EMPTY)
120
+ return if jid.empty?
121
+ JID.new(jid) rescue
122
+ raise StanzaErrors::JidMalformed.new(self, 'modify')
123
+ end
87
124
  end
88
125
  end