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,51 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+
5
+ # An X509 certificate store that validates certificate trust chains.
6
+ # This uses the conf/certs/*.crt files as the list of trusted root
7
+ # CA certificates.
8
+ class Store
9
+ @@certs = nil
10
+
11
+ def initialize
12
+ @store = OpenSSL::X509::Store.new
13
+ certs.each {|c| @store.add_cert(c) }
14
+ end
15
+
16
+ # Return true if the certificate is signed by a CA certificate in the
17
+ # store. If the certificate can be trusted, it's added to the store so
18
+ # it can be used to trust other certs.
19
+ def trusted?(pem)
20
+ if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
21
+ @store.verify(cert).tap do |trusted|
22
+ @store.add_cert(cert) if trusted rescue nil
23
+ end
24
+ end
25
+ end
26
+
27
+ # Return true if the domain name matches one of the names in the
28
+ # certificate. In other words, is the certificate provided to us really
29
+ # for the domain to which we think we're connected?
30
+ def domain?(pem, domain)
31
+ if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
32
+ OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false
33
+ end
34
+ end
35
+
36
+ # Return the trusted root CA certificates installed in conf/certs. These
37
+ # certificates are used to start the trust chain needed to validate certs
38
+ # we receive from clients and servers.
39
+ def certs
40
+ unless @@certs
41
+ pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
42
+ dir = File.join(VINES_ROOT, 'conf', 'certs')
43
+ certs = Dir[File.join(dir, '*.crt')].map {|f| File.read(f) }
44
+ certs = certs.map {|c| c.scan(pattern) }.flatten
45
+ certs.map! {|c| OpenSSL::X509::Certificate.new(c) }
46
+ @@certs = certs.reject {|c| c.not_after < Time.now }
47
+ end
48
+ @@certs
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,198 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ # The base class for various XMPP streams (c2s, s2s, component, http),
5
+ # containing behavior common to all streams like rate limiting, stanza
6
+ # parsing, and stream error handling.
7
+ class Stream < EventMachine::Connection
8
+ include Vines::Log
9
+
10
+ ERROR = 'error'.freeze
11
+ PAD = 20
12
+
13
+ attr_accessor :user
14
+
15
+ def post_init
16
+ router << self
17
+ @remote_addr, @local_addr = [get_peername, get_sockname].map do |addr|
18
+ addr ? Socket.unpack_sockaddr_in(addr)[0, 2].reverse.join(':') : 'unknown'
19
+ end
20
+ @user, @closed, @stanza_size = nil, false, 0
21
+ @bucket = TokenBucket.new(100, 10)
22
+ @store = Store.new
23
+
24
+ @nodes = EM::Queue.new
25
+ process_node_queue
26
+
27
+ @parser = Parser.new.tap do |p|
28
+ p.stream_open {|node| @nodes.push(node) }
29
+ p.stream_close { close_connection }
30
+ p.stanza {|node| @nodes.push(node) }
31
+ end
32
+ log.info { "%s %21s -> %s" %
33
+ ['Stream connected:'.ljust(PAD), @remote_addr, @local_addr] }
34
+ end
35
+
36
+ def close_connection(after_writing=false)
37
+ super
38
+ @closed = true
39
+ advance(Client::Closed.new(self))
40
+ end
41
+
42
+ def receive_data(data)
43
+ return if @closed
44
+ @stanza_size += data.bytesize
45
+ if @stanza_size < max_stanza_size
46
+ @parser << data rescue error(StreamErrors::NotWellFormed.new)
47
+ else
48
+ error(StreamErrors::PolicyViolation.new('max stanza size reached'))
49
+ end
50
+ end
51
+
52
+ # Send the stanza to all recipients, stamping it with from and
53
+ # to addresses first.
54
+ def broadcast(stanza, recipients)
55
+ stanza['from'] = @user.jid.to_s
56
+ recipients.each do |recipient|
57
+ stanza['to'] = recipient.user.jid.to_s
58
+ recipient.write(stanza)
59
+ end
60
+ end
61
+
62
+ # Returns the storage system for the domain. If no domain is given,
63
+ # the stream's storage mechanism is returned.
64
+ def storage(domain=@domain)
65
+ @config.vhosts[domain]
66
+ end
67
+
68
+ # Reload the user's information into their active connections. Call this
69
+ # after storage.save_user() to sync the new user state with their other
70
+ # connections.
71
+ def update_user_streams(user)
72
+ router.connected_resources(user.jid.bare).each do |stream|
73
+ stream.user.update_from(user)
74
+ end
75
+ end
76
+
77
+ def ssl_verify_peer(pem)
78
+ # EM is supposed to close the connection when this returns false,
79
+ # but it only does that for inbound connections, not when we
80
+ # make a connection to another server.
81
+ @store.trusted?(pem).tap do |trusted|
82
+ close_connection unless trusted
83
+ end
84
+ end
85
+
86
+ def cert_domain_matches?(domain)
87
+ @store.domain?(get_peer_cert, domain)
88
+ end
89
+
90
+ # Send the data over the wire to this client.
91
+ def write(data)
92
+ log_node(data, :out)
93
+ if data.respond_to?(:to_xml)
94
+ data = data.to_xml(:indent => 0)
95
+ end
96
+ send_data(data)
97
+ end
98
+
99
+ def encrypt
100
+ cert, key = tls_files
101
+ start_tls(:private_key_file => key, :cert_chain_file => cert, :verify_peer => true)
102
+ end
103
+
104
+ # Returns true if the TLS certificate and private key files for this domain
105
+ # exist and can be used to encrypt this stream.
106
+ def encrypt?
107
+ tls_files.all? {|f| File.exists?(f) }
108
+ end
109
+
110
+ def unbind
111
+ router.delete(self)
112
+ log.info { "%s %21s -> %s" %
113
+ ['Stream disconnected:'.ljust(PAD), @remote_addr, @local_addr] }
114
+ log.info { "Streams connected: #{router.size}" }
115
+ end
116
+
117
+ def advance(state)
118
+ @state = state
119
+ end
120
+
121
+ # Stream level errors close the stream while stanza and SASL errors are
122
+ # written to the client and leave the stream open. All exceptions should
123
+ # pass through this method for consistent handling.
124
+ def error(e)
125
+ case e
126
+ when SaslError, StanzaError
127
+ write(e.to_xml)
128
+ when StreamError
129
+ write(e.to_xml)
130
+ close_stream
131
+ else
132
+ log.error(e)
133
+ write(StreamErrors::InternalServerError.new.to_xml)
134
+ close_stream
135
+ end
136
+ end
137
+
138
+ def router
139
+ Router.instance
140
+ end
141
+
142
+ private
143
+
144
+ def close_stream
145
+ write('</stream:stream>')
146
+ close_connection_after_writing
147
+ end
148
+
149
+ def error?(node)
150
+ ns = node.namespace ? node.namespace.href : nil
151
+ node.name == ERROR && ns == NAMESPACES[:stream]
152
+ end
153
+
154
+ # Schedule a queue pop on the EM thread to handle the next element.
155
+ # This provides the in-order stanza processing guarantee required by
156
+ # RFC 6120 section 10.1.
157
+ def process_node_queue
158
+ @nodes.pop do |node|
159
+ Fiber.new do
160
+ process_node(node)
161
+ process_node_queue
162
+ end.resume unless @closed
163
+ end
164
+ end
165
+
166
+ def process_node(node)
167
+ log_node(node, :in)
168
+ @stanza_size = 0
169
+ enforce_rate_limit
170
+ if error?(node)
171
+ close_stream
172
+ else
173
+ @state.node(node)
174
+ end
175
+ rescue Exception => e
176
+ error(e)
177
+ end
178
+
179
+ def enforce_rate_limit
180
+ unless @bucket.take(1)
181
+ raise StreamErrors::PolicyViolation.new('rate limit exceeded')
182
+ end
183
+ end
184
+
185
+ def log_node(node, direction)
186
+ return unless log.debug?
187
+ from, to = @remote_addr, @local_addr
188
+ from, to = to, from if direction == :out
189
+ label = (direction == :out) ? 'Sent' : 'Received'
190
+ log.debug("%s %21s -> %s\n%s\n" %
191
+ ["#{label} stanza:".ljust(PAD), from, to, node])
192
+ end
193
+
194
+ def tls_files
195
+ %w[crt key].map {|ext| File.join(VINES_ROOT, 'conf', 'certs', "#{@domain}.#{ext}") }
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,131 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+
6
+ # Implements the XMPP protocol for client-to-server (c2s) streams. This
7
+ # serves connected streams using the jabber:client namespace.
8
+ class Client < Stream
9
+ attr_reader :config, :domain
10
+ attr_accessor :last_broadcast_presence
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ @domain = nil
15
+ @requested_roster = false
16
+ @available = false
17
+ @unbound = false
18
+ @last_broadcast_presence = nil
19
+ @state = Start.new(self)
20
+ end
21
+
22
+ def ssl_handshake_completed
23
+ if get_peer_cert
24
+ close_connection unless cert_domain_matches?(@domain)
25
+ end
26
+ end
27
+
28
+ def max_stanza_size
29
+ @config[:client].max_stanza_size
30
+ end
31
+
32
+ def max_resources_per_account
33
+ @config[:client].max_resources_per_account
34
+ end
35
+
36
+ def unbind
37
+ @unbound = true
38
+ @available = false
39
+ if authenticated?
40
+ doc = Nokogiri::XML::Document.new
41
+ el = doc.create_element('presence', 'type' => 'unavailable')
42
+ Stanza::Presence::Unavailable.new(el, self).outbound_broadcast_presence
43
+ end
44
+ super
45
+ end
46
+
47
+ # Returns true if this client has properly authenticated with
48
+ # the server.
49
+ def authenticated?
50
+ !@user.nil?
51
+ end
52
+
53
+ # A connected resource has authenticated and bound a resource
54
+ # identifier.
55
+ def connected?
56
+ !@unbound && authenticated? && !@user.jid.bare?
57
+ end
58
+
59
+ # An available resource has sent initial presence and can
60
+ # receive presence subscription requests.
61
+ def available?
62
+ @available && connected?
63
+ end
64
+
65
+ # An interested resource has requested its roster and can
66
+ # receive roster pushes.
67
+ def interested?
68
+ @requested_roster && connected?
69
+ end
70
+
71
+ def available!
72
+ @available = true
73
+ end
74
+
75
+ def requested_roster!
76
+ @requested_roster = true
77
+ end
78
+
79
+ # Returns streams for available resources to which this user
80
+ # has successfully subscribed.
81
+ def available_subscribed_to_resources
82
+ subscribed = @user.subscribed_to_contacts.map {|c| c.jid }
83
+ router.available_resources(subscribed)
84
+ end
85
+
86
+ # Returns streams for available resources that are subscribed
87
+ # to this user's presence updates.
88
+ def available_subscribers
89
+ subscribed = @user.subscribed_from_contacts.map {|c| c.jid }
90
+ router.available_resources(subscribed)
91
+ end
92
+
93
+ # Returns contacts hosted at remote servers that are subscribed
94
+ # to this user's presence updates.
95
+ def remote_subscribers(to=nil)
96
+ jid = (to.nil? || to.empty?) ? nil : JID.new(to).bare
97
+ @user.subscribed_from_contacts.reject do |c|
98
+ router.local_jid?(c.jid) || (jid && c.jid.bare != jid)
99
+ end
100
+ end
101
+
102
+ def ready?
103
+ @state.class == Client::Ready
104
+ end
105
+
106
+ def start(node)
107
+ @domain, from = %w[to from].map {|a| node[a] }
108
+ send_stream_header(from)
109
+ raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0'
110
+ raise StreamErrors::HostUnknown unless @config.vhost?(@domain)
111
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:client]
112
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
113
+ end
114
+
115
+ private
116
+
117
+ def send_stream_header(to)
118
+ attrs = {
119
+ 'xmlns' => NAMESPACES[:client],
120
+ 'xmlns:stream' => NAMESPACES[:stream],
121
+ 'xml:lang' => 'en',
122
+ 'id' => Kit.uuid,
123
+ 'from' => @domain,
124
+ 'version' => '1.0'
125
+ }
126
+ attrs['to'] = to if to
127
+ write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stream
5
+ class Client
6
+ class Auth < State
7
+ NS = NAMESPACES[:sasl]
8
+ AUTH = 'auth'.freeze
9
+ SUCCESS = %Q{<success xmlns="#{NS}"/>}.freeze
10
+ MAX_AUTH_ATTEMPTS = 3
11
+ AUTH_MECHANISMS = {'PLAIN' => :plain_auth, 'EXTERNAL' => :external_auth}.freeze
12
+
13
+ def initialize(stream, success=BindRestart)
14
+ super
15
+ @attempts, @outstanding = 0, false
16
+ end
17
+
18
+ def node(node)
19
+ raise StreamErrors::NotAuthorized unless auth?(node)
20
+ unless node.text.empty?
21
+ (name = AUTH_MECHANISMS[node['mechanism']]) ?
22
+ method(name).call(node) :
23
+ send_auth_fail(SaslErrors::InvalidMechanism.new)
24
+ else
25
+ send_auth_fail(SaslErrors::MalformedRequest.new)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def auth?(node)
32
+ node.name == AUTH && namespace(node) == NS && !@outstanding
33
+ end
34
+
35
+ # Authenticate s2s streams by comparing their domain to
36
+ # their SSL certificate.
37
+ def external_auth(stanza)
38
+ domain = Base64.decode64(stanza.text)
39
+ cert = OpenSSL::X509::Certificate.new(stream.get_peer_cert) rescue nil
40
+ if (!OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false)
41
+ send_auth_fail(SaslErrors::NotAuthorized.new)
42
+ stream.write('</stream:stream>')
43
+ stream.close_connection_after_writing
44
+ else
45
+ stream.remote_domain = domain
46
+ send_auth_success
47
+ end
48
+ end
49
+
50
+ # Authenticate c2s streams using a username and password. Call the
51
+ # authentication module in a separate thread to avoid blocking stanza
52
+ # processing for other users.
53
+ def plain_auth(stanza)
54
+ jid, node, password = Base64.decode64(stanza.text).split("\000")
55
+ jid = [node, stream.domain].join('@') if jid.nil? || jid.empty?
56
+ log.info("Authenticating user: %s" % jid)
57
+ @outstanding = true
58
+ begin
59
+ user = stream.storage.authenticate(jid, password)
60
+ finish(user || SaslErrors::NotAuthorized.new)
61
+ rescue Exception => e
62
+ log.error("Failed to authenticate: #{e.to_s}")
63
+ finish(SaslErrors::TemporaryAuthFailure.new)
64
+ end
65
+ end
66
+
67
+ def finish(result)
68
+ @outstanding = false
69
+ if result.kind_of?(Exception)
70
+ send_auth_fail(result)
71
+ else
72
+ stream.user = result
73
+ log.info("Authentication succeeded: %s" % stream.user.jid)
74
+ send_auth_success
75
+ end
76
+ end
77
+
78
+ def send_auth_success
79
+ stream.write(SUCCESS)
80
+ advance
81
+ end
82
+
83
+ def send_auth_fail(condition)
84
+ @attempts += 1
85
+ if @attempts >= MAX_AUTH_ATTEMPTS
86
+ stream.error(StreamErrors::PolicyViolation.new("max authentication attempts exceeded"))
87
+ else
88
+ stream.error(condition)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end