IIS7 の機能を拡張してみる-指定した言葉の出力禁止


IIS7 は ASP.NET とのパイプライン (リクエストを受けてからレスポンスを返すまでの処理) の統合により、パイプラインにかかわる処理を .NET のマネージコードで記述することができます。

リクエストのハンドリング方法については、既に機能を紹介していますので、今回はレスポンスはハンドリングし、その中のコンテンツを書き換える方法について紹介させていただきます。

この "コンテンツの内容を書き換える = 出力を行う" という処理は本来ハンドラで行う処理ですが、今回はモジュールを使用したものを紹介します。

そのため紹介さていただきますサンプルコードは、既にハンドラが設定されているページ、つまりは、*.aspx や *.php 等の、サーバーサイドで何らかの処理を行うページでしか動作しません。

それなのに、なぜあえてモジュールとして実装したかというと、ええ、....じつは私の勘違いでモジュールとして作ってしまったからです、はい(恥)

以前、php アプリケーション内のリンクを、フレンドリな短い URL に書き換えるというモジュールを作成したこともあり、"ASP.NET 以外のページでも動作 = 静的なファイルでも動作" と短絡的に考えていたのですが、今回モジュールを作り終えてテストしたところ、静的なファイルでは動作しないこと&理由が判明したしだいです。(すみません)

 

今回のサンプルの機能

のっけから非常に残念な感じの今回のサンプルなのですが、どういった機能を実装したかというと、レスポンス内のコンテンツから任意の言葉を伏字にするという機能です。

この機能を使用すると、任意に指定したワード (言葉) がコンテンツに含まれていた場合、代替えのキャラクタで置き換えてくれるため、サイトの訪問者の目に触れさせたくない言葉をブロックすることができます。

人によっては "そんな機能が本当に必要か? 必要なのか??" と考えられるかもしれません。

しかしどうでしょう、 たとえば、取引先の社の Web サイトを覗いたときに、そのコンテンツ内に公序良俗に反する言葉や、差別用語や、社会通念上許されざる言葉が平然と使用されていたとしたら、今後その会社と深いリレーションを築きたいと思うでしょうか?

また、いくら仲の良い友人からのメールであっても、幼児の三大爆笑ワードである "ば〇、う〇こ、ちん〇ん" といったような、下品な言葉が毎回多用されていたりしたら、その人の品性と精神的健康状態について強い疑問を持たざるを得ないでしょう。

さらに"言葉" には、理解した瞬間に著しく相手の気分を害するものもあります。しかもそれは、人間の肉体に備わった機能ではブロックすることはできません。

たとえば、今、このドキュメントをお読みいただいているわけですが、ご自身の肉眼に実際に写っているものは、じつは "文字" でも "言葉" でなく、モニタ上に並んだ映像素子の点滅でしかありません。

モニタに表示された光のパターンを脳が文字と認識し、文字の並びから文としての解釈が行われ、私の言葉が"再生" (処理が実行) されているのです。

この一連の処理は、人間の読解における基本的な処理ですが、基本的であるがゆえにその一連の処理は無条件に実行され、解釈される内容の良し悪しにかかわらず、その実行は防ぎようがないのです。

たとえば、ここに突然、閲覧者にとって非常に有害な言葉が書かれていたとしてもそれを防ぐ手立ては、閲覧者自身にはないのです。

そういった意味では人間は情報に対して非常に無防備な状態であると言えるでしょう。

そんな、情報に対し "まるごし" でやってくる閲覧者を保護し、迂闊なコンテンツによる自サイトのイメージ低下を防いでくれるのが今回のモジュールなのです。(すみません、ほんというと今考えました)

 

IIS 7 のクライアントへのレスポンスのハンドリング

今回のサンプルは IHttpModule を継承してモジュールとして実装していますが、そのため、HTML などの静的なページでは動作せず、ASP.NET や、PHP といったハンドラを使用しているページでのみ動作します。

リクエストを処理してクライアントへ何らかのレスポンスを返す IIS 拡張を作成する場合はハンドラで作成するようにしましょう。(はい、すみません)

