PowerShellでHTTPサーバを書いた

## 処理の概要

1. PowerShellから.NET C#のSystem.Net.HttpListenerクラスを呼び出し
   ローカルホストとのHTTP通信を行う
1. 下記のPowerShellファイルは、Webサイトのルートフォルダに配置されている
   想定で作られている
1. `http://localhost:<ポート番号>/`でホームページとなるindex.htmlファイルが開く
1. HTML内の内容から、CSS、JavaScript、画像などのファイルを追加で送信可能

## 動作の前提

:::note warn
Webページのルートフォルダに配置して使う
:::

## ソースコード

```powershell:server.ps1
# ポート番号
$Port = "8080"
# URL
$url = "http://localhost:" + $Port + "/"
# localhostに繋げない環境への対応
# $url = "http://+:" + $Port + "/Temporary_Listen_Addresses/"
# リクエストがルート(/)の場合、呼び出されるファイル
$HomePage = "index.html"
# コンテンツタイプ辞書(ブラウザに送るデータの種類)
$ContentType = @{
    "css" = "text/css"
    "js" = "application/javascript"
    "json" = "application/json"
    "html" = "text/html"
    "pdf" = "application/pdf"
    "txt" = "text/plain"
    "jpg" = "image/jpeg"
    "png" = "image/png"
    "xml" = "application/xml"
    "*" = "application/octet-stream"
}

# リクエストがなくなるまでファイル送信を行う
function fileSendToClient([ref]$response, $fileName) {
    $currentDirectory = Convert-Path .
    $fullPath = Join-Path $currentDirectory $fileName
    if ([IO.File]::Exists($fullPath)) {
        # 要求されたファイル名が実際に存在する場合
        $extension = $(Get-Item $fullPath).Extension.Replace(".", "")
        if ($ContentType.ContainsKey($extension)) {
            # 拡張子が事前定義したコンテンツタイプ辞書にある場合
            $response.Value.ContentType = $ContentType[$extension]
        } else {
            # コンテンツタイプが拡張子から特定できない場合、octet-streamをセット
            $response.Value.ContentType = $ContentType["*"]
        }
        $response.Value.ContentType += ";charset=UTF-8"
        $content = [IO.File]::ReadAllBytes($fullPath)
        $response.Value.ContentLength64 = $content.Length
        $output = $response.Value.OutputStream
        $output.Write($content, 0, $content.Length)
        $output.Close()
    } else {
        $response.Value.StatusCode = 404
    }
}

function main {
    $listener = New-Object Net.HttpListener
    $listener.Prefixes.Add($url)
    Write-Output "* Running on $url (Press CTRL+C to quit)"

    try {
        $listener.Start()
        while ($listener.IsListening) {
            $task = $listener.GetContextAsync()

            # Ctrl+Cを機能させる処理
            while ($task.AsyncWaitHandle.WaitOne(500) -eq $false) {}

            $context = $task.GetAwaiter().GetResult()
            If ($page = $context.Request.Url.LocalPath -eq "/") {
                # 最初のページ送信
                $page = $HomePage
            } else {
                # CSS,Javascriptなどのファイルが要求された場合
                $page = $context.Request.RawUrl
            }
            $response = $context.Response
            If ($context.Request.IsLocal) {
                # ローカルPCからの接続の場合
                fileSendToClient ([ref]$response) $page
            }
            Write-Output $context.Request.RawUrl
            Write-Output $context.Response
            $response.Close()
        }
    } catch {
        Write-Error($_.Exception)
    } finally {
        $listener.Close()
    }
}

main
exit
```

## 感想

functionにC#クラスなどのオブジェクトを参照渡しして値を取り出そうとしたら、
`引数名.Value.メソッド名`と書かないといけないことに気付き、脳を破壊された。

```bat
function 関数名([ref]$引数名) { $引数名.Value.ByeByePowerShell }
```

## 参考記事
1. [PowerShellでHTTPサーバー的なものを作る](https://qiita.com/koyoru1214/items/721e528c86ee2baff871)
1. [管理者権限のないプレーンなWindowsでWebサーバを立てる戦い](https://qiita.com/kikuchi/items/785631cb24dfe5fa8d52)