WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

サーバー側で応答コンテンツの取得 (CORE)

by WebSurfer 7. July 2022 16:30

ASP.NET Core Web アプリの応答ボディの文字列を、サーバー側でのログなどの目的で、取得する方法を書きます。先の記事「サーバー側で応答コンテンツの取得」の ASP.NET Core 版です。

応答ボディの文字列

.NET Framework 版 ASP.NET で利用した HttpModule、HttpResponse.Filter プロパティ、HttpResponse.OutputStream プロパティは ASP.NET Core ではサポートされていません。

代わりにカスタム MiddlewareHttpResponse.Body プロパティを使って同様なことを行います。

ただし、HttpResponse.Body プロパティで取得できる Stream は CanRead, CanSeek が false になっており読むことができないという問題があります。

その解決に、先の記事「HttpRequest.Body から読み取る方法 (CORE)」で書いた EnableBuffering メソッドが使えるかと思ったのですが、応答側 HttpResponse ではサポートされていませんでした。

やむを得ず、Middleware で next.Invoke の前に応答ボディ用の Stream を MemoryStream に差し替えて、next.Invoke の後で MemoryStream に書き込まれた応答ボディを取得するという手段を取りました。

そのサンプルコードは以下の通りです。デバッグ実行すると上の画像ように「出力」ウィンドウに応答ボディの文字列が取得できます。

namespace MvcCore6App3.Middleware
{
    public class ResponseContentLogMiddleware
    {
        private readonly RequestDelegate _next;

        public ResponseContentLogMiddleware(RequestDelegate next)
        {
            this._next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            // この例では Home/Privacy のみログを取るという前提
            if (context.Request.Path.ToString().Contains("/Home/Privacy"))
            {
                // 応答ボディの Stream を取得して保持
                Stream responseStream = context.Response.Body;

                // 応答ボディの文字列を取得する
                string bodyContent = "";

                try
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        // 応答ストリームを MemoryStream に差し替えて、それに
                        // 応答ボディを取得。_next.Invoke の前でないとダメ
                        context.Response.Body = memoryStream;

                        await _next.Invoke(context);

                        // この時点ではすでに MemoryStream には応答ボディは
                        // 書き込まれている

                        if (memoryStream.Length > 0L &&
                            memoryStream.CanRead &&
                            memoryStream.CanSeek)
                        {
                            memoryStream.Position = 0L;
                            var reader = new StreamReader(memoryStream);
                            bodyContent = await reader.ReadToEndAsync();

                            // 確認用(デバッグ実行で Visual Studio の「出力」
                            // ウィンドウに表示される)
                            System.Diagnostics.Debug.Write(bodyContent);

                            memoryStream.Position = 0L;

                            // MemoryStream に取得した応答ボディのバイト列を
                            // responseStream にコピー
                            await memoryStream.CopyToAsync(responseStream);
                        }
                    }
                }
                finally
                {
                    // 上のコードで MemoryStream に差し替えた応答ボディの
                    // Stream を元に戻す
                    context.Response.Body = responseStream;
                }
            }
            else
            {
                // 何もしない場合でも以下のコードは必須。これが無いとミドル
                // ウェアのチェーンの途中で止まってしまう
                await _next.Invoke(context);
            }
        }
    }
}

上の Middleware が動くようにするには Program.cs (.NET 5.0 以前では Startup.cs) での設定が必要です。以下のコードで「追記」とコメントした一行を追加します。

// ・・・前略・・・

app.UseAuthentication();
app.UseAuthorization();

// 追記
app.UseMiddleware<ResponseContentLogMiddleware>();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

HttpResponse.Body プロパティで取得できる Stream が読めないのは、HttpRequest.Body と同様に、"lightweight and performant as possible" ということを狙ってのことではないかと思います。

なので、上記のようなことをすると性能上の劣化が生じるかもしれません。どうしてもログが必要と言うような場合にとどめておいた方が良さそうな気がします。

Tags: , , ,

CORE

Android の HTTP 通信を Fiddler でキャプチャ

by WebSurfer 1. July 2022 12:56

Wi-Fi でネットワークに接続した Android スマホの HTTP 通信を、同じ LAN 内の PC にインストールした Fiddler Classic (Windows 専用の無償版) でキャプチャする方法を備忘録として書いておきます。

注: HTTP のみです。HTTPS は証明書の問題で警告が出ます。詳細は後述します。

Fiddler でキャプチャ

上の画像がその結果で、自分の Android スマホの Chrome で自分の HP に接続し、要求・応答をキャプチャした結果を表示したものです。

