Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

目次

  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)によると、HTTP.sys というアプリ(カーネルモードドライバ)を利用し、HTTPパケットの送受信を行うクラス。
HTTP.sys がSSL通信、HTTP接続の管理、HTTPヘッダの解析を行ってくれる。
API(MS-DOC)より、SSL証明書の追加には netsh コマンドを用いる必要がある。
WebSocket に関しては、Windows 8 以降では対応している。

アプリのコード

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() に渡した値でよい。

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 も必要になるが、なんでもよい。

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」を確認する

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした