コーヒー飲みながら仕事したい

仕事で使う技術的なことの備忘録とか


Wordpress に引っ越しました!

SSL/TLS の仕組みについて備忘録

備忘録と言いつつ、ほとんど参考サイトの掲載になるが、↓のサイトの「デジタル証明書の仕組み」の絵が非常に参考になります。

www.infraexpert.com

これで、認証局とか CSR(証明書要求)とかの登場人物がどういう関係かがわかります。最強です。感謝。

なお、自分なりに少し情報を整理してみます。
以前やった OpenSSL による SSL/TLS 環境の作成で出てきた拡張子とそれが何なのか?

tassi-yuzukko.hatenablog.com

  • ca.key認証局秘密鍵と公開鍵の対情報。オレオレ認証のときは自分で作るが、普通は認証局の中で厳密に保管されているはず。
  • ca.crt認証局ルート証明書。中身は認証局の公開鍵と認証局の情報をデジタル署名されたものが含まれている。これもオレオレ認証では自分で作るが、普通は認証局が公開している。
  • server.key:サーバーの秘密鍵と公開鍵の対情報。
  • server.csr:サーバーの証明書署名要求。中身はサーバーの公開鍵とコモンネーム等の各種証明書。各種証明書を認証局にデジタル署名してもらうと *.crt になる。
  • server.crt認証局によるサーバー証明書。中身はサーバー公開鍵と認証局によりディジタル署名されたサーバーの各種証明書。これは認証局の公開鍵で復号できる。

なお、↓も非常に参考になります。

d.hatena.ne.jp

mosquitto を Visual Studio でコンパイルする

githubに公開されている mosquitto のソースファイルを、Visual Studioコンパイルします。

前提

CMake を使用してソリューションファイルを生成

ここでは、CMake のインストール方法については言及しません。

mosquitto のソースファイルの用意

mosquitto のリポジトリgithub からクローンします。

CMake のビルド環境を用意

必要ファイルのダウンロード

Readmeに書いてある通り、以下をそれぞれ用意する必要があります。

環境変数の設定

