vines 0.4.9 → 0.4.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -25,13 +25,22 @@ module Vines
25
25
 
26
26
  attr_reader :stream, :body, :headers, :method, :path, :url, :query
27
27
 
28
+ # Create a new request parsed from an HTTP client connection. We'll try
29
+ # to keep this request open until there are stanzas available to send
30
+ # as a response.
31
+ #
32
+ # stream - The Stream::Http client connection that received the request.
33
+ # parser - The Http::Parser that parsed the HTTP request.
34
+ # body - The String request body.
28
35
  def initialize(stream, parser, body)
29
- @stream, @body = stream, body
36
+ uri = URI(parser.request_url)
37
+ @stream = stream
38
+ @body = body
30
39
  @headers = parser.headers
31
40
  @method = parser.http_method
32
- @path = parser.request_path
33
41
  @url = parser.request_url
34
- @query = parser.query_string
42
+ @path = uri.path
43
+ @query = uri.query
35
44
  @received = Time.now
36
45
  end
37
46
 
@@ -44,10 +53,12 @@ module Vines
44
53
  # directory. Take care to prevent directory traversal attacks with paths
45
54
  # like ../../../etc/passwd. Use the If-Modified-Since request header
46
55
  # to implement caching.
56
+ #
57
+ # Returns nothing.
47
58
  def reply_with_file(dir)
48
59
  path = File.expand_path(File.join(dir, @path))
49
60
 
50
- # redirect requests missing a slash so relative links work
61
+ # Redirect requests missing a slash so relative links work.
51
62
  if File.directory?(path) && !@path.end_with?('/')
52
63
  send_status(301, MOVED, "Location: #{redirect_uri}")
53
64
  return
@@ -69,6 +80,8 @@ module Vines
69
80
 
70
81
  # Send an HTTP 200 OK response wrapping the XMPP node content back
71
82
  # to the client.
83
+ #
84
+ # Returns nothing.
72
85
  def reply(node, content_type)
73
86
  body = node.to_s
