Hero Image
[防火牆] 地理位置規則(GeoIP)

最近我在 GCP 上發現一個惱人的問題:中南美洲的網路流量,竟然每個月讓我多噴了好幾十塊台幣!對於我這種「免費仔」來說,這簡直是不能接受的額外開銷。雖然金額不大,但積少成多也是錢啊! 為什麼要擋流量? 你可能會問,區區幾十塊台幣也要計較?當然要!特別是當這些流量不是你預期的合法訪客時。很多時候,來自特定地區的流量可能是: 惡意掃描:網路攻擊者在尋找網站漏洞。 DDoS 攻擊嘗試:分散式阻斷服務攻擊的前奏或小規模嘗試。 殭屍網路活動:受感染的電腦發出的無意義流量。 這些流量不僅浪費你的錢,還會增加你 GCP 主機的負擔,影響正常服務的效能和穩定性。對我們這些斤斤計較流量費用的使用者來說,擋掉不必要的流量,是省錢、維護效能和安全的好方法。 GCP NGFW 雖好,但口袋不夠深? GCP 自家的 Cloud Next Generation Firewall (NGFW) 確實提供了強大的地理位置封鎖功能。透過 NGFW,你可以輕鬆設定規則,阻擋來自特定國家或地區的 IP 流量。這聽起來很棒,對吧? 然而,問題就在於:GCP NGFW 是要收費的! 它通常會依據你處理的流量來計費,這對於想要最大化免費額度,或是對預算極為敏感的「免費仔」來說,並不是一個理想的方案。我們需要一個開源且免費的替代方案。 開源方案:iptables + GeoLite2 的完美組合 別擔心,我們還是有辦法!對於運行 Linux 主機或 VM 的使用者來說,最直接、最經濟實惠的方法就是利用 Linux 內建的防火牆工具 iptables (或新一代的 nftables),並結合 MaxMind GeoLite2 資料庫。 iptables/nftables:這是 Linux 核心層級的防火牆,可以直接在 IP 層面對流量進行精準的控制。這表示在流量到達你的應用程式之前,就可以直接被阻擋掉,達到最佳的省錢效果。 MaxMind GeoLite2 資料庫:這是一個免費的地理位置 IP 資料庫。它能告訴你每個 IP 位址的歸屬國家或地區,是我們判斷是否需要阻擋的依據。 自動化腳本:省時省力,告別手動設定 手動去找出中南美洲的所有 IP 範圍,然後一條一條地加到防火牆規則裡?光是想想都覺得頭皮發麻!幸好,開源社群已經為我們準備了貼心的工具。 我發現 friendly-bits/geoip-shell 這個 GitHub 專案是個非常棒的選擇。它能幫助你: 自動生成防火牆規則:根據 MaxMind GeoLite2 資料庫,這個腳本會自動產生針對特定國家或地區的 IP 封鎖規則,省去你手動查詢和輸入的麻煩。 排程自動更新 IP:IP 位址的地理歸屬會隨時間變化。geoip-shell 支援設定自動排程,定期更新 GeoLite2 資料庫,並重新生成防火牆規則,確保你的封鎖列表始終是最新的,讓你的防禦滴水不漏。 藉由這個組合,我們就能在不花一毛錢的情況下,精確地阻擋來自中南美洲的流量,有效控制你的 GCP 費用,同時讓你的主機跑得更輕盈、更安全。 # 安裝 geoip-shell # 根據其 GitHub 說明進行安裝 # 生成封鎖南美洲的規則 sudo geoip-shell block --countries AR,BO,BR,CL,CO,EC,GF,GY,PY,PE,SR,UY,VE

Hero Image
[.NET] C# 將 PDF 轉為列印文件送出至印表機

