Skip to content
This repository was archived by the owner on Dec 2, 2018. It is now read-only.

Commit 14ae71b

Browse files
committed
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.
1 parent bcb31d4 commit 14ae71b

File tree

16 files changed

+104
-184
lines changed

16 files changed

+104
-184
lines changed

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ end
3333
END {
3434
if $server
3535
puts 'Shutting down test server...'
36-
Process.kill 'KILL', $server
36+
Process.kill 'TERM', $server
3737
end
3838
}
3939

lib/rack/legacy/cgi.rb

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
require 'fileutils'
21
require 'shellwords'
32

43
class Rack::Legacy::Cgi
5-
attr_reader :public_dir
64

7-
# Will setup a new instance of the Cgi middleware executing
8-
# programs located in the given public_dir
9-
#
10-
# use Rack::Legacy::Cgi, 'cgi-bin'
11-
def initialize(app, public_dir=FileUtils.pwd)
5+
# Will setup a new instance of the CGI middleware executing
6+
# programs located in the given `public_dir`
7+
def initialize app, public_dir=Dir.getwd
128
@app = app
139
@public_dir = public_dir
1410
end
1511

1612
# Middleware, so if it looks like we can run it then do so.
1713
# Otherwise send it on for someone else to handle.
18-
def call(env)
19-
if valid? env['PATH_INFO']
20-
run env, full_path(env['PATH_INFO'])
14+
def call env
15+
path = env['PATH_INFO']
16+
path = path[1..-1] if path =~ /\//
17+
path = ::File.expand_path path, @public_dir
18+
if valid? path
19+
run env, path
2120
else
2221
@app.call env
2322
end
@@ -26,22 +25,15 @@ def call(env)
2625
# Check to ensure the path exists and it is a child of the
2726
# public directory.
2827
def valid?(path)
29-
fp = full_path path
30-
fp.start_with?(::File.expand_path public_dir) &&
31-
::File.file?(fp) && ::File.executable?(fp)
28+
path.start_with?(::File.expand_path @public_dir) &&
29+
::File.file?(path) && ::File.executable?(path)
3230
end
3331

34-
protected
35-
36-
# Returns the path with the public_dir pre-pended and with the
37-
# paths expanded (so we can check for security issues)
38-
def full_path(path)
39-
::File.expand_path ::File.join(public_dir, path)
40-
end
32+
private
4133

4234
# Will run the given path with the given environment
43-
def run(env, *path)
44-
env['DOCUMENT_ROOT'] = public_dir
35+
def run env, path
36+
env['DOCUMENT_ROOT'] = @public_dir
4537
env['SERVER_SOFTWARE'] = 'Rack Legacy'
4638
status = 200
4739
headers = {}
@@ -55,7 +47,7 @@ def run(env, *path)
5547
ENV[key] = value if
5648
value.respond_to?(:to_str) && key =~ /^[A-Z_]+$/
5749
end
58-
exec *path
50+
exec path
5951
else # Parent
6052
# Send request to CGI sub-process
6153
io.write(env['rack.input'].read) if env['rack.input']
@@ -82,7 +74,7 @@ def run(env, *path)
8274
unless $?.exitstatus == 0
8375
# Build full command for easier debugging. Output to
8476
# stderr to prevent user from getting too much information.
85-
cmd = env.inject(path) do |assignments, (key, value)|
77+
cmd = env.inject([path]) do |assignments, (key, value)|
8678
assignments.unshift "#{key}=#{value.to_s.shellescape}" if
8779
value.respond_to?(:to_str) && key =~ /^[A-Z_]+$/
8880
assignments

lib/rack/legacy/php.rb

Lines changed: 42 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,54 @@
11
require 'rack/legacy'
2-
require 'rack/legacy/cgi'
2+
require 'rack/request'
3+
require 'rack/reverse_proxy'
4+
require 'childprocess'
35

4-
class Rack::Legacy::Php < Rack::Legacy::Cgi
6+
class Rack::Legacy::Php
57

6-
# Like Rack::Legacy::Cgi.new except allows an additional argument
7-
# of which executable to use to run the PHP code.
8+
# Proxies off requests to PHP files to the built-in PHP webserver.
89
#
9-
# use Rack::Legacy::Php, 'public', 'php5-cgi'
10-
def initialize(app, public_dir=FileUtils.pwd, php_exe='php-cgi')
11-
super app, public_dir
12-
@php_exe = php_exe
10+
# public_dir::
11+
# Location of PHP files. Default to current working directory.
12+
# php_exe::
13+
# Location of `php` exec. Will process through shell so is
14+
# generally not needed since it is in the path.
15+
# port::
16+
# Requests are proxied off to the built-in PHP webserver. It
17+
# will run on the given port. If you are already using that port
18+
# for something else you may need to change this option.
19+
# quiet::
20+
# By default the PHP server inherits the parent process IO. Set
21+
# this to true to hide the PHP server output
22+
#
23+
def initialize app, public_dir=Dir.getwd, php_exe='php', port=8180, quiet=false
24+
@app = app; @public_dir = public_dir
25+
@proxy = Rack::ReverseProxy.new {reverse_proxy /^.*$/, "http://localhost:#{port}"}
26+
@php = ChildProcess.build php_exe,
27+
'-S', "localhost:#{port}", '-t', public_dir
28+
@php.io.inherit! unless quiet
29+
@php.start
30+
at_exit {@php.stop if @php.alive?}
1331
end
1432

