經過調試 Nginx 源碼來定位有趣 Nginx 轉發合併斜槓和編碼問題

背景

前段時間出現了一個請求在測試環境簽名成功,在線上環境簽名失敗的狀況,排查緣由是線上url中有雙斜槓會被合併成一個傳給後端,在測試環境中不會出現。這個就比較神奇了,Nginx 版本徹底同樣。前端

確認問題

方式是抓包確認:在線上Nginx和測試Nginx抓包,對比 如下例子中nginx

  • 218.218.218.218是線上服務器Nginxip
  • 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的源碼看看tomcat

環境:Mac+Clion 最終跟進了代碼:src/http/modules/ngx_http_proxy_module.cngx_http_proxy_create_request函數bash

下面這段代碼是生成轉發給upstreamhttp服務器

// 拷貝'GET'
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 的狀況下會置位1vars.uri的值的含義是Nginx配置文件中proxy_pass server後面那段 好比proxy_pass http://my-tomcat-server;那麼vars.uri值是NULL 好比proxy_pass http://my-tomcat-server/nimei;那麼vars.uri值是/nimeicurl

unparsed_uri = 0;

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這個基礎,會不會有第一步合併這一步編碼

merge_slashes proxy_pass後有無url 最終轉發url是否合併/
on 合併/
on 不合並/
off 不合並/
off 不合並/

同類的衍生問題

這個問題看起來表面上隻影響了雙斜槓的問題,實際上不少地方都有影響,好比剛恰好線上又出現了一塊兒問題 請求是: /easicar/v1/subCourses/{subCourseId}/comments/create 由於前端問題{subCourseId},沒有用值覆蓋它,在線上不正常,HTTP 狀態碼返回 400,在測試環境正常 。仍是由於那個問題致使的。

實驗結果以下

一、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}

二、server 後面沒有內容的時候 (模擬測試環境)

proxy_pass http://my-tomcat-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容許的

" 0x22 < 0x3c > 0x3e ^ 0x5e ` 0x60 { 0x7b | 0x7c } 0x7d 複製代碼

tomcat源碼也能夠知道

if (IS_CONTROL[i] || i > 127 ||
        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…

相關文章
相關標籤/搜索