[實作筆記] 12-Factor Config 使用 Golang (with Viper) II

前情提要

在前篇提到了如何透過 Viper 來取得 config 檔與環境變數來組成我們的組態檔。
這篇我們會進一步討論,透過 Go 的強型別特性,取得組態物件,並處理巢狀的結構。

假想情境

假設今天我們要作一個金流服務,
後面可能會介接 PayPal、Strip 等等不同金流的服務,
那我的組態設定上可能就會有不同的階層關係

1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "1.10.1",
"Stripe": {
"Enable": true,
"MerchantId": 123,
"ClientId": 123
},
"Paypal": {
"Enable": false,
"MerchantNo": "A003"
}
}

那麼問題來了

  1. 我應該怎麼取用這些組態 ?
    • 可以直接透過 viper.Get("name") 取得值
    • 但是每次都要用組態名稱字串取得值,其實是一個風險;
      比如打錯字,很難有工具有效防範
    • 所以我希望建立一個 type 去定義組態檔,並透過物件去存取,
      這可以有效將寫死字串限定至一次。
  2. 有了上述的方向後,另一個問題是巢狀解構的解析,
    一般來說,我認為組態檔不應該有超過三層的深度,這次就用兩作為範例說明
  3. Viper 本身支援多種組態格式,本篇僅以 envjsonyaml 作範例說明
  4. 在上一篇提到,當存在環境變數時,環境變數的優先度應高於檔案

期待的結果

我們在啟動程式的時候,一次性將組態載入事先定義好的物件之中,
如果有環境變數,優先取用。
如果沒有環境變數,會讀開發者提供的組態檔(env 檔優先,也要能支援 json 或 yaml )
如果完全沒有組態,應該發出警告

定義 Type

首先建立我們的 Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Config struct {
Version string
Stripe StripeType
Paypal PaypalType
}

type StripeType struct {
Number int
Enable bool
}

type PaypalType struct {
MerchantNo string
Enable bool
}

接下來我們如果上一篇宣告 Of 方法用來取得組態

1
2
3
4
5
6
7
8
// define instance
var instance *Config
func Of() *Config {
once.Do(func() {
Load()
})
return instance
}

此時我們要將組態載入,先考慮開發者環境的實體檔案

1
2
3
4
5
6
7
8
9
func Load() {
vp := viper.New()
vp.AddConfigPath(".")
// todo here, change config name & type
vp.SetConfigName("config.json")
vp.SetConfigType("json")
vp.ReadInConfig()
vp.Unmarshal(&instance)
}

實作 env 使用的 config.env 檔案如下

1
2
3
4
5
Version="1.10.1.yaml"
Stripe.Enable=true
Stripe.Number=123
Paypal.Enable=false
Paypal.Merchant_No="A003"

修改 viper 相關設定

1
2
3
// just for development environment
vp.SetConfigName("config.env")
vp.SetConfigType("env")

實作 yml 使用的 config.yml 檔案如下

1
2
3
4
5
6
7
Version: "1.10.1.yml"
Stripe:
Enable: true
Number: 765
Paypal:
Enable: true
MerchantNo: "Yml003"

修改 viper 相關設定

1
2
3
// just for development environment
vp.SetConfigName("config.yml")
vp.SetConfigType("yml")

實作 json 使用的 config.json 檔案如下

1
2
3
4
5
6
7
8
9
10
11
{
"version": "1.10.1.json",
"stripe": {
"enable": true,
"number": 123
},
"paypal": {
"enable": true,
"merchantNo": "Json003"
}
}

修改 viper

1
2
3
// just for development environment
vp.SetConfigName("config.json")
vp.SetConfigType("json")

以上提供了幾種不同的 type

環境變數

我使用的環境變數如下

1
STRIPE__ENABLE=true;STRIPE__No=678;VERSION=8.88.8;PAYPAL__ENABLE=false;PAYPAL__MERCHANT_NO=ENV9999

首先,我們知道環境變數並不像 jsonyaml 檔一樣可以提供巢狀的結構,
這就與我們的需求有了衝突,好在 viper 提供了 BindEnv 的方法, 我們可以強制讓它建立出巢狀的結構,
如下:

1
2
3
4
5
vp.BindEnv("VERSION")
vp.BindEnv("Stripe.NUMBER", "STRIPE__No")
vp.BindEnv("Stripe.Enable", "STRIPE__ENABLE")
vp.BindEnv("Paypal.Enable", "PAYPAL__ENABLE")
vp.BindEnv("Paypal.MerchantNo", "PAYPAL__MERCHANT_NO")

寫在後面,在查找資料的過程中,可以發現 viper 提供了兩個功能強大的方法,
SetEnvPrefix 與 SetEnvKeyReplacer ;
SetEnvPrefix 可以自動加上前綴,SetEnvKeyReplacer 可以將分隔符置換。
可惜在我的情境尚且用不到,未來再作研究。

參考

(fin)

[實作筆記] Hexo Generate Error 處理

前情提要

