vines 0.4.9 → 0.4.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/vines.rb +1 -0
- data/lib/vines/cluster/subscriber.rb +27 -2
- data/lib/vines/storage.rb +157 -80
- data/lib/vines/store.rb +58 -15
- data/lib/vines/stream.rb +71 -13
- data/lib/vines/stream/client.rb +10 -6
- data/lib/vines/stream/component.rb +3 -3
- data/lib/vines/stream/http.rb +35 -9
- data/lib/vines/stream/http/request.rb +26 -5
- data/lib/vines/stream/http/session.rb +8 -0
- data/lib/vines/stream/server.rb +13 -7
- data/lib/vines/version.rb +1 -1
- data/test/store_test.rb +80 -48
- data/test/stream/http/request_test.rb +45 -60
- data/vines.gemspec +6 -6
- metadata +12 -13
data/lib/vines/store.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
3
|
module Vines
|
4
|
-
|
5
4
|
# An X509 certificate store that validates certificate trust chains.
|
6
5
|
# This uses the conf/certs/*.crt files as the list of trusted root
|
7
6
|
# CA certificates.
|
@@ -10,19 +9,25 @@ module Vines
|
|
10
9
|
|
11
10
|
# Create a certificate store to read certificate files from the given
|
12
11
|
# directory.
|
12
|
+
#
|
13
|
+
# dir - The String directory name (absolute or relative).
|
13
14
|
def initialize(dir)
|
14
15
|
@dir = File.expand_path(dir)
|
15
16
|
@store = OpenSSL::X509::Store.new
|
16
|
-
certs.each {|
|
17
|
+
certs.each {|cert| append(cert) }
|
17
18
|
end
|
18
19
|
|
19
20
|
# Return true if the certificate is signed by a CA certificate in the
|
20
21
|
# store. If the certificate can be trusted, it's added to the store so
|
21
22
|
# it can be used to trust other certs.
|
23
|
+
#
|
24
|
+
# pem - The PEM encoded certificate String.
|
25
|
+
#
|
26
|
+
# Returns true if the certificate is trusted.
|
22
27
|
def trusted?(pem)
|
23
28
|
if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
|
24
29
|
@store.verify(cert).tap do |trusted|
|
25
|
-
|
30
|
+
append(cert) if trusted
|
26
31
|
end
|
27
32
|
end
|
28
33
|
end
|
@@ -30,6 +35,11 @@ module Vines
|
|
30
35
|
# Return true if the domain name matches one of the names in the
|
31
36
|
# certificate. In other words, is the certificate provided to us really
|
32
37
|
# for the domain to which we think we're connected?
|
38
|
+
#
|
39
|
+
# pem - The PEM encoded certificate String.
|
40
|
+
# domain - The domain name String.
|
41
|
+
#
|
42
|
+
# Returns true if the certificate was issued for the domain.
|
33
43
|
def domain?(pem, domain)
|
34
44
|
if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
|
35
45
|
OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false
|
@@ -39,8 +49,10 @@ module Vines
|
|
39
49
|
# Return the trusted root CA certificates installed in conf/certs. These
|
40
50
|
# certificates are used to start the trust chain needed to validate certs
|
41
51
|
# we receive from clients and servers.
|
52
|
+
#
|
53
|
+
# Returns an Array of OpenSSL::X509::Certificate objects.
|
42
54
|
def certs
|
43
|
-
|
55
|
+
@@sources ||= begin
|
44
56
|
pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
|
45
57
|
pairs = Dir[File.join(@dir, '*.crt')].map do |name|
|
46
58
|
File.open(name, "r:UTF-8") do |f|
|
@@ -50,7 +62,7 @@ module Vines
|
|
50
62
|
[name, certs]
|
51
63
|
end
|
52
64
|
end
|
53
|
-
|
65
|
+
Hash[pairs]
|
54
66
|
end
|
55
67
|
@@sources.values.flatten
|
56
68
|
end
|
@@ -60,26 +72,43 @@ module Vines
|
|
60
72
|
# wildcard certificate files to serve several subdomains.
|
61
73
|
#
|
62
74
|
# Finding the certificate and private key file for a domain follows these steps:
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
75
|
+
#
|
76
|
+
# - Look for <domain>.crt and <domain>.key files in the conf/certs
|
77
|
+
# directory. If found, return those file names, otherwise . . .
|
78
|
+
#
|
79
|
+
# - Inspect all conf/certs/*.crt files for certificates that contain the
|
66
80
|
# domain name either as the subject common name (CN) or as a DNS
|
67
81
|
# subjectAltName. The corresponding private key must be in a file of the
|
68
82
|
# same name as the certificate's, but with a .key extension.
|
69
83
|
#
|
70
|
-
# So in the simplest configuration, the tea.wonderland.lit encryption files
|
71
|
-
# be named
|
84
|
+
# So in the simplest configuration, the tea.wonderland.lit encryption files
|
85
|
+
# would be named:
|
86
|
+
#
|
87
|
+
# - conf/certs/tea.wonderland.lit.crt
|
88
|
+
# - conf/certs/tea.wonderland.lit.key
|
72
89
|
#
|
73
|
-
# However, in the case of a wildcard certificate for *.wonderland.lit,
|
74
|
-
# files would be
|
75
|
-
#
|
76
|
-
#
|
90
|
+
# However, in the case of a wildcard certificate for *.wonderland.lit,
|
91
|
+
# the files would be:
|
92
|
+
#
|
93
|
+
# - conf/certs/wonderland.lit.crt
|
94
|
+
# - conf/certs/wonderland.lit.key
|
95
|
+
#
|
96
|
+
# These same two files would be returned for the subdomains of:
|
97
|
+
#
|
98
|
+
# - tea.wonderland.lit
|
99
|
+
# - crumpets.wonderland.lit
|
100
|
+
# - etc.
|
101
|
+
#
|
102
|
+
# domain - The String domain name.
|
103
|
+
#
|
104
|
+
# Returns a two element String array for the certificate and private key
|
105
|
+
# file names or nil if not found.
|
77
106
|
def files_for_domain(domain)
|
78
107
|
crt = File.expand_path("#{domain}.crt", @dir)
|
79
108
|
key = File.expand_path("#{domain}.key", @dir)
|
80
109
|
return [crt, key] if File.exists?(crt) && File.exists?(key)
|
81
110
|
|
82
|
-
#
|
111
|
+
# Might be a wildcard cert file.
|
83
112
|
@@sources.each do |file, certs|
|
84
113
|
certs.each do |cert|
|
85
114
|
if OpenSSL::SSL.verify_certificate_identity(cert, domain)
|
@@ -90,5 +119,19 @@ module Vines
|
|
90
119
|
end
|
91
120
|
nil
|
92
121
|
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# Add a trusted certificate to the store, suppressing any OpenSSL errors
|
126
|
+
# caused by the certificate already being stored.
|
127
|
+
#
|
128
|
+
# cert - The OpenSSL::X509::Certificate to add.
|
129
|
+
#
|
130
|
+
# Returns nothing.
|
131
|
+
def append(cert)
|
132
|
+
@store.add_cert(cert)
|
133
|
+
rescue OpenSSL::X509::StoreError
|
134
|
+
# Already added to store.
|
135
|
+
end
|
93
136
|
end
|
94
137
|
end
|
data/lib/vines/stream.rb
CHANGED
@@ -17,6 +17,11 @@ module Vines
|
|
17
17
|
@config = config
|
18
18
|
end
|
19
19
|
|
20
|
+
# Initialize the stream after its connection to the server has completed.
|
21
|
+
# EventMachine calls this method when an incoming connection is accepted
|
22
|
+
# into the event loop.
|
23
|
+
#
|
24
|
+
# Returns nothing.
|
20
25
|
def post_init
|
21
26
|
@remote_addr, @local_addr = addresses
|
22
27
|
@user, @closed, @stanza_size = nil, false, 0
|
@@ -33,23 +38,34 @@ module Vines
|
|
33
38
|
# stream is first connected as well as for stream restarts during
|
34
39
|
# negotiation. Subclasses can override this method to provide a different
|
35
40
|
# type of parser (e.g. HTTP).
|
41
|
+
#
|
42
|
+
# Returns nothing.
|
36
43
|
def create_parser
|
37
|
-
@parser = Parser.new.tap do |
|
38
|
-
|
39
|
-
|
40
|
-
|
44
|
+
@parser = Parser.new.tap do |parser|
|
45
|
+
parser.stream_open {|node| @nodes.push(node) }
|
46
|
+
parser.stream_close { close_connection }
|
47
|
+
parser.stanza {|node| @nodes.push(node) }
|
41
48
|
end
|
42
49
|
end
|
43
50
|
|
44
|
-
# Advance the state machine into the
|
51
|
+
# Advance the state machine into the `Closed` state so any remaining queued
|
45
52
|
# nodes are not processed while we're waiting for EM to actually close the
|
46
53
|
# connection.
|
54
|
+
#
|
55
|
+
# Returns nothing.
|
47
56
|
def close_connection(after_writing=false)
|
48
57
|
super
|
49
58
|
@closed = true
|
50
59
|
advance(Client::Closed.new(self))
|
51
60
|
end
|
52
61
|
|
62
|
+
# Read bytes off the stream and feed them into the XML parser. EventMachine
|
63
|
+
# is responsible for calling this method on its event loop as connections
|
64
|
+
# become readable.
|
65
|
+
#
|
66
|
+
# data - The byte String sent to the server from the client, hopefully XML.
|
67
|
+
#
|
68
|
+
# Returns nothing.
|
53
69
|
def receive_data(data)
|
54
70
|
return if @closed
|
55
71
|
@stanza_size += data.bytesize
|
@@ -62,6 +78,8 @@ module Vines
|
|
62
78
|
|
63
79
|
# Reset the connection's XML parser when a new <stream:stream> header
|
64
80
|
# is received.
|
81
|
+
#
|
82
|
+
# Returns nothing.
|
65
83
|
def reset
|
66
84
|
create_parser
|
67
85
|
end
|
@@ -72,7 +90,7 @@ module Vines
|
|
72
90
|
@config.storage(domain || self.domain)
|
73
91
|
end
|
74
92
|
|
75
|
-
# Returns the
|
93
|
+
# Returns the Config::Host virtual host for the stream's domain.
|
76
94
|
def vhost
|
77
95
|
@config.vhost(domain)
|
78
96
|
end
|
@@ -80,6 +98,10 @@ module Vines
|
|
80
98
|
# Reload the user's information into their active connections. Call this
|
81
99
|
# after storage.save_user() to sync the new user state with their other
|
82
100
|
# connections.
|
101
|
+
#
|
102
|
+
# user - The User whose connection info needs refreshing.
|
103
|
+
#
|
104
|
+
# Returns nothing.
|
83
105
|
def update_user_streams(user)
|
84
106
|
connected_resources(user.jid.bare).each do |stream|
|
85
107
|
stream.user.update_from(user)
|
@@ -112,6 +134,10 @@ module Vines
|
|
112
134
|
end
|
113
135
|
|
114
136
|
# Send the data over the wire to this client.
|
137
|
+
#
|
138
|
+
# data - The XML String or XML::Node to write to the socket.
|
139
|
+
#
|
140
|
+
# Returns nothing.
|
115
141
|
def write(data)
|
116
142
|
log_node(data, :out)
|
117
143
|
if data.respond_to?(:to_xml)
|
@@ -139,7 +165,11 @@ module Vines
|
|
139
165
|
end
|
140
166
|
|
141
167
|
# Advance the stream's state machine to the new state. XML nodes received
|
142
|
-
# by the stream will be passed to this state's
|
168
|
+
# by the stream will be passed to this state's `node` method.
|
169
|
+
#
|
170
|
+
# state - The Stream::State to process the stanzas next.
|
171
|
+
#
|
172
|
+
# Returns the new Stream::State.
|
143
173
|
def advance(state)
|
144
174
|
@state = state
|
145
175
|
end
|
@@ -147,6 +177,10 @@ module Vines
|
|
147
177
|
# Stream level errors close the stream while stanza and SASL errors are
|
148
178
|
# written to the client and leave the stream open. All exceptions should
|
149
179
|
# pass through this method for consistent handling.
|
180
|
+
#
|
181
|
+
# e - The StandardError, usually XmppError, that occurred.
|
182
|
+
#
|
183
|
+
# Returns nothing.
|
150
184
|
def error(e)
|
151
185
|
case e
|
152
186
|
when SaslError, StanzaError
|
@@ -167,7 +201,9 @@ module Vines
|
|
167
201
|
|
168
202
|
private
|
169
203
|
|
170
|
-
#
|
204
|
+
# Determine the remote and local socket addresses used by this connection.
|
205
|
+
#
|
206
|
+
# Returns a two-element Array of String addresses.
|
171
207
|
def addresses
|
172
208
|
[get_peername, get_sockname].map do |addr|
|
173
209
|
addr ? Socket.unpack_sockaddr_in(addr)[0, 2].reverse.join(':') : 'unknown'
|
@@ -176,12 +212,21 @@ module Vines
|
|
176
212
|
|
177
213
|
# Write the StreamError's xml to the stream. Subclasses can override
|
178
214
|
# this method with custom error writing behavior.
|
215
|
+
#
|
216
|
+
# A call to `close_stream` should follow this method. Stream level errors
|
217
|
+
# are fatal to the connection.
|
218
|
+
#
|
219
|
+
# e - The StreamError that caused the connection to close.
|
220
|
+
#
|
221
|
+
# Returns nothing.
|
179
222
|
def send_stream_error(e)
|
180
223
|
write(e.to_xml)
|
181
224
|
end
|
182
225
|
|
183
|
-
# Write a closing stream tag
|
184
|
-
#
|
226
|
+
# Write a closing stream tag and close the connection. Subclasses can
|
227
|
+
# override this method for custom close behavior.
|
228
|
+
#
|
229
|
+
# Returns nothing.
|
185
230
|
def close_stream
|
186
231
|
write('</stream:stream>')
|
187
232
|
close_connection_after_writing
|
@@ -194,7 +239,14 @@ module Vines
|
|
194
239
|
|
195
240
|
# Schedule a queue pop on the EM thread to handle the next element. This
|
196
241
|
# guarantees all stanzas received on this stream are processed in order.
|
197
|
-
#
|
242
|
+
#
|
243
|
+
# http://tools.ietf.org/html/rfc6120#section-10.1
|
244
|
+
#
|
245
|
+
# Once a node is processed, this method recursively schedules itself to pop
|
246
|
+
# the next node and so on. A single call to this method effectively begins
|
247
|
+
# an asynchronous node processing loop.
|
248
|
+
#
|
249
|
+
# Returns nothing.
|
198
250
|
def process_node_queue
|
199
251
|
@nodes.pop do |node|
|
200
252
|
Fiber.new do
|
@@ -232,14 +284,20 @@ module Vines
|
|
232
284
|
["#{label} stanza:".ljust(PAD), from, to, node])
|
233
285
|
end
|
234
286
|
|
235
|
-
#
|
287
|
+
# Inspects the current state of the stream's state machine. Provided as a
|
236
288
|
# method so subclasses can override the behavior.
|
289
|
+
#
|
290
|
+
# Returns the current Stream::State.
|
237
291
|
def state
|
238
292
|
@state
|
239
293
|
end
|
240
294
|
|
241
|
-
#
|
295
|
+
# Determine if this is a valid domain-only JID that can be used in
|
242
296
|
# stream initiation stanza headers.
|
297
|
+
#
|
298
|
+
# jid - The String or JID to verify (e.g. 'wonderland.lit').
|
299
|
+
#
|
300
|
+
# Return true if the jid is domain-only.
|
243
301
|
def valid_address?(jid)
|
244
302
|
JID.new(jid).domain? rescue false
|
245
303
|
end
|
data/lib/vines/stream/client.rb
CHANGED
@@ -60,21 +60,25 @@ module Vines
|
|
60
60
|
|
61
61
|
private
|
62
62
|
|
63
|
-
# The
|
63
|
+
# The `to` domain address set on the initial stream header must not change
|
64
64
|
# during stream restarts. This prevents a user from authenticating in one
|
65
65
|
# domain, then using a stream in a different domain.
|
66
|
+
#
|
67
|
+
# to - The String domain JID to verify (e.g. 'wonderland.lit').
|
68
|
+
#
|
69
|
+
# Returns true if the client connection is misbehaving and should be closed.
|
66
70
|
def domain_change?(to)
|
67
71
|
to != @session.domain
|
68
72
|
end
|
69
73
|
|
70
74
|
def send_stream_header(to)
|
71
75
|
attrs = {
|
72
|
-
'xmlns'
|
76
|
+
'xmlns' => NAMESPACES[:client],
|
73
77
|
'xmlns:stream' => NAMESPACES[:stream],
|
74
|
-
'xml:lang'
|
75
|
-
'id'
|
76
|
-
'from'
|
77
|
-
'version'
|
78
|
+
'xml:lang' => 'en',
|
79
|
+
'id' => Kit.uuid,
|
80
|
+
'from' => @session.domain,
|
81
|
+
'version' => '1.0'
|
78
82
|
}
|
79
83
|
attrs['to'] = to if to
|
80
84
|
write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
|
@@ -46,10 +46,10 @@ module Vines
|
|
46
46
|
|
47
47
|
def send_stream_header
|
48
48
|
attrs = {
|
49
|
-
'xmlns'
|
49
|
+
'xmlns' => NAMESPACES[:component],
|
50
50
|
'xmlns:stream' => NAMESPACES[:stream],
|
51
|
-
'id'
|
52
|
-
'from'
|
51
|
+
'id' => @stream_id,
|
52
|
+
'from' => @remote_domain
|
53
53
|
}
|
54
54
|
write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
|
55
55
|
end
|
data/lib/vines/stream/http.rb
CHANGED
@@ -10,22 +10,28 @@ module Vines
|
|
10
10
|
@session = Http::Session.new(self)
|
11
11
|
end
|
12
12
|
|
13
|
-
# Override
|
13
|
+
# Override Stream#create_parser to provide an HTTP parser rather than
|
14
14
|
# a Nokogiri XML parser.
|
15
|
+
#
|
16
|
+
# Returns nothing.
|
15
17
|
def create_parser
|
16
|
-
@parser = ::Http::Parser.new.tap do |
|
18
|
+
@parser = ::Http::Parser.new.tap do |parser|
|
17
19
|
body = ''
|
18
|
-
|
19
|
-
|
20
|
+
parser.on_body = proc {|data| body << data }
|
21
|
+
parser.on_message_complete = proc do
|
20
22
|
process_request(Request.new(self, @parser, body))
|
21
23
|
body = ''
|
22
|
-
|
24
|
+
end
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
28
|
# If the session ID is valid, switch this stream's session to the new
|
27
29
|
# ID and return true. Some clients, like Google Chrome, reuse one stream
|
28
30
|
# for multiple sessions.
|
31
|
+
#
|
32
|
+
# sid - The String session ID.
|
33
|
+
#
|
34
|
+
# Returns true if the server previously distributed this SID to a client.
|
29
35
|
def valid_session?(sid)
|
30
36
|
if session = Sessions[sid]
|
31
37
|
@session = session
|
@@ -62,13 +68,27 @@ module Vines
|
|
62
68
|
|
63
69
|
# Override Stream#write to queue stanzas rather than immediately writing
|
64
70
|
# to the stream. Stanza responses must be paired with a queued request.
|
71
|
+
#
|
72
|
+
# If a request is not waiting, the written stanzas will buffer until they
|
73
|
+
# can be sent in the next response.
|
74
|
+
#
|
75
|
+
# data - The XML String or XML::Node to write to the HTTP socket.
|
76
|
+
#
|
77
|
+
# Returns nothing.
|
65
78
|
def write(data)
|
66
79
|
@session.write(data)
|
67
80
|
end
|
68
81
|
|
69
|
-
#
|
82
|
+
# Parse the one or more stanzas from a single body element. BOSH clients
|
83
|
+
# buffer stanzas sent in quick succession, and send them as a bundle, to
|
84
|
+
# save on the request/response cycle.
|
85
|
+
#
|
70
86
|
# TODO This parses the XML again just to strip namespaces. Figure out
|
71
87
|
# Nokogiri namespace handling instead.
|
88
|
+
#
|
89
|
+
# body - The XML::Node containing the BOSH `body` element.
|
90
|
+
#
|
91
|
+
# Returns an Array of XML::Node stanzas.
|
72
92
|
def parse_body(body)
|
73
93
|
body.namespace = nil
|
74
94
|
body.elements.map do |node|
|
@@ -133,8 +153,12 @@ module Vines
|
|
133
153
|
@session.reply(node)
|
134
154
|
end
|
135
155
|
|
136
|
-
# Override
|
156
|
+
# Override Stream#send_stream_error to wrap the error XML in a BOSH
|
137
157
|
# terminate body tag.
|
158
|
+
#
|
159
|
+
# e - The StreamError that caused the connection to close.
|
160
|
+
#
|
161
|
+
# Returns nothing.
|
138
162
|
def send_stream_error(e)
|
139
163
|
doc = Nokogiri::XML::Document.new
|
140
164
|
node = doc.create_element('body',
|
@@ -146,12 +170,14 @@ module Vines
|
|
146
170
|
@session.reply(node)
|
147
171
|
end
|
148
172
|
|
149
|
-
# Override
|
173
|
+
# Override Stream#close_stream to simply close the connection without
|
150
174
|
# writing a closing stream tag.
|
175
|
+
#
|
176
|
+
# Returns nothing.
|
151
177
|
def close_stream
|
152
178
|
close_connection_after_writing
|
153
179
|
@session.close
|
154
180
|
end
|
155
181
|
end
|
156
182
|
end
|
157
|
-
end
|
183
|
+
end
|