Quantcast
Channel: Darkthread
Viewing all 428 articles
Browse latest View live

在 ASP.NET MVC Response.End() 不會中斷執行

$
0
0

同事回報了一起奇怪狀況,追查之後又學到新東西。在我的觀念裡,Response.End() 時會立即中斷執行,有時還會觸發討厭的 ThreadAbortException。但在以下的 ASP.NET MVC 範例中,CheckAuth() 在查不到 Cookie 時會導向 /Login 並呼叫 Response.End() 結束執行,結果沒有,程式繼續往下跑,在試圖修改 Response.ContentType 時觸發 HTTP Header 送出後無法修改 ContentType 的錯誤:(修改 ContentType 需求來自 JsonNetResult )

Response.End() 不會中斷執行?這大大違背我的認知,莫非 WebForm 與 ASP.NET MVC 的行為不同?真相在原始碼裡,Use the source, Luke!

追進 HttpResponse 原始碼,很快有了答案:

        /// <devdoc>
        ///    <para>Sends all currently buffered output to the client then closes the
        ///       socket connection.</para>
        /// </devdoc>
        public void End() {
            if (_context.IsInCancellablePeriod) {
                AbortCurrentThread();
            }
            else {
                // when cannot abort execution, flush and supress further output
                _endRequiresObservation = true;

                if (!_flushing) { // ignore Reponse.End while flushing (in OnPreSendHeaders)
                    Flush();
                    _ended = true;

                    if (_context.ApplicationInstance != null) {
                        _context.ApplicationInstance.CompleteRequest();
                    }
                }
            }
        }

原來 Reponse.End() 時會依據 IsCancellablePeriod 屬性決定是否中斷執行緒。由 IsCancellablePeriod 關鍵字追到 Response.End()在Webform和ASP.NET MVC下的表现差异 - 空葫芦 - 博客园,證實了 Reponse.End() 在 WebForm 與 MVC 的行為不同。有趣的是,在該文發現保哥也追過這個問題,IsCancellablePeriod 取決於  _timeoutState 屬性值,在 WebForm 下其值為 1 (IsCancellablePeriod = true),在 ASP.NET MVC 下為 0 (IsCancellablePeriod = false),故 Response.End() 在 WebForm 下會執行 AbortCurrentThread() 在 MVC 則是 Flush() 並執行 ApplicationInstance.CompeteRequest()。

如此即可解釋 Response.End() 後會繼續執行且無法修改 ContentType。(因為在 End() 中已 CompleteRequest() )

既知原因,來看如何解決。最粗暴但有效的解法是自己模擬 AbortCurrentThead() 中止執行,HttpResponse.AbortCurrentThread() 原始碼是呼叫 Thread.CurrentThread.Abort(new HttpAppication.CancelModuleException(false));,所以我們將程式碼修改如下即可搞定。
(此舉如同 Reponse.End() 會有觸發 ThreadAbortException 的副作用,參考:ThreadAbortException When Response.End() - 黑暗執行緒)

        void CheckAuth()
        {
            //模擬Cookie檢查
            if (Request.Cookies["AuthCookie"]?.Value != "X")
            {
                Response.Redirect("/Login");
                Response.End();
                Thread.CurrentThread.Abort();
            }
        }

另一個思考方向是 CheckAuth() 改傳回 bool,呼叫時改寫成 if (CheckAuth()) { …認證成功作業... } else { return Content(null); },但如此一來,所有用到 CheckAuth() 的 Action 都要多一層 if,噁心又麻煩,不優。

而依此案例的認證需求,倒是可以回歸 ASP.NET 內建的表單驗證。CodeProject 有篇文章可以參考:A Beginner's Tutorial on Custom Forms Authentication in ASP.NET MVC Application - CodeProject,先設定 web.config

<authentication mode="Forms"><forms loginUrl="~/Login" timeout="2880" /></authentication>

/Login 認證身分成功後呼叫 FormsAuthentication.SetAuthCookie(username, false); 連自訂認證 Cookie 的功夫都免了。

如果認證邏輯再複雜,則可考量實作 IAuthenticationFilter 實現自訂認證。例如:[ASP.NET MVC]使用IAuthenticationFilter,IAuthorizationFilter實作Form表單登入認證&授權 - 分享是一種學習 - 點部落

追進原始碼,學到新東西,敲開勳~


【茶包射手日記】SQLite 資料庫出錯消失

$
0
0

最近啟動了部落格轉移計劃,打算把我的部落格從 ASP.NET + SQL 移到 ASP.NET Core + SQLite。前陣子試出 Ubuntu + SQLite + Dapper令我信心大增,後續進展也挺順利,寫了匯入程式,從 SQL 匯出物件轉 JSON,開始將 JSON 資料轉換成新平台 Model,再用 Entity Framework 寫入 SQLite 資料庫。

