vines 0.1.1 → 0.2.0

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