Huli's Blog

Learning by sharing

[心得] 在新加坡買淘寶

| Comments

其實在新加坡買淘寶跟在台灣買淘寶流程是一樣的
都是先集中運到某一家集貨倉庫,再從倉庫統一出貨到新加坡來
紀錄一下日程:

3/14 下單購買
3/16 送達四方集運
3/17 從四方集運出貨
3/19 到達新加坡
3/21 從新加坡送貨

也就是說大概一個禮拜左右就可以從中國寄到台灣了,我覺得這速度不錯欸
我記得我從台灣買也是要差不多一個禮拜左右

該如何知道就業所需技能?以前端工程師為例

| Comments

我想成為一個前端工程師,現在有在學一些基本的 HTML, CSS,可是我不知道要到什麼程度才能找到工作,該怎麼辦?

這種類似的問題有一大堆人問過我,我想說乾脆就寫成一篇文章,以後有人問同樣問題的時候我就丟這篇給他看,就不用一直重複回答相同的問題了。

有一種東西你一定看過,就是在徵才說明上面會寫的工作敘述跟需求,前者大概是你進去公司以後會負責一些什麼事情,後者則是這間公司所需要的人才應該要有哪些技能。這個就是很好的切入點,你只要多研究幾份,很快就可以歸納出各個公司幾乎都必備的技能是哪些,你再拿這一個個的關鍵字去查就好了。

來,我示範給你看!

讓我們來談談 CSRF

| Comments

前言

最近剛好碰到一些 CSRF 的案例,趁著這次機會好好研究了一下。深入研究之後才發現這個攻擊其實滿可怕的,因為很容易忽略它。但幸好現在有些 Framework 都有內建防禦 CSRF 的功能,可以很簡單的開啟。

但儘管如此,我認為還是有必要瞭解一下 CSRF 到底在幹嘛,是透過怎樣的手段攻擊,以及該如何防禦。就讓我們先來簡單的介紹一下它吧!

CSRF 是一種 Web 上的攻擊手法,全稱是 Cross Site Request Forgery,跨站請求偽造。不要跟 XSS 搞混了,他們兩種是不同的東西,那到底什麼是 CSRF 呢?先從我自身的一個案例談起好了。

偷懶的刪除功能

以前我有做個一個簡單的後台頁面,就想成是一個部落格吧!可以發表、刪除以及編輯文章,介面大概長得像這樣:

delete

可以看到刪除的那個按鈕,點下去之後就可以把一篇文章刪掉。當初因為偷懶,想說如果我把這個功能做成 GET,我就可以直接用一個連結完成刪除這件事,在前端幾乎不用寫到任何程式碼:

<a href='/delete?id=3'>刪除</a>

很方便對吧?然後我在網頁後端那邊做一下驗證,驗證 request 有沒有帶 session id 上來,也驗證這篇文章是不是這個 id 的作者寫的,都符合的話才刪除文章。

嗯,聽起來該做的都做了啊,我都已經做到:「只有作者本人可以刪除自己的文章」了,應該很安全了,難道還有哪裡漏掉了嗎?

沒錯,的確是「只有作者本人可以刪除自己的文章」,但如果他不是自己「主動刪除」,而是在不知情的情況下刪除呢?你可能會覺得我在講什麼東西,怎麼會有這種事情發生,不是作者主動刪的還能怎麼刪?

好,我就來讓你看看還能怎麼刪!

今天假設小黑是一個邪惡的壞蛋,想要讓小明在不知情的情況下就把自己的文章刪掉,該怎麼做呢?

他知道小明很喜歡心理測驗,於是就做了一個心理測驗網站,並且發給小明。但這個心理測驗網站跟其他網站不同的點在於,「開始測驗」的按鈕長得像這樣:

<a href='https://small-min.blog.com/delete?id=3'>開始測驗</a>