PdfiumViewer 是開源的 C# 控件,用於顯示和列印 PDF 文件。它基於 Chromium 瀏覽器使用的 PDF 渲染引擎 Pdfium 所開發。 而Pdfium 是 Chromium 瀏覽器使用的 PDF 渲染引擎,由 Google 和 Mozilla 共同開發。它是一個開放原始碼的函式庫,用於 PDF 文件的解碼、渲染和編輯。 PdfiumViewer 提供以下功能: 顯示 PDF 文件的所有頁面。 支持縮放、旋轉、翻頁等操作。 支持列印 PDF 文件。 在 c# 中使用 PdfiumViewer 可以將 PDF 轉換為列印文件,送至印表機進行列印。 安裝 Nuget 套件: PdfiumViewer PdfiumViewer.Native.x86.v8-xfa 引用 using PdfiumViewer; using System.Drawing.Printing; using System.IO; 程式範例 從記憶體列印: var pdfBytes = new byte[] { }; // todo: 取得 PDF by docid var printerName = ""; // todo 取得印表機名稱 // 列印 using (MemoryStream memoryStream = new MemoryStream(pdfBytes)) { var pageSettings = new PageSettings() { Margins = new Margins(0, 0, 0, 0) }; var printerSettings = new PrinterSettings(); if (!string.IsNullOrEmpty(printerName)) printerSettings.PrinterName = printerName; using (var document = PdfDocument.Load(memoryStream)) { using (PrintDocument printDocument = document.CreatePrintDocument()) { printDocument.PrinterSettings = printerSettings; printDocument.DefaultPageSettings = pageSettings; printDocument.PrintController = new StandardPrintController(); printDocument.Print(); } } } 從檔案列印: var file = ""; // todo 取得檔案路徑 var printerName = ""; // todo 取得印表機名稱 // 列印 var pageSettings = new PageSettings() { Margins = new Margins(0, 0, 0, 0) }; var printerSettings = new PrinterSettings(); if (!string.IsNullOrEmpty(printerName)) printerSettings.PrinterName = printerName; using (var document = PdfDocument.Load(file)) { using (PrintDocument printDocument = document.CreatePrintDocument()) { printDocument.PrinterSettings = printerSettings; printDocument.DefaultPageSettings = pageSettings; printDocument.PrintController = new StandardPrintController(); printDocument.Print(); } } Reference Github-PdfiumViewer Nuget-PdfiumViewer Nuget-PdfiumViewer.Native.x86.v8-xfa

Hero Image
[授權] OAuth 2.0 Authorization Framework 授權許可

授權許可 (Authorization Grant) 理解授權許可前需要先瞭解解幾個名詞: 授權碼 (Authorization Code):由授權伺服器產生,用於授權第三方應用程式存取資源的授權憑證。 存取權杖 (Access Token):由授權伺服器產生,用於允許第三方應用程式存取資源的授權憑證,時效通常較短。 刷新令牌 (Refresh Token):由授權伺服器產生,用於重新產生存取權杖的授權憑證,時效通常較長。 客戶端憑證:第三方應用程式本身用於 Basic Access Authentication 的驗證資訊,具體來說是 client_id、client_secret。 OAth 2.0 根據授權許可的方式分為四種: 授權碼許可(Authorization Code Grant) 隱含許可(Implicit Grant) 資源擁有者密碼憑證許可(Resource Owner Password Credentials Grant) 客戶端憑證許可(Client Credentials Grant) 授權碼許可(Authorization Code Grant) 第三方應用程式向授權伺服器請求授權碼,並將授權碼傳遞給資源伺服器以存取資源的授權流程,這是最常用的模式。 網站使用者在授權伺服器的登入站點申請授權碼。 授權伺服器將授權碼告知第三方應用程式 第三方應用程式使用客戶端憑證和收到的授權碼 向 授權伺服器 發起請求獲得 token。 隱含許可(Implicit Grant) 因省略對第三方應用的授權碼直接以前端網址列取得 token ,故稱隱含式許可,但直接以前端網址參數的方式傳送 token 給網站使用者,是非常不安全的作法,一般 token 的有效期間設定為 session 期間有效 (關閉網頁即失效)。 網站使用者從授權伺服器登入。 授權伺服器直接在網址列帶入 access_token 轉跳第三方應用程式。 資源擁有者密碼憑證許可(Resource Owner Password Credentials Grant) 讓第三方應用直接以網站使用者密碼取得 token,必須要是高度信任的第三方應用才能用此方法。 第三方應用程式向使用者請求其帳戶密碼(使用者憑證)。 第三方應用程式以使用者帳戶密碼向授權伺服器請求 token。 客戶端憑證許可(Client Credentials Grant) 此種 token 針發放對象為第三方應用而非用戶,與用戶認証無關,由第三方應用傳送其自己的 user credentials 獲得 token。

