passenger 5.0.0.beta3 → 5.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of passenger might be problematic. Click here for more details.

Files changed (218) hide show
  1. checksums.yaml +8 -8
  2. checksums.yaml.gz.asc +7 -7
  3. data.tar.gz.asc +7 -7
  4. data/.editorconfig +11 -5
  5. data/CHANGELOG +38 -0
  6. data/CONTRIBUTING.md +1 -4
  7. data/Gemfile +0 -1
  8. data/Gemfile.lock +0 -2
  9. data/Rakefile +33 -33
  10. data/bin/passenger +1 -1
  11. data/bin/passenger-config +1 -1
  12. data/bin/passenger-install-apache2-module +800 -800
  13. data/bin/passenger-install-nginx-module +592 -592
  14. data/bin/passenger-memory-stats +127 -127
  15. data/bin/passenger-status +216 -216
  16. data/build/agents.rb +127 -127
  17. data/build/apache2.rb +87 -87
  18. data/build/basics.rb +60 -60
  19. data/build/common_library.rb +165 -165
  20. data/build/cplusplus_support.rb +51 -51
  21. data/build/cxx_tests.rb +268 -268
  22. data/build/debian.rb +143 -143
  23. data/build/documentation.rb +58 -58
  24. data/build/integration_tests.rb +81 -81
  25. data/build/misc.rb +132 -132
  26. data/build/nginx.rb +20 -20
  27. data/build/node_tests.rb +7 -7
  28. data/build/oxt_tests.rb +14 -14
  29. data/build/packaging.rb +570 -570
  30. data/build/preprocessor.rb +260 -260
  31. data/build/rake_extensions.rb +71 -71
  32. data/build/ruby_extension.rb +29 -29
  33. data/build/ruby_tests.rb +6 -6
  34. data/build/test_basics.rb +37 -37
  35. data/debian.template/control.template +3 -5
  36. data/dev/copy_boost_headers +134 -134
  37. data/dev/install_scripts_bootstrap_code.rb +25 -25
  38. data/dev/list_tests +20 -20
  39. data/dev/ruby_server.rb +223 -223
  40. data/dev/runner +18 -18
  41. data/doc/ServerOptimizationGuide.txt.md +55 -2
  42. data/doc/Users guide Nginx.txt +0 -26
  43. data/doc/Users guide Standalone.txt +5 -1
  44. data/doc/users_guide_snippets/tips.txt +9 -0
  45. data/ext/common/ApplicationPool2/Group.h +23 -11
  46. data/ext/common/ApplicationPool2/Implementation.cpp +32 -7
  47. data/ext/common/ApplicationPool2/Pool.h +22 -17
  48. data/ext/common/ApplicationPool2/SmartSpawner.h +4 -1
  49. data/ext/common/ApplicationPool2/Spawner.h +1 -1
  50. data/ext/common/Constants.h +1 -1
  51. data/ext/common/agents/Base.cpp +35 -20
  52. data/ext/common/agents/HelperAgent/Main.cpp +8 -1
  53. data/ext/common/agents/HelperAgent/OptionParser.h +18 -4
  54. data/ext/common/agents/HelperAgent/RequestHandler.h +2 -83
  55. data/ext/common/agents/HelperAgent/RequestHandler/ForwardResponse.cpp +54 -1
  56. data/ext/common/agents/HelperAgent/RequestHandler/InitRequest.cpp +7 -4
  57. data/ext/common/agents/Main.cpp +1 -1
  58. data/ext/common/agents/Watchdog/Main.cpp +54 -19
  59. data/ext/nginx/Configuration.c +7 -0
  60. data/ext/nginx/ContentHandler.c +9 -1
  61. data/helper-scripts/backtrace-sanitizer.rb +106 -87
  62. data/helper-scripts/crash-watch.rb +32 -0
  63. data/helper-scripts/download_binaries/extconf.rb +38 -38
  64. data/helper-scripts/meteor-loader.rb +107 -107
  65. data/helper-scripts/prespawn +101 -101
  66. data/helper-scripts/rack-loader.rb +96 -96
  67. data/helper-scripts/rack-preloader.rb +137 -137
  68. data/lib/phusion_passenger.rb +292 -292
  69. data/lib/phusion_passenger/abstract_installer.rb +438 -438
  70. data/lib/phusion_passenger/active_support3_extensions/init.rb +168 -170
  71. data/lib/phusion_passenger/admin_tools.rb +20 -20
  72. data/lib/phusion_passenger/admin_tools/instance.rb +178 -178
  73. data/lib/phusion_passenger/admin_tools/instance_registry.rb +61 -61
  74. data/lib/phusion_passenger/admin_tools/memory_stats.rb +267 -267
  75. data/lib/phusion_passenger/apache2/config_options.rb +182 -182
  76. data/lib/phusion_passenger/common_library.rb +479 -485
  77. data/lib/phusion_passenger/config/about_command.rb +161 -161
  78. data/lib/phusion_passenger/config/admin_command_command.rb +129 -129
  79. data/lib/phusion_passenger/config/agent_compiler.rb +121 -121
  80. data/lib/phusion_passenger/config/build_native_support_command.rb +43 -43
  81. data/lib/phusion_passenger/config/command.rb +25 -25
  82. data/lib/phusion_passenger/config/compile_agent_command.rb +62 -62
  83. data/lib/phusion_passenger/config/compile_nginx_engine_command.rb +88 -73
  84. data/lib/phusion_passenger/config/detach_process_command.rb +72 -72
  85. data/lib/phusion_passenger/config/download_agent_command.rb +246 -227
  86. data/lib/phusion_passenger/config/download_nginx_engine_command.rb +245 -224
  87. data/lib/phusion_passenger/config/install_agent_command.rb +144 -132
  88. data/lib/phusion_passenger/config/install_standalone_runtime_command.rb +205 -185
  89. data/lib/phusion_passenger/config/installation_utils.rb +204 -204
  90. data/lib/phusion_passenger/config/list_instances_command.rb +64 -64
  91. data/lib/phusion_passenger/config/main.rb +152 -152
  92. data/lib/phusion_passenger/config/nginx_engine_compiler.rb +319 -300
  93. data/lib/phusion_passenger/config/reopen_logs_command.rb +67 -67
  94. data/lib/phusion_passenger/config/restart_app_command.rb +155 -155
  95. data/lib/phusion_passenger/config/system_metrics_command.rb +13 -13
  96. data/lib/phusion_passenger/config/utils.rb +95 -95
  97. data/lib/phusion_passenger/config/validate_install_command.rb +198 -198
  98. data/lib/phusion_passenger/console_text_template.rb +25 -25
  99. data/lib/phusion_passenger/constants.rb +90 -90
  100. data/lib/phusion_passenger/debug_logging.rb +106 -106
  101. data/lib/phusion_passenger/loader_shared_helpers.rb +447 -432
  102. data/lib/phusion_passenger/message_channel.rb +312 -312
  103. data/lib/phusion_passenger/message_client.rb +176 -176
  104. data/lib/phusion_passenger/native_support.rb +369 -369
  105. data/lib/phusion_passenger/nginx/config_options.rb +297 -297
  106. data/lib/phusion_passenger/packaging.rb +131 -131
  107. data/lib/phusion_passenger/platform_info.rb +360 -360
  108. data/lib/phusion_passenger/platform_info/apache.rb +767 -767
  109. data/lib/phusion_passenger/platform_info/apache_detector.rb +199 -199
  110. data/lib/phusion_passenger/platform_info/binary_compatibility.rb +107 -107
  111. data/lib/phusion_passenger/platform_info/compiler.rb +570 -570
  112. data/lib/phusion_passenger/platform_info/curl.rb +32 -32
  113. data/lib/phusion_passenger/platform_info/cxx_portability.rb +188 -188
  114. data/lib/phusion_passenger/platform_info/depcheck.rb +372 -372
  115. data/lib/phusion_passenger/platform_info/depcheck_specs/apache2.rb +109 -109
  116. data/lib/phusion_passenger/platform_info/depcheck_specs/compiler_toolchain.rb +4 -4
  117. data/lib/phusion_passenger/platform_info/depcheck_specs/gems.rb +10 -34
  118. data/lib/phusion_passenger/platform_info/depcheck_specs/libs.rb +101 -101
  119. data/lib/phusion_passenger/platform_info/depcheck_specs/ruby.rb +5 -5
  120. data/lib/phusion_passenger/platform_info/depcheck_specs/utilities.rb +13 -13
  121. data/lib/phusion_passenger/platform_info/linux.rb +55 -55
  122. data/lib/phusion_passenger/platform_info/operating_system.rb +149 -149
  123. data/lib/phusion_passenger/platform_info/ruby.rb +468 -448
  124. data/lib/phusion_passenger/platform_info/zlib.rb +9 -9
  125. data/lib/phusion_passenger/plugin.rb +66 -66
  126. data/lib/phusion_passenger/preloader_shared_helpers.rb +126 -126
  127. data/lib/phusion_passenger/public_api.rb +191 -191
  128. data/lib/phusion_passenger/rack/out_of_band_gc.rb +93 -94
  129. data/lib/phusion_passenger/rack/thread_handler_extension.rb +231 -227
  130. data/lib/phusion_passenger/request_handler.rb +567 -577
  131. data/lib/phusion_passenger/request_handler/thread_handler.rb +379 -381
  132. data/lib/phusion_passenger/ruby_core_enhancements.rb +86 -86
  133. data/lib/phusion_passenger/ruby_core_io_enhancements.rb +74 -74
  134. data/lib/phusion_passenger/simple_benchmarking.rb +25 -25
  135. data/lib/phusion_passenger/standalone/app_finder.rb +153 -150
  136. data/lib/phusion_passenger/standalone/command.rb +44 -40
  137. data/lib/phusion_passenger/standalone/config_utils.rb +53 -53
  138. data/lib/phusion_passenger/standalone/control_utils.rb +38 -59
  139. data/lib/phusion_passenger/standalone/main.rb +73 -73
  140. data/lib/phusion_passenger/standalone/start_command.rb +697 -685
  141. data/lib/phusion_passenger/standalone/start_command/builtin_engine.rb +193 -155
  142. data/lib/phusion_passenger/standalone/start_command/nginx_engine.rb +162 -133
  143. data/lib/phusion_passenger/standalone/status_command.rb +64 -64
  144. data/lib/phusion_passenger/standalone/stop_command.rb +72 -72
  145. data/lib/phusion_passenger/standalone/version_command.rb +9 -9
  146. data/lib/phusion_passenger/union_station/connection.rb +32 -32
  147. data/lib/phusion_passenger/union_station/core.rb +251 -251
  148. data/lib/phusion_passenger/union_station/transaction.rb +126 -126
  149. data/lib/phusion_passenger/utils.rb +199 -167
  150. data/lib/phusion_passenger/utils/ansi_colors.rb +128 -128
  151. data/lib/phusion_passenger/utils/download.rb +196 -196
  152. data/lib/phusion_passenger/utils/file_system_watcher.rb +158 -158
  153. data/lib/phusion_passenger/utils/hosts_file_parser.rb +101 -101
  154. data/lib/phusion_passenger/utils/lock.rb +31 -31
  155. data/lib/phusion_passenger/utils/native_support_utils.rb +31 -31
  156. data/lib/phusion_passenger/utils/progress_bar.rb +26 -26
  157. data/lib/phusion_passenger/utils/shellwords.rb +20 -20
  158. data/lib/phusion_passenger/utils/terminal_choice_menu.rb +206 -206
  159. data/lib/phusion_passenger/utils/unseekable_socket.rb +272 -272
  160. data/lib/phusion_passenger/vendor/crash_watch/app.rb +129 -0
  161. data/lib/phusion_passenger/vendor/crash_watch/gdb_controller.rb +341 -0
  162. data/lib/phusion_passenger/vendor/crash_watch/version.rb +24 -0
  163. data/lib/phusion_passenger/vendor/daemon_controller.rb +877 -0
  164. data/lib/phusion_passenger/vendor/daemon_controller/lock_file.rb +127 -0
  165. data/lib/phusion_passenger/vendor/daemon_controller/spawn.rb +26 -0
  166. data/lib/phusion_passenger/vendor/daemon_controller/version.rb +29 -0
  167. data/packaging/rpm/passenger_spec/passenger.spec.template +0 -1
  168. data/passenger.gemspec +0 -1
  169. data/resources/templates/config/nginx_engine_compiler/possible_solutions_for_download_and_extraction_problems.txt.erb +27 -0
  170. data/resources/templates/standalone/config.erb +19 -15
  171. data/test/integration_tests/apache2_tests.rb +566 -566
  172. data/test/integration_tests/downloaded_binaries_tests.rb +126 -125
  173. data/test/integration_tests/native_packaging_spec.rb +296 -296
  174. data/test/integration_tests/nginx_tests.rb +393 -393
  175. data/test/integration_tests/shared/example_webapp_tests.rb +282 -280
  176. data/test/integration_tests/source_packaging_test.rb +138 -138
  177. data/test/integration_tests/spec_helper.rb +5 -5
  178. data/test/integration_tests/standalone_tests.rb +367 -367
  179. data/test/ruby/debug_logging_spec.rb +133 -133
  180. data/test/ruby/message_channel_spec.rb +186 -186
  181. data/test/ruby/rack/loader_spec.rb +28 -28
  182. data/test/ruby/rack/preloader_spec.rb +34 -34
  183. data/test/ruby/rails3.0/loader_spec.rb +12 -12
  184. data/test/ruby/rails3.0/preloader_spec.rb +18 -18
  185. data/test/ruby/rails3.1/loader_spec.rb +12 -12
  186. data/test/ruby/rails3.1/preloader_spec.rb +18 -18
  187. data/test/ruby/rails3.2/loader_spec.rb +12 -12
  188. data/test/ruby/rails3.2/preloader_spec.rb +18 -18
  189. data/test/ruby/rails4.0/loader_spec.rb +12 -12
  190. data/test/ruby/rails4.0/preloader_spec.rb +18 -18
  191. data/test/ruby/rails4.1/loader_spec.rb +12 -12
  192. data/test/ruby/rails4.1/preloader_spec.rb +18 -18
  193. data/test/ruby/request_handler_spec.rb +730 -730
  194. data/test/ruby/shared/loader_sharedspec.rb +224 -224
  195. data/test/ruby/shared/rails/union_station_extensions_sharedspec.rb +327 -327
  196. data/test/ruby/shared/ruby_loader_sharedspec.rb +47 -47
  197. data/test/ruby/spec_helper.rb +65 -65
  198. data/test/ruby/standalone/runtime_installer_spec.rb +384 -384
  199. data/test/ruby/union_station_spec.rb +276 -276
  200. data/test/ruby/utils/file_system_watcher_spec.rb +220 -220
  201. data/test/ruby/utils/hosts_file_parser.rb +248 -248
  202. data/test/ruby/utils/tee_input_spec.rb +215 -215
  203. data/test/ruby/utils/unseekable_socket_spec.rb +57 -57
  204. data/test/ruby/utils_spec.rb +21 -21
  205. data/test/stub/rack/config.ru +87 -87
  206. data/test/stub/rack/library.rb +8 -8
  207. data/test/stub/rack/start.rb +30 -30
  208. data/test/support/apache2_controller.rb +191 -191
  209. data/test/support/nginx_controller.rb +90 -99
  210. data/test/support/placebo-preloader.rb +57 -57
  211. data/test/support/test_helper.rb +435 -435
  212. metadata +11 -21
  213. metadata.gz.asc +7 -7
  214. data/lib/phusion_passenger/standalone/command2.rb +0 -292
  215. data/lib/phusion_passenger/standalone/start2_command.rb +0 -799
  216. data/resources/templates/standalone/download_tool_missing.txt.erb +0 -18
  217. data/resources/templates/standalone/possible_solutions_for_download_and_extraction_problems.txt.erb +0 -17
  218. data/resources/templates/standalone/run_installer_as_root.txt.erb +0 -8
