1. 程式人生 > >淺談 URI 及其轉義

淺談 URI 及其轉義

replace 標識符 typedef 字符轉義 from always cif subject 自身

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,在 schemeauthority 確定下來的情況下標識資源,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 組件,它兼顧 pathquery 的保留字符集,更加嚴格,遵守了 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 及其轉義

淺談 URI 及其轉義