我的 Blog 是透過 Hexo 這套框架建立出來的,
流程是:

  1. 撰寫文章
  2. 執行 hexo g 建立靜態檔
  3. 如果想在本地看 Blog 的效果,可以用 hexo s
  4. 執行 hexo d 部署到 Github

更多細節可以參考我以前寫得相關文章

問題

在上述的第 2 步驟,執行命令後,雖然可以產生靜態檔,
但會伴隨著以下的錯誤訊息,這就非常擾人了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FATAL {
err: [OperationalError: EPERM: operation not permitted, unlink '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'] {
cause: [Error: EPERM: operation not permitted, unlink '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'] {
errno: -1,
code: 'EPERM',
syscall: 'unlink',
path: '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'
},
isOperational: true,
errno: -1,
code: 'EPERM',
syscall: 'unlink',
path: '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'
}
} Something's wrong. Maybe you can find the solution here: %s https://hexo.io/docs/troubleshooting.html

這個錯誤訊息 OperationalError: EPERM: operation not permitted, unlink… .
是一個非常籠統的錯誤訊息,來自作業系統的底層,中間經過 hexo 與 node 的流程,
如果不深入鑽研(但是我沒有要深入),我們難以知道錯誤的細節。

好在,我發現當刪除了 public 資料夾之後,
再次執行 hexo g 就不會有錯誤訊息。

解決方法

簡單來說,我可以在每次 hexo -g 之前刪除 public 資料夾就可以了。
以下可以用一行指令替代。

1
rm -rf public | hexo -g

再進一步,我修改了 alias 指令如下

1
alias hxg="rm -rf public | hexo g"

如此一來,每次我執行 hxg 的時,就會依上述步驟先刪除再建立 public 靜態資料。
有關 alias 的設定,可以參考這篇文章

參考

(fin)

[實作筆記] 12-Factor Config 使用 Golang (with Viper) I

前情提要

最近在開發需要以微服務的形式部署在雲端之上的 web service,
為此,被要求需符合雲原生的 12 Factory
在本篇只討論第三點 Config - Store config in the environment,
簡單介紹一下這個因子;

組態應與程式分離
簡單的問自已程式是否可以立即開源,而不擔心洩露任何機敏資料??

先說明一些非微服務的組態設定實踐方式:

將組態設定以常數寫在程式當中

最糟糕的實踐,你永遠不該在一個產品中這樣作
每次改變一個環境,就要修改程式,這會使得你的服務變得脆弱

使用不同環境的組態檔,但是不納入版本控制

這是個常見的實踐,但是有著以下的缺點

  1. 開發人員將組態檔簽入版控(如果是 Git 應該可以透過 .gitignore 避免這些狀況)
  2. 配置文件很有可能分散在不同的位置,依照你所使用的語言或框架很有可能會有不同的格式(xml、yaml、json 等等…)
    這導致統一管理與維護的困難,特別是在擴展的時候。

以前在開發單體服務的時候,一個組態會達上千個設定值,
而在擴展時,開發者永遠無法理解這上千個組態應該填入什麼值
這有點超出主題了,但是微服務的架構下,單一服務不應該有上千個組態設定

有時候我們的環境不止有 ProdQAStaging
比如說開發環境、多國/語系/開發人員、壓測,
都可能導致環境組態檔案數量爆增,而難以維護。

問題 12-Factor 與開發環境

12-Factor 推薦的方式是,使用環境變數來避免上述的問題。
但在開發環境上這不是很理想的方式

對於新進的開發者:
需要自行加上環境變數,而不是下載程式後即可使用,會形成一種門檻

可以提供 script 或 bash 讓開發者執行後寫入環境變數
但很可能變成一個灰盒子(一次性執行的 script,只有需要編輯時才去會細看),
而且新增、刪除、修改組態都要修改 script,而產生額外的工作

開發者很可能同時開發多個不同服務,
這樣如果發生變數的命名衝突會難以解決。

生產環境(Prod、QA、Staging)不會有這樣的問題,
因為微服務的一個單體(Pod)應該都是單純的
加上服務名的前綴或許可以解決開發環境的問題,
但是會讓變數名變得相對冗長

解決方式

  1. 提供development.env作為開發者的組態檔
  2. 程式會優先讀取環境變數的值,才會讀取development.env的值

Golang 的實作如下:
我們使用 viper package,viper 本身包含一定的順序規則去讀取組態,如下:

  • explicit call to Set 顯示設定
  • flag 命令提示字元參數
  • env 環境變數
  • config 設定檔
  • key/value store 遠端儲存
  • default 默認值

程式如下,先讀取設定檔或環境變數的順序並沒有差,
因為 Viper 已經提供這樣功能實踐(env 環境變數優先於 config 設定檔)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var instance *Config

func Load() {
// new viper
vp := viper.New()
// just for development environment
vp.SetConfigName("development.env")
vp.SetConfigType("env")
vp.AddConfigPath(".")
vp.ReadInConfig()
// follow 12 factors principles https://www.12factor.net/config
vp.AutomaticEnv()
// bind to struct
vp.Unmarshal(&instance)
}

