しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

IIS 10.0 と ASP.NET 4.6 で HTTP/2 のサーバープッシュが使えるようになっていたので試した

IIS 10.0 で HTTP/2 のサーバープッシュに対応しているか分からないと書きましたが、実際には ASP.NET からサーバープッシュを簡単に行えるようになっていました。

まずは HTTP/2 のサーバープッシュについての参考になる記事を紹介しておきます。

初めてのHTTP/2サーバプッシュ | GREE Engineering
HTTP/2 入門 - Yahoo! JAPAN Tech Blog

要するに、リクエストされたページに必要なリソースを予めクライアントに送信しておくための仕様ですね。ページをパースしてからリソースを取りに行くのと異なり、既にダウンロード済みとなるので、素早く表示することが出来るというからくりです。

ASP.NET 4.6 での対応

ASP.NET 4.6 の API を軽く調べてみると、HttpResponse に PushPromise というメソッドが追加されていることに気が付きました。これは ASP.NET 4.6 の HTTP/2 対応らしいです。

HttpResponse.PushPromise Method (System.Web) | Microsoft Learn

HttpResponse クラスと HttpResponseBase クラスの両方に実装されているので、Web Forms と MVC の両方で問題なく使えるようになっています。

簡単に試したいので、Web Forms を使ってサンプルコードを書いてみました。1 行ですけど。

public partial class Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Response.PushPromise("~/iisstart.png");
    }
}

PushPromise メソッドの引数は仮想パスじゃないとエラーになります。

サーバープッシュ無し

まずは PushPromise メソッドの呼び出しをコメントアウトして試したところ、iistart.png のリクエストでは TTFB が大きな時間を占めていることが分かります。

サーバーからデータを受信し始めるまで時間がかかってます。

サーバープッシュ有り

次に PushPromise メソッドの呼び出しを有効にして試したところ、TTFB がほぼ無くなりました。

Request Sent に時間がかかるようになっていますが、これは純粋にプッシュされた画像のダウンロードに時間がかかっているのかもしれません。

Chrome で HTTP/2 のフレームを確認してみたところ、ちゃんとプッシュされていることが確認出来ます。

レンダリングがブロックされる JavaScript や CSS をプッシュした方が効果が大きい気がします。

ASP.NET MVC から使う

本来なら透過的に静的なリソースをプッシュするような HTTP Module を実装するのが良いと思いましたが、ちょっと手間なので MVC の URL ヘルパーとして実装してみました。

public static string PushContent(this UrlHelper urlHelper, string contentPath)
{
    var context = urlHelper.RequestContext.HttpContext;

    context.Response.PushPromise(contentPath);

    return urlHelper.Content(contentPath);
}

実装としては PushPromise を呼び出す単純なヘルパーです。使い方は Url.Content と同じですが、プッシュしたいコンテンツの時に使うようにするだけで恩恵に与れます。

ブラウザで確認すると、TTFB はとても短くなっているのでプッシュされていることが確認出来ます。

今回は手を抜いて全コンテンツに対してプッシュを使ってみましたが、サーバープッシュを使うとキャッシュが効かなくなるので、CDN で配信可能なものに関しては有効にしない方が良い気がしました。

Firefox でも試してみると、プッシュが効いているからかタイミングすら表示されなかったです。

コンテンツを事前に送り込むというのは、キャッシュされないということを考えると、思っていたよりも使いどころが難しい機能だと感じました。

ASP.NET Web API を使って画像を Motion JPEG で配信する

Raspberry Pi 2 が Azure Blob に保存した画像を Motion JPEG として配信してみたくなったので、まずはローカルに保存している画像を Motion JPEG として返す Web API を作って試してみました。

Motion JPEG - Wikipedia

H.264 で圧縮しても良い気がしますが、エンコードしながら HTTP で出力する難易度がとても高そうだったので、今回はとても簡単な Motion JPEG です。

Web API に用意されている PushStreamContent を使うと、サーバーからのプッシュが簡単に実装できるので、これを利用して JPEG データを定期的に送信することで動画っぽく見せます。

http://blogs.msdn.com/b/henrikn/archive/2012/04/23/using-cookies-with-asp-net-web-api.aspx

中の人がブログで簡単な使い方を紹介してくれています。