ここで、ビルドを通すために、以下の通り環境変数を設定する必要があります。

  • OpenSSL
    • OPENSSL_ROOT_DIR という環境変数名で、OpenSSLの の展開フォルダを設定(例: c:\temp\OpenSSL-Win32\

CMake のビルドを実行

CMake を起動し、以下を指定し、 Configure ボタンを押す。

  • Where is the source code: に、 mosquitto の CMakeList.txt の存在するパス
  • Where to build the binaries: に、ソリューションを出力するフォルダパス

コンパイラに何を使うか聞かれるので、今回は Visual Studio 2010 を選択しました。

f:id:tassi-yuzukko:20180308164842p:plain

Finish ボタンを押すと、自動でコンパイルが走ります。

が、たぶん、 c-ares library not found とか言って怒られるので、画面上の WITH_SRVチェックボックスを外し、再度 Generate すします。

f:id:tassi-yuzukko:20180308164018p:plain

正常に終了したら、CMake のコンソール上に Generating done と表示されます。

すると、指定したフォルダに、 Visual Studio のソリューションファイルが出力されています。

参考

github.com

Visual Studio で mosquitto をビルドする

生成されたソリューションファイル(mosquitto.sln)を起動します。

そのままビルドしてもエラーになるので、以下のようにライブラリの設定をします。

OpenSSL の環境変数

system の Path 環境変数に、OpenSSLの の展開フォルダ\bin を設定します。(例: c:\temp\OpenSSL-Win32\bin\

pthreads の設定

pthreads を展開したフォルダから、以下のものをコピーしていきます。

ヘッダファイル

  • pthread.h
  • sched.h
  • semaphore.h

上記ファイルを、C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include へコピー

lib ファイル

  • pthreadVC2.lib
  • pthreadVCE2.lib
  • pthreadVSE2.lib

上記ファイルを、 C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib へコピー

dll ファイル

  • pthreadGC2.dll
  • pthreadGCE2.dll
  • pthreadVC2.dll
  • pthreadVCE2.dll *pthreadVSE2.dll

上記ファイルを、 C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin へコピー

参考

全ては時の中に… : 【VC++】スレッド(pThread)を利用する環境を整える

ビルド実行

上記でたぶん、ビルドできるはずです。

とりあえず mosquitto_sub をコンパイルしてみましたが、警告が出まくりますが、なんとか mosquitto_sub.exe が出力されました・・・

EMQ を使用して mqtt の通信をする

EMQ とは

EMQ とは、 Erlang で実装された mqtt ブローカーです。
mqtt ブローカーとしては mosquitto が有名ですが、以下の特徴があります。

  • mosquitto と比較しても、 EMQ もなかなか性能が良い(らしい)
  • mosquitto ではできない、ブローカーのクラスタを実現できる
  • Web ブラウザの GUI が提供されているので、クライアントの管理がしやすい

Document

英語ですが、こちらに公式ドキュメントが公開されています。

インストール

環境は、 Windows 10 64bit とします。

EMQ からダウンロードして、C直下にでも解凍します。解凍するだけで、OS へのインストール作業は不要です。

ブローカーの設定

とりあえずここでは、クラスタ設定は省略します。単一ブローカーのみの接続を想定します。

C:\emqttd\etc\emq.conf (C直下に解凍した場合)がブローカーの設定ファイルです。

IPアドレスの設定

以下を変更します。ここでは、ブローカーのIPアドレス192.168.0.1 を想定します。

設定キー名 変更前 変更後
node.name node.name = emq@127.0.0.1 node.name = emq@192.168.0.1
listener.tcp.external listener.tcp.external = 127.0.0.1:1883 listener.tcp.external = 192.168.100.106:1883
listener.tcp.internal listener.tcp.internal = 127.0.0.1:11883 listener.tcp.internal = 192.168.100.106

SSLの設定

↓に雑ですが記載しています。

tassi-yuzukko.hatenablog.com

ブローカーの起動

C:\emqttd フォルダに移動し、コマンドプロンプトで以下を実行します。

bin\emqttd console

最初の起動では、ファイアウォールの許可みたいなのが出るかもしれませんが、[OK] すればよいです。

ちょっと待つと、Erlang のウィンドウが表示されます。
ウィンドウ内で (emq@192.168.0.1)1> Load emq_mod_presence module successfully. と表示されれば、起動成功です。

f:id:tassi-yuzukko:20180308101429p:plain

Web ブラウザで確認

上記の起動方法でブローカーが正常に起動しているならば、http://localhost:18083/へアクセスするとダッシュボードが表示されます。

f:id:tassi-yuzukko:20180308101433p:plain

終了方法

Eranlg のウィンドウを閉じれば、同時にブローカーも停止します。

クラスタについて

今回は省略しましたが、クラスタに関しては、以下が参考になります。

qiita.com

EMQ で TLS 接続する

今回はEMQ(emqttd)を使用して、mqtt の TLS 接続をする方法。
EMQのインストール方法とかは割愛。

サーバーの公開鍵/暗号鍵の生成、それらのオレオレ認証

↓が参考になった。
基本的には前記事の mosquitto の場合と同じ。

medium.com

EMQ側の設定を変更

これも↑のサイトのまんまになるが、一つ気を付けないといけないのが、公開鍵の生成時に「CommonName」にブローカーの FQDN を設定する必要があること。
ここは mosquitto の場合と同じで、 CommonName とブローカーのホスト名が異なると、 TLS 接続できない。

mosquitto で OpenSSL を 用いて TLS 接続する

mosquitto をで TLS 接続する方法です。
OpenSSL を使用するのが簡単だと思うけど、ちょっと間が空くとすぐやり方を忘れてしまうので備忘録として残しておきます。

2018/03/11 追記/修正

いろいろ TLS について勉強してみると、言葉の使い方や理解が正しくない箇所が複数あったので修正しました。
そして、まだ勉強不足なので間違っている箇所がある可能性は十分あります・・・。

前提

  1. OS : Windows 10 64bit
  2. mqtt ブローカーもクライアントも mosquitto を使用する
  3. 秘密鍵/公開鍵やサーバー証明書の生成は、OpenSSL を使用して行う
  4. TLS するためのサーバー証明書は、自己認証局(オレオレ認証局)で実現する
  5. あらかじめ pfx 形式1のものが存在しており、それを使用して自己認証局の開局する
    (pfx ファイルのパスワードは把握しているものとする)

今回は 5. が特殊な条件ですが、現案件がこういう条件下なのでこれを前提で記述します。

pfx ファイルを秘密鍵/公開鍵情報とルート証明書に分離する

pfx ファイル(ca.pfx)を OpenSSL を使用して秘密鍵/公開鍵情報(ca.key)と自己認証局ルート証明書(ca.crt)に分離します。

# 秘密鍵/公開鍵の取り出し
$ openssl pkcs12 -in ca.pfx -nocerts -nodes -out ca.key
Enter Import Password: # ca.pfx ファイルのパスワードを入力
MAC verified OK # ca.key が出力される

# 自己認証局のルート証明書の取り出し
$ openssl pkcs12 -in ca.pfx -clcerts -nokeys -out ca.crt
Enter Import Password: # ca.pfx ファイルのパスワードを入力
MAC verified OK # ca.crt が出力される

mosquitto で TSL 接続を行う場合は、出来上がった crt ファイルをOSに「信頼できるルート証明書」として登録する必要はないので、事実上これで自己認証局の開局は済んだことになります。(mosquitto を起動する際に、ブローカーでもクライアントでも、起動引数にパスを付加することでこのルート証明書を使用します)

参考

blog.prophet.jp

サーバー(ブローカー)の秘密鍵/公開鍵を生成

ブローカーの TSL 接続用の秘密鍵/公開鍵として server.key を生成します。

# 秘密鍵/公開鍵生成
$ openssl genrsa -out server.key 2048

サーバー(ブローカー)のサーバー証明書署名要求の作成

「証明書署名要求」とは、公開鍵にコモンネーム(Common Name)等のサーバーの情報を付加したものです。

www.infraexpert.com

ブローカーの証明書署名要求として server.csr を生成します。

# 証明書署名要求の生成
$ openssl req -out server.csr -key server.key -new

いろいろ入力を促されますが、とりあえずはオレオレ認証なので Common Name (e.g. server FQDN or YOUR name) []: 以外は全て未入力(そのまま Enter )でよいです。
Common Name には、ブローカーの FQDN (ホスト名またはIPアドレス)を入力する必要があります。
もし正しく FQDN を入力しなかったら、Error: A TLS error occurred. と言われてブローカーと接続できません。

Common Name (コモンネーム)について追記

そもそも、SSL/TLS 接続を行う際に、クライアント側がサーバーを認証する(なりすましでないと特定する)手法が、サーバーへの接続文字列とコモンネームを比較することらしいです。
つまり、コモンネームが一致しないならば、相手が本物と特定できない(クライアントは相手がなりすましだと思わざるをえない)ので、それくらいコモンネームは重要です。

詳細は↓のサイトが非常に参考になります。

cspssl.jp

このサイトは SSL 接続の知識が体系的にまとめられていて、非常に勉強になります。

サーバー証明書の署名と発行

上記で生成したサーバー証明書署名要求を、自己認証局秘密鍵ca.key)でデジタル署名して、サーバー証明書を発行します。

# サーバー証明書の署名と発行
$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 36500

ここでは、有効期限を100年としています。(普通はこんなことしませんが、オレオレ認証なので・・・)

とりあえずここまでで、 OpenSSL を使用した TSL 接続用の鍵生成や署名は終了です。

参考

qiita.com

mosquitto.conf の設定

TSL 接続で使用する各証明書ファイルのパスを mosquitto.conf に追加します。
ファイルのどこに書いても良いそうです。

// C:\Program Files (x86)\mosquitto\mosquitto.conf
listener 8883
cafile   ./certs/ca.crt
certfile ./certs/server.crt
keyfile  ./certs/server.key

ここでは、上記で生成した鍵等を、 C:\Program Files (x86)\mosquitto\certs に入れているものとしています。

動作確認

# ブローカーの起動
$ mosquitto -v -c mosquitto.conf
1520414168: mosquitto version 1.4.12 (build date 29/05/2017 11:24:45.10) starting
1520414168: Config loaded from mosquitto.conf.
1520414168: Opening ipv6 listen socket on port 8883.
1520414168: Opening ipv4 listen socket on port 8883.
# mosquitto_sub の起動
$ mosquitto_sub -d -t test -h <BrokerのFQDN> -p 8883 --cafile ./certs/ca.crt
# mosquitto_pub の起動
$ mosquitto_pub -d -t test -m aaaaaa -h <BrokerのFQDN> -p 8883 --cafile ./certs/ca.crt

  1. 秘密鍵と公開鍵、サーバー証明書認証局の場合はルート証明書)をまとめて一つにし、パスワードにより暗号化されたもの

