vines 0.1.1 → 0.2.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 (83) hide show
  1. data/README +2 -2
  2. data/Rakefile +63 -8
  3. data/bin/vines +0 -1
  4. data/conf/config.rb +16 -7
  5. data/lib/vines.rb +21 -16
  6. data/lib/vines/command/init.rb +5 -3
  7. data/lib/vines/config.rb +34 -0
  8. data/lib/vines/contact.rb +14 -0
  9. data/lib/vines/stanza.rb +26 -0
  10. data/lib/vines/stanza/iq.rb +1 -1
  11. data/lib/vines/stanza/iq/disco_info.rb +3 -0
  12. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  13. data/lib/vines/stanza/iq/roster.rb +26 -30
  14. data/lib/vines/stanza/presence.rb +0 -12
  15. data/lib/vines/stanza/presence/subscribe.rb +3 -20
  16. data/lib/vines/stanza/presence/subscribed.rb +9 -10
  17. data/lib/vines/stanza/presence/unsubscribe.rb +8 -15
  18. data/lib/vines/stanza/presence/unsubscribed.rb +8 -8
  19. data/lib/vines/storage.rb +28 -0
  20. data/lib/vines/storage/couchdb.rb +29 -0
  21. data/lib/vines/storage/local.rb +22 -0
  22. data/lib/vines/storage/redis.rb +26 -0
  23. data/lib/vines/storage/sql.rb +48 -5
  24. data/lib/vines/stream/client.rb +6 -8
  25. data/lib/vines/stream/http.rb +23 -21
  26. data/lib/vines/stream/http/auth.rb +1 -1
  27. data/lib/vines/stream/http/bind.rb +1 -1
  28. data/lib/vines/stream/http/bind_restart.rb +4 -3
  29. data/lib/vines/stream/http/ready.rb +1 -1
  30. data/lib/vines/stream/http/request.rb +94 -5
  31. data/lib/vines/stream/http/session.rb +8 -6
  32. data/lib/vines/version.rb +1 -1
  33. data/test/config_test.rb +12 -0
  34. data/test/contact_test.rb +40 -0
  35. data/test/rake_test_loader.rb +11 -3
  36. data/test/stanza/iq/private_storage_test.rb +177 -0
  37. data/test/stanza/iq/roster_test.rb +1 -1
  38. data/test/stanza/iq_test.rb +63 -0
  39. data/test/storage/couchdb_test.rb +7 -1
  40. data/test/storage/local_test.rb +8 -2
  41. data/test/storage/redis_test.rb +16 -7
  42. data/test/storage/sql_test.rb +8 -1
  43. data/test/storage/storage_tests.rb +50 -0
  44. data/test/stream/http/auth_test.rb +3 -0
  45. data/test/stream/http/ready_test.rb +3 -0
  46. data/test/stream/http/request_test.rb +86 -0
  47. data/test/stream/parser_test.rb +2 -0
  48. data/web/404.html +43 -0
  49. data/web/apple-touch-icon.png +0 -0
  50. data/web/chat/coffeescripts/chat.coffee +385 -0
  51. data/web/chat/coffeescripts/init.coffee +15 -0
  52. data/web/chat/coffeescripts/logout.coffee +5 -0
  53. data/web/chat/index.html +17 -0
  54. data/web/chat/javascripts/app.js +1 -0
  55. data/web/chat/javascripts/chat.js +436 -0
  56. data/web/chat/javascripts/init.js +21 -0
  57. data/web/chat/javascripts/logout.js +11 -0
  58. data/web/chat/stylesheets/chat.css +290 -0
  59. data/web/favicon.png +0 -0
  60. data/web/lib/coffeescripts/contact.coffee +32 -0
  61. data/web/lib/coffeescripts/layout.coffee +30 -0
  62. data/web/lib/coffeescripts/login.coffee +52 -0
  63. data/web/lib/coffeescripts/navbar.coffee +84 -0
  64. data/web/lib/coffeescripts/router.coffee +40 -0
  65. data/web/lib/coffeescripts/session.coffee +211 -0
  66. data/web/lib/images/default-user.png +0 -0
  67. data/web/lib/images/logo-large.png +0 -0
  68. data/web/lib/images/logo-small.png +0 -0
  69. data/web/lib/javascripts/base.js +9 -0
  70. data/web/lib/javascripts/contact.js +94 -0
  71. data/web/lib/javascripts/icons.js +101 -0
  72. data/web/lib/javascripts/jquery.cookie.js +91 -0
  73. data/web/lib/javascripts/jquery.js +18 -0
  74. data/web/lib/javascripts/layout.js +48 -0
  75. data/web/lib/javascripts/login.js +61 -0
  76. data/web/lib/javascripts/navbar.js +69 -0
  77. data/web/lib/javascripts/raphael.js +8 -0
  78. data/web/lib/javascripts/router.js +105 -0
  79. data/web/lib/javascripts/session.js +322 -0
  80. data/web/lib/javascripts/strophe.js +1 -0
  81. data/web/lib/stylesheets/base.css +223 -0
  82. data/web/lib/stylesheets/login.css +63 -0
  83. metadata +51 -9
