diff --git a/.github/ISSUE_TEMPLATE/quality.md b/.github/ISSUE_TEMPLATE/quality.md index ef0a76c47..febaefe55 100644 --- a/.github/ISSUE_TEMPLATE/quality.md +++ b/.github/ISSUE_TEMPLATE/quality.md @@ -36,7 +36,7 @@ labels: quality improvement .\2_RunAllTests.ps1 .\3_SmokeTest.ps1 -合格の目安は 全ステップ OK / 8-8 差分 0 / 29-29。 +合格の目安は 全ステップ OK / 8-8 差分 0 / 30-30。 --> ## 利用者への影響 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 93bd39f6c..9496259b4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -25,7 +25,7 @@ .\2_RunAllTests.ps1 .\3_SmokeTest.ps1 -合格の目安は 全ステップ OK / 8-8 差分 0 / 29-29。 +合格の目安は 全ステップ OK / 8-8 差分 0 / 30-30。 文書だけの変更なら「不要」と書いてください。 --> diff --git a/root/programs/BUILDING.md b/root/programs/BUILDING.md index 1282f679a..708fd480a 100644 --- a/root/programs/BUILDING.md +++ b/root/programs/BUILDING.md @@ -738,7 +738,7 @@ VS 18 のある環境では従来どおり `18.0` になるため、**挙動は ### 実測(run 30984111639 : 全ステップ成功) > **当時の件数での記録。** その後、単体テストは 8 ケース(#520)、 -> 疎通は 29 件(#528、#546、#566、#570、#571、#578)である。**時間の目安として読むこと。** +> 疎通は 30 件(#528、#546、#566、#570、#571、#578、#580)である。**時間の目安として読むこと。** ### 警告の内訳を見る(`-WarnDetail`、#571) diff --git a/root/programs/CHEATSHEET.md b/root/programs/CHEATSHEET.md index b76f9fe55..afed4c7b1 100644 --- a/root/programs/CHEATSHEET.md +++ b/root/programs/CHEATSHEET.md @@ -26,7 +26,7 @@ cd root\programs |---|---|---| | ビルド | 全ステップ OK | [`BUILDING.md`](BUILDING.md) | | 単体テスト | 8/8 OK、差分 0 | [`TESTING.md`](TESTING.md) | -| 疎通 | 29/29 OK | [`SMOKETEST.md`](SMOKETEST.md) | +| 疎通 | 30/30 OK | [`SMOKETEST.md`](SMOKETEST.md) | **既定は C# 側。VB 側は `-Lang` で回す**(3 節)。 diff --git a/root/programs/CONFIGURATION.md b/root/programs/CONFIGURATION.md index b00222b20..27e7e9340 100644 --- a/root/programs/CONFIGURATION.md +++ b/root/programs/CONFIGURATION.md @@ -557,7 +557,7 @@ cd root\programs | 見たいもの | 場所 | |---|---| | net10.0 の Web(本番向けの切り替えを含む) | `CS/Samples4NetCore/Backend/MVC_Sample/MVC_Sample/appsettings.json` | -| 環境変数・`%変数%`・環境別ファイルの実運用 | `CS/Samples4NetCore/Docker/`([`README.md`](CS/Samples4NetCore/Docker/README.md)) | +| 環境変数・`%変数%`・環境別ファイルの実運用 | `CS/Samples4NetCore/Docker/`(対象ごとに分かれる。[`MVC_Sample`](CS/Samples4NetCore/Docker/MVC_Sample/README.md) / [`ASPNETWebService`](CS/Samples4NetCore/Docker/ASPNETWebService/README.md)) | | net48 の Web(`machineKey` / 転送ヘッダの注記) | `CS/Samples/WebApp_sample/MVC_Sample/MVC_Sample/Web.config` | | 相対パスで自己完結させる書き方 | `CS/Samples4NetCore/Legacy/Bat_sample/*/appsettings.json` | | 領域ごとの事情 | 各 `ANALYSIS.md`([Frameworks](CS/Frameworks/ANALYSIS.md) / [Samples](CS/Samples/ANALYSIS.md) / [Samples4NetCore](CS/Samples4NetCore/ANALYSIS.md)) | diff --git a/root/programs/CS/Samples4NetCore/ANALYSIS.md b/root/programs/CS/Samples4NetCore/ANALYSIS.md index 2cad73021..1fb4200ad 100644 --- a/root/programs/CS/Samples4NetCore/ANALYSIS.md +++ b/root/programs/CS/Samples4NetCore/ANALYSIS.md @@ -17,7 +17,7 @@ Open棟梁の **.NET (Core) 系サンプル**。4 つの区画に分かれてお | **`Backend/`** | `MVC_Sample`(ASP.NET Core MVC) | **現行の推奨サンプル。** ここが本命 | | **`Frontend/`** | README のみ | 別リポジトリ `FrontendTemplates` へ移動済み | | **`Legacy/`** | `2CS_sample` / `Bat_sample` / `CLI_sample` / `WS_sample` | `../Samples`(net48)を .NET へ移植した**独立コピー**。名前どおり「レガシー」扱い | -| **`Docker/`** | `Backend/MVC_Sample` を Linux コンテナで動かす一式 | サンプルのコードは持たない。**手順と設計は [`Docker/README.md`](Docker/README.md)**(#548) | +| **`Docker/`** | `Backend/` のサンプルを Linux コンテナで動かす一式 | **対象ごとに分かれる**(`MVC_Sample` / `ASPNETWebService`)。サンプルのコードは持たない。**手順と設計は各 `README.md`**(#548、#582) | **全プロジェクトが SDK 形式 csproj / `net10.0`(GUI は `net10.0-windows7.0`)。** @@ -364,10 +364,17 @@ net48 側はコードから変えられない(`Request.IsSecureConnection` は **IIS + ARR の URL Rewrite で `HTTPS` サーバ変数を立てる** (`Samples/WebApp_sample/MVC_Sample` の `Web.config` にコメントで記載)。 -### 5-3. コンテナで動かす場合は `Docker/` を見る(#548) +### 5-3. コンテナで動かす場合は `Docker/` を見る(#548、#582) -`Backend/MVC_Sample` を Linux コンテナで動かす一式が `Docker/` にある。 -**手順・設計・落とし穴は [`Docker/README.md`](Docker/README.md) が一次情報**で、ここには書かない。 +`Backend/` のサンプルを Linux コンテナで動かす一式が `Docker/` にある。 +**対象ごとにフォルダが分かれる。** + +| 対象 | 一式 | +|---|---| +| `Backend/MVC_Sample` | [`Docker/MVC_Sample/`](Docker/MVC_Sample/README.md) | +| `Backend/ASPNETWebService` | [`Docker/ASPNETWebService/`](Docker/ASPNETWebService/README.md) | + +**手順・設計・落とし穴は各 `README.md` が一次情報**で、ここには書かない。 上の 5-1 / 5-2 で書いた `%OT_RESOURCE_ROOT%` と 3 つの切り替えを、実際に使っている (`appsettings.Container.json` + `docker-compose.yml` の環境変数)。 diff --git a/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/ASPNETWebService.csproj b/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/ASPNETWebService.csproj index c64500833..5d27fe10e 100644 --- a/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/ASPNETWebService.csproj +++ b/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/ASPNETWebService.csproj @@ -7,6 +7,10 @@ + + + diff --git a/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/Startup.cs b/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/Startup.cs index b773ddb32..b22ef5f5d 100644 --- a/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/Startup.cs +++ b/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/Startup.cs @@ -15,23 +15,91 @@ //* 日時 更新者 内容 //* ---------- ---------------- ------------------------------------------------- //* 20xx/xx/xx XX XX XXXX +//* 2026/08/21 玄人 幸道 OpenAPI(IDL)のドキュメント生成に対応(#580) +//* 2026/08/21 玄人 幸道 MVC_Sample と同じ構成に揃えた(#582)。 +//* 環境を移しても動く形(#541)と、転送ヘッダ(#549)。 +//* **Cookie を使わないため、Cookie 由来の設定は +//* コメントアウトで残す**(必要になったときの手本)。 //********************************************************************************** +// <MVC_Sample との関係>(#582) +// +// **この Startup.cs は Backend/MVC_Sample/MVC_Sample/Startup.cs と +// diff が取れる形にしてある。** 骨格を揃え、 +// Resource Server で使わないものはコメントアウトで残している。 +// +// Resource Server は **OAuth2 の Bearer ヘッダ認証**であり、 +// Cookie 認証・セッション・静的ファイル・Razor を使わない。 +// そのため次は無効にしてある。**消していないのは、 +// 利用者がこのテンプレートから Cookie 認証へ広げるときの手本になるためである。** +// +// UseCookiePolicy / CookiePolicyOptions / UseSession / AddSession +// AddDistributedMemoryCache / AddAuthentication + AddCookie +// AddDataProtection(CookieSecurePolicy / DataProtectionKeyPath) +// UseStaticFiles / MapRazorPages +// +// 逆に、こちらにしか無いのは CORS と OpenAPI である。 + +// <設定で切り替えるもの>(#541) +// +// 本番でだけ有効にしたい設定を「コメントアウトして置いておく」と、 +// 環境を移すときにソースを書き換えることになる。 +// appsettings.json(および環境変数)で切り替えられるようにしてある。 +// +// | キー(appSettings) | 既定 | on にすると | +// |----------------------------------|--------|-------------| +// | UseHttpsRedirection | off | HTTP を HTTPS へリダイレクトする | +// | CookieSecurePolicy | (空) | always で Cookie に Secure 属性を必ず付ける | +// | DataProtectionKeyPath | (空) | データ保護の鍵を、指定フォルダに永続化する | +// | UseForwardedHeaders | off | X-Forwarded-Proto / -For を取り込む(#549) | +// | ForwardedHeadersKnownProxies | (空) | 信用する前段のアドレス(空=範囲を制限しない) | +// +// **既定値は、いずれも従来どおりの動作**である。 +// +// **CookieSecurePolicy と DataProtectionKeyPath は、この Resource Server では効かない。** +// Cookie を使わないため。表に残してあるのは MVC_Sample と揃えるためで、 +// Cookie 認証を足すときに合わせて有効化する(#582)。 +// +// <環境変数で上書きできる> +// +// Host.CreateDefaultBuilder が環境変数を構成に含めるため、 +// appsettings.json の値は環境変数で上書きできる。区切りは「__」(下線 2 つ)。 +// +// appSettings__UseHttpsRedirection=on +// appSettings__FxXMLSPDefinition=/app/files/resource/Xml/SPDefinition.xml +// connectionStrings__ConnectionString_SQL=... +// +// **FxContainerization は要らない。** あちらは「接頭辞なしのキー名」で +// 環境変数を読む別の仕組みで、ON にしたときだけ効く。 + +using System; +using System.IO; +using System.Net; + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.CookiePolicy; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.DataProtection; + +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Caching.Memory; -// using Microsoft.AspNetCore.Mvc.Cors.Internal; +//using Microsoft.AspNetCore.Mvc.Cors.Internal; using Touryo.Infrastructure.Framework.StdMigration; using Touryo.Infrastructure.Public.Util; + namespace ASPNETWebService { /// @@ -70,26 +138,163 @@ public Startup(IConfiguration configuration) /// public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + // 転送ヘッダの取り込み(#549) + // + // **リバース プロキシで TLS を終端すると、アプリから見た接続は HTTP になる。** + // 利用者のブラウザは HTTPS で繋いでいるのに Request.IsHttps は false のままで、 + // Secure 属性が付かない(#536 でフレームワークが立てるようにした分が効かない)。 + // + // 前段が付ける X-Forwarded-Proto を取り込むと、IsHttps が正しくなる。 + // + // **必ずパイプラインの先頭に置く。** + // 後ろに置くと、それより前のミドルウェア(UseHttpsRedirection など)が + // 取り込み前のスキームを見てしまう。 + // + // 既定は off。素の HTTP で動かす開発環境では、転送ヘッダを + // 誰でも付けられる(=クライアントが詐称できる)ため。 + // + // **Cookie を使わない Resource Server でも要る。**(#582) + // HTTPS へのリダイレクト判定と、ログに残る接続元アドレスが変わる。 + if (Startup.IsOn("UseForwardedHeaders")) + { + ForwardedHeadersOptions options = new ForwardedHeadersOptions() + { + ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor + }; + + // 信用する前段を指定する。 + // + // **既定ではループバックからの転送しか信用しない。** + // コンテナや Kubernetes では前段が別アドレスになるため、 + // **指定しないとヘッダが黙って捨てられ、何も起きない。** + // 「on にしたのに直らない」の原因はほぼこれである。 + // + // ForwardedHeadersKnownProxies に、前段のアドレスをカンマ区切りで書く。 + string knownProxies = Startup.GetValue("ForwardedHeadersKnownProxies"); + + if (string.IsNullOrEmpty(knownProxies)) + { + // **前段を特定できない場合(コンテナ等)は、範囲の制限を外す。** + // KnownIPNetworks / KnownProxies を空にすると、 + // 「どこからの転送でも信用する」という意味になる。 + // (KnownNetworks は .NET 10 で非推奨。KnownIPNetworks を使う) + // + // **アプリが前段を経由せず直接叩ける状態では使わないこと。** + // クライアントが X-Forwarded-Proto を詐称でき、 + // HTTP で来ているのに HTTPS だと判断させられる。 + // 前段だけが到達できるネットワークに閉じてから使う。 + options.KnownIPNetworks.Clear(); + options.KnownProxies.Clear(); + } + else + { + options.KnownIPNetworks.Clear(); + options.KnownProxies.Clear(); + + foreach (string ip in knownProxies.Split(',')) + { + string trimmed = ip.Trim(); + + if (!string.IsNullOrEmpty(trimmed)) + { + options.KnownProxies.Add(IPAddress.Parse(trimmed)); + } + } + } + + app.UseForwardedHeaders(options); + } + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { + // **画面を持たないため、エラー画面へは飛ばさない。**(#582) + // MVC_Sample は app.UseExceptionHandler("/Home/Error") を使う。 + // WebAPI では、例外はフレームワーク(MyBaseAsyncApiController)が + // 捕捉して JSON で返す。 + //app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. // You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); - //app.UseHttpsRedirection(); + } + + // HTTPS へのリダイレクト(#541) + // + // **既定は off。** 平文 HTTP で動かす環境(疎通確認や、 + // TLS を前段のリバース プロキシで終端する構成)でリダイレクトすると、 + // 到達できなくなるため。 + // TLS を自分で終端するなら on にする。 + // + // **on にするだけでは足りない。リダイレクト先のポートも要る。** + // 決められないと、ミドルウェアは警告を出すだけで素通りする + // (見落としやすい。有効にしたのに HTTP のまま通ってしまう)。 + // + // warn: ...HttpsRedirectionMiddleware[3] + // Failed to determine the https port for redirect. + // + // ポートは、次のいずれかで決まる。 + // ・https の URL を Kestrel にバインドする(--urls に https://... を含める) + // ・環境変数 ASPNETCORE_HTTPS_PORT=443(単数) + // ・環境変数 HTTPS_PORT=443 + // + // **ASPNETCORE_HTTPS_PORTS(複数)では決まらない。** + // あちらは Kestrel が「どのポートで待ち受けるか」を決めるもので、 + // このミドルウェアが読むのは単数形の方である。紛らわしいので注意。 + if (Startup.IsOn("UseHttpsRedirection")) + { + app.UseHttpsRedirection(); } // HttpContextのマイグレーション用 app._UseHttpContextAccessor(); + // **静的ファイルは持たない。**(#582) + // WebAPI なので /wwwroot が無い。 + //app.UseStaticFiles(); + + // **Cookie を使わない。**(#582) + // Cookie ポリシーは ConfigureServices 側ごとコメントアウトしてある。 + //app.UseCookiePolicy(); + + // **セッションを使わない。**(#582) + // Bearer トークンで都度認証するため、サーバ側に状態を持たない。 + //app.UseSession(new SessionOptions() + //{ + // IdleTimeout = TimeSpan.FromMinutes(30), // ここで調整 + // IOTimeout = TimeSpan.FromSeconds(30), + // Cookie = new CookieBuilder() + // { + // HttpOnly = true, + // Name = "ws_session", + // Path = "/", + // SameSite = SameSiteMode.Strict, + // SecurePolicy = CookieSecurePolicy.SameAsRequest + // } + //}); + + // Routing app.UseRouting(); + // Identity + // Identityではなく、CookieAuthentication + // + // **Resource Server は Bearer ヘッダ認証である。**(#582) + // 認証は MyBaseAsyncApiController の属性 + // (EnumHttpAuthHeader.Bearer)が行うため、 + // ここでミドルウェアを挟まない。 //app.UseAuthentication(); //app.UseAuthorization(); + // CORS(**こちらにしか無い**) + // + // ブラウザから直接叩ける Resource Server として公開するため。 + // **AllowCredentials は付けない。** + // AllowAnyOrigin とは同時に指定できず、 + // 資格情報を送らない構成として一貫させている。 app.UseCors( //認証・認可の後ろ builder => builder .AllowAnyOrigin() @@ -98,9 +303,18 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) //.AllowCredentials()); + // Routingの設定 app.UseEndpoints(endpoints => { endpoints.MapControllers(); + + // OpenAPI(IDL)を返す(#580) + // **開発時だけに絞っていない。** + // Resource Server のテンプレートとして、 + // 利用側が IDL を引けることに意味があるため。 + endpoints.MapOpenApi(); + + //endpoints.MapRazorPages(); }); } @@ -117,6 +331,40 @@ public void ConfigureServices(IServiceCollection services) // HttpContextのマイグレーション用 services._AddHttpContextAccessor(); + // Cookie ポリシー(#541) + // + // **Cookie を使わないため無効にしてある。**(#582) + // Cookie 認証を足すときは、ここと Configure 側の + // app.UseCookiePolicy() を合わせて有効にする。 + //services.Configure(options => + //{ + // options.HttpOnly = HttpOnlyPolicy.Always; + // options.MinimumSameSitePolicy = SameSiteMode.Strict; + // + // // Cookie の Secure 属性(既定は空=各 Cookie の設定に従う) + // // + // // TLS で公開するなら always にする。 + // // 平文 HTTP の環境で always にすると、**Cookie が送られず + // // ログインできなくなる**ので、既定では変えない。 + // if (Startup.GetValue("CookieSecurePolicy").ToLower() == "always") + // { + // options.Secure = CookieSecurePolicy.Always; + // } + //}); + + // Sessionのモード + // + // **セッションを使わないため無効にしてある。**(#582) + // Bearer トークンで都度認証する。 + // セッションを足すなら、AddDistributedMemoryCache が + // プロセス内に持つ点(#541)に注意すること。 + //services.AddDistributedMemoryCache(); // 開発用 + //services.AddDistributedSqlServerCache(); // Microsoft.Extensions.Caching.SqlServer + //services.AddStackExchangeRedisCache(); // Microsoft.Extensions.Caching.StackExchangeRedis + + // Sessionを使用する。 + //services.AddSession(); + // Core 3.0のテンプレートではUseMvcの // 代わりにこれらを使用するようになった。 services @@ -125,6 +373,101 @@ public void ConfigureServices(IServiceCollection services) //services.AddControllersWithViews(); // MVC & WebAPI // services.AddRazorPages(); // Razor Page + + // OpenAPI(IDL)のドキュメント生成(#580) + // + // **.NET 9 以降は標準で入っている。** Swashbuckle は要らない。 + // 既定では /openapi/v1.json で返る。 + services.AddOpenApi(); + + #region Add Frameworks + + // 一般的な Webアプリでは、 + // EF, Identity, MVC などのミドルウェア サービスを登録する。 + // ミドルウェアの実行順序は、IStartupFilter の登録順に設定される。 + + // EF + //services.AddDbContext(options => + // options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + + // Identity + //services.AddIdentity() + // .AddEntityFrameworkStores() + // .AddDefaultTokenProviders(); + + // Add application services. + //services.AddTransient(); + //services.AddTransient(); + + // **AddMvc は要らない。**(#582) + // ビューを持たないため、上の AddControllers で足りる。 + //services.AddMvc(); + + // Forms認証 + // + // **Resource Server は Bearer ヘッダ認証である。**(#582) + // Cookie 認証を足すときに有効にする。 + //services.AddAuthentication(options => + //{ + // options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; + // options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + // options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + //}) + //.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => + //{ + // options.LoginPath = new PathString("/Home/Login"); + // options.AccessDeniedPath = new PathString(GetConfigParameter.GetConfigValue("FxErrorScreenPath")); + // options.ReturnUrlParameter = "ReturnUrl"; + // options.ExpireTimeSpan = TimeSpan.FromHours(1); + // options.SlidingExpiration = true; + // options.Cookie.HttpOnly = true; + //}); + + // データ保護の鍵の置き場所(#541) + // + // 認証 Cookie・セッション・Antiforgery トークンは、この鍵で保護されている。 + // **鍵が変わると、既存の Cookie が読めなくなり、ログインし直しになる。** + // + // **この Resource Server では、いずれも使っていない。**(#582) + // Cookie 認証やセッションを足すときに、合わせて有効にする。 + // コンテナでは**必ず要る**(作り直すたびに鍵が変わるため)。 + //string keyPath = Startup.GetValue("DataProtectionKeyPath"); + // + //if (!string.IsNullOrEmpty(keyPath)) + //{ + // services.AddDataProtection() + // .PersistKeysToFileSystem(new DirectoryInfo(keyPath)) + // // **アプリケーション名も固定する。** + // // 既定ではコンテンツ ルートのパスから決まるため、 + // // 鍵を共有していても**配置先のパスが違うと復号できない**。 + // .SetApplicationName("ASPNETWebService"); + //} + + #endregion + } + + #endregion + + #region 設定の読み取り + + /// appSettings の値を取得する(無ければ空文字) + /// キー + /// + /// + /// 環境変数 appSettings__<キー> でも上書きできる(#541)。 + /// Host.CreateDefaultBuilder が環境変数を構成に含めるため。 + /// + private static string GetValue(string key) + { + return GetConfigParameter.GetConfigValue(key) ?? ""; + } + + /// appSettings の値が on かを判定する + /// キー + /// on なら true + private static bool IsOn(string key) + { + return Startup.GetValue(key).ToLower() == "on"; } #endregion diff --git a/root/programs/CS/Samples4NetCore/Docker/.dockerignore b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/.dockerignore similarity index 100% rename from root/programs/CS/Samples4NetCore/Docker/.dockerignore rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/.dockerignore diff --git a/root/programs/CS/Samples4NetCore/Docker/.gitignore b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/.gitignore similarity index 100% rename from root/programs/CS/Samples4NetCore/Docker/.gitignore rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/.gitignore diff --git a/root/programs/CS/Samples4NetCore/Docker/0_SetupCert.ps1 b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/0_SetupCert.ps1 similarity index 100% rename from root/programs/CS/Samples4NetCore/Docker/0_SetupCert.ps1 rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/0_SetupCert.ps1 diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/1_PublishAndUp.bat b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/1_PublishAndUp.bat new file mode 100644 index 000000000..8ef2e9cfa --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/1_PublishAndUp.bat @@ -0,0 +1,66 @@ +@echo off +rem NOTE: keep this file pure ASCII (root/programs/CODING.md section 4). +rem Japanese explanations are in README.md. +rem +rem Host-build type: publish on the host, then copy into the runtime image. +rem See README.md and #582. + +setlocal +pushd "%~dp0" + +set SLN_DIR=..\..\Backend\ASPNETWebService\ASPNETWebService +set FX_DLL=..\..\..\Frameworks\Infrastructure\Build_netcore100\net10.0\OpenTouryo.Framework.dll + +echo === 1/4 : checking prerequisites === + +if not exist "%FX_DLL%" ( + echo [ERROR] Framework is not built: %FX_DLL% + echo Build the framework first ^(root\programs\1_BuildAll.ps1^). + goto :fail +) + +if not exist ".\https\aspnetapp.pem" ( + echo [ERROR] .\https\aspnetapp.pem not found. + echo Run 0_SetupCert.ps1 first. + goto :fail +) + +docker network inspect common_link >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] docker network "common_link" not found. + echo Start LocalServicesOnDocker first ^(it owns the network and the DB^). + echo https://github.com/NetDevInfraWGinOSSConsortium/LocalServicesOnDocker + goto :fail +) + +echo === 2/4 : dotnet publish === +if exist ".\publish" rmdir /s /q ".\publish" +dotnet publish "%SLN_DIR%\ASPNETWebService.csproj" -c Release -o ".\publish" +if errorlevel 1 goto :fail + +if not exist ".\publish\ASPNETWebService.dll" ( + echo [ERROR] publish output not found: .\publish\ASPNETWebService.dll + goto :fail +) + +echo === 3/4 : docker compose up --build === +docker compose up --build -d +if errorlevel 1 goto :fail + +echo === 4/4 : done === +echo HTTP : http://localhost:8090 ^(redirects to HTTPS^) +echo HTTPS : https://localhost:8091 +echo IDL : https://localhost:8091/openapi/v1.json +echo. +echo logs : docker compose logs -f +echo stop : 2_Down.bat +popd +endlocal +exit /b 0 + +:fail +echo. +echo [FAILED] +popd +endlocal +exit /b 1 diff --git a/root/programs/CS/Samples4NetCore/Docker/2_Down.bat b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/2_Down.bat similarity index 100% rename from root/programs/CS/Samples4NetCore/Docker/2_Down.bat rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/2_Down.bat diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/3_Test.ps1 b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/3_Test.ps1 new file mode 100644 index 000000000..6d22513bc --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/3_Test.ps1 @@ -0,0 +1,291 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + コンテナで動いている ASPNETWebService を確かめる(#582)。 + +.DESCRIPTION + **この Resource Server には画面が無い。** + `MVC_Sample` はブラウザで開けば動作が分かるが、こちらは WebAPI だけなので、 + 確かめる手段(クライアント)が要る。それがこのスクリプトである。 + + `1_PublishAndUp.bat` でコンテナを起動した状態で実行する。 + + <見るもの> + + 1. HTTP → HTTPS のリダイレクト(appsettings.Container.json の UseHttpsRedirection=on) + 2. OpenAPI(IDL)が仕様として読める形で返ること(#580) + 3. WebAPI が応答すること(DB を使わないもの/使うもの) + + **どれも「200 が返る」だけでは足りない。** + リダイレクト先・IDL の中身・応答の JSON まで見る。 + + <自己署名証明書> + + `0_SetupCert.ps1` が作る開発用証明書は自己署名なので、検証を通さないと繋がらない。 + **このスクリプトの中だけで**検証を無効にしている(プロセス全体には残さない)。 + +.PARAMETER HttpPort + HTTP のポート。既定は 8090(docker-compose.yml のホスト側)。 + +.PARAMETER HttpsPort + HTTPS のポート。既定は 8091。 + +.EXAMPLE + powershell -NoProfile -ExecutionPolicy Bypass -File .\3_Test.ps1 + +.NOTES + 作成者 :玄人 幸道 + 更新履歴 : + 日時 更新者 内容 + ---------- ---------------- ------------------------------------------------- + 2026/08/21 玄人 幸道 新規作成(#582) +#> +[CmdletBinding()] +param( + [int]$HttpPort = 8090, + [int]$HttpsPort = 8091 +) + +$ErrorActionPreference = "Stop" + +$httpBase = "http://localhost:$HttpPort" +$httpsBase = "https://localhost:$HttpsPort" + +$script:ng = 0 + +function Write-Result([string]$name, [bool]$ok, [string]$detail) +{ + if ($ok) + { + Write-Host (" OK {0,-34} {1}" -f $name, $detail) -ForegroundColor Green + } + else + { + Write-Host (" NG {0,-34} {1}" -f $name, $detail) -ForegroundColor Red + $script:ng++ + } +} + +# ------------------------------------------------------------------ +# 自己署名証明書を通す +# ------------------------------------------------------------------ +# **5.1 と 7 で書き方が違う。** +# 7 は Invoke-WebRequest -SkipCertificateCheck が使えるが、5.1 には無い。 +# 5.1 は ServicePointManager のコールバックで通す。 +[Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +$isPS7 = $PSVersionTable.PSVersion.Major -ge 6 + +if (-not $isPS7) +{ + Add-Type @" +using System.Net; +using System.Security.Cryptography.X509Certificates; +public class SmokeCertPolicy : ICertificatePolicy { + public bool CheckValidationResult(ServicePoint sp, X509Certificate cert, WebRequest req, int problem) { + return true; + } +} +"@ + [Net.ServicePointManager]::CertificatePolicy = New-Object SmokeCertPolicy +} + +function Invoke-Api +{ + param([string]$Uri, [string]$Method = "GET", [switch]$NoRedirect) + + $a = @{ Uri = $Uri; Method = $Method; UseBasicParsing = $true; TimeoutSec = 30 } + if ($NoRedirect) { $a["MaximumRedirection"] = 0 } + if ($isPS7) { $a["SkipCertificateCheck"] = $true } + + try + { + return @{ Ok = $true; Res = (Invoke-WebRequest @a) } + } + catch + { + # **リダイレクトは例外になることがある。** 応答が取れれば、そこから読む。 + $r = $_.Exception.Response + if ($null -ne $r) { return @{ Ok = $false; Res = $r; Ex = $_ } } + return @{ Ok = $false; Res = $null; Ex = $_ } + } +} + +Write-Host "" +Write-Host "============================================" +Write-Host " ASPNETWebService (Docker) の確認" +Write-Host "============================================" +Write-Host (" HTTP : {0}" -f $httpBase) +Write-Host (" HTTPS : {0}" -f $httpsBase) +Write-Host "" + +# ------------------------------------------------------------------ +# 1. HTTP → HTTPS のリダイレクト +# ------------------------------------------------------------------ +# **リダイレクト先まで見る。** 3xx が返るだけでは、どこへ飛ぶか分からない。 +$r = Invoke-Api ($httpBase + "/openapi/v1.json") -NoRedirect + +$status = 0 +$location = "" + +if ($null -ne $r.Res) +{ + $status = [int]$r.Res.StatusCode + try { $location = [string]$r.Res.Headers["Location"] } catch { } + if (-not $location -and $r.Res.PSObject.Properties.Name -contains "Headers") + { + try { $location = [string]$r.Res.Headers.Location } catch { } + } +} + +if ($status -lt 300 -or $status -ge 400 -or $location -notlike "https://*") +{ + Write-Result "HTTP -> HTTPS リダイレクト" $false ("status={0} location={1}" -f $status, $location) +} +elseif ($location -notlike ("*:{0}/*" -f $HttpsPort)) +{ + # **飛び先のポートまで見る。**(#582) + # 3xx が返り https で始まるだけでは足りない。 + # コンテナ内のポート(8081)へ飛ばしていて、ホストから辿れなかった。 + # UseHttpsRedirection は ASPNETCORE_HTTPS_PORT(**単数形**)を読む。 + Write-Result "HTTP -> HTTPS リダイレクト" $false ` + ("飛び先のポートが違う(ホストから辿れない) : {0}" -f $location) +} +else +{ + # **実際に辿れることまで確かめる。** + $f = Invoke-Api $location + + if ($f.Ok) + { + Write-Result "HTTP -> HTTPS リダイレクト" $true ` + ("{0} -> {1} (辿れる)" -f $status, $location) + } + else + { + Write-Result "HTTP -> HTTPS リダイレクト" $false ` + ("飛び先へ辿れない : {0}" -f $location) + } +} + +# ------------------------------------------------------------------ +# 2. OpenAPI(IDL) +# ------------------------------------------------------------------ +# **200 が返るだけでは足りない。** 壊れた文書でも 200 は返る。 +$r = Invoke-Api ($httpsBase + "/openapi/v1.json") + +if (-not $r.Ok) +{ + Write-Result "OpenAPI (IDL)" $false ("取得できない : " + $r.Ex.Exception.Message) +} +else +{ + $doc = $null + try { $doc = $r.Res.Content | ConvertFrom-Json } catch { } + + if ($null -eq $doc) + { + Write-Result "OpenAPI (IDL)" $false "JSON として読めない" + } + elseif (-not $doc.openapi) + { + Write-Result "OpenAPI (IDL)" $false "openapi の版が無い" + } + else + { + $paths = @($doc.paths.PSObject.Properties.Name) + + # **代表的な API が名前で載っていること。** + # 件数だけだと、コントローラが入れ替わっても気づけない。 + $want = @("/api/Json/Select", "/api/BatchUpdate/BatchUpdate") + $miss = @($want | Where-Object { $paths -notcontains $_ }) + + if ($miss.Count -gt 0) + { + Write-Result "OpenAPI (IDL)" $false ("載っていない : " + ($miss -join ", ")) + } + else + { + Write-Result "OpenAPI (IDL)" $true ("openapi {0} / paths {1} 件" -f $doc.openapi, $paths.Count) + } + } +} + +# ------------------------------------------------------------------ +# 3. WebAPI(DB を使わない) +# ------------------------------------------------------------------ +# **まず DB に依らないもので、アプリが動いていることを確かめる。** +# ここが通れば、次が失敗したときに「DB 側の問題」と切り分けられる。 +# +# **メソッドは IDL のとおり GET。**(#582) +# 最初 POST で叩いて 405 になった。**IDL に書いてある**(paths./api/Json/test.get)。 +$r = Invoke-Api ($httpsBase + "/api/Json/test") -Method GET + +if (-not $r.Ok) +{ + Write-Result "WebAPI /api/Json/test" $false ("応答が無い : " + $r.Ex.Exception.Message) +} +else +{ + $body = $r.Res.Content + Write-Result "WebAPI /api/Json/test" $true ("{0} / {1} バイト" -f [int]$r.Res.StatusCode, $body.Length) +} + +# ------------------------------------------------------------------ +# 4. WebAPI(DB を使う) +# ------------------------------------------------------------------ +# **リソース(Sql / Xml)とデータベース接続まで通ることを見る。** +# コンテナ側の %OT_RESOURCE_ROOT% と connectionStrings が効いているかの確認になる。 +$r = Invoke-Api ($httpsBase + "/api/BatchUpdate/SelectCount") -Method POST + +if (-not $r.Ok) +{ + Write-Result "WebAPI /api/BatchUpdate/SelectCount" $false ("応答が無い : " + $r.Ex.Exception.Message) +} +else +{ + $body = $r.Res.Content + $ok = $false + $detail = $body + + try + { + $j = $body | ConvertFrom-Json + + # 件数が返ること(0 以上の数値) + if ($null -ne $j) + { + $props = @($j.PSObject.Properties.Name) + $detail = ("{0} / {1}" -f [int]$r.Res.StatusCode, ($props -join ", ")) + $ok = $true + } + } + catch + { + $detail = "JSON として読めない : " + $body + } + + Write-Result "WebAPI /api/BatchUpdate/SelectCount" $ok $detail +} + +# ------------------------------------------------------------------ +# 判定 +# ------------------------------------------------------------------ +Write-Host "" +Write-Host "============================================" + +if ($script:ng -eq 0) +{ + Write-Host " すべて OK です。" -ForegroundColor Green + Write-Host "============================================" + Write-Host "" + exit 0 +} + +Write-Host (" **{0} 件が NG**" -f $script:ng) -ForegroundColor Red +Write-Host "============================================" +Write-Host "" +Write-Host " コンテナのログ : docker compose logs -f" +Write-Host "" +exit 1 diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/Dockerfile b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/Dockerfile new file mode 100644 index 000000000..a776460d0 --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/Dockerfile @@ -0,0 +1,47 @@ +# ホストビルド型の Dockerfile(#582) +# +# ホスト側で `dotnet publish` した成果物を、ランタイム イメージへ置くだけ。 +# **コンテナ内ではビルドしない。** +# +# 理由:ASPNETWebService.csproj はフレームワークを +# ..\Frameworks\Infrastructure\Build_netcore100\net10.0\*.dll +# というビルド済み DLL への相対参照で持っている。 +# Frameworks を先にビルドしないと restore すら通らないため、 +# コンテナ内でビルドするならビルド コンテキストを root/programs/CS 全体へ +# 広げることになる(Samples4NetCore/ANALYSIS.md 2-1)。 +# +# publish は 1_PublishAndUp.bat が行う。 +# +# **MVC_Sample/Dockerfile と diff が取れる形にしてある。** +# 違うのは ENTRYPOINT の DLL 名だけである。 + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final + +WORKDIR /app + +# publish 成果物 +COPY ./publish/ ./ + +# コンテナ用の設定(appsettings.json を上書きする。ASPNETCORE_ENVIRONMENT=Container で効く) +COPY ./appsettings.Container.json ./ +# コンテナ用の log4net 定義(出力先を %env{OT_LOG_ROOT}) +COPY ./conf/ ./conf/ + +# ログと鍵の置き場所を、非 root ユーザで書けるようにしておく。 +# +# **名前付きボリュームは、初回にイメージ側の所有者と権限を引き継ぐ。** +# ここで chown しておかないと、マウント後に root 所有のままとなり、 +# USER で切り替えた非 root ユーザが書けない。 +# +# **keys は、この Resource Server では使っていない。**(#582) +# Cookie 認証やセッションを足すときに要る(Startup.cs のコメント参照)ため、 +# 置き場所だけ MVC_Sample と揃えてある。 +RUN mkdir -p /app/logs /app/keys && chown -R $APP_UID /app/logs /app/keys + +# 非 root で動かす(ベース イメージが用意している app ユーザ) +USER $APP_UID + +EXPOSE 8080 +EXPOSE 8081 + +ENTRYPOINT ["dotnet", "ASPNETWebService.dll"] diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md new file mode 100644 index 000000000..c0a7d3161 --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md @@ -0,0 +1,118 @@ +# ASPNETWebService を Docker で動かす + +`Samples4NetCore/Backend/ASPNETWebService`(ResourceServerTemplate、net10.0)を +Linux コンテナで動かす(#582)。 + +**構成は [`../MVC_Sample/README.md`](../MVC_Sample/README.md) と同じである。** +方式(ホストビルド型)・設定の置き方・証明書の扱い・本番へ持って行くときの注意は、 +**あちらが一次情報**なので、ここには書かない。 + +ここに書くのは**この Resource Server だけの事情**である。 + +--- + +## 前提 + +`MVC_Sample` と同じ。DB は +[LocalServicesOnDocker](https://github.com/NetDevInfraWGinOSSConsortium/LocalServicesOnDocker) +が提供するものに相乗りするので、**先に起動しておくこと。** + +## 手順 + +```powershell +# 1. HTTPS の開発用証明書を作る(初回のみ) +powershell -NoProfile -ExecutionPolicy Bypass -File .\0_SetupCert.ps1 + +# 2. publish してコンテナを起動する +.\1_PublishAndUp.bat + +# 3. 動いているか確かめる(**画面が無いので、これがクライアント**) +powershell -NoProfile -ExecutionPolicy Bypass -File .\3_Test.ps1 + +# 4. 止める +.\2_Down.bat +``` + +``` +HTTP : http://localhost:8090 (HTTPS へリダイレクト) +HTTPS : https://localhost:8091 +IDL : https://localhost:8091/openapi/v1.json +``` + +--- + +## `MVC_Sample` との違い + +### ① ホスト側のポートが `8090` / `8091` + +**`MVC_Sample`(8080 / 8081)と同時に起動できるようにしてある。** +Resource Server を MVC から呼ぶ構成を、そのまま試せる。 + +コンテナ内の待ち受けは両者とも `8080` / `8081` のままである +(`ASPNETCORE_HTTP_PORTS` / `ASPNETCORE_HTTPS_PORTS`)。 +**変えているのは `docker-compose.yml` の `ports` の左側だけ。** + +### ② Cookie を使わない + +**OAuth2 の Resource Server であり、認証は `Authorization: Bearer` ヘッダである。** +Cookie 認証・セッションを持たないため、次は設定していない。 + +| | `MVC_Sample` | こちら | +|---|---|---| +| `CookieSecurePolicy` | `always` | **書かない**(効かない) | +| `DataProtectionKeyPath` | `/app/keys` | **書かない**(同上) | + +`Startup.cs` 側もコメントアウトで残してある。 +**消していないのは、Cookie 認証へ広げるときの手本になるためである。** + +> **`/app/keys` のボリュームだけは用意してある。** +> 今は使わないが、Cookie 認証を足した時点で**必ず要る** +> (コンテナを作り直すたびに鍵が変わり、認証 Cookie が失効するため)。 +> そのとき `docker-compose.yml` を書き換えずに済む。 + +### ③ 画面が無い + +静的ファイル(`/wwwroot`)も Razor も持たない。 +ブラウザで確認するなら **IDL(`/openapi/v1.json`)** を開く。 + +--- + +## 動作の確認は `3_Test.ps1` で行う + +**画面が無いため、確かめる手段(クライアント)が要る。** それがこのスクリプトである。 + +``` +OK HTTP -> HTTPS リダイレクト 307 -> https://localhost:8091/... (辿れる) +OK OpenAPI (IDL) openapi 3.1.1 / paths 16 件 +OK WebAPI /api/Json/test 200 +OK WebAPI /api/BatchUpdate/SelectCount 200 / count +``` + +**どれも「200 が返る」だけでは足りない。** + +- リダイレクトは**飛び先のポートと、実際に辿れるか**まで見る +- IDL は `openapi` の版と、**代表的な API が名前で載っているか**まで見る + (疎通テストの判定も同じ。[`SMOKETEST.md`](../../../../SMOKETEST.md) 3 節) +- `SelectCount` は **DB まで通る**ので、リソースと接続文字列の確認になる + +> **リダイレクトの判定は、最初「3xx かつ https で始まる」だけだった。** +> それでは飛び先がコンテナ内のポート(8081)でも OK になり、 +> **実際にホストから辿れない状態を見逃していた**(下記)。 + +### 踏んだところ : リダイレクト先はホスト側のポートで与える + +`UseHttpsRedirection` が読むのは **単数形の `ASPNETCORE_HTTPS_PORT`** である +(複数形の `ASPNETCORE_HTTPS_PORTS` は Kestrel の待ち受け。`Startup.cs` のコメント)。 + +**ここにコンテナ内の 8081 を与えると、ホストから辿れない URL へ飛ばす。** +`ports` で `8091:8081` に付け替えているため。 +`docker-compose.yml` で `ASPNETCORE_HTTPS_PORT=8091` を与えている。 + +**`MVC_Sample` は 1:1(8080/8081)なので、この指定が要らない。** + +--- + +## 対象外 + +**`Samples/WS_sample/ASPNETWebService`(net48)は対象外。** +クラシック ASP.NET であり、Linux コンテナでは動かない。 diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/appsettings.Container.json b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/appsettings.Container.json new file mode 100644 index 000000000..ec816dc16 --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/appsettings.Container.json @@ -0,0 +1,48 @@ +// コンテナで動かすときの設定(#582) +// +// **ASPNETCORE_ENVIRONMENT=Container のときだけ読まれ、ここに書いたキーだけを +// 上書きする**(丸ごと差し替えではない)。さらに環境変数の方が強い。 +// +// appsettings.json → appsettings.Container.json → 環境変数 +// +// 仕組みは CONFIGURATION.md 3〜5 節。%OT_RESOURCE_ROOT% の値は docker-compose.yml で与える。 +// このサンプル固有の事情は README.md。 +{ + "appSettings": { + + // フレームワークの使用するパラメータ - start + // + // **フォルダ名は Xml(XML ではない)。** Linux では綴りが違うと開けない。 + // appsettings.json 側は "C:/root/files/resource/XML/..." だが、 + // **Windows は大文字小文字を区別しないので、あちらは XML のままでも開ける。** + "FxXMLSPDefinition": "%OT_RESOURCE_ROOT%/Xml/SPDefinition.xml", + "FxXMLMSGDefinition": "%OT_RESOURCE_ROOT%/Xml/MSGDefinition.xml", + "FxXMLTCDefinition": "%OT_RESOURCE_ROOT%/Xml/TCDefinition.xml", + "FxXMLTMInProcessDefinition": "%OT_RESOURCE_ROOT%/Xml/TMInProcessDefinition.xml", + // フレームワークの使用するパラメータ - end + + // 共通部品の使用するパラメータ - start + // + // log4net の定義はコンテナ専用のものを使う(出力先が違うため。README.md 参照)。 + "FxLog4NetConfFile": "/app/conf/SampleLogConf.xml", + // 共通部品の使用するパラメータ - end + + // 本番向けの切り替え - start + // + // HTTPS で公開するので、**本番と同じ設定のまま動かせる**。 + "UseHttpsRedirection": "on", + + // **CookieSecurePolicy と DataProtectionKeyPath は書いていない。**(#582) + // この Resource Server は Bearer ヘッダ認証で、Cookie もセッションも持たない。 + // Startup.cs 側もコメントアウトしてあるため、書いても効かない。 + // Cookie 認証を足すときに、Startup.cs と合わせて有効にする。 + //"CookieSecurePolicy": "always", + //"DataProtectionKeyPath": "/app/keys", + // 本番向けの切り替え - end + + // アプリケーションの使用するパラメータ - start + "SpRp_RsaCerFilePath": "%OT_RESOURCE_ROOT%/X509/SHA256RSA_Server.cer", + "SqlTextFilePath": "%OT_RESOURCE_ROOT%/Sql" + // アプリケーションの使用するパラメータ - end + } +} diff --git a/root/programs/CS/Samples4NetCore/Docker/conf/SampleLogConf.xml b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/conf/SampleLogConf.xml similarity index 100% rename from root/programs/CS/Samples4NetCore/Docker/conf/SampleLogConf.xml rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/conf/SampleLogConf.xml diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/docker-compose.yml b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/docker-compose.yml new file mode 100644 index 000000000..9b097793e --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/docker-compose.yml @@ -0,0 +1,96 @@ +# ASPNETWebService(ResourceServerTemplate、net10.0)を Docker で動かす(#582) +# +# 前提:DB は LocalServicesOnDocker が提供する。先に起動しておくこと。 +# https://github.com/NetDevInfraWGinOSSConsortium/LocalServicesOnDocker +# あちらが作る common_link に相乗りし、サービス名 sqlserver で届く。 +# +# <値をどこに書くか> +# ・配置に依らない「コンテナならこう」 → appsettings.Container.json +# ・配置ごとに変わる値・秘密 → ここ(environment) +# 読み込み順は appsettings.json → appsettings.Container.json → 環境変数で、 +# **後が勝つ**ので、ここに書けば JSON より優先される。 +# +# <MVC_Sample との違い>(#582) +# **ホスト側のポートを 8090 / 8091 にしてある。** +# MVC_Sample(8080 / 8081)と**同時に起動できる**ようにするため。 +# コンテナ内の待ち受けは両者とも 8080 / 8081 のままでよい。 + +services: + ws-sample: + image: opentouryo-ws-sample + build: + context: . + dockerfile: Dockerfile + ports: + # **左がホスト側。** MVC_Sample と重ならない番号にしてある。 + - "8090:8080" + - "8091:8081" + environment: + # Container 環境として起動する(appsettings.Container.json が読まれる)。 + # **Development にはしない。** 開発者例外ページが出てしまうため。 + - ASPNETCORE_ENVIRONMENT=Container + + # 待ち受けポート + - ASPNETCORE_HTTP_PORTS=8080 + - ASPNETCORE_HTTPS_PORTS=8081 + + # HTTPS へのリダイレクト先のポート(#582) + # + # **単数形。** 複数形の ASPNETCORE_HTTPS_PORTS は + # 「Kestrel がどこで待ち受けるか」であって、 + # UseHttpsRedirection が読むのはこちらである(Startup.cs のコメント)。 + # + # **ホスト側の番号を与える。** + # コンテナ内の 8081 を書くと、ホストから辿れない URL へ飛ばしてしまう + # (ports で 8091:8081 に付け替えているため)。 + # MVC_Sample は 1:1 なので、この指定が要らない。 + - ASPNETCORE_HTTPS_PORT=8091 + + # 証明書(PEM + 秘密鍵。**パスワードを持たない**) + - Kestrel__Certificates__Default__Path=/https/aspnetapp.pem + - Kestrel__Certificates__Default__KeyPath=/https/aspnetapp.key + + # リソースの置き場所 + # + # Open棟梁は設定値の中の %変数% を自前で展開する + # (ResourceLoader.ResolveFilePath → StringVariableOperator)。 + # Windows の書式に依存しない実装なので、Linux コンテナでも効く。 + - OT_RESOURCE_ROOT=/app/files/resource + # ログの出力先(**リソースは ro でマウントするので、ログは別の場所へ**) + - OT_LOG_ROOT=/app/logs + + # 接続文字列 + # + # **Data Source は localhost ではなく sqlserver。** + # コンテナから見た localhost は自分自身であり、DB には届かない。 + # パスワードを直に書いているのは、すぐ動かせるサンプルだからである。 + # 実際の環境では secrets やシークレット ストアから渡すこと(README 参照)。 + - connectionStrings__ConnectionString_SQL=Data Source=sqlserver;Initial Catalog=Northwind;User ID=sa;Password=seigi@123;Encrypt=false; + + volumes: + # 定義ファイル・SQL・証明書(読み取り専用) + - ../../../../../files/resource:/app/files/resource:ro + # HTTPS の開発用証明書(0_SetupCert.ps1 が作る。読み取り専用) + - ./https:/https:ro + # ログ(書き込む) + - ws-sample-logs:/app/logs + # データ保護の鍵 + # + # **この Resource Server では、まだ使っていない。**(#582) + # Bearer ヘッダ認証で、Cookie もセッションも持たないため。 + # Cookie 認証を足すときに効いてくる(Startup.cs のコメント参照)ので、 + # MVC_Sample と揃えて用意してある。 + - ws-sample-keys:/app/keys + + networks: + - common_link + +volumes: + ws-sample-logs: + ws-sample-keys: + +networks: + # LocalServicesOnDocker が作るネットワークに相乗りする。 + # 先に向こうを起動しておくこと(無いと compose がエラーになる)。 + common_link: + external: true diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.dockerignore b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.dockerignore new file mode 100644 index 000000000..45fac3788 --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.dockerignore @@ -0,0 +1,12 @@ +# ビルド コンテキストから除くもの(#548) +# +# **https/ は必ず除く。** 秘密鍵をイメージに焼かないため。 +# 証明書は実行時にボリュームでマウントする(docker-compose.yml)。 +https/ + +# 手順・スクリプトはイメージに要らない +README.md +*.bat +*.ps1 +.gitignore +.dockerignore diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.gitignore b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.gitignore new file mode 100644 index 000000000..7be6e4754 --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.gitignore @@ -0,0 +1,6 @@ +# dotnet publish の成果物(1_PublishAndUp.bat が作る) +publish/ + +# HTTPS の開発用証明書と秘密鍵(0_SetupCert.ps1 が作る) +# **秘密鍵をリポジトリに入れない。** +https/ diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/0_SetupCert.ps1 b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/0_SetupCert.ps1 new file mode 100644 index 000000000..137f537d4 --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/0_SetupCert.ps1 @@ -0,0 +1,104 @@ +<# +.SYNOPSIS + HTTPS の開発用証明書を、PEM 形式で書き出す(#548)。 + +.DESCRIPTION + <パスワードを持たせない> + + 参照元(ASPNETMVCOnDocker)は PFX + .env の CERT_PASSWORD を使うが、 + ここでは PEM + 秘密鍵ファイルにして、**パスワードという管理対象を無くす**。 + + ・.env が要らない(平文パスワードのファイルを作らない) + ・compose での ${CERT_PASSWORD} 展開が要らない + ・初回に決めるパスワードが無い + + 守る対象は localhost 限定・自己署名・1 年で失効する開発用証明書であり、 + 本番の TLS は前段のリバース プロキシが終端する想定である(README 参照)。 + PFX + パスワードにしても、結局 PFX とパスワードの両方がホストに置かれるので、 + 実質の防御力は変わらない。守るのはファイルの権限である。 + + <出力> + .\https\aspnetapp.pem 証明書 + .\https\aspnetapp.key 秘密鍵(**リポジトリには含めない。.gitignore 済み**) + +.PARAMETER SkipTrust + 証明書を信頼済みにする手順(dotnet dev-certs https --trust)を省く。 + ブラウザで警告が出てもよい場合や、確認ダイアログを出したくない場合に指定する。 + +.EXAMPLE + powershell -NoProfile -ExecutionPolicy Bypass -File .\0_SetupCert.ps1 + +.NOTES + Windows PowerShell 5.1 / PowerShell 7 の両方で動く。 +#> +param( + [switch]$SkipTrust +) + +$ErrorActionPreference = "Stop" + +$httpsDir = Join-Path $PSScriptRoot "https" +$pemPath = Join-Path $httpsDir "aspnetapp.pem" +$keyPath = Join-Path $httpsDir "aspnetapp.key" + +Write-Host "============================================" +Write-Host " HTTPS 開発用証明書のセットアップ(PEM)" +Write-Host "============================================" +Write-Host "" + +if (-not (Test-Path $httpsDir)) +{ + New-Item -ItemType Directory -Path $httpsDir | Out-Null +} + +# ------------------------------------------------------------------ +# PEM 形式で書き出す +# ------------------------------------------------------------------ +# -np(--no-password)を付けるので、パスワードの入力は無い。 +Write-Host "[1/2] 証明書を PEM で書き出す ..." +dotnet dev-certs https --format Pem -ep $pemPath -np + +if ($LASTEXITCODE -ne 0) +{ + Write-Host "[エラー] 証明書の書き出しに失敗しました。" -ForegroundColor Red + exit 1 +} + +if (-not (Test-Path $pemPath) -or -not (Test-Path $keyPath)) +{ + # 終了コードだけでは判断しない(生成物の存在も確認する)。 + Write-Host "[エラー] 出力が見つかりません : $pemPath / $keyPath" -ForegroundColor Red + exit 1 +} + +# ------------------------------------------------------------------ +# 信頼済みにする +# ------------------------------------------------------------------ +if ($SkipTrust) +{ + Write-Host "[2/2] 信頼設定は省略しました(-SkipTrust)。" + Write-Host " ブラウザで証明書の警告が出ます。" +} +else +{ + Write-Host "[2/2] 証明書を信頼済みにする ..." + Write-Host " **Windows のセキュリティ警告が出たら「はい」を選ぶこと。**" + + dotnet dev-certs https --trust + + if ($LASTEXITCODE -ne 0) + { + # 信頼設定は失敗しても起動はできる(ブラウザで警告が出るだけ)。 + Write-Host "[警告] 信頼設定に失敗しました。手動での信頼が要ります。" -ForegroundColor Yellow + } +} + +Write-Host "" +Write-Host "============================================" +Write-Host " 完了" +Write-Host "============================================" +Write-Host " 証明書 : $pemPath" +Write-Host " 秘密鍵 : $keyPath" +Write-Host "" +Write-Host " 次は 1_PublishAndUp.bat を実行してください。" +Write-Host "" diff --git a/root/programs/CS/Samples4NetCore/Docker/1_PublishAndUp.bat b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/1_PublishAndUp.bat similarity index 91% rename from root/programs/CS/Samples4NetCore/Docker/1_PublishAndUp.bat rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/1_PublishAndUp.bat index 529282d04..761e0ed84 100644 --- a/root/programs/CS/Samples4NetCore/Docker/1_PublishAndUp.bat +++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/1_PublishAndUp.bat @@ -8,8 +8,8 @@ rem See README.md and #548. setlocal pushd "%~dp0" -set SLN_DIR=..\Backend\MVC_Sample\MVC_Sample -set FX_DLL=..\..\Frameworks\Infrastructure\Build_netcore100\net10.0\OpenTouryo.Framework.dll +set SLN_DIR=..\..\Backend\MVC_Sample\MVC_Sample +set FX_DLL=..\..\..\Frameworks\Infrastructure\Build_netcore100\net10.0\OpenTouryo.Framework.dll echo === 1/4 : checking prerequisites === diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/2_Down.bat b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/2_Down.bat new file mode 100644 index 000000000..9b54086f9 --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/2_Down.bat @@ -0,0 +1,17 @@ +@echo off +rem NOTE: keep this file pure ASCII (root/programs/CODING.md section 4). +rem Japanese explanations are in README.md. +rem +rem Stops and removes the container. +rem Named volumes (logs / data protection keys) are kept on purpose: +rem removing the keys invalidates auth cookies and sessions. +rem Use "docker compose down -v" if you really want to drop them. + +setlocal +pushd "%~dp0" + +docker compose down +set RC=%ERRORLEVEL% + +popd +endlocal & exit /b %RC% diff --git a/root/programs/CS/Samples4NetCore/Docker/Dockerfile b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/Dockerfile similarity index 100% rename from root/programs/CS/Samples4NetCore/Docker/Dockerfile rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/Dockerfile diff --git a/root/programs/CS/Samples4NetCore/Docker/README.md b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/README.md similarity index 100% rename from root/programs/CS/Samples4NetCore/Docker/README.md rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/README.md diff --git a/root/programs/CS/Samples4NetCore/Docker/appsettings.Container.json b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/appsettings.Container.json similarity index 100% rename from root/programs/CS/Samples4NetCore/Docker/appsettings.Container.json rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/appsettings.Container.json diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/conf/SampleLogConf.xml b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/conf/SampleLogConf.xml new file mode 100644 index 000000000..f0d122cdb --- /dev/null +++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/conf/SampleLogConf.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/root/programs/CS/Samples4NetCore/Docker/docker-compose.yml b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/docker-compose.yml similarity index 98% rename from root/programs/CS/Samples4NetCore/Docker/docker-compose.yml rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/docker-compose.yml index bab5bdea6..0b74add9f 100644 --- a/root/programs/CS/Samples4NetCore/Docker/docker-compose.yml +++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/docker-compose.yml @@ -51,7 +51,7 @@ services: volumes: # 定義ファイル・SQL・証明書(読み取り専用) - - ../../../../files/resource:/app/files/resource:ro + - ../../../../../files/resource:/app/files/resource:ro # HTTPS の開発用証明書(0_SetupCert.ps1 が作る。読み取り専用) - ./https:/https:ro # ログ(書き込む) diff --git a/root/programs/CS/Samples4NetCore/Frontend/README.md b/root/programs/CS/Samples4NetCore/Frontend/README.md deleted file mode 100644 index 62099a5b5..000000000 --- a/root/programs/CS/Samples4NetCore/Frontend/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Moved to the following repository. - -- OpenTouryoProject/FrontendTemplates -https://github.com/OpenTouryoProject/FrontendTemplates/ diff --git a/root/programs/RELEASE.md b/root/programs/RELEASE.md index d8c2147fd..eeed35f7f 100644 --- a/root/programs/RELEASE.md +++ b/root/programs/RELEASE.md @@ -185,7 +185,7 @@ cd root\programs - [ ] **`1_BuildAll.ps1` のエラーが「既知の 1 件」だけである** … `-SkipClean` は**使わない**。前回の成果物が残っていると通ったように見える - [ ] **`2_RunAllTests.ps1` が終了コード 0**(8 ケース) -- [ ] **`3_SmokeTest.ps1` が終了コード 0**(29 件) +- [ ] **`3_SmokeTest.ps1` が終了コード 0**(30 件) > **`1_BuildAll.ps1` は現状ここで終了コード 1 になる。** > `WSClnt_sample (net48)` の ClickOnce 署名エラー(`MSB3482`)が残るため。 @@ -209,7 +209,7 @@ cd root\programs |---|---| | `1_BuildAll.ps1`(31 ステップ) | 5.8 分 | | `2_RunAllTests.ps1`(8 ケース) | 1.3 分 | -| `3_SmokeTest.ps1`(29 件) | 7.4 分 | +| `3_SmokeTest.ps1`(30 件) | 7.4 分 | ### VB 側は、この 3 本に含めない diff --git a/root/programs/SMOKETEST.md b/root/programs/SMOKETEST.md index 5db82ff6e..912f68731 100644 --- a/root/programs/SMOKETEST.md +++ b/root/programs/SMOKETEST.md @@ -81,7 +81,7 @@ cd root\programs --- -## 3. 対象(29 件) +## 3. 対象(30 件) **基盤系を先に置く。**(#571) 環境やツールが壊れていれば、アプリケーションの検証を待たずに分かる。 @@ -170,52 +170,32 @@ NG : /OUTPUT "C:\temp\out" ← \ が消える **配置の成功だけでは足りない。** プロキシを無視して直結しても成功するため、 `Test-ProxyUsed` でログを見る。マニフェストと ZIP の両方が通っていることまで確認する。 -プロキシは `st_Proxy.ps1`(`TcpListener` の最小実装)。 -`HttpListener` は使えない。**プロキシへの要求は要求行が絶対 URI** -(`GET http://host:port/path HTTP/1.1`)で来るため、 -プレフィクス登録で受ける `HttpListener` では扱えない。 +**`.NET Framework` がループバック宛を迂回する件と、その回避(実在しないホスト名)は +通信制御と同じである。** 理由は下記「宛先に実在しないホスト名を使っている理由」を見ること。 +こちらでは別名を `deploy.smoketest` とし、プロキシ(`-MapHost`)が `localhost` へ繋ぎ替える。 -##### 踏んだ落とし穴 +##### 通信制御とは別に立てている理由 -**① `.NET Framework` は localhost 宛のプロキシを無条件にバイパスする。** +**対象が別プロセスの EXE だからである。** +通信制御(3 節)はオリジンもプロキシも `TestTransmission` の中に立てて 1 プロセスに閉じているが、 +`DeployZipPackWithHTTP` は外部の EXE なので、**プロキシも外に立てる**必要がある。 -``` -BypassProxyOnLocal(既定) False -IsBypassed(localhost) **True** ← 設定に関わらず -IsBypassed(example.com) False -``` - -配信が `localhost` にある限り net48 では経路を確かめられない。 -`hosts` は書き換えず、**別名(`deploy.smoketest`)を使い、 -プロキシ側で `localhost` に繋ぎ替える**(`-MapHost`)。 - -**② `Host` ヘッダも繋ぎ替える。** IIS Express は `Host` が `localhost` でない要求を受け付けない。 +`st_Proxy.ps1`(`TcpListener`)を `Start-Process` で待ち受けさせる。 +**`HttpListener` は使えない。** プロキシへの要求は要求行が**絶対 URI** +(`GET http://host:port/path HTTP/1.1`)で来るため、プレフィクス登録では受けられない。 -**③ 応答は長さに従って読む。** 「上流が閉じるまで読む」では**本体を持たない HEAD で止まる。** +##### 外に立てたことで要ったこと -**④ 起動待ちは「ポートに繋がるか」では足りない。** -前回のプロキシが待ち受けを握ったままだと接続は成功し、 -新しいプロセスは死んでいるのに起動したと誤認する。**自分のログに `[start]` が出るまで待つ。** +| | 内容 | +|---|---| +| `Host` ヘッダの繋ぎ替え | IIS Express は `Host` が `localhost` でない要求を受け付けない | +| 応答は長さに従って読む | 「閉じるまで読む」では**本体を持たない HEAD で止まる** | +| 起動待ちは**自分のログで見る** | ポートに繋がるかだけでは、**前回のプロセスが握っていても素通り**する | > **同じ理由で `Stop-DeployWeb` も直した。**(#578) > 残った `iisexpress` は 51084 を握り続け、**前回の web フォルダを配り続ける。** -> 実測では net48 の配置が `deploy_core` のマニフェストを読み、 -> そちらへ展開していた。**配置は成功するため異常が出ず、判定だけが 0 件になる。** - - -``` -/ZIPGEN /TOPONLY → root.zip(ルート直下だけ、書庫内ルート無し) -/ZIPGEN /ROOTINZIP aaa → aaa.zip (フォルダごと、書庫内ルート = aaa) -``` - -**両方のモードを通る。** GUI のチェック ボックス -(「個別のフォルダ圧縮」/「ルート フォルダからの圧縮」)に対応する分岐である。 - -`Sample/FormAppRoot` が配布前の姿なので、**配置結果を MD5 で突き合わせられる**(21 ファイル)。 - -**ここだけ ZIP 部品(`ZipperV2` / `UnZipperV2`)を通る。** -単体テスト(`TestCode/TestZipV2.cs`)は部品の振る舞いを見るが、 -配布フロー全体を通すのはこの 2 件だけ。 +> 実測では net48 の配置が `deploy_core` のマニフェストを読み、そちらへ展開していた。 +> **配置は成功するため異常が出ず、判定だけが 0 件になる。** #### マニュフェストの MD5 は計算し直して突き合わせる @@ -344,6 +324,16 @@ net48 : http://127.0.0.1:51090/ IsBypassed=True ← プロキシを使わ **テスト用プロキシが名前を解決せず、必ずオリジンへ繋ぎ替える**ようにしてある。 `hosts` ファイルの編集(管理者権限)も、ファイアウォールへの露出も要らない。 +> **`DeployZipPackWithHTTP` のプロキシ経由テストも同じ手を使っている。**(#578) +> 別名は `deploy.smoketest`、プロキシは `st_Proxy.ps1`(51089)で、 +> `localhost:51084` の配信へ繋ぎ替える。 +> **迂回の理由と回避策はここが一次情報**であり、あちら側には書かない。 +> +> ただし**外部の EXE が対象**なので、プロキシは別プロセスとして立てる +> (通信制御は `TestTransmission` の中に閉じている)。 +> そのぶん `Host` ヘッダの繋ぎ替え、応答の長さ管理、起動待ちの判定が要る +> (「DeployZipPackWithHTTP の CUI モード」を見ること)。 + #### 対象外のオプション | | 理由 | @@ -449,6 +439,42 @@ JSON をまたいだことにならないため、HTTP 越しに送って戻す > 部分一致にしていたところ、**IIS Express の 500.19 が返す HTML に "test" が含まれ、 > 疎通が OK と表示された。**「通ったこと」は正しさの証拠にならない。 +### ResourceServer の OpenAPI(IDL)(1 件) + +| 対象 | 判定 | +|---|---| +| `OpenAPI (net10.0)` | `/openapi/v1.json` が**仕様として読める形で返る** | + +**.NET 9 以降、ASP.NET Core は標準で OpenAPI ドキュメントを生成する。** +`Swashbuckle` は要らない(`Microsoft.AspNetCore.OpenApi`)。 + +```csharp +services.AddOpenApi(); // ConfigureServices +endpoints.MapOpenApi(); // UseEndpoints +``` + +**200 が返るだけでは足りない。** ドキュメントとして壊れていても 200 は返る。 + +- `openapi` の版が入っていること +- `paths` が空でないこと +- **代表的な API が名前で載っていること**(`/api/Json/Select` と `/api/BatchUpdate/BatchUpdate`) +- その `post` が定義されていること + +**数ではなく名前で見る。** 件数だけだと、コントローラが入れ替わっても気づけない。 + +#### net48 側は対象外 + +[`Samples/WS_sample/ASPNETWebService`](CS/Samples/WS_sample/ASPNETWebService) は +**クラシック ASP.NET(`v4.8` / 非 SDK)**であり、`Microsoft.AspNetCore.OpenApi` は使えない。 +`Swashbuckle`(ASP.NET Web API 2 向け)は入手できるが 2016 年で更新が止まっており、 +生成されるのも Swagger 2.0 である。**古い依存を増やしてまで揃えない**(#580)。 + +#### `Microsoft.OpenApi` は版を明示している + +`Microsoft.AspNetCore.OpenApi` 10.0.5 が引く `Microsoft.OpenApi` 2.0.0 は、 +**高深刻度の脆弱性**([GHSA-v5pm-xwqc-g5wc](https://github.com/advisories/GHSA-v5pm-xwqc-g5wc))に該当する。 +修正は 2.7.5 で、**推移的依存のままでは上がらない**ため csproj に明示した(#574 と同じ手)。 + ### Web アプリ(3 件) | 対象 | ホスト | 認証の実装 | 判定 | @@ -503,10 +529,13 @@ WinForms / WPF 系は、リリース チェックリスト(段階 4)の**手 `ConnectionString_SQL` を読む。ここで別途ハードコードすると追随できなくなるため - **IIS Express** がインストールされていること(net48 の Web アプリ) - **ASP.NET 状態サービスが開始されている**こと(net48 の Web アプリ) -- **ポート 51081 - 51086、51090 - 51092 が空いている**こと +- **ポート 51081 - 51093 が空いている**こと - 51081 - 51083 … Web アプリ(net48 の 2 つと net10.0) - 51084 … `DeployZipPackWithHTTP` の配信 - 51085 - 51086 … VB 側の Web アプリ(`-Lang VB`。10 節) + - 51087 - 51088 … `TestWebAPIClient` の相手(net48 / net10.0) + - **51089 … `DeployZipPackWithHTTP` のプロキシ(3 節)** + - **51093 … ResourceServer の OpenAPI(3 節)** - **51090 - 51092 … 通信制御のオリジン・プロキシ・オリジン(TLS)(3 節)** > **GitHub Actions でも実行している。** 前提の揃え方(SQL Server の導入と Northwind の @@ -687,7 +716,7 @@ MVC_Sample (net10.0) OK ログイン後 /Crud1/Index = 200 全対象 OK ``` -全 29 件(ビルド 10 バッチ + 疎通 29 件)で **約 8 分**。 +全 30 件(ビルド 10 バッチ + 疎通 30 件)で **約 8 分**。 **#571 の統合で、13.9 分から半減した。** 同じ WebAPI を相手にしながらホストを 2 回起動していたのをやめたため。 @@ -823,7 +852,7 @@ $client.Connect("localhost", $port) ```powershell .\3_SmokeTest.ps1 -Lang VB # VB のみ(6 件) -.\3_SmokeTest.ps1 -Lang Both # C# 29 件 + VB 6 件 +.\3_SmokeTest.ps1 -Lang Both # C# 30 件 + VB 6 件 ``` **既定に VB を含めない。** リリース時の検証([`RELEASE.md`](RELEASE.md) 3 節)は diff --git a/root/programs/st_Flow.ps1 b/root/programs/st_Flow.ps1 index c6f8c8cd2..b4c29a9b3 100644 --- a/root/programs/st_Flow.ps1 +++ b/root/programs/st_Flow.ps1 @@ -93,3 +93,35 @@ $webFormsFlow = { return @{ Ok = $true; Detail = "ログイン後 menu.aspx = 200" } } +# ResourceServer の OpenAPI(IDL): /openapi/v1.json が仕様として読める形で返ること(#580) +# +# **200 が返るだけでは足りない。** +# ドキュメントとして壊れていても 200 は返る。 +# 版・パス・スキーマまで見て、初めて「IDL になっている」と言える。 +$openApiFlow = { + param($base) + + $r = Invoke-Http "$base/openapi/v1.json" + if ($r.Status -ne 200) { return @{ Ok = $false; Detail = "GET /openapi/v1.json = $($r.Status)" } } + + try { $doc = $r.Content | ConvertFrom-Json } + catch { return @{ Ok = $false; Detail = "JSON として読めない" } } + + if (-not $doc.openapi) { return @{ Ok = $false; Detail = "openapi の版が無い" } } + + $paths = @($doc.paths.PSObject.Properties.Name) + if ($paths.Count -eq 0) { return @{ Ok = $false; Detail = "paths が空" } } + + # **代表的な API が載っていること。** + # コントローラを足しても気づけるよう、数ではなく名前で見る。 + foreach ($p in @("/api/Json/Select", "/api/BatchUpdate/BatchUpdate")) + { + if ($paths -notcontains $p) { return @{ Ok = $false; Detail = "$p が載っていない" } } + } + + # POST の要求本体にスキーマが付いていること(型情報が落ちていないか) + $op = $doc.paths.'/api/BatchUpdate/BatchUpdate'.post + if (-not $op) { return @{ Ok = $false; Detail = "/api/BatchUpdate/BatchUpdate に post が無い" } } + + return @{ Ok = $true; Detail = ("openapi {0} / paths {1} 件" -f $doc.openapi, $paths.Count) } +} diff --git a/root/programs/st_Targets.ps1 b/root/programs/st_Targets.ps1 index 23a9d3ae8..fbfaf126c 100644 --- a/root/programs/st_Targets.ps1 +++ b/root/programs/st_Targets.ps1 @@ -214,6 +214,17 @@ $targetsCS = @( Verify = { Stop-ApiWeb; return $true } } + # ResourceServer の OpenAPI(IDL)(#580) + # **.NET 9 以降の ASP.NET Core は標準で OpenAPI を生成する。** + # Swashbuckle は要らない。net48 側(Samples/WS_sample)は + # クラシック ASP.NET のため対象外。 + @{ + Name = "OpenAPI (net10.0)"; Bat = "6_Build_WSSrvCore_sample.bat" + Kind = "Web"; WebHost = "Kestrel"; Port = 51093 + Exe = "Samples4NetCore\Backend\ASPNETWebService\ASPNETWebService\bin\Debug\net10.0\ASPNETWebService.dll" + Flow = $openApiFlow + } + # --- Web アプリ --- @{ Name = "WebForms_Sample (net48)"; Bat = "10_Build_WebApp_sample.bat" diff --git a/root/programs/st_Utility.ps1 b/root/programs/st_Utility.ps1 index b6971db47..1359ce208 100644 --- a/root/programs/st_Utility.ps1 +++ b/root/programs/st_Utility.ps1 @@ -473,7 +473,10 @@ $verifyDeployCore = { $r = Test-Deploy "core"; Stop-DeployWeb; return $r } # **#575 で HttpWebRequest を HttpClient へ移した経路の確認。** # HttpClientHandler では Proxy = null が「使わない」にならず # UseProxy = false が要る、という差があったため、実際に通して確かめる。 -$deployProxyPort = 51085 +# **51085 は VB 側の Web アプリが使う。**(SMOKETEST.md 9 節) +# 51081-51086 は Web アプリ、51087/51088 は TestWebAPIClient、 +# 51090-51092 は通信制御のオリジン・プロキシ。空いているのは 51089。 +$deployProxyPort = 51089 $deployProxyUser = "pxuser" $deployProxyPwd = "pxpass"