小明收到網頁之後很開心,就點擊「開始測驗」。點擊之後瀏覽器就會發送一個 GET 請求給https://small-min.blog.com/delete?id=3,並且因為瀏覽器的運行機制,一併把 small-min.blog.com 的 cookie 都一起帶上去。

Server 端收到之後檢查了一下 session,發現是小明,而且這篇文章也真的是小明發的,於是就把這篇文章給刪除了。

這就是 CSRF,你現在明明在心理測驗網站,假設是 https://test.com 好了,但是卻在不知情的狀況下刪除了 https://small-min.blog.com 的文章,你說這可不可怕?超可怕!

這也是為什麼 CSRF 又稱作 one-click attack 的緣故,你只要點一下就中招了。

你可能會說:「可是這樣小明不就知道了嗎,不就連過去部落格了?不符合『不知情的狀況』啊!」

好,那如果我們改成這樣呢:

<img src='https://small-min.blog.com/delete?id=3' width='0' height='0' />
<a href='/test'>開始測驗</a>

在開啟頁面的同時,一樣發送一個刪除的 request 出去,但這次小明是真的完全不知道這件事情。這樣就符合了吧!

CSRF 就是在不同的 domain 底下卻能夠偽造出「使用者本人發出的 request」。要達成這件事也很簡單,因為瀏覽器的機制,你只要發送 request 給某個網域,就會把關聯的 cookie 一起帶上去。如果使用者是登入狀態,那這個 request 就理所當然包含了他的資訊(例如說 session id),這 request 看起來就像是使用者本人發出的。

那我把刪除改成 POST 不就好了嗎?

沒錯,聰明!我們不要那麼懶,好好把刪除的功能做成 POST,這樣不就無法透過 <a> 或是 <img> 來攻擊了嗎?除非有哪個 HTML 元素可以發送 POST request!

有,正好有一個,就叫做 form。

<form action="https://small-min.blog.com/delete" method="POST">
  <input type="hidden" name="id" value="3"/>
  <input type="submit" value="開始測驗"/>
</form>

小明點下去以後,照樣中招,一樣刪除了文章。你可能又會疑惑說,但是這樣小明不就知道了嗎?我跟你一樣很疑惑,於是我 Google 到了這篇:Example of silently submitting a POST FORM (CSRF)

這篇提供的範例如下,網頁的世界真是博大精深:

<iframe style="display:none" name="csrf-frame"></iframe>
<form method='POST' action='https://small-min.blog.com/delete' target="csrf-frame" id="csrf-form">
  <input type='hidden' name='id' value='3'>
  <input type='submit' value='submit'>
</form>
<script>document.getElementById("csrf-form").submit()</script>

開一個看不見的 iframe,讓 form submit 之後的結果出現在 iframe 裡面,而且這個 form 還可以自動 submit,完全不需要經過小明的任何操作。

到了這步,你就知道改成 POST 是沒用的。

那我後端改成只接收 json 呢?

聰明的你靈機一動:「既然在前端只有 form 可以送出 POST 的話,那我的 api 改成用 json 收資料不就可以了嗎?這樣總不能用 form 了吧!」

spring 的 document告訴你:這還是沒用的!

<form action="https://small-min.blog.com/delete" method="post" enctype="text/plain">
<input name='{"id":3, "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
  value="delete!"/>
</form>

這樣子會產生如下的 request body:

{ "id": 3,
"ignore_me": "=test"
}

但這邊值得注意的一點是,form能夠帶的 content type 只有三種:application/x-www-form-urlencoded, multipart/form-datatext/plain。在上面的攻擊中我們用的是最後一種,text/plain,如果你在你的後端 Server 有檢查這個 content type 的話,是可以避免掉上面這個攻擊的。

只是,上面這幾個攻擊我們都還沒講到一種情況:如果你的 api 接受 cross origin 的 request 呢?

