前言
前陣子我為了幫自己的學生們更熟悉 HTTP 以及 API 的串接,寫出了一個小遊戲:Lidemy HTTP Challenge,需要根據每一關的說明取得正確的 token,一共有十五關,前十關基本,後五關進階。
經過了一些朋友的測試之後,慢慢調整、改善,最後讓學生測試發現反應都不錯,於是就在前端社群正式對外公開這個遊戲,讓大家也能一起參與。
如果你還沒玩過,那強烈建議你不要看這篇文章,因為這篇文章會破壞你遊玩的興致(大概就跟電影爆雷一樣)。建議可以先去玩一下,等全破了再回來看這篇文章,會得到一些不同的收穫。
接下來我會講一下這個遊戲誕生的歷程以及每個關卡的設計。
前人種樹,後人乘涼
這種以遊戲當做外殼,內容卻是滿滿技術的手法大家應該並不陌生,至少我很不陌生。
最一開始有想要做成遊戲的這個想法,其實是因為一個學生傳給我這個:devtest,這是法國某一間公司的面試考題,如果你看到畫面一片白,那絕對不是網頁壞掉,不用擔心。
破完了上面這個遊戲之後,我才想起自己對這種模式其實很不陌生。小時候玩過高手過招,也玩過類似 Hack This Site! 的遊戲。
或是以前有一陣子解謎遊戲正夯(跟程式無關的那種),我也曾經自己做了一個,那時候是用 PIXNET 文章鎖密碼的方式來做關卡,現在想想真是個非常方便的方法。
總之呢,雖然小時候都玩過,但長大之後卻慢慢忘記還有這種模式。這種模式的好處就在於它是遊戲。遊戲做得好,每個人都會愛上它。而且會比一般的你問我答或是簡答題有趣多了,所以遊戲是個很好的切入點。
想起遊戲的好處以後,我就決定要自己來做一個了,而主題就是之前我的學生們最不熟悉的串接 Web API!
最初的構想
最一開始的想法是:
我希望這是一個你用 curl 也可以玩的遊戲
因為我覺得這樣很 cool,你用 terminal 搭配指令就可以玩這個遊戲,甚至連瀏覽器都不需要開!
因此,在畫面的呈現上從最初我就打算是純文字的,沒有任何連結或是花花綠綠的東西,它就是個純文字檔!如果有連結也不會有 <a>
,就只會是個 URL 而已。
形式的話,就是比照其他遊戲用闖關的。
大致上都定好之後,就是要來決定關卡的內容要有哪些。在一開始我原本想做二十關,後來列一列想要出現的題目,發現大概只能做個六七關左右。
原本的構想如下:
- CURD 一定要有,要讓學生會串 API 的這四種基本操作
- custom header 一定要有
- origin 相關的題目一定要有
- user agent 相關的題目一定要有
後三個一定要有是因為我覺得這在理解 HTTP 跟串 API 上面也很重要。
custom header 常常會來帶一些額外資訊,或是最常做的就是驗證。origin 的話想讓學生理解 same origin policy 只跟瀏覽器有關,脫離了瀏覽器就完全沒有這個限制。user agent 則是工作上滿實用的,會需要判斷使用者的瀏覽器或是偵測是不是搜尋引擎來做相對應的處理。
要出現的東西大致上有想法了,最後就是實際的內容跟 token 的設計。遊戲的內容如果只有:「請你 POST 一筆資料去 XXX」太無聊了,所以我把場景設定在玩家是一個去圖書館幫忙的新手,要幫助老爺爺解決一些圖書資訊系統的問題。
至於書籍資料我就很快去某網站爬了一下然後稍微做處理,資料的部分就這樣很快搞定了。
有了故事之後,還想要藏幾個彩蛋在裡面。與其說是彩蛋,不如說是一些我覺得比較有趣的小東西,看看有沒有人會發現。因此在關卡的內容跟 token 上面其實都有藏一些東西。
我記得最初的版本我大概花了兩天就做完了。一天想關卡另外一天寫 code,想關卡的部分花比較久,因為程式碼實做的部分滿簡單的。
接著就讓我們先來看看前十關每一關的內容吧!
再次強調,如果你還沒破完,強烈建議不要觀看!趕快先去玩:Lidemy HTTP Challenge。
第一關
啊...好久沒有看到年輕人到我這個圖書館了,我叫做 lib,是這個圖書館的管理員
很開心看到有年輕人願意來幫忙,最近圖書館剛換了資訊系統,我都搞不清楚怎麼用了...
這是他們提供給我的文件,我一個字都看不懂,但對你可能會有幫助
先把這文件放一旁吧,這個待會才會用到
你叫做什麼名字呢?用 GET 方法跟我說你的 name 叫做什麼吧!
除了 token 以外順便把 name 一起帶上來就可以了
第一關只是想讓大家先拿到 API 文件,然後熟悉一下有些關卡會需要直接把資訊帶在網址上,因此第一關就只是讓大家熟悉環境而已。
傳入 name 以後,就可以拿到第二關的 token。
其實這邊剛開始的時候有不少人卡住,因為題目說明不清楚,所以有些人會以為是不是要去 call API 之類的。後來我就改了一下題目說明,盡可能講清楚,之後也新增了提示的功能。
第二關
我前陣子在整理書籍的時候看到了一本我很喜歡的書,可是現在卻怎麼想都想不起來是哪一本...
我只記得那本書的 id 是兩位數,介於 54~58 之間,你可以幫幫我嗎?
找到是哪一本之後把書的 id 用 GET 傳給我就行了。
這關的 id 範圍是 54~58,其實原本就是想讓大家一個一個試,沒有其他方法。
這邊藏的小彩蛋是 id 56 的書籍就是 5566 的書:
{"id":56,"name":"5566-認真","author":"鄭佩芬","ISBN":"0614361311"}
所以下一關的 token 才會是 5566NO1
第三關
真是太感謝你幫我找到這本書了!
剛剛在你找書的時候有一批新的書籍送來了,是這次圖書館根據讀者的推薦買的新書,其中有一本我特別喜歡,想要優先上架。
書名是《大腦喜歡這樣學》,ISBN 為 9789863594475。
就拜託你了。
新增完之後幫我把書籍的 id 用 GET 告訴我。
這關就只是在測驗會不會用 POST 而已。
有一個小地方是原本 API 文件沒有寫清楚要怎樣 POST,content type 是 form 還是 JSON?所以後來把這塊補上去了,避免產生歧義。
第四關
我翻了一下你之前幫我找的那本書,發現我記錯了...這不是我朝思暮想的那一本。
我之前跟你講的線索好像都是錯的,我記到別本書去了,真是抱歉啊。
我記得我想找的那本書,書名有:「世界」兩字,而且是村上春樹寫的,可以幫我找到書的 id 並傳給我嗎?
這關測驗會不會使用 API 的參數來查詢書籍,但要作弊直接在 local 搜尋其實也可以。
我自己滿愛村上春樹,而我有個朋友酷愛《世界末日與冷酷異境》這本書,所以就把它放進來了。為了讓搜尋「世界」的時候不要只出現一個結果,我還去找了其他幾本也有這個關鍵字的書放進去。
而下一關的 token HarukiMurakami
就是村上春樹的名字。
第五關
昨天有個人匆匆忙忙跑過來說他不小心捐錯書了,想要來問可不可以把書拿回去。
跟他溝通過後,我就把他捐過來的書還他了,所以現在要把這本書從系統裡面刪掉才行。
那本書的 id 是 23,你可以幫我刪掉嗎?
這關考 DELETE 的使用而已,沒什麼難度。
這邊藏的小彩蛋是他捐錯的書是雞排妹寫真集,所以想趕快拿回去。這也對應到了下一關的 token:CHICKENCUTLET
。
第六關
我終於知道上次哪裡怪怪的了!
照理來說要進入系統應該要先登入才對,怎麼沒有登入就可以新增刪除...
這太奇怪了,我已經回報給那邊的工程師了,他們給了我一份新的文件:
這邊是帳號密碼,你先登入試試看吧,可以呼叫一個 /me 的 endpoint,裡面會給你一個 email。
把 email 放在 query string 上面帶過來,我看看是不是對的。
帳號:admin
密碼:admin123
對新手來說其實算是比較有挑戰性的一關。
這關考的是知不知道怎麼樣在 header 裡面放內容,以及根據資料去找出怎麼用 http basic authorization。主要是想讓大家知道 HTTP 的其中一種驗證方式。
第七關
那邊的工程師說系統整個修復完成了,剛好昨天我們發現有一本書被偷走了...
這本書我們已經買第五次了,每次都被偷走,看來這本書很熱門啊。
我們要把這本書從系統裡面刪掉,就拜託你了。
對了!記得要用新的系統喔,舊的已經完全廢棄不用了。
書的 id 是 89。
其實只是沒梗了所以又加一個刪除資料的關卡,這邊的一個小插曲是原本沒有「對了!記得要用新的系統喔,舊的已經完全廢棄不用了。」這句,導致有些人還是用舊版 API,因此才加上去,避免大家搞混。
這本很熱門的書你實際上去看的話,會發現是《跟著月亮走:韓國瑜的夜襲精神與奮進人生》,對應到了下一關的 token:HsifnAerok
,倒過來就變 KoreanFish。
第八關
我昨天在整理書籍的時候發現有一本書的 ISBN 編號跟系統內的對不上,仔細看了一下發現我當時輸入系統時 key 錯了。
哎呀,人老了就是這樣,老是會看錯。
那本書的名字裡面有個「我」,作者的名字是四個字,key 錯的 ISBN 最後一碼為 7,只要把最後一碼改成 3 就行了。
對了!記得要用新的系統喔,舊的已經完全廢棄不用了。
這關就考找資料跟修改資料而已,沒什麼特別的。
下一關也就是第九關的 token 是NeuN
,德文中的九。
第九關
API 文件裡面有個獲取系統資訊的 endpoint 你記得嗎?
工程師跟我說這個網址不太一樣,用一般的方法是沒辦法成功拿到回傳值的。
想要存取的話要符合兩個條件:
1. 帶上一個 X-Library-Number 的 header,我們圖書館的編號是 20
2. 伺服器會用 user agent 檢查是否是從 IE6 送出的 Request,不是的話會擋掉
順利拿到系統資訊之後應該會有個叫做 version 的欄位,把裡面的值放在 query string 給我吧。
這關考兩個東西:
- 會不會傳 custom header
- 知不知道怎麼改 user agent,以及是否知道 user agent 代表的意義
這兩個就是前面有說過我一定要放進來的元素,因為我覺得很重要。
我想讓學生們知道說 user agent 其實有滿多作用,其中一個就包含讓 server 知道你的瀏覽器跟作業系統之類的;也想讓他們知道這些東西都可以偽造。
原本其實是設定 Server 會檢查是不是從 Safari 送出的 Request,但用 mac 的人就可以開 Safari 過關,因此後來才改成用 IE6。如果你要去裝 IE6 的 VM 那我也就算了XD
下一關的 token 是duZDsG3tvoA
,其實是 YouTube 的影片 ID,對應到的是周杰倫的半島鐵盒。因為我滿喜歡這首歌,而且這首歌跟書也有點關係。
第十關
時間過得真快啊,今天是你在這邊幫忙的最後一天了。
我們來玩個遊戲吧?你有玩過猜數字嗎?
出題者會出一個四位數不重複的數字,例如說 9487。
你如果猜 9876,我會跟你說 1A2B,1A 代表 9 位置對數字也對,2B 代表 8 跟 7 你猜對了但位置錯了。
開始吧,把你要猜的數字放在 query string 用 num 當作 key 傳給我。
原本是想讓大家真的來玩猜數字,預計猜個五六次就可以破關。但判斷邏輯我沒寫好,所以你傳一個數字或是重複數字我都沒擋掉,或者是你要直接暴力嘗試 9999 種組合也沒人攔你,所以這題的解法就很多種。
到這邊為止,就是前十關的內容。
第一次優化
做完前十關之後讓一些朋友先試玩,得到的反應都還不錯,但也發現一些問題,其中有些我上面已經提過了,例如說:
- 第一關說明不清楚,不知道 name 要傳到哪裡
- 沒有提示要用新版 API,以為可以用舊的
- 如果瀏覽器那關限制 Safari,對 Mac 使用者毫無難度
上述問題基本上都可以透過加強文字敘述來改善,但還發現一個更大的問題:
卡關
雖然說卡關是人之常情,但其實我不希望大家一直卡關。畢竟這個遊戲的最終目的其實是學習,好玩對我來說只是附加價值。可是我又不能破壞遊戲體驗,直接講解答,因此我必須提供一個方法讓他們可以看到提示。
你可能會問我那提示幹嘛不用白色文字就好,還要加 &hint=1
這麼麻煩。你可能忘了,我開頭有說初衷是想讓 curl 也可以玩這個遊戲,所以白色文字是沒有用的。
總之呢,最後加上了提示的功能,讓遊戲變得更完整了,體驗也變得更好。
原本遊戲就到這邊結束了,但剛好我又有了一些靈感,所以繼續往下做了一些關卡,下面來講講進階關卡。
第十一關
嘿!很開心看到你願意回來繼續幫忙,這次我們接到一個新的任務,要跟在菲律賓的一個中文圖書館資訊系統做串連
這邊是他們的 API 文件,你之後一定會用到。
現在就讓我們先跟他們打個招呼吧,只是我記得他們的 API 好像會限制一些東西就是了...
這關是開頭所提到的,一定要做的 origin 相關關卡。會放在進階關是因為怕對我學生來說有些太難,所以才放這裡。
總之是想讓大家理解就算 Server 檢查 origin,Client 也可以輕易偽造。然後這跟瀏覽器的 CORS 一點關係都沒有,大家要很清楚 Request 從瀏覽器發出來以及自己發 Request 是兩件很不一樣的事,前者會有許多限制,後者沒有。
下一關的 tokenr3d1r3c7
其實是 leet 的redirect
,已經暗示了下一關的解法。
第十二關
打完招呼之後我們要開始送一些書過去了,不過其實運送沒有你想像中的簡單,不是單純的 A 到 B 而已
而是像轉機那樣,A 到 C,C 才到 B,中間會經過一些轉運點才會到達目的地...算了,我跟你說那麼多幹嘛
現在請你幫我把運送要用的 token 給拿回來吧,要有這個 token 我們才能繼續往下一步走
這一關也是後期我很想放的一關,覺得這樣的概念滿有趣的。透過在 redirect 過程的途中塞東西,強迫大家去理解 server side redirect 的原理是什麼(301 跟 302 status code)。
若是你不懂為什麼可以轉址以及轉址背後的原理,你就解不開這題。
下一關的 token 為qspyz
,往左平移一個字元之後變成proxy
,一樣暗示著下一關的解法。
第十三關
太好了!自從你上次把運送用的 token 拿回來以後,我們就密切地與菲律賓在交換書籍
可是最近碰到了一些小問題,不知道為什麼有時候會傳送失敗
我跟他們反映過後,他們叫我們自己去拿 log 來看,你可以幫我去看看嗎?
從系統日誌裡面應該可以找到一些端倪。
這關是在考 proxy 的使用,因為 Server 會檢查使用者的 IP 是否來自於菲律賓。
檢查的方法是用 node-geoip:
advancedRouter.get('/logs', (req, res) => {
const ip = req.ip || ''
const info = geoip.lookup(ip) || {}
if (info.country === 'PH') {
res.end(text.lv13.reply)
} else {
res.end(text.lv13.wa)
}
})
所以只要隨便找一個在菲律賓的 proxy 來送 request 就可以過關了。
不過這關有兩件出乎意料的事,第一件事是滿多人都會嘗試偽造Accept-Language
這個 header,這我當初完全沒想到(不過也沒用就是了)。
第二件事是這題還有另一個解答,就是偽造X-Forwarded-For
,這也是我當初完全沒想到的事。
我在 Express 裡面有開app.set('trust proxy', true)
,所以在拿使用者 IP 的時候如果有X-Forwarded-For
這個 header,會以這邊的資訊為準。
剛好前陣子讀到一篇類似的文章:利用X-Forwarded-For伪造客户端IP漏洞成因及防范。
雖然不是我預設的解法,但我覺得這個解法更有趣,所以就沒有特地修掉了。
第十四關
跟那邊的溝通差不多都搞定了,真是太謝謝你了,關於這方面沒什麼問題了!
不過我老大昨天給了我一個任務,他希望我去研究那邊的首頁內容到底是怎麼做的
為什麼用 Google 一搜尋關鍵字就可以排在第一頁,真是太不合理了
他們的網站明明就什麼都沒有,怎麼會排在那麼前面?
難道說他們偷偷動了一些手腳?讓 Google 搜尋引擎看到的內容跟我們看到的不一樣?
算了,還是不要瞎猜好了,你幫我們研究一下吧!
這關想讓大家知道的事情是不只瀏覽器,各家爬蟲也會帶特定的 User-Agent,所以 Server 一樣可以針對不同的 UA 來輸出不同的資訊(雖然不被推薦就是了)。
舉例來說,一個 SPA 可以只針對 Google 搜尋引擎跟 Facebook 啟用 Server side render 來輸出內容,對一般用戶還是 Client side render。
或是像 HTTP Challenge 這個網站,本身就有針對不同的 UA 做處理(因為網站都是純文字,但我希望在臉書上被分享時有自訂標題跟敘述):
// base on UA return differect result
router.get('/start', (req, res) => {
const UA = req.header('User-Agent') || ''
if (UA.indexOf('facebookexternalhit') >= 0 || UA.indexOf('Googlebot') >= 0 ){
res.end(text.seo)
} else {
res.end(text.start.intro)
}
})
不過好像弄得怪怪的,不知道有沒有成功就是了。
這就是最後一關囉,第十五關是結語。
第二次優化
做完進階關卡之後,敘述的部分其實也有改一點,例如說第十四關就有學生以為跟 Chrome 有關(想到 Google 就只想到 Chrome XD),所以我特地強調說「Google 搜尋引擎」,要往這方向找才是正確的。
而其中最讓我驚訝的還是第十三關那個我沒有想到的解法X-Forwarded-For
。
在對外公開之後,一個朋友跟我說最後應該放一個 gist 讓大家留言。我內心一驚:對欸,可以放 gist。
因為我原本就有想說要不要放個排行榜或者是留言板之類的,讓破完的人可以留個言當作紀念,可是要做這功能挺麻煩的,我懶得做。被朋友提醒才突然發現 gist 本來就有內建留言功能,那就直接放一個 gist 就好了!
所以前期破關的朋友是沒有 gist 可以留言的,是到後面才新增的。
總結
這次很開心可以把這些知識包裝成游戲跟大家分享,迴響似乎也挺不錯的。雖然有些人期待會再加新關卡,但我目前完全沒靈感就是了。
之後比較有可能的是做個 HTML、CSS 跟 JavaScript 的版本,類型差不多,但就是每關的解法跟知識點不一樣,到時候再來跟大家分享。
感謝早期幫我測試的朋友們,也感謝跟我一起享受遊戲的大家。底下是一些相關的闖關心得,有興趣的話也可以看看:
評論