tech.guitarrapc.cóm

Technical updates

base64urlを扱えるNuGetライラブラリと.NET Core Global Toolを作りました(MicroBatchFrameworkも使ったよ)

最近 JWT を取り扱っているのですが、仕様上base64url フォーマットを頻繁に利用します。 C# で base64 というと、Convert.FromBase64String あたりですが、base64url にしてくれるような気の利いた仕組みはなく、入力がbase64url仕様に沿ってないとすぐに例外を吐いて使いにくさが目立ちます。*1

個人的には、base64url への変換、base64 と base64url の相互変換 をしてくれれば十分で、フォーマットもstring で上出来です。そこで、自分のJWT操作用に base64url / base64 の対応をするnuget ライブラリ、それとJWTをコンソール上で検証するために、CLIを .NET Core Global Toolとして作りました。

今回はその内容と、dotnet global tool (要はコンソールアプリ) で MicroBatchFramework を使えるようにフィードバックしてた話です。

目次

TL;DR;

C#CLIでbase64url をさくっと操作できます。

.NET Core のCLIもMicroBatchFrameworkで書きやすくなったのでオススメです。

GitHub

ライブラリと .NET Core Global Tool はnuget に置いてあります。普段使っており、npmなどほか言語実装との挙動チェックはしているので問題ないと思いますが、何かあればリポジトリまでお願いします。

github.com

今回、CLIを提供するにあたり.NET Core Global Tool を作りました。*2 当初自前コマンドライン処理で書いたのですが、MicroBatchFrameworkに改善フィードバックを送り続けた結果、CLIでも使いやすくなり MicroBatchFramework へ移行完了しました。

github.com

ゴールとなる使い心地

NuGet ライブラリ

文字列やバイト配列を受けて、utf8 *3 のbase64urlでエンコード/デコードします。また、base64url と base64 の文字列をお互いに切り替えます。普通です。

CLI

次のコマンド入力を満たしつつ、MicroBatchFramework で提供することを目指します。

base64urls [-version] [-help] [encode|decode|escape|unescape] [args]

これはnpm で提供されている b64-cli や base64-url-cli の base64url [encode|decode|escape|unescape|binarydecode] [input] がちょうどいい使い勝手のバランス、かつよく利用されているので、コマンド入力から想定されていない入力時のエンコード/デコードも含めてこの挙動に合わせました。*4

www.npmjs.com

www.npmjs.com

NuGet ライブラリ

.NET Standard 2.0で作っています。

www.nuget.org

仕様に沿ってもくもくと書くだけなので余り書くことがありません。ほとんどすべて1行の処理で、コメントを除くと全部で20行程度なことからも察しかと。

Base64UrlCore/Base64Url.cs at d0538fdb8aa7386b01a9644b2563b0f8b88c5d1d · guitarrapc/Base64UrlCore · GitHub

base64url では、エンコーディング時に文字列長が4の倍数となるようにパディング(=)を末尾追加するのですが、ショートハンドでこう書けます。*5

base64String.Length + (4 - base64String.Length % 4) % 4, '='

.NET Core Global Tool

.NET Core 2.2以上で作っています。実は .NET Core 2.1 でもいいのですが、私はもう 2.2 未満は作らないので上げておきます。*6

www.nuget.org

さて、.NET Core Global Tool はただの.NET Core なコンソールアプリで、配布、インストール、アップグレード、アンインストールをNuGet基盤を利用できます。

利用側は、.NET Core SDKがあればokすぐに使えます。

docs.microsoft.com

開発側は、.NET Core Console テンプレートで作ればokです。詳細は公式Docs で十分書かれているので参照してみるといいでしょう。

docs.microsoft.com

ポイントは、csproj にいれるPackAsToolです。また、私は base64urls とパッケージの名称でC# プロジェクトをわざわざ切りましたが、ToolCommandName を使うことでパッケージに別名を付けることができるので厳守したい方はこっちでどうぞ。*7

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>

    <PackAsTool>true</PackAsTool>
    <ToolCommandName>botsay</ToolCommandName>
    <PackageOutputPath>./nupkg</PackageOutputPath>

  </PropertyGroup>

</Project>

自前のコマンドパーサーで組んでみる

さて、 base64urls としてはじめにCLIを組んだときは自分で引数からコマンドまで書いていました。その時に書いていたコードを置いておきます。

Base64UrlCore/Program.cs at d0538fdb8aa7386b01a9644b2563b0f8b88c5d1d · guitarrapc/Base64UrlCore · GitHub

ごく少量のコマンドをサクッと処理すればいいだけだったので、MonoOptions なども使わずざくっとゴールと同じ挙動を作っています。引数を検査して、コマンドに導いて処理をするだけのよくある小規模なものです。

MicroBatchFramework をこの時使わなかったのは、バッチ用途に限定されており、CLIとして使うには違和感が大きかったためです。例えば次のことがはじめはできませんでした。

* `base64urls encode 入力` という、提供したいコマンド入力の提供はできない。
    * BatchBaseを継承してメソッドをたたくのですが、`-p params` 形式で解釈されるため、一般的なCLIとして提供するのは苦しいです。