意思就是,如果你的 api 的 Access-Control-Allow-Origin 設成 * 的話,代表任何 domain 都可以發送 ajax 到你的 api server,這樣無論你是改成 json,或甚至把 method 改成 PUT, DELETE 都沒有用。

我們舉的例子是刪除文章,這你可能覺得沒什麼,那如果是銀行轉帳呢?攻擊者只要在自己的網頁上寫下轉帳給自己帳號的 code,再把這個網頁散佈出去就好,就可以收到一大堆錢。

講了這麼多,來講該怎麼防禦吧!先從最簡單的「使用者」開始講。

使用者的防禦

CSRF 攻擊之所以能成立,是因為使用者在被攻擊的網頁是處於已經登入的狀態,所以才能做出一些行為。雖然說這些攻擊應該由網頁那邊負責處理,但如果你真的很怕,怕網頁會處理不好的話,你可以在每次使用完網站就登出,就可以避免掉 CSRF。

或者呢,關閉執行 js 或把上面這些 pattern 的程式碼過濾掉不要執行,也是一個方法(但應該很難判定哪些是 CSRF 攻擊的程式碼)。

所以使用者能做的其實有限,真的該做事的是 Server 那邊!

Server 的防禦

CSRF 之所以可怕是因為 CS 兩個字:Cross Site,你可以在任何一個網址底下發動攻擊。CSRF 的防禦就可以從這個方向思考,簡單來說就是:「我要怎麼擋掉從別的 domain 來的 request」

你仔細想想,CSRF 的 reuqest 跟使用者本人發出的 request 有什麼區別?區別在於 domain 的不同,前者是從任意一個 domain 發出的,後者是從同一個 domain 發出的(這邊假設你的 api 跟你的前端網站在同一個 domain)

檢查 Referer

request 的 header 裡面會帶一個欄位叫做 referer,代表這個 request 是從哪個地方過來的,可以檢查這個欄位看是不是合法的 domain,不是的話直接 reject 掉即可。

但這個方法要注意的地方有三個,第一個是有些瀏覽器可能不會帶 referer,第二個是有些使用者可能會關閉自動帶 referer 的這個功能,這時候你的 server 就會 reject 掉由真的使用者發出的 request。

第三個是你判定是不是合法 domain 的程式碼必須要保證沒有 bug,例如:

const referer = request.headers.referer;
if (referer.indexOf('small-min.blog.com') > -1) {
  // pass

}

你看出上面這段的問題了嗎?如果攻擊者的網頁是small-min.blog.com.attack.com的話,你的檢查就破功了。

所以,檢查 referer 並不是一個很完善的解法

加上圖形驗證碼、簡訊驗證碼等等

就跟網路銀行轉帳的時候一樣,都會要你收簡訊驗證碼,多了這一道檢查就可以確保不會被 CSRF 攻擊。

圖形驗證碼也是,攻擊者並不知道圖形驗證碼的答案是什麼,所以就不可能攻擊了。

這是一個很完善的解決方法,但如果使用者每次刪除 blog 都要打一次圖形驗證碼,他們應該會煩死吧!

加上 CSRF token

要防止 CSRF 攻擊,我們其實只要確保有些資訊「只有使用者知道」即可。那該怎麼做呢?

我們在 form 裡面加上一個 hidden 的欄位,叫做csrftoken,這裡面填的值由 server 隨機產生,並且存在 server 的 session 中。

<form action="https://small-min.blog.com/delete" method="POST">
  <input type="hidden" name="id" value="3"/>
  <input type="hidden" name="csrftoken" value="fj1iro2jro12ijoi1"/>
  <input type="submit" value="刪除文章"/>
</form>

按下 submit 之後,server 比對表單中的csrftoken與自己 session 裡面存的是不是一樣的,是的話就代表這的確是由使用者本人發出的 request。這個 csrftoken 由 server 產生,並且每一段不同的 session 就應該要更換一次。

那這個為什麼可以防禦呢?因為攻擊者並不知道 csrftoken 的值是什麼,也猜不出來,所以自然就無法進行攻擊了。

