ぷろじぇくと、みすじら。

ASP.NET CoreでLet's EncryptのChallengeのような拡張子なしのファイルを返す

Created at:

ASP.NET Coreの StaticFiles ミドルウェアは拡張子が既知のMIMEタイプに見つからない場合(不明なMIMEタイプ)、ファイルが見つからないものとして扱います。ところが場合によってこの挙動では困る場合があります。

例えばLet’s Encryptは認証のために /.well-known/acme-challenge/Kk5osRC8Q2IKAVgreqBx6lCNgZ-nxzyKje8HJa611_c のようなパスにアクセスを試みますが、標準の挙動のままであればファイルがあっても404が返ってエラーとなります。わかりやすい組み合わせではAzure App Serviceで.NET CoreとLet’s Encrypt Site Extensionを利用している場合に発生します。

解決方法1. StaticFiles のオプションで不明な種類も返せるようにする

UseStaticFiles メソッドには StaticFileOptions という StaticFiles ミドルウェアの設定を渡せますが、その際に不明な種類を返せるようにする ServeUnknownFileTypes プロパティがあるのでそれを true にすることで返せるようになります。

app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true,
DefaultContentType = "application/octet-stream"
});

ただしこの設定を行うと、アプリケーション全体に反映されてしまうので意図せずファイルが公開される可能性が増えるのでやらなくていいのであれば設定したくないものでもあります。

解決方法2. ファイルの種類を提供する IContentTypeProvider を実装する

StaticFiles ミドルウェアはファイルの種類を取得するためのプロバイダー IContentTypeProvider を受け取ることもできます。この IContentTypeProvider にはファイルパスも渡ってくるのでパスを見て判断できます。

例えばLet’s Encryptが来るのは /.well-known/acme-challenge/ 以下と分かっているのでそこだけ知っているかのように処理をして、それ以外はデフォルトの FileExtensionContentTypeProvider や指定された IContentTypeProvider に任せます。

public class LetsEncryptWellKnownContentTypeProvider : IContentTypeProvider
{
private IContentTypeProvider _baseProvider;

public LetsEncryptWellKnownContentTypeProvider()
: this(new FileExtensionContentTypeProvider())
{ }

public LetsEncryptWellKnownContentTypeProvider(IContentTypeProvider baseProvider)
{
_baseProvider = baseProvider;
}

public bool TryGetContentType(string subpath, out string contentType)
{
if (subpath.StartsWith("/.well-known/acme-challenge/"))
{
contentType = "application/octet-stream";
return true;
}

return _baseProvider.TryGetContentType(subpath, out contentType);
}
}

IContentTypeProvider を実装したら、StaticFileOptionsContentTypeProvider プロパティにセットすれば完了です。

app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = new LetsEncryptWellKnownContentTypeProvider()
});

ASP.NET CoreのJSON設定をファイル以外から読み込む

Created at:

大抵の場合ではASP.NETのConfigurationBuilderでAddJsonFileを呼んでファイルパスを指定してJSONファイルを読み込みますが、もしかすると物理的なファイルではなく例えばStringから読み込みたいといった場合があるかもしれません。

そんな時は IFileProvider を実装することで対応できます。

例えば、AddJsonFileに指定されたパスをJSONそのものとして扱ってしまえというようなものも作れて、以下のようなコードで実現できます。

// Install-Package Microsoft.Extensions.Configuration
// Install-Package Microsoft.Extensions.Configuration.Json
// Install-Package Microsoft.Extensions.FileProviders.Abstractions
void Main()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile(new VirtualFileProvider(), @"{
""Foo"": ""barbaz"",
""Hoge"": 1234
}", false, false)
.Build();

configuration.GetChildren().Dump();
}

public class VirtualFileProvider : IFileProvider
{
public IDirectoryContents GetDirectoryContents(string subpath)
{
return new NotFoundDirectoryContents();
}

public IFileInfo GetFileInfo(string subpath)
{
return new VirtualFileInfo("virtual.json", Encoding.UTF8.GetBytes(subpath));
}

public IChangeToken Watch(string filter)
{
return NullChangeToken.Singleton;
}
}

public class VirtualFileInfo : IFileInfo
{
private byte[] _bytes;

public VirtualFileInfo(string path, byte[] bytes)
{
_bytes = bytes;
PhysicalPath = path;
}

public bool Exists => true;

public long Length => _bytes.Length;

public string PhysicalPath { get; }

public string Name => Path.GetFileName(PhysicalPath);

public DateTimeOffset LastModified => DateTime.Now;

public bool IsDirectory => false;

public Stream CreateReadStream()
{
return new MemoryStream(_bytes);
}
}

要するにファイルを表す IFileInfo を返す、IFileProvider を実装して AddJsonFile メソッドに渡すことでその IFileProvider を通して内容を読み込むことになります。

上記の例では指定されたパスをそのままbyte配列にして IFileInfo で Stream として返すというだけのものです(重要ではないプロパティは適当なものを返す)。

テストやちょっと試したいときなどにファイルを作れない/作りたくないといった場合、他のデータソースから取得するといったパターンが必要になったときには役立つかもしれません。