2026/07/13(月)令和にPerlでCGIを作る

更新日:
投稿日:

令和の時代にPerlでCGI…?という感じだが、PHP-FPMの上で動いてるLaravelとかもCGIの親戚みたいなものなので、そこまで違和感はないと思う。何せFPMはFastCGI Process Managerの略だからね。

でも令和の時代にPerl…?というのは、ちょっと面白いと思っている。

確認環境

Env Ver
Perl v5.38.2
fcgiwrap 1.1.0-14build1
nginx 1.24.0

実装方法

nginxの設定例

この設定ではドメイン直下のどこにアクセスされてもファイルがなければindex.cgiに飛ばしてCGIにルーティングを処理させるようにしている。

パスごとにCGIファイルを配置する方式ならtry_filesは不要だ。

server {
  listen [::]:443 ssl;
  server_name example.com;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  root /var/www;

  location / {
    index index.cgi;
    try_files $uri $uri/ /index.cgi;
  }

  location ~ ^.+\.cgi$ {
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
    fastcgi_index index.cgi;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param HTTPS on;
    fastcgi_param SERVER_PORT 443;
  }
}

最低限の実装

Hello worldを出すだけの奴。レスポンスヘッダを書かないとnginxではエラーになった。Apacheでは試していない。

コード
#!/bin/env perl

# HTTPメッセージに近い構造になっている
# レスポンスヘッダを書いている
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
# レスポンスボディ
print 'Hello world';

これをindex.cgiとかで置いてやって所有者をwww-data:www-dataにして実行権限を付与したら動く。

sudo chown www-data:www-data index.cgi
sudo chmod u+x index.cgi

実行時エラーを出したりする

ファイルの先頭に以下の二行を加えると、実行時エラーをサーバーログに吐き出すようになる。

use strict;
use warnings;
コード
#!/bin/env perl
use strict;
use warnings;

# HTTPメッセージに近い構造になっている
# レスポンスヘッダを書いている
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
# レスポンスボディ
print 'Hello world';

環境変数を出す

コード
#!/bin/env perl
use strict;
use warnings;

print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
print "QUERY_STRING => $ENV{QUERY_STRING}<br>";
print "REQUEST_METHOD => $ENV{REQUEST_METHOD}<br>";
print "CONTENT_TYPE => $ENV{CONTENT_TYPE}<br>";
print "CONTENT_LENGTH => $ENV{CONTENT_LENGTH}<br>";
print "SCRIPT_NAME => $ENV{SCRIPT_NAME}<br>";
print "REQUEST_URI => $ENV{REQUEST_URI}<br>";
print "DOCUMENT_URI => $ENV{DOCUMENT_URI}<br>";
print "DOCUMENT_ROOT => $ENV{DOCUMENT_ROOT}<br>";
print "SERVER_PROTOCOL => $ENV{SERVER_PROTOCOL}<br>";
print "REQUEST_SCHEME => $ENV{REQUEST_SCHEME}<br>";
print "HTTPS => $ENV{HTTPS}<br>";
print "GATEWAY_INTERFACE => $ENV{GATEWAY_INTERFACE}<br>";
print "SERVER_SOFTWARE => $ENV{SERVER_SOFTWARE}<br>";
print "REMOTE_ADDR => $ENV{REMOTE_ADDR}<br>";
print "REMOTE_PORT => $ENV{REMOTE_PORT}<br>";
print "REMOTE_USER => $ENV{REMOTE_USER}<br>";
print "SERVER_ADDR => $ENV{SERVER_ADDR}<br>";
print "SERVER_PORT => $ENV{SERVER_PORT}<br>";
print "SERVER_NAME => $ENV{SERVER_NAME}<br>";
実行結果

ライブラリを呼び出す

lib/Lyndbaum/Core.pmに次のライブラリを作成したとする。

パス名とpackage名の大文字小文字が一致してないと動かないので注意。

use strict;
use warnings;

package Lyndbaum::Core;

sub getEnv() {
    return (
        QUERY_STRING => $ENV{QUERY_STRING},
        REQUEST_METHOD => $ENV{REQUEST_METHOD},
        CONTENT_TYPE => $ENV{CONTENT_TYPE},
        CONTENT_LENGTH => $ENV{CONTENT_LENGTH},
        SCRIPT_NAME => $ENV{SCRIPT_NAME},
        REQUEST_URI => $ENV{REQUEST_URI},
        DOCUMENT_URI => $ENV{DOCUMENT_URI},
        DOCUMENT_ROOT => $ENV{DOCUMENT_ROOT},
        SERVER_PROTOCOL => $ENV{SERVER_PROTOCOL},
        REQUEST_SCHEME => $ENV{REQUEST_SCHEME},
        HTTPS => $ENV{HTTPS},
        GATEWAY_INTERFACE => $ENV{GATEWAY_INTERFACE},
        SERVER_SOFTWARE => $ENV{SERVER_SOFTWARE},
        REMOTE_ADDR => $ENV{REMOTE_ADDR},
        REMOTE_PORT => $ENV{REMOTE_PORT},
        REMOTE_USER => $ENV{REMOTE_USER},
        SERVER_ADDR => $ENV{SERVER_ADDR},
        SERVER_PORT => $ENV{SERVER_PORT},
        SERVER_NAME => $ENV{SERVER_NAME}
    );
}