可是有另外一種狀況,假設你的 server 支持 cross origin 的 request,會發生什麼事呢?攻擊者就可以在他的頁面發起一個 request,順利拿到這個 csrf token 並且進行攻擊。不過前提是你的 server 接受這個 domain 的 request。

接著讓我們來看看另外一種解法

Double Submit Cookie

上一種解法需要 server 的 state,亦即 csrf token 必須被保存在 server 當中,才能驗證正確性。而現在這個解法的好處就是完全不需要 server 儲存東西。

這個解法的前半段與剛剛的相似,由 server 產生一組隨機的 token 並且加在 form 上面。但不同的點在於,除了不用把這個值寫在 session 以外,同時也讓 client side 設定一個名叫 csrftoken 的 cookie,值也是同一組 token。

Set-Cookie: csrftoken=fj1iro2jro12ijoi1

<form action="https://small-min.blog.com/delete" method="POST">
  <input type="hidden" name="id" value="3"/>
  <input type="hidden" name="csrftoken" value="fj1iro2jro12ijoi1"/>
  <input type="submit" value="刪除文章"/>
</form>

你可以仔細思考一下 CSRF 攻擊的 request 與使用者本人發出的 request 有什麼不一樣?不一樣的點就在於,前者來自不同的 domain,後者來自相同的 domain。所以我們只要有辦法區分出這個 request 是不是從同樣的 domain 來,我們就勝利了。

而 Double Submit Cookie 這個解法正是從這個想法出發。

當使用者按下 submit 的時候,server 比對 cookie 內的 csrftoken 與 form 裡面的 csrftoken,檢查是否有值並且相等,就知道是不是使用者發的了。

為什麼呢?假設現在攻擊者想要攻擊,他可以隨便在 form 裡面寫一個 csrf token,這當然沒問題,可是因為瀏覽器的限制,他並不能在他的 domain 設定 small-min.blog.com 的 cookie 啊!所以他發上來的 request 的 cookie 裡面就沒有 csrftoken,就會被擋下來。

當然,這個方法看似好用,但也是有缺點的,可以參考:Double Submit Cookies vulnerabilities,攻擊者如果掌握了你底下任何一個 subdomain,就可以幫你來寫 cookie,並且順利攻擊了。

client side 的 Double Submit Cookie

會特別提到 client side,是因為我之前所碰到的專案是 Single Page Application,上網搜尋一下就會發現有人在問:「SPA 該如何拿到 CSRF token?」,難道要 server 再提供一個 api 嗎?這樣好像有點怪怪的。

但是呢,我認為我們可以利用 Double Submit Cookie 的精神來解決這個問題。而解決這問題的關鍵就在於:由 client side 來生 csrf token。就不用跟 server api 有任何的互動。

其他的流程都跟之前一樣,生成之後放到 form 裡面以及寫到 cookie。或者說如果你是 SPA 的話,也可以把這資訊直接放到 request header,你就不用在每一個表單都做這件事情,只要統一加一個地方就好。

事實上,我自己常用的 library axios 就有提供這樣的功能,你可以設置 header 名稱跟 cookie 名稱,設定好以後你每一個 request,它都會自動幫你把 header 填上 cookie 裡面的值。

 // `xsrfCookieName` is the name of the cookie to use as a value for xsrf token
xsrfCookieName: 'XSRF-TOKEN', // default

// `xsrfHeaderName` is the name of the http header that carries the xsrf token value
xsrfHeaderName: 'X-XSRF-TOKEN', // default

那為什麼由 client 來生這個 token 也可以呢?因為這個 token 本身的目的其實不包含任何資訊,只是為了「不讓攻擊者」猜出而已,所以由 client 還是由 server 來生成都是一樣的,只要確保不被猜出來即可。Double Submit Cookie 靠的核心概念是:「攻擊者的沒辦法讀寫目標網站的 cookie,所以 request 的 csrf token 會跟 cookie 內的不一樣」

