unicorn 0.1.0

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