Protocol Buffers の C# 版で遊んでみる

Protocol Buffers のC#

有名どころでは、以下の2つがあるようです。

※ 追記:前者でも .proto ファイルを使用することができるみたいです。

今回の私が手掛ける案件では、後者のほうが適する(CのサービスとC#のサービスがやりとりする)ので、ここでは Google.Protobuf を主に取り上げます。

参考サイト

tnakamura.hatenablog.com

C# 版 protobuf (Google.Protobuf) の導入

githubのReadmeに書かれている手順で導入していきます。

条件

以下が条件です。

  • Visual Studio 2012 意向であること
  • .NET4.5 以降または .NET Core であること

Nuget

Google.Protobuf を使用するだけなら、 Google.Protobuf を Nuget すればよいです。
しかし、それに加えて .proto ファイルを使用してクラスファイルを生成するならば、 Google.Protobuf.Tools も Nuget する必要があります。

Google.Protobuf はライブラリなのだが、 Google.Protobuf.Tools はライブラリではなく、バイナリ(ptoroc.exe)が入っています。
ちなみに、 Nuget した際のダウンロード先は、私の場合はC:\Users\XXX\.nuget\packages\google.protobuf.tools\3.5.1\tools でした。
ソリューションファイル内にダウンロードされているものだと思っていたので、ちょっとはまってしまいました。

Google.Protobuf でチュートリアルする

とりあえず、公式のチュートリアルを実行してみます。
公式は英語なので、備忘録として意訳したやつを残しておきます。

.proto ファイルの用意

とりあえずチュートリアルで示されている addressbook.proto を使用します。これはgithubに掲載されているのだが、このまま使用するとエラーになる(import "google/protobuf/timestamp.proto" なんてねーよって言われる)ので、修正したのを↓に載せときます。

// [START declaration]
syntax = "proto3";
package tutorial;

// [END declaration]

// [START java_declaration]
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]

