WindowsでMongoose 3.2で色んな言語でCGIしてみた

クロスプラットフォームの軽量HTTPサーバというか・HTTPサーバライブラリというか、Mongooseをちょっと触っている。

Apachelighttpdなどに比べると対応していない機能も色々とあるけど、小さいしインストールや使い方は単純だしCGISSLぐらいには対応しているので、ローカルネットワーク上で一時的にHTTPサーバを使いたい時に手軽に使えそうだと期待している。

一時的にPCからPCにファイルを転送する時に使ったら便利そう。CGIを書けばファイルのダウンロードだけじゃなくアップロードにも対応できそうだ。

で、そのCGIなのだけど、ソースを見た限り設定ファイルに記述したインタプリタないしCGIファイルのシバン(shebang)に書いたインタプリタをCreateProcess()やらexecle(3)で起動するというシンプルな実装だった。

こうもシンプルだと手元の色々な言語処理系(それっぽいものを含む)でCGIしてみたくなるものだ。なので試してみた。

下準備(Mongooseのインストールと設定)

https://github.com/valenok/mongooseからMongoose 3.2のWindowsバイナリをダウンロードする。DLLは不要。ダウンロードしたバイナリは適当な場所に置いておく。念の為パスに空白文字や日本語が含まれる場所は避けたほうがよいかも。

MongooseはデフォルトではTCPポート8080を使用するので、実行する前に他のプロセスが8080を使っていないことを確認すること。

実行するとタスクトレイにアルファベットの小文字 `m' のアイコンが表示される。この状態でWebブラウザlocalhost:8080 にHTTPでアクセスすると、Mongooseのバイナリを置いてある場所のファイル一覧が表示されるはず(もしそのフォルダにindex.html等の名前のファイルが置いてあったら、ファイル一覧ではなくそのファイルが表示される)。

タスクトレイのアイコンを右クリックするとメニューが表示される。`Edit config file' を選択するとMongooseの設定ファイルmongoose.confが表示される(設定ファイルが無い場合は同時にMongooseのバイナリを置いてあるフォルダにファイルが生成される)。`Exit' を選択すれば終了する。

設定ファイルの項目は(後で「遊び方」の項で挙げている項目を除けば)大半は弄る必要はない。但しHTTPの待ち受けに使用するTCPポート番号を指定するlistening_portsとコンテンツファイルの置き場所を指定するdocument_rootは調整が必要かもしれない。

基本的に設定ファイルを編集したらMongooseを一旦終了して再度起動する必要がある。もしかしたらUnix環境では何らかのシグナルを送ると再読み込みしてくれるかもしれない(未確認)。

遊び方

まずmongoose.confのcgi_interpreterCGIファイルを実行させる言語処理系を設定する。cgi_interpreterに値を設定しない場合はCGIファイル1行目のシバンを見て処理系を選択するようだけど、それだと文法的にシバンを書けない言語をCGIとして使えない。今回は必ずcgi_interpreterに値を設定する。*1

設定の仕方はこんな感じ。

cgi_interpreter C:\Perl\bin\perl.exe

使用する処理系のパスを環境変数PATHに通している場合はこんな風にも書ける。

cgi_interpreter perl

但しセキュリティ的にはおススメできない(はず)。今回みたいにちょっと遊ぶ程度なら許されるかもしれないけど……。

言語によってはcgi_patternを編集する必要もある。これについては適宜説明する。

mongoose.confを編集し終えたらMongooseを終了し、再度起動すること。

書いたCGIファイルをdocument_rootで指定したフォルダ置く。仮にhello.cgiという名前だったとする。

listening_portsに値を設定していないなら、次のURIをブラウザで開けばOK。

http://localhost:8080/hello.cgi

listening_portsに値を設定している場合は、上記8080の部分をその値に置き換えること。例えばlistening_portsが5432なら、次のようなURIになる。

http://localhost:5432/hello.cgi

CGIの内容はベタにhello, world的な内容。本格的にCGIするなら環境変数を取得できて標準入力から(バイナリを含む)データを読み込むことが可能である必要があるし、更に言えば便利なライブラリもあった方が好ましいのだけど、今回はそこまでは掘り下げない。

Perl (ActivePerl)

CGIなので、まずはベタにPerlから。

