giraffesoft-unicorn 0.93.5

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.
Files changed (141) hide show
  1. data/.CHANGELOG.old +25 -0
  2. data/.document +16 -0
  3. data/.gitignore +20 -0
  4. data/.mailmap +26 -0
  5. data/CONTRIBUTORS +31 -0
  6. data/COPYING +339 -0
  7. data/DESIGN +105 -0
  8. data/Documentation/.gitignore +5 -0
  9. data/Documentation/GNUmakefile +30 -0
  10. data/Documentation/unicorn.1.txt +167 -0
  11. data/Documentation/unicorn_rails.1.txt +169 -0
  12. data/GIT-VERSION-GEN +40 -0
  13. data/GNUmakefile +270 -0
  14. data/HACKING +113 -0
  15. data/KNOWN_ISSUES +40 -0
  16. data/LICENSE +55 -0
  17. data/PHILOSOPHY +144 -0
  18. data/README +153 -0
  19. data/Rakefile +108 -0
  20. data/SIGNALS +97 -0
  21. data/TODO +16 -0
  22. data/TUNING +70 -0
  23. data/bin/unicorn +165 -0
  24. data/bin/unicorn_rails +208 -0
  25. data/examples/echo.ru +27 -0
  26. data/examples/git.ru +13 -0
  27. data/examples/init.sh +53 -0
  28. data/ext/unicorn_http/c_util.h +107 -0
  29. data/ext/unicorn_http/common_field_optimization.h +111 -0
  30. data/ext/unicorn_http/ext_help.h +73 -0
  31. data/ext/unicorn_http/extconf.rb +14 -0
  32. data/ext/unicorn_http/global_variables.h +91 -0
  33. data/ext/unicorn_http/unicorn_http.rl +715 -0
  34. data/ext/unicorn_http/unicorn_http_common.rl +74 -0
  35. data/lib/unicorn.rb +730 -0
  36. data/lib/unicorn/app/exec_cgi.rb +150 -0
  37. data/lib/unicorn/app/inetd.rb +109 -0
  38. data/lib/unicorn/app/old_rails.rb +31 -0
  39. data/lib/unicorn/app/old_rails/static.rb +60 -0
  40. data/lib/unicorn/cgi_wrapper.rb +145 -0
  41. data/lib/unicorn/configurator.rb +403 -0
  42. data/lib/unicorn/const.rb +37 -0
  43. data/lib/unicorn/http_request.rb +74 -0
  44. data/lib/unicorn/http_response.rb +74 -0
  45. data/lib/unicorn/launcher.rb +39 -0
  46. data/lib/unicorn/socket_helper.rb +138 -0
  47. data/lib/unicorn/tee_input.rb +174 -0
  48. data/lib/unicorn/util.rb +64 -0
  49. data/local.mk.sample +53 -0
  50. data/setup.rb +1586 -0
  51. data/test/aggregate.rb +15 -0
  52. data/test/benchmark/README +50 -0
  53. data/test/benchmark/dd.ru +18 -0
  54. data/test/exec/README +5 -0
  55. data/test/exec/test_exec.rb +855 -0
  56. data/test/rails/app-1.2.3/.gitignore +2 -0
  57. data/test/rails/app-1.2.3/Rakefile +7 -0
  58. data/test/rails/app-1.2.3/app/controllers/application.rb +6 -0
  59. data/test/rails/app-1.2.3/app/controllers/foo_controller.rb +36 -0
  60. data/test/rails/app-1.2.3/app/helpers/application_helper.rb +4 -0
  61. data/test/rails/app-1.2.3/config/boot.rb +11 -0
  62. data/test/rails/app-1.2.3/config/database.yml +12 -0
  63. data/test/rails/app-1.2.3/config/environment.rb +13 -0
  64. data/test/rails/app-1.2.3/config/environments/development.rb +9 -0
  65. data/test/rails/app-1.2.3/config/environments/production.rb +5 -0
  66. data/test/rails/app-1.2.3/config/routes.rb +6 -0
  67. data/test/rails/app-1.2.3/db/.gitignore +0 -0
  68. data/test/rails/app-1.2.3/public/404.html +1 -0
  69. data/test/rails/app-1.2.3/public/500.html +1 -0
  70. data/test/rails/app-2.0.2/.gitignore +2 -0
  71. data/test/rails/app-2.0.2/Rakefile +7 -0
  72. data/test/rails/app-2.0.2/app/controllers/application.rb +4 -0
  73. data/test/rails/app-2.0.2/app/controllers/foo_controller.rb +36 -0
  74. data/test/rails/app-2.0.2/app/helpers/application_helper.rb +4 -0
  75. data/test/rails/app-2.0.2/config/boot.rb +11 -0
  76. data/test/rails/app-2.0.2/config/database.yml +12 -0
  77. data/test/rails/app-2.0.2/config/environment.rb +17 -0
  78. data/test/rails/app-2.0.2/config/environments/development.rb +8 -0
  79. data/test/rails/app-2.0.2/config/environments/production.rb +5 -0
  80. data/test/rails/app-2.0.2/config/routes.rb +6 -0
  81. data/test/rails/app-2.0.2/db/.gitignore +0 -0
  82. data/test/rails/app-2.0.2/public/404.html +1 -0
  83. data/test/rails/app-2.0.2/public/500.html +1 -0
  84. data/test/rails/app-2.1.2/.gitignore +2 -0
  85. data/test/rails/app-2.1.2/Rakefile +7 -0
  86. data/test/rails/app-2.1.2/app/controllers/application.rb +4 -0
  87. data/test/rails/app-2.1.2/app/controllers/foo_controller.rb +36 -0
  88. data/test/rails/app-2.1.2/app/helpers/application_helper.rb +4 -0
  89. data/test/rails/app-2.1.2/config/boot.rb +111 -0
  90. data/test/rails/app-2.1.2/config/database.yml +12 -0
  91. data/test/rails/app-2.1.2/config/environment.rb +17 -0
  92. data/test/rails/app-2.1.2/config/environments/development.rb +7 -0
  93. data/test/rails/app-2.1.2/config/environments/production.rb +5 -0
  94. data/test/rails/app-2.1.2/config/routes.rb +6 -0
  95. data/test/rails/app-2.1.2/db/.gitignore +0 -0
  96. data/test/rails/app-2.1.2/public/404.html +1 -0
  97. data/test/rails/app-2.1.2/public/500.html +1 -0
  98. data/test/rails/app-2.2.2/.gitignore +2 -0
  99. data/test/rails/app-2.2.2/Rakefile +7 -0
  100. data/test/rails/app-2.2.2/app/controllers/application.rb +4 -0
  101. data/test/rails/app-2.2.2/app/controllers/foo_controller.rb +36 -0
  102. data/test/rails/app-2.2.2/app/helpers/application_helper.rb +4 -0
  103. data/test/rails/app-2.2.2/config/boot.rb +111 -0
  104. data/test/rails/app-2.2.2/config/database.yml +12 -0
  105. data/test/rails/app-2.2.2/config/environment.rb +17 -0
  106. data/test/rails/app-2.2.2/config/environments/development.rb +7 -0
  107. data/test/rails/app-2.2.2/config/environments/production.rb +5 -0
  108. data/test/rails/app-2.2.2/config/routes.rb +6 -0
  109. data/test/rails/app-2.2.2/db/.gitignore +0 -0
  110. data/test/rails/app-2.2.2/public/404.html +1 -0
  111. data/test/rails/app-2.2.2/public/500.html +1 -0
  112. data/test/rails/app-2.3.3.1/.gitignore +2 -0
  113. data/test/rails/app-2.3.3.1/Rakefile +7 -0
  114. data/test/rails/app-2.3.3.1/app/controllers/application_controller.rb +5 -0
  115. data/test/rails/app-2.3.3.1/app/controllers/foo_controller.rb +36 -0
  116. data/test/rails/app-2.3.3.1/app/helpers/application_helper.rb +4 -0
  117. data/test/rails/app-2.3.3.1/config/boot.rb +109 -0
  118. data/test/rails/app-2.3.3.1/config/database.yml +12 -0
  119. data/test/rails/app-2.3.3.1/config/environment.rb +17 -0
  120. data/test/rails/app-2.3.3.1/config/environments/development.rb +7 -0
  121. data/test/rails/app-2.3.3.1/config/environments/production.rb +6 -0
  122. data/test/rails/app-2.3.3.1/config/routes.rb +6 -0
  123. data/test/rails/app-2.3.3.1/db/.gitignore +0 -0
  124. data/test/rails/app-2.3.3.1/public/404.html +1 -0
  125. data/test/rails/app-2.3.3.1/public/500.html +1 -0
  126. data/test/rails/app-2.3.3.1/public/x.txt +1 -0
  127. data/test/rails/test_rails.rb +280 -0
  128. data/test/test_helper.rb +296 -0
  129. data/test/unit/test_configurator.rb +150 -0
  130. data/test/unit/test_http_parser.rb +492 -0
  131. data/test/unit/test_http_parser_ng.rb +308 -0
  132. data/test/unit/test_request.rb +184 -0
  133. data/test/unit/test_response.rb +110 -0
  134. data/test/unit/test_server.rb +188 -0
  135. data/test/unit/test_signals.rb +202 -0
  136. data/test/unit/test_socket_helper.rb +133 -0
  137. data/test/unit/test_tee_input.rb +229 -0
  138. data/test/unit/test_upload.rb +297 -0
  139. data/test/unit/test_util.rb +96 -0
  140. data/unicorn.gemspec +42 -0
  141. metadata +228 -0