browser 本身的防禦

我們剛剛提到了使用者自己可以做的事、網頁前後端可以做的事情,那瀏覽器呢?之所以能成立 CSRF,是因為瀏覽器的機制所導致的,有沒有可能從瀏覽器方面下手,來解決這個問題呢?

有!而且已經有了。而且啟用的方法非常非常簡單。

Google 在 Chrome 51 版的時候正式加入了這個功能:SameSite cookie,對詳細運行原理有興趣的可參考:draft-west-first-party-cookies-07

先引一下 Google 的說明:

Same-site cookies (née "First-Party-Only" (née "First-Party")) allow servers to mitigate the risk of CSRF and information leakage attacks by asserting that a particular cookie should only be sent with requests initiated from the same registrable domain.

啟用這個功能有多簡單?超級無敵簡單。

你原本設置 Cookie 的 header 長這樣:

Set-Cookie: session_id=ewfewjf23o1;

你只要在後面多加一個 SameSite 就好:

Set-Cookie: session_id=ewfewjf23o1; SameSite

但其實 SameSite 有兩種模式,LaxStrict,默認是後者,你也可以自己指定模式:

Set-Cookie: session_id=ewfewjf23o1; SameSite=Strict
Set-Cookie: foo=bar; SameSite=Lax

我們先來談談默認的 Strict模式,當你加上 SameSite 這個關鍵字之後,就代表說「我這個 cookie 只允許 same site 使用,不應該在任何的 cross site request 被加上去」。

意思就是你加上去之後,我們上面所講的<a href="">, <form>, new XMLHttpRequest,只要是瀏覽器驗證不是在同一個 site 底下發出的 request,全部都不會帶上這個 cookie。

可是這樣其實會有個問題,連<a href="..."都不會帶上 cookie 的話,當我從 Google 搜尋結果或者是朋友貼給我的連結點進某個網站的時候,因為不會帶 cookie 的關係,所以那個網站就會變成是登出狀態。這樣子的使用者體驗非常不好。

有兩種解法,第一種是跟 Amazon 一樣,準備兩組不同的 cookie,第一組是讓你維持登入狀態,第二組則是做一些敏感操作的時候會需要用到的(例如說購買、設定帳戶等等)。第一組不設定 SameSite,所以無論你從哪邊來,都會是登入狀態。但攻擊者就算有第一組 cookie 也不能幹嘛,因為不能做任何操作。第二組因為設定了 SameSite 的緣故,所以完全避免掉 CSRF。

但這樣子還是有點小麻煩,所以你可以考慮第二種,就是調整為 SameSite 的另一種模式:Lax

Lax 模式放寬了一些限制,例如說<a>, <link rel="prerender">, <form method="GET"> 這些都還是會帶上 cookie。但是 POST 方法 的 form,或是只要是 POST, PUT, DELETE 這些方法,就不會帶上 cookie。

所以一方面你可以保有彈性,讓使用者從其他網站連進你的網站時還能夠維持登入狀態,一方面也可以防止掉 CSRF 攻擊。但 Lax 模式之下就沒辦法擋掉 GET 形式的 CSRF,這點要特別注意一下。

講到這種比較新的東西,相信大家一定都很想知道瀏覽器的支援度如何,caniuse 告訴我們說:目前只有 Chrome 支援這個新的特性(畢竟是 Google 自己推的方案,自己當然要支持一下)。

雖然瀏覽器的支援度不太高,但日後其他瀏覽器可能也會跟進實做這個方案,不妨在現在就把 SameSite 加上去,以後就不用再為 CSRF 煩惱了。

我其實只是大略的介紹一下,draft-west-first-party-cookies-07 裡面講到很多細節,例如說到底怎樣算是 cross site? 一定要在同一個 domain 嗎?那 sub domain 行不行?