要するに PushStreamContent を Content として返すと、レスポンスのストリームを明示的に閉じるか、HTTP 自体が閉じられるまで自由に書き込みできるという話です。動画をバッファリングして配信したり、Server-Sent Events の実装もこれを使えば簡単でしょう。

ちなみに Motion JPEG として配信するための HTTP レスポンスについては、以下の Wiki に書いてあったので参考に実装しました。

[Wiki] Motion JPEG | en.code-bude.net

まずは、ローカルに保存してある画像を出力するだけの API を作ってみました。

public class StreamController : ApiController
{
    private readonly byte[] _newLine = Encoding.UTF8.GetBytes("\r\n");
    private readonly string _boundary = "0123456789";

    public HttpResponseMessage Get()
    {
        var response = Request.CreateResponse();

        response.Content = new PushStreamContent(async (stream, content, context) =>
        {
            foreach (var file in Directory.GetFiles(@"C:\Users\shibayan\Documents\snapshot", "*.jpg"))
            {
                var fileInfo = new FileInfo(file);

                // ヘッダー書き込み
                var header = string.Format("--{0}\r\nContent-Type: image/jpeg\r\nContent-Length: {1}\r\n\r\n", _boundary, fileInfo.Length);
                var headerData = Encoding.UTF8.GetBytes(header);

                await stream.WriteAsync(headerData, 0, headerData.Length);

                // JPEG データ書き込み
                await fileInfo.OpenRead().CopyToAsync(stream);

                // 最後の改行書き込み
                await stream.WriteAsync(_newLine, 0, _newLine.Length);

                // 30fps で頑張ってみるつもり
                await Task.Delay(1000 / 30);
            }
        });

        // PushStreamContent の mediaType に指定すると検証エラーになったのでここで追加
        response.Content.Headers.TryAddWithoutValidation("Content-Type", "multipart/x-mixed-replace;boundary=" + _boundary);

        return response;
    }
}

いつも通り手抜きな感じですが、とりあえずこれで動作を確認してみます。

残念と言うか当然という感じではありますが、世間的には multipart/x-mixed-replace は全く流行らなかったらしく、ブラウザとしては Firefox しか対応していないようです。

問題なく再生出来ました。正確には素早く JPEG を差し替えているだけですが。

他には VLC Media Player を使って、ネットワークストリームとして開くことで再生出来ました。

本来の目的である、Azure Blob に保存している画像を Motion JPEG として配信することに関しては、JPEG を読み込む元を変更するぐらいで対応出来ると思います。

ASP.NET Web API でバージョニングを実現する方法を調べた

一般的に Web API のバージョニングを実現する方法として、URL にバージョンを含める方法とクエリパラメータや HTTP ヘッダーでバージョンを指定する方法の 2 種類が使われています。

例を挙げると Twitter や Instagram の API は URL でバージョンを指定します。

https://api.twitter.com/1.1/statuses/user_timeline.json

https://api.instagram.com/v1/media/popular?client_id=CLIENT-ID

対して Azure の REST API では、クエリパラメータや HTTP ヘッダーでバージョンを指定します。

https://[search service name].search.windows.net/indexes?api-version=2015-02-28

今回は Twitter や Instagram ような URL ベースのバージョニングを、ASP.NET Web API で実現する方法を調べました。思ったよりめんどくさい感じでした。

IHttpControllerSelector を実装して実現

Web API の DefaultHttpControllerSelector はコントローラをクラス名だけでグルーピングしてしまうので、名前空間が異なっていたとしても同じものとして扱われてしまいます。

string controllerName = controllerTypeGroup.Key;

foreach (IGrouping<string, Type> controllerTypesGroupedByNs in controllerTypeGroup.Value)
{
    foreach (Type controllerType in controllerTypesGroupedByNs)
    {
        if (result.Keys.Contains(controllerName))
        {
            duplicateControllers.Add(controllerName);
            break;
        }
        else
        {
            result.TryAdd(controllerName, new HttpControllerDescriptor(_configuration, controllerName, controllerType));
        }
    }
}
ASP.NET MVC / Web API / Web Pages - Source Code

クラス名に V1 とか V2 と付ければ問題無いですが、やはり名前空間でバージョンを分けておきたいので IHttpControllerSelector を実装して対応します。

規約ベースで実装

最初に考えられるのは規約ベースでバージョンを決めてしまう方法だと思います。実際に MSDN Blog でもバージョンを URL ルーティングに追加し、名前空間にマッピングさせる方法が紹介されています。