// [START csharp_declaration]
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
// [END csharp_declaration]

// [START messages]
message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}
// [END messages]

.proto ファイルからクラスファイルを生成する

Nuget した Google.Protobuf.Tools 内に含まれる tools\windows_x64\protoc.exe を使用して、addressbook.protoシリアライズ/デシリアライズ用のクラスファイルを生成しあmす。

protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto

上記を実行すると、Addressbook.cs$DST_DIR 内に生成されます。

Addressbook.cs の中身を見ると、なかなかキモくて焦るが、たぶん利用する側は中身を意識する必要はそんなになさそう)

生成されたクラスファイルと Google.Protobuf を使用して、シリアライズ/デシリアライズをやってみる

チュートリアルを参考に、以下のように動作確認用の Program.cs を作成して実行してみます。
実行結果、正しく動作していることが確認できました。

using System;
using System.IO;
using System.Text;
using Google.Protobuf;
using Google.Protobuf.Examples.AddressBook; // protoc.exeにより自動生成されたクラスの名前空間
using static Google.Protobuf.Examples.AddressBook.Person.Types; // C# 6 の書き方で、クラス内クラスを省略形式で記述することができるようになる(protobufとは関係なし)

namespace ProtobufCsharp
{
    class Program
    {
        static void Main(string[] args)
        {
            // AddressBook.csで定義されているPresonクラスを実体化する
            Person person = new Person
            {
                Id = 1234,
                Name = "Yamada Tarou",
                Email = "yamada@sample.com",
                Phones = { new PhoneNumber { Number = "555-4321", Type = PhoneType.Home },
                    new PhoneNumber { Number = "222-1111", Type = PhoneType.Work } }
            };

            // 文字列にシリアライズ
            var data = Serialize(person);

            // シリアライズした文字列を読み込んでデシリアライズする
            var someone = Deserialize<Person>(data);

            // 動作確認
            Console.WriteLine($"Id:{someone.Id}, Name:{someone.Name}, Email:{someone.Email}, " +
                $"Phones[0](Number:{someone.Phones[0].Number}, Type:{someone.Phones[0].Type}), " +
                $"Phones[1](Number:{someone.Phones[1].Number}, Type:{someone.Phones[1].Type})");

            Console.ReadKey();
        }

