jinhailang / blog

技术博客:知其然,知其所以然
https://github.com/jinhailang/blog/issues
60 stars 6 forks source link

url 特殊字符编码问题 #14

Open jinhailang opened 7 years ago

jinhailang commented 7 years ago

url 特殊字符编码

RFC 3986 指定了保留字符,分为两类,如下:

gen-delims    = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
                 / "*" / "+" / "," / ";" / "="

可能只会选择对其中的部分保留字符编码,而不同的编程语言和函数选择编码的字符不一样,这就导致不同系统之间的解析出来的 http url 不一致。

以 Go 为例,运行下面两段代码。

代码 1:

    murl := "http://testwhc5.b0.upaiyun.com/anything/<Wake Up To Dream> What's Media Lab 2016.mp4"
    mu, _ := url.Parse(murl)
    fmt.Printf("url: %s\r\n", murl)
    fmt.Printf("url.string: %s\r\n", mu.String())

输出:

url: http://testwhc5.b0.upaiyun.com/anything/<Wake Up To Dream> What's Media Lab 2016.mp4
url.string: http://testwhc5.b0.upaiyun.com/anything/%3CWake%20Up%20To%20Dream%3E%20What%27s%20Media%20Lab%202016.mp4

代码 2:

    murl = "http://testwhc5.b0.upaiyun.com/anything/%3CWake%20Up%20To%20Dream%3E%20What's%20Media%20Lab%202016.mp4"
    mu, _ = url.Parse(murl)
    fmt.Printf("url: %s\r\n", murl)
    fmt.Printf("url.string: %s\r\n", mu.String())

输出:

url: http://testwhc5.b0.upaiyun.com/anything/%3CWake%20Up%20To%20Dream%3E%20What's%20Media%20Lab%202016.mp4
url.string: http://testwhc5.b0.upaiyun.com/anything/%3CWake%20Up%20To%20Dream%3E%20What's%20Media%20Lab%202016.mp4

运行以上两段代码,会发现输出的 url string 有微小的差异,上面的将 ' 转码成了 %27,而下面没有转码。

按理来说,调用同样的函数,编码后的 url 应该一样才对,问题出在哪儿呢?

查看函数源码发现,函数 url.String 输出的是编码后的 url 字符串。但是,如果输入的原始 url 已经是编码的,那么就不会做处理直接使用原始 url。判断函数 validEncodedPath 代码片段:

        case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@':
            // ok
        case '[', ']':
            // ok - not specified in RFC 3986 but left alone by modern browsers
        case '%':
            // ok - percent encoded, will decode
        default:
            if shouldEscape(s[i], encodePath) {
                return false
            }

那么,上面两段代码输出结果不一致的原因基本找到了,那就是因为 validEncodedPath 判断认为代码 2 输入的 url 是已经编码过(已被 ngx_lua 编码)的,就将原始字符串输出了。而代码 1 输入的 url 被 Go 函数 EscapedPath 编码之后再输出。

再进一步查看 Go 的编码函数 escape(s string, mode encoding),判断是否需要编码函数 shouldEscape(c byte, mode encoding)判断,代码片段如下:

        case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved)
        // Different sections of the URL allow a few of
        // the reserved characters to appear unescaped.
        switch mode {
        case encodePath: // §3.3
            // The RFC allows : @ & = + $ but saves / ; , for assigning
            // meaning to individual path segments. This package
            // only manipulates the path as a whole, so we allow those
            // last three as well. That leaves only ? to escape.
            return c == '?'

            ...

可以看到 Go 会对 url path 中 ', ? 等特殊字符编码,而不会对 ;, =等字符编码。当然更多不同语言的编码差异, 需要具体分析了。对于使用多种编程语言的复杂系统,需要特别注意这点,最好是最后处理 url 的程序自己保持编码一致,即将输入的 url,处理成符合自己的编码规范。因为,一般解码函数(如 Go)没有这种差异,所以可以先对输入的 url 解码,再编码,这样能保证编码的 url 字符串,符合本程序语言规范的,从而保证本程序最终的 url 编码字符串一致

总结

所以,最好是不要在 url 中使用这些保留字符。不同的语言会有微小的差异,很容易导致 bug,查找和处理都很麻烦。

jinhailang commented 7 years ago

字符 + 的编码问题

今天又遇到一个坑,使用 ngx.unescape_uri 解码函数,会将 + 转换成 `(空格)[openresty 讨论贴](https://github.com/openresty/lua-nginx-module/issues/941),[W3C标准规定](https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1) 规定的应该是参数部分编码的时候,将 转成+,而路径部分应该还是转码成%2B,而ngx.escape_uri又会将+转成%2B,空格转成%20,所以这里应该是个 bug,而且ngx.var.uri解码的时候是不会转换+`,golang 也是正常的。

解决方案,是自己封装路径编码函数 unescape_path:

function _M.unescape_path(path)
    local s = gsub(path, "([^+]+)", function (s)
                       return unescape_uri(s)
    end)
    return s
end