サンプルコードの配置方法は以下の通りです。今回もコンパイルは任意ですので、"Visual Studio" を持っていない、"プログラムを書いたことがない" という方もぜひお試しください。

  1. テキストエディタに以下のコードを貼り付けて、WordFilter.cs という名前で保存してください。

    using System;
    using System.IO;
    using System.Text;
    using System.Web;
    using System.Text.RegularExpressions;
    using System.Configuration;
    using System.Web.Caching;

    namespace MyIisExtentionModure
    {
        public class WordFilter : IHttpModule
        {
            public void Dispose() {}

            public void Init(HttpApplication context)
            {
                //イベントハンドラの登録
                context.PreRequestHandlerExecute += new EventHandler(application_PreRequestHandlerExecute);
            }

            //イベント ハンドラ (ページ、XML Web サービスなど) の実行を開始する直前に発生
            void application_PreRequestHandlerExecute(object sender, EventArgs e)
            {
                HttpApplication app = (HttpApplication)sender;
                HttpContext context = app.Context;
                context.Response.Filter = new ContentsRewriter(context);
            }
        }

        //レスポンスコンテキスト中のレスポンスを書き換える
        public class ContentsRewriter : Stream
        {
            private HttpContext context;
            private Stream innerStream;
            private MemoryStream bufferStream;

            //コンテキストをリライト
            public ContentsRewriter(HttpContext contxt)
            {
                context = contxt;
                innerStream = context.Response.Filter;
                bufferStream = new MemoryStream();
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                //レスポンスをバッファに保存
                bufferStream.Write(buffer, offset, count);
            }

            //レスポンスをフラッシュ
            public override void Flush()
            {
                if (bufferStream.Length == 0) return;

                Encoding enc = Encoding.GetEncoding(context.Response.Charset);
                byte[] responseBytes = bufferStream.ToArray();
                bufferStream = new MemoryStream();

                //レスポンスされるコンテンツを文字列として取り出す。
                //単純にレスポンスされるコンテンツの内容を編集したい場合は、
                //変数 responseString の内容を文字列操作。
                String responseString = enc.GetString(responseBytes);

                //伏字処理。
                responseString = execWordBlock(responseString);
                responseBytes = enc.GetBytes(responseString);
                innerStream.Write(responseBytes, 0, responseBytes.Length);
                innerStream.Flush();
            }

            //伏字処理を実行
            private string execWordBlock(string responseString)
            {
                string subChar = ConfigurationSettings.AppSettings["SubstituteChar"];
                string[] blockWordList;

                //使用している正規表現文字列が、改行があるとうまく動作しないため改行を削除
                string contectString = responseString.Replace(Environment.NewLine, "");

                //キャッシュに禁止単語のリストがあるかチェック
                if (HttpRuntime.Cache["BlockWordList"] == null)
                {
                    //キャッシュに無い場合はファイルから情報を読み込みリストを作成
                    string blockWordFilePath = ConfigurationSettings.AppSettings["BlockWordListPath"];
                    string listString = File.ReadAllText(blockWordFilePath, Encoding.Default);
                    blockWordList = listString.Split(',');
                    //リストをキャッシュに保存
                    HttpRuntime.Cache.Insert("BlockWordList", blockWordList);
                }
                else
                {
                    //キャッシュからリストを取り出し
                    blockWordList = (string[])HttpRuntime.Cache["BlockWordList"];
                }

                foreach (string chkWord in blockWordList)
                {
                    string subWord = MakeSubstituteString(chkWord.Length - 1, subChar);

                    //正規表現を使用した置換処理を行う関数を呼んでいますが、
                    //ここで単純に Replace してしまったほうがパフォーマンスは良いでしょう。
                    contectString = FilterWord(contectString, chkWord, subWord);
                }
                return contectString;
            }

            //検出されたワードを伏字で置き換える
            private string FilterWord(String inputString, string chkWord, string subWord)
            {
                //正規表現の文字列。より適切なものがあれば変えてください。
                string regExpString = ">.*?<";
                string inputString_wrk = inputString;
                Regex regExp = new Regex(regExpString,
                    RegexOptions.IgnoreCase | RegexOptions.Compiled);

                for (Match mtc = regExp.Match(inputString); mtc.Success; mtc = mtc.NextMatch())
                {
                    string targetString = mtc.Groups[0].ToString();
                    string cureString = targetString.Replace(chkWord, subWord);
                    inputString_wrk = inputString_wrk.Replace(targetString, cureString);
                }
                return inputString_wrk;
            }

            //伏字に使用する文字列を生成する
            private string MakeSubstituteString(int wordLength, string subChar)
            {
                string returnString = "";
                for (int i = 0; i <= wordLength; i++)
                {
                    returnString += subChar;
                }
                return returnString;
            }

            public override void Close()
            {
                try { Flush(); }
                catch (Exception) { throw; }
                finally { }
            }

            /* 以下 Stream クラスを継承するための抽象メンバ */
            public override bool CanRead
            {
                get { return false; }
            }

            public override bool CanSeek
            {
                get { return false; }
            }

            public override bool CanWrite
            {
                get { return true; }
            }

            public override long Length
            {
                get { throw new NotImplementedException(); }
            }

            public override long Position
            {
                get { throw new NotImplementedException(); }
                set { throw new NotImplementedException(); }
            }

            public override void SetLength(long value)
            {
                throw new NotImplementedException();
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                throw new NotImplementedException();
            }

            public override int Read(byte[] buffer, int offset, int count)
            {
                throw new NotImplementedException();
            }
        }
    }

  2. Default Web Site 下の、ASP.NET が動作するように設定してある 仮想ディレクトリの物理フォルダ内に App_Code というフォルダを作成します。
  3. 作成した App_Code フォルダに WordFilter.cs ファイルを配置します。
  4. テキストエディタに表示禁止文字を , (カンマ) 区切りで入力し、BlockWordList.dic という名前で保存します。(UTF-8 で保存してください)
  5. 作成したファイル BlockWordList.dic を仮想ディレクトリのルートに配置します。
  6. 同仮想ディレクトリ内の Web.config の <configuration> 内に以下の設定を追加します。
    <add key="BlockWordListPath" value="BlockWordList.dic ファイルまでの物理パス" />
    <add key="SubstituteChar" value="×などの伏字に使用するキャラクタ" />
  7. IIS 管理ツールを起動します。
  8. 画面左のツリービューから目的の Web サイト、あるい仮想ディレクトリを選択します。
  9. [機能 ビュー] から [モジュール] アイコンをダブルクリックします。

  10. [モジュール] リストが表示されるので、画面右の [操作パネル] から [マネージモジュールの追加] リンクをクリックします。
  11. [マネージモジュールの追加] ダイアログボックスが表示されるので、[名前] テキストボックスに WordFilter と入力します。
  12. 同ダイアログボックスの [種類] ドロップダウンリストボックスから、"MyIisExtentionModure.WordFilter" を選択します。
  13. [OK] ボタンをクリックしてダイアログボックスを閉じます。