#!/usr/bin/perl

use strict;
use warnings;

print <<'EOS';
Content-Type: text/html; charset=utf-8
Connection: close

<!DOCTYPE html>
<html>
  <head>
    <title>hello, Perl</title>
  </head>
  <body>
    <h1>hello, Perl</h1>
  </body>
</html>
EOS

普通に動作する。CGIモジュールを使えばもっと手軽に書けるのかも。

Bourne shell (MSYS)

CGIのベタな古典としてシェルスクリプトでも書いてみた。

#!/bin/sh

cat << EOS
Content-Type: text/html; charset=utf-8
Connection: close

<!DOCTYPE html>
<html>
  <head>
    <title>hello, Bourne shell</title>
  </head>
  <body>
    <h1>hello, Bourne shell</h1>
  </body>
</html>
EOS

こちらも普通に動作する。もっともエスケープ絡みが面倒なので実際に本格的にCGIを書くときにシェルスクリプトを使おうとは思わない。

Ruby (RubyInstaller)

PerlがOKということはRubyでもいけるはず。

#!/usr/bin/ruby -w

print <<'EOS'
Content-Type: text/html; charset=utf-8
Connection: close

<!DOCTYPE html>
<html>
  <head>
    <title>hello, Ruby</title>
  </head>
  <body>
    <h1>hello, Ruby</h1>
  </body>
</html>
EOS

意図せずPerl版と似たコードになってしまった。

HTML等を直書きせず、標準ライブラリのcgiを使って組み立てる方法もある。

#!/usr/bin/ruby -w

require "cgi"

cgi = CGI.new('html4')

cgi.out({ 'charset' => 'utf-8', 'connection' => 'close' }) {
  cgi.html({ 'DOCTYPE' => '<!DOCTYPE html>' }) {
    cgi.head {
      cgi.title { 'hello, Ruby' }
    } <<
    cgi.body {
      cgi.h1 { 'hello, Ruby' }
    }
  }
}

まだHTML5には対応していないようなので、HTML4用のオブジェクトを使った上で強引にDOCTYPEをHTML5の形式にしている。cgiライブラリは悪くないけど、同階層のタグ同士を明示的に文字列連結する必要がある所がちょっと面倒。

GNU Awk

PerlRubyがOKなら、系譜をさかのぼってAWKはどうだろうか? まあGNUが頭につくAWKだけど……。

#!/usr/bin/gawk -f

BEGIN {
    OFS = ORS
    print "Content-Type: text/html; charset=utf-8",
          "Connection: close",
          "",
          "<!DOCTYPE html>",
          "<html>",
          "  <head>",
          "    <title>hello, GNU Awk</title>",
          "  </head>",
          "  <body>",
          "    <h1>hello, GNU Awk</h1>",
          "  </body>",
          "</html>";
}

cgi_interpreterにはオプション `-f' を付けておくこと。

cgi_interpreter C:\soft\bin\gawk.exe -f

うん、動く。もっとも惜しいことに本格的にCGIを書くにはちょっと力不足っぽいんだよなあ。

Tcl

TclでCGIなネタは意外とある模様。こんなサイトもある(但しTclの基本の解説のみでCGIに特化した内容はない)。

#!/usr/bin/tclsh

set auto_noexec 1
puts {Content-Type: text/html; charset=utf-8
Connection: close

<!DOCTYPE html>
<html>
  <head>
    <title>hello, Tcl</title>
  </head>
  <body>
    <h1>hello, Tcl</h1>
  </body>
</html>}

問題なく動くけど……CGI書く用のライブラリって何かあるのだろうか? 無いと色々と面倒だよなあ。

bc (GnuWin32)

固定のテキストを返すだけならbcでもできる。

#!/usr/bin/bc -q

"Content-Type: text/html; charset=utf-8
Connection: close

<!DOCTYPE html>
<html>
  <head>
    <title>hello, bc</title>
  </head>
  <body>
    <h1>hello, bc</h1>
  </body>
</html>
"
quit

余分な出力をしないよう、cgi_interpreterにオプション `-q' を付けておく。

cgi_interpreter C:\soft\bin\bc.exe -q

まあ環境変数は見れないみたいだしメッセージボディの取得も基本的にはNGなので、実用度はゼロだ。

