vines 0.4.9 → 0.4.10
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.
- 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
|