好奇的可以自己研究一下,或者是這篇:SameSite Cookie,防止 CSRF 攻击也有提到。

SameSite 相關的參考資料:

  1. Preventing CSRF with the same-site cookie attribute
  2. 再见,CSRF:讲解set-cookie中的SameSite属性
  3. SameSite Cookie,防止 CSRF 攻击
  4. SameSite——防御 CSRF & XSSI 新机制
  5. Cross-Site Request Forgery is dead!

總結

這篇主要介紹了 CSRF 的攻擊原理以及兩種防禦方法,針對比較常見的場景做介紹。一般在做網頁開發的時候,比起 XSS,CSRF 是一個比較常被忽略的重點。在網頁上有任何比較重要的操作時,都要特別留意是否有被 CSRF 的風險。

這次找了很多參考資料,但發現跟 CSRF 有關的文章其實都大同小異,想知道更細節的地方需要花很多的心力去找,但幸好 Stackoverflow 上面也有不少資料可以參考。因為我在資訊安全這塊沒有涉獵太多,如果文章有哪部分講錯的話,還麻煩各位在留言不吝指出。

也感謝我朋友 shik 的指點,告訴我有 SameSite 這麼一個東西,讓我補上最後一段。

希望這篇文章能讓大家對 CSRF 有更全面的認識。

參考資料

  1. Cross-Site Request Forgery (CSRF)
  2. Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet
  3. 一次较为深刻的CSRF认识
  4. [技術分享] Cross-site Request Forgery (Part 2)
  5. Spring Security Reference
  6. CSRF 攻击的应对之道

Bitcoin 新幣轉台幣初體驗心得

| Comments

由於現在來到新加坡工作的關係,領的薪水都是新幣。在新加坡這邊也順利的拿到工作證以及當地銀行的戶頭。萬事俱備之後,就找了個時間嘗試自己之前一直很想試試看的事情:

從新加坡買比特幣,轉到台灣,再用台幣賣出。想看看這樣子的匯率大概是多少。

我在新加坡用的網站叫做:coinhako,購買的手續費好像是 0.9%。

你要先驗證,驗證方式跟等等講的 xfers 差不多。然後這個網站你也沒辦法直接在上面購買比特幣,你要先從其他地方轉錢到 coinhako 裡面的你的錢包。可以選擇用新加坡帳號轉帳或者是他推薦的另外一個網站:xfers.io,研究一下決定用那個網站。

要把錢轉到 xfers 的帳戶也很簡單,你就用你的銀行帳號匯款到指定的地方,然後在備註的地方填自己的手機號碼就行了。從轉帳過去一直到 xfers 確認入帳,雖然他是說很快啦,但我自己上次試,大概一兩個小時才確認入帳,也有可能是因為那時候不是銀行營業時間的關係(晚上九點)。

xfers 需要認證,拍你的工作證正反面還有自己的照片 + 他要求的字 + 工作證就可以了。驗證的過程也滿快,一天之內可以搞定。

總之呢,上面步驟都完成之後你就可以從你的帳戶存錢到 xfers,再從 xfers 轉進 coinhako,你就可以買比特幣了。買完之後發送到你台灣的比特幣地址。這邊手續費是 0.0001 bitcoin,大概 3 塊台幣左右。

台灣的比特幣錢包我用的是 maicoin,這網站的特點就是手續費都算在匯率裡面了,所以很多操作都不用手續費(你好像也不會知道手續費實際上到底是多少)。從新加坡那邊發過來到確認大概半小時以內可以完成。

接著就是要在台灣賣出了,這邊一樣要驗證。你必須用你的台灣身分證或是護照加上台灣的手機號碼去驗證,因為我台灣的手機門號沒開漫遊所以超級麻煩,用了朋友的手機驗證。換手機之後必須要等 24 小時才能操作交易,超級久...不過也滿安全就是了。