Hero Image
[K8s] 安裝

Prerequest 已安裝 Debian 11,並且 ssh 可連線 disable swap sed -i '/\/swap/s/^/#/' /etc/fstab swapoff -a Container Runtime (CRI-O) Forwarding IPv4 and letting iptables cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter # sysctl params required by setup, params persist across reboots cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF # Apply sysctl params without reboot sysctl --system #!/bin/bash OS=Debian_11 VERSION=1.27 echo 'deb http://deb.debian.org/debian buster-backports main' > /etc/apt/sources.list.d/backports.list apt update apt install -y -t buster-backports libseccomp2 || apt update -y -t buster-backports libseccomp2 apt install -y gnupg gnupg2 curl echo "deb [signed-by=/usr/share/keyrings/libcontainers-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /" > /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list echo "deb [signed-by=/usr/share/keyrings/libcontainers-crio-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/ /" > /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$VERSION.list mkdir -p /usr/share/keyrings curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | gpg --dearmor -o /usr/share/keyrings/libcontainers-archive-keyring.gpg curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/Release.key | gpg --dearmor -o /usr/share/keyrings/libcontainers-crio-archive-keyring.gpg apt-get update apt-get install -y cri-o cri-o-runc systemctl daemon-reload systemctl enable crio systemctl start crio Install kubeadm apt-get update apt-get install -y apt-transport-https ca-certificates curl mkdir -p /etc/apt/keyrings curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-archive-keyring.gpg echo "deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list apt-get update apt-get install -y kubelet kubeadm kubectl apt-mark hold kubelet kubeadm kubectl Creating a cluster with kubeadm # 設定 k8s server上網路 cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF sudo sysctl --system kubeadm init --pod-network-cidr=10.85.0.0/16 --cri-socket=unix:///var/run/crio/crio.sock ## kubectl 設定 ### root ```bash export KUBECONFIG=/etc/kubernetes/admin.conf non-root user make user sudor

Hero Image
[.NET] WebView2 單一檔案部屬

