Node.js スクリプトを CGI として動かしてみる

CGI という仕組みは、Perl・Ruby・PHP などの言語に限らず、標準入力と標準出力を扱える言語なら何でもいいらしい。ということは、Node.js をランタイムにした CGI も可能だと思われる。

と、ふと思い立って実際にやってみた。

目次

前例がある:CGI-Node

同じようなことを考えていた人がいた。

$ cat cgi-bin/node_test.cgi
#!/usr/bin/node

console.log('Content-type: text/plain');
console.log('');
console.log('Hello World');

$ chmod 755 cgi-bin/node_test.cgi

シンプルなコードが掲載されていたが、コレでやれるみたいだ。

1行目の Shebang は node (Windows なら node.exe) へのフルパスを渡してやる必要があり、/usr/bin/node/usr/bin/env node なんかだとうまく行かないことがあるみたい。Nodebrew なんかを使っている場合はそもそも ~/.nodebrew/ 配下にあったりするので、この辺は実行環境に合わせてフルパスを書いてやる必要がありそうだ。

Node.js の組み込みパッケージである fs なんかは使えそうだが、それ以外の npm パッケージを require() するのはできないんじゃないかな。試してないけど。

CGI として動かせる一式が組まれた、CGI-Node というスクリプトも存在した。

リクエストヘッダとかを処理してくれるのは分かったが、.htaccess にて cgi-node.js を挟んで処理するよう設定する必要があるらしい。若干面倒臭い。

Node.js ならサーバを立てて動かせばいいじゃん、とも思うだろうが、Apache や nginx から、localhost:3000 とかで動いてるサーバにリクエストをフォワードしてやるのも面倒臭い気がする。既存の CGI サーバで、CGI スクリプトとして動かしたい時に、開発言語として Node.js を選びたい、という時のための検証である。w

前提条件

今回の前提として、Apache サーバで CGI が動かせるようにしてあって、拡張子は .cgi を対象としている。お好みで .jss とかを CGI として動かせるようにしておくと良いかと (.js を対象にしてしまうと通常の JS ファイルと混在して辛い)。

Node.js は Nodebrew でインストールし、$ type node コマンドで確認できたフルパスを Shebang に書くこととする。

最低限 CGI として動かせるコード

Node-CGI は大仰なので、一切の依存を持たず、完全なシングルファイルで CGI として動作するスクリプトを組んでみる。最低限以下を書けば、CGI として動かせる。

#!/usr/bin/node
// ↑実行パスは適宜調整する

(async () => {
  let postBody = '';  // POST 時にリクエストボディを取得する
  if(process.env.REQUEST_METHOD === 'POST') for await(const chunk of process.stdin) postBody += chunk;
  
  // HTTP ヘッダを出力する
  console.log('Content-Type: text/html; charset=UTF-8\n\n');
  
  // 以下、任意の処理
  console.log('<h1>Hello Node.js CGI</h1>');
})();

asyncawait を使っている他、for await of を使っているので Node.js v12 以降が対象。それ以前の古いバージョンで動かすには、次のようなコードにすれば動かせる。

#!/usr/bin/node

new Promise((resolve) => {
  // POST 時にリクエストボディを取得する
  if(process.env.REQUEST_METHOD !== 'POST') return resolve();
  
  let postBody = '';
  process.stdin.on('data', (chunk) => { postBody += chunk; });
  process.stdin.on('end', () => { resolve(postBody); });
}).then((postBody) => {
  // HTTP ヘッダを出力する
  console.log('Content-Type: text/html; charset=UTF-8\n\n');
  
  // 以下、任意の処理
  console.log('<h1>Hello Node.js CGI</h1>');
});

POST 時のリクエストボディのみ、process.stdin から取得するため、このような処理を入れている。GET でしか処理しないのであれば、これらの前処理すら省いていきなり「HTTP ヘッダの出力」から実装しても良い。

Node.js を CGI として使うためのアレコレ

POST 時のリクエストボディのみ、取得方法が特殊なので上のとおり実装しておいたが、それ以外の情報はどのように受け取れば良いか。

GET 時のクエリ文字列や、サーバの情報、リクエスト情報などは、全て process.env を見ることで確認できる。代表的なモノは以下のとおり。

環境変数名 内容
HTTP_HOST サーバの Host Name or Public IP
SERVER_NAME
SERVER_ADDR サーバの Private IP
SERVER_PORT サーバのポート (80 とか 443 とか)
DOCUMENT_ROOT Apache サーバ等のドキュメントルート
CONTEXT_DOCUMENT_ROOT
REMOTE_ADDR リクエスト元の Public IP
HTTP_USER_AGENT リクエスト元の User Agent
HTTPS HTTPS 接続時は on が設定される
REQUEST_SCHEME プロトコル (httphttps)
REQUEST_METHOD メソッド (GETPOST)
SCRIPT_FILENAME CGI ファイルのフルパス
SCRIPT_NAME CGI ファイルへのルート相対パス (/ 始まり)
REQUEST_URI リクエストパス (/ 始まり、クエリ文字列を含む)
QUERY_STRING クエリ文字列 (? は含まない)

レスポンスは process.stdout.write()console.log() で書けば良い。

console.log('<pre>', process.env, '</pre>');

などとコーディングすれば、リクエスト時の環境変数一覧が出力できる (セキュリティ的に注意が必要だが)。

その他、エラー発生時の処理やプロセス終了時の処理を process.on で定義しておくと安全であろう。

// エラー発生時に行う処理
process.on('uncaughtException', (error) => {
  // HTTP ヘッダを出力してあるかどうか確認できるようフラグ変数を管理しておくと良いだろう
  console.log('Error :', error);
});

// 終了時の処理を予め定義する
process.on('exit', () => {
  // 全ての処理が終わった後に以下が実行される。ちゃんとレスポンスに乗る
  console.log('Exit');
});

ボイラープレートを作った

こうしたノウハウをまとめた、ボイラープレートプロジェクトを作った。以下の GitHub にある index.js をベースとして利用してもらえればと思う。

以上

ちゃんと Node.js でも CGI として動かせた。外部 npm パッケージが使えなさそうだが、単一ファイルとして動かせるようにビルドしてやればイケるかも?