data/README CHANGED
@@ -17,8 +17,8 @@ database. SSL encryption is mandatory on all client and server connections.
17
17
  == Dependencies
18
18
 
19
19
  * bcrypt-ruby >= 2.1.4
20
- * eventmachine >= 1.0.0
21
- * nokogiri >= 1.4.4
20
+ * eventmachine >= 0.12.10
21
+ * nokogiri >= 1.5.0
22
22
  * ruby >= 1.9.2
23
23
 
24
24
  == Ubuntu setup
data/Rakefile CHANGED
@@ -1,7 +1,8 @@
1
1
  require 'rake'
2
2
  require 'rake/clean'
3
- require 'rake/gempackagetask'
4
3
  require 'rake/testtask'
4
+ require 'rubygems/package_task'
5
+ require 'nokogiri'
5
6
  require_relative 'lib/vines/version'
6
7
 
7
8
  spec = Gem::Specification.new do |s|
@@ -21,28 +22,28 @@ all client and server connections."
21
22
  s.email = %w[david@negativecode.com chris@negativecode.com]
22
23
  s.homepage = "http://www.getvines.com"
23
24
 
24
- s.files = FileList['[A-Z]*', '{bin,lib,conf}/**/*']
25
+ s.files = FileList['[A-Z]*', '{bin,lib,conf,web}/**/*']
25
26
  s.test_files = FileList["test/**/*"]
26
27
  s.executables = %w[vines]
27
28
  s.require_path = "lib"
28
29
 
29
30
  s.add_dependency "activerecord", "~> 3.0"
30
31
  s.add_dependency "bcrypt-ruby", "~> 2.1"
31
- s.add_dependency 'em-http-request', '>= 1.0.0.beta.3'
32
+ s.add_dependency "em-http-request", "~> 0.3"
32
33
  s.add_dependency "em-redis", "~> 0.3"
33
- s.add_dependency "eventmachine", ">= 1.0.0.beta.3"
34
+ s.add_dependency "eventmachine", "~> 0.12"
34
35
  s.add_dependency "http_parser.rb", "~> 0.5"
35
36
  s.add_dependency "net-ldap", "~> 0.2"
36
- s.add_dependency "nokogiri", "~> 1.4"
37
+ s.add_dependency "nokogiri", "~> 1.5"
37
38
 
38
- s.add_development_dependency "minitest"
39
+ s.add_development_dependency "minitest", "= 2.2.2"
39
40
  s.add_development_dependency "rake"
40
41
  s.add_development_dependency "sqlite3"
41
42
 
42
43
  s.required_ruby_version = '>= 1.9.2'
43
44
  end
44
45
 
45
- Rake::GemPackageTask.new(spec) do |pkg|
46
+ Gem::PackageTask.new(spec) do |pkg|
46
47
  pkg.need_tar = true