Fiddler はプロキシとして HTTP 通信をキャプチャするという仕組みになっています。なので、Android スマホの HTTP 通信がプロキシ (Fiddler) を通るように設定すれば、上の画像のように要求・応答が表示されるはずです。

その設定方法を以下に書きます。この記事を書いた時点での Windows OS, Fiddler, スマートフォンのバージョン等は以下の通りです。

  • Windows 10 Pro 64-bit 21H2
  • Fiddler Classic v5.0.20211.51073
  • スマートフォン Sony SO-04H, Android 8.0.0
  • Chrome 103.0.5060.70

最初に書きましたが、Android は Wi-Fi で LAN に接続、Fiddler は同じ LAN 内の PC にインストールしたものを使うということと、HTTP のみで HTTPS はダメという制約がありますので注意してください。

(1) Fiddler の設定

Windows PC の Fiddler に Android スマートフォンが接続できるように設定します。

Fiddler を立ち上げてメニューバーの[Tools]⇒[Options...]をクリックし下の Options ダイアログを表示します。

Fiddler のオプション設定

Options ダイアログの[Connections]タブを開いて[Allow remote computers to connect]にチェックを入れます。

(2) Android スマートフォンの設定

注: この記事で使ったスマートフォンは 2016 年のもので、今の機種では設定方法は変わっているかもしれません。プロキシの設定はどの機種でもできるはずなので、検索などで調べれ���機種固有の設定方法は分かると思います

まず[アプリ]⇒[設定]⇒[ネットワークとインターネット]⇒[Wi-Fi]とクリックしていって Wi-Fi 設定画面を表示し、接続に使っている項目 (この記事の例では Buffalo-G-E90) を長押しすると下の画像のメニューが現れるので[ネットワークを変更]をクリックします。

ネットワークを変更

デフォルトでは[詳細オプション]のプロキシが[設定しない]になっているはずですが、これを[手動]にすると、下の画像のように利用するプロキシを設定できるようになります。

プロキシ設定