GDB (MinGW)

bcに引き続きGDBでも固定のテキストを返すぐらいならできるという……。

#!/usr/bin/gdb --batch -x

echo Content-Type: text/html; charset=utf-8\n
echo Connection: close\n
echo \n
echo <!DOCTYPE html>\n
echo <html>\n
echo   <head>\n
echo     <title>hello, GDB</title>\n
echo   </head>\n
echo   <body>\n
echo     <h1>hello, GDB</h1>\n
echo   </body>\n
echo </html>\n

オプションを忘れないように。

cgi_interpreter C:\soft\mingw\bin\gdb.exe --batch -x

こっちも実用度はゼロ。環境変数を見ることぐらいはできそうだけど。

Gauche (Windows用バイナリ)

bcやGDBとは対照的に、Gaucheなら柔軟に色々とできそうな感じ。

#!/usr/bin/gosh

(display
 (string-join
  '("Content-Type: text/html; charset=utf-8"
    "Connection: close"
    ""
    "<!DOCTYPE html>"
    "<html>"
    "  <head>"
    "    <title>hello, Gauche</title>"
    "  </head>"
    "  <body>"
    "    <h1>hello, Gauche</h1>"
    "  </body>"
    "</html>"
    "")
  "\r\n"))

出力するテキストを直書きするよりもライブラリを使った方が楽だ。

#!/usr/bin/gosh

(use www.cgi)
(use text.html-lite)

(cgi-main
 (lambda (params)
   (list
    "Connection: close\r\n"
    (cgi-header)
    "<!DOCTYPE html>\n"
    (html:html
     (html:head
      (html:title "hello, Gauche"))
     (html:body
      (html:h1 "hello, Gauche"))))))

HTMLってツリー構造だから、意外とリストと合うんだ……。レスポンスヘッダやDOCTYPEを意図した内容で出力させようと小細工している。

JavaScript (Node.js)

Node.jsを単純なCGIとして使うなんて本末転倒な気もするけど、一応書いてみた。

/*jslint node: true, indent: 2, maxerr: 50 */
(function () {
  'use strict';
  console.log([
    'Content-Type: text/html; charset=utf-8',
    'Connection: close',
    '',
    '<!DOCTYPE html>',
    '<html>',
    '  <head>',
    '    <title>hello, Node.js</title>',
    '  </head>',
    '  <body>',
    '    <h1>hello, Node.js</h1>',
    '  </body>',
    '</html>'
  ].join('\n'));
}.call(this));

問題なく動くし色々と本格的にできそうだけど、Node.js自体をサーバとして動かす方がベターなような。

JavaScript (PhantomJS)

Node.jsだけじゃなくPhantomJSでも書ける。

/*jslint devel: true, indent: 2, maxerr: 50 */
/*global phantom: false */
(function () {
  'use strict';
  console.log([
    'Content-Type: text/html; charset=utf-8',
    'Connection: close',
    '',
    '<!DOCTYPE html>',
    '<html>',
    '  <head>',
    '    <title>hello, PhantomJS</title>',
    '  </head>',
    '  <body>',
    '    <h1>hello, PhantomJS</h1>',
    '  </body>',
    '</html>'
  ].join('\n'));
  phantom.exit();
}.call(this));

PhantomJSでサーバサイドする日が来るとは(何か違う)。

Lua

シンプルな所でLuaなんてどうだろうか?

#!/usr/bin/lua

io.stdout:write([[
Content-Type: text/html; charset=utf-8
Connection: close

<!DOCTYPE html>
<html>
  <head>
    <title>hello, Lua</title>
  </head>
  <body>
    <h1>hello, Lua</h1>
  </body>
</html>
]])

結構いけるかも。LuaGaucheならCGI書くときにスタックを気にせず末尾再帰できるぞ。

Io

Luaよりも更にシンプル且つマイナー(主観)なIoではどうか?

#!/usr/local/bin/io

"""Content-Type: text/html; charset=utf-8
Connection: close

<!DOCTYPE html>
<html>
  <head>
    <title>hello, Io</title>
  </head>
  <body>
    <h1>hello, Io</h1>
  </body>
</html>
""" print

printが末尾にくるあたりが独特。このコードでは改行コードをLFにする必要があるようだ。CRLFでは余分に改行されてうまく動かなかった。