1;
コード
#!/bin/env perl
use strict;
use warnings;

# @INCはライブラリをインクルードするパスを指定するための組み込み変数で
# unshiftは配列に要素を追加する関数
# つまりインクルードパスに./libを追加している
unshift(@INC, './lib');
# 読み込んだライブラリの使用
use Lyndbaum::Core;
# %で始まる変数は連想配列(ハッシュ)として機能する
my %e = Lyndbaum::Core::getEnv();

print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
# ハッシュは$変数名{キー名}で参照できる
print "REQUEST_SCHEME: $e{REQUEST_SCHEME}<br>";
print "HTTPS: $e{HTTPS}<br>";
print "HOST: $e{HOST}<br>";
print "SERVER_NAME: $e{SERVER_NAME}<br>";
print "REQUEST_URI: $e{REQUEST_URI}<br>";
print "DOCUMENT_URI: $e{DOCUMENT_URI}<br>";
print "SCRIPT_NAME: $e{SCRIPT_NAME}<br>";

unshift(@INC, './lib');use lib './lib';元の構文みたいなものらしい。adiaryの実装がこうだったので真似してみたが、バッドプラクティスの可能性がある。

ステータスコードなどを返す

コード
#!/bin/env perl
use strict;
use warnings;

unshift(@INC, './lib');

use Lyndbaum::Core;

my %e = Lyndbaum::Core::getEnv();

# Status: <コード>でレスポンスステータスコードを返せる
# 指定しないと200になった
print "Status: 200\r\n";
# 好きなサーバー名を書いて君だけのサーバーを名乗ろう!
print "Server: Lyndbaum\r\n";
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
# レスポンスボディ
print "REQUEST_SCHEME: $e{REQUEST_SCHEME}<br>";
print "HTTPS: $e{HTTPS}<br>";
print "HOST: $e{HOST}<br>";
print "SERVER_NAME: $e{SERVER_NAME}<br>";
print "REQUEST_URI: $e{REQUEST_URI}<br>";
print "DOCUMENT_URI: $e{DOCUMENT_URI}<br>";
print "SCRIPT_NAME: $e{SCRIPT_NAME}<br>";

ルーティングする

パスが取れるのでもう朝飯前。

コード
#!/bin/env perl
use strict;
use warnings;

unshift(@INC, './lib');

use Lyndbaum::Core;

my %e = Lyndbaum::Core::getEnv();

print "Status: 200\r\n";
print "Server: Lyndbaum\r\n";
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";

if ($e{REQUEST_URI} eq '/hoge') {
    # 文字列比較はeqなので注意、シェルスクリプトと同じ!!
    print "hoge!!";
} else {
    print "REQUEST_SCHEME: $e{REQUEST_SCHEME}<br>";
    print "HTTPS: $e{HTTPS}<br>";
    print "HOST: $e{HOST}<br>";
    print "SERVER_NAME: $e{SERVER_NAME}<br>";
    print "REQUEST_URI: $e{REQUEST_URI}<br>";
    print "DOCUMENT_URI: $e{DOCUMENT_URI}<br>";
    print "SCRIPT_NAME: $e{SCRIPT_NAME}<br>";
}

あとがき

何故Perlかというところだが、単にCGIとして実装したかったからだ。PHPやRuby, Pythonなどの選択肢もあると思うが、私はPythonが好きではなく、Rubyには興味がなく、最近のPHPもあまり好きではない。そこでPerlにすることにした。

PerlはPHPと比べたときに破壊的変更が少なく、枯れており、構文を覚えればワンライナーや簡単なスクリプトを書くのも便利だ。他にも構文が柔軟な部分も素晴らしい。

例えばPHPであれば以下のようにエスケープが必要なコードは

$hoge = '"/hoge/\'fuga\'/piyo/"';
if (preg_match("/(\/'fuga'\/)/", $hoge, $mat) === 1) {
    echo $mat[1];
}

Perlではこう書ける。

$hoge = qq("/hoge/'fuga'/piyo/");
if ($hoge =~ m|(/'fuga'/)|) {
    print $1;
}