不料,批次匯入時發現每次一轉到某篇文章時固定出錯,最可怕的是,出錯後整個資料庫檔案會消失無蹤。

頓如五雷轟頂!喵的,這還得了,一出錯整個 SQLite 資料庫檔案消失,網站全部內容化為烏有,如果這是 SQLite .NET Core 的 Bug,誰敢用?原本寄以厚望的架構這下要作廢了,心中猶如萬頭羚羊狂奔…

冷靜了一下,想想不對。SQLite 也算當今迷你資料庫主流,應用廣泛,不應存在如此嚴重 Bug,即使有也很快會被抓到並修復。

於是再反覆測試,發現一絲曙光。

SQLite 出錯的當下,桌面右下角剛好彈出 Windows Defender 警示:

啊!是防毒軟體搞鬼,SQLite 資料庫被誤判含有病毒,檔案消失原因是被防毒軟體隔離!由 Windows Defender 掃瞄記錄證實這點,blog.db 被判定包含 JS/ShellCode.gen 病毒遭到移除。

案情急轉之下,SQLite 無罪,仍是值得信任的好夥伴,我大大鬆了一口氣,接下來便是找出到底是哪篇文章造成誤判,避開即可。

有趣的是,批次匯入作業出錯都是在試圖寫入 Distributed Transaction With MS OLEDB Provider For Oracle 時,所以是這篇文章觸發防毒警報導致資料庫被刪,但這篇跟病毒有個毛關係?不得已,我使用愚公移山法反覆測試不同組合,最後找出問題跟另一篇包含木馬程式的 有趣的木馬解剖文章有關。資料庫檔同時存在這兩篇文章會觸發 Windows Defender 誤判,單獨寫入其中任何一篇沒問題,只要別同時存在就沒事。

有趣的木馬解剖由於內含解說用的木馬程式範例,過去已有多次被防毒軟體誤抓坐黑牢的記錄(木馬解剖一點都不有趣呀),為此也改過 CLSID,調過程式碼樣本,但跟另一篇文章組合而被誤判倒是頭一遭。

一怒之下,將程式碼部分都換成圖檔,徹底解決誤判風險,事件落幕。

程式範例 - 正式台測試台 JSON + Dapper 資料搬移術

$
0
0

野人現曝,分享最近在寫的正式、測試台間的小規模資料搬移法。

情境是正式台跟測試台各有自己的資料庫,想將正式台某幾筆資料匯出,備份保存或是匯入測試台資料庫模擬測試;或是反過來,資料先在測試台輸入驗證無誤後要上線,希望將將輸入好的資料直接匯入正式台,省去在正式台重新登打的工夫。

這類情境用 Entity Framework 不難實現的,這篇介紹則介紹不用 EF 的做法。匯出匯入的前題是要有強型別的 Entity 型別,你可以手工宣告,也可借用 Visual Studio 強大的「貼上 JSON 做為類別」、「貼上 XML 做為類別」功能快速產生(參考:Visual Studio 的選擇性貼上,貼上Json作為類別 - 50懶 - 點部落)。

匯出時先用 .Query<EntityType>("SELECT * FROM Table") 產生 EntityType[],JSON 序列化後供使用者下載保存或匯入到另一套系統。實務上可透過壓縮提高傳輸與儲存效率(參考:程式範例:byte[] 不落地壓縮 ZIP 檔 - 黑暗執行緒)。

在匯入端則是先將 JSON 反序列化為 EntityType[],再來我選擇用 Reflection 自動產生 INSERT INTO 指令,再用 Dapper .Execute(InsertScript, EntityType[]) 一次塞入多筆資料,一行搞定。