* メソッドごとにコマンドにマッピングして提供したいが、`Class.Method` が強制される。
    * バッチとしてはいいのですが、CLIとしては困ります。
* helpとversionはGNUスタイルに沿うようにしたいが、任意のフォーマットでのパラメーター形式ができない
    * クロスプラットフォームで利用するので、GNU スタイルで、-h と -help、--help を提供したいもの対応できません。
* help はデフォルトの引数で、オーバーライドが許可されていない
    * どうしようもない
* コマンド入力なしの時に help を出したいがhelp をオーバーライドできず、独自内容になる。
    * バッチとしては理解して使う分にはいいのですが、CLIとしては初めて使う人にとってはヘルプになってないのでアウト
* 想定されていないコマンド入力時にヘルプを出したいがエラーに回される。
    * しょうがない

MicroBatchFramework で CLI を組む

CLIとしてはいまいち使いにくいことを作者にフィードバックしたところ、「メソッドに対してサブコマンドのマッピングが[Command("decode")]のようなフォーマットで可能になった」、「引数も([Option(0)]T param) で可能になった「と連絡が来ました。

つまり、base64urls decode 値 を次のように表現できるようになったということです。

        [Command("encode")]
        public void Encode([Option(0)]string input) => コマンド;

連絡を受けて、MicroBatchFramework に移行することにしたのが次のPRです。

github.com

MicroBatchFramework 0.45-beta3 の状態で組んでみたところ、次のようになりました。

Base64UrlCore/Program.cs at 8a36ea0f4692581fb65eb1516bc820b3b6ab5c07 · guitarrapc/Base64UrlCore · GitHub

自作コマンドと比較してみると、処理に集中できるようになってきているのが分かります。

左: MicroBatchFramework / 右 : Self command parser

一方で、次の課題が残っています。

* MicroBatchFramework のデフォルト引数である help の挙動をオーバーライドできない。
    * 引数段階で`help` を`-help` に差し替えている
* MicroBatchFramework のデフォルト引数である list の挙動をオーバーライドできない
    * 引数段階で`list` を`-help` に差し替えている
* `[Command]` 属性が複数のコマンドを受け付けない
    * `-v`、`-version`、`--version` や -helpのメソッドを冗長に組んでいる
* コマンドに引数を空で渡したときに、オーバーライドしたhelp が表示されない
* 誤ったコマンドを渡したときに、オーバーライドしたhelpが表示されない

これらは、0.4.5-beta9 のリリースで次のように改善されました。

* MicroBatchFramework のデフォルト引数である help の挙動をオーバーライドできない。
    * コマンドでオーバーライド可能になりました。
* MicroBatchFramework のデフォルト引数である list の挙動をオーバーライドできない
    * コマンドでオーバーライド可能になりました。
* `[Command]` 属性が複数のコマンドを受け付けない`
    * `[Command(new [] {"-v", "-version", "--version"})]` のようなフォーマットで指定できるようになりました。
* コマンドに引数を空で渡したときに、オーバーライドしたhelp が表示されない
    * 空の引数を渡したときにオーバーライドされたhelp が表示できるようになりました。

残りの課題は1つですが、これはすぐに対応はしないとのことだったので、受け付けるコマンドのホワイトリストを作って事前検査することで対応しました。

* 誤ったコマンドを渡したときに、オーバーライドしたhelpが表示されない

CLI のAPI、挙動的にはこの時点で自前コマンドからMicroBatchFrameworkで差し替え可能になりました。コードを比較するとかなりシンプルになっています。

左: MicroBatchFramework / 右 : Self command parser
残った課題は、0.4.5-beta10 で 対応されたので私がGolang や C# で書くCLI的には要件がすべて達成されています。各種処理がほぼ1行に収まりユニットテストと処理内容的にデバッガを挟む必要もないので Expression Methodに書き換えて、MicroBatchFramework への移行が完了しました。

Merge pull request #3 from guitarrapc/chore/ready_for_new_release · guitarrapc/Base64UrlCore@0a6d836 · GitHub

コードを比較してみましょう。自前コマンドパーサーの時から見ると、引数の取り扱いやヘルプにどうやって回すかを考えることなく、やりたい処理だけに集中できるようになっていることが分かるかと思います。特に、Mainメソッドが1行になったことで見通しが良くなり、とっつきやすくなっています。

左: MicroBatchFramework / 右 : Self command parser

まとめ

MicroBatchFramework は、私も注目しているGenericHost の仕組みをうまく活用しつつ、.NET Core でCLIを書くときも書きやすいレベルまで改善されたのでオススメです。*8

*1:NuGet を見ても使いやすいライブラリがなく、 Convert.FromBase64String と Convert.FromBase64String しているだけのものだったりしてカオス

*2:いろいろあるのどうかと思いつつ、あってもいいし、配布機構を考えたくなかった

*3:base64はutf8を想定していますが、一応任意のエンコーディングも

*4:私も自作しても時にこれを使っているので同じ体験がうれしい。

*5:採用していませんが

*6:下げたのをあげるのは大変なので初めから上げておくスタイル

*7:私はこういうところは気にしないというか、あえて合わせに行く方が後日分かりやすいので別名を避けています

*8:NuGet上はPreleaseなので注意です