structdevelopment.env ,純量或是巢狀結構都可以支援,
見以下的程式 Config 內部包含另一個 struct NestConfig

1
2
3
4
5
6
7
8
9
type Config struct {
Port string `env:"PORT"`
Nest NestConfig `env:"NestConfig"`
}

type NestConfig struct {
MarsenNo int `env:"MarsenNo"`
MarsenFlag bool `env:"MarsenFlag"`
}

development.env範例如下

1
2
3
Port=9009
Nest.MarsenNo=1231456
Nest.MarsenFlag=1

Config 的使用:
這裡用了 singleton 的方法實作,有查看了一些文章可能會有問題,未來可能會再調整

1
2
3
4
5
6
7
8
var instance *Config

func Of() *Config {
if instance == nil {
Load()
}
return instance
}

完整代碼可以見此

未解之問題

提供給開發者使用的組態,仍有機敏資料進版的疑慮?
考慮使用 Consul ???
或是改成 docker image 並在其中寫入環境變數 ???

參考

(fin)

[閱讀筆記] Learn Go with tests

前情提要

在學習 Go 的過程中偶然發現的資源,
作者提供了一種有效學習新的語言(Go)的方法—測試。
我很認同這個想法,作者的文章簡單易懂,配合上測試案例,很快就能掌握 Go 語言的幾大特性,
同時你也會了測試,很酷的是,Golang 本身的測試語法就很好用,除了 Mocking 那個部份需引入外部資源外,
其它就內建其中了,也就是說你不會像 C# 需要面對選擇的障礙(MsTest、NUnit、XUnit),
同時網路上已經有簡體中文的資源(就像它們的文字一樣有些許殘缺,但對英文不夠好的人也是個福音了)

測試心法

Mocking 是萬惡之源嗎?

通常,如果你的程式需要大量 mocking 這隱含著錯誤的抽象。
而背後代表的意義是你正在作糟糕的設計。

What people see here is a weakness in TDD but it is actually a strength,
more often than not poor test code is a result of bad design or put more nicely,
well-designed code is easy to test.

測試的壞味道與方針

如果你的測試變得複雜,或是需要 Mocking 需多依賴,這一種壞味道,
可能是你的模組負擔太多的事情,試著去切分它。(注:沒切或切太塊,一個模組要運作要 mocking 數 10 個相依)
或是依賴關係變得太細緻,試著將適當的模組作分類(注:切太細,a 依賴 b、b 依賴 c……一路 mocking 到天涯海角)
或是太注重細節,應專注於行為而不是功能的實現(注:太注重細節會變成整合測試,需要完成的功能實現)

1
2
3
4
5
6
- The thing you are testing is having to do too many things (because it has too many dependencies to mock)
- Break the module apart so it does less
- Its dependencies are too fine-grained
- Think about how you can consolidate some of these dependencies into one meaningful module
- Your test is too concerned with implementation details
- Favour testing expected behaviour rather than the implementation

傳說中 KentBeck 大叔說的過的話

Make It Work Make It Right Make It Fast

在本書中 work 意謂通過測試,right 是指重構代碼使意圖明顯好懂,最後 fast 才是優化效能。
如果沒有 workright 之前是無法變 fast

章節 reflection 的重構步驟