@@ -6,22 +6,22 @@ if RUBY_VERSION_INT >= 190
6
6
  module PhusionPassenger
7
7
 
8
8
  describe "Rack loader with Rails 4.1" do
9
- include LoaderSpecHelper
9
+ include LoaderSpecHelper
10
10
 
11
- before :each do
12
- @stub = register_stub(RackStub.new("rails4.1"))
13
- end
11
+ before :each do
12
+ @stub = register_stub(RackStub.new("rails4.1"))
13
+ end
14
14
 
15
- def start(options = {})
16
- @loader = Loader.new(["ruby", "#{PhusionPassenger.helper_scripts_dir}/rack-loader.rb"], @stub.app_root)
17
- return @loader.start(options)
18
- end
15
+ def start(options = {})
16
+ @loader = Loader.new(["ruby", "#{PhusionPassenger.helper_scripts_dir}/rack-loader.rb"], @stub.app_root)
17
+ return @loader.start(options)
18
+ end
19
19
 
20
- def rails_version
21
- return "4.1"
22
- end
20
+ def rails_version
21
+ return "4.1"
22
+ end
23
23
 
24
- include_examples "Union Station extensions for Rails"
24
+ include_examples "Union Station extensions for Rails"
25
25
  end
26
26
 
27
27
  end # module PhusionPassenger
