CGIエコーサーバー(生CGI)とRack/CGI
プログラミング::web
序
もはや使うことも少なくなったCGI。
現代ではほとんどの場合HTTPを理解するアプリケーションサーバーか、もしくは(特にPHPの場合は)バイナリプロトコルであるFastCGIを利用するため、CGIを使うことは本当に減った。
しかし、CGIの仕組みは非常に単純であり、コードが書きやすい。 そのため、簡単なプログラムを動かしたい場合は現代でも利用価値はある。
一方で「レガシーな技術」とみなされているために、有効なアクセスが難しい部分がある。 例えば、リクエストボディがクエリストリングならばCGIライブラリがよきにはからってくれるが、リクエストボディがJSONという、現代ではごくごくごくごく普通のことがなかなか取り扱えなかったりする。
恐らく最も現代的なCGIライブラリは、RubyのRack/CGIハンドラだろう。
ライブラリはrackup
、クラスはRackup::Handler::CGI
である。
だがこれはこれで、なんだか不器用な動作をする。
CGIの基本的な仕組み
ウェブサーバーはCGIアプリケーションを普通にプロセスとして起動する。
Apacheの場合は対象プログラムを普通に実行する。 つまり、対象プログラムは実行可能である必要がある。
LighttpdやHiawathaは拡張子に対応したハンドラに対象プログラムのパスを渡す。 対象ファイルは実行可能である必要はないが、スクリプトファイルではない場合はひと工夫必要になる。
このとき、環境変数として各種パラメータをセットした上でプロセスをspawnする。 プロセスの標準入力はリクエストボディにつながっている。 また、プロセスの標準出力はHTTPレスポンスになる。
CGIプログラムの出力はHTTPヘッダではなくCGIヘッダと呼ばれるものになる。
特徴的な要素として、Status
というヘッダがあり、このヘッダがウェブサーバーによってHTTPレスポンスステータスに変換される。
以下はBashによるハローワールドCGIである。
#!/bin/bash
cat <<EOF
Status: 200 OK
Content-Type: text/plain
Hello, world.
EOF
リクエストボディを読む
リクエストボディは標準入力から読むことができる。
しかし、EOFはこないため、単純なSTDIN.read
のようなことはできない。
リクエストボディの長さは$CONTENT_LENGTH
環境変数に格納されている。
ただ、これはクライアント側のContent-Length
ヘッダに従うため、正しいことが保証されているわけではない。
また、リクエストボディの長さとContent-Length
の値が等しい必要はない。
これは「受け取ってほしいデータはこの長さです」と言っているだけで、それ以上のデータを送ることが許されている。
Content-Length
に送信データが満たない場合、サーバーはデータが送られてくるのを待つことになる。
問題はあるが、ありえないことではない。
Content-Length
を送ってこないというケースもある。
これも違反ではない。ありえることだ。
Content-Length
を送ってこないことを許容できない場合、411 Length Required
というステータスを返すことができる。特にチャンクで送信してくることを許容できないケース、というのがある。
このような場合の扱いは難しく、生CGIを使う状況では考えたくない。
Content-Length
がなければ滅多に使わない411
ステータスコードを返しておく、というのが簡単な話だろう。相手がそれに合わせてどうこうしてくれることは期待できないが、拒否理由の明示にはなる。
生CGIでのエコーサーバー
以上を踏まえて次のようなエコーサーバーが書ける。
一応、ウェブサーバーがContent-Type
とヘッダターミネーターを必須にしている場合に備えて少し余計なものを入れてある。
#!/usr/bin/ruby
def main
= ENV["REQUEST_METHOD"]
meth = ENV["CONTENT_LENGTH"].to_i
content_length
# POSTメソッドだけを受け入れる
unless meth == "POST"
puts <<-EOF
Status: 405 Method Not Allowed
Content-Type: text/plain
EOF
return
end
# Content Lengthがないリクエストを拒否する
if !content_length || content_length.zero?
puts <<-EOF
Status: 411 Length Required
Content-Type: text/plain
EOF
return
end
# 標準入力から $CONTENT_LENGTH 分だけ読み込む
= STDIN.read(content_length)
content
# 応答を返す
puts <<-EOF
Status: 200 OK
Content-Type: text/plain
#{content}
EOF
end
main
explainのために最小限で書いているが、ウェブブラウザからリクエストされるのであればOPTION
メソッドに対応する必要がある場合がある。
また、この処理ではContent-Length
が実際のデータよりも大きい場合に発生するタイムアウトはウェブサーバーが処理してくれることを期待しており、タイムアウト処理を入れていない。
だが、ウェブブラウザからアクセスされるようなスクリプトは生CGI取り扱いをしないほうが良い。
Rack/CGI
RubyのウェブアプリケーションインターフェイスライブラリであるRackは、Rubyでウェブアプリケーションを開発する場合のデファクトスタンダードとなる選択肢である。
そして、RackのCGIハンドラというのが存在し、CGIとして起動されるスクリプトでもRackを使うことができる。 ただし、Rack/CGIは割とCGIの事情に左右されるため、単にハンドラを変更するだけでCGI以外へ移植する、というのはできない場合が多い。
次に示すのはRack/CGIでただただ204 No Content
を返すだけのRackアプリケーションである。
これは、ハンドラを書き換えれば他のハンドラでも動作する。
require 'rack'
require 'rackup'
class MyApp
def call env
= Rack::Response.new
res = Rack::Request.new env
req .status = 204
res
.finish
resend
end
Rackup::Handler::CGI.run MyApp.new
普通のRackアプリケーションなら、次の要領でエコーサーバーが書ける。
require 'rack'
require 'rackup'
class MyApp
def call env
= Rack::Response.new
res = Rack::Request.new env
req = req.body.read
body .status = 200
res.write body
res
.finish
resend
end
Rackup::Handler::CGI.run MyApp.new
実は、ApacheやLighttpdはCGIアプリケーションに渡すSTDINをEOFつきになるように渡すため、これで動作する。 しかし、Hiawathaはストリームをそのまま渡すため、タイムアウトしてしまう。
これは、よくやるJSON.load(req.body.read)
やJSON.load env["rack_input"].read
でも同じことが言える。
というのも、このreq.body
は単なるIO::STDIN
であり、生CGIと同じ問題を抱えているのだ。
これについて、ApacheやLighttpdのほうが自然な挙動に見えるが、Hiawathaのほうが仕様に忠実な挙動である。 これに対応するためには、生CGIと同じような処理が必要になり、一気に汚くなる。
require 'rack'
require 'rackup'
class MyApp
def call env
= Rack::Response.new
res = Rack::Request.new env
req = req.body.read(ENV["CONTENT_LENGTH"].to_i)
body .status = 200
res.write body
res
.finish
resend
end
Rackup::Handler::CGI.run MyApp.new
bodyがない場合や、メソッドのことを考えていないのも問題だ。 これらの考慮を入れると、生CGIとあまり変わらない話になり、Rackの嬉しさはほとんど残らない。
本来、こういう問題はRackup::Handler::CGI
が面倒を見るべきで、それでこそ適切な抽象化なのだが、このハンドラはそこまで丁寧に作られていないので、かなり微妙な結果になる。
ちなみに、Rackup::Handler::CGI
を使う場合、HiawathaではRack::Request#params
も動作しない。
つまり、EOFがない入力に関しては全くカバーされていない。
Rack/CGIは本当に嬉しいか?
例えば当座はレンタルサーバーの(というか、Rackが動かせるというところまで話を限定するとほぼほぼConoHa WINGの)CGIで動作させるが、将来的にはVPSでアプリケーションサーバーに以降したい、などと考える場合、Rack/CGIを使うというのも悪い手ではない。
また、現在のプラットフォームがレンタルサーバーではなく、あくまで静的ファイルが主体で、一部簡単なアプリケーションがある程度の構成であるためにLighttpdをウェブサーバーに採用している、というような場合も、将来的な移行を考えてRack/CGIで書いておくことは、CGIライブラリを使うよりは建設的だと見ることができる。
単に簡単なプログラムをCGIとして動作させたい場合は、Rack/CGIにこだわる意味はないだろう。 CGIライブラリでもいいし、生CGIでもいい。 CGIライブラリの存在価値はほとんどクエリストリングの解析にあるため、JSONで渡されるのならばCGIライブラリを使う必要もない。
ただ間違いなく言えるのは、HiawathaのCGIはとてもきつい、ということだ。 ちなみに、RubyのCGIライブラリはHiawathaの環境でもちゃんと動く。
だが、アプリケーションを簡単に動作させるためにLighttpdもしくはHiawathaでCGIを利用し、リバースプロキシとしてNginxを採用する、というのは、ものすごく価値に乏しい。
というのも、生Rackはそこまで便利でもなく、Sinatraが圧倒的に書きやすい、という利点がある。 生RackではなくSinatraで書くことで得られる省エネ効果は、Systemdユニットを1つ書いてNginxにパスを1本生やす手間よりもずっと大きい可能性が高い。
つまり、ハローワールドならこんな感じで書いて
#!/bin/ruby
require 'sinatra'
:server, ["thin"]
set :bind, "127.0.10.2"
set :port, 8001
set
"/" do
get "Hello?"
end
こんなユニット作って
[Unit]
Description=Sinatra 8001
[Service]
Type=simple
ExecStart=/usr/local/bin/sinatra8001.rb
Restart=always
[Install]
WantedBy=multi-user.target
systemctl enable --now sinatra8001.service
とかして、あとはNginxに
location /fooapp {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.10.2:8001;
}
とかしてリロードしたら出来上がりである。
これだけならSinatraのほうが手間に思えるが、ここから機能を色々足していく場合、比較にならないほどSinatraで書くほうが楽。
小さいのをたくさん書くとlocalhost
(=127.0.0.1
,
::1
)のポート枯渇が……と気になるかもしれないが、localhostのポート競合を気にせずサーバーを起動するで述べたように127.0.0.2
とかしておけば問題は解決である。
このように、VPS等でウェブサーバーを運用している諸兄諸姉におかれては、「Rack/CGI、ひいてはCGIを使うことで楽になる要素はほとんどない」だと思って良い。 もちろん、言語が違えばライブラリが違ってくるので、事情は変わってくるかもしれないが、Rubyの場合はSinatraのアドバンテージが大きすぎる。
あと、アプリケーションサーバー方式を取れば簡単にNode(Express等)で書けるという選択肢の広がりも見逃せない。
ちなみに、私はConoHa WINGから突貫でVultr (VPS)に移行させたため、WINGで動かしていたCGIの移植作業を行う余裕がなく、現在も一部はCGIで動作させている。 が、もし今新規に書くならCGIを採用することはないだろう。
とはいえ、本当に簡単な目的(ほとんどの場合はWebhookの受け口)に使いたいことがあるため、CGIが動く環境がほしいことはある。だから、Lighttpdをそれ用に動かしておくのも、悪くはないと思うが。