Nginx api gateway replacement - lyonwang/TechNotes GitHub Wiki

架構上使用 Nginx 的目的

Test Scenario and Dev Isolation

  • 將 QA 第三方 Endpoint 導入開發區,區隔第三方開發與測試

Logging & Tracking

  • 內外部網路存取 Access Log 集中
  • Trace Log (Trace/Span)

Application Gateway

  • 應用層網路存取控管 (TCP/UDP, HTTP...)
  • IP 黑白名單
  • Cross Origin Resource Sharing (CORS)

安裝版本選擇

  • 因應現有黑白名單控管,以及後續擴展需求,選擇具有 Lua script 功能的 Openresty 版本:

Openresty

Openresty on Docker

Nginx Lua Module

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

黑白名單 Lua script

Github Source

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 ...

⚠️ **GitHub.com Fallback** ⚠️