@@ -6,28 +6,28 @@ if RUBY_VERSION_INT >= 190
6
6
  module PhusionPassenger
7
7
 
8
8
  describe "Rack preloader with Rails 4.1" do
9
- include LoaderSpecHelper
9
+ include LoaderSpecHelper
10
10
 
11
- before :each do
12
- @stub = register_stub(RackStub.new("rails4.1"))
13
- end
11
+ before :each do
12
+ @stub = register_stub(RackStub.new("rails4.1"))
13
+ end
14
14
 
15
- def start(options = {})
16
- @preloader = Preloader.new(["ruby", "#{PhusionPassenger.helper_scripts_dir}/rack-preloader.rb"], @stub.app_root)
17
- result = @preloader.start(options)
18
- if result[:status] == "Ready"
19
- @loader = @preloader.spawn(options)
20
- return @loader.start(options)
21
- else
22
- return result
23
- end
24
- end
15
+ def start(options = {})
16
+ @preloader = Preloader.new(["ruby", "#{PhusionPassenger.helper_scripts_dir}/rack-preloader.rb"], @stub.app_root)
17
+ result = @preloader.start(options)
18
+ if result[:status] == "Ready"
19
+ @loader = @preloader.spawn(options)
20
+ return @loader.start(options)
21
+ else
22
+ return result
23
+ end
24
+ end
25
25
 
26
- def rails_version
27
- return "4.1"
28
- end
26
+ def rails_version
27
+ return "4.1"
28
+ end
29
29
 
30
- include_examples "Union Station extensions for Rails"
30
+ include_examples "Union Station extensions for Rails"
31
31
  end
32
32
 
33
33
  end # module PhusionPassenger
@@ -12,736 +12,736 @@ require 'tmpdir'
12
12
  module PhusionPassenger
13
13
 
14
14
  describe RequestHandler do
