網頁本身是個合法的 JSON 的展示

Hacker News Daily 上看到「Web Data Render」這個有趣的東西,這個網頁本身是個 JSON,透過一些技巧載入 javascript 後就可以讀取資料 render...

不過網頁本身就不是合法的 HTML 了:「Showing results for https://webdatarender.com/」,只能算是個有趣的作法...

用 GitHub Actions 記錄 API 資料的變化

Hacker News Daily 上看到的方式,Simon Willison 利用了 GitHub Actions 定時去抓資料更新 git repository:「Git scraping: track changes over time by scraping to a Git repository」。

文章裡面測試了 JSON 檔案的變化:

這個方式利用了 GitHub 自家的架構做完所有的事情,因為他的範例是拉加州政府的資料,感覺 g0v 裡應該有些專案也用這個方式搞,翻了一下 Telegram 上的記錄,果然翻到記錄了:「零營運費用開源開發」。

另外我猜用 free-for.dev 這邊的資源應該也有機會堆出類似的東西...

PHP 8 的提案,將 JSON library 放入必須項目

Twitter 上看到的通知,這個提案將 PHPJSON 列為語言的必須項目,目前的狀態是 Under Discussion:「PHP RFC: Always available JSON extension」。

以前沒有引入的一個原因是因為底層使用的 library 的授權 JSON license 不是 open-source license,這對於要打包出 binary 散佈時的問題很大 (跟其他 license 衝突):

The Software shall be used for Good, not Evil.

在 PHP 7 之後,JSON 的實做決定改用 jsond (參考「PHP RFC: Replacing current json extension with jsond」),這邊用的是 PHP License 授權:「LICENSE」,這個因素就緩解了。

而這個提案提議拔掉 ./configure –-disable-json 關閉 JSON library 的能力,把 JSON library 變成 PHP language 的一部份:

Make it impossible to disable the JSON extension through configuration or build options. Require that JSON be built statically instead of as a shared library.

這個提案如果通過的話,對大多數人應該還是沒什麼影響,因為一般在用的版本都會裝 JSON library。而且現在會透過 Composor 管理套件,很容易就會有 dependency 會用到 JSON 而需要安裝 JSON library,問題不太大...

JSON Canonicalization

這篇是講 JSON object 上的簽名,但實際上就是在討論 JSON Canonicalization 的前因後果:「How (not) to sign a JSON object」。

在處理 JSON 資料時,「判斷兩個 JSON object 是否相同」是一個不怎麼簡單的問題,其中一個想法是找一個機制可以把意義相同的 JSON object 都轉成相同的 (byte)string representative,這也就是 JSON Canonicalization。當你可以確保意義相同的 JSON Canonicalization 後,你就可以對 string 本身簽名。

這件事情其實在 XML 就有過同樣的歷史故事 (yeah,總是有人愛在某種資料格式上面疊上簽名),也就是「XML Signature」這個方式。

在 XML 這邊不幸的是,還不少標準選用 XML Signature,像是當年為了實做 Google Apps (現在叫做 G Suite) 的 SSO,而需要接 SAML...

回到原來的 JSON Canonicalization,可以馬上想到的變化包括了空白與 object 裡 key 的順序,也就是這兩個:

{"a":1,"b":2}
{
  "b": 2,
  "a": 1
}

但不幸的是,還有 Unicode 來一起亂,也就是下面這個跟上面有相同的意思:

{
  "\u0062": 2,
  "\u0061": 1
}

另外還有其他的地雷是平常不會想到的,如果你因為複雜而決定用 library 來做,那也代表 library 必須面對這些複雜的情境,未必沒有 bug...

所以文章作者在最後面才會請大家不要再來亂了 XDDD

Maybe you don’t need request signing? A bearer token header is fine, or HMAC(k, timestamp) if you’re feeling fancy, or mTLS if you really care.

Canonicalization is fiendishly difficult.

Add a signature on the outside of the request body, make sure the request body is complete, and don’t worry about “signing what is said versus what is meant” – it’s OK to sign the exact byte sequence.

gron:把 JSON 結構轉成條列式的資料,方便後續的文字處理...

在「gron makes JSON more greppable」這邊看到 gron 這個工具,可以將 JSON 轉成條列式的資料 (或是反過來,將條列式的資料轉回 JSON)。

像是網站上給的範例之一:

▶ gron testdata/two.json 
json = {};
json.contact = {};
json.contact.email = "mail@tomnomnom.com";
json.contact.twitter = "@TomNomNom";
json.github = "https://github.com/tomnomnom/";
json.likes = [];
json.likes[0] = "code";
json.likes[1] = "cheese";
json.likes[2] = "meat";
json.name = "Tom";

這讓 grep 或是 sed 之類的工具會更好操作,不然得用 jq 盧半天...

新出的 RFC 8259:The JavaScript Object Notation (JSON) Data Interchange Format