順帶一提,另外一個台灣的錢包 bitoEX 要求台灣人一定要身分證,不能用護照驗證,一怒之下就不理他了。

maicoin 驗證完以後就可以來賣比特幣了,賣完之後會匯款到你的指定帳戶,這一段超級久。我晚上十點多賣的,隔天下午三點才入帳,我也不知道為什麼會拖那麼久,我原本以為早上十點多應該就可以入帳了。

結論

我在新加坡用 100 SGD 買了 0.0707 的比特幣,在台灣賣掉,實際匯入帳戶的錢是 2143,由此可以得知,透過比特幣交易把新幣換成台幣,匯率是 21.43。

不過比特幣匯率變化超級大,大概一分鐘就變一次,所以世事難料,有可能漲也有可能跌,要承擔這些匯差的風險。

因為我自己也沒試過從新加坡直接跨國轉帳給台灣,不知道手續費是多少,只知道兩邊的銀行都會抽一次手續費。如果是要轉非常小額的,感覺選擇抽趴數的比特幣會比較划算一點。

直播協議 hls 筆記

| Comments

前言

最近剛好在做直播相關的東西,雖然說是做前端,但還是必須懂一些直播的原理
至少要知道有哪些格式,以及各種格式的優缺點是什麼,做起來也會比較踏實

這篇就簡單記錄一些心得跟資料,如果想比較深入了解 hls 的,可以參考下面這兩篇文章:

  1. 直播协议的选择:RTMP vs. HLS
  2. 在线视频之HLS协议—学习笔记:M3U8格式讲解及实际应用分析

hls 是什麼?

我覺得以直播來說,hls 是一個相當好懂的協定,其實就是透過一個 .m3u8 的播放列表,然後裡面有多個 .ts 的檔案
你只要照著播放列表裡面給你的檔案順序播放就好了,聽起來很容易吧!

為了讓大家更明白,直接附上擷取自某處的播放列表:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:YES
#EXT-X-MEDIA-SEQUENCE:4454
#EXT-X-TARGETDURATION:4
#EXTINF:3.998, no desc
25133_src/4460.ts
#EXTINF:3.992, no desc
25133_src/4461.ts
#EXTINF:3.985, no desc
25133_src/4462.ts
#EXTINF:3.979, no desc
25133_src/4463.ts
#EXTINF:3.996, no desc
25133_src/4464.ts

就算你沒看過這個格式,你大概看一下也可以猜出來它在做什麼
每一個 ts 就是一個片段,然後 #EXTINF:3.996 代表這個片段的時間長度
#EXT-X-TARGETDURATION:4,這邊的數字必須比播放清單中的任何一個影片的時間都大
代表播放器應該每隔幾秒去抓一次新的播放清單

例如說,下一次抓到的可能會長這樣:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:YES
#EXT-X-MEDIA-SEQUENCE:4455
#EXT-X-TARGETDURATION:4
#EXTINF:3.992, no desc
25133_src/4461.ts
#EXTINF:3.985, no desc
25133_src/4462.ts
#EXTINF:3.979, no desc
25133_src/4463.ts
#EXTINF:3.996, no desc
25133_src/4464.ts
#EXTINF:3.998, no desc
25133_src/4465.ts

就是最後面多了一個片段。所以只要一直維持這個規則,就能夠不斷取到新的片段
那如果很不巧的,server 沒有及時產生出播放列表怎麼辦呢?

例如說在第 4 秒的時候去拿,發現沒更新,server 在第 4.5 秒才把新的播放片段產生出來
如果發生這種「拿了播放清單,但長的一樣」的情形,就會把抓取的時間減一半,直到抓到為止
像以上情形,第 4 秒沒拿到新的,就會隔 2 秒之後再去抓

這個規則可以參考:HTTP Live Streaming draft-pantos-http-live-streaming-20

When a client loads a Playlist file for the first time or reloads a
Playlist file and finds that it has changed since the last time it
was loaded, the client MUST wait for at least the target duration
before attempting to reload the Playlist file again, measured from
the last time the client began loading the Playlist file.

