IIS上のPowershell CGI ScriptでPOSTリクエストを処理する

IISではCGIモジュールを利用することで、スタンドアロン形式のスクリプト実行プログラムを通してCGIスクリプトを実行することが出来る。PowerShellも当然ながら例外ではない。検索エンジンで適当なワードを検索すれば、いくつもの導入事例を見つけられるだろう。
しかし、PowershellでのPOSTメソッドを使った導入事例については、探してみても見当たらなかった。試行例はあるようだけど、(CGIモジュールで設定した)タイムアウトまで戻ってこないという結論だった。
CGIにおけるPOSTリクエストの処理は、環境変数と標準出入力を用いた、ごく単純なデータのやりとりなので、Powershellだけ無理と言うことは考えにくい。
それでは、ちょっと頭をひねって実現方法を考えてみよう。
POSTメソッドによるリクエストでは、

  1. エンティティボディはバイナリデータとして読み取る
  2. エンティティボディのサイズは環境変数のCONTENT_LENGTHで受ける

が一般的な知識になっている。
これをPowershellの世界に置き換えると、読み込みに[Console]::Inや、[Console]の読み込みメソッドを直接使わず、BinaryReaderないしBinaryStreamを通して CONTENT_LENGTH バイトだけを読め、ということになる。
これをコードに落としてみよう。

$bodyReader = New-Object System.IO.BinaryReader -ArgumentList @([Console]::OpenStandardInput())
$bodyBytes = $entityBodyReader.ReadBytes($ENV:CONTENT_LENGTH)

実際に使うには、CONTENT_LENGTHに対する範囲チェックや、例外捕捉が必要になるけど、ここでは割愛する。
実はここにPowershellの落とし穴が1つあって、-InputFormatコマンドラインオプションで、バイナリデータ相当の受付入力書式に切り替えなければならない。さもなくばハングアップする。

PowerShell[.exe]
(snip)
[-InputFormat {Text | XML}]

PowerShell.exe Console Help | Microsoft Docs

ヘルプではText(テキスト)かXML(XML)しかないように見えるが、None(?)という隠しオプション(?)を指定することが出来て、これがバイナリデータ相当になるらしい。IISのハンドラ設定で、Powershellハンドラにスクリプトファイルパスを与える-Fileしか指定していない場合は、この-InputFormatオプションも指定する必要がある。
標準出力にレスポンスを出力する部分については、UTF-8である限り Echo で問題ない。
問題は出力しきった後、スクリプトを終了するときだ。不確かな情報だけど、IISはPOSTリクエストの時、Powershellを自動的に終了しないという。

POWERSHELL.EXE will hang because on a remote script from IIS because it does not close off the POWERSHELL.EXE process automatically... all you have to do is add NUL to the end of the command.

powershell script does not complete when launched from IIS : The Official Microsoft IIS Forums

コマンドの最後にNULを付加すればいいとあるけど、いまいち解せない。
試しに標準出力にNULを付加し、Exitでみずから終了すれば動くことが分かった。Out.Writeはいらないかも知れない。

[System.Console]::Out.Write([char]0)
[System.Environment]::Exit(0)

これらをまとめて、

  • inputフィールド"name","id"のShift_JIS HTMLフォームからPOSTで受け取る
  • TABLEでフィールド名、フィールド値を返す

という例で、一つのコードに表現してみよう。

#requires -version 2.0
Add-Type -AssemblyName System.Web
Add-Type -AssemblyName System.Web.Extensions

if ($Env:CONTENT_TYPE -ne "application/x-www-form-urlencoded") {
        throw New-Object System.IO.InvalidDataException -ArgumentList@("Unexpected content type")
        [System.Environment]::Exit(-1)
}

$entityBodyReader = New-Object System.IO.BinaryReader -ArgumentList @([Console]::OpenStandardInput())
$entityBody = [System.Text.Encoding]::ASCII.GetString($entityBodyReader.ReadBytes($ENV:CONTENT_LENGTH))
$queryValues = [System.Web.HttpUtility]::ParseQueryString($entityBody, [System.Text.Encoding]::GetEncoding(932))

Echo "Content-Type: text/html"
Echo ""
Echo "<HTML><BODY><TABLE>"
Echo "<TR><TD>id</TD><TD>$($queryValues.Get(`"id`"))</TD></TR>"
Echo "<TR><TD>name</TD><TD>$($queryValues.Get(`"name`"))</TD></TR>"
Echo "</TABLE></BODY></HTML>"

[System.Console]::Out.Write([char]0)
[System.Environment]::Exit(0)

こんなことは、目立つところに誰かまとめてくれたらいいのに!
え、IISで、PowershellCGIスクリプトを組んで、POSTリクエストを処理する方がおかしい?それはごもっとも。