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,33 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class AuthRestart < State
7
+ def initialize(stream, success=Auth)
8
+ super
9
+ end
10
+
11
+ def node(node)
12
+ raise StreamErrors::NotAuthorized unless stream?(node)
13
+ stream.start(node)
14
+ doc = Document.new
15
+ features = doc.create_element('stream:features') do |el|
16
+ el << doc.create_element('mechanisms') do |parent|
17
+ parent.default_namespace = NAMESPACES[:sasl]
18
+ mechanisms.each {|name| parent << doc.create_element('mechanism', name) }
19
+ end
20
+ end
21
+ stream.write(features)
22
+ advance
23
+ end
24
+
25
+ private
26
+
27
+ def mechanisms
28
+ ['EXTERNAL', 'PLAIN']
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class Bind < State
7
+ NS = NAMESPACES[:bind]
8
+ MAX_ATTEMPTS = 5
9
+
10
+ def initialize(stream, success=Ready)
11
+ super
12
+ @attempts = 0
13
+ end
14
+
15
+ def node(node)
16
+ @attempts += 1
17
+ raise StreamErrors::NotAuthorized unless bind?(node)
18
+ raise StreamErrors::PolicyViolation.new('max bind attempts reached') if @attempts > MAX_ATTEMPTS
19
+ raise StanzaErrors::ResourceConstraint.new(node, 'wait') if resource_limit_reached?
20
+ stream.user.jid.resource = resource(node)
21
+ doc = Document.new
22
+ result = doc.create_element('iq', 'id' => node['id'], 'type' => 'result') do |el|
23
+ el << doc.create_element('bind') do |bind|
24
+ bind.default_namespace = NS
25
+ bind << doc.create_element('jid', stream.user.jid.to_s)
26
+ end
27
+ end
28
+ stream.write(result)
29
+ stream.write('<stream:features/>')
30
+ advance
31
+ end
32
+
33
+ private
34
+
35
+ def bind?(node)
36
+ node.name == 'iq' && node['type'] == 'set' && node.xpath('ns:bind', 'ns' => NS).any?
37
+ end
38
+
39
+ def resource(node)
40
+ el = node.xpath('ns:bind/ns:resource', 'ns' => NS).first
41
+ resource = el ? el.text.strip : ''
42
+ resource.empty? || resource_used?(resource) ? Kit.uuid : resource
43
+ end
44
+
45
+ def resource_limit_reached?
46
+ used = stream.router.connected_resources(stream.user.jid.bare).size
47
+ used >= stream.max_resources_per_account
48
+ end
49
+
50
+ def resource_used?(resource)
51
+ stream.router.available_resources(stream.user.jid).any? do |c|
52
+ c.user.jid.resource == resource
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class BindRestart < State
7
+ def initialize(stream, success=Bind)
8
+ super
9
+ end
10
+
11
+ def node(node)
12
+ raise StreamErrors::NotAuthorized unless stream?(node)
13
+ stream.start(node)
14
+ doc = Document.new
15
+ features = doc.create_element('stream:features') do |el|
16
+ el << doc.create_element('bind', 'xmlns' => NAMESPACES[:bind])
17
+ el << doc.create_element('session', 'xmlns' => NAMESPACES[:session])
18
+ end
19
+ stream.write(features)
20
+ advance
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class Closed < State
7
+ def node(node)
8
+ # ignore data received after close_connection
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class Ready < State
7
+ def node(node)
8
+ stanza = to_stanza(node)
9
+ raise StreamErrors::UnsupportedStanzaType unless stanza
10
+ stanza.process
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class Start < State
7
+ def initialize(stream, success=TLS)
8
+ super
9
+ end
10
+
11
+ def node(node)
12
+ raise StreamErrors::NotAuthorized unless stream?(node)
13
+ stream.start(node)
14
+ doc = Document.new
15
+ features = doc.create_element('stream:features') do |el|
16
+ el << doc.create_element('starttls') do |tls|
17
+ tls.default_namespace = NAMESPACES[:tls]
18
+ tls << doc.create_element('required')
19
+ end
20
+ end
21
+ stream.write(features)
22
+ advance
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class TLS < State
7
+ NS = NAMESPACES[:tls]
8
+ PROCEED = %Q{<proceed xmlns="#{NS}"/>}.freeze
9
+ FAILURE = %Q{<failure xmlns="#{NS}"/>}.freeze
10
+ STARTTLS = 'starttls'.freeze
11
+
12
+ def initialize(stream, success=AuthRestart)
13
+ super
14
+ end
15
+
16
+ def node(node)
17
+ raise StreamErrors::NotAuthorized unless starttls?(node)
18
+ if stream.encrypt?
19
+ stream.write(PROCEED)
20
+ stream.encrypt
21
+ advance
22
+ else
23
+ stream.write(FAILURE)
24
+ stream.write('</stream:stream>')
25
+ stream.close_connection_after_writing
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def starttls?(node)
32
+ node.name == STARTTLS && namespace(node) == NS
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+
6
+ # Implements the XMPP protocol for trusted, external component (XEP-0114)
7
+ # streams. This serves connected streams using the jabber:component:accept
8
+ # namespace.
9
+ class Component < Stream
10
+ attr_reader :config, :remote_domain
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ @remote_domain = nil
15
+ @stream_id = Kit.uuid
16
+ @state = Start.new(self)
17
+ end
18
+
19
+ def max_stanza_size
20
+ @config[:component].max_stanza_size
21
+ end
22
+
23
+ def ready?
24
+ @state.class == Component::Ready
25
+ end
26
+
27
+ def start(node)
28
+ @remote_domain = node['to']
29
+ send_stream_header
30
+ raise StreamErrors::HostUnknown unless @config[:component].password(@remote_domain)
31
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:component]
32
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
33
+ end
34
+
35
+ def secret
36
+ password = @config[:component].password(@remote_domain)
37
+ Digest::SHA1.hexdigest(@stream_id + password)
38
+ end
39
+
40
+ private
41
+
42
+ def send_stream_header
43
+ attrs = {
44
+ 'xmlns' => NAMESPACES[:component],
45
+ 'xmlns:stream' => NAMESPACES[:stream],
46
+ 'id' => @stream_id,
47
+ 'from' => @remote_domain
48
+ }
49
+ write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Component
6
+ class Handshake < State
7
+ def initialize(stream, success=Ready)
8
+ super
9
+ end
10
+
11
+ def node(node)
12
+ raise StreamErrors::NotAuthorized unless handshake?(node)
13
+ stream.write('<handshake/>')
14
+ advance
15
+ end
16
+
17
+ private
18
+
19
+ def handshake?(node)
20
+ node.name == 'handshake' && node.text == stream.secret
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Component
6
+ class Ready < State
7
+ def node(node)
8
+ stanza = to_stanza(node)
9
+ raise StreamErrors::UnsupportedStanzaType unless stanza
10
+ to = (node['to'] || '').strip
11
+ from = JID.new(node['from'] || '')
12
+ raise StreamErrors::ImproperAddressing if to.empty? || from.domain != stream.remote_domain
13
+ if stanza.local?
14
+ stream.router.connected_resources(to).each do |recipient|
15
+ recipient.write(node)
16
+ end
17
+ else
18
+ stanza.route
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Component
6
+ class Start < State
7
+ def initialize(stream, success=Handshake)
8
+ super
9
+ end
10
+
11
+ def node(node)
12
+ raise StreamErrors::NotAuthorized unless stream?(node)
13
+ stream.start(node)
14
+ advance
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,111 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Http < Client
6
+ include Thin
7
+ include Vines::Log
8
+
9
+ attr_accessor :last_broadcast_presence, :last_activity
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ @domain = nil
14
+ @requested_roster = false
15
+ @available = false
16
+ @unbound = false
17
+ @last_broadcast_presence = nil
18
+ @request = Thin::Request.new
19
+ @@http_states ||= HttpStates.new
20
+ @state = Auth.new(self)
21
+ end
22
+
23
+ def user
24
+ @http_state.user
25
+ end
26
+
27
+ def user=(user)
28
+ @http_state.user = user
29
+ end
30
+
31
+ def receive_data(data)
32
+ #TODO: make sure we add max stanza size enforcement
33
+ if @request.parse(data)
34
+ process_http_request(@request)
35
+ @request = Thin::Request.new
36
+ end
37
+ rescue InvalidRequest => e
38
+ error(StreamErrors::NotWellFormed.new)
39
+ end
40
+
41
+ def write(data)
42
+ @http_state.write(data)
43
+ end
44
+
45
+ def setup_new_client(rid, domain)
46
+ sid = Kit.uuid
47
+ log.info("Setting up a new client SID: #{sid} for RID: #{rid}.")
48
+ @http_state = HttpState.new(self, sid, rid, domain)
49
+ @@http_states[sid] = @http_state
50
+ end
51
+
52
+ def unbind
53
+ #router.delete(@http_state)
54
+ log.info("HTTP Stream disconnected:\tfrom=#{@remote_addr}\tto=#{@local_addr}")
55
+ log.info("Streams connected: #{router.size}")
56
+ end
57
+
58
+ def process_http_request(request)
59
+ if request.body.string.empty?
60
+ #Respond to proxy servers' status pings
61
+ log.info("A status request has been received.")
62
+ send_data("Online")
63
+ close_connection_after_writing
64
+ return
65
+ end
66
+ body = Nokogiri::XML(request.body.string).root
67
+ body.namespace = nil
68
+ #TODO: Confirm this is a valid body stanza.
69
+ # If it isn't a body, return proxy ping result
70
+
71
+ if body['sid']
72
+ @http_state = @@http_states[body['sid']]
73
+ unless @http_state
74
+ log.info("Client was not found #{body['sid']}")
75
+ send_bosh_error
76
+ return
77
+ end
78
+ @domain = @http_state.domain
79
+ @user = @http_state.user
80
+ @http_state.request(body['rid'])
81
+ if body['restart']
82
+ @http_state.handle_restart
83
+ router << @http_state
84
+ @state = Bind.new(self)
85
+ end
86
+
87
+ body.elements.each do |node|
88
+ @nodes.push(Nokogiri::XML(node.to_s.sub(' xmlns="jabber:client"', '')).root)
89
+ end
90
+ else
91
+ self.setup_new_client(body['rid'], body['to'])
92
+ end
93
+ end
94
+
95
+ def send_bosh_error
96
+ body = "<body type='terminate' condition='remote-connection-failed' xmlns='http://jabber.org/protocol/httpbind'/>"
97
+ header = [
98
+ "HTTP/1.1 404 OK",
99
+ "Content-Type: text/xml; charset=utf-8",
100
+ "Content-Length: #{body.bytesize}"
101
+ ].join("\r\n")
102
+
103
+ send_data([header, body].join("\r\n\r\n"))
104
+ end
105
+
106
+ def domain
107
+ @http_state.domain
108
+ end
109
+ end
110
+ end
111
+ end