10个Bug环环相扣,你能解开几个?
簡介:由阿里云云效主辦的2021年第3屆83行代碼挑戰賽已經收官。超2萬人圍觀,近4000人參賽,85個團隊組團來戰。大賽采用游戲闖關玩兒法,融合元宇宙科幻和劇本殺元素,讓一眾開發者玩得不亦樂乎。
今天請來決賽賽題設計者杜萬,給大家分享一下設計與解題思路。
搭配《用代碼玩劇本殺?第3屆83行代碼大賽劇情官方解析》使用效果更佳。
第四題整體是一個C/S架構,客戶客戶端是一個編譯好的命令行程序,不可被修改,服務端是一個 Spring Boot 的 Web 應用;賽題要求,找出服務端程序的 BUG 并修復;客戶端有兩個職責,一個是說去向服務端發送正常 HTTP 請求,讓參賽者發現BUG。
另一個是驗證 bug 修復情況,然后發送給遠端的評分程序,獲得評分。整個賽題是跑在我們阿里云 DevStudio 上面,在 DevStudio 里我們啟動一個Intellij IDEA 的社區版,內置了應用觀測器(AppObserver) 插件。
Bug 1 :修復 Regex
我們來看第一個bug 如何修復吧。運行 ‘mvn test’,10 個測試有 9 個錯誤。
這里有好幾個BUG,我們先看正則表達式相關的,我們先修復ExtractHtmlTest,翻閱源碼,很快能定位到 Utils.stripHtmlTag 方法,方法名字面意思是去除 HTML Tag 標簽,然后仔細查看日志會發現。
刪除的Tag內容包括了 > 和 ,那說明正則有問題,下圖是對正則的剖析。
所以該 BUG上述兩種修復方法都是 OK 的。
解法:將 Utils.java 里的正則表達式`<(?.*)>`改為`<(?[^>]*)>`。?
Bug 2:修復尾串缺失
再次執行 mvn test,發現還有單測沒有通過,我們會發現字符串少了一截。
再次查看 Utils.stripHtmlTag 方法,發現 matcher.appendReplacement 方法,如果不熟悉該方法,查看JDK的注釋后,會發現 matcher.appendReplacement 和 matcher.appendTail 是成對出現的。所以在循環外補上 matcher.appendTail(builder)。
看圖是 matcher.appendReplacement 和 matcher.appendTail 的工作機制,巧用該方法,替換字符串更得心應手。 ?
Bug 3:修復 EOFException
再次執行 mvn test,僅剩下 EOFException 錯誤了,很快能定位到報錯的方法是 Utils.decodeMessage。
通過分析 ReactiveWebSocketHandler 的頭部注釋和 Utils.encodeMessage 的方法,我們了解到二進制的包結構:
/*** 二進制包格式* byte 字符集長度; n1* byte[n1] 字符集數據;n1 = 字符集長度* byte[n2] 有效數據;n2 = 包總長度 - n1 - 1*/ @Component("ReactiveWebSocketHandler") public class ReactiveWebSocketHandler implements WebSocketHandler { public static byte[] encodeMessage(String message, Charset charset) {ByteArrayOutputStream out = new ByteArrayOutputStream();DataOutputStream dos = new DataOutputStream(out); byte[] charsetNameBytes = charset.toString().getBytes(ISO_8859_1);try {dos.write((byte) charsetNameBytes.length); dos.write(charsetNameBytes);dos.write(message.getBytes(charset));dos.flush();} catch (IOException e) {e.printStackTrace();}return out.toByteArray();}然后在對比 Utils.decodeMessage 可以發現是一個調用時序問題,改正方法如下:
return new String(dis.readAllBytes(), charsetNameDecoder.apply(dis));=>String charsetName = charsetNameDecoder.apply(dis); return new String(dis.readAllBytes(), charsetName);此單測 Bug 已經修完了,接下來我們來修運行態的BUG。
配置應用觀測器
首先我們先配置一下應用觀測器(AppObserver),在賽題的DevStudio中,已經預安裝了 AppObserver ,這里配置一下IDEA的啟動器,加上應用觀測器的 Agent 就好了。
配置好應用觀測器后,通過 Spring Boot 的 main 函數啟動 Server 端進程。
Bug 4:修復CSRF
執行項目根目錄的客戶端程序 round4
$./round4___ _ ___ _____/ __\___ __| | ___ ( _ )___ // / / _ \ / _` |/ _ \/ _ \ |_ \ / /___ (_) | (_| | __/ (_) |__) | \____/\___/ \__,_|\___|\___/____/「第四關」 致命真相 當你直面致命的真相,你是否能面對這殘酷的現實?:: 通關要求 :: 達到 60 分以上 :: 獲勝要求 :: 分數最高且用時最短啟動客戶端程序....=== Step 1 ==== 成功獲得數據通道: ["/ws/Codeup","/ws/AppObserver","/ws/DevStudio", ]=== Step 2 ==== 添加用戶 reporter 失敗!響應狀態碼: 403 Forbidden, 響應消息: "An expected CSRF token cannot be found", 請求頭:"Authorization: Basic YWRtaW46YWRtaW4xMjM="=== Step 3 ==== 使用 reporter 用戶無法連接到:ws://localhost:8080/ws/DevStudio, 響應狀態碼: 401 Unauthorized, 請求頭: {"authorization": "Basic cmVwb3J0ZXI6cmVwb3J0ZXI="}Step2 有一個 CSRF 的報錯,由于無法修改客戶端程序,需要在 Server 端解決這個問題,關閉掉 CSRF 校驗。
使用上面的報錯關鍵字Google一下,很快能找到Spring Security的修改方法。
然后照下午修改,再驗證一下,發現響應碼從 403 變成了 401,所以修改生效了。
Bug 5:修復 Admin 用戶密碼錯誤
上一步再次執行 ./round4 ,Step2 返回了 401,并提示了請求頭:"Authorization: Basic YWRtaW46YWRtaW4xMjM=",這里可以看出,使用了HTTP Basic的驗證方式,然后401提示,可能是用戶名和密碼不對,所以這里可以用 base64 解開認證頭,修改一下服務端的用戶名密碼。
Bug 6:Admin 角色不對
再次執行 ./round4 后我們發現,又變回了 403,但是返回錯誤變成了 Access Denied。看來密碼對了,但是沒有權限訪問,打開 WebSecurityConfig 文件,我們會發現admin角色有兩種寫法“ADMIN”和“admin”,問題就出在這里,我們統一改成大寫試試。
Step2,算過了,接下來出來Step3 的問題了。
Bug 7:缺失 REPORTER 角色
Step 3 報錯,使用 reporter 用戶無法連接到:ws://localhost:8080/ws/AppObserver, 響應狀態碼: 403 Forbidden, 請求頭: {"authorization": "Basic cmVwb3J0ZXI6cmVwb3J0ZXI="}。
又是一個權限問題,先解開 base64 編碼的 Authorization,發現用戶密碼都是 reporter。接下來需要借助于應用觀測器,使用應用觀測器在 Round4Controller.addUser 加上虛擬斷點,虛擬斷點和普通斷點一樣可以獲得執行上下文的線程堆棧和變量信息,但是虛擬斷點不會阻塞執行,這個特性對于生產系統非常有用。
具體操作如下圖所示
通過虛擬斷點,我們發現 reporter 用戶的角色名為 REPORTER,而 endpoint "/ws/**", 當前只允許ADMIN角色訪問,所以在Security配置里,給該路徑添加 REPORTER 角色即可。
解決了角色問題,4 個 Spring Security 相關的 BUG 都已經已經修復掉了。重啟服務并執行 ./round4 我們會先發有亂碼,那看看亂碼怎么修
Bug 8:共享 Buffer
通過對 ReactiveWebSocketHandler 里一連串mapper的分析,我們會發現 getBufferConverter 方法返回了定長的buffer,而這個buffer后面會有一連串的0值,這個很可疑。仔細看代碼發現,多次調用之間共享了同一個buffer,而沒有清空。解法也很簡單,把共享buffer改成每次新建即可。如下圖所示:
修復以后,再次執行 ./round4 亂碼沒有,但是返回內容有點少了,說明還有其他問題。
Bug 9:修復 NPE
修掉上面亂碼問題以后,從客戶端 round4 的運行輸出里已經看不到明顯的錯誤了,這是發現內容有點短,看Server這邊的日志,會看到一個NPE的報錯:
NPE比較好修,很快能排查到一個 return null。
改成 return ""; 即可。
Bug 10:去除 ThreadLocal
重啟服務端,并再次執行 ./round4,內容多了,不過再次亂碼。
最后一個Bug,不太好調試,需要靠認證的閱讀代碼,理解一下上下文,能看到有一個奇怪的ThreadLocal 變量用于緩存 charsetName。
在一個Thread里charset是不變的?去掉估計也不會影響效果,最多性能差一點,嘗試去掉。
重啟服務端,并再次執行 ./round4。
這下一切正常了。
提取線索
上面三個頻道的返回包含了大賽的線索,所以我們可以使用 grep 工具賽選出來。
劇情題我們這里就不討論了,可以看另外一篇解密文章。
小結
共計修了 10 個 Bug
- Regex 2個
- Spring Security 4個
- NPE 1個
- EOF 1個
- 共享狀態 2個
賽題涉及到的技術
- Spring Boot
- Spring Security
- Spring WebFlux
- Java IO
- JUnit 5
- Regex
- Websocket
- CSRF
- HTTP Basic Auth
工具
- DevStudio(Web 版 Intellij IDEA)
- AppObserver (CloudToolkit 插件)
原文鏈接
本文為阿里云原創內容,未經允許不得轉載。?
總結
以上是生活随笔為你收集整理的10个Bug环环相扣,你能解开几个?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 智能巡检云监控指标的实践
- 下一篇: 云原生时代的运维体系进化