Permalink
Browse files

Switch to PHP's built-in server.

While PHP's built-in web server is not really meant for production use, neither
is RackLegacy. But it should offer a performance boost because it keeps the
PHP process running between requests. This means:

* No PHP startup time.
* PHP can cache compiled code.
* PHP can cache database connections between requests.

In addition to a probable performance boost is massively simplifies the code.
We use two new gems to make the interaction simple and PHP ensures we are
responding just as PHP normally would.

This change might break code using Rack::Legacy for the following reasons.

* If you are specifying the php binary you will need to switch from the
  php-cgi binary to the php CLI binary.
* I am no longer processing directory requests. I think that job is better
  reserved for a new middleware (my next task). The current method was
  inflexible because it didn't support index.html or index.cgi. Even if we
  added that support there would not be a way to specify your preference.
* If you are using the default port we picked you will need further config.
* There are some areas where we were actually not mimicing PHP correctly.
  For example we were processing HTTP status's incorrectly and we were
  returning a 500 error when exiting with a non-zero status (like CGI).
  Now that PHP is doing the work these deviations from how PHP worked have
  been removed.
  • Loading branch information...
1 parent bcb31d4 commit 14ae71bad2dfca52b365c90de042176a238d2340 @eric1234 committed Nov 10, 2013
View
@@ -33,7 +33,7 @@ end
END {
if $server
puts 'Shutting down test server...'
- Process.kill 'KILL', $server
+ Process.kill 'TERM', $server
end
}
View
@@ -1,23 +1,22 @@
-require 'fileutils'
require 'shellwords'
class Rack::Legacy::Cgi
- attr_reader :public_dir
- # Will setup a new instance of the Cgi middleware executing
- # programs located in the given public_dir
- #
- # use Rack::Legacy::Cgi, 'cgi-bin'
- def initialize(app, public_dir=FileUtils.pwd)
+ # Will setup a new instance of the CGI middleware executing
+ # programs located in the given `public_dir`
+ def initialize app, public_dir=Dir.getwd
@app = app
@public_dir = public_dir
end
# Middleware, so if it looks like we can run it then do so.
# Otherwise send it on for someone else to handle.
- def call(env)
- if valid? env['PATH_INFO']
- run env, full_path(env['PATH_INFO'])
+ def call env
+ path = env['PATH_INFO']
+ path = path[1..-1] if path =~ /\//
+ path = ::File.expand_path path, @public_dir
+ if valid? path
+ run env, path
else
@app.call env
end
@@ -26,22 +25,15 @@ def call(env)
# Check to ensure the path exists and it is a child of the
# public directory.
def valid?(path)
- fp = full_path path
- fp.start_with?(::File.expand_path public_dir) &&
- ::File.file?(fp) && ::File.executable?(fp)
+ path.start_with?(::File.expand_path @public_dir) &&
+ ::File.file?(path) && ::File.executable?(path)
end
- protected
-
- # Returns the path with the public_dir pre-pended and with the
- # paths expanded (so we can check for security issues)
- def full_path(path)
- ::File.expand_path ::File.join(public_dir, path)
- end
+ private
# Will run the given path with the given environment
- def run(env, *path)
- env['DOCUMENT_ROOT'] = public_dir
+ def run env, path
+ env['DOCUMENT_ROOT'] = @public_dir
env['SERVER_SOFTWARE'] = 'Rack Legacy'
status = 200
headers = {}
@@ -55,7 +47,7 @@ def run(env, *path)
ENV[key] = value if
value.respond_to?(:to_str) && key =~ /^[A-Z_]+$/
end
- exec *path
+ exec path
else # Parent
# Send request to CGI sub-process
io.write(env['rack.input'].read) if env['rack.input']
@@ -82,7 +74,7 @@ def run(env, *path)
unless $?.exitstatus == 0
# Build full command for easier debugging. Output to
# stderr to prevent user from getting too much information.
- cmd = env.inject(path) do |assignments, (key, value)|
+ cmd = env.inject([path]) do |assignments, (key, value)|
assignments.unshift "#{key}=#{value.to_s.shellescape}" if
value.respond_to?(:to_str) && key =~ /^[A-Z_]+$/
assignments
View
@@ -1,76 +1,54 @@
require 'rack/legacy'
-require 'rack/legacy/cgi'
+require 'rack/request'
+require 'rack/reverse_proxy'
+require 'childprocess'
-class Rack::Legacy::Php < Rack::Legacy::Cgi
+class Rack::Legacy::Php
- # Like Rack::Legacy::Cgi.new except allows an additional argument
- # of which executable to use to run the PHP code.
+ # Proxies off requests to PHP files to the built-in PHP webserver.
#
- # use Rack::Legacy::Php, 'public', 'php5-cgi'
- def initialize(app, public_dir=FileUtils.pwd, php_exe='php-cgi')
- super app, public_dir
- @php_exe = php_exe
+ # public_dir::
+ # Location of PHP files. Default to current working directory.
+ # php_exe::
+ # Location of `php` exec. Will process through shell so is
+ # generally not needed since it is in the path.
+ # port::
+ # Requests are proxied off to the built-in PHP webserver. It
+ # will run on the given port. If you are already using that port
+ # for something else you may need to change this option.
+ # quiet::
+ # By default the PHP server inherits the parent process IO. Set
+ # this to true to hide the PHP server output
+ #
+ def initialize app, public_dir=Dir.getwd, php_exe='php', port=8180, quiet=false
+ @app = app; @public_dir = public_dir
+ @proxy = Rack::ReverseProxy.new {reverse_proxy /^.*$/, "http://localhost:#{port}"}
+ @php = ChildProcess.build php_exe,
+ '-S', "localhost:#{port}", '-t', public_dir
+ @php.io.inherit! unless quiet
+ @php.start
+ at_exit {@php.stop if @php.alive?}
end
- # Override to check for php extension. Still checks if
- # file is in public path and it is a file like superclass.
- def valid?(path)
- sp = path_parts(full_path path)[0]
-
- # Must have a php extension or be a directory
- return false unless
- (::File.file?(sp) && sp =~ /\.php$/) ||
- ::File.directory?(sp)
-
- # Must be in public directory for security
- sp.start_with? ::File.expand_path(@public_dir)
- end
-
- # Monkeys with the arguments so that it actually runs PHP's cgi
- # program with the path as an argument to that program.
- def run(env, path)
- script, info = *path_parts(path)
- if ::File.directory? script
- # If directory then assume index.php
- script = ::File.join script, 'index.php';
- # Ensure it ends in / which some PHP scripts depend on
- path = "#{path}/" unless path =~ /\/$/
+ # If it looks like it is one of ours proxy off to PHP server.
+ # Otherwise send down the stack.
+ def call env
+ if valid? env['PATH_INFO']
+ @php.start unless @php.alive?
+ @proxy.call env
+ else
+ @app.call env
end
- env['SCRIPT_FILENAME'] = script
- env['SCRIPT_NAME'] = strip_public script
- env['PATH_INFO'] = info
- env['REQUEST_URI'] = strip_public path
- env['REQUEST_URI'] += '?' + env['QUERY_STRING'] if
- env.has_key?('QUERY_STRING') && !env['QUERY_STRING'].empty?
- super env, @php_exe, "-d cgi.force_redirect=0"
- end
-
- private
-
- def strip_public(path)
- path.sub ::File.expand_path(public_dir), ''
end
- # Given a full path will separate the script part from the
- # path_info part. Returns an array. The first element is the
- # script. The second element is the path info.
- def path_parts(path)
- return [path, nil] unless path =~ /.php/
- script, info = *path.split('.php', 2)
- script += '.php'
- [script, info]
- end
+ # Make sure it points to a valid PHP file. No need to ensure it
+ # is in the public directory since PHP will do that for us.
+ def valid? path
+ return false unless path =~ /\.php/
- # Given a full path will extract just the info part. So
- #
- # /index.php/foo/bar
- #
- # will return /foo/bar, but
- #
- # /index.php
- #
- # will return an empty string.
- def info_path(path)
- path.split('.php', 2)[1].to_s
+ path = path[1..-1] if path =~ /^\//
+ path = path.split('.php', 2)[0] + '.php'
+ path = ::File.expand_path path, @public_dir
+ ::File.file? path
end
end
View
@@ -1,12 +1,15 @@
Gem::Specification.new do |s|
s.name = 'rack-legacy'
- s.version = '0.6.0'
+ s.version = '0.7.0'
s.homepage = 'https://github.com/eric1234/rack-legacy'
s.author = 'Eric Anderson'
s.email = 'eric@pixelwareinc.com'
s.licenses = ['Public Domain']
s.executables << 'rack_legacy'
s.add_dependency 'rack'
+ s.add_dependency 'childprocess'
+ s.add_dependency 'rack-reverse-proxy'
+ s.add_development_dependency 'pry-byebug'
s.add_development_dependency 'rake'
s.add_development_dependency 'httparty'
s.add_development_dependency 'flexmock'
View
@@ -2,6 +2,6 @@ require 'rack/showexceptions'
require 'rack-legacy'
use Rack::ShowExceptions
-use Rack::Legacy::Php, Dir.getwd
-use Rack::Legacy::Cgi, Dir.getwd
+use Rack::Legacy::Php
+use Rack::Legacy::Cgi
run Rack::File.new Dir.getwd
View
@@ -1,3 +0,0 @@
-php_value foo bar
-php_value baz boo
-php_flag output_buffering on
View
@@ -1 +1 @@
-<?php header('Status: 404 Not Found'); ?>
+<?php http_response_code(404); ?>
@@ -1,4 +0,0 @@
-php_value include_path backend:ext:.
-php_value auto_prepend_file backend/lib/setup.php
-php_value auto_append_file backend/lib/teardown.php
-php_flag output_buffering off
@@ -1 +0,0 @@
-<?php echo 'default directory' ?>
View
@@ -1 +0,0 @@
-<?php exit(1) ?>
View
@@ -1 +0,0 @@
-<?php echo "default" ?>
View
@@ -1 +0,0 @@
-<?php echo ini_get('output_buffering') ?>
@@ -15,37 +15,10 @@ def test_success
assert_match /^PHP/, response.header['x-powered-by']
end
- def test_default
- response = Mechanize.new.get 'http://localhost:4000/'
- assert_equal 'default', response.body
- assert_equal '200', response.code
- assert_equal 'text/html', response.header['content-type']
- assert_match /^PHP/, response.header['x-powered-by']
- end
-
- def test_default_directory
- response = Mechanize.new.get 'http://localhost:4000/dir1'
- assert_equal 'default directory', response.body
- assert_equal '200', response.code
- assert_equal 'text/html', response.header['content-type']
- assert_match /^PHP/, response.header['x-powered-by']
- end
-
- def test_error
- begin
- Mechanize.new.get 'http://localhost:4000/error.php'
- rescue Mechanize::ResponseCodeError
- assert_match 'Rack::Legacy::ExecutionError', $!.page.body
- assert_equal '500', $!.page.code
- assert_equal 'text/html', $!.page.header['content-type']
- end
- end
-
def test_syntax_error
begin
Mechanize.new.get 'http://localhost:4000/syntax_error.php'
rescue Mechanize::ResponseCodeError
- assert_match 'Rack::Legacy::ExecutionError', $!.page.body
assert_equal '500', $!.page.code
assert_equal 'text/html', $!.page.header['content-type']
end
View
@@ -18,7 +18,7 @@ class ::WEBrick::BasicLog; def log(level, data); end end
app = Rack::Builder.app do
use Rack::ShowExceptions
- use Rack::Legacy::Php, File.join(File.dirname(__FILE__), 'fixtures')
+ use Rack::Legacy::Php, File.join(File.dirname(__FILE__), 'fixtures'), 'php', 8180, true
use Rack::Legacy::Cgi, File.join(File.dirname(__FILE__), 'fixtures')
run lambda { |env| [200, {'Content-Type' => 'text/html'}, ['Endpoint']] }
View
@@ -6,10 +6,11 @@
class CgiTest < Test::Unit::TestCase
def test_valid?
- assert app.valid?('success.cgi') # Valid file
- assert !app.valid?('../unit/cgi_test.rb') # Valid file but outside public
- assert !app.valid?('missing.cgi') # File not found
- assert !app.valid?('./') # Directory
+ assert app.valid?(fixture_file('success.cgi')) # Valid file
+ assert !app.valid?(fixture_file('success.php')) # Valid file but not executable
+ assert !app.valid?(fixture_file('../unit/cgi_test.rb')) # Valid file but outside public
+ assert !app.valid?(fixture_file('missing.cgi')) # File not found
+ assert !app.valid?(fixture_file('./')) # Directory
end
def test_call
@@ -69,6 +70,10 @@ def test_environment
private
+ def fixture_file path
+ File.expand_path path, File.join(File.dirname(__FILE__), '../fixtures')
+ end
+
def app
Rack::Legacy::Cgi.new \
proc {[200, {'Content-Type' => 'text/html'}, 'Endpoint']},
Oops, something went wrong.

0 comments on commit 14ae71b

Please sign in to comment.