ASP.NET Web API: Using Namespaces to Version Web APIs - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

規約ベースで書けるので追加の設定など必要なく、名前空間とコントローラを増やすだけで複数バージョンに対応出来ますし、サンプルコードが CodePlex で提供されているので、そのまま使うことが出来そうです。

http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/NamespaceControllerSelector/

規約ベースで開発している場合は、こっちを選んだ方がスムーズな気がします。

Attribute Routing で実装

既に ASP.NET Web API で Attribute Routing を使った開発をしている場合には、名前空間でコントローラを分けて RoutePrefix でバージョンを直接指定する方法も考えられます。

namespace SampleWebApi.Controllers.V1
{
    [RoutePrefix("v1/products")]
    public class ProductsController : BaseApiController
    {
        [Route]
        public async Task<IHttpActionResult> GetProduct()
        {
            return Ok();
        }
    }
}

残念ながら DefaultHttpControllerSelector の制限でこのままでは動作しませんし、サンプルコードで用意されている NamespaceControllerSelector を使っても対応出来ないので、自前で同じように実装しました。

重要な部分は SelectController メソッドなので、その部分だけ引っ張り出してきました。

public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
    var routeData = request.GetRouteData();

    if (routeData == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    var subRoute = routeData.GetSubRoutes().FirstOrDefault();

    if (subRoute == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    var actions = (HttpActionDescriptor[])subRoute.Route.DataTokens["actions"];

    if (actions.Length == 0)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    return actions[0].ControllerDescriptor;
}

Attribute Routing を使った場合には MS_SubRoutes というキーが RouteData に含まれていて、更にその中に HttpActionDescriptor が入っているので辿っていきます。

最後に HttpControllerDescriptor を返せば、そのコントローラが使われるようになります。

ライブラリを使って実現

自分で IHttpControllerSelector を実装するのもめんどくさい場合には、手軽に公開されているライブラリを使うという方法もあります。

このライブラリを使えば URL と HTTP ヘッダーを使ったバージョニングの両方が使えるようになるみたいです。実際に使い方を紹介している方が居たのでリンクを載せておきます。

Versioning ASP.Net Web API | Culbertson Exchange

最近は Attribute Routing ばかり使っているので、IHttpControllerSelector を実装して解決しました。名前空間を無視するかどうかを設定で切り替えさせてほしかったですね。

ASP.NET Web API でアップロードされたファイルを扱う方法を知らなかったので調べた

ASP.NET MVC でファイルアップロードを実装する場合は、モデルバインダが HttpPostedFileBase クラスなどに自動的にバインドしてくれていましたが、Web API だとそこまで面倒は見てくれないみたいです。

これまで Web API での実装方法を知らなかったですが、今回ついに観念して調べました。

Sending HTML Form Data in ASP.NET Web API: File Upload and Multipart MIME | The ASP.NET Site

MultipartFormDataStreamProvider クラスを使うとアップロードされたファイルをストレージ上に保存してくれるみたいですね。メモリストリームとして扱い続けるよりも楽な感じがします。

サンプルコードでは App_Data に入れてましたが、個人的に Temp のが良い気がしたので変えました。

public async Task<IHttpActionResult> FileUpload()
{
    // multipart/form-data 以外は 415 を返す
    if (!Request.Content.IsMimeMultipartContent())
    {
        return StatusCode(HttpStatusCode.UnsupportedMediaType);
    }

    // マルチパートデータを一時的に保存する場所を指定
    var rootPath = Path.GetTempPath();
    var provider = new MultipartFormDataStreamProvider(rootPath);

    // 実際に読み込む
    await Request.Content.ReadAsMultipartAsync(provider);

    // ファイルデータを読む
    foreach (var file in provider.FileData)
    {
        // ファイル情報を取得
        var fileInfo = new FileInfo(file.LocalFileName);
    }

    return Ok();
}

アップロード時に渡されたファイル名などの情報は Headers プロパティから参照できます。

MultipartFormDataStreamProvider クラスを使って実際にアップロードしてみると、Temp ディレクトリにユニークなファイル名でアップロードしたファイルが保存されていることが分かりますね。

f:id:shiba-yan:20150319160736p:plain

単純なファイルアップロードが必要な場合はこれで十分だと思います。

他にファイルアップロード時に使えるプロバイダーとしては MultipartFormDataRemoteStreamProvider クラスも用意されています。このプロバイダーは書き込み可能なストリームを返すような実装を用意することで、アップロードされたファイルを直接保存することが出来ます。

実際に Azure Blob へのストリームを返すプロバイダーを実装してみました。

public class AzureBlobRemoteStreamProvider : MultipartFormDataRemoteStreamProvider
{
    public AzureBlobRemoteStreamProvider()
    {
        _storageAccount = CloudStorageAccount.Parse("...");
    }

    private readonly CloudStorageAccount _storageAccount;

    public override RemoteStreamInfo GetRemoteStream(HttpContent parent, HttpContentHeaders headers)
    {
        var fileName = headers.ContentDisposition.FileName.Trim('"');

        var blobClient = _storageAccount.CreateCloudBlobClient();

        var container = blobClient.GetContainerReference("upload");
        var blob = container.GetBlockBlobReference(fileName);

        var stream = blob.OpenWrite();

        return new RemoteStreamInfo(stream, blob.Uri.AbsoluteUri, fileName);
    }
}

アップロードされたファイル名のまま upload というコンテナに保存するだけの簡単なプロバイダーです。

使い方はシンプルで、ReadAsMultipartAsync に渡すプロバイダーを切り替えるだけです。

public async Task<IHttpActionResult> Post()
{
    var provider = new AzureBlobRemoteStreamProvider();

    await Request.Content.ReadAsMultipartAsync(provider);

    foreach (var file in provider.FileData)
    {
        var blobUri = file.Location;
    }

    return Ok();
}

実際にアップロードすると、Blob に書きこまれていることが確認出来ます。

f:id:shiba-yan:20150319162151p:plain

GetRemoteStream メソッドでもうちょっと処理を頑張ると、アップロードしたユーザー別にコンテナを作成して保存するといった処理も実装できると思います。

SendGrid の Inbound Parse Webhook を ASP.NET MVC から簡単に使うためのヘルパーを作った

以前に SendGrid の Parse API を使ってメールの受信をフックする - しばやん雑記 で使ったことがある SendGrid の Inbound Parse Webhook は、簡単にメールの受信が出来てとても便利なのですが、送信されるパラメータが多く、部分的に JSON だったりもするので、簡単に使うためにヘルパークラスを作ってみました。

ちなみに Inbound Parse Webhook のドキュメントは以下の通り。当然ながら英語です。*1

Inbound Parse Webhook - SendGrid Documentation | SendGrid

今回は ASP.NET MVC を使って Parse Webhook 向けにモデルバインダと属性を用意しました。サンプルコードは既に GitHub にて公開してあります。

折角なので、それぞれについて簡単に使い方を紹介しておきます。

ParseModelBinder クラス

Webhook の呼び出しと同時に渡されるパラメータをいい感じにクラスにバインドする ParseModelBinder と、その器である ParseInfo クラスを用意しました。

使い方は以下のようにとてもシンプルです。

public class WebhookController : Controller
{
    [HttpPost]
    public ActionResult Index(ParseInfo model)
    {
        
        return new HttpStatusCodeResult(200);
    }
}

ParseInfo クラスに ModelBinder 属性を付けてあるので、普通にメソッドの引数として受け取るだけです。

実際にメールを受信した際には、以下のようにデータがバインドされた形で渡されます。

f:id:shiba-yan:20150127144327p:plain

ParseInfo には扱いやすい形でデータを入れているので、空メールのハンドリングや添付ファイルを利用するアプリケーションなどで便利に使えると思います。

ParseTo 属性

Parse Webhook では特定のホスト宛てのメールを全て同じエンドポイントで受信するので、どのアドレス宛に送られたのかは Webhook 側で判断する必要があります。

しかし、やはりちょっとめんどくさいと思ったので ASP.NET MVC の ActionMethodSelector 属性を継承した ParseTo 属性を実装しました。コンストラクタで指定したメールアドレス宛てのメールのみ、そのアクションが実行されるようになります。

これもコードは以下のようにシンプルです。

public class WebhookController : Controller
{
    [HttpPost]
    [ParseTo("info@shibayan.jp")]
    public ActionResult Index(ParseInfo model)
    {
        return new HttpStatusCodeResult(200);
    }

    [HttpPost]
    [ParseTo("web@shibayan.jp")]
    public ActionResult Index(string to, ParseInfo model)
    {
        return new HttpStatusCodeResult(204);
    }
}

実際にメールを送り、ngrok を使って確認してみました。

f:id:shiba-yan:20150127151523p:plain

送信先メールアドレスと同じ値の ParseTo 属性が付いているアクションが呼ばれているのが確認出来ました。

メールを受信したタイミングで何らかのアクションを実行したいという要求は思った以上にあると思うので、これからも Parse Webhook を便利に使っていきたいですね。

*1:KKE に相談すれば日本語のドキュメントをくれるのかもしれないけど

ASP.NET の Response.Cookies を弄ると空のクッキーが生成されて困った話

ASP.NET で Response.Cookies にとある名前のクッキーが既に入っているかをチェックするコードを書いたところ、空っぽのセッションクッキーが作られることに気が付きました。

例を挙げると以下のようなコードを書いた時です。

public partial class Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (Response.Cookies["hoge"] != null)
        {
            // 何かする
        }
    }
}