程式範例如下:

       public class Player
        {
            public int SeqNo { get; set; }
            public string Id { get; set; }
            public string Name { get; set; }
            public DateTime RegDate { get; set; }
            public int Score { get; set; }
        }


        static void Main(string[] args)
        {
            string json = null;
            //模擬由伺服器A匯出Player[],再轉成JSON
            using (var cn = new SqlConnection(cs))
            {
                var data = cn.Query<Player>(
                    @"SELECT Id,Name,RegDate,Score FROM Players").ToArray();
                json = JsonConvert.SerializeObject(data);
            }
            //補充:實務應用時可使用ZIP壓縮技術
            //http://blog.darkthread.net/post-2018-08-14-zip-byte-array.aspx


            //模擬上傳JSON到伺服器B,還原後INSERT進資料庫
            var restored = JsonConvert.DeserializeObject<Player[]>(json);
            var insertScript = GenerateInserScript(restored[0], "Players", "SeqNo".Split(','));
            Console.WriteLine(insertScript);
            using (var cn = new SqlConnection(cs))
            {
                //傳入Insert Script跟物件陣列,完成資料匯入
                cn.Execute(insertScript, restored);
            }
        }


        /// <summary>
        /// 使用Reflection產生SQL INSERT腳本
        /// </summary>
        /// <param name="sample">樣本型別</param>
        /// <param name="tableName">資料表名稱</param>
        /// <param name="ignoredColNames">忽略欄位名稱</param>
        /// <returns></returns>
        static string GenerateInserScript(object sample, string tableName, 
            string[] ignoredColNames = null)
        {
            var sb = new StringBuilder();
            //此處假設tableName由開發人員決定,不開放使用者輸入,否則要防範SQL Injection
            sb.AppendLine("INSERT INTO " + tableName);

            string[] props = sample.GetType()
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Select(o => o.Name).ToArray();
            props = props.Where(o =>
                !o.StartsWith("_") && //排除_開頭及ignoredColNames列舉欄位名
                (ignoredColNames == null || !ignoredColNames.Contains(o)))
                .ToArray();
            sb.AppendLine($"({string.Join(", ", props)})");
            sb.AppendLine("VALUES");
            sb.AppendLine($"({string.Join(", ", props.Select(o => "@" + o).ToArray())});");
            return sb.ToString();
        }

透過 Reflection 產生的 INSERT 語法如下,欄位名稱前面加上@就變成變數,剛好讓 Dapper 會欄位名稱自動對應,比想像中簡單很多吧! 這裡只設計可排除特定欄位(例如:自動跳號欄位)的設計,實務若有需要,可加入客製化邏輯。

以上是小量資料匯出匯入功能的簡單示範,我們下次見。

【茶包射手日記】web.config 設定鬼故事

$
0
0

同事報案,某網站委託 OP 上線出現異常,連至本機查看詳細錯誤訊息,ASP.NET 回報看不懂 </location> 元素。

上圖的 <location path="Area51">...</location> 是本次新增設定,是應用先前介紹過的 Windows 驗證網站設定部分匿名存取技巧,用 <location> 針對部分路徑做不同設定的做法過去用得很多,不該有錯,更何況同一 web.config 的前一段就活生生是另一個 <location> 相安無事,惟獨新加的 <location> 有問題。

用記事本反覆檢查 web.config,兩個人四隻眼睛看到出血,逐字母檢查一遍又一遍,就是看不出哪裡有問題:

做了幾個測試:

  1. 將 <location path="Area51"> 改成 <location path="Shit" >,錯誤仍指向同段 <location>
  2. 將 <location path="Area51"> 該段刪除,網站正常
  3. 將 <location path="Area51"> 與前段 <location> 交換位置,錯誤仍指向 Area51 這則
  4. 將 <location path="Area51"> 該段刪除,另外重新輸入完全相同內容,網站正常

同事高呼「見鬼了」,老射手則嘟嚷:「不對,有妖氣!」。

請同事將 web.config 傳回本機用 Notepad++ 開啟,薑! 薑! 薑! 薑~ 答案揭曉,<location path="Area51"> 這段前方的縮排空白,其實是特殊字元:

這就能解釋為什麼只有這段 <location path="Area51"> 會出錯,刪掉正常,手工重打一次也正常。至於這些看似空白的字元從何而來,問了同事。原來是這段設定是透過 HipChat 交談追加的(如下圖示意),OP 直接複製對話內容貼上,未察覺縮排前方的空白其實是特殊字元。

而這些奇怪字元是什麼呢? 它們是 EN SPACE (&ensp;),UTF8 字碼為 0xe2 0x80 0x82:

