Haskell で CGI を書く:Network.CGI(cgiパッケージ)

Haskell で CGI を書いてみよう。

目次

CGI の基本

CGI とは Common Gateway Interface の略で、動的な Web ページをプログラムで実装するための枠組みである。HTTP ヘッダや内容は、プログラムに対する環境変数や標準入出力で受け渡しでき、実装に使う言語は問わない。

例:

main = do
    putStrLn "Content-Type: text/plain; charset=UTF-8"
    putStrLn ""
    putStrLn "Hello world!"

なお、ここでは CGI の効率とか代替技術とかの話はしない。

Network.CGI (cgi パッケージ)

先の例のように、ライブラリに頼らず直接 CGI を書くこともできるが、クエリ文字列の解釈とかクッキーの扱いとかになってくるとさすがに自分で実装するのは面倒である。

そこで、 cgi パッケージを使う:https://hackage.haskell.org/package/cgi

例:

import Network.CGI

cgiMain :: CGI CGIResult
cgiMain = do
  setHeader "Content-Type" "text/plain; charset=UTF-8"
  output "Hello world!"

main = runCGI cgiMain

Network.CGI では CGI モナドが定義されていて、 HTTP リクエストの情報の取得、および HTTP ヘッダーの出力 (setHeader 関数) ができる。

足し算する例:

import Network.CGI

cgiMain :: CGI CGIResult
cgiMain = do
  ma <- readInput "a" :: CGI (Maybe Integer)
  mb <- readInput "b"
  case (ma,mb) of
    (Just a,Just b) -> do
      setHeader "Content-Type" "text/plain; charset=UTF-8"
      output $ concat [show a," + ",show b," = ",show (a + b)]
    _ -> do
      setHeader "Content-Type" "text/html; charset=UTF-8"
      output "<body><form><input name=\"a\"> + <input name=\"b\"> = ? <button type=\"submit\">submit</button></form></body>"

main = runCGI cgiMain

CGIへのパラメーターは getInput :: String -> CGI (Maybe String)readInput :: (Read a) => String -> CGI (Maybe a) で取得できる。

Network.CGI の問題点:Unicode 文字列の扱い

Network.CGI を素朴に使うと、 Unicode 文字列の取り扱いに難がある。

例えば、次のようなプログラムを実行してみる:

import Network.CGI

cgiMain :: CGI CGIResult
cgiMain = do
  a <- getInput "a"
  case a of
    Just "槍ヶ岳" -> do
      setHeader "Content-Type" "text/plain; charset=UTF-8"
      output "正解です!"
    Just s -> do
      setHeader "Content-Type" "text/plain; charset=UTF-8"
      output $ "不正解です。 (a=" ++ s ++ ", length=" ++ show (length s) ++ ")"
    Nothing -> do
      setHeader "Content-Type" "text/html; charset=UTF-8"
      output "<body><form>日本で5番目に高い山は?<input name=\"a\"><button type=\"submit\">submit</button></form></body>"

main = runCGI cgiMain

これを実行すると文字化けする。

どういうことかというと、 Network.CGI の output や getInput 関数はバイト文字列と String を変換する際に安直に Data.ByteString.Lazy.Char8 を使っており、非8ビット文字の上位ビットが切り捨てられる。

幸い、 Network.CGI には ByteString をそのまま扱う関数も用意されているので、それを使って自前で UTF-8 でコードする処理を書いてやれば良い。getInput 関数の ByteString 版は getInputFPS :: String -> CGI (Maybe Data.ByteString.Lazy.ByteString) で、 output 関数の ByteString 版は outputFPS :: Data.ByteString.Lazy.ByteString -> CGI CGIResult である。

ちゃんと UTF-8 でエンコードするようにした例:

{-# LANGUAGE OverloadedStrings #-}
import Network.CGI
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import Data.Text.Encoding.Error (lenientDecode)

getInputT :: String -> CGI (Maybe TL.Text)
getInputT name = fmap (fmap $ TL.decodeUtf8With lenientDecode) (getInputFPS name)

outputT :: TL.Text -> CGI CGIResult
outputT content = outputFPS (TL.encodeUtf8 content)

cgiMain :: CGI CGIResult
cgiMain = do
  a <- getInputT "a"
  case a of
    Just "槍ヶ岳" -> do
      setHeader "Content-Type" "text/plain; charset=UTF-8"
      outputT ("正解です!")
    Just s -> do
      setHeader "Content-Type" "text/plain; charset=UTF-8"
      outputT $ TL.concat ["不正解です。 (a=", s, ", length=", TL.pack $ show (TL.length s), ")"]
    Nothing -> do
      setHeader "Content-Type" "text/html; charset=UTF-8"
      outputT "<body><form>日本で5番目に高い山は?<input name=\"a\"><button type=\"submit\">submit</button></form></body>"

main = runCGI cgiMain

(Unicode文字列を扱うのに String の代わりに Data.Text.Lazy を使うようにした。また、 Data.Text の型の文字列リテラルを書けるようにするために OverloadedStrings 拡張を有効にしている。)


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です