因為家裡有丟一些測試用的網站(Web),所以常常用到 nginx 做轉發避免太多測試用的 VM 搶光有限的 IP ,方法如下:透過 nginx 解析 http 請求 (Request) 中的 Host 標頭 (header) 後,再 Host 標頭轉發到不同的 VM。因為有借朋友放 .NET Core ,他在測試 SingnalR 有發生一些問題,以下紀錄災難發生與修復的過程。
原本的 nginx 設定檔大概長這個樣子:
server { include config.pool/listen.conf; server_name dotnet-test.harekaze.moe; access_log /var/log/nginx/access_dotnet-test-harekaze-moe.site.log; error_log /var/log/nginx/error_dotnet-test-harekaze-moe.site.log; # SSL 跳過,反正都是用 nginx-auto-config 產生的 location / { proxy_pass http://192.168.88.70:8080; include proxy_params; } }
SignalR 使用 WebSocket 做傳輸,按照設定了多次 WebSocket 搭配 nginx 設定的經驗,因為 nginx 預設會覆寫部分瀏覽器的請求標頭,再轉發過去 proxy_pass 的目標導致 WebSocket 通道建立失敗,所以參考了 Socket.io 的設定檔貼上,大概長這樣:
server { # ... location / { proxy_pass http://192.168.88.70:8080; include proxy_params; } location /socket { proxy_pass http://192.168.88.70:8080; include proxy_params; # ref: https://socket.io/docs/using-multiple-nodes/#NginX-configuration proxy_set_header Host $host; # enable WebSockets proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
不過這時候奇怪的事情就發生了,本來通道建立失敗的 /socket 路徑,現在直接變成 400 ,連握手的過程都不見了,如下:
首先當然是追查 log ,但是 error.log 裡面沒東西,所以肯定不是 nginx 回應 400 Bad Request,而是轉到 .NET Core 網站服務後,才返回 400 Bad Request。一行一行的取消註解後,最後發現必須這樣設定才能讓 Socket 正常握手
server {
# ...
location /socket {
proxy_pass http://192.168.88.70:8080;
include proxy_params;
# ref: https://socket.io/docs/using-multiple-nodes/#NginX-configuration
# proxy_set_header Host $host;
# enable WebSockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
或是這樣
server {
# ...
location /socket {
proxy_pass http://192.168.88.70:8080;
# include proxy_params;
# ref: https://socket.io/docs/using-multiple-nodes/#NginX-configuration
proxy_set_header Host $host;
# enable WebSockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Host 這個標頭為什麼會造成讓 .NET Core 的 http 服務認為這是不正常的請求呢?回頭去看看 proxy_params 裡面寫了什麼
proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
根據 nginx 的論壇上的討論, $http_host 會有主機名稱(Host)與連接埠號(Port)資訊 ,而 $host 只會有主機名稱。顯然我們透過 proxy_set_header 設定了兩次 Host 這個標頭(header)。那究竟是什麼樣的 Host 標頭會讓 .NET Core 直接回應 400 錯誤呢? curl 是重現特殊 Request 很方便的工具,透過 Chrome 開發者工具複製 curl 的 bash 指令並精簡後如下:
curl 'http://192.168.88.70:8080/' \ -H 'Connection: keep-alive' \ -H 'Upgrade-Insecure-Requests: 1' \ -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' \ -H 'Accept-Encoding: gzip, deflate' --compressed
但是 nginx 的文件也沒說用兩次 proxy_set_header 會發生什麼事,那只好猜猜看了:
- 會出現兩個 Host 標頭
- 只會有一個 Host 標頭,但是有兩串資訊
如果是第一種狀況, curl 指令如下:
curl 'http://192.168.88.70:8080/' \ -H 'Host: dotnet-test.harekaze.moe:80' -H 'Host: dotnet-test.harekaze.moe' -H 'Connection: keep-alive' \ -H 'Upgrade-Insecure-Requests: 1' \ -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' \ -H 'Accept-Encoding: gzip, deflate' --compressed
不過 .NET Core 正確回應網頁內容出來了,那試試看兩串資訊被塞進同一個 Host 標頭,根據順序應該會變成 $http_host$host ,所以 curl 指令如下:
curl 'http://192.168.88.70:8080/' \
-H 'Host: dotnet-test.harekaze.moe:80dotnet-test.harekaze.moe'
-H 'Connection: keep-alive' \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' \
-H 'Accept-Encoding: gzip, deflate' --compressed
接著 .NET Core 就再也沒有把 html 內容吐出來了,加上 -v 也看到 http 狀態是 400 ,結案收工。