@@ -0,0 +1,133 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ require 'test/test_helper'
4
+ require 'tempfile'
5
+
6
+ class TestSocketHelper < Test::Unit::TestCase
7
+ include Unicorn::SocketHelper
8
+ attr_reader :logger
9
+ GET_SLASH = "GET / HTTP/1.0\r\n\r\n".freeze
10
+
11
+ def setup
12
+ @log_tmp = Tempfile.new 'logger'
13
+ @logger = Logger.new(@log_tmp.path)
14
+ @test_addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
15
+ GC.disable
16
+ end
17
+
18
+ def teardown
19
+ GC.enable
20
+ end
21
+
22
+ def test_bind_listen_tcp
23
+ port = unused_port @test_addr
24
+ @tcp_listener_name = "#@test_addr:#{port}"
25
+ @tcp_listener = bind_listen(@tcp_listener_name)
26
+ assert TCPServer === @tcp_listener
27
+ assert_equal @tcp_listener_name, sock_name(@tcp_listener)
28
+ end
29
+
30
+ def test_bind_listen_options
31
+ port = unused_port @test_addr
32
+ tcp_listener_name = "#@test_addr:#{port}"
33
+ tmp = Tempfile.new 'unix.sock'
34
+ unix_listener_name = tmp.path
35
+ File.unlink(tmp.path)
36
+ [ { :backlog => 5 }, { :sndbuf => 4096 }, { :rcvbuf => 4096 },
37
+ { :backlog => 16, :rcvbuf => 4096, :sndbuf => 4096 }
38
+ ].each do |opts|
39
+ assert_nothing_raised do
40
+ tcp_listener = bind_listen(tcp_listener_name, opts)
41
+ assert TCPServer === tcp_listener
42
+ tcp_listener.close
43
+ unix_listener = bind_listen(unix_listener_name, opts)
44
+ assert UNIXServer === unix_listener
45
+ unix_listener.close
46
+ end
47
+ end
48
+ #system('cat', @log_tmp.path)
49
+ end
50
+
51
+ def test_bind_listen_unix
52
+ old_umask = File.umask(0777)
53
+ tmp = Tempfile.new 'unix.sock'
54
+ @unix_listener_path = tmp.path
55
+ File.unlink(@unix_listener_path)
56
+ @unix_listener = bind_listen(@unix_listener_path)
57
+ assert UNIXServer === @unix_listener
58
+ assert_equal @unix_listener_path, sock_name(@unix_listener)
59
+ assert File.readable?(@unix_listener_path), "not readable"
60
+ assert File.writable?(@unix_listener_path), "not writable"
61
+ assert_equal 0777, File.umask
62
+ ensure
63
+ File.umask(old_umask)
64
+ end
65
+
66
+ def test_bind_listen_unix_idempotent
67
+ test_bind_listen_unix
68
+ a = bind_listen(@unix_listener)
69
+ assert_equal a.fileno, @unix_listener.fileno
70
+ unix_server = server_cast(@unix_listener)
71
+ assert UNIXServer === unix_server
72
+ a = bind_listen(unix_server)
73
+ assert_equal a.fileno, unix_server.fileno
74
+ assert_equal a.fileno, @unix_listener.fileno
75
+ end
76
+
77
+ def test_bind_listen_tcp_idempotent
78
+ test_bind_listen_tcp
79
+ a = bind_listen(@tcp_listener)
80
+ assert_equal a.fileno, @tcp_listener.fileno
81
+ tcp_server = server_cast(@tcp_listener)
82
+ assert TCPServer === tcp_server
83
+ a = bind_listen(tcp_server)
84
+ assert_equal a.fileno, tcp_server.fileno
85
+ assert_equal a.fileno, @tcp_listener.fileno
86
+ end
87
+
88
+ def test_bind_listen_unix_rebind
89
+ test_bind_listen_unix
90
+ new_listener = bind_listen(@unix_listener_path)
91
+ assert UNIXServer === new_listener
92
+ assert new_listener.fileno != @unix_listener.fileno
93
+ assert_equal sock_name(new_listener), sock_name(@unix_listener)
94
+ assert_equal @unix_listener_path, sock_name(new_listener)
95
+ pid = fork do
96
+ client = server_cast(new_listener).accept
97
+ client.syswrite('abcde')
98
+ exit 0
99
+ end
100
+ s = UNIXSocket.new(@unix_listener_path)
101
+ IO.select([s])
102
+ assert_equal 'abcde', s.sysread(5)
103
+ pid, status = Process.waitpid2(pid)
104
+ assert status.success?
105
+ end
106
+
107
+ def test_server_cast
108
+ assert_nothing_raised do
109
+ test_bind_listen_unix
110
+ test_bind_listen_tcp
111
+ end
112
+ unix_listener_socket = Socket.for_fd(@unix_listener.fileno)
113
+ assert Socket === unix_listener_socket
114
+ @unix_server = server_cast(unix_listener_socket)
115
+ assert_equal @unix_listener.fileno, @unix_server.fileno
116
+ assert UNIXServer === @unix_server
117
+ assert File.socket?(@unix_server.path)
118
+ assert_equal @unix_listener_path, sock_name(@unix_server)
119
+
120
+ tcp_listener_socket = Socket.for_fd(@tcp_listener.fileno)
121
+ assert Socket === tcp_listener_socket
122
+ @tcp_server = server_cast(tcp_listener_socket)
123
+ assert_equal @tcp_listener.fileno, @tcp_server.fileno
124
+ assert TCPServer === @tcp_server
125
+ assert_equal @tcp_listener_name, sock_name(@tcp_server)
126
+ end
127
+
128
+ def test_sock_name
129
+ test_server_cast
130
+ sock_name(@unix_server)
131
+ end
132
+
133
+ end
@@ -0,0 +1,229 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ require 'test/unit'
4
+ require 'digest/sha1'
5
+ require 'unicorn'
6
+
7
+ class TestTeeInput < Test::Unit::TestCase
8
+
9
+ def setup
10
+ @rs = $/
11
+ @env = {}
12
+ @rd, @wr = IO.pipe
13
+ @rd.sync = @wr.sync = true
14
+ @start_pid = $$
15
+ end
16
+
17
+ def teardown
18
+ return if $$ != @start_pid
19
+ $/ = @rs
20
+ @rd.close rescue nil
21
+ @wr.close rescue nil
22
+ begin
23
+ Process.wait
24
+ rescue Errno::ECHILD
25
+ break
26
+ end while true
27
+ end
28
+
29
+ def test_gets_long
30
+ init_parser("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size)
31
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
32
+ status = line = nil
33
+ pid = fork {
34
+ @rd.close
35
+ 3.times { @wr.write("ffff" * 4096) }
36
+ @wr.write "#$/foo#$/"
37
+ @wr.close
38
+ }
39
+ @wr.close
40
+ assert_nothing_raised { line = ti.gets }
41
+ assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size)
42
+ assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line)
43
+ assert_nothing_raised { line = ti.gets }
44
+ assert_equal "foo#$/", line
45
+ assert_nil ti.gets
46
+ assert_nothing_raised { pid, status = Process.waitpid2(pid) }
47
+ assert status.success?
48
+ end
49
+
50
+ def test_gets_short
51
+ init_parser("hello", 5 + "#$/foo".size)
52
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
53
+ status = line = nil
54
+ pid = fork {
55
+ @rd.close
56
+ @wr.write "#$/foo"
57
+ @wr.close
58
+ }
59
+ @wr.close
60
+ assert_nothing_raised { line = ti.gets }
61
+ assert_equal("hello#$/", line)
62
+ assert_nothing_raised { line = ti.gets }
63
+ assert_equal "foo", line
64
+ assert_nil ti.gets
65
+ assert_nothing_raised { pid, status = Process.waitpid2(pid) }
66
+ assert status.success?
67
+ end
68
+
69
+ def test_small_body
70
+ init_parser('hello')
71
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
72
+ assert_equal 0, @parser.content_length
73
+ assert @parser.body_eof?
74
+ assert_equal StringIO, ti.instance_eval { @tmp.class }
75
+ assert_equal 0, ti.instance_eval { @tmp.pos }
76
+ assert_equal 5, ti.size
77
+ assert_equal 'hello', ti.read
78
+ assert_equal '', ti.read
79
+ assert_nil ti.read(4096)
80
+ end
81
+
82
+ def test_read_with_buffer
83
+ init_parser('hello')
84
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
85
+ buf = ''
86
+ rv = ti.read(4, buf)
87
+ assert_equal 'hell', rv
88
+ assert_equal 'hell', buf
89
+ assert_equal rv.object_id, buf.object_id
90
+ assert_equal 'o', ti.read
91
+ assert_equal nil, ti.read(5, buf)
92
+ assert_equal 0, ti.rewind
93
+ assert_equal 'hello', ti.read(5, buf)
94
+ assert_equal 'hello', buf
95
+ end
96
+
97
+ def test_big_body
98
+ init_parser('.' * Unicorn::Const::MAX_BODY << 'a')
99
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
100
+ assert_equal 0, @parser.content_length
101
+ assert @parser.body_eof?
102
+ assert_kind_of File, ti.instance_eval { @tmp }
103
+ assert_equal 0, ti.instance_eval { @tmp.pos }
104
+ assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
105
+ end
106
+
107
+ def test_read_in_full_if_content_length
108
+ a, b = 300, 3
109
+ init_parser('.' * b, 300)
110
+ assert_equal 300, @parser.content_length
111
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
112
+ pid = fork {
113
+ @wr.write('.' * 197)
114
+ sleep 1 # still a *potential* race here that would make the test moot...
115
+ @wr.write('.' * 100)
116
+ }
117
+ assert_equal a, ti.read(a).size
118
+ _, status = Process.waitpid2(pid)
119
+ assert status.success?
120
+ @wr.close
121
+ end
122
+
123
+ def test_big_body_multi
124
+ init_parser('.', Unicorn::Const::MAX_BODY + 1)
125
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
126
+ assert_equal Unicorn::Const::MAX_BODY, @parser.content_length
127
+ assert ! @parser.body_eof?
128
+ assert_kind_of File, ti.instance_eval { @tmp }
129
+ assert_equal 0, ti.instance_eval { @tmp.pos }
130
+ assert_equal 1, ti.instance_eval { tmp_size }
131
+ assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
132
+ nr = Unicorn::Const::MAX_BODY / 4
133
+ pid = fork {
134
+ @rd.close
135
+ nr.times { @wr.write('....') }
136
+ @wr.close
137
+ }
138
+ @wr.close
139
+ assert_equal '.', ti.read(1)
140
+ assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
141
+ nr.times {
142
+ assert_equal '....', ti.read(4)
143
+ assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
144
+ }
145
+ assert_nil ti.read(1)
146
+ status = nil
147
+ assert_nothing_raised { pid, status = Process.waitpid2(pid) }
148
+ assert status.success?
149
+ end
150
+
151
+ def test_chunked
152
+ @parser = Unicorn::HttpParser.new
153
+ @buf = "POST / HTTP/1.1\r\n" \
154
+ "Host: localhost\r\n" \
155
+ "Transfer-Encoding: chunked\r\n" \
156
+ "\r\n"
157
+ assert_equal @env, @parser.headers(@env, @buf)
158
+ assert_equal "", @buf
159
+
160
+ pid = fork {
161
+ @rd.close
162
+ 5.times { @wr.write("5\r\nabcde\r\n") }
163
+ @wr.write("0\r\n")
164
+ }
165
+ @wr.close
166
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
167
+ assert_nil @parser.content_length
168
+ assert_nil ti.instance_eval { @size }
169
+ assert ! @parser.body_eof?
170
+ assert_equal 25, ti.size
171
+ assert @parser.body_eof?
172
+ assert_equal 25, ti.instance_eval { @size }
173
+ assert_equal 0, ti.instance_eval { @tmp.pos }
174
+ assert_nothing_raised { ti.rewind }
175
+ assert_equal 0, ti.instance_eval { @tmp.pos }
176
+ assert_equal 'abcdeabcdeabcdeabcde', ti.read(20)
177
+ assert_equal 20, ti.instance_eval { @tmp.pos }
178
+ assert_nothing_raised { ti.rewind }
179
+ assert_equal 0, ti.instance_eval { @tmp.pos }
180
+ assert_kind_of File, ti.instance_eval { @tmp }
181
+ status = nil
182
+ assert_nothing_raised { pid, status = Process.waitpid2(pid) }
183
+ assert status.success?
184
+ end
185
+
186
+ def test_chunked_ping_pong
187
+ @parser = Unicorn::HttpParser.new
188
+ @buf = "POST / HTTP/1.1\r\n" \
189
+ "Host: localhost\r\n" \
190
+ "Transfer-Encoding: chunked\r\n" \
191
+ "\r\n"
192
+ assert_equal @env, @parser.headers(@env, @buf)
193
+ assert_equal "", @buf
194
+ chunks = %w(aa bbb cccc dddd eeee)
195
+ rd, wr = IO.pipe
196
+
197
+ pid = fork {
198
+ chunks.each do |chunk|
199
+ rd.read(1) == "." and
200
+ @wr.write("#{'%x' % [ chunk.size]}\r\n#{chunk}\r\n")
201
+ end
202
+ @wr.write("0\r\n")
203
+ }
204
+ ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
205
+ assert_nil @parser.content_length
206
+ assert_nil ti.instance_eval { @size }
207
+ assert ! @parser.body_eof?
208
+ chunks.each do |chunk|
209
+ wr.write('.')
210
+ assert_equal chunk, ti.read(16384)
211
+ end
212
+ _, status = Process.waitpid2(pid)
213
+ assert status.success?
214
+ end
215
+
216
+ private
217
+
218
+ def init_parser(body, size = nil)
219
+ @parser = Unicorn::HttpParser.new
220
+ body = body.to_s.freeze
221
+ @buf = "POST / HTTP/1.1\r\n" \
222
+ "Host: localhost\r\n" \
223
+ "Content-Length: #{size || body.size}\r\n" \
224
+ "\r\n#{body}"
225
+ assert_equal @env, @parser.headers(@env, @buf)
226
+ assert_equal body, @buf
227
+ end
228
+
229
+ end
@@ -0,0 +1,297 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ # Copyright (c) 2009 Eric Wong
4
+ require 'test/test_helper'
5
+ require 'digest/md5'
6
+
7
+ include Unicorn
8
+
9
+ class UploadTest < Test::Unit::TestCase
10
+
11
+ def setup
12
+ @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
13
+ @port = unused_port
14
+ @hdr = {'Content-Type' => 'text/plain', 'Content-Length' => '0'}
15
+ @bs = 4096
16
+ @count = 256
17
+ @server = nil
18
+
19
+ # we want random binary data to test 1.9 encoding-aware IO craziness
20
+ @random = File.open('/dev/urandom','rb')
21
+ @sha1 = Digest::SHA1.new
22
+ @sha1_app = lambda do |env|
23
+ input = env['rack.input']
24
+ resp = {}
25
+
26
+ @sha1.reset
27
+ while buf = input.read(@bs)
28
+ @sha1.update(buf)
29
+ end
30
+ resp[:sha1] = @sha1.hexdigest
31
+
32
+ # rewind and read again
33
+ input.rewind
34
+ @sha1.reset
35
+ while buf = input.read(@bs)
36
+ @sha1.update(buf)
37
+ end
38
+
39
+ if resp[:sha1] == @sha1.hexdigest
40
+ resp[:sysread_read_byte_match] = true
41
+ end
42
+
43
+ if expect_size = env['HTTP_X_EXPECT_SIZE']
44
+ if expect_size.to_i == input.size
45
+ resp[:expect_size_match] = true
46
+ end
47
+ end
48
+ resp[:size] = input.size
49
+ resp[:content_md5] = env['HTTP_CONTENT_MD5']
50
+
51
+ [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
52
+ end
53
+ end
54
+
55
+ def teardown
56
+ redirect_test_io { @server.stop(true) } if @server
57
+ @random.close
58
+ end
59
+
60
+ def test_put
61
+ start_server(@sha1_app)
62
+ sock = TCPSocket.new(@addr, @port)
63
+ sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
64
+ @count.times do |i|
65
+ buf = @random.sysread(@bs)
66
+ @sha1.update(buf)
67
+ sock.syswrite(buf)
68
+ end
69
+ read = sock.read.split(/\r\n/)
70
+ assert_equal "HTTP/1.1 200 OK", read[0]
71
+ resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
72
+ assert_equal length, resp[:size]
73
+ assert_equal @sha1.hexdigest, resp[:sha1]
74
+ end
75
+
76
+ def test_put_content_md5
77
+ md5 = Digest::MD5.new
78
+ start_server(@sha1_app)
79
+ sock = TCPSocket.new(@addr, @port)
80
+ sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \
81
+ "Trailer: Content-MD5\r\n\r\n")
82
+ @count.times do |i|
83
+ buf = @random.sysread(@bs)
84
+ @sha1.update(buf)
85
+ md5.update(buf)
86
+ sock.syswrite("#{'%x' % buf.size}\r\n")
87
+ sock.syswrite(buf << "\r\n")
88
+ end
89
+ sock.syswrite("0\r\n")
90
+
91
+ content_md5 = [ md5.digest! ].pack('m').strip.freeze
92
+ sock.syswrite("Content-MD5: #{content_md5}\r\n\r\n")
93
+ read = sock.read.split(/\r\n/)
94
+ assert_equal "HTTP/1.1 200 OK", read[0]
95
+ resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
96
+ assert_equal length, resp[:size]
97
+ assert_equal @sha1.hexdigest, resp[:sha1]
98
+ assert_equal content_md5, resp[:content_md5]
99
+ end
100
+
101
+ def test_put_trickle_small
102
+ @count, @bs = 2, 128
103
+ start_server(@sha1_app)
104
+ assert_equal 256, length
105
+ sock = TCPSocket.new(@addr, @port)
106
+ hdr = "PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n"
107
+ @count.times do
108
+ buf = @random.sysread(@bs)
109
+ @sha1.update(buf)
110
+ hdr << buf
111
+ sock.syswrite(hdr)
112
+ hdr = ''
113
+ sleep 0.6
114
+ end
115
+ read = sock.read.split(/\r\n/)
116
+ assert_equal "HTTP/1.1 200 OK", read[0]
117
+ resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
118
+ assert_equal length, resp[:size]
119
+ assert_equal @sha1.hexdigest, resp[:sha1]
120
+ end
121
+
122
+ def test_put_keepalive_truncates_small_overwrite
123
+ start_server(@sha1_app)
124
+ sock = TCPSocket.new(@addr, @port)
125
+ to_upload = length + 1
126
+ sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{to_upload}\r\n\r\n")
127
+ @count.times do
128
+ buf = @random.sysread(@bs)
129
+ @sha1.update(buf)
130
+ sock.syswrite(buf)
131
+ end
132
+ sock.syswrite('12345') # write 4 bytes more than we expected
133
+ @sha1.update('1')
134
+
135
+ buf = sock.readpartial(4096)
136
+ while buf !~ /\r\n\r\n/
137
+ buf << sock.readpartial(4096)
138
+ end
139
+ read = buf.split(/\r\n/)
140
+ assert_equal "HTTP/1.1 200 OK", read[0]
141
+ resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
142
+ assert_equal to_upload, resp[:size]
143
+ assert_equal @sha1.hexdigest, resp[:sha1]
144
+ end
145
+
146
+ def test_put_excessive_overwrite_closed
147
+ start_server(lambda { |env|
148
+ while env['rack.input'].read(65536); end
149
+ [ 200, @hdr, [] ]
150
+ })
151
+ sock = TCPSocket.new(@addr, @port)
152
+ buf = ' ' * @bs
153
+ sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
154
+
155
+ @count.times { sock.syswrite(buf) }
156
+ assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
157
+ ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
158
+ end
159
+ assert_equal "HTTP/1.1 200 OK\r\n", sock.gets
160
+ end
161
+
162
+ # Despite reading numerous articles and inspecting the 1.9.1-p0 C
163
+ # source, Eric Wong will never trust that we're always handling
164
+ # encoding-aware IO objects correctly. Thus this test uses shell
165
+ # utilities that should always operate on files/sockets on a
166
+ # byte-level.
167
+ def test_uncomfortable_with_onenine_encodings
168
+ # POSIX doesn't require all of these to be present on a system
169
+ which('curl') or return
170
+ which('sha1sum') or return
171
+ which('dd') or return
172
+
173
+ start_server(@sha1_app)
174
+
175
+ tmp = Tempfile.new('dd_dest')
176
+ assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
177
+ "bs=#{@bs}", "count=#{@count}"),
178
+ "dd #@random to #{tmp}")
179
+ sha1_re = %r!\b([a-f0-9]{40})\b!
180
+ sha1_out = `sha1sum #{tmp.path}`
181
+ assert $?.success?, 'sha1sum ran OK'
182
+
183
+ assert_match(sha1_re, sha1_out)
184
+ sha1 = sha1_re.match(sha1_out)[1]
185
+ resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
186
+ assert $?.success?, 'curl ran OK'
187
+ assert_match(%r!\b#{sha1}\b!, resp)
188
+ assert_match(/sysread_read_byte_match/, resp)
189
+
190
+ # small StringIO path
191
+ assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
192
+ "bs=1024", "count=1"),
193
+ "dd #@random to #{tmp}")
194
+ sha1_re = %r!\b([a-f0-9]{40})\b!
195
+ sha1_out = `sha1sum #{tmp.path}`
196
+ assert $?.success?, 'sha1sum ran OK'
197
+
198
+ assert_match(sha1_re, sha1_out)
199
+ sha1 = sha1_re.match(sha1_out)[1]
200
+ resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
201
+ assert $?.success?, 'curl ran OK'
202
+ assert_match(%r!\b#{sha1}\b!, resp)
203
+ assert_match(/sysread_read_byte_match/, resp)
204
+ end
205
+
206
+ def test_chunked_upload_via_curl
207
+ # POSIX doesn't require all of these to be present on a system
208
+ which('curl') or return
209
+ which('sha1sum') or return
210
+ which('dd') or return
211
+
212
+ start_server(@sha1_app)
213
+
214
+ tmp = Tempfile.new('dd_dest')
215
+ assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
216
+ "bs=#{@bs}", "count=#{@count}"),
217
+ "dd #@random to #{tmp}")
218
+ sha1_re = %r!\b([a-f0-9]{40})\b!
219
+ sha1_out = `sha1sum #{tmp.path}`
220
+ assert $?.success?, 'sha1sum ran OK'
221
+
222
+ assert_match(sha1_re, sha1_out)
223
+ sha1 = sha1_re.match(sha1_out)[1]
224
+ cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
225
+ -isSf --no-buffer -T- " \
226
+ "http://#@addr:#@port/"
227
+ resp = Tempfile.new('resp')
228
+ resp.sync = true
229
+
230
+ rd, wr = IO.pipe
231
+ wr.sync = rd.sync = true
232
+ pid = fork {
233
+ STDIN.reopen(rd)
234
+ rd.close
235
+ wr.close
236
+ STDOUT.reopen(resp)
237
+ exec cmd
238
+ }
239
+ rd.close
240
+
241
+ tmp.rewind
242
+ @count.times { |i|
243
+ wr.write(tmp.read(@bs))
244
+ sleep(rand / 10) if 0 == i % 8
245
+ }
246
+ wr.close
247
+ pid, status = Process.waitpid2(pid)
248
+
249
+ resp.rewind
250
+ resp = resp.read
251
+ assert status.success?, 'curl ran OK'
252
+ assert_match(%r!\b#{sha1}\b!, resp)
253
+ assert_match(/sysread_read_byte_match/, resp)
254
+ assert_match(/expect_size_match/, resp)
255
+ end
256
+
257
+ def test_curl_chunked_small
258
+ # POSIX doesn't require all of these to be present on a system
259
+ which('curl') or return
260
+ which('sha1sum') or return
261
+ which('dd') or return
262
+
263
+ start_server(@sha1_app)
264
+
265
+ tmp = Tempfile.new('dd_dest')
266
+ # small StringIO path
267
+ assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
268
+ "bs=1024", "count=1"),
269
+ "dd #@random to #{tmp}")
270
+ sha1_re = %r!\b([a-f0-9]{40})\b!
271
+ sha1_out = `sha1sum #{tmp.path}`
272
+ assert $?.success?, 'sha1sum ran OK'
273
+
274
+ assert_match(sha1_re, sha1_out)
275
+ sha1 = sha1_re.match(sha1_out)[1]
276
+ resp = `curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
277
+ -isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}`
278
+ assert $?.success?, 'curl ran OK'
279
+ assert_match(%r!\b#{sha1}\b!, resp)
280
+ assert_match(/sysread_read_byte_match/, resp)
281
+ assert_match(/expect_size_match/, resp)
282
+ end
283
+
284
+ private
285
+
286
+ def length
287
+ @bs * @count
288
+ end
289
+
290
+ def start_server(app)
291
+ redirect_test_io do
292
+ @server = HttpServer.new(app, :listeners => [ "#{@addr}:#{@port}" ] )
293
+ @server.start
294
+ end
295
+ end
296
+
297
+ end