檢視原始碼 統一資源識別符
基本概念
在撰寫本文時(2020 年 10 月),關於通用資源識別符和通用資源定位符有兩個主要標準
前者是具有適當形式語法的經典標準,使用所謂的 擴增巴科斯範式 (ABNF) 來描述語法,而後者是描述當前實踐的活文件,也就是說,大多數 Web 瀏覽器如何處理 URI。WHAT WG URL 專注於 Web,它沒有形式語法,只有對應該遵循的演算法的純英文描述。
它們之間有什麼差異嗎?它們為資源識別符提供了重疊的定義,並且它們不相容。uri_string
模組實作 RFC 3986,並且本文將通篇使用術語 URI。URI 是一個識別符,一個字元字串,用於識別特定的資源。
有關 URI 的更完整問題陳述,請查看 URL 問題陳述和方向。
什麼是 URI?
讓我們先從它不是什麼開始。它不是您在 Web 瀏覽器位址欄中輸入的文字。Web 瀏覽器會盡可能進行所有啟發式方法,將輸入轉換為可以透過網路傳送的有效 URI。
URI 是一個識別符,由符合 RFC 3986 中名為 URI
的語法規則的字元序列組成。
必須澄清的是,「字元」是指顯示在終端機上或寫在紙上的符號,不應與其內部表示混淆。
更具體地說,URI 是來自 US ASCII 字元集子集的字元序列。通用 URI 語法由分層的元件序列組成,這些元件稱為方案、授權、路徑、查詢和片段。在 RFC 3986 中,每個元件都有 ABNF 符號的正式描述。
URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
hier-part = "//" authority path-abempty
/ path-absolute
/ path-rootless
/ path-empty
scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
authority = [ userinfo "@" ] host [ ":" port ]
userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
reserved = gen-delims / sub-delims
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
uri_string 模組
由於產生和使用標準 URI 可能變得相當複雜,因此 Erlang/OTP 提供了一個模組 uri_string
,來處理所有最困難的操作,例如剖析、重組、正規化以及針對基準 URI 解析 URI。
uri_string
中的 API 函數使用兩種基本資料類型 uri_string()
和 uri_map()
。uri_string()
代表標準 URI,而 uri_map()
是一個更廣泛的資料類型,可以使用 Unicode 字元來表示 URI 元件。uri_map()
是一種方便的選擇,可以啟用諸如從具有特殊或 Unicode 字元的元件產生符合標準的 URI 等操作。用一個例子來解釋會更容易。
假設我們想要建立以下 URI 並透過網路傳送:http://cities/örebro?foo bar
。這不是有效的 URI,因為它包含 URI 中不允許的字元,例如「ö」和空格。我們可以透過剖析 URI 來驗證這一點
1> uri_string:parse("http://cities/örebro?foo bar").
{error,invalid_uri,":"}
URI 剖析器會嘗試所有可能的組合來解譯輸入,並在遇到冒號字元 ":"
時,在最後一次嘗試中失敗。請注意,最初的錯誤發生在剖析器嘗試解譯字元 "ö"
時,並在失敗後回溯到它有另一個可能剖析替代項的位置。
解決這個問題的正確方法是使用 uri_string:recompose/1
,並以 uri_map()
作為輸入
2> uri_string:recompose(#{scheme => "http", host => "cities", path => "/örebro",
query => "foo bar"}).
"http://cities/%C3%B6rebro?foo%20bar"
結果是一個有效的 URI,其中所有特殊字元都按照標準定義進行編碼。在 URI 上套用 uri_string:parse/1
和 uri_string:percent_decode/1
會傳回原始輸入
3> uri_string:percent_decode(uri_string:parse("http://cities/%C3%B6rebro?foo%20bar")).
#{host => "cities",path => "/örebro",query => "foo bar",
scheme => "http"}
這種對稱屬性在我們的屬性測試套件中大量使用。
百分比編碼
如您在前一章中所見,標準 URI 只能包含 US ASCII 字元集的嚴格子集,而且,允許的字元集在不同的 URI 元件中並不相同。當該八位元組的對應字元在允許的集合之外或被用作分隔符時,百分比編碼是一種在元件中表示資料八位元組的機制。這就是您看到 "ö"
編碼為 %C3%B6
和 space
編碼為 %20
的原因。大多數 API 函數在處理百分比編碼的三位元組時,都預期使用 UTF-8 編碼。Unicode 字元 "ö"
的 UTF-8 編碼是兩個八位元組:OxC3 0xB6
。字元 space
在 Unicode 的前 128 個字元中,並且使用單個八位元組 0x20
進行編碼。
注意
Unicode 向下相容於 ASCII,前 128 個字元的編碼與 ASCII 中的二進位值相同。
究竟哪些字元將被百分比編碼是一個主要的混淆來源。為了更容易回答這個問題,程式庫提供了一個實用程式函數 uri_string:allowed_characters/0
,該函數列出了每個主要 URI 元件以及最重要的標準字元集中允許的字元集。
1> uri_string:allowed_characters().
[{scheme,
"+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"},
{userinfo,
"!$%&'()*+,-.0123456789:;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"},
{host,
"!$&'()*+,-.0123456789:;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"},
{ipv4,".0123456789"},
{ipv6,".0123456789:ABCDEFabcdef"},
{regname,
"!$%&'()*+,-.0123456789;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"},
{path,
"!$%&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"},
{query,
"!$%&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"},
{fragment,
"!$%&'()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"},
{reserved,"!#$&'()*+,/:;=?@[]"},
{unreserved,
"-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"}]
如果 URI 元件的字元不被允許,則在產生 URI 時將會進行百分比編碼
2> uri_string:recompose(#{scheme => "https", host => "local#host", path => ""}).
"https://local%23host"
使用包含百分比編碼三位元組的 URI 可能需要許多步驟。以下範例顯示如何處理未正規化且包含多個百分比編碼三位元組的輸入 URI。首先,要將輸入 uri_string()
剖析為 uri_map()
。剖析只會將 URI 分割成其元件,而不會進行任何解碼
3> uri_string:parse("http://%6C%6Fcal%23host/%F6re%26bro%20").
#{host => "%6C%6Fcal%23host",path => "/%F6re%26bro%20",
scheme => "http"}}
輸入是有效的 URI,但是如何解碼這些百分比編碼的八位元組?您可以嘗試使用 uri_string:normalize/1
來正規化輸入。正規化操作會解碼那些對應於未保留集合中字元的百分比編碼三位元組。正規化是一個安全、等冪的操作,可將 URI 轉換為其標準形式
4> uri_string:normalize("http://%6C%6Fcal%23host/%F6re%26bro%20").
"http://local%23host/%F6re%26bro%20"
5> uri_string:normalize("http://%6C%6Fcal%23host/%F6re%26bro%20", [return_map]).
#{host => "local%23host",path => "/%F6re%26bro%20",
scheme => "http"}
輸出中仍然有一些百分比編碼的三位元組。此時,當 URI 已經被剖析時,可以安全地對剩餘的字元三位元組應用特定於應用程式的解碼。Erlang/OTP 提供了一個函數 uri_string:percent_decode/1
,用於原始百分比解碼,您可以在主機和路徑元件上或在整個映射上使用該函數
6> uri_string:percent_decode("local%23host").
"local#host"
7> uri_string:percent_decode("/%F6re%26bro%20").
{error,invalid_utf8,<<"/öre&bro ">>}
8> uri_string:percent_decode(#{host => "local%23host",path => "/%F6re%26bro%20",
scheme => "http"}).
{error,{invalid,{path,{invalid_utf8,<<"/öre&bro ">>}}}}
host
已成功解碼,但路徑包含至少一個具有非 UTF-8 編碼的字元。為了能夠解碼這個字元,您必須對這些三位元組中使用的編碼做出假設。最明顯的選擇是 _latin-1_,因此您可以嘗試使用 uri_string:transcode/2
將路徑轉碼為 UTF-8,並在轉碼後的字串上執行百分比解碼操作
9> uri_string:transcode("/%F6re%26bro%20", [{in_encoding, latin1}]).
"/%C3%B6re%26bro%20"
10> uri_string:percent_decode("/%C3%B6re%26bro%20").
"/öre&bro "
必須強調的是,直接在輸入 URI 上套用 uri_string:percent_decode/1
並不安全
11> uri_string:percent_decode("http://%6C%6Fcal%23host/%C3%B6re%26bro%20").
"http://local#host/öre&bro "
12> uri_string:parse("http://local#host/öre&bro ").
{error,invalid_uri,":"}
注意
百分比編碼在
uri_string:recompose/1
中實作,並且發生在將uri_map()
轉換為uri_string()
時。直接在輸入 URI 上套用任何百分比編碼是不安全的,就像uri_string:percent_decode/1
一樣,輸出可能是無效的 URI。引用函數允許使用者對應用程式資料執行原始百分比編碼和解碼,這些資料無法由uri_string:recompose/1
自動處理。例如,在使用者需要在路徑元件中使用 '/' 或子分隔符作為資料而不是分隔符的情況下。
正規化
正規化是將輸入 URI 轉換為 _標準_ 形式並保留對相同基礎資源的參照的操作。正規化最常見的應用是確定兩個 URI 是否等效,而無需存取它們參照的資源。
正規化有 6 個不同的步驟。首先,將輸入 URI 剖析為可以處理 Unicode 字元的仲介形式。這種資料類型是 uri_map()
,它可以在類型為 unicode:chardata/0
的映射元素中保存 URI 的元件。取得仲介形式後,會將一系列正規化演算法套用到個別 URI 元件
大小寫正規化 - 將
scheme
和host
元件轉換為小寫,因為它們不區分大小寫。百分比編碼正規化 - 解碼對應於未保留字元集中字元的百分比編碼三元組。
基於 scheme 的正規化 - 應用於 http、https、ftp、ssh、sftp 和 tftp 等 scheme 的規則。
路徑片段正規化 - 將路徑轉換為正規形式。
經過這些步驟後,中間資料結構,一個 uri_map()
,就被完全正規化了。最後一步是應用 uri_string:recompose/1
,將中間結構轉換為有效的正規 URI 字串。
請注意順序,我們在本用戶指南中多次使用的 uri_string:normalize(URIMap, [return_map])
是正規化過程中的一個快捷方式,它會返回中間資料結構,並且允許我們檢視並對剩餘的百分比編碼三元組應用進一步解碼。
13> uri_string:normalize("hTTp://LocalHost:80/%c3%B6rebro/a/../b").
"http://localhost/%C3%B6rebro/b"
14> uri_string:normalize("hTTp://LocalHost:80/%c3%B6rebro/a/../b", [return_map]).
#{host => "localhost",path => "/%C3%B6rebro/b",
scheme => "http"}
特殊考量
目前的 URI 實作提供了產生和使用標準 URI 的支援。此 API 並不打算直接暴露在網頁瀏覽器的網址列中,讓使用者可以隨意輸入自由文字。應用程式設計者應實作適當的啟發式方法,將輸入映射到可解析的 URI。