qq()のqはquoteを指し、quoteが二つあるのでダブルクォートを指し示す。この演算子を使うことでPerlでは文字のエスケープを減らすことができ、書きやすさと可読性を保ちやすくなる。qq<>とかqq{}みたいに任意の記号を使うことができるので、文字列の中に(出たら終わりじゃんみたいなこともない。大変柔軟性がある。

$hoge =~ m|(/'fuga'/)|=~は正規表現の比較に使う演算子だが、ついでにキャプチャもしてくれる。更にm||のように書くことで正規表現内に/が含まれていてもエスケープ不要になる。qq同様に使う記号は好きに変えられる。

反面でPerl特有の演算子でもあるため、Perlが分からない人には暗号にしか見えないのは欠点と言えるだろう。

またPerlにはNode.jsの面倒さのようなものもない。例えばNode.jsならパッケージマネージャーをnpm, yarn, pnpmなどのどれにするかとか、実行環境をDeno, Bun, Node.jsなどのどれにするかとか、バージョン管理をVolta, nodenv, nvmなどのどれにするかとか、様々な争いがあると思うが、Perlにはそんなものはないと思う。

それに、このブログで利用しているadiaryはPerl CGIで実装されているし、lycolia.info配下のページも実は全部Perl CGIとして実装している[1]。つまり最近私の中でPerlブームが訪れているわけだ。ソフトウェア配布に置いてるのも現状Perl実装だしね。


  1. しかも恐ろしいことにルーティングなしでページ単位にcgiファイルがあるぞ!

2026/01/30(金)CGIのラッパー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にバナー広告を差し込みたいといったケースがある場合に、今回のような手法は便利だろう。

今時、往年のレンタルサーバーを新規に始め、それもバナー広告を出したいと考える人物がいるかどうかは謎だが、共通的に何かを差し込みたいなど、何かしら活用方法はあるかもしれない。

2026/01/30(金)さくらのレンタルサーバーに任意のCPANモジュールを入れて使う方法

更新日:
投稿日:

ググって出てきた記事が軒並み古くて役に立たなかったので、令和八年最新版として書いておく。

やり方

  1. CPANMのインストール
    curl -L https://cpanmin.us | perl - App::cpanminus
    
  2. CPANMのパスを.cshrcに書く
    私はzshを使っているため以下に.zshrcでの設定例を記述する。デフォルト環境はcshなので、cshの人は.cshrcに同じようなことを書けば成り立つはずだ。
    echo PATH=${HOME}/perl5/bin:${PATH}
    
  3. local::libをインストールする
    local::libはroot以外にあるCPANモジュールを使うためのものらしい。これを自分のホームディレクトリ配下に入れるようにコマンドを流す。
    cpanm --local-lib=~/perl5 local::lib && eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
    

使い方

以下のようなコマンドを流すとパスを指定してCPANモジュールを取得・展開できる。

# 書式
cpanm -l <パス> <モジュール名>

例えば以下を流すとpwd配下にextlibディレクトリが生成され、IP::Geolocation::MMDBがダウンロードされて展開される。勿論、依存関係も勝手に解決してくれる。

cpanm -l extlib IP::Geolocation::MMDB

モジュールを利用するときはライブラリの配置されているルートを指定し、次にモジュール名を指定するとうまくいくようだ。

use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;

あとがき

https://cpanmin.us/を見に行くと以下の記述があり、辿ってみると日本の人が作っていてちょっと驚いた。

# This is a pre-compiled source code for the cpanm (cpanminus) program.
# For more details about how to install cpanm, go to the following URL:
#
# https://github.com/miyagawa/cpanminus

2024/02/17(土)Perlのループ処理で正規表現キャプチャを扱う

投稿日:

確認環境

Env Ver
Perl 5.34.0

コード例

キャプチャ変数をifのスコープに封じ込める必要がある

my $text = <<EOF;
  ```unya
  hoge
    piyo
      fuga
  hpf
  ```
EOF

my @lines = split(/\n/, $text);

foreach my $line (@lines){
  if ($line =~ /^(?<indent> *)(?<bquote>```)(?<lang>[a-z]*)$/) {
    print "SYMBOL: '$+{indent}', '$+{bquote}', '$+{lang}'\n";
  } else {
    print "$line\n";
  }
}

上手くいかないコードの例

その1

$+変数はマッチするまで中身が消えないので、このプログラムは予期した動きにならない

my $text = <<EOF;
  ```unya
  hoge
    piyo
      fuga
  hpf
  ```
EOF

my @lines = split(/\n/, $text);

foreach my $line (@lines){
  $line =~ /^(?<indent> *)(?<bquote>```)(?<lang>[a-z]*)$/;
  print "$+{indent}, $+{bquote}, $+{lang}\n";
}

その2

my $text = <<EOF;
  ```unya
  hoge
    piyo
      fuga
  hpf
  ```
EOF

my @lines = split(/\n/, $text);

foreach my $line (@lines){
  $line =~ /^(?<indent> *)(?<bquote>```)(?<lang>[a-z]*)$/;
  if (defined $+) {
    print "$line: ";
    print "$+{indent}, $+{bquote}, $+{lang}\n";
    # この変数はundef出来ない
    undef $+;
  } else {
    print "$line\n";
  }
}

2024/02/16(金)Perlでの配列の扱い方を観察してみる

投稿日:

Perlで配列を初期化するときにいろいろ試したので、そのログ

確認環境

Env Ver
Perl 5.34.0

初期化方法

配列変数を配列で初期化

@始まりの変数に()で配列リストを作成して初期化する。

my @hoge = (1, 2, 3);
# 3<>1 2 3
print @hoge."<>@hoge\n";

配列変数をそのまま呼び出した場合は配列の長さが、文字列にするとスペース区切りになったものが出てくる。

配列変数を配列以外で初期化

@始まりの変数に配列以外を入れて初期化する。

my @hoge = 3;
# 1<>3<>3
print @hoge."<>@hoge<>$hoge[0]\n";

この場合は暗黙的に配列が作成され、ゼロ番目の要素にその値が入る。

配列変数を無名配列で初期化

前項の応用。参考までに無名配列で初期化した場合、[0]に無名配列が入り、二重配列になる。

my @hoge = [1, 2, 3];
# 1<>ARRAY(0x55e45749d860)<>ARRAY(0x55e45749d860)<>1<>2<>3
print @hoge."<>@hoge<>$hoge[0]<>$hoge[0][0]<>$hoge[0][1]<>$hoge[0][2]\n";

スカラ変数を配列で初期化

スカラ値にキャストされ、配列長が入る。これは配列変数をそのまま使った場合同様の振る舞い。

my $hoge = (1, 2, 3);
# 3
print $hoge."\n";

スカラ変数を無名配列で初期化

そのままでは配列として扱うことができず、配列として扱うためには@{ $var }形式にする必要がある。

my $hoge = [1, 2, 3];
# ARRAY(0x5596ea845de0)<>ARRAY(0x5596ea845de0)<>3<>1 2 3
print $hoge."<>$hoge<>".@{ $hoge }."<>@{ $hoge }\n";

またハッシュを配列として扱う場合は、この形式になり@{ $hash->{hoge} }のようにして扱う。

ハッシュ初期化時にキーを配列で初期化

配列が分解されてハッシュになるため、意図した値が入らない

my $hash = { hoge => (1, 2, 3) };
# 1<>1
print $hash->{hoge}."<>$hash->{hoge}\n";

実際にできる構造

$VAR1 = {
  'hoge' => 1,
  '2' => 3
}

ハッシュ初期化時にキーを無名配列で初期化

基本的にはこれで初期化するといい。使い方は「スカラ変数を無名配列で初期化」と同様

my $hash = { hoge => [1, 2, 3] };
# ARRAY(0x55599ba8dd08)<>3<>1 2 3
print $hash->{hoge}."<>".@{ $hash->{hoge} }."<>@{ $hash->{hoge} }\n";

ハッシュのキーに配列を代入

@{ $var }構文でやると上手く入る。普通に代入するとスカラ値になるので配列長が入る

my $hash = { hoge => undef };
@{ $hash->{hoge} } = (1, 2, 3);
# ARRAY(0x564efec0e4e8)<>3<>1 2 3
print $hash->{hoge}."<>".@{ $hash->{hoge} }."<>@{ $hash->{hoge} }\n";

ハッシュのキーに無名配列を代入

my $hash = { hoge => undef };
$hash->{hoge} = [1, 2, 3];
# ARRAY(0x55844312a4e8)<>3<>1 2 3
print $hash->{hoge}."<>".@{ $hash->{hoge} }."<>@{ $hash->{hoge} }\n";

備考

配列と無名配列の違いについて

配列は配列の実体そのもので、無名配列は参照らしい。

恐らくスカラ変数に無名配列を入れた場合、変数そのものには配列への参照が入るので、ARRAY(0x5596ea845de0)的な値が出てくるが、@{ $var }とすると参照先の実体が見れるので、配列として扱うことができるようになるのだと思う。

反対に配列変数の場合は、基本的に配列の実体を見るようになっていて、文字列化するときのみ参照が取れるようになっているように見える。

詳しくは以下の資料を参照するとわかる気がするので、また暇なときにでも見てみようと思う。

最近は値と参照の境界が分かりづらくなっている気もするので、Perlのこう言った書き口は少し面白いなと思った。要するにポインタを明示的に扱える訳だ。