47
48
  end
48
49
 
@@ -62,4 +63,58 @@ Rake::TestTask.new(:test) do |test|
62
63
  test.warning = false
63
64
  end
64
65
 
65
- task :default => [:clobber, :test, :gem]
66
+ # Find lib and chat js includes and return them as two arrays.
67
+ def scripts(doc)
68
+ lib, chat = [], []
69
+ doc.css('script').each do |node|
70
+ file = node['src'].split('/').last()
71
+ if node['src'].start_with?('/lib')
72
+ lib << file
73
+ elsif node['src'].start_with?('/chat')
74
+ chat << file
75
+ end
76
+ end
77
+ [lib, chat]
78
+ end
79
+
80
+ # Replace script tags with combined and minimized files.
81
+ def rewrite_js(doc)
82
+ doc.css('script').each {|node| node.remove }
83
+ doc.css('head').each do |node|
84
+ %w[/lib/javascripts/base.js /chat/javascripts/app.js].each do |src|
85
+ script = doc.create_element('script',
86
+ 'type' => 'text/javascript',
87
+ 'src' => src)
88
+ node.add_child(script)
89
+ node.add_child(doc.create_text_node("\n"))
90
+ end
91
+ end
92
+ end
93
+
94
+ task :compile do
95
+ index = 'web/chat/index.html'
96
+ doc = Nokogiri::HTML(File.read(index))
97
+ lib, chat = scripts(doc)
98
+
99
+ rewrite_js(doc)
100
+ # save index.html before rewriting
101
+ FileUtils.cp(index, '/tmp/index.html')
102
+ File.open(index, 'w') {|f| f.write(doc.to_xml(:indent => 2)) }
103
+
104
+ lib = lib.map {|f| "web/lib/javascripts/#{f}"}.join(' ')
105
+ chat = chat.map {|f| "web/chat/javascripts/#{f}"}.join(' ')
106
+
107
+ sh %{coffee -c -b -o web/chat/javascripts web/chat/coffeescripts/*.coffee}
108
+ sh %{cat #{chat} | uglifyjs -nc > web/chat/javascripts/app.js}
109
+
110
+ sh %{coffee -c -b -o web/lib/javascripts web/lib/coffeescripts/*.coffee}
111
+ sh %{cat #{lib} | uglifyjs -nc > web/lib/javascripts/base.js}
112
+ end
113
+
114
+ task :cleanup do
115
+ # move index.html back into place after gem packaging
116
+ FileUtils.cp('/tmp/index.html', 'web/chat/index.html')
117
+ File.delete('/tmp/index.html')
118
+ end
119
+
120
+ task :default => [:clobber, :test, :compile, :gem, :cleanup]
data/bin/vines CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: UTF-8
3
3
 
4
- require 'fileutils'
5
4
  require 'optparse'
6
5
  require 'vines'
7
6
 
@@ -19,13 +19,13 @@ Vines::Config.configure do
19
19
  # Shared storage example:
20
20
  # host 'verona.lit', 'wonderland.lit' do
21
21
  # storage 'fs' do
22
- # dir 'conf/users'
22
+ # dir 'data/users'
23
23
  # end
24
24
  # end
25
25
 
26
26
  host 'wonderland.lit' do
27
27
  storage 'fs' do
28
- dir 'conf/users'
28
+ dir 'data/users'
29
29
  end
30
30
  end
31
31
 
@@ -36,7 +36,7 @@ Vines::Config.configure do
36
36
  #
37
37
  # host 'wonderland.lit' do
38
38
  # storage 'fs' do
39
- # dir 'conf/users'
39
+ # dir 'data/users'
40
40
  # end
41
41
  # ldap 'ldap.wonderland.lit', 636 do
42
42
  # dn 'cn=Directory Manager'
@@ -51,7 +51,10 @@ Vines::Config.configure do
51
51
 
52
52
  # Configure the client-to-server port. The max_resources_per_account attribute
53
53
  # 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.
54
56
  client '0.0.0.0', 5222 do
57
+ private_storage true
55
58
  max_stanza_size 65536
56
59
  max_resources_per_account 5
57
60
  end
@@ -65,11 +68,17 @@ Vines::Config.configure do
65
68
  hosts []
66
69
  end
67
70
 
68
- # Configure the XEP-0124 BOSH port. This allows HTTP clients to connect to
69
- # the XMPP server.
71
+ # Configure the built-in HTTP server that serves static files and responds to
72
+ # XEP-0124 BOSH requests. This allows HTTP clients to connect to
73
+ # the XMPP server. The root attribute defines the web server's document root.
74
+ # It will only serve files out of this directory. The bind attribute defines
75
+ # the URL to which BOSH clients must POST their XMPP stanza requests.
70
76
  http '0.0.0.0', 5280 do
71
- max_stanza_size 131072
77
+ bind '/xmpp'
78
+ private_storage true
79
+ max_stanza_size 65536
72
80
  max_resources_per_account 5
81
+ root 'web'
73
82
  end
74
83
 
75
84
  # Configure the XEP-0114 external component port. Add entries for each
@@ -85,7 +94,7 @@ end
85
94
  # Available storage implementations:
86
95
 
87
96
  #storage 'fs' do
88
- # dir 'conf/users'
97
+ # dir 'data/users'
89
98
  #end
90
99
 
91
100
  #storage 'couchdb' do
@@ -2,22 +2,25 @@
2
2
 
3
3
  module Vines
4
4
  NAMESPACES = {
5
- :stream => 'http://etherx.jabber.org/streams'.freeze,
6
- :client => 'jabber:client'.freeze,
7
- :server => 'jabber:server'.freeze,
8
- :component => 'jabber:component:accept'.freeze,
9
- :roster => 'jabber:iq:roster'.freeze,
10
- :non_sasl => 'jabber:iq:auth'.freeze,
11
- :sasl => 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze,
12
- :tls => 'urn:ietf:params:xml:ns:xmpp-tls'.freeze,
13
- :bind => 'urn:ietf:params:xml:ns:xmpp-bind'.freeze,
14
- :session => 'urn:ietf:params:xml:ns:xmpp-session'.freeze,
15
- :ping => 'urn:xmpp:ping'.freeze,
16
- :disco_items => 'http://jabber.org/protocol/disco#items'.freeze,
17
- :disco_info => 'http://jabber.org/protocol/disco#info'.freeze,
18
- :http_bind => 'http://jabber.org/protocol/httpbind'.freeze,
19
- :bosh => 'urn:xmpp:xbosh'.freeze,
20
- :vcard => 'vcard-temp'.freeze
5
+ :stream => 'http://etherx.jabber.org/streams'.freeze,
6
+ :client => 'jabber:client'.freeze,
7
+ :server => 'jabber:server'.freeze,
8
+ :component => 'jabber:component:accept'.freeze,
9
+ :roster => 'jabber:iq:roster'.freeze,
10
+ :non_sasl => 'jabber:iq:auth'.freeze,
11
+ :storage => 'jabber:iq:private'.freeze,
12
+ :sasl => 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze,
13
+ :tls => 'urn:ietf:params:xml:ns:xmpp-tls'.freeze,
14
+ :bind => 'urn:ietf:params:xml:ns:xmpp-bind'.freeze,
15
+ :session => 'urn:ietf:params:xml:ns:xmpp-session'.freeze,
16
+ :ping => 'urn:xmpp:ping'.freeze,
17
+ :disco_items => 'http://jabber.org/protocol/disco#items'.freeze,
18
+ :disco_info => 'http://jabber.org/protocol/disco#info'.freeze,
19
+ :http_bind => 'http://jabber.org/protocol/httpbind'.freeze,
20
+ :bosh => 'urn:xmpp:xbosh'.freeze,
21
+ :vcard => 'vcard-temp'.freeze,
22
+ :si => 'http://jabber.org/protocol/si'.freeze,
23
+ :byte_streams => 'http://jabber.org/protocol/bytestreams'.freeze
21
24
  }.freeze
22
25
 
23
26
  module Log
@@ -52,6 +55,7 @@ end
52
55
  em-redis
53
56
  eventmachine
54
57
  fiber
58
+ fileutils
55
59
  http/parser
56
60
  logger
57
61
  net/ldap
@@ -70,6 +74,7 @@ end
70
74
  vines/stanza/iq/disco_items
71
75
  vines/stanza/iq/error
72
76
  vines/stanza/iq/ping
77
+ vines/stanza/iq/private_storage
73
78
  vines/stanza/iq/result
74
79
  vines/stanza/iq/roster
75
80
  vines/stanza/iq/session
@@ -10,9 +10,11 @@ module Vines
10
10
  raise "Directory already initialized: #{domain}" if File.exists?(dir)
11
11
  Dir.mkdir(dir)
12
12
 
13
- FileUtils.cp_r(File.expand_path('../../../../conf', __FILE__), dir)
14
- users, log, pid = %w[conf/users log pid].map do |sub|
15
- File.join(dir, sub).tap {|subdir| Dir.mkdir(subdir) }
13
+ %w[conf web].each do |sub|
14
+ FileUtils.cp_r(File.expand_path("../../../../#{sub}", __FILE__), dir)
15
+ end
16
+ users, log, pid = %w[data/users log pid].map do |sub|
17
+ File.join(dir, sub).tap {|subdir| FileUtils.makedirs(subdir) }
16
18
  end
17
19
 
18
20
  create_users(domain, users)
@@ -144,6 +144,14 @@ module Vines
144
144
  @settings[:max_resources_per_account]
145
145
  end
146
146
  end
147
+
148
+ def private_storage(enabled)
149
+ @settings[:private_storage] = !!enabled
150
+ end
151
+
152
+ def private_storage?
153
+ @settings[:private_storage]
154
+ end
147
155
  end
148
156
 
149
157
  class ServerPort < Port
@@ -166,6 +174,8 @@ module Vines
166
174
  def initialize(config, host='0.0.0.0', port=5280, &block)
167
175
  @stream = Vines::Stream::Http
168
176
  super(config, host, port, &block)
177
+ defaults = {:root => File.expand_path('web'), :bind => '/xmpp'}
178
+ @settings = defaults.merge(@settings)
169
179
  end
170
180
 
171
181
  def max_resources_per_account(max=nil)
@@ -175,6 +185,30 @@ module Vines
175
185
  @settings[:max_resources_per_account]
176
186
  end
177
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
178
212
  end
179
213
 
180
214
  class ComponentPort < Port
@@ -80,6 +80,20 @@ module Vines
80
80
  }
81
81
  end
82
82
 
83
+ # Write an iq stanza to the recipient stream representing this contact's
84
+ # current roster item state.
85
+ def send_roster_push(recipient)
86
+ doc = Nokogiri::XML::Document.new
87
+ node = doc.create_element('iq',
88
+ 'id' => Kit.uuid,
89
+ 'to' => recipient.user.jid.to_s,
90
+ 'type' => 'set')
91
+ node << doc.create_element('query', 'xmlns' => NAMESPACES[:roster]) do |query|
92
+ query << to_roster_xml
93
+ end
94
+ recipient.write(node)
95
+ end
96
+
83
97
  # Returns this contact as an xmpp <item> element.
84
98
  def to_roster_xml
85
99
  doc = Nokogiri::XML::Document.new
@@ -55,6 +55,32 @@ module Vines
55
55
  raise 'subclass must implement'
56
56
  end
57
57
 
58
+ # Broadcast unavailable presence from the user's available resources to the
59
+ # recipient's available resources. Route the stanza to a remote server if
60
+ # the recipient isn't hosted locally.
61
+ def send_unavailable(from, to)
62
+ router.available_resources(from).each do |stream|
63
+ 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
68
+ else
69
+ router.route(el)
70
+ end
71
+ end
72
+ end
73
+
74
+ # Return an unavailable presence stanza addressed to the given JID.
75
+ def unavailable(from, to)
76
+ doc = Document.new
77
+ doc.create_element('presence',
78
+ 'from' => from.to_s,
79
+ 'id' => Kit.uuid,
80
+ 'to' => to.to_s,
81
+ 'type' => 'unavailable')
82
+ end
83
+
58
84
  def method_missing(method, *args, &block)
59
85
  @node.send(method, *args, &block)
60
86
  end
@@ -15,7 +15,7 @@ module Vines
15
15
 
16
16
  def process
17
17
  if self['id'] && VALID_TYPES.include?(self['type'])
18
- raise StanzaErrors::FeatureNotImplemented.new(@node, 'cancel')
18
+ route_iq or raise StanzaErrors::FeatureNotImplemented.new(@node, 'cancel')
19
19
  else
20
20
  raise StanzaErrors::BadRequest.new(@node, 'modify')
21
21
  end
@@ -15,6 +15,9 @@ module Vines
15
15
  query.default_namespace = NS
16
16
  query << el.document.create_element('feature', 'var' => NAMESPACES[:ping])
17
17
  query << el.document.create_element('feature', 'var' => NAMESPACES[:vcard])
18
+ if stream.private_storage?
19
+ query << el.document.create_element('feature', 'var' => NAMESPACES[:storage])
20
+ end
18
21
  end
19
22
  end
20
23
  stream.write(result)
@@ -0,0 +1,83 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ # Implements the Private Storage feature defined in XEP-0049. Clients are
7
+ # allowed to save arbitrary XML documents on the server, identified by
8
+ # element name and namespace.
9
+ class PrivateStorage < Query
10
+ NS = NAMESPACES[:storage]
11
+
12
+ register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS
13
+
14
+ def process
15
+ unless stream.private_storage?
16
+ raise StanzaErrors::ServiceUnavailable.new(self, 'cancel')
17
+ end
18
+ validate_to_address
19
+ validate_children_size
20
+ validate_namespaces
21
+ get? ? retrieve_fragment : update_fragment
22
+ end
23
+
24
+ private
25
+
26
+ def retrieve_fragment
27
+ found = storage.find_fragment(stream.user.jid, elements.first.elements.first)
28
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless found
29
+
30
+ result = to_result do |node|
31
+ node << node.document.create_element('query') do |query|
32
+ query.default_namespace = NS
33
+ query << found
34
+ end
35
+ end
36
+ stream.write(result)
37
+ end
38
+
39
+ def update_fragment
40
+ elements.first.elements.each do |node|
41
+ storage.save_fragment(stream.user.jid, node)
42
+ end
43
+ stream.write(to_result)
44
+ end
45
+
46
+ private
47
+
48
+ def to_result
49
+ doc = Document.new
50
+ node = doc.create_element('iq',
51
+ 'from' => stream.user.jid.to_s,
52
+ 'id' => self['id'],
53
+ 'to' => stream.user.jid.to_s,
54
+ 'type' => 'result')
55
+ yield node if block_given?
56
+ node
57
+ end
58
+
59
+ def validate_children_size
60
+ size = elements.first.elements.size
61
+ if (get? && size != 1) || (set? && size == 0)
62
+ raise StanzaErrors::NotAcceptable.new(self, 'modify')
63
+ end
64
+ end
65
+
66
+ def validate_to_address
67
+ to = (self['to'] || '').strip
68
+ unless to.empty? || to == stream.user.jid.bare.to_s
69
+ raise StanzaErrors::Forbidden.new(self, 'cancel')
70
+ end
71
+ end
72
+
73
+ def validate_namespaces
74
+ elements.first.elements.each do |node|
75
+ if node.namespace.nil? || NAMESPACES.values.include?(node.namespace.href)
76
+ raise StanzaErrors::NotAcceptable.new(self, 'modify')
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end