vines 0.1.0

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