        static byte[] Serialize<T>(T obj) where T : IMessage<T>
        {
            using (var stream = new MemoryStream())
            {
                obj.WriteTo(stream);
                return stream.ToArray();
            }
        }

        static T Deserialize<T>(byte[] data) where T : IMessage<T>, new()
        {
            var parser = new MessageParser<T>(() => new T());
            return parser.ParseFrom(new MemoryStream(data));
        }
    }
}

解説

  • WriteTo(stream) メソッドで、Stream 型にシリアライズすることができる
  • Parser.ParseFrom(stream) メソッドで、Stream 型からデシリアライズすることができる
  • 実際、ネットワーク間でメッセージのやり取りをする場合は、文字列が都合がいい場合が多いので、その場合は MemoryStream 型を byte 配列に変換すればよい

とりあえず思ったより簡単に実行できました。
ただし、Stream 型を使用するシリアライズ/デシリアライズはちょっと面倒だ。直に byte 配列にできればいいのに。
ちょっと調査します。

直に byte 配列にシリアライズ/デシリアライズするようにサンプルを変更してみました。たぶんこれでいける。

Visual Studio 2017 で protobuf-c を試してみる

前回の続き。

tassi-yuzukko.hatenablog.com

もろもろの準備がやっとできたので、 Visual Studio 2017 で protobuf-c を試してみる。

前準備

あらかじめ下記内容で amessage.proto を用意しておく(前回生成済み)

syntax = "proto3";

message AMessage {
  int32 a=1; 
  int32 b=2;
}

そして、protoc-c --c_out=. amessage.proto で、以下のファイルを生成しておく(これも前回実施済み)

  • amessage.pb-c.c
  • amessage.pb-c.h

ソリューションの作成

上記のファイル2つと、 protobuf-c.c および protobuf-c.h をソリューションフォルダ内につっこむ。

この際、便宜上、以下を変更した。

amessage.pb-c.h

7:  --- #include <protobuf-c/protobuf-c.h>
7:  +++ #include "protobuf-c.h"    // protobuf-c.hをローカルに置いたため & <>の括弧ではアクセスできないため

protobuf-c.c

316:  --- return (-(uint32_t)v) * 2 - 1;
316:  +++ return ((uint32_t)(-v)) * 2 - 1;    // Visual Studio コンパイル時にエラーとなったため(符号なし型にマイナスをつけることができないみたい)

381:  --- return (-(uint64_t)v) * 2 - 1;
381:  +++ return ((uint64_t)(-v)) * 2 - 1;    // Visual Studio コンパイル時にエラーとなったため(符号なし型にマイナスをつけることができないみたい)

2413:  --- return -(v >> 1) - 1;
2413:  +++ return -1 * (v >> 1) - 1;    // Visual Studio コンパイル時にエラーとなったため(符号なし型にマイナスをつけることができないみたい)

2457:  --- return -(v >> 1) - 1;
2457:  +++ return -1 * (v >> 1) - 1;    // Visual Studio コンパイル時にエラーとなったため(符号なし型にマイナスをつけることができないみたい)

3147:  --- tmp.length_prefix_len = pref_len;
3147:  +++ tmp.length_prefix_len = (uint8_t)pref_len;    // 警告が出たので一応

注意

Visual Studio の環境では、これらのファイルをコンパイル対象とするために、明示的にソリューション(というかプロジェクト)構成配下に登録する必要がある。