參考章節 reflection,當完成 slice 的 test case 時候,
程式已經變得相當噁心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func walk(x interface{}, fn func(input string)) {
val := getValue(x)

if val.Kind() == reflect.Slice {
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
return
}

for i := 0; i < val.NumField(); i++ {
field := val.Field(i)

switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}

我來探討一下重構的思路,不然書中的重構步驟(對我來說)太快了,無法掌握變化的過程。
首先,我們有一個前提是每個步驟完成都要跑測試,並全數通過才行。
除了 Production Code 我們不會異動測試的任何程式碼。

消除 return

這一步應該不難理解,我們用 switch 語法取代 if return 的寫法
這樣讓我們的程式更有整體性,而不是被 if return 切分成兩個區塊
但是這樣又產生了新的壞味道,巢狀 switch
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
switch val.Kind() {
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
default:
for i := 0; i < val.NumField(); i++ {

field := val.Field(i)
switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}
}

巢狀 switch

先來看一下這個巢狀 switch 的條件判斷為何?
我們可以發現兩個 switch 最終都是在對 .Kind() 作判斷,
這帶來了可能性,
我們可以把內層 switch 的處理往上提昇
下層使用遞迴呼叫 walk(... 如果內層的 case 都被提昇至上層,
那麼內層的 switch 就可以被剝離

巢狀 switch : case reflect.String

先把 case reflect.String: 往上層提昇
內層保留 case ,但改呼叫遞迴,執行測試,全部通過

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
switch val.Kind() {
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
case reflect.String:
fn(val.String())
default:
for i := 0; i < val.NumField(); i++ {

field := val.Field(i)
switch field.Kind() {
case reflect.String:
walk(field.Interface(), fn)
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}
}

巢狀 switch 消除重複

在本篇我只對內層的 switch 進行重構,
下面我只展示內層的 switch。
兩個問題,

  1. case 重複(這是我們刻意製造出來的)→ 所以要消重複
  2. 消除重複後,參數 field 就變得有點多餘,我們可以用 inline 的手法消除
1
2
3
4
5
6
7
field := val.Field(i)
switch field.Kind() {
case reflect.String:
walk(field.Interface(), fn)
case reflect.Struct:
walk(field.Interface(), fn)
}

測試通過後,我們的內層 switch 就會變成這樣

1
2
3
4
switch val.Field(i).Kind() {
case reflect.String, reflect.Struct:
walk(val.Field(i).Interface(), fn)
}

巢狀 switch default case

這個時候我們看整體的程式碼,會發現一個怪異的現象,
外層的 default 值會直接視作存在多個 Field 進行遞迴拆解 for i := 0; i < val.NumField(); i++ {
內層的 switch 語法只有一個 case 同時處理 reflect.Structreflect.String 兩種條件,
以邏輯來說,外層的 default 只會處理 reflect.Struct 其它的資料型態都不處理,
reflect.String 在同層的 switch 其它條件被處理掉了
所以我們可以把 default 的區段改寫如下,執行測試,通過

1
2
3
4
5
6
7
8
case reflect.Struct:
for i := 0; i < val.NumField(); i++ {
switch val.Field(i).Kind() {
//case reflect.String, reflect.Struct://這個寫法也可以
default://這個寫法比較有交換率的等價概念
walk(val.Field(i).Interface(), fn)
}
}

這時候內層的 switch 就變得相當的多餘,可以整個拿掉。
執行測試,通過。
最後的程式重構就會與書上的一致,十分雅緻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
switch val.Kind() {
case reflect.String:
fn(val.String())
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
case reflect.Struct:
for i := 0; i < val.NumField(); i++ {
walk(val.Field(i).Interface(), fn)
}
}
}

參考

(fin)

[實作筆記] 錯誤處理 dotnet core @ GKE logger

問題

為了追蹤 Dotnet Core 專案上的 log ,
參考這篇文章進行了設定,
沒想到在 GKE 上啟動服務時,會產生錯誤,導致服務啟動異常

1
2
3
4
system.io.ioexception:
error loading native library "/app/runtimes/linux-x64/native/libgrpc_csharp_ext.x64.so".
error loading shared library ld-linux-x86-64.so.2:
no such file or directory (needed by /app/runtimes/linux-x64/native/libgrpc_csharp_ext.x64.so)

原因

看起來是專案啟動之時,會需要底層的 libgrpc_csharp_ext.x64.so 檔案。
tl;dr
簡而言之,我使用的 docker image mcr.microsoft.com/dotnet/aspnet:6.0-alpine
裡面沒有包 Grpc.Core ,但是 Grpc.Core 壞壞要被 grpc-dotnet 汰換(tl;dr)
所以要嘛你自已在 alpine image 自已裝上 libgrpc_csharp_ext.x64.so
或是使用一個不公開的 image 版本
ex:6.0.1-cbl-mariner1.0-distroless-amd64
這些不公開的版本可以在這裡查到喔

參考

(fin)

[實作筆記] 錯誤處理 the gcp auth plugin is deprecated

前情提要

環境 Mac OS,
安裝了
Docker daemon 版本 4.8.2(79419)
gcloud
版本資訊如下

1
2
3
4
5
gcloud --version
Google Cloud SDK 389.0.0
beta 2022.06.03
bq 2.0.74
core 2022.06.03

執行以下指令時

❯ kubectl -n qa get secret deposit-secret -o json

會出現以下的警告訊息

1
2
W0608 15:50:50.291340   13396 gcp.go:120] WARNING: the gcp auth plugin is deprecated in v1.22+, unavailable in v1.25+; use gcloud instead.
To learn more, consult https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke

原因

在安裝 Docker 時,會同時安裝上 kubectl
我們可以用以下的指令作查詢,

1
2
❯ where kubectl
/usr/local/bin/kubectl

這個時候我們可以看看這篇,
簡而言之,就是在 kubectl v1.25 版本後,
對授權的處理作了一點變化,改用 plugin 處理。
所以當使用舊版本( < v1.25 版)時會出示警告

解法

兩個步驟
移除原有的 kubectl 並透過 gcloud 安裝新版本的 kubectl

1
sudo rm kubectl

這是在安裝 Docker 時產生的。
如果你想查看 link 的資訊,可以在移除前用以下指令切換到系統目錄

❯ cd /usr/local/bin

再透過 ll 檢視細節,大概的回應如下

1
2
3
4
5
❯ ll
total 412936
...略
lrwxr-xr-x 1 root wheel 55B 6 8 15:45 kubectl -> /Applications/Docker.app/Contents/
...略

gcloud 安裝 kubectl

1
gcloud components install kubectl

出現以下訊息再按 y

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Your current Google Cloud CLI version is: 389.0.0
Installing components from version: 389.0.0

┌──────────────────────────────────────────────────────────────────┐
│ These components will be installed. │
├─────────────────────┬─────────────────────┬──────────────────────┤
│ Name │ Version │ Size │
├─────────────────────┼─────────────────────┼──────────────────────┤
│ kubectl │ 1.22.9 │ 65.2 MiB │
│ kubectl │ 1.22.9 │ < 1 MiB │
└─────────────────────┴─────────────────────┴──────────────────────┘

For the latest full release notes, please visit:
https://cloud.google.com/sdk/release_notes

Do you want to continue (Y/n)? y

再次執行 kubectl 相關的指令就不會噴錯了

參考

(fin)

[實作筆記] 貢獻微軟文件 MSDN 記錄

起因

專案上我使用 .net core 並且有需要使用到 Config 相關的設定,
在看文件時不小心,查錯了文件(查到了這一篇 ContextInformation.GetSection(String) Method)。

這裡寫到 configuration section 是區分大小寫的(case-sensitive)

Remarks

When specifying a section within the configuration,
note that the name of the configuration section is case-sensitive.

而我實測時,才發現並沒有區分大小寫,才注意到我查錯了文件

起念

進一步我去微軟 MSDN 找到正確的文件查詢 ConfigurationManager.GetSection,發現並沒有註明是否區分大小寫。

所以我就想貢獻一下成就一之已力,
幫微軟加上說明,並好奇多久可以將修改的文件上線

歷程

首先只要點選右上角的鉛筆圖示,就可以連線至 Github 頁面,
很快的我完成了修改並且發出了 Pull Request
發出後很快就有自動化的機器人 dotnet-issue-labeler 與 opbld33 進行審查。

接下來有人工審核,Gewarren 很友善的給我一些建議,
大概就在我發出 PR 的幾個小時內,不過我自已的動作沒那麼快,
我過了 5 天才發現,並依照建議完成了修改(我想他們應該有一些文件修改的 Guide Line),
修改完後送出,一樣會有自動化審核,Gewarren approve 後,再有 3 個自動化的檢查,

  • snippets-build
  • OpenPublishing.Build Validation status: passed
  • license/cla All CLA requirements met.

接下來就 merged,這是 2022/05/17 ,不知道多久才會更新文件。

結尾

2022/05/23 我已經在微軟官方文件見到更新,小小成就達成 🎉

(fin)

[實作筆記] 滲透測試報告、原因與調整方針

前情提要,測試報告

測試廠商使用 Acunetix 掃描與 Burp Suite 攔截封包進行測試:
報告為以下幾個 Header 未加而使網站存在風險

  1. X-Frame-Options
  2. X-XSS-Protection
  3. X-Content-Type-Options
  4. Strict-Transport-Security
  5. Content-Security-Policy

除了第五項,以下將會介紹各個 Header 賦於瀏覽器的行為,以及防範何種可能的攻擊

Header 的用意

X-Frame-Options

  • 新版的瀏覽器使用 frame-ancestors
  • 用意:防範 ClickJack 攻擊

    Click Jacking 直譯為點擊劫持,常見的攻擊手段為,
    攻擊者透過 frame 嵌入受害網站;當受害者誤入攻擊者網站時(常用社交攻擊),
    會誤以為是正常網站。
    接下來就可以誘騙受害者,ex:輸入帳密,或是點擊特定連結或下載惡意軟體。

選項

  • DENY
  • SAMEORIGIN
  • ALLOW-FROM <uri>

X-XSS-Protection

  • 新版的瀏覽器透過 CSP (Content-Security-Policy) 來防止 XSS 攻擊
  • 用意:為提供舊版的瀏覽器防範 XSS 網站攻擊,這不屬於任何正式的規格書或草案之中

    XSS(Cross-Site Scripting),直接跨站腳本攻擊,手段非常的多樣,
    大多是在受害網站注入腳本語法(javascript),使用者瀏覽時就會遭受攻擊。

    ex:在留言區留下無限 loop 的 alert ,如果對方的網站沒有防範,
    使用者瀏覽留言區的時候,就會彈出關不完的 alter 視窗。
    (真正惡意的攻擊會更神不知鬼不覺)
    參考

選項

  • 0
    禁止 XSS 過濾。
  • 1
    啟用 XSS 過濾(通常瀏覽器是默認的)。如果檢測到跨站腳本攻擊,瀏覽器將清除頁面(刪除不安全的部分)。
  • 1;mode=block
    啟用 XSS 過濾。如果檢測到攻擊,瀏覽器將不會清除頁面,而是阻止頁面加載。
  • 1; report=<reporting-URI> (Chromium only)
    啟用 XSS 過濾。如果檢測到跨站腳本攻擊,瀏覽器將清除頁面並使用 CSP report-uri (en-US)指令的功能發送違規報告。

X-Content-Type-Options

  • 通知瀏覽器,不要自行猜測(MIME type sniffing)Content Type

原先 MIME type sniffing 是上古時代被設計來避免開發人員誤設 Content Type,
比如說明明是 javascript 檔, content Type 卻被設定 text/plain,
這時瀏覽器 sniff 判斷為 javascript 後,可以執行該 js 檔
但是被用來惡意攻擊,比如說連結 src/fake.jpg 看起來像一張圖片
實際上 Response 卻回傳可執行的 javascript (內含惡意的程式),
瀏覽器的 sniffing 行為會自行解析為腳本(javascript)並執行攻擊

選項

  • nosniff

Strict-Transport-Security

HTTP Strict-Transport-Security 回應標頭(簡稱為 HSTS (en-US))告知瀏覽器應強制使用 HTTPS 以取代 HTTP。
現代的瀏覽器優先使用 HTTPS 已是共識,不加這個 Header 也會出現不安全的警告

選項

  • max-age 指定秒數
  • includeSubDomains 包含子網域

Content-Security-Policy

這個坑很深,我另外再撰文說明,
可以透過另一個 Header Content-Security-Policy-Report-Only
產生報告逐一修正 Policy。

補充

如何使用 How to use testssl

testssl.sh 是一個免費的 command tool,
它檢查任何端口上的服務器服務是否支持 TLS/SSL 、
協議以及最近的密碼缺陷等。

安裝

1
brew install testssl

使用

1
testssl.sh {uri}

如何在 GCP Bucket 加上 header

GCP > Network Services > Load Balancing

  • Edit
  • Backend Configuration
  • 找到特定的 Bucket 點擊鉛筆符號
  • Edit backend bucket
  • 展開 ADVANCED CONFIGURATIONS (RESPONSE HEADERS)
  • Custom response headers > ADD HEADER
  • 輸入 Header Key 與 Value
    • X-Frame-Options: DENY
    • X-XSS-Protection: 1;mode=block
    • X-Content-Type-Options: nosniff

補充: 如何使用 Burp Suite 觀察

概念說明:
Burp 可以在你的本機建立一個 Proxy Server,
並透過它提供的瀏覽器(Chromium)連線到你指定的 uri 並進行欄截。

You ←→ Burp(Proxy) ←→ Internet。

注意: 網路上大多數的文章都會有 Proxy 的設定,其實這是可以省略的,
新版本的 Burp 用提供的瀏覽器就會連結 Burp Proxy。

  1. 下載並安裝 Burp Suite,Burp Suite Community Edition 版本即可
  2. 選擇 Temporary project > Next
  3. 選擇 Use Burp defaults > Start Burp
  4. 開啟攔截器: Proxy > Intercept is on
  5. 開啟網站: Proxy > Open Browser(Chromium) > Key In <target site URL>
  6. 決定欄截或丟棄 Proxy > Forward / Drop
  7. 查看歷史記錄 Proxy > HTTP / WebSockets history

參考

(fin)

[生活筆記] 2022 新冠(武漢)肺炎疫情

進況

2021 年 5 月 ~ 7 月進入 3 級後,理論上都是在 2 級的狀態,
入境人數每日約有 50~100 名確診者入境,
到了 2022 年 4 月以來,疫情指數成長
爆
近日單日確診已破 90000 例。

重複發生的事

  • 2020 搶/吵口罩
  • 2021 搶/吵疫苗
  • 2022 搶/吵快篩

該作的事

  • 載口罩
  • 勤洗手
  • 少去人多的地方
  • 注意自身生心理的變化

現況的因應方針

準備適量的快篩,以 2 人的小家庭其實 1 盒(5 劑)已經夠用,
買不到就作好該作的事,台灣的狀況是可以在幾周到幾個月內調整到位的。
(參考口罩、疫苗與雞蛋的狀況)
無症狀就正常生活,保持樂觀,不要過度被搧動性的偽新聞網軍談話節目影響,
如果真的要看,請深入研究各方的說詞,不要因喜好而偏重某些節目。

自覺有症狀先快篩,
陰性就保持正常生活,擔心的話請就醫。
陽性參考 CDC 指示,自我隔離或就醫,並聽從醫囑。

有症狀而無法篩檢的就可以先自我隔離,
所幸我的工作模式可以遠端處理大部份事宜,
家中也沒有老小,並且都接種過三劑疫苗。
正面面對目前的疫情,並不想介入過多的口水,
看數據作事,或許有點無情但是大局來說才是正確的方向。

分不清的名詞

角色有確診者、密切接觸者、其它
方針居家檢疫、居家隔離`、居家照護、
隔離、自主健康管理、自主防疫、
自我健康監測 ??、自主應變 ??

總覺得每天都在產生新名詞呢

資訊的取得

  • 參考國外數據
  • 聽取 CDC/(所在)縣市記者會
  • 多方參考海外新聞、數據報表
  • 盡可能獲取一些科普、醫療相關的知識,但是要注意背後的科學支持是否可信

心態上不要盲信,也不需要被風向帶到彼此敵視

未來的方向

長期來說,只能試著與病毒共存,
讓整體的生活恢復正常,問題是這個過程是要付出多少的代價 ?

第一個全面接種疫苗,
我們的第一劑覆蓋率只有 85% ,第二劑 80% ,第三劑 69%(資料來源 202205021,GOOGLE
對比世界其它國家不算低,但是疫苗的效力會隨時間下滑,所以定期要打加強劑,
此外仍有特定族群無法接種疫苗,特別為小孩與慢性病者,應儘快加速孩童疫苗的進口與接種.
而之前打氣下滑導致疫苗過期而被丟棄,實在十分浪費。

指揮中心發言人莊人祥 3 月底曾表示,莫德納及 BNT 約有 26.4 萬劑、AZ 則約 30 萬劑,
共計 56.4 萬劑疫苗因到期得銷毀。根據疾管署最新統計,
台灣 COVID-19 疫苗接種人口涵蓋率第 1 劑 84.57%、第 2 劑 79.84%、追加劑接種率 58.24%。

ETtoday <<3 天 1.6 萬例!瘋搶疫苗 醫嘆:丟掉你罵浪費,現在罵打不到?>> 記者陳俊宏/綜合報導

第二個,在不破壞醫療量能前提下,儘可能讓抵抗力高的人感染而達成自然免疫。
不論如何會有人因此死亡,特別是未接種者,
雖然接種疫苗中壯年在數據上中重率不高,
但是對於個體而言,代表的是一個活生生的生命。
沒有人想當那個 0.03 %。
不要過渡消耗醫療量能,
目前快篩的排隊或是 PCR 的排隊,有些人是為了領取保險金,取得確診証明。
無症或輕症者,其實正在壓縮真正需要就醫的人的生命,
讓醫護人員花費大量時間在作文書與檢驗作業。

無症或輕症者,雖是下策,可以透過線上或是購買成藥等方式來撐過疫情。
如果藥物足夠的話,快篩陽且有診狀,醫師就該判斷是否進行投藥,避免後續重症的發生。

(fin)

[活動筆記] 線上活動 TypeScript tips and Tricks with Matt

前情提要

YouTube 頻道Visual Studio Code
前陣子推出了由大神 Matt 所介紹有關 TypeScript 奇技淫巧影片
相當燒腦,特別作此記錄。

5:06 generic-table-component

難度:★★☆☆☆

這題需要有一些 React 的基礎概念,
首先我們有一些 React Component(以下簡稱 RC),
並且用 interface 建立相對應 props。

問題是如果要讓我們的 RC 更泛用應該怎麼作,
在這個例子中,我們建立了一個 Table RC(以下簡稱 Table) ,並且在 props 中傳入一些參數,
這個參數的型別,直接與 Table 產生了耦合,我們可用泛型(Generic)處理。

Before

1
2
3
4
5
6
7
8
interface TableProps {
items: { id: string }[];
renderItem: (item: { id: string }) => React.ReactNode;
}

export const Table = (props: TableProps) => {
return null;
};

After

1
2
3
4
5
6
7
8
interface TableProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}

export const Table = <T,>(props: TableProps<T>) => {
return null;
};

這裡是很簡單的泛型觀念,如果有寫過具備泛型的語言(ex:C#)應該不難理解,
另外需要注意的一個有關 React 的小小 Tricky
注意到 <T,> 這裡有一個小小的逗號,其實是不必要的,
但是在 React 的開發環境之中,角括號<>會被視為未封閉的 RC;
封閉的例子,ex:<myComponent></myComponent> 或是 <myComponent />
未了必免編輯器的警告,需要加上一個逗號,

另外這裡在寫 React Component 時,使用了 arrow function,
我們也可以換成一般的 function 寫法,這種寫法就可以省略 <T,> 這樣 tricky 的寫法

1
2
3
export const Table = function <T>(props: TableProps<T>) {
return null;
};

泛型不難理解,但是考量到需要具備的 React 知識,難度我給他兩顆星

11:19 get-deep-value

難度:★★★☆☆

這題要動態的取出物件深層的屬性的型別,
具體好處是可以讓編輯型別檢查

看一下影片中的範例 getDeepValue 是一個函數,可以用來取物件深層屬性的值
這裡我們看到 obj:any Matt 說道可以視作一個壞味道(不知道他對 unknown 的看法)

1
2
3
export const getDeepValue = (obj: any, firstKey: string, secondKey: string) => {
return obj[firstKey][secondKey];
};

看一下原始範例如下

1
2
3
4
5
6
7
8
9
10
const obj = {
foo: {
a: true,
b: 2,
},
bar: {
c: "12",
d: 18,
},
};

我的作法

如果比較直觀的寫法可以考慮寫個 ObjectType,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// this one is NOT from the sample

const obj: ObjType = {
foo: {
a: true,
b: 2,
},
bar: {
c: "12",
d: 18,
},
};

type FirstKeyType = "foo" | "bar";
type SecondKeyType = "a" | "b" | "c" | "d";
type ObjType = {
[key in FirstKeyType]: { a: boolean; b: number } | { c: string; d: number };
};
export const getDeepValue = (obj: ObjType //...

註:這裡有用到 index-signatures 的概念
看看官方文件的說法:

1
2
3
Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values.

In those cases you can use an index signature to describe the types of possible values
1
const value = getDeepValue(obj, "foo", "a");

Matt 在影片中的作法

1
2
3
4
5
6
7
8
9
10
11
export const getDeepValue = <
TObj,
TFirstKey extends keyof TObj,
TSecondKey extends keyof TObj[TFirstKey]
>(
obj: TObj,
firstKey: TFirstKey,
secondKey: TSecondKey
) => {
return obj[firstKey][secondKey];
};

keyof 關鍵字可以直接參考官方文件

比較兩者的作法

我們加上了 Type,就是要為程式碼加上保護
我跟 Matt 的作法,下面的程式碼都會提出警告

1
var error_value = getDeepValue(obj, "error", "wrong");

但是無法避免以下的錯誤

1
var error_value = getDeepValue(obj, "foo", "c");

obj.foo 是不存在 c 屬性,這樣我們就需要動態判斷型別
看看 Matt 使用 keyof 作法。

1
2
3
4
5
6
7
export const getDeepValue = <TObj, TFirstKey extends keyof TObj>(
obj: TObj,
firstKey: TFirstKey,
secondKey: keyof TObj[TFirstKey]
) => {
return obj[firstKey][secondKey];
};

這裡有點不可思議,首先是 ts 的動態型別推導,
在呼叫方法 getDeepValue 的第 1 個參數會動態推導出 TObj 的型別
大概等價以下的型別

1
2
3
4
5
6
7
8
9
10
type TObj = {
foo: {
a: boolean;
b: number;
};
bar: {
c: string;
d: number;
};
};

keyof TObj 的值會是 foo | bar
而 TFirstKey 的型別,在第 2 個參數傳入之時決定是 foobar
不是上述兩種的參數傳入時,編輯會拋出錯誤警告,非常好用。
下一個燒腦的部份,第 3 個參數 secondKey: keyof TObj[TFirstKey]
透過類似陣列取值的手法,我們可以在傳入第 2 個參數時決定第 3 個參數的值,並且讓編輯器提供保護。
例如,第 2 個參數為 foo 時,第 3 個參數會限定只能傳入 "a"|"b"

這裡需要思考一下,並不難理解,所以給 3 顆星。

如果能應用得好的話,對於開發 ts Library 會相當有幫助
不過我覺得自已無法用得很得心應手,比如說,如果物件深度不固定,應該如處理?
又或者如果不用 keyof 的技巧,對一個已知的物件,我能不能作到類似的效果(編輯器動態檢查)?

28:54 conditional-types

難度:★★★☆☆

這其實是 TypeScript 一個基本的概念與技巧,
可以在官方文件看到更多資訊

一個標準的寫法如下

1
SomeType extends OtherType ? TrueType : FalseType;

記得這裡最終只會回傳 TrueType 或是 FalseType,而 SomeType 只是作為條件檢查。
當然也可以作多重的條件檢查

1
2
3
4
5
SomeType extends OtherType ?
TrueType :
SomeType extends OtherConditionType?
OtherThing :
FalseType;

35:20 更進一步

我們看一下怎麼應用 Condition Type 這個技巧?
參考error-messages-in-ts這個教案
在 deepEqualCompare 這個方法之中,陣列比較需要例外處理,
因為陣列比較永遠會回傳 false
原本的作法,會在程式的 runtime 作型別檢查並拋出例外

1
2
3
4
5
6
7
export const deepEqualCompare = <Arg>(a: Arg, b: Arg): boolean => {
// runtime handle
if (Array.isArray(a) || Array.isArray(b)) {
throw new Error("You cannot compare two arrays using deepEqualCompare");
}
return a === b;
};

而透過 condition type,可以讓編輯器在開發時直接檢查,看起來更加的優雅

1
2
3
4
5
6
export const deepEqualCompare = <Arg>(
a: Arg extends any[] ? "not allow array" : Arg,
b: Arg extends any[] ? "not allow array" : Arg
): boolean => {
return a === b;
};

這應該算是 TypeScript 的基本工,也有在官方文件上有很清楚的講解,
對我來說,第一次看到仍然相當驚訝,我給他三顆星
另外,我有兩個延申問題,

  1. 程式碼中有 any 算不算壞味道? 我是不是應該用 unknown[] 取代 any[]
  2. Arg extends any[] ? "not allow array" : Arg 明顯出現了重複,我有沒有什麼技巧可以消除這樣的重複?

其它

整個影片還有許多燒腦的部分,
Q&A 或是最後的挑戰都有相當多有趣的東西的可挖掘,
有機會我再補上。

參考

(fin)