giraffesoft-unicorn 0.93.5

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