ラッパーCGIを作る方法
投稿日:
既存CGIを一切触らずに、前段にアクセス制御やアクセスロガーをつけたいとかの用途でラッパーCGIを使うと上手くいくので、その方法を書く。
なお前段が動くことしか試していないが、備考に後段で処理をさせる方法についても軽く触れている。
動作機序
CGIはコマンドライン引数、環境変数、標準入力を受け取り、何かを処理した結果を標準出力するプログラムである。
つまりラッパーCGIはコマンドライン引数、環境変数、標準入力を受け取り、それをラッピングするCGIにそのまま受け渡し、このCGIの標準出力をリダイレクトできればよい。
やり方
以下のようなコードを書き、exec()前に前段の処理を書けばよい。
#!/usr/bin/perl
use strict;
use warnings;
#
# ここに実行前に挟みたい処理
#
my $original_cgi = './hoge.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";
今回実際に作ったサンプル
adiaryにはアクセス制限をする機能がなく、本体を弄るのが嫌だったので前段に処理を入れることで実現した。
#!/usr/bin/perl
use strict;
use warnings;
# 以下のコマンドでIP::Geolocation::MMDBをインストールしていることが前提
# cpanm -l extlib IP::Geolocation::MMDB
use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;
# https://download.db-ip.com/free/dbip-city-lite-YYYY-MM.mmdb.gz
# ex. https://download.db-ip.com/free/dbip-city-lite-2026-01.mmdb.gz
my $db = IP::Geolocation::MMDB->new(file => './DBIP-City.mmdb');
my $country_code = $db->getcc($ENV{REMOTE_ADDR});
# 日台韓は許可する方針(怪しい挙動を見たことがないため)
my @allow_country_codes = ('JP', 'TW', 'KR');
my $user_agent = $ENV{HTTP_USER_AGENT};
# 許可する国コードかチェック
my $is_allowed_country = grep { $_ eq $country_code } @allow_country_codes;
# 許可するBOTのUAパターン
my @allowed_bot_patterns = (
qr/bot/i,
qr/curl/i,
qr/wget/i,
qr/google/i,
qr/bing/i,
qr/mastodon/i,
qr/misskey/i,
qr/pleroma/i,
qr/akkoma/i,
qr/lemmy/i,
qr/activitypub/i,
qr/hatena/i,
qr/github/i,
qr/tumblr/i,
qr/meta/i
);
# 許可するBOTかチェック
my $is_allowed_bot = 0;
for my $pattern (@allowed_bot_patterns) {
if ($user_agent =~ $pattern) {
$is_allowed_bot = 1;
last;
}
}
# 許可国でもなく、許可BOTでもなければエラーにする
if (!$is_allowed_country && !$is_allowed_bot) {
# 未知のSNS BOTを将来的に許可するために、BOTくさいUAのログを集めておく
if ($user_agent !~ /Windows|Mac OS|Linux|Android|iOS|iPhone|iPad/i) {
# OGP取得BOTに間違いなく含まれない文字列が入ってるものはログに入れない
my $deny_ua_log_file = './deny_ua.log';
if (open my $fh, '>>', $deny_ua_log_file) {
my $time = localtime();
my $remote = $ENV{REMOTE_ADDR} // 'unknown';
my $uri = $ENV{REQUEST_URI} // 'unknown';
print $fh "[$time]\t\"$user_agent\"\t$country_code\t$remote\t$uri\n";
close $fh;
}
}
print "Status: 403 Forbidden\n";
print "Content-Type: text/plain; charset=UTF-8\n\n";
print "Access denied.\n";
exit;
}
# adiary呼び出し
my $original_cgi = './adiary.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";
備考
perldocを読んだ感じ、互換性に問題が出る可能性も少なからずあるようだ。
perldocのexec関数の説明を見る感じ、ENDブロックや、オブジェクトのDESTROYメソッドを起動しないとあるので、実装方法次第では正しく動かない可能性もあるのかもしれない。
また「戻って欲しい場合には、execではなく system関数を使ってください」とあるため、もし後処理をしたい場合はexec関数でなくsystem関数を使うとよいと思う。
あとがき
レンタルサーバーではWAFが自由に使えないため、なんちゃってWAFの様なものを作りたいとか、レンタルサーバーを新規に始めたく、CGIにバナー広告を差し込みたいといったケースがある場合に、今回のような手法は便利だろう。
今時、往年のレンタルサーバーを新規に始め、それもバナー広告を出したいと考える人物がいるかどうかは謎だが、共通的に何かを差し込みたいなど、何かしら活用方法はあるかもしれない。