查看源代码 统一资源标识符
基础知识
在撰写本文时(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 是来自美国 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 只能包含美国 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_string:percent_decode/1
的情况一样,直接对输入 URI 应用任何百分比编码都是不安全的,输出可能是一个无效的 URI。 引用函数允许用户对应用程序数据执行原始百分比编码和解码,而uri_string:recompose/1
无法自动处理这些数据。 例如,在用户需要在路径组件中使用“/”或子分隔符而不是分隔符作为数据的情况下。
规范化
规范化是将输入 URI 转换为规范形式并保留对同一底层资源的引用的操作。 规范化的最常见应用是确定两个 URI 是否等效,而无需访问它们引用的资源。
规范化包含 6 个不同的步骤。首先,输入的 URI 被解析为可以处理 Unicode 字符的中间形式。这种数据类型是 uri_map()
,它可以在类型为 unicode:chardata/0
的 map 元素中保存 URI 的各个组成部分。获得中间形式后,将对各个 URI 组件应用一系列规范化算法。
大小写规范化 - 将
scheme
和host
组件转换为小写,因为它们不区分大小写。百分号编码规范化 - 解码对应于未保留字符集中字符的百分号编码的三元组。
基于方案的规范化 - 应用于 http、https、ftp、ssh、sftp 和 tftp 方案的规则。
路径段规范化 - 将路径转换为规范形式。
完成这些步骤后,中间数据结构 uri_map()
就完全规范化了。最后一步是应用 uri_string:recompose/1
,它将中间结构转换为有效的规范 URI 字符串。
请注意顺序,我们在本用户指南中多次使用的 uri_string:normalize(URIMap, [return_map])
是规范化过程中的一个快捷方式,它返回中间数据结构,允许我们检查并对剩余的百分号编码三元组应用进一步的解码。
13> uri_string:normalize("hTTp://LocalHost:80/%c3%B6rebro/a/../b").
"https://127.0.0.1/%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 并非旨在直接暴露在 Web 浏览器的地址栏中,用户可以在其中输入自由文本。应用程序设计人员应实现适当的启发式方法,以将输入映射到可解析的 URI。