以前只學過 &nbsp;(ASC 160) 其實還有 &ensp;、&emsp;,都屬於不會被合併的空白,而寬度分別為一個標準空白鍵字元、半個中文字寬及一個中文字寬(參考:HTML字元符號 &Nbsp; &Ensp; &Emsp; 的差異 - ShunNien's Blog)。

進一步爬文,在 Unicode 字元還有很多看起來像空白但不是空白的特殊字元。(參考:HTML Unicode UTF-8)

【心得】

  1. 下回遇到看不出任何異樣卻回報有錯的 config、XML、程式碼,不要只用眼睛檢查,應先以工具排除假空白及隱形字元陷阱。
  2. 想讓討厭的程式設計師同事發瘋,不用紮稻草人下降頭,偷偷在他的程式碼裡加點料就可以了。

2018 苗栗山城星光馬

$
0
0

六七八月休了三個月避暑,2018 下半年第一場由苗栗山城星光馬掀開序幕。

會場設在苗栗縣立體育場,下午四點半起跑,氣溫是嚇人的 35 度。

主持人報告本屆全馬大約有八百多人(這種天氣下場,堪稱八百壯士,對於跑步肯定是真愛),比賽也算小而美。

起跑時,起跑拱門正前方有一大片烏雲,可惜老沒飄到賽道這一帶,透過薄雲的陽光仍帶有殺氣,熱呀~

大會封了整條單向車道,寬敞好跑又不必與車爭道,缺點是風景單調了點,全馬同樣路線得跑兩趟,枯燥度加倍。水站補給中規中矩,水、運動飲料、汽水、小糕點、檸檬片、西瓜、香蕉... 應有盡有,供應也很充足,服務同學很熱情,活力十足,再加分。

雖然封了單向車道但沒全面交管,因此起跑沒多久便偶爾要等紅燈過馬路。我志不在成績,對於此種安排倒也不為意,反倒覺得能減少其他用路人的困擾,減少抱怨,是好事一椿。

穿阿媽裝的大哥,背上貼了【加油!! 阿媽跑得比你快】從我旁邊呼嘯而過... 嗚~

這兩年少跑中台灣場次,碰到久違的滾鐵圈大哥,依照往例,當然又被無情碾壓了~ 話說當年連在葡萄馬山路都被他海放,這回在平地相逄就更不用說了 Orz

跑著跑著,夕陽西下,才開始進入所謂的星光馬。

新港大橋有一段無路燈,大會在馬路兩側灑上小螢光棒,黑夜裡跑在其間很有 fu,我幻想著自己正在夜訪電影阿凡達裡的潘朵拉星球。:P

第一次造訪苗栗高鐵站。(忽然一驚! 我居然到現在還沒坐過高鐵)

第一次造訪客家圓樓,又一個因跑馬才有緣一睹的地標。

來張夜景,應該是新東大橋吧。

32 公里,真正的馬拉松考驗從這裡才開始,但拎杯已經累惹...

最後打起精神小拼一下保住 530,回終點已是晚上十點,會場溫度計顯示還有 31 度,我的完賽時間是 5:27:05,總排還在前 50%,可見戰況之慘烈。

賽後有客家發粿、長壽麵當伴手禮,簡章提到完賽有冰品可吃,滿心期待會有挫冰吃到飽,沒想到只有一支紅豆粿冰棒,還限一人一枝,有點小失望。但整體而言,仍是場令人滿意的好賽事。

久未跑外地馬,順道安排全家到苗栗民宿小住一晚。上山的路超窄,僅容一車通行兩側還是水溝,但環境清幽景觀也美,值得。

隔日返家前順路去了明德水庫的日新島晃晃,一家子都很宅的好處是哪都沒去過,去哪都新鮮,哈!

最後補上奬牌照,為久違的外地馬畫下完美的句點。

 

UrlEncode() 與空白變加號問題

$
0
0

在 ASP.NET Core 遇到轉換網址中文及特殊字元的需求,由於 .NET Core 不適合再用 System.Web.HttpUtility,爬文查到有個替代品 - System.Net.WebUtility.UrlEncode,開開心心上路卻踢到鐵板。

問題出在 System.Net.WebUtility.UrlEncode() 會將空白字元轉成加號,而 IIS7+ 預設禁止在網址使用加號,否則必須修改 allowDoubleEscaping 設定,但如此將增加風險。(延伸參考:IIS 7+禁止URL路徑使用加號代表空白 - 黑暗執行緒)

既然遇到,順手蒐集整理資料,看看 .NET 裡 UrlEncode 該怎麼寫才好。

  1. 如果是寫 .NET Framework,最普遍的做法是用 System.Web.HttpUtility.UrlEncode(),而官方文件提到 UrlEncode() 會將空白換成+,用於傳送表單時的 application/x-www-form-urlencoded 編碼 OK,但當成 URL 網址就有問題,文件建議改用 UrlPathEncode(),但 UrlPathEncode() 方法又說「這方法過時了別用,請改用 UrlEncode()!」(Do not use; intended only for browser compatibility. Use UrlEncode(String).) 這...  (第一次看到兩個 API 互踢皮球,很妙)
  2. 在 ASP.NET Core 不建議再用 System.Web,System.Net.WebUtility.UrlEncode()可為替代,但它跟 HttpUtility.UrlEncode() 一樣,空白會被轉成 +,而且沒有 UrlPathEncode() 這種版本。
  3. .NET Core 還有個套件 System.Text.Encodings.Web,提供 HtmlEncode、UrlEncode、JavaScriptEncoder 等編碼功能,可透過 System.Text.Encodings.Web.UrlEncoder.Default.Encode("...") 轉換,空白會轉成 %20,可用。但缺點是它限定 .NET Core 平台,且只為了一個函式要加裝套件似乎有點搞剛。
  4. 最後,我想起 System.Uri 有個 EscapeDataString()也可以用。System.Uri 本身是處理 URL 的專家,轉換結果用在 URL 肯定沒問題。 加上 System.Uri 為 .NET 核心型別,不管 .NET Framework 或 .NET Core 均可直接引用,評估之後是最方便的解法。
    註:Uri.EscapeDataString() 之外還有個 Uri.EscapeUriString(),二者差不多就是 JavaScript encodeURIComponent 與 encodeURI 的區別。(EscapeDataString = encodeURIComponent, EscapeUriString,詳情可參考這篇:JavaScript 網址編碼函式 escape encodeURI encodeURIComponent 的不同 @ Vexed's Blog )

【結論】

未來要將中文跟特殊字元做 UrlEncode 編碼串進 URL,JavaScript 端用 encodeURICompoent,.NET 端則一律改用 Uri.EscapeDataString() 就對了。

ASP.NET Core View 中文變 & # x4E2D; & # x6587;

$
0
0

發現 ASP.NET Core 有個特性造成困擾。

我們都知道在 cshtml 以 @textFromServerSide 嵌入字串時,預設會被 HtmlEncode 以防止 Cross-Site Scripting 攻擊,如要將字串視為 HTML 標籤處理需額外呼叫 Html.Raw()。但在 ASP.NET Core 裡,結果跟我原本想像不同,例如以下 cshtml,表面上看來正常,中文正常顯示:

@{
    Layout = null;
}<html><body><div>
        Chinese Text = @("<b>中文測試</b>")</div></body></html>

檢視網頁原始碼卻讓我大吃一驚,中文字元都被轉成 &#xhhhh; 形式! 若以 UTF-8 編碼計算,每個中文字元由 3 Bytes 變成 8 Bytes 增肥近三倍,而更令人困擾的一點,原始碼裡的中文字消失讓網頁偵錯難度驟升好幾個等級。

爬文找到解法,Prevent Cross-Site Scripting (XSS) in ASP.NET Core - Microsoft Docs文件提到 ASP.NET Core 的 TagHelper 及 HtmlHelper 預設會將所有非拉丁字元都當成特殊符號進行編碼,但此一設計對中文、西里爾文(斯拉夫語族)網頁開發者很不友善。所幸這個行為可透過設定調整,前幾天談 UrlEncode時提到 .NET Core 新推出的 System.Text.Encodings.Web.HthmlEncoder 就用在這裡。而修改方法為在 Startup.cs ConfigureServices() 加入以下程式片段,將 ASP.NET Core cshtml 透過 DI 取得的 HtmlEcoder 換成我們的自訂版本:

services.AddSingleton<HtmlEncoder>(HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,     UnicodeRanges.CjkUnifiedIdeographs }));