やりたいことは既にクッキーが発行されているかのチェックなのですが、どうもインデクサで触った瞬間にクッキーが作られてしまうようです。

実際に動かすと、空のクッキーが発行されているのが確認できます。

f:id:shiba-yan:20150114124727p:plain

RCW 経由で COM のインデクサを使った時にメモリリークしたのを思い出しました。

Request.Cookies を使う時と同じような感覚で使うと、ちょっとめんどくさいことになるようです。なので AllKeys をまずチェックしてから値を取り出すような拡張メソッドを書きました。

public static class HttpCookieExtension
{
    public static bool Contains(this HttpCookieCollection cookies, string name)
    {
        return cookies.AllKeys.Any(p => p == name);
    }

    public static HttpCookie Peek(this HttpCookieCollection cookies, string name)
    {
        return cookies.AllKeys.Any(p => p == name) ? cookies[name] : null;
    }
}

色々とメソッドが足りていないので、やっぱり HttpCookieCollection って古いコレクションなんだと実感出来ますね。きっと中身は NameValueCollection とかになってるんでしょう。

先ほどの例のようにクッキーが既に返されているかチェックする処理は、以下のように書けます。

public partial class Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (Response.Cookies.Contains("hoge"))
        {
            // 何かする
        }
    }
}

これで新しくクッキーが作成されることなく弄ることが出来ました。

