unicorn 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.
@@ -0,0 +1,103 @@
1
+ # Copyright (c) 2005 Zed A. Shaw
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+ #
4
+ # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
5
+ # for more information.
6
+
7
+
8
+ HERE = File.dirname(__FILE__) unless defined?(HERE)
9
+ %w(lib ext bin test).each do |dir|
10
+ $LOAD_PATH.unshift "#{HERE}/../#{dir}"
11
+ end
12
+
13
+ require 'test/unit'
14
+ require 'net/http'
15
+ require 'digest/sha1'
16
+ require 'uri'
17
+ require 'stringio'
18
+ require 'unicorn'
19
+ require 'tmpdir'
20
+
21
+ if ENV['DEBUG']
22
+ require 'ruby-debug'
23
+ Debugger.start
24
+ end
25
+
26
+ def redirect_test_io
27
+ orig_err = STDERR.dup
28
+ orig_out = STDOUT.dup
29
+ STDERR.reopen("test_stderr.#{$$}.log")
30
+ STDOUT.reopen("test_stdout.#{$$}.log")
31
+
32
+ at_exit do
33
+ File.unlink("test_stderr.#{$$}.log") rescue nil
34
+ File.unlink("test_stdout.#{$$}.log") rescue nil
35
+ end
36
+
37
+ begin
38
+ yield
39
+ ensure
40
+ STDERR.reopen(orig_err)
41
+ STDOUT.reopen(orig_out)
42
+ end
43
+ end
44
+
45
+ # Either takes a string to do a get request against, or a tuple of [URI, HTTP] where
46
+ # HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.)
47
+ def hit(uris)
48
+ results = []
49
+ uris.each do |u|
50
+ res = nil
51
+
52
+ if u.kind_of? String
53
+ res = Net::HTTP.get(URI.parse(u))
54
+ else
55
+ url = URI.parse(u[0])
56
+ res = Net::HTTP.new(url.host, url.port).start {|h| h.request(u[1]) }
57
+ end
58
+
59
+ assert res != nil, "Didn't get a response: #{u}"
60
+ results << res
61
+ end
62
+
63
+ return results
64
+ end
65
+
66
+ # unused_port provides an unused port on +addr+ usable for TCP that is
67
+ # guaranteed to be unused across all unicorn builds on that system. It
68
+ # prevents race conditions by using a lock file other unicorn builds
69
+ # will see. This is required if you perform several builds in parallel
70
+ # with a continuous integration system or run tests in parallel via
71
+ # gmake. This is NOT guaranteed to be race-free if you run other
72
+ # processes that bind to random ports for testing (but the window
73
+ # for a race condition is very small). You may also set UNICORN_TEST_ADDR
74
+ # to override the default test address (127.0.0.1).
75
+ def unused_port(addr = '127.0.0.1')
76
+ retries = 100
77
+ base = 5000
78
+ port = sock = nil
79
+ begin
80
+ begin
81
+ port = base + rand(32768 - base)
82
+ sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
83
+ sock.bind(Socket.pack_sockaddr_in(port, addr))
84
+ sock.listen(5)
85
+ rescue Errno::EADDRINUSE, Errno::EACCES
86
+ sock.close rescue nil
87
+ retry if (retries -= 1) >= 0
88
+ end
89
+
90
+ # since we'll end up closing the random port we just got, there's a race
91
+ # condition could allow the random port we just chose to reselect itself
92
+ # when running tests in parallel with gmake. Create a lock file while
93
+ # we have the port here to ensure that does not happen .
94
+ lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
95
+ lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
96
+ at_exit { File.unlink(lock_path) rescue nil }
97
+ rescue Errno::EEXIST
98
+ sock.close rescue nil
99
+ retry
100
+ end
101
+ sock.close rescue nil
102
+ port
103
+ end
@@ -0,0 +1,45 @@
1
+ require 'socket'
2
+ require 'stringio'
3
+
4
+ def do_test(st, chunk)
5
+ s = TCPSocket.new('127.0.0.1',ARGV[0].to_i);
6
+ req = StringIO.new(st)
7
+ nout = 0
8
+ randstop = rand(st.length / 10)
9
+ STDERR.puts "stopping after: #{randstop}"
10
+
11
+ begin
12
+ while data = req.read(chunk)
13
+ nout += s.write(data)
14
+ s.flush
15
+ sleep 0.1
16
+ if nout > randstop
17
+ STDERR.puts "BANG! after #{nout} bytes."
18
+ break
19
+ end
20
+ end
21
+ rescue Object => e
22
+ STDERR.puts "ERROR: #{e}"
23
+ ensure
24
+ s.close
25
+ end
26
+ end
27
+
28
+ content = "-" * (1024 * 240)
29
+ st = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\nContent-Length: #{content.length}\r\n\r\n#{content}"
30
+
31
+ puts "length: #{content.length}"
32
+
33
+ threads = []
34
+ ARGV[1].to_i.times do
35
+ t = Thread.new do
36
+ size = 100
37
+ puts ">>>> #{size} sized chunks"
38
+ do_test(st, size)
39
+ end
40
+
41
+ t.abort_on_exception = true
42
+ threads << t
43
+ end
44
+
45
+ threads.each {|t| t.join}
@@ -0,0 +1,48 @@
1
+ require 'test/unit'
2
+ require 'tempfile'
3
+ require 'unicorn/configurator'
4
+
5
+ class TestConfigurator < Test::Unit::TestCase
6
+
7
+ def test_config_defaults
8
+ assert_nothing_raised { Unicorn::Configurator.new {} }
9
+ end
10
+
11
+ def test_config_invalid
12
+ tmp = Tempfile.new('unicorn_config')
13
+ tmp.syswrite(%q(asdfasdf "hello-world"))
14
+ assert_raises(NoMethodError) do
15
+ Unicorn::Configurator.new(:config_file => tmp.path)
16
+ end
17
+ end
18
+
19
+ def test_config_non_existent
20
+ tmp = Tempfile.new('unicorn_config')
21
+ path = tmp.path
22
+ tmp.close!
23
+ assert_raises(Errno::ENOENT) do
24
+ Unicorn::Configurator.new(:config_file => path)
25
+ end
26
+ end
27
+
28
+ def test_config_defaults
29
+ cfg = Unicorn::Configurator.new(:use_defaults => true)
30
+ assert_nothing_raised { cfg.commit!(self) }
31
+ Unicorn::Configurator::DEFAULTS.each do |key,value|
32
+ assert_equal value, instance_variable_get("@#{key.to_s}")
33
+ end
34
+ end
35
+
36
+ def test_config_defaults_skip
37
+ cfg = Unicorn::Configurator.new(:use_defaults => true)
38
+ skip = [ :logger ]
39
+ assert_nothing_raised { cfg.commit!(self, :skip => skip) }
40
+ @logger = nil
41
+ Unicorn::Configurator::DEFAULTS.each do |key,value|
42
+ next if skip.include?(key)
43
+ assert_equal value, instance_variable_get("@#{key.to_s}")
44
+ end
45
+ assert_nil @logger
46
+ end
47
+
48
+ end
@@ -0,0 +1,161 @@
1
+ # Copyright (c) 2005 Zed A. Shaw
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+ #
4
+ # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
5
+ # for more information.
6
+
7
+ require 'test/test_helper'
8
+
9
+ include Unicorn
10
+
11
+ class HttpParserTest < Test::Unit::TestCase
12
+
13
+ def test_parse_simple
14
+ parser = HttpParser.new
15
+ req = {}
16
+ http = "GET / HTTP/1.1\r\n\r\n"
17
+ nread = parser.execute(req, http, 0)
18
+
19
+ assert nread == http.length, "Failed to parse the full HTTP request"
20
+ assert parser.finished?, "Parser didn't finish"
21
+ assert !parser.error?, "Parser had error"
22
+ assert nread == parser.nread, "Number read returned from execute does not match"
23
+
24
+ assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
25
+ assert_equal '/', req['REQUEST_PATH']
26
+ assert_equal 'HTTP/1.1', req['HTTP_VERSION']
27
+ assert_equal '/', req['REQUEST_URI']
28
+ assert_equal 'CGI/1.2', req['GATEWAY_INTERFACE']
29
+ assert_equal 'GET', req['REQUEST_METHOD']
30
+ assert_nil req['FRAGMENT']
31
+ assert_nil req['QUERY_STRING']
32
+
33
+ parser.reset
34
+ assert parser.nread == 0, "Number read after reset should be 0"
35
+ end
36
+
37
+ def test_parse_strange_headers
38
+ parser = HttpParser.new
39
+ req = {}
40
+ should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n"
41
+ nread = parser.execute(req, should_be_good, 0)
42
+ assert_equal should_be_good.length, nread
43
+ assert parser.finished?
44
+ assert !parser.error?
45
+
46
+ # ref: http://thread.gmane.org/gmane.comp.lang.ruby.Unicorn.devel/37/focus=45
47
+ # (note we got 'pen' mixed up with 'pound' in that thread,
48
+ # but the gist of it is still relevant: these nasty headers are irrelevant
49
+ #
50
+ # nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit: -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n"
51
+ # parser = HttpParser.new
52
+ # req = {}
53
+ # nread = parser.execute(req, nasty_pound_header, 0)
54
+ # assert_equal nasty_pound_header.length, nread
55
+ # assert parser.finished?
56
+ # assert !parser.error?
57
+ end
58
+
59
+ def test_parse_ie6_urls
60
+ %w(/some/random/path"
61
+ /some/random/path>
62
+ /some/random/path<
63
+ /we/love/you/ie6?q=<"">
64
+ /url?<="&>="
65
+ /mal"formed"?
66
+ ).each do |path|
67
+ parser = HttpParser.new
68
+ req = {}
69
+ sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n)
70
+ nread = parser.execute(req, sorta_safe, 0)
71
+ assert_equal sorta_safe.length, nread
72
+ assert parser.finished?
73
+ assert !parser.error?
74
+ end
75
+ end
76
+
77
+ def test_parse_error
78
+ parser = HttpParser.new
79
+ req = {}
80
+ bad_http = "GET / SsUTF/1.1"
81
+
82
+ error = false
83
+ begin
84
+ nread = parser.execute(req, bad_http, 0)
85
+ rescue => details
86
+ error = true
87
+ end
88
+
89
+ assert error, "failed to throw exception"
90
+ assert !parser.finished?, "Parser shouldn't be finished"
91
+ assert parser.error?, "Parser SHOULD have error"
92
+ end
93
+
94
+ def test_fragment_in_uri
95
+ parser = HttpParser.new
96
+ req = {}
97
+ get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
98
+ assert_nothing_raised do
99
+ parser.execute(req, get, 0)
100
+ end
101
+ assert parser.finished?
102
+ assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
103
+ assert_equal 'posts-17408', req['FRAGMENT']
104
+ end
105
+
106
+ # lame random garbage maker
107
+ def rand_data(min, max, readable=true)
108
+ count = min + ((rand(max)+1) *10).to_i
109
+ res = count.to_s + "/"
110
+
111
+ if readable
112
+ res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40)
113
+ else
114
+ res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20)
115
+ end
116
+
117
+ return res
118
+ end
119
+
120
+
121
+ def test_horrible_queries
122
+ parser = HttpParser.new
123
+
124
+ # then that large header names are caught
125
+ 10.times do |c|
126
+ get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n"
127
+ assert_raises Unicorn::HttpParserError do
128
+ parser.execute({}, get, 0)
129
+ parser.reset
130
+ end
131
+ end
132
+
133
+ # then that large mangled field values are caught
134
+ 10.times do |c|
135
+ get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
136
+ assert_raises Unicorn::HttpParserError do
137
+ parser.execute({}, get, 0)
138
+ parser.reset
139
+ end
140
+ end
141
+
142
+ # then large headers are rejected too
143
+ get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n"
144
+ get << "X-Test: test\r\n" * (80 * 1024)
145
+ assert_raises Unicorn::HttpParserError do
146
+ parser.execute({}, get, 0)
147
+ parser.reset
148
+ end
149
+
150
+ # finally just that random garbage gets blocked all the time
151
+ 10.times do |c|
152
+ get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
153
+ assert_raises Unicorn::HttpParserError do
154
+ parser.execute({}, get, 0)
155
+ parser.reset
156
+ end
157
+ end
158
+
159
+ end
160
+ end
161
+
@@ -0,0 +1,45 @@
1
+ # Copyright (c) 2005 Zed A. Shaw
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+ #
4
+ # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
5
+ # for more information.
6
+
7
+ require 'test/test_helper'
8
+
9
+ include Unicorn
10
+
11
+ class ResponseTest < Test::Unit::TestCase
12
+
13
+ def test_response_headers
14
+ out = StringIO.new
15
+ HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])
16
+
17
+ assert out.length > 0, "output didn't have data"
18
+ end
19
+
20
+ def test_response_OFS_set
21
+ old_ofs = $,
22
+ $, = "\f\v"
23
+ out = StringIO.new
24
+ HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])
25
+ resp = out.read
26
+ assert ! resp.include?("\f\v"), "output didn't use $, ($OFS)"
27
+ ensure
28
+ $, = old_ofs
29
+ end
30
+
31
+ def test_response_200
32
+ io = StringIO.new
33
+ HttpResponse.write(io, [200, {}, []])
34
+ assert io.length > 0, "output didn't have data"
35
+ end
36
+
37
+ def test_response_with_default_reason
38
+ code = 400
39
+ io = StringIO.new
40
+ HttpResponse.write(io, [code, {}, []])
41
+ io.rewind
42
+ assert_match(/.* #{HTTP_STATUS_CODES[code]}$/, io.readline.chomp, "wrong default reason phrase")
43
+ end
44
+ end
45
+
@@ -0,0 +1,96 @@
1
+ # Copyright (c) 2005 Zed A. Shaw
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+ #
4
+ # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
5
+ # for more information.
6
+
7
+ require 'test/test_helper'
8
+
9
+ include Unicorn
10
+
11
+ class TestHandler
12
+
13
+ def call(env)
14
+ # response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n")
15
+ [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
16
+ end
17
+ end
18
+
19
+
20
+ class WebServerTest < Test::Unit::TestCase
21
+
22
+ def setup
23
+ @valid_request = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\n\r\n"
24
+ @port = unused_port
25
+ @tester = TestHandler.new
26
+ redirect_test_io do
27
+ @server = HttpServer.new(@tester, :listeners => [ "127.0.0.1:#{@port}" ] )
28
+ end
29
+ @server.start
30
+ end
31
+
32
+ def teardown
33
+ redirect_test_io do
34
+ @server.stop(true)
35
+ end
36
+ end
37
+
38
+ def test_simple_server
39
+ results = hit(["http://localhost:#{@port}/test"])
40
+ assert_equal 'hello!\n', results[0], "Handler didn't really run"
41
+ end
42
+
43
+
44
+ def do_test(string, chunk, close_after=nil, shutdown_delay=0)
45
+ # Do not use instance variables here, because it needs to be thread safe
46
+ socket = TCPSocket.new("127.0.0.1", @port);
47
+ request = StringIO.new(string)
48
+ chunks_out = 0
49
+
50
+ while data = request.read(chunk)
51
+ chunks_out += socket.write(data)
52
+ socket.flush
53
+ sleep 0.2
54
+ if close_after and chunks_out > close_after
55
+ socket.close
56
+ sleep 1
57
+ end
58
+ end
59
+ sleep(shutdown_delay)
60
+ socket.write(" ") # Some platforms only raise the exception on attempted write
61
+ socket.flush
62
+ end
63
+
64
+ def test_trickle_attack
65
+ do_test(@valid_request, 3)
66
+ end
67
+
68
+ def test_close_client
69
+ assert_raises IOError do
70
+ do_test(@valid_request, 10, 20)
71
+ end
72
+ end
73
+
74
+ def test_bad_client
75
+ redirect_test_io do
76
+ do_test("GET /test HTTP/BAD", 3)
77
+ end
78
+ end
79
+
80
+ def test_header_is_too_long
81
+ redirect_test_io do
82
+ long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n"
83
+ assert_raises Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EINVAL, IOError do
84
+ do_test(long, long.length/2, 10)
85
+ end
86
+ end
87
+ end
88
+
89
+ def test_file_streamed_request
90
+ body = "a" * (Unicorn::Const::MAX_BODY * 2)
91
+ long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
92
+ do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400)
93
+ end
94
+
95
+ end
96
+