Nginx api gateway replacement - lyonwang/TechNotes GitHub Wiki
- 將 QA 第三方 Endpoint 導入開發區,區隔第三方開發與測試
- 內外部網路存取 Access Log 集中
- Trace Log (Trace/Span)
- 應用層網路存取控管 (TCP/UDP, HTTP...)
- IP 黑白名單
- Cross Origin Resource Sharing (CORS)
- 因應現有黑白名單控管,以及後續擴展需求,選擇具有 Lua script 功能的 Openresty 版本:
sudo yum install yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install openresty
sudo yum install openresty-resty
sudo yum install openresty-opm
- Nginx.service for "systemctl start/stop/restart nginx"
[Unit]
Description=Nginx Service For Openresty
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/openresty/nginx/sbin/nginx -p /usr/local/openresty/nginx/conf -c nginx.conf
ExecReload=/usr/local/openresty/nginx/sbin/nginx -p /usr/local/openresty/nginx/conf -c nginx.conf -s reload
ExecStop=/usr/local/openresty/nginx/sbin/nginx -p /usr/local/openresty/nginx/conf -c nginx.conf -s stop
ExecReopen=kill -USR1 $(cat /usr/local/openresty/nginx/logs/nginx.pid)
privateTmp=true
[Install]
WantedBy=multi-user.target
- Nginx config file location
/usr/local/openresty/nginx/conf/nginx.conf
- Operresty Package Manager: opm
sudo curl -k https://raw.githubusercontent.com/openresty/opm/master/bin/opm > /usr/local/openresty/bin/opm
sudo chmod +x /usr/local/openresty/bin/opm
- Install IPIP Module
sudo opm install pintsized/lua-resty-http
sudo opm get linsir/lua-resty-ipip # use root
Zeus IP 白名單: IPInfo_WhileList.csv
Zeus 地區名單(ipip資料庫): mydata4vipday.datx
Lua IP 白名單: ip_whitelist.csv
Lua IP 地區黑名單: region_blacklist.csv
Lua CORS allow 設定檔: cors_allow.list
Lua IP 黑名單: ip_blacklist.csv
//sudo cp 1,3,4,5 to /ipip
//sudo cp 2 to /usr/local/openresty/site/lualib/resty/ipip/data
http {
...
# 設定引用 ipip 函數庫路徑
lua_package_path "/usr/local/openresty/site/lualib/resty/ipip/?.lua;;";
# Lua 客製程式區塊
init_by_lua_block {
local ipip = require "resty.ipip.client"
cjson = require "cjson"
local opts = {
path = '/usr/local/openresty/site/lualib/resty/ipip/data/mydata4vipday.datx',
token = '97dc010a0793d97bad1a7d7eb98d5ab87ef0f8ee',
timeout = '2000',
}
ipipc = ipip:new(opts)
-- 檢查 IP 白名單: 有在白名單則回傳 true
function checkIpWhiteList(ipaddr, data_path)
local found = false
if not data_path or not ipaddr then
ngx.log(ngx.ERR, data_path)
--ngx.say(data_path)
return false
end
local ip1, ip2, ip3, ip4 = string.match(ipaddr, "(%d+).(%d+).(%d+).(%d+)")
local ip_uint32 = ip1 * 256 ^ 3 + ip2 * 256 ^ 2 + ip3 * 256 + ip4
--ngx.say(ip_uint32)
local file, err = io.open(data_path, "r")
if file == nil then
ngx.log(ngx.ERR, data_path)
--ngx.say(string.format("file is nill => %s", data_path))
return false
else
for line in file:lines() do
local f, t = line:match("(%d+),(%d+),(.*)")
if ip_uint32 >= tonumber(f) and ip_uint32 <= tonumber(t) then
ngx.log(ngx.NOTICE, ipaddr .. " is in IP White List.")
found = true
break
end
end
file:close()
end
return found
end
-- IP 黑名單(優先檢查,只要在就不允許進入): 有在黑名單則回傳 true
function checkIPBlackList(ipaddr)
local found = false
if not ipaddr then
ngx.log(ngx.ERR, ipaddr)
return false
end
local file, err = io.open("/ipip/ip_blacklist.csv", "r")
if file == nil then
ngx.log(ngx.ERR, "IP Balck list file:/ipip/ip_blacklist.csv does not exist or read fail.")
return false
else
local ip1, ip2, ip3, ip4 = string.match(ipaddr, "(%d+).(%d+).(%d+).(%d+)")
local ip_uint32 = ip1 * 256 ^ 3 + ip2 * 256 ^ 2 + ip3 * 256 + ip4
for line in file:lines() do
local f, t = line:match("(%d+),(%d+),(.*)")
if ip_uint32 >= tonumber(f) and ip_uint32 <= tonumber(t) then
ngx.log(ngx.NOTICE, ipaddr .. " is in IP Black List.")
found = true
break
end
end
file:close()
end
return found
end
-- IP 地區資料庫黑名單: 有在黑名單或為私有IP則回傳 true
function checkRegionBlackList(ipaddr)
local ipipc = ipipc
local cjson = cjson
-- 查詢 ipip 資料庫
local res, err = ipipc:query_file(ipaddr)
if res == nil then
ngx.log(ngx.ERR, "ipipc:query_file("..ipaddr..") fail")
return false, "ipipc:query_file("..ipaddr..") fail";
else
ngx.log(ngx.NOTICE, cjson.encode(res))
ngx.log(ngx.NOTICE, string.format("iso_2: %s", res[12]))
end
-- 檢查是否為私有 IP
local isPrivIP = is_ip_private(ipaddr)
if isPrivIP == true then
ngx.log(ngx.NOTICE, ipaddr .. " is in Private IP.")
return true, cRes
end
-- 取得地區黑名單
local blist = {}
local blist_file, err = io.open("/ipip/region_blacklist.csv", "r")
if blist_file == nil then
ngx.log(ngx.ERR, "/ipip/region_blacklist.csv")
else
local blist_str = blist_file:read("*all")
--ngx.log(ngx.NOTICE, blist_str)
blist = string.split(blist_str, ",")
--ngx.log(ngx.NOTICE, cjson.encode(blist))
blist_file:close()
end
-- 檢查黑名單
-- Blacklist: nation code(TW, HK, US, PH, SG)
for key, value in ipairs(blist) do
if res[12] == value then
ngx.log(ngx.NOTICE, ipaddr .. " is in Regin Black List.")
return true, res
end
end
return false, res
end
function is_ip_private(ipaddr)
local pri_addrs = {
{ 167772160, 184549375 }, -- 10.0.0.0 ~ 10.255.255.255 : single class A network
{ 2886729728, 2887778303 }, -- 172.16.0.0 ~ 172.31.255.255 : 16 contiguous class B network
{ 3232235520, 3232301055 }, -- 192.168.0.0 ~ 192.168.255.255 : 256 contiguous class C network
{ 2851995648, 2852061183 }, -- 169.254.0.0 ~ 169.254.255.255 : Link-local address also refered to as Automatic Private IP Addressing
{ 2130706432, 2147483647 } -- 127.0.0.0 ~ 127.255.255.255 : localhost
}
local o1,o2,o3,o4 = ipaddr:match("(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)" )
local num = 2^24*o1 + 2^16*o2 + 2^8*o3 + o4 -- ip to long
for k,v in pairs(pri_addrs) do
if num >= v[1] and num <=v[2] then
return true
end
end
return false
end
string.split = function(s, p)
local rt= {}
string.gsub(s, "[^"..p.."]+", function(w) table.insert(rt, w) end )
return rt
end
function getClientIP(remoteIP, realIP, xForwardedFor)
local retIP = remoteIP
if xForwardedFor then
local iplist = string.split(xForwardedFor, ", ")
if iplist[1] then
retIP = iplist[1]
end
end
if realIP then
retIP = realIP
end
return retIP
end
-- 跨來源資源共享 Cross-Origin Resource Sharing (CORS) 白名單
function processCORS()
varyHeaders = { "Origin",
"Access-Control-Request-Method",
"Access-Control-Request-Headers" }
allowMethods = { "GET",
"POST",
"PUT",
"DELETE",
"OPTIONS" }
allowHeaders = { "Accept",
"Authorization",
"Cache-Control",
"Content-Type",
"DNT",
"If-Modified-Since",
"Keep-Alive",
"Origin",
"User-Agent",
"X-Requested-With",
"X-Forwarded-For",
"X-REQUEST-ID",
"X-REQUEST-START-UTCTIME" }
-- Response headers for CORS: Vary 表示要求客戶端要傳送給 Server 的 CORS Request headers
ngx.header["Vary"] = table.concat(varyHeaders, ",")
if (ngx.var.http_origin) then
if (isInCORSList(ngx.var.http_origin)) then
ngx.header["Access-Control-Allow-Origin"] = ngx.var.http_origin
ngx.header["Access-Control-Allow-Credentials"] = "true"
ngx.header["Access-Control-Allow-Methods"] = table.concat(allowMethods, ",")
ngx.header["Access-Control-Allow-Headers"] = table.concat(allowHeaders, ",")
if(ngx.var.request_method == "OPTIONS") then
-- Tell client that this pre-flight info is valid for 20 days
ngx.header["Access-Control-Max-Age"] = 1728000
ngx.header["Content-Type"] = "text/plain charset=UTF-8"
ngx.header["Content-Length"] = 0;
ngx.exit(204);
end
end
end
end
function trim(s)
return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
function string.startwith(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
function isInCORSList(origin)
local cors_list = {}
local cors_list_file, err = io.open("/ipip/cors_allow.list", "r")
if cors_list_file == nil then
ngx.log(ngx.ERR, "/ipip/cors_allow.list")
else
for line in cors_list_file:lines() do
cors_list[table.getn(cors_list) + 1] = trim(line)
end
cors_list_file:close()
end
for key, value in ipairs(cors_list) do
if origin:startwith("http://"..value) then
return true
end
end
ngx.log(ngx.NOTICE, ipaddr .. " is not in CORS Allow List.")
return false
end
}
# Test site
server {
listen 80;
server_name 127.0.0.1 172.20.5.27 10.101.5.154 www.lyon.com www.lyon1.com;
charset utf-8;
#default_type text/plain;
#root /usr/local/openresty/nginx/html;
add_header X-REQUEST-ID $request_id; # Return to client
add_header X-REQUEST-START-UTCTIME $time_iso8601; # Return to client
location /tracelog {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
echo "X-REQUEST-ID: $request_id, X-REQUEST-START-UTCTIME: $time_iso8601"
proxy_set_header X-REQUEST-ID $request_id; # Pass to app server
proxy_set_header X-REQUEST-START-UTCTIME $time_iso8601; # Pass to app server
}
location /cors {
access_by_lua_block {
processCORS()
ngx.say('<html><body><a href="http://www.lyon1.com/cors1">To lyon1</a><img src="http://www.lyon.com/image/logo.png" /></body></html>')
}
}
location /test {
set $clientIP $remote_addr;
set $result "non";
access_by_lua_block {
ngx.var.clientIP = getClientIP(ngx.var.remote_addr, ngx.var.http_x_real_ip, ngx.var.http_x_forwarded_for)
res, err = ipipc:query_file(ngx.var.clientIP)
ngx.var.result = cjson.encode(res)
}
echo "clientIP: $clientIP, X-Forwarded-For: $http_x_forwarded_for, remote_addr: $remote_addr";
echo $result;
}
location /ipcheck {
set $a 1;
# rewrite_by_lua also worked
access_by_lua_block {
ngx.var.clientIP = getClientIP(ngx.var.remote_addr, ngx.var.http_x_real_ip, ngx.var.http_x_forwarded_for)
local is_pass = true
local is_ip_in_balck_list = checkIPBlackList(ngx.var.clientIP)
if is_ip_in_balck_list == true then
is_pass = false
else
is_black = checkRegionBlackList(ngx.var.clientIP)
if is_black == true then
is_white = checkIpWhiteList(ngx.var.clientIP, "/ipip/ip_whitelist.csv")
if not is_white then
is_pass = false
end
end
end
if is_pass then
ngx.var.a = "http://127.0.0.1/pass"
else
ngx.var.a = "http://127.0.0.1/access_denied"
end
}
#echo $a;
proxy_pass $a;
}
location /pass {
echo "Pass";
}
location /access_denied {
echo "Denied";
}
}
...
}
** NOTICE log 需要在 nginx.conf 開啟才能在 error.log 中看見
Configuration files
/etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf
nginx conf 組織結構與維護方式: Nginx_Gateway_Replacement.zip
nginx.conf 檔案內容 ... http { include services/*.conf; } ... Services Folder: 包含 service conf ([service_name].conf: 含有 http section 可填的設定值) server { map $request_uri $up_url { default ""; include uri_map/[service name].map } server { listen [port number]; include server_list/[service_name].slist
location / {
proxy_pass $up_url;
}
}
} server_list/[service name].slist Regular expression [domain name] ... uri_map/[service name].map Regular expression value ...