淺談 URI 及其轉義

又拍雲發表於2018-01-11

URI

URI,全稱是 Uniform Resource Identifiers,即統一資源識別符號,用於在網際網路上標識一個資源,比如  這個 URI,指向的是一張漂亮的,描述又拍雲 CDN 產品特性的網頁。

URI 的組成

完整的 URI,由四個主要的部分構成:

<scheme>://<authority><path>?<query>

scheme 表示協議,比如 httpftp 等等,詳細介紹可以參考 rfc2396#section-3.1

authority,用 :// 來和 scheme 區分。從字面意思看就是“認證”,“鑑權”的意思,引用 rfc2396#secion-3.2 的一句話:

This authority component is typically defined by an Internet-based server or a scheme-specific registry of naming authorities.

這個“認證”部分,由一個基於 Internet 的伺服器定義或者由命名機關注冊登記(和具體的協議有關)。

而常見的 authority 則是:“由基於 Internet 的伺服器定義”,其格式如下:

<userinfo>@<host>:<port>

userinfo 這個域用於填寫一些使用者相關的資訊,比如可能會填寫 “user:password”,當然這是不被建議的。拋開這個不講,後面的 <host>:<port> 則是被熟知的伺服器地址了,host 可以是域名,也可以是對應的 IP 地址,port 表示埠,這是一個可選項,如果不填寫,會使用預設埠(也是和協議相關,比如 http 協議預設埠是 80)。

path,在 scheme 和 authority 確定下來的情況下標識資源,path 由幾個段組成,每個段用 / 來分隔。注意,path 不等同於檔案系統定義的路徑。

query,查詢串(或者說引數串),用 ? 和 path 區分開來,其具體的含義由這個具體資源來定義。

保留字元

從上面的描述裡看,URI 的這 4 個元件,由特定的分隔符來分離,這些分隔符各自有著特殊含義,而如果這些分隔符出現在某個元件內,比如 path 是 /a/b?c.html,那麼從 URI 整體角度來看的話, c.html 會被當做是 query,這樣就破壞了 path 原本的含義,因此 URI 引入了保留字符集,這些字元有著特殊的目的,如果它們被用於描述資源(而不是作為分隔符出現),那麼必須對它們轉義。

那麼什麼情況下需要對一個字元轉義呢,引用 rfc2395#section-2.2 的一句話:

In general, a character is reserved if the semantics of the URI changes if the character is replaced with its escaped US-ASCII encoding.

即如果轉義前後這個字元會影響到整個 URI 的意義,則它必須被轉義。

由於 URI 由多個元件構成,一個字元不轉義,可能會對其中一個元件會造成影響,但對另一個元件沒有影響,所以“保留字符集”是由具體的 URI 元件來規定的。

  • 對 path 部分而言,保留字符集是(參考自 rfc2396):

reserved = "/" | "?" | ";" | "="

  • 對 query 部分而言,保留字符集是(參考自 rfc2396):

reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "," | "$"

字元的轉義規則如下:

escaped     = "%" hex hex
hex         = digit | "A" | "B" | "C" | "D" | "E" | "F" |
                      "a" | "b" | "c" | "d" | "e" | "f"

比如 , 轉義後為 %2C

特殊字元

有一類不被允許用在 URI 裡的特殊字元,它們被稱為控制字元,即 ASCII 範圍在0-31 之間的字元,以及 ASCII 碼為 127 的這個字元。比如 \t\a 這些(不包括空格),因為這些字元不可列印而且在某些場景下可能會消失。

另外一類則是擴充套件 ASCII 碼,即範圍 128-255 的那些字元,它們不屬於 “US-ASCII coded character set”,因此這些字元如果出現在 URI 中,需要被轉義。

URLs are written only with the graphic printable characters of the US-ASCII coded character set. The octets 80-FF hexadecimal are not used in US-ASCII, and the octets 00-1F and 7F hexadecimal represent control characters; these must be encoded.

不安全字元

Characters can be unsafe for a number of reasons. The space character is unsafe because significant spaces may disappear and insignificant spaces may be introduced when URLs are transcribed or typeset or subjected to the treatment of word-processing programs. The characters “<” and “>” are unsafe because they are used as the delimiters around URLs in free text; the quote mark (“””) is used to delimit URLs in some systems. The character “#” is unsafe and should always be encoded because it is used in World Wide Web and in other systems to delimit a URL from a fragment/anchor identifier that might follow it. The character “%” is unsafe because it is used for encodings of other characters. Other characters are unsafe because gateways and other transport agents are known to sometimes modify such characters. These characters are “{“, “}”, “|”, “", “^”, “~”, “[”, “]”, and “`”.

這段話引用自 rfc1738 2.2 節。因為種種的原因,存在一類字元,它們是 “unsafe” 的,不加處理地存在在 URI 裡,會破壞 URI 的語義完整性,對於這類字元,如果要出現在 URI 裡,那麼也得進行轉義。

nginx 的 URI 轉義機制

nginx (以現在最新的 1.13.8 版本為準)提供了一個名為 ngx_escape_uri 的函式,函式原型如下:

