vines 0.1.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 (119) hide show
  1. data/LICENSE +19 -0
  2. data/README +34 -0
  3. data/Rakefile +55 -0
  4. data/bin/vines +95 -0
  5. data/conf/certs/README +32 -0
  6. data/conf/certs/ca-bundle.crt +3987 -0
  7. data/conf/config.rb +114 -0
  8. data/lib/vines.rb +155 -0
  9. data/lib/vines/command/bcrypt.rb +12 -0
  10. data/lib/vines/command/cert.rb +49 -0
  11. data/lib/vines/command/init.rb +58 -0
  12. data/lib/vines/command/ldap.rb +35 -0
  13. data/lib/vines/command/restart.rb +12 -0
  14. data/lib/vines/command/schema.rb +24 -0
  15. data/lib/vines/command/start.rb +28 -0
  16. data/lib/vines/command/stop.rb +18 -0
  17. data/lib/vines/config.rb +191 -0
  18. data/lib/vines/contact.rb +99 -0
  19. data/lib/vines/daemon.rb +78 -0
  20. data/lib/vines/error.rb +150 -0
  21. data/lib/vines/jid.rb +56 -0
  22. data/lib/vines/kit.rb +23 -0
  23. data/lib/vines/router.rb +125 -0
  24. data/lib/vines/stanza.rb +55 -0
  25. data/lib/vines/stanza/iq.rb +50 -0
  26. data/lib/vines/stanza/iq/auth.rb +18 -0
  27. data/lib/vines/stanza/iq/disco_info.rb +25 -0
  28. data/lib/vines/stanza/iq/disco_items.rb +23 -0
  29. data/lib/vines/stanza/iq/error.rb +16 -0
  30. data/lib/vines/stanza/iq/ping.rb +16 -0
  31. data/lib/vines/stanza/iq/query.rb +10 -0
  32. data/lib/vines/stanza/iq/result.rb +16 -0
  33. data/lib/vines/stanza/iq/roster.rb +153 -0
  34. data/lib/vines/stanza/iq/session.rb +22 -0
  35. data/lib/vines/stanza/iq/vcard.rb +58 -0
  36. data/lib/vines/stanza/message.rb +41 -0
  37. data/lib/vines/stanza/presence.rb +119 -0
  38. data/lib/vines/stanza/presence/error.rb +23 -0
  39. data/lib/vines/stanza/presence/probe.rb +38 -0
  40. data/lib/vines/stanza/presence/subscribe.rb +66 -0
  41. data/lib/vines/stanza/presence/subscribed.rb +64 -0
  42. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  43. data/lib/vines/stanza/presence/unsubscribe.rb +57 -0
  44. data/lib/vines/stanza/presence/unsubscribed.rb +50 -0
  45. data/lib/vines/storage.rb +216 -0
  46. data/lib/vines/storage/couchdb.rb +119 -0
  47. data/lib/vines/storage/ldap.rb +59 -0
  48. data/lib/vines/storage/local.rb +66 -0
  49. data/lib/vines/storage/redis.rb +108 -0
  50. data/lib/vines/storage/sql.rb +174 -0
  51. data/lib/vines/store.rb +51 -0
  52. data/lib/vines/stream.rb +198 -0
  53. data/lib/vines/stream/client.rb +131 -0
  54. data/lib/vines/stream/client/auth.rb +94 -0
  55. data/lib/vines/stream/client/auth_restart.rb +33 -0
  56. data/lib/vines/stream/client/bind.rb +58 -0
  57. data/lib/vines/stream/client/bind_restart.rb +25 -0
  58. data/lib/vines/stream/client/closed.rb +13 -0
  59. data/lib/vines/stream/client/ready.rb +15 -0
  60. data/lib/vines/stream/client/start.rb +27 -0
  61. data/lib/vines/stream/client/tls.rb +37 -0
  62. data/lib/vines/stream/component.rb +53 -0
  63. data/lib/vines/stream/component/handshake.rb +25 -0
  64. data/lib/vines/stream/component/ready.rb +24 -0
  65. data/lib/vines/stream/component/start.rb +19 -0
  66. data/lib/vines/stream/http.rb +111 -0
  67. data/lib/vines/stream/http/http_request.rb +22 -0
  68. data/lib/vines/stream/http/http_state.rb +139 -0
  69. data/lib/vines/stream/http/http_states.rb +53 -0
  70. data/lib/vines/stream/parser.rb +78 -0
  71. data/lib/vines/stream/server.rb +126 -0
  72. data/lib/vines/stream/server/auth.rb +13 -0
  73. data/lib/vines/stream/server/auth_restart.rb +19 -0
  74. data/lib/vines/stream/server/final_restart.rb +20 -0
  75. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  76. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  77. data/lib/vines/stream/server/outbound/auth_result.rb +28 -0
  78. data/lib/vines/stream/server/outbound/final_features.rb +27 -0
  79. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  80. data/lib/vines/stream/server/outbound/start.rb +20 -0
  81. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  82. data/lib/vines/stream/server/outbound/tls_result.rb +31 -0
  83. data/lib/vines/stream/server/ready.rb +20 -0
  84. data/lib/vines/stream/server/start.rb +13 -0
  85. data/lib/vines/stream/server/tls.rb +13 -0
  86. data/lib/vines/stream/state.rb +55 -0
  87. data/lib/vines/token_bucket.rb +46 -0
  88. data/lib/vines/user.rb +124 -0
  89. data/lib/vines/version.rb +5 -0
  90. data/lib/vines/xmpp_server.rb +25 -0
  91. data/test/config_test.rb +396 -0
  92. data/test/error_test.rb +59 -0
  93. data/test/ext/nokogiri.rb +14 -0
  94. data/test/jid_test.rb +71 -0
  95. data/test/kit_test.rb +21 -0
  96. data/test/router_test.rb +60 -0
  97. data/test/stanza/iq/roster_test.rb +198 -0
  98. data/test/stanza/iq/session_test.rb +30 -0
  99. data/test/stanza/iq/vcard_test.rb +159 -0
  100. data/test/stanza/message_test.rb +124 -0
  101. data/test/stanza/presence/subscribe_test.rb +75 -0
  102. data/test/storage/couchdb_test.rb +102 -0
  103. data/test/storage/ldap_test.rb +207 -0
  104. data/test/storage/local_test.rb +54 -0
  105. data/test/storage/redis_test.rb +75 -0
  106. data/test/storage/sql_test.rb +55 -0
  107. data/test/storage/storage_tests.rb +134 -0
  108. data/test/storage_test.rb +90 -0
  109. data/test/stream/client/auth_test.rb +127 -0
  110. data/test/stream/client/ready_test.rb +47 -0
  111. data/test/stream/component/handshake_test.rb +46 -0
  112. data/test/stream/component/ready_test.rb +105 -0
  113. data/test/stream/component/start_test.rb +41 -0
  114. data/test/stream/parser_test.rb +121 -0
  115. data/test/stream/server/outbound/auth_test.rb +77 -0
  116. data/test/stream/server/ready_test.rb +100 -0
  117. data/test/token_bucket_test.rb +24 -0
  118. data/test/user_test.rb +64 -0
  119. metadata +318 -0
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class FinalRestart < State
7
+ def initialize(stream, success=Ready)
8
+ super
9
+ end
10
+
11
+ def node(node)
12
+ raise StreamErrors::NotAuthorized unless stream?(node)
13
+ stream.start(node)
14
+ stream.write('<stream:features/>')
15
+ advance
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Outbound
7
+ class Auth < State
8
+ NS = NAMESPACES[:sasl]
9
+
10
+ def initialize(stream, success=AuthResult)
11
+ super
12
+ end
13
+
14
+ def node(node)
15
+ raise StreamErrors::NotAuthorized unless external?(node)
16
+ authz = Base64.encode64(stream.domain).chomp
17
+ stream.write(%Q{<auth xmlns="#{NS}" mechanism="EXTERNAL">#{authz}</auth>})
18
+ advance
19
+ end
20
+
21
+ private
22
+
23
+ def external?(node)
24
+ external = node.xpath("ns:mechanisms/ns:mechanism[text()='EXTERNAL']", 'ns' => NS).any?
25
+ node.name == 'features' && namespace(node) == NAMESPACES[:stream] && external
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Outbound
7
+ class AuthRestart < State
8
+ def initialize(stream, success=Auth)
9
+ super
10
+ end
11
+
12
+ def node(node)
13
+ raise StreamErrors::NotAuthorized unless stream?(node)
14
+ advance
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Outbound
7
+ class AuthResult < State
8
+ def initialize(stream, success=FinalRestart)
9
+ super
10
+ end
11
+
12
+ def node(node)
13
+ raise StreamErrors::NotAuthorized unless namespace(node) == NAMESPACES[:sasl]
14
+ case node.name
15
+ when 'success'
16
+ stream.start(node)
17
+ advance
18
+ when 'failure'
19
+ stream.close_connection
20
+ else
21
+ raise StreamErrors::NotAuthorized
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Outbound
7
+ class FinalFeatures < State
8
+ def initialize(stream, success=Server::Ready)
9
+ super
10
+ end
11
+
12
+ def node(node)
13
+ raise StreamErrors::NotAuthorized unless empty_features?(node)
14
+ advance
15
+ stream.notify_connected
16
+ end
17
+
18
+ private
19
+
20
+ def empty_features?(node)
21
+ node.name == 'features' && namespace(node) == NAMESPACES[:stream] && node.elements.empty?
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Outbound
7
+ class FinalRestart < State
8
+ def initialize(stream, success=FinalFeatures)
9
+ super
10
+ end
11
+
12
+ def node(node)
13
+ raise StreamErrors::NotAuthorized unless stream?(node)
14
+ advance
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Outbound
7
+ class Start < State
8
+ def initialize(stream, success=TLS)
9
+ super
10
+ end
11
+
12
+ def node(node)
13
+ raise StreamErrors::NotAuthorized unless stream?(node)
14
+ advance
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Outbound
7
+ class TLS < State
8
+ NS = NAMESPACES[:tls]
9
+
10
+ def initialize(stream, success=TLSResult)
11
+ super
12
+ end
13
+
14
+ def node(node)
15
+ raise StreamErrors::NotAuthorized unless tls?(node)
16
+ stream.write("<starttls xmlns='#{NS}'/>")
17
+ advance
18
+ end
19
+
20
+ private
21
+
22
+ def tls?(node)
23
+ tls = node.xpath('ns:starttls', 'ns' => NS).any?
24
+ node.name == 'features' && namespace(node) == NAMESPACES[:stream] && tls
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Outbound
7
+ class TLSResult < State
8
+ NS = NAMESPACES[:tls]
9
+
10
+ def initialize(stream, success=AuthRestart)
11
+ super
12
+ end
13
+
14
+ def node(node)
15
+ raise StreamErrors::NotAuthorized unless namespace(node) == NS
16
+ case node.name
17
+ when 'proceed'
18
+ stream.encrypt
19
+ stream.start(node)
20
+ advance
21
+ when 'failure'
22
+ stream.close_connection
23
+ else
24
+ raise StreamErrors::NotAuthorized
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Ready < State
7
+ def node(node)
8
+ stanza = to_stanza(node)
9
+ raise StreamErrors::UnsupportedStanzaType unless stanza
10
+ to, from = %w[to from].map {|attr| JID.new(stanza[attr] || '') }
11
+ raise StreamErrors::ImproperAddressing if [to, from].any? {|addr| (addr.domain || '').strip.empty? }
12
+ raise StreamErrors::InvalidFrom unless from.domain == stream.remote_domain
13
+ raise StreamErrors::HostUnknown unless to.domain == stream.domain
14
+ stream.user = User.new(:jid => from)
15
+ stanza.process
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class Start < Client::Start
7
+ def initialize(stream, success=TLS)
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Server
6
+ class TLS < Client::TLS
7
+ def initialize(stream, success=AuthRestart)
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+
6
+ # The base class of Stream state machines. States know how to process XML
7
+ # nodes and advance to their next valid state or fail the stream.
8
+ class State
9
+ include Nokogiri::XML
10
+ include Vines::Log
11
+
12
+ attr_reader :stream
13
+
14
+ STREAM = 'stream'.freeze
15
+
16
+ def initialize(stream, success=nil)
17
+ @stream, @success = stream, success
18
+ end
19
+
20
+ def node(node)
21
+ raise 'subclass must implement'
22
+ end
23
+
24
+ def ==(state)
25
+ self.class == state.class
26
+ end
27
+
28
+ def eql?(state)
29
+ state.is_a?(State) && self == state
30
+ end
31
+
32
+ def hash
33
+ self.class.hash
34
+ end
35
+
36
+ private
37
+
38
+ def advance
39
+ stream.advance(@success.new(stream))
40
+ end
41
+
42
+ def stream?(node)
43
+ node.name == STREAM && namespace(node) == NAMESPACES[:stream]
44
+ end
45
+
46
+ def namespace(node)
47
+ node.namespace ? node.namespace.href : nil
48
+ end
49
+
50
+ def to_stanza(node)
51
+ Stanza.from_node(node, stream)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+
5
+ # The token bucket algorithm is useful for rate limiting.
6
+ # Before an operation can be completed, a token is taken from
7
+ # the bucket. If no tokens are available, the operation fails.
8
+ # The bucket is refilled with tokens at the maximum allowed rate
9
+ # of operations.
10
+ class TokenBucket
11
+
12
+ # Create a full bucket with capacity number of tokens to be filled
13
+ # at the given rate of tokens/second.
14
+ def initialize(capacity, rate)
15
+ raise ArgumentError.new('capacity must be > 0') unless capacity > 0
16
+ raise ArgumentError.new('rate must be > 0') unless rate > 0
17
+ @capacity = capacity
18
+ @tokens = capacity
19
+ @rate = rate
20
+ @timestamp = Time.new
21
+ end
22
+
23
+ # Returns true if tokens can be taken from the bucket.
24
+ def take(tokens)
25
+ 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
32
+ end
33
+
34
+ private
35
+
36
+ def fill
37
+ if @tokens < @capacity
38
+ now = Time.new
39
+ delta = (@rate * (now - @timestamp)).round
40
+ @tokens = [@capacity, @tokens + delta].min
41
+ @timestamp = now
42
+ end
43
+ @tokens
44
+ end
45
+ end
46
+ end
data/lib/vines/user.rb ADDED
@@ -0,0 +1,124 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class User
5
+ include Comparable
6
+
7
+ attr_accessor :name, :password, :roster
8
+ attr_reader :jid
9
+
10
+ def initialize(args={})
11
+ @jid = JID.new(args[:jid])
12
+ raise ArgumentError, 'invalid jid' unless @jid.node && !@jid.domain.empty?
13
+ @name = args[:name]
14
+ @password = args[:password]
15
+ @roster = args[:roster] || []
16
+ end
17
+
18
+ def <=>(user)
19
+ self.jid.to_s <=> user.jid.to_s
20
+ end
21
+
22
+ def eql?(user)
23
+ user.is_a?(User) && self == user
24
+ end
25
+
26
+ def hash
27
+ jid.to_s.hash
28
+ end
29
+
30
+ # Update this user's information from the given user object.
31
+ def update_from(user)
32
+ @name = user.name
33
+ @password = user.password
34
+ @roster = user.roster.map {|c| c.clone }
35
+ end
36
+
37
+ # Return true if the jid is on this user's roster.
38
+ def contact?(jid)
39
+ !contact(jid).nil?
40
+ end
41
+
42
+ # Returns the contact with this jid or nil if not found.
43
+ def contact(jid)
44
+ bare = JID.new(jid).bare
45
+ @roster.find {|c| c.jid.bare == bare }
46
+ end
47
+
48
+ # Returns true if the user is subscribed to this contact's
49
+ # presence updates.
50
+ def subscribed_to?(jid)
51
+ contact = contact(jid)
52
+ contact && contact.subscribed_to?
53
+ end
54
+
55
+ # Returns true if the user has a presence subscription from this contact.
56
+ # The contact is subscribed to this user's presence.
57
+ def subscribed_from?(jid)
58
+ contact = contact(jid)
59
+ contact && contact.subscribed_from?
60
+ end
61
+
62
+ # Removes the contact with this jid from the user's roster.
63
+ def remove_contact(jid)
64
+ bare = JID.new(jid).bare
65
+ @roster.reject! {|c| c.jid.bare == bare }
66
+ end
67
+
68
+ # Returns a list of the contacts to which this user has
69
+ # successfully subscribed.
70
+ def subscribed_to_contacts
71
+ @roster.select {|c| c.subscribed_to? }
72
+ end
73
+
74
+ # Returns a list of the contacts that are subscribed to this user's
75
+ # presence updates.
76
+ def subscribed_from_contacts
77
+ @roster.select {|c| c.subscribed_from? }
78
+ end
79
+
80
+ # Update the contact's jid on this user's roster to signal that this user
81
+ # has requested the contact's permission to receive their presence updates.
82
+ def request_subscription(jid)
83
+ unless contact = contact(jid)
84
+ contact = Contact.new(:jid => jid)
85
+ @roster << contact
86
+ end
87
+ contact.ask = 'subscribe' if %w[none from].include?(contact.subscription)
88
+ end
89
+
90
+ # Add the user's jid to this contact's roster with a subscription state of
91
+ # 'from.' This signals that this contact has approved a user's subscription.
92
+ def add_subscription_from(jid)
93
+ unless contact = contact(jid)
94
+ contact = Contact.new(:jid => jid)
95
+ @roster << contact
96
+ end
97
+ contact.subscribe_from
98
+ end
99
+
100
+ def remove_subscription_to(jid)
101
+ if contact = contact(jid)
102
+ contact.unsubscribe_to
103
+ end
104
+ end
105
+
106
+ def remove_subscription_from(jid)
107
+ if contact = contact(jid)
108
+ contact.unsubscribe_from
109
+ end
110
+ end
111
+
112
+ # Returns this user's roster contacts as an iq query element.
113
+ def to_roster_xml(id)
114
+ doc = Nokogiri::XML::Document.new
115
+ doc.create_element('iq', 'id' => id, 'type' => 'result') do |el|
116
+ el << doc.create_element('query', 'xmlns' => 'jabber:iq:roster') do |query|
117
+ @roster.sort!.each do |contact|
118
+ query << contact.to_roster_xml
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end