1. washikawau

    Posted

    washikawau
Changes in title
+C#でHTTPSサーバ(Ver. HttpListener)
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,214 @@
+### 目次
+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」を確認する