JSON 的規格書又被更新了 XD

在「The Last JSON Spec」這邊,Tim Bray 寫了這篇關於新的 RFC 8259 跟之前的差異,以及大家對於雙重標準的顧慮。

最大的差異在於,在 RFC 8259 規定了「如果 JSON 被用在非封閉的系統交換資料,必須使用 UTF-8」:

8259 con­tains one new sen­tence: “JSON text ex­changed be­tween sys­tems that are not part of a closed ecosys­tem MUST be en­cod­ed us­ing UTF-8 [RFC3629].” Giv­en that, by 2017, an at­tempt to ex­change JSON en­cod­ed in any­thing but UTF-8 would be ir­ra­tional, this hard­ly needs say­ing; but its ab­sence felt like an omis­sion.

而關於 ECMA-404 與 RFC 8259 都定義了 JSON 的問題他也說明了,因為很多人花了很多力氣在確保這兩份文件的正確性上,所以應該不會有問題 (i.e. 衝突):

The rea­son 8259 ex­ists is that the ECMAScript gang went and wrote their own ex­treme­ly min­i­mal spec, Stan­dard ECMA-404: The JSON Da­ta In­ter­change Syn­tax, and there was rea­son for con­cern over du­el­ing stan­dard­s. But, af­ter a cer­tain amount of standards-org elephant-gavotte, each of ECMA 404 and RFC 8259 nor­ma­tive­ly ref­er­ences the oth­er and con­tains a com­mit­ment to keep them con­sis­tent in case any er­rors turn up. Which is a good thing, but this text has been re-examined and re-polished so many times that I doubt ei­ther side will ev­er re­vis­it the ter­ri­to­ry, thank good­ness.

另外他也提到了對於不同情境下可以看不同的文件。像是要了解 JSON 的話,可以看當初發明 JSON 的 Doug Crockford 所設立的網站 (在「JSON」這邊);而在交換時應該參考 I-JSON (Internet JSON,RFC 7493):

Which spec should you use? · If you want to un­der­stand JSON syn­tax, you still can’t beat Doug Crockford’s orig­i­nal for­mu­la­tion at JSON.org. If you want to use an RFC as foun­da­tion for a REST API or some oth­er In­ter­net pro­to­col, I ac­tu­al­ly don’t rec­om­mend 8259, I rec­om­mend I-JSON, RFC 7493, which de­scribes ex­act­ly the same syn­tax as all the oth­er specs (by ref­er­enc­ing 7159), but ex­plic­it­ly rules out some legal-but-dumbass things you could do that might break your pro­to­col, for ex­am­ple us­ing any­thing but UTF-8 or hav­ing du­pli­cate mem­ber names in your ob­ject­s.

I-JSON 是 JSON 的子集合,比較重要的:

  • (MUST) 使用 UTF-8。
  • (SHOULD NOT) 浮點數的部份,不得超過 IEEE 754-2008 binary64 (double precision) 的範圍。
  • (SHOULD NOT) 整數的部份,不得超過 [-(2**53)+1, (2**53)-1]) 的範圍。
  • (RECOMMEND) 有超過的需求使用字串表示。
  • (MUST NOT) JSON object 內不得有重複的 name。
  • (SHOULD NOT) 最上層的型態不得使用字串,只能使用 object 或是 array。
  • (MUST NOT) 遇到先前沒有定義過的元素不得視為錯誤。(像是新版 API 內會在 object 裡增加元素)
  • (RECOMMEND) 時間使用 ISO 8601 表示 (在 RFC 3339 有提到),英文字的部份全部使用大寫,一定要標上時區,而秒數的 0 一定要加上去 (也就是 00 秒)。
  • (RECOMMEND) 時間長度也建議依照 RFC 3339 處理。
  • (RECOMMEND) Binary 資料用 base64url 傳 (RFC 4648)。

JSON 的 Object 裡 Key 重複的問題

tl;dr:不要亂來啦... 這是 UB (Undefined behavior) 的一種。

因為看到這則 tweet,所以去查一下 JSON 的資料:

首先是找標準是什麼。在維基百科的 JSON 條目裡提到了有兩份標準,一份是 RFC 7159,一份是 ECMA-404

Douglas Crockford originally specified the JSON format in the early 2000s; two competing standards, RFC 7159 and ECMA-404, defined it in 2013. The ECMA standard describes only the allowed syntax, whereas the RFC covers some security and interoperability considerations.

ECMA-404 裡面就真的只講語法沒講其他東西,而在 RFC 7159 內的 Object 則是有提到 (重點我就用粗體標起來了):

An object structure is represented as a pair of curly brackets surrounding zero or more name/value pairs (or members). A name is a string. A single colon comes after each name, separating the name from the value. A single comma separates a value from a following name. The names within an object SHOULD be unique.

   object = begin-object [ member *( value-separator member ) ]
            end-object

   member = string name-separator value