難点といえばIoで環境変数を取得する方法が分からないんだよなあ。

2012/10/05追記

環境変数はSystem getEnvironmentVariableで取得できるみたい。

REXX (Regina)

マイナーさで言えば――メインフレームの世界を除けば、若い世代のプログラマの中では――REXXも負けていない。

#!/usr/bin/rexx

say "Content-Type: text/html; charset=utf-8"
say "Connection: close"
say ""
say "<!DOCTYPE html>"
say "<html>"
say "  <head>"
say "    <title>hello, REXX</title>"
say "  </head>"
say "  <body>"
say "    <h1>hello, REXX</h1>"
say "  </body>"
say "</html>"

REXXでも意外とCGIを書ける気がする。ただ言語自体が独特というか……。

JScript (WSH)

せっかくWindows上でMongooseを動かしているので、試しにWSHCGIできないか試してみた。ひとまず言語はJScript

/*jslint indent: 2, maxerr: 50, windows: true */
WScript.Quit((function () {
  'use strict';
  WScript.StdOut.Write([
    'Content-Type: text/html; charset=utf-8',
    'Connection: close',
    '',
    '<!DOCTYPE html>',
    '<html>',
    '  <head>',
    '    <title>hello, JScript</title>',
    '  </head>',
    '  <body>',
    '    <h1>hello, JScript</h1>',
    '  </body>',
    '</html>',
    ''
  ].join('\r\n'));
  return 0;
}.call(this)));

cscriptは拡張子で使用言語を判定しているので、CGIファイルの拡張子を `js' にした上でcgi_patternを書き換える必要がある。あと余分な出力を除くためにcgi_interpreterにオプション `//B' を付加しておくこと。

cgi_pattern **.js$
cgi_interpreter C:\WINDOWS\system32\cscript.exe //B

環境変数も取得できるので、一応実用レベルでCGIできそう。ただ何というか冗長なコードになりそう。

VBScript (WSH)

JScriptCGIできるなら当然VBScriptでもいける。

Option Explicit 

WScript.StdOut.Write(Join(Array( _
    "Content-Type: text/html; charset=utf-8", _
    "Connection: close", _
    "", _
    "<!DOCTYPE html>", _
    "<html>", _
    "  <head>", _
    "    <title>hello, VBScript</title>", _
    "  </head>", _
    "  <body>", _
    "    <h1>hello, VBScript</h1>", _
    "  </body>", _
    "</html>", _
    ""), vbCrLf))

WScript.Quit(0)

JScriptと同様にcgi_patternとcgi_interpreterを調整すること。拡張子は `vbs' で。

cgi_pattern **.vbs$
cgi_interpreter C:\WINDOWS\system32\cscript.exe //B

VBScriptCGIって需要あるのかなあ。JScript同様にWSH絡みで冗長なコードになりそうな気はする。

バッチファイル

そして実はバッチファイルでもCGIできなくはない……。

@echo off

echo Content-Type: text/html; charset=utf-8
echo Connection: close
echo.
echo ^<!DOCTYPE html^>
echo ^<html^>
echo   ^<head^>
echo     ^<title^>hello, Batch file^</title^>
echo   ^</head^>
echo   ^<body^>
echo     ^<h1^>hello, Batch file^</h1^>
echo   ^</body^>
echo ^</html^>

うーん、特殊文字エスケープが面倒。

JScriptVBScriptと同様に拡張子絡みでcgi_patternを書き換える必要がある。CGIファイル自体も拡張子を `bat' にすること。cgi_interpreterの末尾の `call' は決して書き間違いではないので注意。

cgi_pattern **.bat$
cgi_interpreter C:\WINDOWS\system32\cmd.exe /c call

これ本当に動く。動くけど実用的なのか否か迷うよなあ、バッチファイルのCGIだなんて。

動かなかったもの

F#(F# Interactive)は処理系は起動しているけど何も出力しない(Mongoose側で取得できない?)ようで、HTTP 500になってしまう。

PowerShellCLISPはどちらも処理系自体は起動しているようだけど、いつまで経ってもレスポンスを返さない。何でだろう?

*1:なので今回はCGIにシバンを書く必要は無いけど、趣味で書いておく。それもUnix環境用のパスを……。