ASP.NET MVC で任意のタイミングでモデルの検証を行う方法

MVC 5 でウィザード的に 1 ページごとに項目を入れていくページを作っていると、実際に DB に格納する前にセッションに入れておいたモデルの状態が正しいか調べたくなりました。

てっきり「あー、UpdateModel / TryUpdateMode の中身参考にしないといけないのか、めんどくさいなー」と思っていたところ、かなり前*1から Controller クラスに ValidateModel / TryValidateModel メソッドが用意されていることに気が付きました。

Controller.TryValidateModel メソッド (System.Web.Mvc)
Controller.ValidateModel メソッド (System.Web.Mvc)

MVC 5 の機能は一通りは触ってるだろうと思っていたら、こんな初歩的な部分を見落としていたことに恥ずかしい限り…。戒めとしてこの記事を書きます。

これらのメソッドは基本的にモデルバインダーが内部で行っている検証とほぼ同じ処理になるので、データアノテーションや IValidatableObject を使った検証まで行ってくれるようです。

public class SampleModel
{
    [Required]
    public string Title { get; set; }

    [Range(0, 1000)]
    public int Price { get; set; }
}

動作を確認するために、上のような簡単なモデルクラスを用意しました。

public ActionResult Confirm()
{
    // わざと検証エラーになるデータを入れる
    var model = new SampleModel
    {
        Price = -1
    };

    // データに不整合が発生していないかチェック
    TryValidateModel(model);

    return View(model);
}

簡単なビューを用意して実行してみたところ、モデルバインダーを使っていないにもかかわらずエラーが表示されることが確認できます。

f:id:shiba-yan:20141121004308p:plain

ちなみに以下のようにネストした場合にはルートのプロパティしか検証してくれないので、実際に使う場合にはちょっと工夫とか注意が必要になるかと思います。

public class SampleModel
{
    [Required]
    public string Title { get; set; }

    [Range(0, 1000)]
    public int Price { get; set; }