If the client reloads a Playlist file and finds that it has not
changed then it MUST wait for a period of one-half the target
duration before retrying.

至於做直播最關心的延遲問題,也可以直接從這個播放列表直接推測出來
以上面的例子來說,一共有 5 個片段,每一個片段 4 秒,延遲就是 20 秒
Apple 官方建議的是 3 個片段,每個片段 10 秒

What duration should media files be?
A duration of 10 seconds of media per file seems to strike a reasonable balance for most broadcast content.

How many files should be listed in the index file during a continuous, ongoing session?
The normal recommendation is 3, but the optimum number may be larger.

可參考:Apple: HTTP Live Streaming Overview

不過依照官方的建議,就會有 30 秒的延遲,當然延遲越久直播的狀況會越好,可是體驗也會比較差一點
因此,我們可以來看看幾個直播網站都是怎麼設定的

先來看看直播大頭:Twitch

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:5
#ID3-EQUIV-TDTG:2016-11-26T02:40:23
#EXT-X-MEDIA-SEQUENCE:376
#EXT-X-TWITCH-ELAPSED-SYSTEM-SECS:1511.137
#EXT-X-TWITCH-ELAPSED-SECS:1508.980
#EXT-X-TWITCH-TOTAL-SECS:1535.137
#EXTINF:4.000,
index-0000000377-6zCW.ts
#EXTINF:4.000,
index-0000000378-vHZS.ts
#EXTINF:4.000,
index-0000000379-Gkgv.ts
#EXTINF:4.000,
index-0000000380-PNoG.ts
#EXTINF:4.000,
index-0000000381-h58g.ts
#EXTINF:4.000,
index-0000000382-W88t.ts

6 個片段 * 4 秒 = 24 秒
可是如果你仔細觀察(開 chrome devtool 就可以了),實際上 twtich 的播放器在拿到列表以後
會直接嘗試從「倒數第三個」片段開始載入,所以延遲就縮短為 3*4 = 12 秒了

再來看看台灣的 livehouse.in

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-MEDIA-SEQUENCE:2291
#EXT-X-TARGETDURATION:6

#EXTINF:5.2090001106262207,
1480116261segment_v02291.ts
#EXTINF:5.2080001831054688,
1480116261segment_v02292.ts
#EXTINF:5.2080001831054688,
1480116261segment_v02293.ts

5*3 = 15 秒

所以一般用 hls 的直播網站,延遲大概都會在 10~20 秒這個區間以內
我猜比這個短的話對 server 壓力可能很大,而且網速慢的話,看起來會很卡
比這個長的話雖然很順,但是使用者體驗不太好,延遲太高
所以能找到最好的延遲就是在這個區間內了

最後,我們來看看如果要在網頁上播放的話,有哪些選擇
因為現在已經是個 flash 快死掉的年代了,所以如果可以的話,首選當然是 html5
瀏覽器支援度不夠高的話再 fallback 回去 flash

先來介紹一下現成的商業授權播放器,例如說 jwplayer 或是 flowplayer,都是很不錯的選項。
尤其是當 open source 的方案出現問題你又修不好的時候,就會很希望公司花錢買一個商業播放器,一切問題都搞定。

open source 的方案大概就是 videojs 一支獨秀了,有沒有其他的後起之秀我是不知道啦,有的話麻煩推薦一下。

然後因為 hls 這個格式瀏覽器本身是沒辦法播放的,所以要搭配一些 plugin
videojs 官方有一個 videojs-contrib-hls,加上去之後就可以播放了,但我自己用過以後感覺不是很好。

最後選擇了知名的影音網站 dailymotion 提供的開源解決方案 hls.js

這一篇是他們官方的部落格,有介紹說為什麼要自己寫一套,以及解決了哪些問題,滿值得一看的,可以順便了解一下。