3

通过调试 Nginx 源码来定位有趣 Nginx 转发合并斜杠和编码问题

 3 years ago
source link: https://club.perfma.com/article/2344568
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
通过调试 Nginx 源码来定位有趣 Nginx 转发合并斜杠和编码问题

通过调试 Nginx 源码来定位有趣 Nginx 转发合并斜杠和编码问题

挖坑的张师傅
nginx
2天前

背景
前段时间出现了一个请求在测试环境签名成功,在线上环境签名失败的情况,排查原因是线上url中有双斜杠会被合并成一个传给后端,在测试环境中不会出现。这个就比较神奇了,Nginx 版本完全一样。
确认问题
方式是抓包确认:在线上Nginx和测试Nginx抓包,对比
以下例子中

  • 218.218.218.218是线上服务器Nginx的ip
  • 121.121.121.121是自己电脑出口ip
  • 10.0.0.1是线上Nginx的局域网ip
  • 10.0.0.2是 Java 业务机的局域网 ip
1. 从自己电脑到线上Nginx的包如下:

17:41:47.110728 IP 121.121.121.121.50935 > 218.218.218.218.80: Flags [P.]
GET /easicar/v1//subCourses/9952078022974031963e5d9a399e9958/text?subCourseId=9952078022974031963e5d9a399e9958 HTTP/1.1
Host: masaike.seewo.com
User-Agent: curl/7.54.0
Accept: */*

2. Nginx到后端的请求如下

17:41:47.113138 IP 10.0.0.1.49610 > 10.0.0.2.40088: Flags [P.]

GET /easicar/v1/subCourses/9952078022974031963e5d9a399e9958/text?subCourseId=9952078022974031963e5d9a399e9958 HTTP/1.1
x-ccloud-pre: 1
X-Forwarded-Url: http://masaike.seewo.com/easicare/v1//subCourses/9952078022974031963e5d9a399e9958/text?subCourseId=9952078022974031963e5d9a399e9958
Host: masaike.seewo.com
X-Real-IP: 121.121.121.121
X-Forwarded-For: 121.121.121.121
X-Forwarded-Proto: http
User-Agent: curl/7.54.0
Accept: */*

可以看到Nginx转发到后端Java这里的时候,/easicar/v1//subCourses/已经没有两个斜杠了,但是测试环境转到后端的时候是有的,这里就不贴包内容了。
自己在本地测试了很久,发现都不会合并多余的/,决定debug一下Nginx的源码看看
环境:Mac+Clion
最终跟进了代码:src/http/modules/ngx_http_proxy_module.c的ngx_http_proxy_create_request函数
下面这段代码是生成转发给upstream的http包

b->last = ngx_copy(b->last, method.data, method.len);
*b->last++ = ' ';

u->uri.data = b->last;

// 拷贝uri,核心差别就在这里
// 如果unparsed_uri=1,url部分就使用unparsed_uri.data,就是没有合并斜杠的url
// 如果unparsed_uri=0,url部分就使用uri.data,就是合并过斜杠的url

if (plcf->proxy_lengths && ctx->vars.uri.len) {
    b->last = ngx_copy(b->last, ctx->vars.uri.data, ctx->vars.uri.len);
} else if (unparsed_uri) {
    // 如果unparsed_uri=1,url使用unparsed_uri.data
    b->last = ngx_copy(b->last, r->unparsed_uri.data, r->unparsed_uri.len);

} else {
    if (r->valid_location) {
        b->last = ngx_copy(b->last, ctx->vars.uri.data, ctx->vars.uri.len);
    }

    if (escape) {
        ngx_escape_uri(b->last, r->uri.data + loc_len,
                       r->uri.len - loc_len, NGX_ESCAPE_URI);
        b->last += r->uri.len - loc_len + escape;

    } else {
        // 如果unparsed_uri=0,url使用uri.data,uri.data是合并过的url
        b->last = ngx_copy(b->last, r->uri.data + loc_len,
                           r->uri.len - loc_len);
    }

    // 这里是拼接querystring
    if (r->args.len > 0) {
        *b->last++ = '?';
        b->last = ngx_copy(b->last, r->args.data, r->args.len);
    }
}



那么unparsed_uri这个标记位怎么来的?
ctx->vars.uri.len == 0 的情况下会置位1,vars.uri的值的含义是Nginx配置文件中proxy_pass server后面那段
比如proxy_pass http://my-tomcat-server;那么vars.uri值是NULL
比如proxy_pass http://my-tomcat-server/nimei;那么vars.uri值是/nimei