    public SampleChildModel ChildModel { get; set; }
}

public class SampleChildModel
{
    [Required]
    public string Name { get; set; }
}

以下のようなアクションに書き換えて実行をしてみると、ChildModel.Name プロパティは必須にもかかわらずエラーとなりません。これはいまいちな挙動です。

public ActionResult Confirm()
{
    // わざと検証エラーになるデータを入れる
    var model = new SampleModel
    {
        Price = -1,
        ChildModel = new SampleChildModel()
    };

    // データに不整合が発生していないかチェック
    TryValidateModel(model);
    TryValidateModel(model.ChildModel);

    return View(model);
}

なので、とりあえずはネストしたモデルに対しても TryValidateModel メソッドを実行することで、意図したとおりの挙動にはなります。拡張メソッドを用意してもいいかもしれません。

f:id:shiba-yan:20141121004941p:plain

DB 登録前には最終的なモデルのチェックをしたかったのに、このメソッドの存在を知らなかった為に各方面に迷惑をかけた気がします。*2

*1:少なくとも MVC 2 の頃にはあったらしい…

*2:主に蠣殻町の某社さん

ASP.NET MVC の RequireHttps 属性は GET 以外の場合には例外を投げるので注意

タイトルの通りですが、ASP.NET MVC を使って SSL が必須のページを実装するのには RequireHttps 属性が便利ですね。しかし、この属性は GET 以外のリクエストが来ると無条件で例外を投げるようになってます。

例えば HEAD リクエストを投げてみると 500 が返ってきます。

f:id:shiba-yan:20141117123133p:plain

実際にコードを見てみると GET 決め打ちのコードとなってます。

// only redirect for GET requests, otherwise the browser might not propagate the verb and request
// body correctly.

if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
    throw new InvalidOperationException(MvcResources.RequireHttpsAttribute_MustUseSsl);
}
ASP.NET MVC / Web API / Web Pages - Source Code

正直なところ、この挙動はいまいちな感じがします。少なくとも例外を投げるのは勘弁してもらいたい。

ヘッダ情報を要求してるのでリダイレクト先を教えるのは問題無いのではないかと思うんですが、新しくこの修正のためだけに属性を作るのも微妙なので、URL Rewrite を使って https へリダイレクトさせます。

<rule name="RequireHttps" enabled="true">
  <match url="(.*)" ignoreCase="false" />
  <conditions>
    <add input="{HTTPS}" pattern="off" />
  </conditions>
  <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" appendQueryString="true" redirectType="Permanent" />
</rule>

サーバー変数 HTTPS に on/off が入ってくるので、それを見てリダイレクトするという簡単なルールです。

f:id:shiba-yan:20141117123457p:plain

今度は HEAD リクエストを投げた場合でもリダイレクトになっていることが分かります。こういった処理は URL Rewrite の段階で処理した方がパフォーマンス的にも有利かもしれませんね。

Azure Web サイトに追加されている ASP.NET の設定について

Azure Web サイトの App Settings にはデフォルトで以下のような設定が追加されています。

f:id:shiba-yan:20141101193712p:plain

aspnet:PortableCompilationOutput と aspnet:DisableFcnDaclRead という設定は名前から ASP.NET に関係する設定ということは分かりますが、これまでに見たことが無かったので調べてみました。

aspnet:PortableCompilationOutput

検索してみると以下の KB がヒットしました。

Hotfix rollup 2803755 is available for the .NET Framework 4.5 in Windows 8 and Windows Server 2012

.NET Framework 4.5 の ASP.NET ビューのコンパイラには、最新版か判断するためにコンパイルを行ったマシンの情報を使う挙動が原因で、コンパイル結果を複数のマシン間で共有できないという問題があるようです。

この設定を追加することで、ポータブルな形でコンパイルが行われるみたいです。オートスケールやフェールオーバーで仮想マシン間をころころと移動する Azure Web サイトでは有効にしておくべき設定でした。*1

aspnet:DisableFcnDaclRead

さっきの設定は名前から割と想像できたんですが、こっちの設定は分かりにくいです。検索してみたところ、特に Hotfix などで追加されたものというわけではなさそうです。

debugging.io blog | Undocumented asp.net flag to improve performance of asp.net website when content is hosted on SMB share