15-
# Override to check for php extension. Still checks if
16-
# file is in public path and it is a file like superclass.
17-
def valid?(path)
18-
sp = path_parts(full_path path)[0]
19-
20-
# Must have a php extension or be a directory
21-
return false unless
22-
(::File.file?(sp) && sp =~ /\.php$/) ||
23-
::File.directory?(sp)
24-
25-
# Must be in public directory for security
26-
sp.start_with? ::File.expand_path(@public_dir)
27-
end
28-
29-
# Monkeys with the arguments so that it actually runs PHP's cgi
30-
# program with the path as an argument to that program.
31-
def run(env, path)
32-
script, info = *path_parts(path)
33-
if ::File.directory? script
34-
# If directory then assume index.php
35-
script = ::File.join script, 'index.php';
36-
# Ensure it ends in / which some PHP scripts depend on
37-
path = "#{path}/" unless path =~ /\/$/
33+
# If it looks like it is one of ours proxy off to PHP server.
34+
# Otherwise send down the stack.
35+
def call env
36+
if valid? env['PATH_INFO']
37+
@php.start unless @php.alive?
38+
@proxy.call env
39+
else
40+
@app.call env
3841
end
39-
env['SCRIPT_FILENAME'] = script
40-
env['SCRIPT_NAME'] = strip_public script
41-
env['PATH_INFO'] = info
42-
env['REQUEST_URI'] = strip_public path
43-
env['REQUEST_URI'] += '?' + env['QUERY_STRING'] if
44-
env.has_key?('QUERY_STRING') && !env['QUERY_STRING'].empty?
45-
super env, @php_exe, "-d cgi.force_redirect=0"
46-
end
47-
48-
private
49-
50-
def strip_public(path)
51-
path.sub ::File.expand_path(public_dir), ''
5242
end
5343

54-
# Given a full path will separate the script part from the
55-
# path_info part. Returns an array. The first element is the
56-
# script. The second element is the path info.
57-
def path_parts(path)
58-
return [path, nil] unless path =~ /.php/
59-
script, info = *path.split('.php', 2)
60-
script += '.php'
61-
[script, info]
62-
end
44+
# Make sure it points to a valid PHP file. No need to ensure it
45+
# is in the public directory since PHP will do that for us.
46+
def valid? path
47+
return false unless path =~ /\.php/
6348

64-
# Given a full path will extract just the info part. So
65-
#
66-
# /index.php/foo/bar
67-
#
68-
# will return /foo/bar, but
69-
#
70-
# /index.php
71-
#
72-
# will return an empty string.
73-
def info_path(path)
74-
path.split('.php', 2)[1].to_s
49+
path = path[1..-1] if path =~ /^\//
50+
path = path.split('.php', 2)[0] + '.php'
51+
path = ::File.expand_path path, @public_dir
52+
::File.file? path
7553
end
7654
end

rack-legacy.gemspec

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
Gem::Specification.new do |s|
22
s.name = 'rack-legacy'
3-
s.version = '0.6.0'
3+
s.version = '0.7.0'
44
s.homepage = 'https://github.com/eric1234/rack-legacy'
55
s.author = 'Eric Anderson'
66
s.email = '[email protected]'
77
s.licenses = ['Public Domain']
88
s.executables << 'rack_legacy'
99
s.add_dependency 'rack'
10+
s.add_dependency 'childprocess'
11+
s.add_dependency 'rack-reverse-proxy'
12+
s.add_development_dependency 'pry-byebug'
1013
s.add_development_dependency 'rake'
1114
s.add_development_dependency 'httparty'
1215
s.add_development_dependency 'flexmock'

share/rack_legacy.ru

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ require 'rack/showexceptions'
22
require 'rack-legacy'
33

44
use Rack::ShowExceptions
5-
use Rack::Legacy::Php, Dir.getwd
6-
use Rack::Legacy::Cgi, Dir.getwd
5+
use Rack::Legacy::Php
6+
use Rack::Legacy::Cgi
77
run Rack::File.new Dir.getwd

test/fixtures/.htaccess

Lines changed: 0 additions & 3 deletions
This file was deleted.

test/fixtures/404.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php header('Status: 404 Not Found'); ?>
1+
<?php http_response_code(404); ?>

test/fixtures/dir1/dir2/.htaccess

Lines changed: 0 additions & 4 deletions
This file was deleted.

test/fixtures/dir1/index.php

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/fixtures/error.php

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)