題注
最近我在學習nginx的lua插件,發現結合nginx的異步io和lua的流程控制能力,還是有很豐富的想象空間的:幾乎所有常見的http請求的處理邏輯都能搞定,諸如查查數據庫,訪問一下memcache,讀寫一下本地文件等,都不在話下。恰好我正在研究一個第三方http服務的調用api,就產生了將其移植到lua上的想法。
其中涉及到了構造https請求的需求。我先采用ngx.socket.tcp()自己構造了一個http格式的請求,測試結果為可用,然后考慮能不能找到封裝好了的httpclient。放狗搜索了一番后,只找到了這個庫:lua-resty-http (https://github.com/liseen/lua-resty-http)。無奈下載下來后發現其不支持https,遂fork並修改了一番(https://github.com/kyriosli/lua-resty-http),加上了https支持。
到此貌似整個移植過程就完了?NO,nginx本身就是玩http的,為啥我們還要自己構造http請求,解析請求結果,管理連接復用呢?想到這一層,我又把眼光轉到了nginx自帶的api上
原理
ngx_lua本身不提供httpclient功能,但提供了ngx.location.capture接口。這個接口可以發送一個子請求(subrequest),並獲取子請求的響應結果。子請求可以調用lua,或者返回文件,甚至通過proxy_pass訪問另一個地址。
關鍵就在這里!如果我們使用proxy_pass機制結合subrequest,不就能實現http接口請求了嗎?
且慢!
一般來說,httpclient要求能夠指定method,scheme,域名,端口,請求頭,以及數據。但默認的subrequest將繼承所有當前請求的信息。好在ngx.location.capture接口支持指定method和data。那么只要搞定scheme,域名,端口,請求頭就可以了。
實現
使用過nginx的proxy_pass的同學都知道,proxy_pass支持從變量中獲取值(如常見的proxy_pass $scheme://xxx$request_uri;)。而rewrite能夠利用捕獲組設置變量,所以,我們可以在nginx的配置文件中加入這么一條location規則:
location /proxy/ { internal; rewrite ^/proxy/(https?)/([^/]+)/(\d+)/(.*) /$4 break; proxy_pass $1://$2:$3; }
- 第一行,指定規則為internal規則,防止外部請求命中此規則
- 第二行,使用rewrite將地址中的scheme,host,port匹配出來;使用break終止匹配,不再檢查其它location
- 第三行,使用proxy_pass將scheme,host,port拼裝成服務器地址
這樣,我們要請求如https://www.example.com:443/foo/bar時,只需要在lua代碼中構造一個子請求,地址為/proxy/https/www.example.com/443/foo/bar即可。
接下來就是header問題,通過ngx.req.set_header可以在lua中添加需要的請求頭。
由於header的繼承性,我們需要考慮來自客戶端的哪些header不能被傳遞到遠端接口中:
- Host: 因為proxy_pass默認會重寫host,所以無需處理(在常規的proxy_pass中,可能需要使用proxy_set_header命令修正Host頭)
- Content-Type: 如果來自客戶端的請求類型為POST,需要將該頭去掉
- Content-Length: 同上
- Accept-Encoding: 絕大多數時候,瀏覽器接受gzip, deflate格式的壓縮,如果遠端接口啟用了壓縮,此頭將導致返回的數據格式被壓縮過,從而lua無法正確處理。因此需要去掉此頭
- Cookie: 涉及到隱私問題,所以這個頭也需要去掉
通過ngx.req.clear_header可以將需要去掉的頭去掉。注意為了不影響后續處理,所有被去掉/改寫的頭應該被恢復,所有被添加的頭應該被去掉。
下面是一個簡單的示例代碼:
-- POST https://www.example.com:443/foo/bar?hello=world ngx.req.set_header("Content-Type", "application/json;charset=utf8"); ngx.req.set_header("Accept", "application/json"); local res = ngx.location.capture('/proxy/https/www.example.com/443/foo/bar', { method = ngx.HTTP_POST, body = body, args = {hello = 'world'} });