上の画像の[プロキシのホスト名]に Fiddler をインストールしてある PC の IP アドレスを、[プロキシポート]に 8888 を入力し、[保存」をクリックすれば Android 側の設定は完了です。

以上で HTTP 通信であればこの記事の一番上の画像のように Android のブラウザで任意のサイトにアクセスして要求・応答をキャプチャできます。

HTTPS 通信はサーバー証明書問題で警告が出ます。これは Fiddler の証明書 DO_NOT_TRUST_FiddlerRoot が Android にはインストールされてないからだと思われます。

先の記事「Firefox で Fiddler を使う方法」で書きましたが、Fiddler をインストールした PC には OS の「信頼されたルート証明機関」の中に Fiddler の証明書が含まれるようになります。

Fiddler の証明書 DO_NOT_TRUST_FiddlerRoot

なので、Fiddler をインストールした PC では HTTPS 通信で証明書の問題は出ません。Fiddler の証明書が無い Android ではそうはいかないということのようです。

Android に Fiddler の証明書 DO_NOT_TRUST_FiddlerRoot がインストールできるのか、できたとして証明書の問題が解決され Fiddler でキャプチャできるようになるのかは調べてないので分かりません。今後の検討課題ということで・・・


【2022/7/4 追記】

上に書いた HTTPS 通信でサーバー証明書の問題で警告が出る件ですが、Telerik のサイトの記事 Configure Fiddler Classic for Android Devices に対応方法の説明がありました。

それによると "Ensure that you have installed and using BouncyCastle as a certificate generator" ということで、証明書は Bouncy Castle を使って生成したものを使わないとダメらしいです。

上のページからリンクが張ってある Understanding Fiddler Certificate Generators によると "On Windows, Fiddler includes the MakeCert and CertEnroll certificate generators by default" とのことで、Bouncy Castle を使うには別途ダウンロードしてインストールする必要があるそうです。(自分の Fiddler を調べてみたら CertEnroll となってました)

ダウンロードしてみようとそのページの Bouncy Castle Certificate Generator のリンク (http://www.fiddler2.com/r/?fiddlercertMaker) をクリックしたら「fiddlercertmaker.exe を安全にダウンロードすることはできません」と出て失敗します。

そのリンクの応答は 301 Moved Permanently となっています。応答ヘッダに指定されている Location の http://telerik-fiddler.s3.amazonaws.com/fiddler/addons/fiddlercertmaker.exe を直接ブラウザのアドレスバーに設定して要求をかけると fiddlercertmaker.exe というファイルがダウンロードできます。

fiddlercertmaker.exe が Fiddler を Update して Bouncy Castle 証明書を発行できるようにするインストーラーらしいです。

fiddlercertmaker.exe をインストールし、Fiddler Echo Service にアクセスし (Fiddler を起動してからブラウザのアドレスバーに http://<IP アドレス>:8888/ と入力して要求をかけると表示されます)、証明書をダウンロードして、Android インストールすれば HTTPS 通信もキャプチャできるようになる・・・

・・・はずですが、まだ未検証です。今のところ Android + Fiddler で HTTPS 通信をキャプチャする必要がないので、余計なことをしてトラブるのもアレですので。(笑)

どうしても HTTPS 通信をキャプチャする必要が出てきたらやってみてその結果を追記します。

Tags: ,

DevelopmentTools

.NET 6.0 WinForms アプリで Chart

by WebSurfer 4. June 2022 14:44

Visual Studio 2022 で作成する .NET 6.0 の Windows Forms アプリで Chart を使って、先の記事「WPF で Chart を表示」と同じ棒グラフを表示してみました。

.NET 6.0 WinForms アプリに Chart 表示

調べてみると System.Windows.Forms.DataVisualization は .NET Core/.NET ではサポートされてないようです。そこを何とかしようとすると、自力でライブラリに手を加えるなどするしかなさそうですがそれは自分には無理なので、先人が作ってくれたライブラリに頼ることにしました。

で、NuGet を探してみるとこの記事を書いた時点では Maikebing.System.Windows.Forms.DataVisualization と HIC.System.Windows.Forms.DataVisualization という 2 つのパッケージが見つかりました。

ただしいずれも .NET 5.0 用とのこと。一旦はそこで断念したのですが、試しに前者の NuGet パッケージをインストールしてやってみました。その結果が上の画像で、この記事に書いた範囲に限っては一応は期待通りの結果になっています。

詳しく検証はしていませんので .NET Framework 版と同じ機能がサポートされているか、実用上問題ないのか等は分かりませんが、せっかくなのでブログの記事として残しておくことにしました。

プロジェクトの作成に使ったのは Visual Studio Community 2022 のテンプレート「Windows フォームアプリ」で、フレームワークは .NET 6.0 です。

まず Maikebing.System.Windows.Forms.DataVisualization を NuGet からインストールします。この記事を書いた時点では v5.0.1 が最新でしたのでそれをインストールしました。

NuGet パッケージ

後者の Microsoft.Data.SqlClient は SQL Server からデータを取得して DataTable を作成するためのものです。以前は System.Data.SqlClient を使っていたのですが代えてみました。(後述しますが、おかげでトラブりました)

自動生成された Form1.cs のデザイン画面でツールボックスから Button をドラッグ&ドロップし Click イベントのハンドラを生成したら、Form1.cs のコード画面で以下のコードを実装します。先の記事「WPF で Chart を表示」のコードと同じです。これを実行した結果がこの記事の一番上の画像です。

using System.Data;
using Microsoft.Data.SqlClient;
using System.Windows.Forms.DataVisualization.Charting;

namespace WinFormsChart
{
    public partial class Form1 : Form
    {
        private Chart chart;

        public Form1()
        {
            InitializeComponent();

            this.chart = new Chart
            {
                Name = "chart1",
                Width = 600,
                DataSource = CreateDataTable().DefaultView
            };

            this.Controls.Add(chart);

            Legend legend = new Legend()
            {
                DockedToChartArea = "ChartArea1",
                IsDockedInsideChartArea = false,
                Name = "Legend1"
            };

            chart.Legends.Add(legend);

            Series series = new Series()
            {
                Name = "三吉",
                ChartType = SeriesChartType.StackedColumn,
                CustomProperties = "DrawingStyle=Cylinder",
                IsValueShownAsLabel = true,
                Label = "#PERCENT{P1}",
                Legend = "Legend1",
                XValueMember = "Month",
                YValueMembers = "三吉"
            };
            chart.Series.Add(series);

            series = new Series()
            {
                Name = "春日",
                ChartType = SeriesChartType.StackedColumn,
                CustomProperties = "DrawingStyle=Cylinder",
                IsValueShownAsLabel = true,
                Label = "#PERCENT{P1}",
                Legend = "Legend1",
                XValueMember = "Month",
                YValueMembers = "春日"
            };
            chart.Series.Add(series);

            series = new Series()
            {
                Name = "東雲",
                ChartType = SeriesChartType.StackedColumn,
                CustomProperties = "DrawingStyle=Cylinder",
                IsValueShownAsLabel = true,
                Label = "#PERCENT{P1}",
                Legend = "Legend1",
                XValueMember = "Month",
                YValueMembers = "東雲"
            };
            chart.Series.Add(series);

            series = new Series()
            {
                Name = "府中",
                ChartType = SeriesChartType.StackedColumn,
                CustomProperties = "DrawingStyle=Cylinder",
                IsValueShownAsLabel = true,
                Label = "#PERCENT{P1}",
                Legend = "Legend1",
                XValueMember = "Month",
                YValueMembers = "府中"
            };
            chart.Series.Add(series);

            series = new Series()
            {
                Name = "広島",
                ChartType = SeriesChartType.StackedColumn,
                CustomProperties = "DrawingStyle=Cylinder",
                IsValueShownAsLabel = true,
                Label = "#PERCENT{P1}",
                Legend = "Legend1",
                XValueMember = "Month",
                YValueMembers = "広島"
            };
            chart.Series.Add(series);

            ChartArea chartArea = new ChartArea()
            {
                Name = "ChartArea1",
                AxisY = new Axis() { Title = "売上高" },
                AxisX = new Axis() { Title = "売上月" }
            };
            chart.ChartAreas.Add(chartArea);
        }


        private DataTable CreateDataTable()
        {
            string connString = @"接続文字列";
            string selectQuery = @"SELECT Month, 
                SUM(CASE WHEN Name = N'三吉' THEN Sales ELSE 0 END) AS 三吉,
                SUM(CASE WHEN Name = N'春日' THEN Sales ELSE 0 END) AS 春日,
                SUM(CASE WHEN Name = N'東雲' THEN Sales ELSE 0 END) AS 東雲,
                SUM(CASE WHEN Name = N'府中' THEN Sales ELSE 0 END) AS 府中,
                SUM(CASE WHEN Name = N'広島' THEN Sales ELSE 0 END) AS 広島
                FROM Shop GROUP BY Month";

            using (SqlConnection connection = new SqlConnection(connString))
            {
                using (SqlCommand command = new SqlCommand(selectQuery, connection))
                {
                    SqlDataAdapter adapter = new SqlDataAdapter(command);
                    DataTable table = new DataTable();
                    adapter.Fill(table);
                    return table;
                }
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            SaveFileDialog saveFileDialog1 = new SaveFileDialog();

            saveFileDialog1.Filter = "BMP形式|*.bmp|JPEG形式|*.jpeg|PNG形式|*.png";
            saveFileDialog1.FilterIndex = 2;
            saveFileDialog1.RestoreDirectory = true;

            if (saveFileDialog1.ShowDialog() == DialogResult.OK)
            {
                string filename = saveFileDialog1.FileName;
                string ext = Path.GetExtension(filename).ToLower();
                ChartImageFormat format;
                if (ext == ".jpeg")
                {
                    format = ChartImageFormat.Jpeg;
                }
                else if (ext == ".png")
                {
                    format = ChartImageFormat.Png;
                }
                else if (ext == ".bmp")
                {
                    format = ChartImageFormat.Bmp;
                }
                else
                {
                    throw new InvalidOperationException();
                }
                this.chart.SaveImage(filename, format);
            }
        }
    }
}

最後に、この記事の Chart の話とは関係ないことですが、System.Data.SqlClient に代えて Microsoft.Data.SqlClient を使ってトラブったのでそれを忘れないように書いておきます。

同じ .NET6.0 フレームワークで上と全く同じコードで System.Data.SqlClient の時は何ら問題なかったのに、Microsoft.Data.SqlClient を使うと以下の例外がスローされます。

SqlClient の例外

Microsoft.Data.SqlClient.SqlException A connection was successfully established with the server, but then an error occurred during the login process. (provider: SSL Provider, error: 0 - 信頼されていない機関によって証明書チェーンが発行されました。)

上のエラーメッセージでググってみると Microsoft のドキュメント ログイン フェーズのエラー が見つかって、それには考えられる原因として「SQL Server で TLS 1.2 がサポートされていない」と書いてあります。

確かにその通りで、接続先の SQL Server 2012 のバージョンは 11.0.2100 で TSL 1.2 はサポートしてないです。

別の Microsoft のドキュメント New features in 4.0 によると v4.0 からデフォルトで暗号化を行うよう設定が変わったとのことで、それが影響しているようです。

接続先を LocalDB v13.0.4001 (TLS 1.2 がサポートされている SQL Server 2016 相当)に変更して上の問題は解決できました。

Tags: , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。その後ブログ2を追加し、ここはプログラミング関係、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  July 2022  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar