C#でHTTPSサーバ(Ver. HttpListener)

### 目次
1. 目的
2. System.Net.HttpListener とは
3. アプリのコード
4. 動作環境の設定手順
  1. HTTP.sys にサーバURLを登録
  2. ファイアーウォールに HTTP.sys を許可
  3. HTTP.sys にサーバ証明書とURLの関連を登録
5. 動作確認

### 目的
- C#でHTTPSサーバアプリを作成し、動作環境の設定を行う。
- 利用するライブラリは System.Net.HttpListener とする。
- System.Net.HttpListener を利用するため、コードよりはむしろ、動作環境の設定方法を残しておきたい。

### System.Net.HttpListener とは
[概要(MS-DOC)](https://docs.microsoft.com/ja-jp/dotnet/framework/network-programming/httplistener)によると、[HTTP.sys](https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/servers/httpsys?view=aspnetcore-2.2) というアプリ(カーネルモードドライバ)を利用し、HTTPパケットの送受信を行うクラス。
HTTP.sys がSSL通信、HTTP接続の管理、HTTPヘッダの解析を行ってくれる。
[API(MS-DOC)](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.httplistener?view=netframework-4.7.2)より、SSL証明書の追加には netsh コマンドを用いる必要がある。
WebSocket に関しては、Windows 8 以降では対応している。

### アプリのコード
```c#:Program.cs
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var server = new SampleServer();
            server.Start(
                "http://+:8080/sample/",
                "https://+:44300/sample/");
            char ch;
            while ((ch = Console.ReadKey().KeyChar) != 'q')
                ;
            server.Stop();
        }
    }

    class SampleServer
    {
        HttpListener listener;

        public void Start(params string[] prefixes)
        {
            listener = new HttpListener();
            foreach (var prefix in prefixes)
                listener.Prefixes.Add(prefix);
            listener.Start();
            listener.BeginGetContext(OnRequested, null);
        }

        void OnRequested(IAsyncResult ar)
        {
            if (!listener.IsListening)
                return;

            HttpListenerContext context = listener.EndGetContext(ar);
            listener.BeginGetContext(OnRequested, listener);

            try
            {
                if (ProcessGetRequest(context))
                    return;
                if (ProcessPostRequest(context))
                    return;
                if (ProcessWebSocketRequest(context))
                    return;
            }
            catch (Exception e)
            {
                ReturnInternalError(context.Response, e);
            }
        }

        static bool CanAccept(HttpMethod expected, string requested)
        {
            return string.Equals(expected.Method, requested, StringComparison.CurrentCultureIgnoreCase);
        }

        static bool ProcessGetRequest(HttpListenerContext context)
        {
            var request = context.Request;
            var response = context.Response;
            if (!CanAccept(HttpMethod.Get, request.HttpMethod) || request.IsWebSocketRequest)
                return false;

            response.StatusCode = (int)HttpStatusCode.OK;
            using (var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
                writer.WriteLine($"you have sent headers:\n{request.Headers}");
            response.Close();
            return true;
        }

        static bool ProcessPostRequest(HttpListenerContext context)
        {
            var request = context.Request;
            var response = context.Response;
            if (!CanAccept(HttpMethod.Post, request.HttpMethod))
                return false;

            response.StatusCode = (int)HttpStatusCode.OK;
            using (var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
                writer.WriteLine($"you have sent headers:\n{request.Headers}");
            response.Close();
            return true;
        }

        static bool ProcessWebSocketRequest(HttpListenerContext context)
        {
            if (!context.Request.IsWebSocketRequest)
                return false;

            WebSocket webSocket = context.AcceptWebSocketAsync(null).Result.WebSocket;
            ProcessReceivedMessage(webSocket, message =>
            {
                webSocket.SendAsync(
                    Encoding.UTF8.GetBytes($"you have sent message:\n{message}"),
                    WebSocketMessageType.Text,
                    true,
                    CancellationToken.None);
            });

            return true;
        }

        static async void ProcessReceivedMessage(WebSocket webSocket, Action<string> onMessage)
        {
            var buffer = new ArraySegment<byte>(new byte[1024]);
            while (webSocket.State == WebSocketState.Open)
            {
                WebSocketReceiveResult receiveResult = await webSocket.ReceiveAsync(
                    buffer,
                    CancellationToken.None);
                if (receiveResult.MessageType == WebSocketMessageType.Text)
                {
                    var message = Encoding.UTF8.GetString(
                        buffer
                            .Slice(0, receiveResult.Count)
                            .ToArray());
                    onMessage.Invoke(message);
                }
            }
        }

        static void ReturnInternalError(HttpListenerResponse response, Exception cause)
        {
            Console.Error.WriteLine(cause);
            response.StatusCode = (int)HttpStatusCode.InternalServerError;
            response.ContentType = "text/plain";
            try
            {
                using (var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
                    writer.Write(cause.ToString());
                response.Close();
            }
            catch (Exception e)
            {
                Console.Error.WriteLine(e);
                response.Abort();
            }
        }

        public void Stop()
        {
            listener.Stop();
            listener.Close();
        }
    }
}
```

### 動作環境の設定手順
1.HTTP.sys にサーバURLを登録
  サーバアプリを管理者権限で実行するなら必要ないが、一般ユーザで実行する場合はあらかじめHTTP.sysが監視するURLを登録しておく必要がある。
コマンドプロンプトから netsh コマンドを実行することになるが、コマンドプロンプトの起動は管理者として行うこと。
登録するURLは HttpListener.Prefixes.Add() に渡した値でよい。

```shell:cmd
> netsh http add urlacl url=http://+:8080/sample/ user=Everyone
> netsh http add urlacl url=https://+:44300/sample/ user=Everyone
```

2.ファイアーウォールに HTTP.sys を許可
  コントロールパネルの「Windows ファイアーウォール」>「詳細設定」>「受信の規則」に「新しい規則」を追加する。
新しい規則は system プログラムを許可するような規則とする。

3.HTTP.sys にサーバ証明書とURLの関連を登録
  管理者として起動したコマンドプロンプトから netsh コマンドを実行する。
サーバ証明書はあらかじめ登録しておく。pfx ファイルをエクスプローラから実行すればよい。その際、「保存場所」は「ローカルコンピュータ」にしておくこと。
その後、サーバ証明書の拇印を確認するため、mmc を起動し、スナップインに証明書を追加し、「証明書(ローカルコンピュータ)」>「個人」>「証明書」に表示される証明書を選択し、拇印を確認する。
URLは ワイルドカードが使えないので、具体的に指定する。
GUID も必要になるが、なんでもよい。

```shell:cmd
> netsh http add sslcert ipport=localhost:44300 certhash=<サーバ証明書の拇印> appid={00112233-4455-6677-8899-AABBCCDDEEFF}
> netsh http add sslcert ipport=127.0.0.1:44300 certhash=<サーバ証明書の拇印> appid={00112233-4455-6677-8899-AABBCCDDEEFF}
```

### 動作確認
- Get の確認方法
ブラウザで https://127.0.0.1:44300 にアクセスし、応答を確認する

- WebSocket の確認方法
ブラウザで https://www.websocket.org/echo.html にアクセスし、「Location」に 「wss://127.0.0.1:44300」を入力後、「Connect」を押下し、「Log」を確認する