有些部屬環境要求能單一執行檔,如果要使用自己的 DLL 就會有問題,研究了內嵌 DLL 的作法應用於 WebView2 專案上。 以 .NET Framework 4.7.2 的 WinForm 專案為例,目標環境為 windows x64。 Dependency 安裝 Nuget 上的 Microsoft.Web.WebView2。 把這些資料夾底下的 Dll 複製到專案資料夾下,並加入版控 packages\Microsoft.Web.WebView2.1.0.1823.32\runtimes packages\Microsoft.Web.WebView2.1.0.1823.32\lib\net45 參考移除上述 Dll 參考(移除 Nuget 參考),改直接參考專案資料夾下的 Dll。 把參考的 Dll 調整為內嵌資源 AssemblyHelper.cs public class AssemblyHelper { public string Name { get { return this.assembly.GetName().Name; } } public string AppDataPath { get; set; } private Assembly assembly; public AssemblyHelper() { assembly = Assembly.GetCallingAssembly(); AppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); AppDataPath = Path.Combine(AppDataPath, Name); } /// <summary> /// Extract embeded dll to target path /// </summary> /// <param name="resourceName">Dll embed path</param> /// <param name="targetPath">Dll extract distination</param> public void ExtractEmbeddedDLL(string resourceName, string targetPath) { var targetDir = Path.GetDirectoryName(targetPath); if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) Directory.CreateDirectory(targetDir); using (Stream resourceStream = assembly.GetManifestResourceStream(resourceName)) { using (FileStream fileStream = new FileStream(targetPath, FileMode.Create)) { resourceStream.CopyTo(fileStream); } } } /// <summary> /// 設置解析組件路徑的事件處理常式 /// </summary> public void EnableEmbededManifestDll() => AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly; /// <summary> /// Assembly 解析行為 /// </summary> /// <param name="sender"></param> /// <param name="args"></param> /// <returns></returns> public static Assembly OnResolveAssembly(object sender, ResolveEventArgs args) { Assembly assembly = Assembly.GetCallingAssembly(); string project = Assembly.GetEntryAssembly().GetName().Name; string manifestItem = $"{project}.{new AssemblyName(args.Name).Name}.dll"; using (Stream stream = assembly.GetManifestResourceStream(manifestItem)) { if (stream == null) return null; byte[] assemblyRawBytes = new byte[stream.Length]; stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length); return Assembly.Load(assemblyRawBytes); } } } Program.cs internal static class Program { [STAThread] static void Main() { try { var asm = new AssemblyHelper(); // Load an extracted DLL dynamically asm.EnableEmbededManifestDll(); var loaderDllFolderPath = Path.Combine(asm.AppDataPath, "runtimes\\win-x64\\native"); var dll = Path.Combine(loaderDllFolderPath, "WebView2Loader.dll"); var loaderDllEmbedPath = $"{asm.Name}.runtimes.win_x64.native.WebView2Loader.dll"; asm.ExtractEmbeddedDLL(loaderDllEmbedPath, dll); // 將需注入 DLL 的邏輯抽離 Main 才能跑 run(loaderDllFolderPath); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private static void run(string loaderDllFolderPath) { CoreWebView2Environment.SetLoaderDllFolderPath(loaderDllFolderPath); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } 隱藏暫存檔 假設 webview2 元件變數是wv:

Hero Image
[Code] 格式化民國年

這個方法用 proxy 擴充 dayjs,讓 dayjs 支援格式化民國年。 JS 程式碼 // dayx.js import day from "dayjs"; const prototype = Object.getPrototypeOf(day()); const yyy = (dt) => (dt.getFullYear() - 1911).toString().padStart(3, "0"); const handler = { get: function (target, prop, receiver) { // age, dte, tme if (prop === "age") return day().diff(receiver, "years", false); if (prop === "dte") return receiver.format("YYYMMDD"); if (prop === "tme") return receiver.format("HHmmss"); // format if (prop === "format") return (format) => { const formattingTokens = /Y{4,}|Y{3}|[^Y{3}]+/g; const arr = format.match(formattingTokens); const format2 = arr .map((x) => (x === "YYY" ? yyy(target.$d) : x)) .join(""); console.log("ktformat", format2); return new day(target.$d).format(format2); }; // Return the original property value if (typeof target[prop] !== "function") return target[prop]; // Wrap the original function with custom behavior return function (...args) { const result = target[prop].apply(target, args); const isReturnDayjs = Object.getPrototypeOf(result) === prototype; return isReturnDayjs ? p(result, handler) : result; }; }, set(obj, prop, v, receiver) { if (prop === "dte") { v = v.padStart(7, "0"); const yyy = parseInt(v.substring(0, 3)) + 1911; obj.$d.setFullYear(yyy); obj.$d.setMonth(parseInt(v.substring(3, 5)) - 1); obj.$d.setDate(parseInt(v.substring(5, 7))); return receiver; } if (prop === "tme") { v = v.padEnd(6, "0"); obj.$d.setHours(parseInt(v.substring(0, 2))); obj.$d.setMinutes(parseInt(v.substring(2, 4))); obj.$d.setSeconds(parseInt(v.substring(4, 6))); return receiver; } return Reflect.set(...arguments); }, }; const p = (...args) => new Proxy(day(...args), handler); export default p; 型別定義 // dayx.d.ts import dayjs from "dayjs"; declare module "dayjs" { interface Dayjs { age: number; dte: string; tme: string; } } export = dayjs; export as namespace dayjs; 使用方式 可直接設定民國年,此外時間的加減乘除都可以參照 dayjs 文件