自訂版 HtmlEcoder 將基本拉丁字元與中日韓字元納入允許範圍不做轉碼。

經過這番手腳,網頁原始碼總算清爽多了,阿彌陀佛~

CODE - WebClient 下載檔案自動取得檔名

$
0
0

透過 WebClient.DownloadFile() 或 DownloadData() 對 .NET 老鳥而言是雕蟲小技(參考:CODE-使用C#程式從網站下載檔案 ),但此種寫法檔名需自行指定。若下載對象非靜態檔案,伺服器端程式會透過 Content-Disposition Response Header 傳回檔名供客戶端參考,WebClient 是否能由 Response Header 自動取得檔名呢?

答案是可以! 程式範例如下:

static string DownloadFile(string url, string saveFolder)
{
    using (var wc = new WebClient())
    {
        using (var stream = wc.OpenRead(url))
        {
            //若伺服器未提供檔名,預設以下載時間產生檔名
            var fn = DateTime.Now.ToString("yyyyMMddHHmmss") + ".data";
            var cd = wc.ResponseHeaders["content-disposition"];
            if (!string.IsNullOrEmpty(cd)) {
                var p = Regex.Split(cd, "filename=");
                if (p.Length == 2) fn = p.Last();
            }
            using (var file = File.Create(Path.Combine(saveFolder, fn)))
            {
                stream.CopyTo(file);
            }
            return fn;
        }
    }
}

Viewing all 428 articles
Browse latest View live