PerlのCGIで、requireがInternal Server Error

PerlのCGIで、Internal Server Errorが発生するのを調査した際のメモ。

#環境
##ソフトウェア
- CentOS release 6.9 (Final)
    - kernel-2.6.32-696.30.1.el6.x86_64
- Apache/2.2.15
    - httpd-2.2.15-60.el6.centos.6.x86_64
    - mod_perl-2.0.4-11.el6_5.x86_64
- perl-5.10.1-144.el6.x86_64

##関連ファイル
- 設定ファイル
    - /etc/httpd/conf/httpd.conf
    - /etc/httpd/conf.d/perl.conf
- ログファイル
    - /var/log/httpd/error_log
- テスト用CGI
    - /var/www/perl/hello.cgi
    - /var/www/perl/hello_sub.cgi

#現象
使用したPerlのCGIは以下の通り。
同じディレクトリにあるhello_sub.cgiをrequireしている。

```perl:/var/www/perl/hello.cgi
#!/usr/bin/perl
require 'hello_sub.cgi';
print "Content-type: text/html\n\n";
print "<html><head><title>$title</title></head><body>$body</body></html>\n";
```

```perl:/var/www/perl/hello_sub.cgi
#!/usr/bin/perl
$title="hello cgi";
$body="<h1>test require</h1>hello perl";
1;
```

shell上では、問題なく実行できることを確認。

```
[perl]$ ./hello.cgi
Content-type: text/html

<html><head><title>hello cgi</title></head><body><h1>test require</h1>hello perl</body></html>
```

しかしながら、`http://servername/perl/hello.cgi` にアクセスすると、Internal Server Errorとなる。
そのときのエラーログは以下の通り。`@INC`にはカレントディレクトリ(`.`)が含まれている。

```log:/var/log/httpd/error_log
[WWW MMM ddhh:mm:ss yyyy] [error] Can't locate hello_sub.cgi in @INC (@INC contains:
 /usr/local/lib64/perl5
 /usr/local/share/perl5
 /usr/lib64/perl5/vendor_perl
 /usr/share/perl5/vendor_perl
 /usr/lib64/perl5 /usr/share/perl5
 .
 /etc/httpd) at /var/www/perl/hello.cgi line 2.\n
```
ちなみに、hello_sub.cgi をカレントディレクトリ(`.`)以外の`@INC`にある場所（例えば、`/usr/share/perl5/vendor_perl/hello_sub.cgi` とか）に配置すると、CGIは正常に実行できる（Internal Server Errorにはならない）。

#対処
##Internal Server Errorが発生していたときの設定ファイル

```conf:/etc/httpd/conf.d/perl.conf【設定A1】
Alias /perl /var/www/perl
<Directory /var/www/perl>
    SetHandler perl-script
    PerlResponseHandler ModPerl::Registry
    PerlOptions +ParseHeaders
    Options +ExecCGI
</Directory>
```

##解決策
以下のように設定ファイルを修正したところ、同じディレクトリのファイルをrequireしてもInternal Server Errorにはならず、CGIが正常に実行できるようになった。

```conf:/etc/httpd/conf.d/perl.conf【設定B1】
Alias /perl /var/www/perl
<Directory /var/www/perl>
#    SetHandler perl-script
#    PerlResponseHandler ModPerl::Registry
#    PerlOptions +ParseHeaders
    Options +ExecCGI
</Directory>
```

```conf:/etc/httpd/conf/httpd.conf【設定B2】
#
# AddHandler allows you to map certain file extensions to "handlers":
# actions unrelated to filetype. These can be either built into the server
# or added with the Action directive (see below)
#
# To use CGI scripts outside of ScriptAliased directories:
# (You will also need to add "ExecCGI" to the "Options" directive.)
#
AddHandler cgi-script .cgi
```

#【追記】
おいおい原因も調べなければと考えていた矢先、詳細な原因解説をコメントしていただきました。
xtetsuji氏に感謝です。
なので、原因についてはコメントを参照ください。
ここでは、付随する検証結果を追記します。

なお、今回、perlの実行環境が必要になったのは、「とある古いPerlのWebアプリケーションをPHPに移植する」為でした。したがって、その参照環境を構築するにあたり、「元のPerlのソースコードは変更しない」という要件がありました。