image.png


if (plcf->proxy_lengths && ctx->vars.uri.len) {
    uri_len = ctx->vars.uri.len;

} else if (ctx->vars.uri.len == 0 && r->valid_unparsed_uri && r == r->main)
{
    // ctx->vars.uri.len == 0 的情况下会置位1
    unparsed_uri = 1;
    uri_len = r->unparsed_uri.len;

} else {
    loc_len = (r->valid_location && ctx->vars.uri.len) ?
                  plcf->location.len : 0;

    if (r->quoted_uri || r->space_in_uri || r->internal) {
        escape = 2 * ngx_escape_uri(NULL, r->uri.data + loc_len,
                                    r->uri.len - loc_len, NGX_ESCAPE_URI);
    }

    uri_len = ctx->vars.uri.len + r->uri.len - loc_len + escape
              + sizeof("?") - 1 + r->args.len;
}


回过来看这个问题,就很简单了

location /easicar {
     proxy_pass http://easicar/easicar;
     
测试环境配置
location / {
     proxy_pass http://nginx-ingress;     


线上配置server后面多了/easicar,会走unparsed_uri=0的逻辑,会使用合并过/的url,测试环境server后面是空的,会走unparsed_uri=1的逻辑,会不合并url
还有一个问题,merge_slashes这个指令有什么用?merge_slashes这个指令默认是开的,会决定会不会自动合并uri中的/,决定了uri这个基础,会不会有第一步合并这一步
image.png

同类的衍生问题

这个问题看起来表面上只影响了双斜杠的问题,实际上很多地方都有影响,比如刚刚好线上又出现了一起问题
请求是: /easicar/v1/subCourses/{subCourseId}/comments/create
因为前端问题{subCourseId},没有用值覆盖它,在线上不正常,HTTP 状态码返回 400,在测试环境正常
。还是因为那个问题导致的。
实验结果如下
1、server 后面有内容的时候 (模拟线上情况)

proxy_pass http://my-tomcat-server/nimei 

客户端请求到 Nginx
19:03:01.396763 IP 127.0.0.1.61759 > 127.0.0.1.8080: Flags [P.]
POST /apm-demo-server/%7Bfoo%7D//bar HTTP/1.1
Content-Type: text/plain; charset=utf-8

Nginx请求到upstream
19:03:01.398280 IP6 ::1.61760 > ::1.8111: Flags [P.]
POST /nimei/{foo}/bar HTTP/1.1
X-Forwarded-Url: http://ya-dev.test.xiwo.com/apm-demo-server/%7Bfoo%7D//bar

可以看到转发到后端服务器那里的时候已经是解码过的{foo}

2、server 后面没有内容的时候 (模拟测试环境)


客户端请求到 Nginx
19:16:37.949701 IP 127.0.0.1.62054 > 127.0.0.1.8080: Flags [P.]
POST /apm-demo-server/%7Bfoo%7D//bar HTTP/1.1

Nginx请求到upstream
19:16:37.953191 IP6 ::1.62055 > ::1.8111: Flags [P.]
POST /apm-demo-server/%7Bfoo%7D//bar HTTP/1.1
X-Forwarded-Url: http://ya-dev.test.xiwo.com/apm-demo-server/%7Bfoo%7D//bar


可以看到转发到后端服务器那里的时候已经是未解码过的 %7Bfoo%7D
Nginx 在 server 后面 uri 不为空的时候,会把 url 解码、合并斜杆后的 url 传给 upstream 服务器,但是 tomcat 会拒绝掉部分未经编码的他觉得不合法的字符。无论发起方编码的多么好,都会有问题
解决办法有两种:

  • 修改Nginx配置
  • 修改tomcat配置 Connector 中 relaxedQueryChars 属性,使之支持 { 这些特殊字符

那么哪些是http协议认为的合法的字符呢?
写了一段代码,经过一层Nginx,转发到tomcat,遍历了0-127的所有字符,以下字符是一定不被tomcat允许的

< 0x3c
> 0x3e
^ 0x5e
` 0x60
{ 0x7b
| 0x7c
} 0x7d

看tomcat源码也可以知道

        i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' ||
        i == '^' || i == '`'  || i == '{' || i == '|' || i == '}') {
    if (!REQUEST_TARGET_ALLOW[i]) {
        IS_NOT_REQUEST_TARGET[i] = true;
    }
}


具体可以参考:RFC 7230 和 RFC 3986

参考链接:stackoverflow.com/questions/1…


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK