[實作筆記] 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 本身支援多種組態格式,本篇僅以 env、json、yaml 作範例說明
  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

首先,我們知道環境變數並不像 json 或 yaml 檔一樣可以提供巢狀的結構,
這就與我們的需求有了衝突,好在 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 等等…)
    這導致統一管理與維護的困難,特別是在擴展的時候。

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

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

問題 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)
}

struct 與 development.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 才是優化效能。
如果沒有 work 與 right 之前是無法變 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.Struct 與 reflect.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 個參數傳入之時決定是 foo 或 bar,
不是上述兩種的參數傳入時,編輯會拋出錯誤警告,非常好用。
下一個燒腦的部份,第 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)

Please enable JavaScript to view the LikeCoin. :P