##解決策（その２）
以下の設定でも、同じディレクトリのファイルをrequireできることを確認。
mod_perlが有効になるので、性能を考慮するとこちらを推奨。

```conf:/etc/httpd/conf.d/perl.conf【設定C1】
Alias /perl /var/www/perl
<Directory /var/www/perl>
    SetHandler perl-script
#   PerlResponseHandler ModPerl::Registry
    PerlResponseHandler ModPerl::RegistryPrefork
    PerlOptions +ParseHeaders
    Options +ExecCGI
</Directory>
```
##検証

```conf:/etc/httpd/conf/httpd.conf
#
# ServerRoot: The top of the directory tree under which the server's
# configuration, error, and log files are kept.
#
# NOTE!  If you intend to place this on an NFS (or otherwise network)
# mounted filesystem then please read the LockFile documentation
# (available at <URL:http://httpd.apache.org/docs/2.2/mod/mpm_common.html#lockfile>);
# you will save yourself a lot of trouble.
#
# Do NOT add a slash at the end of the directory path.
#
ServerRoot "/etc/httpd"
...
#
# DocumentRoot: The directory out of which you will serve your
# documents. By default, all requests are taken from this directory, but
# symbolic links and aliases may be used to point to other locations.
#
DocumentRoot "/var/www/html"
```
実行時のカレントディレクトリ、環境変数を確認するCGI。

```perl:/var/www/perl/env.cgi
#!/usr/bin/perl
use Cwd qw(getcwd);
my $cwd = getcwd();
print "Content-type: text/html\n\n";
print "<html><head><title>$title</title></head><body>$body\n";
print "<p>Current Working Directory is \"$cwd\"</p>\n";
print "<p>ENV{'MOD_PERL'}:$ENV{'MOD_PERL'}</p>\n";
print "<p>ENV{'DOCUMENT_ROOT'}:$ENV{'DOCUMENT_ROOT'}</p>\n";
print "<p>ENV{'SCRIPT_NAME'}:$ENV{'SCRIPT_NAME'}</p>\n";
print "<p>ENV{'SCRIPT_FILENAME'}:$ENV{'SCRIPT_FILENAME'}</p>\n";
print "</body></html>\n";
```

各設定毎の実行結果は以下の通り。
同じディレクトリのファイルをrequireできる設定B、設定Cでは、カレントディレクトリがスクリプトのあるディレクトリになっている。

||設定A|設定B|設定C|
|:--|:--|:--|:--|
|getcwd()|/|/var/www/perl|/var/www/perl|
|ENV{'MOD_PERL'}|mod_perl/2.0.4|(undef)|mod_perl/2.0.4|

いずれの設定でも、以下の環境変数の値は同じ。

|環境変数|値|
|:--|:--|
|ENV{'DOCUMENT_ROOT'}|/var/www/html|
|ENV{'SCRIPT_NAME'}|/perl/env.cgi|
|ENV{'SCRIPT_FILENAME'}|/var/www/perl/env.cgi|

##おまけ
設定Aの環境で、”スクリプトの置き場所に依らず”同じディレクトリのファイルをrequireする実装例。

requireするパスを動的に変更。

```perl:/var/www/perl/hello2.cgi
#!/usr/bin/perl
use File::Basename;
use File::Spec;
$dir = dirname( $ENV{'SCRIPT_FILENAME'} );
$path = File::Spec->catfile( $dir, 'hello_sub.cgi' );
require $path;
print "Content-type: text/html\n\n";
print "<html><head><title>$title</title></head><body>$body<p>dirname: $dir<p><p>require: $path<p></body></html>\n";
```

use libするディレクトリを動的に変更。

```perl:/var/www/perl/hello3.cgi
#!/usr/bin/perl
use File::Basename;
$dir = dirname($ENV{'SCRIPT_FILENAME'});
eval "use lib $dir";
#use lib '/var/www/perl';
require 'hello_sub.cgi';
print "Content-type: text/html\n\n";
print "<html><head><title>$title</title></head><body>$body<p>dirname: $dir<p></body></html>\n";
```