74
87
  header = [
@@ -90,6 +103,8 @@ module Vines
90
103
  # Send a 200 OK response, allowing any origin domain to connect to the
91
104
  # server, in response to CORS preflight OPTIONS requests. This allows
92
105
  # any web application using strophe.js to connect to our BOSH port.
106
+ #
107
+ # Returns nothing.
93
108
  def reply_to_options
94
109
  allow = @headers['Access-Control-Request-Headers']
95
110
  headers = [
@@ -107,6 +122,8 @@ module Vines
107
122
  # wasn't sent by the client, just return the relative path that
108
123
  # was requested. The Location response header must contain the fully
109
124
  # qualified URI, but most browsers will accept relative paths as well.
125
+ #
126
+ # Returns the String URL.
110
127
  def redirect_uri
111
128
  host = headers['Host']
112
129
  uri = "#{path}/"
@@ -137,6 +154,8 @@ module Vines
137
154
  # Stream the contents of the file to the client in a 200 OK response.
138
155
  # Send a Last-Modified response header so clients can send us an
139
156
  # If-Modified-Since request header for caching.
157
+ #
158
+ # Returns nothing.
140
159
  def send_file(path, status=200, message='OK')
141
160
  header = [
142
161
  "HTTP/1.1 #{status} #{message}",
@@ -162,6 +181,8 @@ module Vines
162
181
  # HTTP server. Reverse proxy servers (nginx/apache) can use this cookie
163
182
  # to implement sticky sessions. Return nil if vroute was not set in
164
183
  # config.rb and no cookie should be sent.
184
+ #
185
+ # Returns a String cookie value or nil if disabled.
165
186
  def vroute_cookie
166
187
  route = @stream.config[:http].vroute
167
188
  route ? "Set-Cookie: vroute=#{route}; path=/; HttpOnly" : nil
@@ -169,4 +190,4 @@ module Vines
169
190
  end
170
191
  end
171
192
  end
172
- end
193
+ end
@@ -73,6 +73,10 @@ module Vines
73
73
 
74
74
  # Send an HTTP 200 OK response wrapping the XMPP node content back
75
75
  # to the client.
76
+ #
77
+ # node - The XML::Node to send to the client.
78
+ #
79
+ # Returns nothing.
76
80
  def reply(node)
77
81
  if request = @requests.shift
78
82
  request.reply(node, @content_type)
@@ -83,6 +87,10 @@ module Vines
83
87
  # Write the XMPP node to the client stream after wrapping it in a BOSH
84
88
  # body tag. If there's a waiting request, the node is written
85
89
  # immediately. If not, it's queued until the next request arrives.
90
+ #
91
+ # data - The XML String or XML::Node to send in the next HTTP response.
92
+ #
93
+ # Returns nothing.
86
94
  def write(node)
87
95
  if request = @requests.shift
88
96
  request.reply(wrap_body(node), @content_type)
@@ -125,23 +125,29 @@ module Vines
125
125
 
126
126
  private
127
127
 
128
- # The +to+ and +from+ domain addresses set on the initial stream header
128
+ # The `to` and `from` domain addresses set on the initial stream header
129
129
  # must not change during stream restarts. This prevents a server from
130
130
  # authenticating as one domain, then sending stanzas from users in a
131
131
  # different domain.
132
+ #
133
+ # to - The String domain the other server thinks we are.
134
+ # from - The String domain the other server is asserting as its identity.
135
+ #
136
+ # Returns true if the other server is misbehaving and its connection
137
+ # should be closed.
132
138
  def domain_change?(to, from)
133
139
  to != @domain || from != @remote_domain
134
140
  end
135
141
 
136
142
  def send_stream_header
137
143
  attrs = {
138
- 'xmlns' => NAMESPACES[:server],
144
+ 'xmlns' => NAMESPACES[:server],
139
145
  'xmlns:stream' => NAMESPACES[:stream],
140
- 'xml:lang' => 'en',
141
- 'id' => Kit.uuid,
142
- 'from' => @domain,
143
- 'to' => @remote_domain,
144
- 'version' => '1.0'
146
+ 'xml:lang' => 'en',
147
+ 'id' => Kit.uuid,
148
+ 'from' => @domain,
149
+ 'to' => @remote_domain,
150
+ 'version' => '1.0'
145
151
  }
146
152
  write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
147
153
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Vines
4
- VERSION = '0.4.9'
4
+ VERSION = '0.4.10'
5
5
  end
@@ -3,96 +3,113 @@
3
3
  require 'test_helper'
4
4
 
5
5
  describe Vines::Store do
6
- before do
7
- dir = 'conf/certs'
8
-
9
- domain, key = certificate('wonderland.lit')
10
- File.open("#{dir}/wonderland.lit.crt", 'w') {|f| f.write(domain) }
11
- File.open("#{dir}/wonderland.lit.key", 'w') {|f| f.write(key) }
12
-
13
- wildcard, key = certificate('*.wonderland.lit')
14
- File.open("#{dir}/wildcard.lit.crt", 'w') {|f| f.write(wildcard) }
15
- File.open("#{dir}/wildcard.lit.key", 'w') {|f| f.write(key) }
6
+ let(:dir) { 'conf/certs' }
7
+ let(:domain_pair) { certificate('wonderland.lit') }
8
+ let(:wildcard_pair) { certificate('*.wonderland.lit') }
9
+ subject { Vines::Store.new(dir) }
16
10
 
17
- @store = Vines::Store.new('conf/certs')
11
+ before do
12
+ @files =
13
+ save('wonderland.lit', domain_pair) +
14
+ save('wildcard.lit', wildcard_pair) +
15
+ save('duplicate.lit', domain_pair)
18
16
  end
19
17
 
20
18
  after do
21
- %w[wonderland.lit.crt wonderland.lit.key wildcard.lit.crt wildcard.lit.key].each do |f|
22
- name = "conf/certs/#{f}"
19
+ @files.each do |name|
23
20
  File.delete(name) if File.exists?(name)
24
21
  end
25
22
  end
26
23
 
27
- it 'parses certificate files' do
28
- refute @store.certs.empty?
29
- assert_equal OpenSSL::X509::Certificate, @store.certs.first.class
30
- end
24
+ describe 'creating a store' do
25
+ it 'parses certificate files' do
26
+ refute subject.certs.empty?
27
+ assert_equal OpenSSL::X509::Certificate, subject.certs.first.class
28
+ end
29
+
30
+ it 'ignores expired certificates' do
31
+ assert subject.certs.all? {|c| c.not_after > Time.new }
32
+ end
31
33
 
32
- it 'ignores expired certificates' do
33
- assert @store.certs.all? {|c| c.not_after > Time.new }
34
+ it 'does not raise an error for duplicate certificates' do
35
+ assert Vines::Store.new(dir)
36
+ end
34
37
  end
35
38
 
36
39
  describe 'files_for_domain' do
37
40
  it 'handles invalid input' do
38
- assert_nil @store.files_for_domain(nil)
39
- assert_nil @store.files_for_domain('')
41
+ assert_nil subject.files_for_domain(nil)
42
+ assert_nil subject.files_for_domain('')
40
43
  end
41
44
 
42
45
  it 'finds files by name' do
43
- refute_nil @store.files_for_domain('wonderland.lit')
44
- cert, key = @store.files_for_domain('wonderland.lit')
46
+ refute_nil subject.files_for_domain('wonderland.lit')
47
+ cert, key = subject.files_for_domain('wonderland.lit')
45
48
  assert_certificate_matches_key cert, key
46
49
  assert_equal 'wonderland.lit.crt', File.basename(cert)
47
50
  assert_equal 'wonderland.lit.key', File.basename(key)
48
51
  end
49
52
 
50
53
  it 'finds files for wildcard' do
51
- refute_nil @store.files_for_domain('foo.wonderland.lit')
52
- cert, key = @store.files_for_domain('foo.wonderland.lit')
54
+ refute_nil subject.files_for_domain('foo.wonderland.lit')
55
+ cert, key = subject.files_for_domain('foo.wonderland.lit')
53
56
  assert_certificate_matches_key cert, key
54
57
  assert_equal 'wildcard.lit.crt', File.basename(cert)
55
58
  assert_equal 'wildcard.lit.key', File.basename(key)
56
59
  end
57
60
  end
58
61
 
62
+ describe 'trusted?' do
63
+ it 'does not trust malformed certificates' do
64
+ refute subject.trusted?('bogus')
65
+ end
66
+
67
+ it 'does not trust unsigned certificates' do
68
+ pair = certificate('something.lit')
69
+ refute subject.trusted?(pair.cert)
70
+ end
71
+ end
72
+
59
73
  describe 'domain?' do
60
74
  it 'handles invalid input' do
61
- cert, key = certificate('wonderland.lit')
62
- refute @store.domain?(nil, nil)
63
- refute @store.domain?(cert, nil)
64
- refute @store.domain?(cert, '')
65
- refute @store.domain?(nil, '')
66
- assert @store.domain?(cert, 'wonderland.lit')
75
+ pair = certificate('wonderland.lit')
76
+ refute subject.domain?(nil, nil)
77
+ refute subject.domain?(pair.cert, nil)
78
+ refute subject.domain?(pair.cert, '')
79
+ refute subject.domain?(nil, '')
80
+ assert subject.domain?(pair.cert, 'wonderland.lit')
67
81
  end
68
82
 
69
83
  it 'verifies certificate subject domains' do
70
- cert, key = certificate('wonderland.lit')
71
- refute @store.domain?(cert, 'bogus')
72
- refute @store.domain?(cert, 'www.wonderland.lit')
73
- assert @store.domain?(cert, 'wonderland.lit')
84
+ pair = certificate('wonderland.lit')
85
+ refute subject.domain?(pair.cert, 'bogus')
86
+ refute subject.domain?(pair.cert, 'www.wonderland.lit')
87
+ assert subject.domain?(pair.cert, 'wonderland.lit')
74
88
  end
75
89
 
76
90
  it 'verifies certificate subject alt domains' do
77
- cert, key = certificate('wonderland.lit', 'www.wonderland.lit')
78
- refute @store.domain?(cert, 'bogus')
79
- refute @store.domain?(cert, 'tea.wonderland.lit')
80
- assert @store.domain?(cert, 'www.wonderland.lit')
81
- assert @store.domain?(cert, 'wonderland.lit')
91
+ pair = certificate('wonderland.lit', 'www.wonderland.lit')
92
+ refute subject.domain?(pair.cert, 'bogus')
93
+ refute subject.domain?(pair.cert, 'tea.wonderland.lit')
94
+ assert subject.domain?(pair.cert, 'www.wonderland.lit')
95
+ assert subject.domain?(pair.cert, 'wonderland.lit')
82
96
  end
83
97
 
84
98
  it 'verifies certificate wildcard domains' do
85
- cert, key = certificate('wonderland.lit', '*.wonderland.lit')
86
- refute @store.domain?(cert, 'bogus')
87
- refute @store.domain?(cert, 'one.two.wonderland.lit')
88
- assert @store.domain?(cert, 'tea.wonderland.lit')
89
- assert @store.domain?(cert, 'www.wonderland.lit')
90
- assert @store.domain?(cert, 'wonderland.lit')
99
+ pair = certificate('wonderland.lit', '*.wonderland.lit')
100
+ refute subject.domain?(pair.cert, 'bogus')
101
+ refute subject.domain?(pair.cert, 'one.two.wonderland.lit')
102
+ assert subject.domain?(pair.cert, 'tea.wonderland.lit')
103
+ assert subject.domain?(pair.cert, 'www.wonderland.lit')
104
+ assert subject.domain?(pair.cert, 'wonderland.lit')
91
105
  end
92
106
  end
93
107
 
94
108
  private
95
109
 
110
+ # A public certificate + private key pair.
111
+ Pair = Struct.new(:cert, :key)
112
+
96
113
  def assert_certificate_matches_key(cert, key)
97
114
  refute_nil cert
98
115
  refute_nil key
@@ -102,7 +119,7 @@ describe Vines::Store do
102
119
  end
103
120
 
104
121
  def certificate(domain, altname=nil)
105
- # use small key so tests are fast
122
+ # Use small key so tests are fast.
106
123
  key = OpenSSL::PKey::RSA.generate(256)
107
124
 
108
125
  name = OpenSSL::X509::Name.parse("/C=US/ST=Colorado/L=Denver/O=Test/CN=#{domain}")
@@ -125,6 +142,21 @@ describe Vines::Store do
125
142
  ].map {|k, v| factory.create_ext(k, v) }
126
143
  end
127
144
 
128
- [cert.to_pem, key.to_pem]
145
+ Pair.new(cert.to_pem, key.to_pem)
146
+ end
147
+
148
+ # Write the domain's certificate and private key files to the filesystem for
149
+ # the store to use.
150
+ #
151
+ # domain - The domain name String to use in the file name (e.g. wonderland.lit).
152
+ # pair - The Pair containing the public certificate and private key data.
153
+ #
154
+ # Returns a String Array of file names that were written.
155
+ def save(domain, pair)
156
+ crt = File.expand_path("#{domain}.crt", dir)
157
+ key = File.expand_path("#{domain}.key", dir)
158
+ File.open(crt, 'w') {|f| f.write(pair.cert) }
159
+ File.open(key, 'w') {|f| f.write(pair.key) }
160
+ [crt, key]
129
161
  end
130
162
  end
@@ -3,20 +3,13 @@
3
3
  require 'test_helper'
4
4
 
5
5
  describe Vines::Stream::Http::Request do
6
- PASSWORD = File.expand_path('../passwords')
7
- INDEX = File.expand_path('index.html')
6
+ PASSWORD = File.expand_path('../passwords').freeze
7
+ INDEX = File.expand_path('index.html').freeze
8
8
 
9
9
  before do
10
10
  File.open(PASSWORD, 'w') {|f| f.puts '/etc/passwd contents' }
11
11
  File.open(INDEX, 'w') {|f| f.puts 'index.html contents' }
12
-
13
12
  @stream = MiniTest::Mock.new
14
- @parser = MiniTest::Mock.new
15
- @parser.expect(:headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'})
16
- @parser.expect(:http_method, 'GET')
17
- @parser.expect(:request_path, '/blogs/12')
18
- @parser.expect(:request_url, '/blogs/12?ok=true')
19
- @parser.expect(:query_string, 'ok=true')
20
13
  end
21
14
 
22
15
  after do
@@ -26,21 +19,26 @@ describe Vines::Stream::Http::Request do
26
19
 
27
20
  describe 'initialize' do
28
21
  it 'copies request info from parser' do
29
- request = Vines::Stream::Http::Request.new(@stream, @parser, '<html></html>')
30
- assert_equal request.headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'}
22
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
23
+ parser = parser('GET', '/blogs/12?ok=true', headers)
24
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
25
+
26
+ assert_equal request.headers, {'Host' => 'wonderland.lit', 'Content-Type' => 'text/html'}
31
27
  assert_equal request.method, 'GET'
32
28
  assert_equal request.path, '/blogs/12'
33
29
  assert_equal request.url, '/blogs/12?ok=true'
34
30
  assert_equal request.query, 'ok=true'
35
31
  assert_equal request.body, '<html></html>'
36
32
  assert @stream.verify
37
- assert @parser.verify
38
33
  end
39
34
  end
40
35
 
41
36
  describe 'reply_with_file' do
42
37
  it 'returns 404 file not found' do
43
- request = Vines::Stream::Http::Request.new(@stream, @parser, '<html></html>')
38
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
39
+ parser = parser('GET', '/blogs/12?ok=true', headers)
40
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
41
+
44
42
  headers = [
45
43
  "HTTP/1.1 404 Not Found",
46
44
  "Content-Length: 0"
@@ -50,17 +48,11 @@ describe Vines::Stream::Http::Request do
50
48
 
51
49
  request.reply_with_file(Dir.pwd)
52
50
  assert @stream.verify
53
- assert @parser.verify
54
51
  end
55
52
 
56
53
  it 'prevents directory traversal with 404 response' do
57
- parser = MiniTest::Mock.new
58
- parser.expect(:headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'})
59
- parser.expect(:http_method, 'GET')
60
- parser.expect(:request_path, '/../passwords')
61
- parser.expect(:request_url, '/../passwords')
62
- parser.expect(:query_string, '')
63
-
54
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
55
+ parser = parser('GET', '/../passwords', headers)
64
56
  request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
65
57
 
66
58
  headers = [
@@ -72,17 +64,11 @@ describe Vines::Stream::Http::Request do
72
64
 
73
65
  request.reply_with_file(Dir.pwd)
74
66
  assert @stream.verify
75
- assert parser.verify
76
67
  end
77
68
 
78
69
  it 'serves index.html for directory request' do
79
- parser = MiniTest::Mock.new
80
- parser.expect(:headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'})
81
- parser.expect(:http_method, 'GET')
82
- parser.expect(:request_path, '/')
83
- parser.expect(:request_url, '/?ok=true')
84
- parser.expect(:query_string, 'ok=true')
85
-
70
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
71
+ parser = parser('GET', '/?ok=true', headers)
86
72
  request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
87
73
 
88
74
  mtime = File.mtime(INDEX).utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
@@ -98,17 +84,11 @@ describe Vines::Stream::Http::Request do
98
84
 
99
85
  request.reply_with_file(Dir.pwd)
100
86
  assert @stream.verify
101
- assert parser.verify
102
87
  end
103
88
 
104
89
  it 'redirects for missing trailing slash' do
105
- parser = MiniTest::Mock.new
106
- parser.expect(:headers, {'Content-Type' => 'text/html', 'Host' => 'wonderland.lit'})
107
- parser.expect(:http_method, 'GET')
108
- parser.expect(:request_path, '/http')
109
- parser.expect(:request_url, '/http?ok=true')
110
- parser.expect(:query_string, 'ok=true')
111
-
90
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
91
+ parser = parser('GET', '/http?ok=true', headers)
112
92
  request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
113
93
 
114
94
  headers = [
@@ -121,23 +101,18 @@ describe Vines::Stream::Http::Request do
121
101
  # so the /http url above will work
122
102
  request.reply_with_file(File.expand_path('../../', __FILE__))
123
103
  assert @stream.verify
124
- assert parser.verify
125
104
  end
126
105
  end
127
106
 
128
107
  describe 'reply_to_options' do
129
108
  it 'returns cors headers' do
130
- parser = MiniTest::Mock.new
131
- parser.expect(:headers, {
132
- 'Content-Type' => 'text/xml',
133
- 'Host' => 'wonderland.lit',
134
- 'Origin' => 'remote.wonderland.lit',
135
- 'Access-Control-Request-Headers' => 'Content-Type, Origin'})
136
- parser.expect(:http_method, 'OPTIONS')
137
- parser.expect(:request_path, '/xmpp')
138
- parser.expect(:request_url, '/xmpp')
139
- parser.expect(:query_string, '')
140
-
109
+ headers = [
110
+ 'Content-Type: text/xml',
111
+ 'Host: wonderland.lit',
112
+ 'Origin: remote.wonderland.lit',
113
+ 'Access-Control-Request-Headers: Content-Type, Origin'
114
+ ]
115
+ parser = parser('OPTIONS', '/xmpp', headers)
141
116
  request = Vines::Stream::Http::Request.new(@stream, parser, '')
142
117
 
143
118
  headers = [
@@ -152,7 +127,6 @@ describe Vines::Stream::Http::Request do
152
127
  @stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n"])
153
128
  request.reply_to_options
154
129
  assert @stream.verify
155
- assert parser.verify
156
130
  end
157
131
  end
158
132
 
@@ -168,6 +142,21 @@ describe Vines::Stream::Http::Request do
168
142
 
169
143
  private
170
144
 
145
+ # Create a parser that has completed one valid HTTP request.
146
+ #
147
+ # method - The HTTP method String (e.g. 'GET', 'POST').
148
+ # url - The request URL String (e.g. '/blogs/12?ok=true').
149
+ # headers - The optional Array of request headers.
150
+ #
151
+ # Returns an Http::Parser.
152
+ def parser(method, url, headers = [])
153
+ head = ["#{method} #{url} HTTP/1.1"].concat(headers).join("\r\n")
154
+ body = '<html></html>'
155
+ Http::Parser.new.tap do |parser|
156
+ parser << "%s\r\n\r\n%s" % [head, body]
157
+ end
158
+ end
159
+
171
160
  def reply_with_cookie(cookie)
172
161
  config = Vines::Config.new do
173
162
  host 'wonderland.lit' do
@@ -178,15 +167,12 @@ describe Vines::Stream::Http::Request do
178
167
  end
179
168
  end
180
169
 
181
- parser = MiniTest::Mock.new
182
- parser.expect(:headers, {
183
- 'Content-Type' => 'text/xml',
184
- 'Host' => 'wonderland.lit',
185
- 'Origin' => 'remote.wonderland.lit'})
186
- parser.expect(:http_method, 'POST')
187
- parser.expect(:request_path, '/xmpp')
188
- parser.expect(:request_url, '/xmpp')
189
- parser.expect(:query_string, '')
170
+ headers = [
171
+ 'Content-Type: text/xml',
172
+ 'Host: wonderland.lit',
173
+ 'Origin: remote.wonderland.lit'
174
+ ]
175
+ parser = parser('POST', '/xmpp', headers)
190
176
 
191
177
  request = Vines::Stream::Http::Request.new(@stream, parser, '')
192
178
  message = '<message>hello</message>'
@@ -204,6 +190,5 @@ describe Vines::Stream::Http::Request do
204
190
  @stream.expect(:config, config)
205
191
  request.reply(message, 'application/xml')
206
192
  assert @stream.verify
207
- assert parser.verify
208
193
  end
209
194
  end