表示禁止に指定した言葉を含んだ *.aspx ファイルを仮想ディレクトリに配置し、ブラウザからアクセスして、伏字が表示されることを確認してください。

私の環境での実行結果を掲載します。"PHP" という言葉を "♥"(黒はーと) で伏字にしています。


"♥" を効果的に使い、コンテンツのセクシーさ神秘性を上げた、おしゃれ上級生のテク。

 

このサンプルコードの反省点

今回のサンプルコードの至らなかった点を以下にまとめましたので、このサンプルからインスパイアされてなんか作るさいは以下の点にご注意ください。

  1. モジュールで作成してしまつたために静的ファイルを処理しない

    処理を行い何らかのコンテキストを返す場合はハンドラで作りましょう。

  2. 正規表現の条件文字列がよくない

    正規表現の検索に使用する文字列を ">.*?<" としていますが、この指定はすべての >< 間の文字列を取得する条件であり、効率が良くありません。実際のところ処理速度は速いと言えません。"禁止ワードを含んだ >< 間" という条件を設定することで処理速���のアップが見込めます。また、日本語などの HTML のタグやファイル名に使用される可能性が低い文字列のみ検閲する場合は、正規表現を使用せずに単純に Replace 処理をしたほうが実行速度が上がります。

  3. キャッシュのクリア機能を実装していない

    今回のサンプルプログラムでは、パフォーマンス低下を防ぐため、表示禁止ワードのリストをインメモリキャッシュに保持します。このキャッシュをクリアするには、アプリケーションプールをリサイクルするか、仮想ディレクトリ内の web.config, bin フォルダ内のファイル、App_Config フォルダ内のファイルいずれかを更新します。
    なお、リストファイルの更新と合わせてキャッシュをクリアする機能を実行するには、以下のキャッシュ追加の Insert メソッドの第 3 引数にリストファイルを指定して作成した CacheDependency オブジェクトを指定します。(ただし実行して試したわけではありません)

    //リストをキャッシュに保存
    HttpRuntime.Cache.Insert("BlockWordList", blockWordList);

今回のサンプルコードはイマイチですが、念のために SkyDrive にプロジェクトをアップしましたので、コンパイルして使いたい方はどうぞ。(App_Data フォルダの代わりに Bin フォルダを作成して、dll を配置します。IIS 管理ツールでの操作は同じです。)

http://cid-a52c0f4368cad145.skydrive.live.com/self.aspx/MSDN%5E_Blog/WordFilter.zip

 

お知らせ
==========

IIS の師匠 奥主 さんと一緒に作成している"インターネット Web サーバー構築ガイドライン (ドラフト版)"が公開されました。

http://technet.microsoft.com/ja-jp/iis/ff625168.aspx

"IIS を使いたいけれど、なにから初めていいかわからない" というお客様の声から生まれました。

引き続きこれからもリリース予定ですので、周りに Web サーバーを使ってみたいという方がいらっしゃいましたらぜひご紹介のほどよろしくお願いいたします。

Real Time Analytics

Clicky

Comments (0)

Skip to main content