Simple complete example チュートリアル実行

基本的に、Examples · protobuf-c/protobuf-c Wiki · GitHub を参照して、 protobuf-c の Simple complete example チュートリアルを実行してみる。

しかし以下を変更してやってみる。

結果、以下のようなファイルを用意した。

// AMessageSerialize.h

#pragma once
#include <windows.h>

BOOL AMessageSerialize(int argc, const char * argv[]);
// AMessageSerialize.c

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include "amessage.pb-c.h"
#include "AMessageSerialize.h"

static void MyWriteFile(void const * buf, size_t len, const char* filename);

BOOL AMessageSerialize(int argc, const char * argv[])
{
    AMessage msg = AMESSAGE__INIT; // AMessage
    void *buf;                     // Buffer to store serialized data
    unsigned len;                  // Length of serialized data

    if (argc != 2 && argc != 3)
    {   // Allow one or two integers
        fprintf(stderr, "usage: amessage a [b]\n");
        return FALSE;
    }

    msg.a = atoi(argv[1]);
    msg.b = atoi(argv[2]);
    len = amessage__get_packed_size(&msg);

    buf = malloc(len);
    amessage__pack(&msg, buf);

    fprintf(stderr, "Writing %d serialized bytes\n", len); // See the length of message
    MyWriteFile(buf, len, "test.txt"); // Write to stdout to allow direct command line piping

    free(buf); // Free the allocated serialized buffer
    return TRUE;
}

static void MyWriteFile(void const * buf, size_t len, const char* filename)
{
    FILE* fp;

    fopen_s(&fp, filename, "wb");
    if (fp == NULL) {
        fprintf(stderr, "failed to write file.");
        goto END;
    }

    fwrite(buf, len, 1, fp);

END:
    fclose(fp);

    return;
}
// AMessageDeserialize.h

#pragma once
#include <windows.h>

BOOL AMessageDeserialize();
// AMessageDeserialize.c

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include "amessage.pb-c.h"
#include "AMessageDeserialize.h"
#define MAX_MSG_SIZE 1024

static size_t
read_buffer(unsigned max_length, uint8_t *out, const char* filename)
{
    size_t cur_len = 0;
    size_t nread;
    FILE* fp;

    fopen_s(&fp, filename, "rb");
    if (fp == NULL) {
        fprintf(stderr, "failed to read file.");
        goto END;
    }

    while ((nread = fread(out + cur_len, 1, max_length - cur_len, fp)) != 0)
    {
        cur_len += nread;
        if (cur_len == max_length)
        {
            fprintf(stderr, "max message length exceeded\n");
            exit(1);
        }
    }

END:
    fclose(fp);

    return cur_len;
}


BOOL AMessageDeserialize()
{
    AMessage *msg;

    // Read packed message from standard-input.
    uint8_t buf[MAX_MSG_SIZE];
    size_t msg_len = read_buffer(MAX_MSG_SIZE, buf, "test.txt");

    // Unpack the message using protobuf-c.
    msg = amessage__unpack(NULL, msg_len, buf);
    if (msg == NULL)
    {
        fprintf(stderr, "error unpacking incoming message\n");
        return FALSE;
    }

    // display the message's fields.
    printf("Received: a=%d  b=%d \n", msg->a, msg->b);  // required field

    // Free the unpacked message
    amessage__free_unpacked(msg, NULL);
    return TRUE;
}
// main.c

#pragma once

#include <stdio.h>
#include "AMessageSerialize.h"
#include "AMessageDeserialize.h"

int main(int argc, const char * argv[])
{
    AMessageSerialize(argc, argv);
    AMessageDeserialize();

    system("pause");

    return 0;
}

実行結果

引数を 10 2 で実行してみると、以下になった。

Writing 4 serialized bytes
Received: a=10  b=2
続行するには何かキーを押してください . . .

んーたぶんできてるっぽい。
とりあえず目的達成。シリアライズとデシリアライズの方法も直感的だし、C言語でここまでできるのはかなり魅力的だなーと思いました。

ソースコード

一応、ソースコードを載せとく。