An object whose names are all unique is interoperable in the sense that all software implementations receiving that object will agree on the name-value mappings. When the names within an object are not unique, the behavior of software that receives such an object is unpredictable. Many implementations report the last name/value pair only. Other implementations report an error or fail to parse the object, and some implementations report all of the name/value pairs, including duplicates.

JSON parsing libraries have been observed to differ as to whether or not they make the ordering of object members visible to calling software. Implementations whose behavior does not depend on member ordering will be interoperable in the sense that they will not be affected by these differences.

粗體有描述唯一性,但尷尬的地方在於他用 SHOULD 而非 MUST,所以 library 理論上都要能接受。但後面提到如果不唯一時,行為無法預測 (會到 rm -rf / 嗎?XDDD 最像的應該還是 crash?),所以還是不要亂來啦...

不過如果真的會 crash 的話,應該也會因為 DoS issue 而被發 CVE,所以實務上應該是不會 crash 啦...

在 CLI 下開關以及查詢 EC2 的狀態...

有時候需要開 Ubuntu 測試東西,會在 AWS 上開 EC2 起來測試,但開 console 太麻煩了,寫幾個 function 丟進 shell script 裡面比較乾脆。其中查詢 Ubuntu AMI 的程式出自「How do I know what Ubuntu AMI to launch on EC2?」這邊。

ec2.ls() 裡,我的 jq 版本比較舊,不過不影響我的 copy & paste,所以就沒有 hack 他了。新版的應該可以多加上 | @tsv 變成 tab 隔開 (沒測過,查資料時查到而已)。

ec2.run() 裡,我這邊是先到 console 上查出 security group 與 subnet 的 id,然後這邊 hard code 進去。我的預設是開 t2.medium,臨時要指定的話就 ec2.run t2.nano 就可以改開 t2.nano 了,不過要注意的是,這邊程式在查詢時的條件是 hvm:ebs,換的時候要注意 image 相容性...

# AWS-related
function ec2.ls() {
    aws ec2 describe-instances | \
        jq -c -M '.Reservations[] | .Instances[] | [.InstanceId, .InstanceType, .PublicIpAddress]'
}

function ec2.rm() {
    local INSTANCE_ID=${1:i-xxxxxxxxxxxxxxxxx}
    aws ec2 terminate-instances --instance-id ${INSTANCE_ID}
}

function ec2.run() {
    local INSTANCE_TYPE=${1:-t2.medium}
    aws ec2 run-instances --image-id $(ec2.ubuntu_ami()) --key-name gslin --security-group-ids sg-xxxxxxxx --instance-type ${INSTANCE_TYPE} --subnet-id subnet-xxxxxxxx
}

function ec2.ubuntu_ami() {
    curl -s "https://cloud-images.ubuntu.com/locator/ec2/releasesTable" | \
    sed '$x;$G;/\(.*\),/!H;//!{$!d};$!x;$s//\1/;s/^\n//' | \
    jq -c '.aaData[] | select(contains(["16.04", "us-east-1", "hvm:ebs"]))' | \
    grep -o 'ami-[a-z0-9]\+' | \
    head -1
}

這種工具自己用的順手比較重要,要什麼功能自己改自己加...

話說 Ubuntu 網站上的 JSON 居然吐出 malformed data (trailing comma),這是自己 printf() 之類硬幹出來的嗎... XD

HTTP/2 時代的 API 設計

在「Let’s Stop Building APIs Around a Network Hack」這邊提到了以前為了解決 HTTP/1.1 的問題而發展出來的 workaround,在 2015 年發展出來的 HTTP/2 從底層直接解了不少問題,加上很快被許多瀏覽器支援 (就算不支援 HTTP/2 也只是降到 HTTP/1.1 跑,比較慢而已):

Guess what else was released in May 2015? RFC 7540, otherwise known as HTTP/2. In retrospect this seems highly poetic, as HTTP/2 kinda makes the compound document aspect of JSON-API a little bit pointless, and compound documents to me go hand in hand with what JSON-API is as a standard.

2012 年在 MOPCON 第一屆講的「API Design Optimized for Mobile Platform」剛好就是這個主題:

有種懷念感... XD

PHP 7.3 的 json_decode() 將會用 Exception 處理錯誤

在「PHP: rfc:json_throw_on_error」這邊提到 PHP 7.3 會解決 json_decode() 發生錯誤時的處理方式:

PHP has two functions for dealing with JSON, json_decode() and json_encode(). Unfortunately, both have suboptimal error handling. json_decode() returns null upon erroring, but null is also a possible valid result (if decoding the JSON “null”).

在這之前唯一的判斷方式是另外再呼叫 json_last_error() 或是 json_last_error_msg(),但這樣寫很辛苦,所以要引入 JsonException 了,總算...