Azure Web サイトはアーキテクチャ的にユーザーデータは SMB でマウントされた Blob に保存されています。ASP.NET アプリケーションは SMB で共有されている仮想ディレクトリ上で動かすと、パフォーマンスの低下や以下のようなエラーが発生することがあるようです。

"The network BIOS command limit has been reached" error message in Windows Server 2003, in Windows XP, and in Windows 2000 Server

原因は ASP.NET はファイルの変更監視時に DACL を読み込むみたいですが、この処理が SMB 上だと問題になるみたいです。なので DACL の読み込みをスキップする設定がこれです。

これも Web サイトのアーキテクチャ的に必須な設定ですね。SMB で wwwroot 以下をマウントしている場合には設定を追加しておきましょう。

*1:というか ASP.NET が動く PaaS なら必要な気がしないこともない

Azure と ASP.NET に関係する SSL 3.0 の脆弱性への注意点

SSL 3.0 の脆弱性に関して何回か取り上げてきましたので、最後に Azure と ASP.NET に関係がある部分での注意点についてまとめておきます。

Azure CDN

f:id:shiba-yan:20141017002018p:plain

Azure の CDN は SSL 3.0 が既に無効化されていました。これにより CSS や画像などのリソースが読み込めないという現象が発生する可能性が出てきます。

これは Azure 側が無効化したというよりも、EdgeCast が SSL 3.0 を無効化したという話でしょうけど。

Azure Storage

f:id:shiba-yan:20141017002402p:plain

CDN は SSL 3.0 が無効化されていましたが、Blob などのストレージに関しては有効なままでした。Blob に関して SSL 3.0 をオフにしたい場合には CDN を経由させるのが良さそうです。

Microsoft Ajax CDN

f:id:shiba-yan:20141017001724p:plain

Microsoft Ajax CDN も SSL 3.0 が無効になっていました。と言うか Azure CDN の時と同じ IP アドレスなので、Ajax CDN は EdgeCast を使って運用されていることがわかります。

Azure CDN や Microsoft Ajax CDN に限らず、jQuery や Bootstrap などの CDN 系は Google Hosted Libraries ぐらいしか 3.0 が有効になってない感じがしました。とても対応が早いです。

Azure Web サイト

今のところ、サーバー側で SSL 3.0 を強制的にオフにするという考えはないように思えます。

No.1 に教えてもらった会話ですが、ハンセルマン的にはブラウザでオフにすればいいよと言う感じ。

と言うか、マルチテナントである Azure Web サイトでは、簡単にサイトごとに SSL 3.0 のオンオフ機能とかを実装出来ない事情があります。Web サイトと言うか Windows の制約という感じですが。*1

f:id:shiba-yan:20141017113243p:plain

しかし David Ebbo 的には ARR と Site Extensions で SSL 3.0 での接続はさせつつも、コンテンツは表示させないという方法を検討しているようです。ちなみにインストールしてもまだ動作しません。

これだとクッキーをデコードされる危険性は残りますが、ユーザーに SSL 3.0 をオフにして TLS を使えと言う意思表示にはなるのかなと。

f:id:shiba-yan:20141017113850p:plain

ちなみに同じ PaaS である Heroku はさっさと SSL 3.0 をオフにしているようです。

追記

これまでは Site Extensions でコンテンツを表示させないという方法しかなかった Azure Web サイトですが、どうやら一斉に SSL 3.0 が無効化されることになったようです。

Disable SSL 3.0 in Azure Websites (updated!)

Azure Websites will disable SSL 3.0 for all sites by default to protect our customers from the vulnerability mentioned before. We are rolling out the changes across our data-centers and monitoring traffic in the process. The changes will be rolling out through the week of Monday October 27th, 2014. Once this is complete, customers will no longer need to take any action to disable SSL 3.0 in Azure Websites and should have protection by default.

How to Disable SSL 3.0 in Azure Websites, Roles, and Virtual Machines | Microsoft Azure Blog

今週中にはメンテナンスが行われて、SSL 3.0 がデフォルトで無効化されます。

古いガラケー向けのサービスを Azure Web サイトで提供している場合には、この変更で繋がらなくなる可能性があるのでちょっとだけ注意。

おまけ - Azure MVP の意見

個人的には SNI SSL の時は暗黙的に SSL 3.0 無効とか出来ればいいなと思いましたが、こういうのは落としどころが結構難しそうです。

*1:レジストリ変更 + OS の再起動が必要になるため