15
- class DummyThreadHandler < RequestHandler::ThreadHandler
16
- def process_request(*args)
17
- # Do nothing.
18
- end
19
- end
20
-
21
- before :each do
22
- @temp_dir = Dir.mktmpdir
23
- preinitialize if respond_to?(:preinitialize)
24
- @owner_pipe = IO.pipe
25
- @options ||= {}
26
- @thread_handler = Class.new(DummyThreadHandler)
27
- @options = {
28
- "app_group_name" => "foobar",
29
- "thread_handler" => @thread_handler,
30
- "socket_dir" => @temp_dir,
31
- "keepalive" => false
32
- }.merge(@options)
33
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
34
- end
35
-
36
- after :each do
37
- stop_request_handler
38
- if @temp_dir
39
- FileUtils.chmod_R(0777, @temp_dir)
40
- FileUtils.rm_rf(@temp_dir)
41
- end
42
- end
43
-
44
- def stop_request_handler
45
- if @request_handler
46
- @request_handler.cleanup
47
- @owner_pipe[0].close rescue nil
48
- @request_handler = nil
49
- end
50
- end
51
-
52
- def connect(socket_name = :main)
53
- address = @request_handler.server_sockets[socket_name][:address]
54
- return Utils.connect_to_server(address)
55
- end
56
-
57
- def send_binary_request(socket, env)
58
- channel = MessageChannel.new(socket)
59
- data = ""
60
- env.each_pair do |key, value|
61
- data << key << "\0"
62
- data << value << "\0"
63
- end
64
- channel.write_scalar(data)
65
- end
66
-
67
- it "exits if the owner pipe is closed" do
68
- @request_handler.start_main_loop_thread
69
- @owner_pipe[0].close
70
- eventually do
71
- !@request_handler.main_loop_running?
72
- end
73
- end
74
-
75
- it "creates a socket file in the Phusion Passenger temp folder, unless when using TCP sockets" do
76
- if @request_handler.server_sockets[:main][1] == "unix"
77
- File.chmod(0700, "#{@temp_dir}/backends")
78
- Dir["#{@temp_dir}/backends/*"].should_not be_empty
79
- end
80
- end
81
-
82
- specify "the main socket rejects headers that are too large" do
83
- stderr = StringIO.new
84
- DebugLogging.log_level = DEFAULT_LOG_LEVEL
85
- DebugLogging.stderr_evaluator = lambda { stderr }
86
- @request_handler.start_main_loop_thread
87
- begin
88
- client = connect
89
- client.sync = true
90
- block = lambda do
91
- data = "REQUEST_METHOD\0/"
92
- data << "x" * (RequestHandler::ThreadHandler::MAX_HEADER_SIZE * 2)
93
- data << "\0"
94
- MessageChannel.new(client).write_scalar(data)
95
- end
96
- block.should raise_error(Errno::EPIPE)
97
- stderr.string.should_not be_empty
98
- ensure
99
- client.close rescue nil
100
- end
101
- end
102
-
103
- specify "the main socket rejects unauthenticated connections, if a connect password is supplied" do
104
- @request_handler.connect_password = "1234"
105
- @request_handler.start_main_loop_thread
106
- begin
107
- client = connect
108
- channel = MessageChannel.new(client)
109
- channel.write_scalar("REQUEST_METHOD\0PING\0")
110
- client.read.should == ""
111
- ensure
112
- client.close rescue nil
113
- end
114
- begin
115
- client = connect
116
- channel = MessageChannel.new(client)
117
- channel.write_scalar("REQUEST_METHOD\0PING\0PASSENGER_CONNECT_PASSWORD\0001234\0")
118
- client.read.should == "pong"
119
- ensure
120
- client.close rescue nil
121
- end
122
- end
123
-
124
- it "accepts pings on the main server socket" do
125
- @request_handler.start_main_loop_thread
126
- client = connect
127
- begin
128
- channel = MessageChannel.new(client)
129
- channel.write_scalar("REQUEST_METHOD\0PING\0")
130
- client.read.should == "pong"
131
- ensure
132
- client.close
133
- end
134
- end
135
-
136
- it "accepts pings on the HTTP server socket" do
137
- @request_handler.start_main_loop_thread
138
- client = connect(:http)
139
- begin
140
- client.write("PING / HTTP/1.1\r\n")
141
- client.write("Host: foo.com\r\n\r\n")
142
- client.close_write
143
- client.read.should == "pong"
144
- ensure
145
- client.close
146
- end
147
- end
148
-
149
- specify "the HTTP socket rejects headers that are too large" do
150
- stderr = StringIO.new
151
- DebugLogging.log_level = DEFAULT_LOG_LEVEL
152
- DebugLogging.stderr_evaluator = lambda { stderr }
153
- @request_handler.start_main_loop_thread
154
- begin
155
- client = connect(:http)
156
- client.sync = true
157
- block = lambda do
158
- client.write("GET /")
159
- client.write("x" * RequestHandler::ThreadHandler::MAX_HEADER_SIZE)
160
- sleep 0.01 # Context switch
161
- client.write("x" * RequestHandler::ThreadHandler::MAX_HEADER_SIZE)
162
- sleep 0.01 # Context switch
163
- client.write(" HTTP/1.1\r\n")
164
- end
165
- block.should raise_error(SystemCallError)
166
- stderr.string.should_not be_empty
167
- ensure
168
- client.close rescue nil
169
- end
170
- end
171
-
172
- specify "the HTTP socket rejects unauthenticated connections, if a connect password is supplied" do
173
- DebugLogging.log_level = LVL_ERROR
174
- @request_handler.connect_password = "1234"
175
- @request_handler.start_main_loop_thread
176
- begin
177
- client = connect(:http)
178
- client.write("PING / HTTP/1.1\r\n")
179
- client.write("\r\n")
180
- client.read.should == ""
181
- ensure
182
- client.close rescue nil
183
- end
184
- begin
185
- client = connect(:http)
186
- client.write("PING / HTTP/1.1\r\n")
187
- client.write("X-Passenger-Connect-Password: 1234\r\n")
188
- client.write("\r\n")
189
- client.read.should == "pong"
190
- ensure
191
- client.close rescue nil
192
- end
193
- end
194
-
195
- it "catches exceptions generated by the Rack application object" do
196
- @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
197
- include Rack::ThreadHandlerExtension
198
- end
199
-
200
- lambda_called = false
201
-
202
- # Here we test that the exception is not propagated to outside the request handler.
203
- @options["app"] = lambda do |env|
204
- lambda_called = true
205
- raise "an error"
206
- end
207
-
208
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
209
- @request_handler.start_main_loop_thread
210
- client = connect
211
- begin
212
- send_binary_request(client,
213
- "REQUEST_METHOD" => "GET",
214
- "PATH_INFO" => "/")
215
- client.read
216
- ensure
217
- client.close
218
- end
219
-
220
- lambda_called.should == true
221
- end
222
-
223
- it "catches exceptions generated by the Rack body object" do
224
- @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
225
- include Rack::ThreadHandlerExtension
226
- end
227
-
228
- lambda_called = false
229
-
230
- # Here we test that the exception is not propagated to outside the request handler.
231
- @options["app"] = lambda do |env|
232
- lambda_called = true
233
- body = Object.new
234
- def body.each
235
- raise "an error"
236
- end
237
- [200, { "Content-Type" => "text/plain" }, body]
238
- end
239
-
240
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
241
- @request_handler.start_main_loop_thread
242
- client = connect
243
- begin
244
- send_binary_request(client,
245
- "REQUEST_METHOD" => "GET",
246
- "PATH_INFO" => "/")
247
- client.read
248
- ensure
249
- client.close
250
- end
251
-
252
- lambda_called.should == true
253
- end
254
-
255
- it "allows the application to take over the socket completely through the full hijack API" do
256
- @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
257
- include Rack::ThreadHandlerExtension
258
- end
259
-
260
- lambda_called = false
261
-
262
- @options["app"] = lambda do |env|
263
- lambda_called = true
264
- env['rack.hijack?'].should be_true
265
- env['rack.hijack_io'].should be_nil
266
- env['rack.hijack'].call
267
- Thread.new do
268
- Thread.current.abort_on_exception = true
269
- sleep 0.1
270
- env['rack.hijack_io'].write("Hijacked response!")
271
- env['rack.hijack_io'].close
272
- end
273
- end
274
-
275
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
276
- @request_handler.start_main_loop_thread
277
- client = connect
278
- begin
279
- send_binary_request(client,
280
- "REQUEST_METHOD" => "GET",
281
- "PATH_INFO" => "/")
282
- sleep 0.1 # Give it some time to handle the request.
283
- stop_request_handler
284
- client.read.should == "Hijacked response!"
285
- ensure
286
- client.close
287
- end
288
-
289
- lambda_called.should == true
290
- end
291
-
292
- it "allows the application to take over the socket after sending headers through the partial hijack API" do
293
- @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
294
- include Rack::ThreadHandlerExtension
295
- end
296
-
297
- lambda_called = false
298
- hijack_callback_called = false
299
-
300
- @options["app"] = lambda do |env|
301
- lambda_called = true
302
- env['rack.hijack?'].should be_true
303
- env['rack.hijack_io'].should be_nil
304
- hijack_callback = lambda do |socket|
305
- hijack_callback_called = true
306
- env['rack.hijack_io'].should_not be_nil
307
- env['rack.hijack_io'].should == socket
308
- socket.write("Hijacked partial response!")
309
- socket.close
310
- end
311
- [200, { 'Content-Type' => 'text/html', 'rack.hijack' => hijack_callback }]
312
- end
313
-
314
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
315
- @request_handler.start_main_loop_thread
316
- client = connect
317
- begin
318
- send_binary_request(client,
319
- "REQUEST_METHOD" => "GET",
320
- "PATH_INFO" => "/")
321
- client.read.should ==
322
- "HTTP/1.1 200 Whatever\r\n" +
323
- "Content-Type: text/html\r\n" +
324
- "Connection: close\r\n" +
325
- "\r\n" +
326
- "Hijacked partial response!"
327
- ensure
328
- client.close
329
- end
330
-
331
- lambda_called.should == true
332
- hijack_callback_called.should == true
333
- end
334
-
335
- specify "requests with Content-Length are assumed to have a request body" do
336
- @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
337
- include Rack::ThreadHandlerExtension
338
- end
339
-
340
- lambda_called = false
341
-
342
- @options["app"] = lambda do |env|
343
- lambda_called = true
344
- env['rack.input'].read(3).should == "abc"
345
- [200, {}, ["ok"]]
346
- end
347
-
348
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
349
- @request_handler.start_main_loop_thread
350
- client = connect
351
- begin
352
- send_binary_request(client,
353
- "REQUEST_METHOD" => "GET",
354
- "PATH_INFO" => "/",
355
- "CONTENT_LENGTH" => "3")
356
- client.write("abc")
357
- client.close_write
358
- client.read.should ==
359
- "HTTP/1.1 200 Whatever\r\n" +
360
- "Connection: close\r\n" +
361
- "Content-Length: 2\r\n" +
362
- "\r\n" +
363
- "ok"
364
- ensure
365
- client.close
366
- end
367
-
368
- lambda_called.should be_true
369
- end
370
-
371
- specify "requests with Transfer-Encoding chunked are assumed to have a request body" do
372
- @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
373
- include Rack::ThreadHandlerExtension
374
- end
375
-
376
- lambda_called = false
377
-
378
- @options["app"] = lambda do |env|
379
- lambda_called = true
380
- env['rack.input'].read(13).should ==
381
- "3\r\n" +
382
- "abc\r\n" +
383
- "0\r\n\r\n"
384
- [200, {}, ["ok"]]
385
- end
386
-
387
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
388
- @request_handler.start_main_loop_thread
389
- client = connect
390
- begin
391
- send_binary_request(client,
392
- "REQUEST_METHOD" => "GET",
393
- "PATH_INFO" => "/",
394
- "TRANSFER_ENCODING" => "chunked")
395
- client.write(
396
- "3\r\n" +
397
- "abc\r\n" +
398
- "0\r\n\r\n")
399
- client.close_write
400
- client.read.should ==
401
- "HTTP/1.1 200 Whatever\r\n" +
402
- "Connection: close\r\n" +
403
- "Content-Length: 2\r\n" +
404
- "\r\n" +
405
- "ok"
406
- ensure
407
- client.close
408
- end
409
-
410
- lambda_called.should be_true
411
- end
412
-
413
- specify "requests with neither Content-Length nor Transfer-Encoding are assumed to have no request body" do
414
- @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
415
- include Rack::ThreadHandlerExtension
416
- end
417
-
418
- lambda_called = false
419
-
420
- @options["app"] = lambda do |env|
421
- lambda_called = true
422
- env['rack.input'].read(1).should be_nil
423
- env['rack.input'].gets.should be_nil
424
- [200, {}, ["ok"]]
425
- end
426
-
427
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
428
- @request_handler.start_main_loop_thread
429
- client = connect
430
- begin
431
- send_binary_request(client,
432
- "REQUEST_METHOD" => "POST",
433
- "PATH_INFO" => "/")
434
- client.close_write
435
- client.read.should ==
436
- "HTTP/1.1 200 Whatever\r\n" +
437
- "Connection: close\r\n" +
438
- "Content-Length: 2\r\n" +
439
- "\r\n" +
440
- "ok"
441
- ensure
442
- client.close
443
- end
444
-
445
- lambda_called.should be_true
446
- end
447
-
448
- describe "on requests that are not supposed to have a body" do
449
- before :each do
450
- @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
451
- include Rack::ThreadHandlerExtension
452
- end
453
- end
454
-
455
- it "doesn't allow reading from rack.input" do
456
- lambda_called = false
457
-
458
- @options["app"] = lambda do |env|
459
- lambda_called = true
460
- body = env['rack.input'].read.inspect
461
- [200, { "Content-Type" => "text/plain" }, [body]]
462
- end
463
-
464
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
465
- @request_handler.start_main_loop_thread
466
- client = connect
467
- begin
468
- send_binary_request(client,
469
- "REQUEST_METHOD" => "GET",
470
- "PATH_INFO" => "/")
471
- client.read.should ==
472
- "HTTP/1.1 200 Whatever\r\n" +
473
- "Content-Type: text/plain\r\n" +
474
- "Connection: close\r\n" +
475
- "Content-Length: 2\r\n" +
476
- "\r\n" +
477
- "\"\""
478
- ensure
479
- client.close
480
- end
481
-
482
- lambda_called.should be_true
483
- end
484
-
485
- it "allows reading from the client socket once the socket has been fully hijacked" do
486
- lambda_called = false
487
-
488
- @options["app"] = lambda do |env|
489
- lambda_called = true
490
- env['rack.hijack'].call
491
- io = env['rack.hijack_io']
492
- begin
493
- io.read.should == "hi"
494
- io.write("ok")
495
- ensure
496
- io.close
497
- end
498
- end
499
-
500
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
501
- @request_handler.start_main_loop_thread
502
- client = connect
503
- begin
504
- send_binary_request(client,
505
- "REQUEST_METHOD" => "GET",
506
- "PATH_INFO" => "/")
507
- client.write("hi")
508
- client.close_write
509
- client.read.should == "ok"
510
- ensure
511
- client.close
512
- end
513
-
514
- lambda_called.should be_true
515
- end
516
-
517
- it "allows reading from the client socket once the socket has been partially hijacked" do
518
- lambda_called = false
519
-
520
- @options["app"] = lambda do |env|
521
- block = lambda do |io|
522
- lambda_called = true
523
- begin
524
- io.read.should == "hi"
525
- io.write("ok")
526
- ensure
527
- io.close
528
- end
529
- end
530
- headers = { 'rack.hijack' => block }
531
- [200, headers, []]
532
- end
533
-
534
- @request_handler = RequestHandler.new(@owner_pipe[1], @options)
535
- @request_handler.start_main_loop_thread
536
- client = connect
537
- begin
538
- send_binary_request(client,
539
- "REQUEST_METHOD" => "GET",
540
- "PATH_INFO" => "/")
541
- client.write("hi")
542
- client.close_write
543
- client.read.should ==
544
- "HTTP/1.1 200 Whatever\r\n" +
545
- "Connection: close\r\n" +
546
- "\r\n" +
547
- "ok"
548
- ensure
549
- client.close
550
- end
551
-
552
- lambda_called.should be_true
553
- end
554
- end
555
-
556
- describe "if Union Station core is given" do
557
- def preinitialize
558
- if @agent_pid
559
- Process.kill('KILL', @agent_pid)
560
- Process.waitpid(@agent_pid)
561
- end
562
- @dump_file = "#{@temp_dir}/log.txt"
563
- @logging_agent_password = "1234"
564
- @agent_pid, @socket_filename, @socket_address = spawn_logging_agent(
565
- @temp_dir, @dump_file, @logging_agent_password)
566
-
567
- @union_station_core = UnionStation::Core.new(@socket_address, "logging",
568
- "1234", "localhost")
569
- @options = { "union_station_core" => @union_station_core }
570
- end
571
-
572
- after :each do
573
- if @agent_pid
574
- Process.kill('KILL', @agent_pid)
575
- Process.waitpid(@agent_pid)
576
- end
577
- end
578
-
579
- def base64(data)
580
- return [data].pack('m').gsub("\n", "")
581
- end
582
-
583
- it "makes the analytics log object available through the request env and a thread-local variable" do
584
- header_value = nil
585
- thread_value = nil
586
- @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
587
- header_value = headers[UNION_STATION_REQUEST_TRANSACTION]
588
- thread_value = Thread.current[UNION_STATION_REQUEST_TRANSACTION]
589
- end
590
- @request_handler.start_main_loop_thread
591
- client = connect
592
- begin
593
- send_binary_request(client,
594
- "REQUEST_METHOD" => "GET",
595
- "PASSENGER_TXN_ID" => "1234-abcd",
596
- "PASSENGER_GROUP_NAME" => "foobar")
597
- client.read
598
- ensure
599
- client.close
600
- end
601
- header_value.should be_kind_of(UnionStation::Transaction)
602
- thread_value.should be_kind_of(UnionStation::Transaction)
603
- header_value.should == thread_value
604
- end
605
-
606
- it "logs uncaught exceptions for requests that have a transaction ID" do
607
- reraised = false
608
- @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
609
- raise "something went wrong"
610
- end
611
- @thread_handler.any_instance.stub(:should_reraise_error?).and_return do |e|
612
- reraised = true
613
- e.message != "something went wrong"
614
- end
615
- @request_handler.start_main_loop_thread
616
- client = connect
617
- begin
618
- send_binary_request(client,
619
- "REQUEST_METHOD" => "GET",
620
- "PASSENGER_TXN_ID" => "1234-abcd")
621
- ensure
622
- client.close
623
- end
624
- eventually(5) do
625
- flush_logging_agent(@logging_agent_password, @socket_address)
626
- if File.exist?(@dump_file)
627
- log_data = File.read(@dump_file)
628
- else
629
- log_data = ""
630
- end
631
- log_data.include?("Request transaction ID: 1234-abcd\n") &&
632
- log_data.include?("Message: " + base64("something went wrong")) &&
633
- log_data.include?("Class: RuntimeError") &&
634
- log_data.include?("Backtrace: ")
635
- end
636
- reraised.should be_true
637
- end
638
- end
639
-
640
- describe "HTTP parsing" do
641
- before :each do
642
- @request_handler.start_main_loop_thread
643
- @client = connect(:http)
644
- @client.sync = true
645
- end
646
-
647
- after :each do
648
- @client.close if @client
649
- end
650
-
651
- it "correctly parses HTTP requests without query string" do
652
- @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
653
- headers["REQUEST_METHOD"].should == "POST"
654
- headers["SERVER_PROTOCOL"].should == "HTTP/1.1"
655
- headers["HTTP_HOST"].should == "foo.com"
656
- headers["HTTP_X_FOO_BAR"].should == "baz"
657
- headers["PATH_INFO"].should == "/foo/bar"
658
- headers["SCRIPT_NAME"].should == ""
659
- headers["QUERY_STRING"].should == ""
660
- headers["REQUEST_URI"].should == "/foo/bar"
661
- headers["HTTP_CONTENT_LENGTH"].should be_nil
662
- headers["HTTP_CONTENT_TYPE"].should be_nil
663
- headers["CONTENT_LENGTH"].should == "10"
664
- headers["CONTENT_TYPE"].should == "text/plain"
665
- end
666
-
667
- @client.write("POST /foo/bar HTTP/1.1\r\n")
668
- @client.write("Host: foo.com\r\n")
669
- @client.write("X-Foo-Bar: baz\r\n")
670
- @client.write("Content-Length: 10\r\n")
671
- @client.write("Content-Type: text/plain\r\n")
672
- @client.write("\r\n")
673
- @client.close_write
674
- @client.read
675
- end
676
-
677
- it "correctly parses HTTP requests with query string" do
678
- @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
679
- headers["REQUEST_METHOD"].should == "POST"
680
- headers["SERVER_PROTOCOL"].should == "HTTP/1.1"
681
- headers["HTTP_HOST"].should == "foo.com"
682
- headers["HTTP_X_FOO_BAR"].should == "baz"
683
- headers["PATH_INFO"].should == "/foo/bar"
684
- headers["SCRIPT_NAME"].should == ""
685
- headers["QUERY_STRING"].should == "hello=world&a=b+c"
686
- headers["REQUEST_URI"].should == "/foo/bar?hello=world&a=b+c"
687
- headers["HTTP_CONTENT_LENGTH"].should be_nil
688
- headers["HTTP_CONTENT_TYPE"].should be_nil
689
- headers["CONTENT_LENGTH"].should == "10"
690
- headers["CONTENT_TYPE"].should == "text/plain"
691
- end
692
-
693
- @client.write("POST /foo/bar?hello=world&a=b+c HTTP/1.1\r\n")
694
- @client.write("Host: foo.com\r\n")
695
- @client.write("X-Foo-Bar: baz\r\n")
696
- @client.write("Content-Length: 10\r\n")
697
- @client.write("Content-Type: text/plain\r\n")
698
- @client.write("\r\n")
699
- @client.close_write
700
- @client.read
701
- end
702
-
703
- it "correct parses HTTP requests that come in arbitrary chunks" do
704
- @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
705
- headers["REQUEST_METHOD"].should == "POST"
706
- headers["SERVER_PROTOCOL"].should == "HTTP/1.1"
707
- headers["HTTP_HOST"].should == "foo.com"
708
- headers["HTTP_X_FOO_BAR"].should == "baz"
709
- headers["PATH_INFO"].should == "/foo/bar"
710
- headers["SCRIPT_NAME"].should == ""
711
- headers["QUERY_STRING"].should == "hello=world&a=b+c"
712
- headers["REQUEST_URI"].should == "/foo/bar?hello=world&a=b+c"
713
- headers["HTTP_CONTENT_LENGTH"].should be_nil
714
- headers["HTTP_CONTENT_TYPE"].should be_nil
715
- headers["CONTENT_LENGTH"].should == "10"
716
- headers["CONTENT_TYPE"].should == "text/plain"
717
- headers["HTTP_PLUS_SOME"].should be_nil
718
- end
719
-
720
- @client.write("POST /fo")
721
- sleep 0.001
722
- @client.write("o/bar?hello=world&a=b+c HT")
723
- sleep 0.001
724
- @client.write("TP/1.1\r")
725
- sleep 0.001
726
- @client.write("\nHost: foo.com")
727
- sleep 0.001
728
- @client.write("\r\n")
729
- sleep 0.001
730
- @client.write("X-Foo-Bar: baz\r\n")
731
- sleep 0.001
732
- @client.write("Content-Len")
733
- sleep 0.001
734
- @client.write("gth: 10\r\nContent-Type: text/pla")
735
- sleep 0.001
736
- @client.write("in\r\n\r")
737
- sleep 0.001
738
- @client.write("\nPlus-Some: garbage data that should be ignored.")
739
- @client.close_write
740
- @client.read
741
- end
742
- end
743
-
744
- ############################
15
+ class DummyThreadHandler < RequestHandler::ThreadHandler
16
+ def process_request(*args)
17
+ # Do nothing.
18
+ end
19
+ end
20
+
21
+ before :each do
22
+ @temp_dir = Dir.mktmpdir
23
+ preinitialize if respond_to?(:preinitialize)
24
+ @owner_pipe = IO.pipe
25
+ @options ||= {}
26
+ @thread_handler = Class.new(DummyThreadHandler)
27
+ @options = {
28
+ "app_group_name" => "foobar",
29
+ "thread_handler" => @thread_handler,
30
+ "socket_dir" => @temp_dir,
31
+ "keepalive" => false
32
+ }.merge(@options)
33
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
34
+ end
35
+
36
+ after :each do
37
+ stop_request_handler
38
+ if @temp_dir
39
+ FileUtils.chmod_R(0777, @temp_dir)
40
+ FileUtils.rm_rf(@temp_dir)
41
+ end
42
+ end
43
+
44
+ def stop_request_handler
45
+ if @request_handler
46
+ @request_handler.cleanup
47
+ @owner_pipe[0].close rescue nil
48
+ @request_handler = nil
49
+ end
50
+ end
51
+
52
+ def connect(socket_name = :main)
53
+ address = @request_handler.server_sockets[socket_name][:address]
54
+ return Utils.connect_to_server(address)
55
+ end
56
+
57
+ def send_binary_request(socket, env)
58
+ channel = MessageChannel.new(socket)
59
+ data = ""
60
+ env.each_pair do |key, value|
61
+ data << key << "\0"
62
+ data << value << "\0"
63
+ end
64
+ channel.write_scalar(data)
65
+ end
66
+
67
+ it "exits if the owner pipe is closed" do
68
+ @request_handler.start_main_loop_thread
69
+ @owner_pipe[0].close
70
+ eventually do
71
+ !@request_handler.main_loop_running?
72
+ end
73
+ end
74
+
75
+ it "creates a socket file in the Phusion Passenger temp folder, unless when using TCP sockets" do
76
+ if @request_handler.server_sockets[:main][1] == "unix"
77
+ File.chmod(0700, "#{@temp_dir}/backends")
78
+ Dir["#{@temp_dir}/backends/*"].should_not be_empty
79
+ end
80
+ end
81
+
82
+ specify "the main socket rejects headers that are too large" do
83
+ stderr = StringIO.new
84
+ DebugLogging.log_level = DEFAULT_LOG_LEVEL
85
+ DebugLogging.stderr_evaluator = lambda { stderr }
86
+ @request_handler.start_main_loop_thread
87
+ begin
88
+ client = connect
89
+ client.sync = true
90
+ block = lambda do
91
+ data = "REQUEST_METHOD\0/"
92
+ data << "x" * (RequestHandler::ThreadHandler::MAX_HEADER_SIZE * 2)
93
+ data << "\0"
94
+ MessageChannel.new(client).write_scalar(data)
95
+ end
96
+ block.should raise_error(Errno::EPIPE)
97
+ stderr.string.should_not be_empty
98
+ ensure
99
+ client.close rescue nil
100
+ end
101
+ end
102
+
103
+ specify "the main socket rejects unauthenticated connections, if a connect password is supplied" do
104
+ @request_handler.connect_password = "1234"
105
+ @request_handler.start_main_loop_thread
106
+ begin
107
+ client = connect
108
+ channel = MessageChannel.new(client)
109
+ channel.write_scalar("REQUEST_METHOD\0PING\0")
110
+ client.read.should == ""
111
+ ensure
112
+ client.close rescue nil
113
+ end
114
+ begin
115
+ client = connect
116
+ channel = MessageChannel.new(client)
117
+ channel.write_scalar("REQUEST_METHOD\0PING\0PASSENGER_CONNECT_PASSWORD\0001234\0")
118
+ client.read.should == "pong"
119
+ ensure
120
+ client.close rescue nil
121
+ end
122
+ end
123
+
124
+ it "accepts pings on the main server socket" do
125
+ @request_handler.start_main_loop_thread
126
+ client = connect
127
+ begin
128
+ channel = MessageChannel.new(client)
129
+ channel.write_scalar("REQUEST_METHOD\0PING\0")
130
+ client.read.should == "pong"
131
+ ensure
132
+ client.close
133
+ end
134
+ end
135
+
136
+ it "accepts pings on the HTTP server socket" do
137
+ @request_handler.start_main_loop_thread
138
+ client = connect(:http)
139
+ begin
140
+ client.write("PING / HTTP/1.1\r\n")
141
+ client.write("Host: foo.com\r\n\r\n")
142
+ client.close_write
143
+ client.read.should == "pong"
144
+ ensure
145
+ client.close
146
+ end
147
+ end
148
+
149
+ specify "the HTTP socket rejects headers that are too large" do
150
+ stderr = StringIO.new
151
+ DebugLogging.log_level = DEFAULT_LOG_LEVEL
152
+ DebugLogging.stderr_evaluator = lambda { stderr }
153
+ @request_handler.start_main_loop_thread
154
+ begin
155
+ client = connect(:http)
156
+ client.sync = true
157
+ block = lambda do
158
+ client.write("GET /")
159
+ client.write("x" * RequestHandler::ThreadHandler::MAX_HEADER_SIZE)
160
+ sleep 0.01 # Context switch
161
+ client.write("x" * RequestHandler::ThreadHandler::MAX_HEADER_SIZE)
162
+ sleep 0.01 # Context switch
163
+ client.write(" HTTP/1.1\r\n")
164
+ end
165
+ block.should raise_error(SystemCallError)
166
+ stderr.string.should_not be_empty
167
+ ensure
168
+ client.close rescue nil
169
+ end
170
+ end
171
+
172
+ specify "the HTTP socket rejects unauthenticated connections, if a connect password is supplied" do
173
+ DebugLogging.log_level = LVL_ERROR
174
+ @request_handler.connect_password = "1234"
175
+ @request_handler.start_main_loop_thread
176
+ begin
177
+ client = connect(:http)
178
+ client.write("PING / HTTP/1.1\r\n")
179
+ client.write("\r\n")
180
+ client.read.should == ""
181
+ ensure
182
+ client.close rescue nil
183
+ end
184
+ begin
185
+ client = connect(:http)
186
+ client.write("PING / HTTP/1.1\r\n")
187
+ client.write("X-Passenger-Connect-Password: 1234\r\n")
188
+ client.write("\r\n")
189
+ client.read.should == "pong"
190
+ ensure
191
+ client.close rescue nil
192
+ end
193
+ end
194
+
195
+ it "catches exceptions generated by the Rack application object" do
196
+ @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
197
+ include Rack::ThreadHandlerExtension
198
+ end
199
+
200
+ lambda_called = false
201
+
202
+ # Here we test that the exception is not propagated to outside the request handler.
203
+ @options["app"] = lambda do |env|
204
+ lambda_called = true
205
+ raise "an error"
206
+ end
207
+
208
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
209
+ @request_handler.start_main_loop_thread
210
+ client = connect
211
+ begin
212
+ send_binary_request(client,
213
+ "REQUEST_METHOD" => "GET",
214
+ "PATH_INFO" => "/")
215
+ client.read
216
+ ensure
217
+ client.close
218
+ end
219
+
220
+ lambda_called.should == true
221
+ end
222
+
223
+ it "catches exceptions generated by the Rack body object" do
224
+ @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
225
+ include Rack::ThreadHandlerExtension
226
+ end
227
+
228
+ lambda_called = false
229
+
230
+ # Here we test that the exception is not propagated to outside the request handler.
231
+ @options["app"] = lambda do |env|
232
+ lambda_called = true
233
+ body = Object.new
234
+ def body.each
235
+ raise "an error"
236
+ end
237
+ [200, { "Content-Type" => "text/plain" }, body]
238
+ end
239
+
240
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
241
+ @request_handler.start_main_loop_thread
242
+ client = connect
243
+ begin
244
+ send_binary_request(client,
245
+ "REQUEST_METHOD" => "GET",
246
+ "PATH_INFO" => "/")
247
+ client.read
248
+ ensure
249
+ client.close
250
+ end
251
+
252
+ lambda_called.should == true
253
+ end
254
+
255
+ it "allows the application to take over the socket completely through the full hijack API" do
256
+ @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
257
+ include Rack::ThreadHandlerExtension
258
+ end
259
+
260
+ lambda_called = false
261
+
262
+ @options["app"] = lambda do |env|
263
+ lambda_called = true
264
+ env['rack.hijack?'].should be_true
265
+ env['rack.hijack_io'].should be_nil
266
+ env['rack.hijack'].call
267
+ Thread.new do
268
+ Thread.current.abort_on_exception = true
269
+ sleep 0.1
270
+ env['rack.hijack_io'].write("Hijacked response!")
271
+ env['rack.hijack_io'].close
272
+ end
273
+ end
274
+
275
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
276
+ @request_handler.start_main_loop_thread
277
+ client = connect
278
+ begin
279
+ send_binary_request(client,
280
+ "REQUEST_METHOD" => "GET",
281
+ "PATH_INFO" => "/")
282
+ sleep 0.1 # Give it some time to handle the request.
283
+ stop_request_handler
284
+ client.read.should == "Hijacked response!"
285
+ ensure
286
+ client.close
287
+ end
288
+
289
+ lambda_called.should == true
290
+ end
291
+
292
+ it "allows the application to take over the socket after sending headers through the partial hijack API" do
293
+ @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
294
+ include Rack::ThreadHandlerExtension
295
+ end
296
+
297
+ lambda_called = false
298
+ hijack_callback_called = false
299
+
300
+ @options["app"] = lambda do |env|
301
+ lambda_called = true
302
+ env['rack.hijack?'].should be_true
303
+ env['rack.hijack_io'].should be_nil
304
+ hijack_callback = lambda do |socket|
305
+ hijack_callback_called = true
306
+ env['rack.hijack_io'].should_not be_nil
307
+ env['rack.hijack_io'].should == socket
308
+ socket.write("Hijacked partial response!")
309
+ socket.close
310
+ end
311
+ [200, { 'Content-Type' => 'text/html', 'rack.hijack' => hijack_callback }]
312
+ end
313
+
314
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
315
+ @request_handler.start_main_loop_thread
316
+ client = connect
317
+ begin
318
+ send_binary_request(client,
319
+ "REQUEST_METHOD" => "GET",
320
+ "PATH_INFO" => "/")
321
+ client.read.should ==
322
+ "HTTP/1.1 200 Whatever\r\n" +
323
+ "Content-Type: text/html\r\n" +
324
+ "Connection: close\r\n" +
325
+ "\r\n" +
326
+ "Hijacked partial response!"
327
+ ensure
328
+ client.close
329
+ end
330
+
331
+ lambda_called.should == true
332
+ hijack_callback_called.should == true
333
+ end
334
+
335
+ specify "requests with Content-Length are assumed to have a request body" do
336
+ @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
337
+ include Rack::ThreadHandlerExtension
338
+ end
339
+
340
+ lambda_called = false
341
+
342
+ @options["app"] = lambda do |env|
343
+ lambda_called = true
344
+ env['rack.input'].read(3).should == "abc"
345
+ [200, {}, ["ok"]]
346
+ end
347
+
348
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
349
+ @request_handler.start_main_loop_thread
350
+ client = connect
351
+ begin
352
+ send_binary_request(client,
353
+ "REQUEST_METHOD" => "GET",
354
+ "PATH_INFO" => "/",
355
+ "CONTENT_LENGTH" => "3")
356
+ client.write("abc")
357
+ client.close_write
358
+ client.read.should ==
359
+ "HTTP/1.1 200 Whatever\r\n" +
360
+ "Connection: close\r\n" +
361
+ "Content-Length: 2\r\n" +
362
+ "\r\n" +
363
+ "ok"
364
+ ensure
365
+ client.close
366
+ end
367
+
368
+ lambda_called.should be_true
369
+ end
370
+
371
+ specify "requests with Transfer-Encoding chunked are assumed to have a request body" do
372
+ @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
373
+ include Rack::ThreadHandlerExtension
374
+ end
375
+
376
+ lambda_called = false
377
+
378
+ @options["app"] = lambda do |env|
379
+ lambda_called = true
380
+ env['rack.input'].read(13).should ==
381
+ "3\r\n" +
382
+ "abc\r\n" +
383
+ "0\r\n\r\n"
384
+ [200, {}, ["ok"]]
385
+ end
386
+
387
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
388
+ @request_handler.start_main_loop_thread
389
+ client = connect
390
+ begin
391
+ send_binary_request(client,
392
+ "REQUEST_METHOD" => "GET",
393
+ "PATH_INFO" => "/",
394
+ "TRANSFER_ENCODING" => "chunked")
395
+ client.write(
396
+ "3\r\n" +
397
+ "abc\r\n" +
398
+ "0\r\n\r\n")
399
+ client.close_write
400
+ client.read.should ==
401
+ "HTTP/1.1 200 Whatever\r\n" +
402
+ "Connection: close\r\n" +
403
+ "Content-Length: 2\r\n" +
404
+ "\r\n" +
405
+ "ok"
406
+ ensure
407
+ client.close
408
+ end
409
+
410
+ lambda_called.should be_true
411
+ end
412
+
413
+ specify "requests with neither Content-Length nor Transfer-Encoding are assumed to have no request body" do
414
+ @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
415
+ include Rack::ThreadHandlerExtension
416
+ end
417
+
418
+ lambda_called = false
419
+
420
+ @options["app"] = lambda do |env|
421
+ lambda_called = true
422
+ env['rack.input'].read(1).should be_nil
423
+ env['rack.input'].gets.should be_nil
424
+ [200, {}, ["ok"]]
425
+ end
426
+
427
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
428
+ @request_handler.start_main_loop_thread
429
+ client = connect
430
+ begin
431
+ send_binary_request(client,
432
+ "REQUEST_METHOD" => "POST",
433
+ "PATH_INFO" => "/")
434
+ client.close_write
435
+ client.read.should ==
436
+ "HTTP/1.1 200 Whatever\r\n" +
437
+ "Connection: close\r\n" +
438
+ "Content-Length: 2\r\n" +
439
+ "\r\n" +
440
+ "ok"
441
+ ensure
442
+ client.close
443
+ end
444
+
445
+ lambda_called.should be_true
446
+ end
447
+
448
+ describe "on requests that are not supposed to have a body" do
449
+ before :each do
450
+ @options["thread_handler"] = Class.new(RequestHandler::ThreadHandler) do
451
+ include Rack::ThreadHandlerExtension
452
+ end
453
+ end
454
+
455
+ it "doesn't allow reading from rack.input" do
456
+ lambda_called = false
457
+
458
+ @options["app"] = lambda do |env|
459
+ lambda_called = true
460
+ body = env['rack.input'].read.inspect
461
+ [200, { "Content-Type" => "text/plain" }, [body]]
462
+ end
463
+
464
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
465
+ @request_handler.start_main_loop_thread
466
+ client = connect
467
+ begin
468
+ send_binary_request(client,
469
+ "REQUEST_METHOD" => "GET",
470
+ "PATH_INFO" => "/")
471
+ client.read.should ==
472
+ "HTTP/1.1 200 Whatever\r\n" +
473
+ "Content-Type: text/plain\r\n" +
474
+ "Connection: close\r\n" +
475
+ "Content-Length: 2\r\n" +
476
+ "\r\n" +
477
+ "\"\""
478
+ ensure
479
+ client.close
480
+ end
481
+
482
+ lambda_called.should be_true
483
+ end
484
+
485
+ it "allows reading from the client socket once the socket has been fully hijacked" do
486
+ lambda_called = false
487
+
488
+ @options["app"] = lambda do |env|
489
+ lambda_called = true
490
+ env['rack.hijack'].call
491
+ io = env['rack.hijack_io']
492
+ begin
493
+ io.read.should == "hi"
494
+ io.write("ok")
495
+ ensure
496
+ io.close
497
+ end
498
+ end
499
+
500
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
501
+ @request_handler.start_main_loop_thread
502
+ client = connect
503
+ begin
504
+ send_binary_request(client,
505
+ "REQUEST_METHOD" => "GET",
506
+ "PATH_INFO" => "/")
507
+ client.write("hi")
508
+ client.close_write
509
+ client.read.should == "ok"
510
+ ensure
511
+ client.close
512
+ end
513
+
514
+ lambda_called.should be_true
515
+ end
516
+
517
+ it "allows reading from the client socket once the socket has been partially hijacked" do
518
+ lambda_called = false
519
+
520
+ @options["app"] = lambda do |env|
521
+ block = lambda do |io|
522
+ lambda_called = true
523
+ begin
524
+ io.read.should == "hi"
525
+ io.write("ok")
526
+ ensure
527
+ io.close
528
+ end
529
+ end
530
+ headers = { 'rack.hijack' => block }
531
+ [200, headers, []]
532
+ end
533
+
534
+ @request_handler = RequestHandler.new(@owner_pipe[1], @options)
535
+ @request_handler.start_main_loop_thread
536
+ client = connect
537
+ begin
538
+ send_binary_request(client,
539
+ "REQUEST_METHOD" => "GET",
540
+ "PATH_INFO" => "/")
541
+ client.write("hi")
542
+ client.close_write
543
+ client.read.should ==
544
+ "HTTP/1.1 200 Whatever\r\n" +
545
+ "Connection: close\r\n" +
546
+ "\r\n" +
547
+ "ok"
548
+ ensure
549
+ client.close
550
+ end
551
+
552
+ lambda_called.should be_true
553
+ end
554
+ end
555
+
556
+ describe "if Union Station core is given" do
557
+ def preinitialize
558
+ if @agent_pid
559
+ Process.kill('KILL', @agent_pid)
560
+ Process.waitpid(@agent_pid)
561
+ end
562
+ @dump_file = "#{@temp_dir}/log.txt"
563
+ @logging_agent_password = "1234"
564
+ @agent_pid, @socket_filename, @socket_address = spawn_logging_agent(
565
+ @temp_dir, @dump_file, @logging_agent_password)
566
+
567
+ @union_station_core = UnionStation::Core.new(@socket_address, "logging",
568
+ "1234", "localhost")
569
+ @options = { "union_station_core" => @union_station_core }
570
+ end
571
+
572
+ after :each do
573
+ if @agent_pid
574
+ Process.kill('KILL', @agent_pid)
575
+ Process.waitpid(@agent_pid)
576
+ end
577
+ end
578
+
579
+ def base64(data)
580
+ return [data].pack('m').gsub("\n", "")
581
+ end
582
+
583
+ it "makes the analytics log object available through the request env and a thread-local variable" do
584
+ header_value = nil
585
+ thread_value = nil
586
+ @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
587
+ header_value = headers[UNION_STATION_REQUEST_TRANSACTION]
588
+ thread_value = Thread.current[UNION_STATION_REQUEST_TRANSACTION]
589
+ end
590
+ @request_handler.start_main_loop_thread
591
+ client = connect
592
+ begin
593
+ send_binary_request(client,
594
+ "REQUEST_METHOD" => "GET",
595
+ "PASSENGER_TXN_ID" => "1234-abcd",
596
+ "PASSENGER_GROUP_NAME" => "foobar")
597
+ client.read
598
+ ensure
599
+ client.close
600
+ end
601
+ header_value.should be_kind_of(UnionStation::Transaction)
602
+ thread_value.should be_kind_of(UnionStation::Transaction)
603
+ header_value.should == thread_value
604
+ end
605
+
606
+ it "logs uncaught exceptions for requests that have a transaction ID" do
607
+ reraised = false
608
+ @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
609
+ raise "something went wrong"
610
+ end
611
+ @thread_handler.any_instance.stub(:should_reraise_error?).and_return do |e|
612
+ reraised = true
613
+ e.message != "something went wrong"
614
+ end
615
+ @request_handler.start_main_loop_thread
616
+ client = connect
617
+ begin
618
+ send_binary_request(client,
619
+ "REQUEST_METHOD" => "GET",
620
+ "PASSENGER_TXN_ID" => "1234-abcd")
621
+ ensure
622
+ client.close
623
+ end
624
+ eventually(5) do
625
+ flush_logging_agent(@logging_agent_password, @socket_address)
626
+ if File.exist?(@dump_file)
627
+ log_data = File.read(@dump_file)
628
+ else
629
+ log_data = ""
630
+ end
631
+ log_data.include?("Request transaction ID: 1234-abcd\n") &&
632
+ log_data.include?("Message: " + base64("something went wrong")) &&
633
+ log_data.include?("Class: RuntimeError") &&
634
+ log_data.include?("Backtrace: ")
635
+ end
636
+ reraised.should be_true
637
+ end
638
+ end
639
+
640
+ describe "HTTP parsing" do
641
+ before :each do
642
+ @request_handler.start_main_loop_thread
643
+ @client = connect(:http)
644
+ @client.sync = true
645
+ end
646
+
647
+ after :each do
648
+ @client.close if @client
649
+ end
650
+
651
+ it "correctly parses HTTP requests without query string" do
652
+ @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
653
+ headers["REQUEST_METHOD"].should == "POST"
654
+ headers["SERVER_PROTOCOL"].should == "HTTP/1.1"
655
+ headers["HTTP_HOST"].should == "foo.com"
656
+ headers["HTTP_X_FOO_BAR"].should == "baz"
657
+ headers["PATH_INFO"].should == "/foo/bar"
658
+ headers["SCRIPT_NAME"].should == ""
659
+ headers["QUERY_STRING"].should == ""
660
+ headers["REQUEST_URI"].should == "/foo/bar"
661
+ headers["HTTP_CONTENT_LENGTH"].should be_nil
662
+ headers["HTTP_CONTENT_TYPE"].should be_nil
663
+ headers["CONTENT_LENGTH"].should == "10"
664
+ headers["CONTENT_TYPE"].should == "text/plain"
665
+ end
666
+
667
+ @client.write("POST /foo/bar HTTP/1.1\r\n")
668
+ @client.write("Host: foo.com\r\n")
669
+ @client.write("X-Foo-Bar: baz\r\n")
670
+ @client.write("Content-Length: 10\r\n")
671
+ @client.write("Content-Type: text/plain\r\n")
672
+ @client.write("\r\n")
673
+ @client.close_write
674
+ @client.read
675
+ end
676
+
677
+ it "correctly parses HTTP requests with query string" do
678
+ @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
679
+ headers["REQUEST_METHOD"].should == "POST"
680
+ headers["SERVER_PROTOCOL"].should == "HTTP/1.1"
681
+ headers["HTTP_HOST"].should == "foo.com"
682
+ headers["HTTP_X_FOO_BAR"].should == "baz"
683
+ headers["PATH_INFO"].should == "/foo/bar"
684
+ headers["SCRIPT_NAME"].should == ""
685
+ headers["QUERY_STRING"].should == "hello=world&a=b+c"
686
+ headers["REQUEST_URI"].should == "/foo/bar?hello=world&a=b+c"
687
+ headers["HTTP_CONTENT_LENGTH"].should be_nil
688
+ headers["HTTP_CONTENT_TYPE"].should be_nil
689
+ headers["CONTENT_LENGTH"].should == "10"
690
+ headers["CONTENT_TYPE"].should == "text/plain"
691
+ end
692
+
693
+ @client.write("POST /foo/bar?hello=world&a=b+c HTTP/1.1\r\n")
694
+ @client.write("Host: foo.com\r\n")
695
+ @client.write("X-Foo-Bar: baz\r\n")
696
+ @client.write("Content-Length: 10\r\n")
697
+ @client.write("Content-Type: text/plain\r\n")
698
+ @client.write("\r\n")
699
+ @client.close_write
700
+ @client.read
701
+ end
702
+
703
+ it "correct parses HTTP requests that come in arbitrary chunks" do
704
+ @thread_handler.any_instance.should_receive(:process_request).and_return do |headers, connection, full_http_response|
705
+ headers["REQUEST_METHOD"].should == "POST"
706
+ headers["SERVER_PROTOCOL"].should == "HTTP/1.1"
707
+ headers["HTTP_HOST"].should == "foo.com"
708
+ headers["HTTP_X_FOO_BAR"].should == "baz"
709
+ headers["PATH_INFO"].should == "/foo/bar"
710
+ headers["SCRIPT_NAME"].should == ""
711
+ headers["QUERY_STRING"].should == "hello=world&a=b+c"
712
+ headers["REQUEST_URI"].should == "/foo/bar?hello=world&a=b+c"
713
+ headers["HTTP_CONTENT_LENGTH"].should be_nil
714
+ headers["HTTP_CONTENT_TYPE"].should be_nil
715
+ headers["CONTENT_LENGTH"].should == "10"
716
+ headers["CONTENT_TYPE"].should == "text/plain"
717
+ headers["HTTP_PLUS_SOME"].should be_nil
718
+ end
719
+
720
+ @client.write("POST /fo")
721
+ sleep 0.001
722
+ @client.write("o/bar?hello=world&a=b+c HT")
723
+ sleep 0.001
724
+ @client.write("TP/1.1\r")
725
+ sleep 0.001
726
+ @client.write("\nHost: foo.com")
727
+ sleep 0.001
728
+ @client.write("\r\n")
729
+ sleep 0.001
730
+ @client.write("X-Foo-Bar: baz\r\n")
731
+ sleep 0.001
732
+ @client.write("Content-Len")
733
+ sleep 0.001
734
+ @client.write("gth: 10\r\nContent-Type: text/pla")
735
+ sleep 0.001
736
+ @client.write("in\r\n\r")
737
+ sleep 0.001
738
+ @client.write("\nPlus-Some: garbage data that should be ignored.")
739
+ @client.close_write
740
+ @client.read
741
+ end
742
+ end
743
+
744
+ ############################
745
745
  end
746
746
 
747
747
  end # module PhusionPassenger