uintptr_t ngx_escape_uri(u_char *dst, u_char *src, size_t size,
 	ngx_uint_t type);

第三個引數,type,可以接受這些值:

#define NGX_ESCAPE_URI            0
#define NGX_ESCAPE_ARGS           1
#define NGX_ESCAPE_URI_COMPONENT  2
#define NGX_ESCAPE_HTML           3
#define NGX_ESCAPE_REFRESH        4
#define NGX_ESCAPE_MEMCACHED      5
#define NGX_ESCAPE_MAIL_AUTH      6

我們只關心其中的 NGX_ESCAPE_URI NGX_ESCAPE_ARGS NGX_ESCAPE_URI_COMPONENT ,根據 nginx 官方所提供的 nginx 模組和核心 API 介紹,這三個巨集的含義如下:

TypeDefinitionNGX_ESCAPE_URIEscape a standard URINGX_ESCAPE_ARGSEscape query argumentsNGX_ESCAPE_URI_COMPONENTEscape the URI after the domain

對應地,ngx_escape_uri這個函式,內建了幾個相關的 bitmap,區別就是在於各自的轉義字符集,具體可以查閱 nginx 的原始碼(src/core/ngx_string.c)。

其中針對整個 URI 的轉義處理,ngx_escape_uri 會把 " ", "#", "%", "?" 以及 %00-%1F 和 %7F-%FF 的字元轉義;針對 query 的轉義,會把 " ", "#", "%", "&", "+", "?" 以及 %00-%1F 和 %7F-%FF 的字元轉義;針對 path + query(稱之為 the URI after the domain)的轉義,會把除英文字母,數字,以及 "-", ".", "_", "~" 這些以外的字元全部轉義。

可以看到,NGX_ESCAPE_URI 和 NGX_ESCAPE_ARGS 沒有處理不安全字元,前者站在處理整個的 URI 的角度上編碼,後者站在處理 query 的角度上編碼;而 NGX_ESCAPE_URI_COMPONENT ,處理角度不是整個 URI,而是 domain 之後的 URI 元件,它兼顧 path 和 query 的保留字符集,更加嚴格,遵守了 rfc3986#section-2.2 的規範。

這裡順便提一下 ngx_proxy 模組對應的 URI 轉義處理,在構造向上遊傳送的請求行時,ngx_proxy 模組針對 proxy_pass 指令做出了不同的處理:

  • 如果指定的 URI 包含了變數,將解析變數,然後直接將解析後的 URI 傳送到上游;
  • 如果 URI 不含變數,且沒有指定 path 部分,將使用客戶端發來的 path 部分拼接到 URI 中,然後傳送到上游;
  • 如果URI 不含變數,且指定了 path,這裡的處理比較特殊,nginx 會把解碼過的,由客戶端發來的 URI 裡的 path 部分(去掉和當前 location 的公共字首),進行編碼(按 NGX_ESCAPE_URI 來操作),和 proxy_pass 指令指定 的 path 拼接,傳送到上游,比如這樣的配置:
location /foo {
    proxy_pass http://127.0.0.1:8082/bar;
}

 

如果客戶端發來的 URI 裡 path 是 /foo/%5B-%5D,最終上游的 URI path 會是 /bar/[-]

因此我們在做 nginx conf 配置的時候,也需要小心考慮 URI 編碼的問題。

ngx_lua 的 URI 轉義機制

ngx_lua 提供的 ngx.escape_uri 函式,和 nginx 核心的轉義機制也有一些差異(基於 ngx_lua v0.10.11),體現在對保留字元的處理上,ngx.escape_uri底層使用的 ngx_http_lua_escape_uri,結構和 ngx_escape_uri 一致,而對應的 bitmap 不同。

對於整個 URI 的轉義處理,在 ngx_escape_uri 的基礎上,對 '"', '&', '+', '/', ':', ';', '<', '=', '>', '[', '\', ']', '^', '_', '{' , '}'進行轉義;對於 query 的處理,這裡去掉了 & 的轉義;對於 path + query 的處理,去掉了對 "'", "*", ")", "(", "!" 的轉義。目前 ngx.escape_uri 使用的是 NGX_ESCAPE_URI_COMPONENT,從 PR 提交的資訊來看,目前 ngx.escape_uri 的行為和 Chrome JS 實現的 encodeURIComponent 一致。

另外,ngx_lua 對 URI 的解碼操作,除了它把 + 解碼為空格以外,其他和 nginx 相同。

總結

在做相關的代理服務,閘道器服務時,URI 的編解碼處理都是非常重要的,某些場景我們可能需要用 URI 來做 key(比如作為 hash 函式的因子),如果不處理好編解碼問題,可能在 URI 複雜的情況下會達不到我們的預期效果,反而會浪費很多時間去排查問題的原因,特別地,在使用 nginx 和 ngx_lua 做服務時,我們更應該熟知它們在 URI 編解碼上的區別,在理解它們的區別上做自身的業務處理,避免踩坑。

參考資料

原文閱讀

淺談 URI 及其轉義

相關文章