From 6915b7c7bccc00a6c6e86440bcaec00a10b65dad Mon Sep 17 00:00:00 2001 From: James Loh Date: Wed, 17 Oct 2018 21:12:44 +1100 Subject: [PATCH] Migrate to Maxmind GeoLite2 (#30) Rewrite GeoJS backend to use GeoLite2 DBs. Shift over to Openresty! --- .circleci/Geoip.conf | 2 +- .circleci/config.yml | 56 ++-- .circleci/images/primary/Dockerfile | 9 +- conf/v1/dns.conf | 86 +++++- conf/v1/hooks.conf | 28 +- conf/v1/ip.conf | 220 +++++++++------ lib/geojs/utils.lua | 418 +++++++++++++++++++++++++++- t/v1/01-utils.t | 90 +++++- t/v1/hooks/01-sanity.t | 3 - t/v1/hooks/02-hipchat.t | 9 +- t/v1/hooks/03-slack.t | 7 +- t/v1/hooks/04-twist.t | 7 +- t/v1/ip/01-sanity.t | 3 - t/v1/ip/02-ip.t | 3 - t/v1/ip/03-country.t | 128 ++++++++- t/v1/ip/04-dns.t | 5 +- t/v1/ip/05-geo.t | 81 +++++- 17 files changed, 957 insertions(+), 198 deletions(-) diff --git a/.circleci/Geoip.conf b/.circleci/Geoip.conf index e01561c..1388c4b 100644 --- a/.circleci/Geoip.conf +++ b/.circleci/Geoip.conf @@ -15,7 +15,7 @@ EditionIDs GeoLite2-City GeoLite2-ASN # The remaining settings are OPTIONAL. # The directory to store the database files. Defaults to /usr/local/share/GeoIP -DatabaseDirectory /root/project/download-cache/maxmind2 +#DatabaseDirectory /root/project/download-cache/maxmind2 # The server to use. Defaults to "updates.maxmind.com". # Host updates.maxmind.com diff --git a/.circleci/config.yml b/.circleci/config.yml index dcd6b53..e627032 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,52 +2,54 @@ version: 2 jobs: build: docker: - - image: jloh/geojs-tests:beta-0.0.4 + - image: jloh/geojs-tests:beta-0.0.12 steps: - checkout - - restore_cache: - keys: - - maxmind-geolite2-v1 - - run: - name: 'Download GeoLite1 Maxmind DBs' - command: | - mkdir -p download-cache/maxmind - test -s download-cache/maxmind/GeoLiteCityv6.dat || curl -s http://geolite.maxmind.com/download/geoip/database/GeoLiteCityv6-beta/GeoLiteCityv6.dat.gz | gzip -dc > download-cache/maxmind/GeoLiteCityv6.dat - test -s download-cache/maxmind/GeoIPv6.dat || curl -s http://geolite.maxmind.com/download/geoip/database/GeoIPv6.dat.gz | gzip -dc > download-cache/maxmind/GeoIPv6.dat - test -s download-cache/maxmind/GeoIPASNumv6.dat || curl -s http://download.maxmind.com/download/geoip/database/asnum/GeoIPASNumv6.dat.gz | gzip -dc > download-cache/maxmind/GeoIPASNumv6.dat + # We have to do it this way since wget'ing it lands us weird dir names - run: name: 'Download GoeLite2 Maxmind DBs' command: | mkdir -p download-cache/maxmind2 cp .circleci/Geoip.conf /etc/GeoIP.conf geoipupdate -v - - save_cache: - key: maxmind-geolite2-v1 - paths: - - "download-cache/maxmind2" - # Required repos - # TODO: Work out a way to cache these? - - run: - name: 'Checkout dependencies' + - run: + name: 'Install OPM dependencies' + command: | + opm install \ + pintsized/lua-resty-http=0.12 \ + openresty/lua-resty-dns=0.21 \ + openresty/lua-resty-upload=0.10 \ + bungle/lua-resty-reqargs=1.4 \ + xiaooloong/lua-resty-iconv=0.2.0 \ + anjia0532/lua-resty-maxminddb=0.06 + - run: + name: 'Add in Maxmind ASN' command: | - git clone -q --branch v0.10 https://github.com/pintsized/lua-resty-http.git repos/lua-resty-http - git clone -q --branch v0.18 https://github.com/openresty/lua-resty-dns.git repos/lua-resty-dns - git clone -q --branch v0.10 https://github.com/openresty/lua-resty-upload.git repos/lua-resty-upload - git clone -q --branch v1.4 https://github.com/bungle/lua-resty-reqargs.git repos/lua-resty-reqargs - git clone -q https://github.com/xiaooloong/lua-resty-iconv.git repos/lua-resty-iconv + mkdir -p lib/resty + curl -s -o lib/resty/maxminddb_asn.lua $MAXMIND_LUA_URL + - run: + name: 'Openresty version' + command: openresty -V - run: - name: 'Nginx version' - command: nginx -V + name: 'Link openresty for tests' + command: ln -s /usr/bin/openresty /usr/bin/nginx + - run: + name: 'Directory for test results' + command: mkdir -p test-results/prove - run: name: 'Tests' - command: prove -r t -a test_results.tgz + command: prove -r t -a test_results.tgz --formatter TAP::Formatter::JUnit > test-results/prove/nosetests.xml - run: name: 'Test Coverage' command: | if [[ "${TEST_COVERAGE}x" == '1x' ]]; then luacov; fi if [[ "${TEST_COVERAGE}x" == '1x' ]]; then luacov-coveralls; fi + - store_test_results: + path: test-results - store_artifacts: path: test_results.tgz + - store_artifacts: + path: test-results/ notify: webhooks: diff --git a/.circleci/images/primary/Dockerfile b/.circleci/images/primary/Dockerfile index 0eab872..94378d5 100644 --- a/.circleci/images/primary/Dockerfile +++ b/.circleci/images/primary/Dockerfile @@ -1,7 +1,8 @@ -FROM ubuntu:xenial +FROM openresty/openresty:1.13.6.2-xenial RUN apt-get update && apt-get install -y software-properties-common RUN apt-add-repository ppa:maxmind/ppa -RUN apt-get update && apt-get install -y lua-cjson nginx-extras cpanminus libluajit-5.1-dev libgd-dev git luarocks geoipupdate -RUN cpanm -v --notest Test::Nginx TAP::Harness::Archive -RUN luarocks install luacov-coveralls +RUN apt-get update && apt-get install -y cpanminus libgd-dev git luarocks geoipupdate libmaxminddb0 libmaxminddb-dev +RUN cpanm -v --notest Test::Nginx TAP::Harness::Archive TAP::Formatter::JUnit +# luacov is broken currently :() +#RUN luarocks install luacov-coveralls --tree=/usr/local/openresty/luajit diff --git a/conf/v1/dns.conf b/conf/v1/dns.conf index db8d884..63cf214 100644 --- a/conf/v1/dns.conf +++ b/conf/v1/dns.conf @@ -16,13 +16,34 @@ location = /v1/dns/ptr { ngx.say(ptr) } } + +location ~ "/v1/dns/ptr/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))$" { + default_type text/plain; + content_by_lua_block { + -- Function to get our PTR! + local getptr = require("geojs.utils").get_ptr + local ngx_vars = ngx.var + + -- Vars + local record + + -- Get the IP we want + record = ngx_vars.ip + + local ptr = getptr(record) + ngx.say(ptr) + + } +} + location = /v1/dns/ptr.json { default_type application/json; content_by_lua_block { -- Function to get our PTR! - local getptr = require("geojs.utils").get_ptr - local cjson = require("cjson") - local json_encode = cjson.encode + local getptr = require("geojs.utils").get_ptr + local cjson = require("cjson") + local cjson = require("cjson") + local json_encode = cjson.encode -- Vars! local record @@ -41,6 +62,32 @@ location = /v1/dns/ptr.json { ) } } + +location ~ "/v1/dns/ptr/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))\.json$" { + default_type application/json; + content_by_lua_block { + -- Function to get our PTR! + local getptr = require("geojs.utils").get_ptr + local ngx_vars = ngx.var + local cjson = require("cjson") + local json_encode = cjson.encode + + -- Vars! + local record + + -- Get the IP we want + record = ngx_vars.ip + + local ptr = getptr(record) + + ngx.say( + json_encode({ + ["ptr"] = ptr + }) + ) + } +} + location = /v1/dns/ptr.js { default_type application/javascript; content_by_lua_block { @@ -75,3 +122,36 @@ location = /v1/dns/ptr.js { ) } } + +location ~ "/v1/dns/ptr/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))\.js$" { + default_type application/javascript; + content_by_lua_block { + -- Function to get our PTR! + local getptr = require("geojs.utils").get_ptr + local get_callback = require("geojs.utils").generate_callback + local ngx_vars = ngx.var + local args = ngx.req.get_uri_args() + local cjson = require("cjson") + local json_encode = cjson.encode + + -- Vars! + local record + local callback + + -- Get the IP we want + record = ngx_vars.ip + + -- Override our callback if it exists + callback = get_callback('ptr', args) + + local ptr = getptr(record) + ngx.say( + callback, + '(', + cjson.encode({ + ["ptr"] = ptr + }), + ')' + ) + } +} diff --git a/conf/v1/hooks.conf b/conf/v1/hooks.conf index 416cb6d..4a8f2a6 100644 --- a/conf/v1/hooks.conf +++ b/conf/v1/hooks.conf @@ -46,14 +46,13 @@ location = /v1/hooks/hipchat { local get, post, files = require "resty.reqargs"() local split = require("geojs.utils").split - local upstreamreq = require("geojs.utils").upstream_req + local geo_lookup = require("geojs.utils").geo_lookup local getptr = require("geojs.utils").get_ptr local trim = require("geojs.utils").trim local validate_ip = require("geojs.utils").validate_ip local cjson = require("cjson") local json_encode = cjson.encode local json_decode = cjson.decode - local reqpath = '/v1/ip/geo.json' -- Get the message from HipChat local message = post["item"]["message"]["message"] @@ -77,9 +76,8 @@ location = /v1/hooks/hipchat { end -- Get our data with the requested IP - local req_resp = upstreamreq(reqpath, req_ip) + local req_data = geo_lookup(req_ip) local req_ptr = getptr(req_ip) - local req_data = json_decode(req_resp) -- Just in case we can't find out country if req_data['country'] == nil then @@ -87,7 +85,7 @@ location = /v1/hooks/hipchat { end -- Define some card vars - local card_title = string.format("%s is a %s IP belonging to %s", req_ip, req_data['country'], req_data['organization']) + local card_title = string.format("%s is a %s IP belonging to %s", req_ip, req_data['country'], req_data['organization_name']) local txt_message = string.format("Results for %s

", req_ip) local card_simp_title = string.format("GeoIP results for %s", req_ip) local card_body = "" @@ -113,8 +111,8 @@ location = /v1/hooks/hipchat { card_body = card_body .. '\nCity: ' .. req_data['city'] txt_message = txt_message .. '
City: ' .. req_data['city'] end - if req_data['organization'] then - txt_message = txt_message .. '
Organization: ' .. req_data['organization'] + if req_data['organization_name'] then + txt_message = txt_message .. '
Organization: ' .. req_data['organization_name'] end txt_message = txt_message .. '

Powered by GeoJS' @@ -156,7 +154,7 @@ location = /v1/hooks/slack { -- Setup local vars local cjson = require "cjson" local split = require("geojs.utils").split - local upstream_req = require("geojs.utils").upstream_req + local geo_lookup = require("geojs.utils").geo_lookup local getptr = require("geojs.utils").get_ptr local trim = require("geojs.utils").trim local validate_ip = require("geojs.utils").validate_ip @@ -229,10 +227,8 @@ location = /v1/hooks/slack { local response_url = post['response_url'] -- Get Geo data about this IP - local req_resp = upstream_req(reqpath, req_ip) + local req_data = geo_lookup(req_ip) local req_ptr = getptr(req_ip) - -- Turn our response into data - local req_data = json_decode(req_resp) local resp = '' -- Build out response @@ -249,7 +245,7 @@ location = /v1/hooks/slack { resp = resp .. '\nCity: ' .. req_data['city'] end if req_data['organization'] then - resp = resp .. '\nOrganization: ' .. req_data['organization'] + resp = resp .. '\nOrganization: ' .. req_data['organization_name'] end local title = "IP Information for " .. req_ip message = { @@ -360,7 +356,7 @@ location = /v1/hooks/twistapp { local ngx_var = ngx.var local cjson = require "cjson" local split = require("geojs.utils").split - local upstream_req = require("geojs.utils").upstream_req + local geo_lookup = require("geojs.utils").geo_lookup local getptr = require("geojs.utils").get_ptr local trim = require("geojs.utils").trim local validate_ip = require("geojs.utils").validate_ip @@ -403,10 +399,8 @@ location = /v1/hooks/twistapp { end -- Get Geo data about this IP - local req_resp = upstream_req(reqpath, req_ip) + local req_data = geo_lookup(req_ip) local req_ptr = getptr(req_ip) - -- Turn our response into data - local req_data = cjson.decode(req_resp) -- Build out response local resp = '### IP information for **' .. req_ip .. '**' @@ -423,7 +417,7 @@ location = /v1/hooks/twistapp { resp = resp .. '\nCity: ' .. req_data['city'] end if req_data['organization'] then - resp = resp .. '\nOrganization: ' .. req_data['organization'] + resp = resp .. '\nOrganization: ' .. req_data['organization_name'] end resp = resp .. '\nPowered by [GeoJS](https://geojs.io)' message = { diff --git a/conf/v1/ip.conf b/conf/v1/ip.conf index 3234279..443f7ce 100644 --- a/conf/v1/ip.conf +++ b/conf/v1/ip.conf @@ -30,10 +30,10 @@ location = /v1/ip.js { location = /v1/ip/country { default_type text/plain; content_by_lua_block { - local args = ngx.req.get_uri_args() - local split = require("geojs.utils").split - local upstreamreq = require("geojs.utils").upstream_req - local reqpath = '/v1/ip/country' + local args = ngx.req.get_uri_args() + local split = require("geojs.utils").split + local lookup = require("geojs.utils").country_lookup + if args.ip then local ips = {} local msg = "" @@ -42,24 +42,36 @@ location = /v1/ip/country { ips = split(args.ip, ',') for k,v in ipairs(ips) do -- Get our info from upstream - info = upstreamreq(reqpath, v) + info = lookup(v) -- Add our new info to our msg - msg = msg .. v .. ': ' .. info + msg = msg .. v .. ': ' .. info["country"] .. '\n' end - ngx.say(msg) + ngx.print(msg) else - ngx.say(ngx.var.geoip_country_code) + local info = lookup(ngx.var.remote_addr) + ngx.say(info["country"]) end } } +location ~ "/v1/ip/country/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))$" { + default_type text/plain; + content_by_lua_block { + local ngx_var = ngx.var + local lookup = require("geojs.utils").country_lookup + + local info = lookup(ngx_var.ip) + ngx.say(info["country"]) + } +} + location = /v1/ip/country/full { default_type text/plain; content_by_lua_block { - local args = ngx.req.get_uri_args() - local split = require("geojs.utils").split - local upstreamreq = require("geojs.utils").upstream_req - local reqpath = '/v1/ip/country/full' + local args = ngx.req.get_uri_args() + local split = require("geojs.utils").split + local lookup = require("geojs.utils").country_lookup + if args.ip then local ips = {} local msg = "" @@ -68,27 +80,39 @@ location = /v1/ip/country/full { ips = split(args.ip, ',') for k,v in ipairs(ips) do -- Get our info from upstream - info = upstreamreq(reqpath, v) + info = lookup(v) -- Add our new info to our msg - msg = msg .. v .. ': ' .. info + msg = msg .. v .. ': ' .. info["name"] .. '\n' end - ngx.say(msg) + ngx.print(msg) else - ngx.say(ngx.var.geoip_country_name) + local info = lookup(ngx.var.remote_addr) + ngx.say(info["name"]) end } } +location ~ "/v1/ip/country/full/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))$" { + default_type text/plain; + content_by_lua_block { + local ngx_var = ngx.var + local lookup = require("geojs.utils").country_lookup + + local info = lookup(ngx_var.ip) + ngx.say(info["name"]) + } +} + location = /v1/ip/country.json { default_type application/json; content_by_lua_block { local args = ngx.req.get_uri_args() local ngx_var = ngx.var local split = require("geojs.utils").split - local upstreamreq = require("geojs.utils").upstream_req + local lookup = require("geojs.utils").country_lookup local cjson = require("cjson") local json_encode = cjson.encode - local reqpath = '/v1/ip/country.json' + if args.ip then local ips = {} local msg = {} @@ -98,37 +122,43 @@ location = /v1/ip/country.json { -- Get our info for each IP for k,v in ipairs(ips) do -- Get our info form upstream - info = upstreamreq(reqpath, v) + info = lookup(v) -- Add our new info to our msg table - table.insert(msg, cjson.decode(info)) + table.insert(msg, info) end ngx.say(json_encode(msg)) else -- Set our req IP - req_ip = ngx.var.remote_addr - ngx.say(json_encode({ - ip = req_ip, - request_ip = ngx.req.get_headers()["X-Orig-IP"], - country_3 = ngx_var.geoip_country_code3, - country = ngx_var.geoip_country_code, - name = ngx_var.geoip_country_name, - })) + local info = lookup(ngx_var.remote_addr) + ngx.say(json_encode(info)) end } } +location ~ "/v1/ip/country/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))\.json$" { + default_type application/json; + content_by_lua_block { + local ngx_var = ngx.var + local lookup = require("geojs.utils").country_lookup + local cjson = require("cjson") + local json_encode = cjson.encode + + -- Set our req IP + local info = lookup(ngx_var.ip) + ngx.say(json_encode(info)) + } +} + location = /v1/ip/country.js { default_type application/javascript; content_by_lua_block { local args = ngx.req.get_uri_args() local ngx_var = ngx.var local split = require("geojs.utils").split - local upstreamreq = require("geojs.utils").upstream_req local get_callback = require("geojs.utils").generate_callback + local lookup = require("geojs.utils").country_lookup local cjson = require("cjson") local json_encode = cjson.encode - local json_decode = cjson.decode - local reqpath = '/v1/ip/country.json' -- Get our custom callback is it exists local callback = get_callback('countryip', args) @@ -143,9 +173,9 @@ location = /v1/ip/country.js { -- Get our info for each IP for k,v in ipairs(ips) do -- Get our info from upstream - info = upstreamreq(reqpath, v) + info = lookup(v) -- Add our new info to our msg table - table.insert(msg, json_decode(info)) + table.insert(msg, info) end ngx.say( callback, @@ -156,34 +186,50 @@ location = /v1/ip/country.js { else -- Since we don't have an IP arg we're assuming we've just got a single IP local req_ip = ngx.var.remote_addr + local info = lookup(req_ip) ngx.say( callback, '(', - json_encode({ - ip = req_ip, - request_ip = ngx.req.get_headers()["X-Orig-IP"], - country_3 = ngx_var.geoip_country_code3, - country = ngx_var.geoip_country_code, - name = ngx_var.geoip_country_name, - }), + json_encode(info), ')' ) end } } +location ~ "/v1/ip/country/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))\.js$" { + default_type application/javascript; + content_by_lua_block { + local args = ngx.req.get_uri_args() + local ngx_var = ngx.var + local get_callback = require("geojs.utils").generate_callback + local lookup = require("geojs.utils").country_lookup + local cjson = require("cjson") + local json_encode = cjson.encode + + -- Get our custom callback is it exists + local callback = get_callback('countryip', args) + + local info = lookup(ngx_var.ip) + ngx.say( + callback, + '(', + json_encode(info), + ')' + ) + } +} + location = /v1/ip/geo.json { default_type application/json; content_by_lua_block { local args = ngx.req.get_uri_args() local ngx_var = ngx.var local split = require("geojs.utils").split - local upstreamreq = require("geojs.utils").upstream_req - local to_utf8 = require("geojs.utils").to_utf8 + local lookup = require("geojs.utils").geo_lookup local cjson = require("cjson") local json_encode = cjson.encode - local json_decode = cjson.decode - local reqpath = '/v1/ip/geo.json' + if args.ip then local ips = {} local msg = {} @@ -193,47 +239,44 @@ location = /v1/ip/geo.json { -- Get our info for each IP for k,v in ipairs(ips) do -- Get our info form upstream - info = upstreamreq(reqpath, v) + info = lookup(v) -- Add our new info to our msg table - table.insert(msg, json_decode(info)) + table.insert(msg, info) end ngx.say(json_encode(msg)) else -- Set our req IP local req_ip = ngx.var.remote_addr - ngx.say(json_encode({ - ip = req_ip, - request_ip = ngx.req.get_headers()["X-Orig-IP"], - country_code = ngx_var.geoip_country_code, - country_code3 = ngx_var.geoip_country_code3, - country = ngx_var.geoip_country_name, - continent_code = ngx_var.geoip_city_continent_code, - region = to_utf8(ngx_var.geoip_region_name), - city = to_utf8(ngx_var.geoip_city), - latitude = ngx_var.geoip_latitude, - longitude = ngx_var.geoip_longitude, - area_code = ngx_var.geoip_area_code, - timezone = ngx_var.geoip_timezone, - organization = to_utf8(ngx_var.geoip_org), - offset = ngx_var.geoip_offset - })) + local info = lookup(req_ip) + ngx.say(json_encode(info)) end } } +location ~ "/v1/ip/geo/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))\.json$" { + default_type application/json; + content_by_lua_block { + local ngx_var = ngx.var + local lookup = require("geojs.utils").geo_lookup + local cjson = require("cjson") + local json_encode = cjson.encode + + -- Set our req IP + local info = lookup(ngx_var.ip) + ngx.say(json_encode(info)) + } +} + location = /v1/ip/geo.js { default_type application/javascript; content_by_lua_block { local args = ngx.req.get_uri_args() local ngx_var = ngx.var local split = require("geojs.utils").split - local upstreamreq = require("geojs.utils").upstream_req + local lookup = require("geojs.utils").geo_lookup local get_callback = require("geojs.utils").generate_callback - local to_utf8 = require("geojs.utils").to_utf8 local cjson = require("cjson") local json_encode = cjson.encode - local json_decode = cjson.decode - local reqpath = '/v1/ip/geo.json' -- Get our custom callback if we've got one local callback = get_callback('geoip', args) @@ -247,9 +290,9 @@ location = /v1/ip/geo.js { ips = split(args.ip, ",") -- Get our info for each IP for k,v in ipairs(ips) do - info = upstreamreq(reqpath, v) + info = lookup(v) -- Add our new info to our msg table - table.insert(msg, json_decode(info)) + table.insert(msg, info) end -- Send our response back! ngx.say( @@ -261,27 +304,36 @@ location = /v1/ip/geo.js { else -- Since we don't have an IP arg we're assuming we've just got a single IP local req_ip = ngx.var.remote_addr + local info = lookup(req_ip) ngx.say( callback, '(', - json_encode({ - ip = req_ip, - request_ip = ngx.req.get_headers()["X-Orig-IP"], - country_code = ngx_var.geoip_country_code, - country_code3 = ngx_var.geoip_country_code3, - country = ngx_var.geoip_country_name, - continent_code = ngx_var.geoip_city_continent_code, - region = to_utf8(ngx_var.geoip_region_name), - city = to_utf8(ngx_var.geoip_city), - latitude = ngx_var.geoip_latitude, - longitude = ngx_var.geoip_longitude, - area_code = ngx_var.geoip_area_code, - timezone = ngx_var.geoip_timezone, - organization = to_utf8(ngx_var.geoip_org), - offset = ngx_var.geoip_offset - }), + json_encode(info), ')' ) end } } + +location ~ "/v1/ip/geo/(?((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])))\.js$" { + default_type application/javascript; + content_by_lua_block { + local args = ngx.req.get_uri_args() + local ngx_var = ngx.var + local lookup = require("geojs.utils").geo_lookup + local get_callback = require("geojs.utils").generate_callback + local cjson = require("cjson") + local json_encode = cjson.encode + + -- Get our custom callback if we've got one + local callback = get_callback('geoip', args) + + local info = lookup(ngx_var.ip) + ngx.say( + callback, + '(', + json_encode(info), + ')' + ) + } +} diff --git a/lib/geojs/utils.lua b/lib/geojs/utils.lua index 4a80c09..83284ac 100644 --- a/lib/geojs/utils.lua +++ b/lib/geojs/utils.lua @@ -2,6 +2,8 @@ local ngx_log = ngx.log local ngx_var = ngx.var local escape_uri = ngx.escape_uri local ngx_re = ngx.re +local geo = require('resty.maxminddb') +local geo_asn = require('resty.maxminddb_asn') local log_level = { STDERR = ngx.STDERR, @@ -16,7 +18,287 @@ local log_level = { } local _M = { - _VERSION = "0.0.1" + _VERSION = "0.0.2" +} + +local country_code_3 = { + ["AD"] = "AND", + ["AE"] = "ARE", + ["AF"] = "AFG", + ["AG"] = "ATG", + ["AI"] = "AIA", + ["AL"] = "ALB", + ["AM"] = "ARM", + ["AO"] = "AGO", + ["AQ"] = "ATA", + ["AR"] = "ARG", + ["AS"] = "ASM", + ["AT"] = "AUT", + ["AU"] = "AUS", + ["AW"] = "ABW", + ["AX"] = "ALA", + ["AZ"] = "AZE", + ["BA"] = "BIH", + ["BB"] = "BRB", + ["BD"] = "BGD", + ["BE"] = "BEL", + ["BF"] = "BFA", + ["BG"] = "BGR", + ["BH"] = "BHR", + ["BI"] = "BDI", + ["BJ"] = "BEN", + ["BL"] = "BLM", + ["BM"] = "BMU", + ["BN"] = "BRN", + ["BO"] = "BOL", + ["BQ"] = "BES", + ["BR"] = "BRA", + ["BS"] = "BHS", + ["BT"] = "BTN", + ["BV"] = "BVT", + ["BW"] = "BWA", + ["BY"] = "BLR", + ["BZ"] = "BLZ", + ["CA"] = "CAN", + ["CC"] = "CCK", + ["CD"] = "COD", + ["CF"] = "CAF", + ["CG"] = "COG", + ["CH"] = "CHE", + ["CI"] = "CIV", + ["CK"] = "COK", + ["CL"] = "CHL", + ["CM"] = "CMR", + ["CN"] = "CHN", + ["CO"] = "COL", + ["CR"] = "CRI", + ["CU"] = "CUB", + ["CV"] = "CPV", + ["CW"] = "CUW", + ["CX"] = "CXR", + ["CY"] = "CYP", + ["CZ"] = "CZE", + ["DE"] = "DEU", + ["DJ"] = "DJI", + ["DK"] = "DNK", + ["DM"] = "DMA", + ["DO"] = "DOM", + ["DZ"] = "DZA", + ["EC"] = "ECU", + ["EE"] = "EST", + ["EG"] = "EGY", + ["EH"] = "ESH", + ["ER"] = "ERI", + ["ES"] = "ESP", + ["ET"] = "ETH", + ["FI"] = "FIN", + ["FJ"] = "FJI", + ["FK"] = "FLK", + ["FM"] = "FSM", + ["FO"] = "FRO", + ["FR"] = "FRA", + ["GA"] = "GAB", + ["GB"] = "GBR", + ["GD"] = "GRD", + ["GE"] = "GEO", + ["GF"] = "GUF", + ["GG"] = "GGY", + ["GH"] = "GHA", + ["GI"] = "GIB", + ["GL"] = "GRL", + ["GM"] = "GMB", + ["GN"] = "GIN", + ["GP"] = "GLP", + ["GQ"] = "GNQ", + ["GR"] = "GRC", + ["GS"] = "SGS", + ["GT"] = "GTM", + ["GU"] = "GUM", + ["GW"] = "GNB", + ["GY"] = "GUY", + ["HK"] = "HKG", + ["HM"] = "HMD", + ["HN"] = "HND", + ["HR"] = "HRV", + ["HT"] = "HTI", + ["HU"] = "HUN", + ["ID"] = "IDN", + ["IE"] = "IRL", + ["IL"] = "ISR", + ["IM"] = "IMN", + ["IN"] = "IND", + ["IO"] = "IOT", + ["IQ"] = "IRQ", + ["IR"] = "IRN", + ["IS"] = "ISL", + ["IT"] = "ITA", + ["JE"] = "JEY", + ["JM"] = "JAM", + ["JO"] = "JOR", + ["JP"] = "JPN", + ["KE"] = "KEN", + ["KG"] = "KGZ", + ["KH"] = "KHM", + ["KI"] = "KIR", + ["KM"] = "COM", + ["KN"] = "KNA", + ["KP"] = "PRK", + ["KR"] = "KOR", + ["XK"] = "XKX", + ["KW"] = "KWT", + ["KY"] = "CYM", + ["KZ"] = "KAZ", + ["LA"] = "LAO", + ["LB"] = "LBN", + ["LC"] = "LCA", + ["LI"] = "LIE", + ["LK"] = "LKA", + ["LR"] = "LBR", + ["LS"] = "LSO", + ["LT"] = "LTU", + ["LU"] = "LUX", + ["LV"] = "LVA", + ["LY"] = "LBY", + ["MA"] = "MAR", + ["MC"] = "MCO", + ["MD"] = "MDA", + ["ME"] = "MNE", + ["MF"] = "MAF", + ["MG"] = "MDG", + ["MH"] = "MHL", + ["MK"] = "MKD", + ["ML"] = "MLI", + ["MM"] = "MMR", + ["MN"] = "MNG", + ["MO"] = "MAC", + ["MP"] = "MNP", + ["MQ"] = "MTQ", + ["MR"] = "MRT", + ["MS"] = "MSR", + ["MT"] = "MLT", + ["MU"] = "MUS", + ["MV"] = "MDV", + ["MW"] = "MWI", + ["MX"] = "MEX", + ["MY"] = "MYS", + ["MZ"] = "MOZ", + ["NA"] = "NAM", + ["NC"] = "NCL", + ["NE"] = "NER", + ["NF"] = "NFK", + ["NG"] = "NGA", + ["NI"] = "NIC", + ["NL"] = "NLD", + ["NO"] = "NOR", + ["NP"] = "NPL", + ["NR"] = "NRU", + ["NU"] = "NIU", + ["NZ"] = "NZL", + ["OM"] = "OMN", + ["PA"] = "PAN", + ["PE"] = "PER", + ["PF"] = "PYF", + ["PG"] = "PNG", + ["PH"] = "PHL", + ["PK"] = "PAK", + ["PL"] = "POL", + ["PM"] = "SPM", + ["PN"] = "PCN", + ["PR"] = "PRI", + ["PS"] = "PSE", + ["PT"] = "PRT", + ["PW"] = "PLW", + ["PY"] = "PRY", + ["QA"] = "QAT", + ["RE"] = "REU", + ["RO"] = "ROU", + ["RS"] = "SRB", + ["RU"] = "RUS", + ["RW"] = "RWA", + ["SA"] = "SAU", + ["SB"] = "SLB", + ["SC"] = "SYC", + ["SD"] = "SDN", + ["SS"] = "SSD", + ["SE"] = "SWE", + ["SG"] = "SGP", + ["SH"] = "SHN", + ["SI"] = "SVN", + ["SJ"] = "SJM", + ["SK"] = "SVK", + ["SL"] = "SLE", + ["SM"] = "SMR", + ["SN"] = "SEN", + ["SO"] = "SOM", + ["SR"] = "SUR", + ["ST"] = "STP", + ["SV"] = "SLV", + ["SX"] = "SXM", + ["SY"] = "SYR", + ["SZ"] = "SWZ", + ["TC"] = "TCA", + ["TD"] = "TCD", + ["TF"] = "ATF", + ["TG"] = "TGO", + ["TH"] = "THA", + ["TJ"] = "TJK", + ["TK"] = "TKL", + ["TL"] = "TLS", + ["TM"] = "TKM", + ["TN"] = "TUN", + ["TO"] = "TON", + ["TR"] = "TUR", + ["TT"] = "TTO", + ["TV"] = "TUV", + ["TW"] = "TWN", + ["TZ"] = "TZA", + ["UA"] = "UKR", + ["UG"] = "UGA", + ["UM"] = "UMI", + ["US"] = "USA", + ["UY"] = "URY", + ["UZ"] = "UZB", + ["VA"] = "VAT", + ["VC"] = "VCT", + ["VE"] = "VEN", + ["VG"] = "VGB", + ["VI"] = "VIR", + ["VN"] = "VNM", + ["VU"] = "VUT", + ["WF"] = "WLF", + ["WS"] = "WSM", + ["YE"] = "YEM", + ["YT"] = "MYT", + ["ZA"] = "ZAF", + ["ZM"] = "ZMB", + ["ZW"] = "ZWE", + ["CS"] = "SCG", + ["AN"] = "ANT" +} + +local default_geo_lookup = { + ["city"] = { + ["names"] = {} + }, + ["continent"] = { + ["names"] = {} + }, + ["country"] = { + ["names"] = {} + }, + ["location"] = {}, + ["postal"] = {}, + ["registered_country"] = { + ["names"] = {} + }, + ["subdivisions"] = {{ + ["names"] = {} + }} +} + +local default_asn_lookup = { + ["autonomous_system_number"] = 64512, -- Start of the private ASN block + ["autonomous_system_organization"] = "Unknown" } local config = { @@ -26,6 +308,61 @@ local config = { }, } +-- THe below two functions are taken from the ledge codebase under the 2-clause BSD license. +-- This code was written by James Hurst james@pintsized.co.uk (https://github.com/pintsized/ledge) +-- +-- Returns a new table, recursively copied from the one given, retaining +-- metatable assignment. +-- +-- @param table table to be copied +-- @return table +local function tbl_copy(orig) + local orig_type = type(orig) + local copy + if orig_type == "table" then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[tbl_copy(orig_key)] = tbl_copy(orig_value) + end + setmetatable(copy, tbl_copy(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy +end + + +-- Returns a new table, recursively copied from the combination of the given +-- table `t1`, with any missing fields copied from `defaults`. +-- +-- If `defaults` is of type "fixed field" and `t1` contains a field name not +-- present in the defults, an error will be thrown. +-- +-- @param table t1 +-- @param table defaults +-- @return table a new table, recursively copied and merged +local function tbl_copy_merge_defaults(t1, defaults) + if t1 == nil then t1 = {} end + if defaults == nil then defaults = {} end + if type(t1) == "table" and type(defaults) == "table" then + local copy = {} + for t1_key, t1_value in next, t1, nil do + copy[tbl_copy(t1_key)] = tbl_copy_merge_defaults( + t1_value, tbl_copy(defaults[t1_key]) + ) + end + for defaults_key, defaults_value in next, defaults, nil do + if t1[defaults_key] == nil then + copy[tbl_copy(defaults_key)] = tbl_copy(defaults_value) + end + end + return copy + else + return t1 -- not a table + end +end + + -- Splits strings! function _M.split(str, pat) local t = {} -- NOTE: use {n = 0} in Lua-5.0 @@ -83,14 +420,14 @@ end -- Makes requests to our upstream server function _M.upstream_req(reqpath, ip) - local http = require "resty.http" + local http = require "resty.http" local httpc = http.new() local uri = config.http.upstream .. reqpath local res, err = httpc:request_uri(uri, { method = "GET", headers = { ["X-Real-IP"] = ip, - ["Host"] = "get.geojs.io", + ["Host"] = "get.geojs.io", } }) @@ -137,4 +474,79 @@ function _M.generate_callback(default, req_args) return callback end +-- Maxmind DB implemntation + +function init_dbs() + -- Init our DBs if they haven't been + if not geo.initted() then + geo.init("/usr/share/GeoIP/GeoLite2-City.mmdb") + end + + if not geo_asn.initted() then + geo_asn.init("/usr/share/GeoIP/GeoLite2-ASN.mmdb") + end +end + +local function geoip_lookup(ip) + -- Ensure DBs are init'd + init_dbs() + + -- Lookup Geo data + local ip_geo, ip_geo_err = geo.lookup(ip) + local ip_asn, ip_asn_err = geo_asn.lookup(ip) + + -- Copy in our default/fallback values + ip_geo = tbl_copy_merge_defaults(ip_geo, default_geo_lookup) + ip_asn = tbl_copy_merge_defaults(ip_asn, default_asn_lookup) + + local ip_data = {} + + -- Merge our two tables + for k,v in pairs(ip_geo) do ip_data[k] = v end + for k,v in pairs(ip_asn) do ip_data[k] = v end + + -- Add 3 letter country code + if ip_data['country']['iso_code'] then ip_data['country']["iso_code3"] = country_code_3[ip_geo['country']['iso_code']] end + + -- Copy in our defaults so if info is missing we fail gracefully + return ip_data +end +_M.geoip_lookup = geoip_lookup + +function _M.country_lookup(ip) + -- Lookup IP + local lookup = geoip_lookup(ip) + local res = { + ["country"] = lookup["country"]["iso_code"], + ["country_3"] = lookup["country"]["iso_code3"], + ["ip"] = ip, + ["name"] = lookup["country"]["names"]["en"] + } + return res +end + +function _M.geo_lookup(ip) + -- Lookup IP + local lookup = geoip_lookup(ip) + local res = { + ["ip"] = ip, + ["area_code"] = '0', -- depreciated but we should return a value + ["country"] = lookup["country"]["names"]["en"], + ["country_code"] = lookup["country"]["iso_code"], + ["country_code3"] = lookup["country"]["iso_code3"], + ["continent_code"] = lookup["continent"]["code"], + ["city"] = lookup["city"]["names"]["en"], + ["region"] = lookup["subdivisions"][1]["names"]["en"], + ["latitude"] = tostring(lookup["location"]["latitude"]), -- Sadly these two were an int at the start so can't be until v2 + ["longitude"] = tostring(lookup["location"]["longitude"]), + ["accuracy"] = lookup["location"]["accuracy_radius"], + ["timezone"] = lookup["location"]["time_zone"], + ["organization"] = 'AS' .. table.concat({lookup["autonomous_system_number"], lookup["autonomous_system_organization"]}, ' '), + ["asn"] = lookup["autonomous_system_number"], + ["organization_name"] = lookup["autonomous_system_organization"] + } + + return res +end + return _M diff --git a/t/v1/01-utils.t b/t/v1/01-utils.t index b8ae7cf..ad26435 100644 --- a/t/v1/01-utils.t +++ b/t/v1/01-utils.t @@ -11,10 +11,7 @@ our $HttpConfig = qq{ require("luacov.runner").init() end } - geoip_country "$pwd/download-cache/maxmind/GeoIPv6.dat"; - geoip_city "$pwd/download-cache/maxmind/GeoLiteCityv6.dat"; - geoip_org "$pwd/download-cache/maxmind/GeoIPASNumv6.dat"; - lua_package_path "$pwd/lib/?.lua;$pwd/repos/lua-resty-dns/lib/?.lua;$pwd/repos/lua-resty-http/lib/?.lua;$pwd/repos/lua-resty-iconv/lualib/?.lua;;"; + lua_package_path "$pwd/lib/?.lua;;"; real_ip_header X-IP; set_real_ip_from 127.0.0.1/32; }; @@ -95,7 +92,7 @@ test string "$::HttpConfig" --- config location /t { - set $geojs_dns_server '8.8.8.8'; + set $geojs_dns_server '1.1.1.1'; content_by_lua_block { local getptr = require("geojs.utils").get_ptr local ptr = getptr('8.8.8.8') @@ -133,7 +130,7 @@ Failed to query DNS servers "$::HttpConfig" --- config location /t { - set $geojs_dns_server '8.8.8.8'; + set $geojs_dns_server '1.1.1.1'; content_by_lua_block { local getptr = require("geojs.utils").get_ptr local ptr = getptr('192.168.0.1') @@ -182,6 +179,7 @@ GET /t --- response_body chomp nil + === TEST 9: Iconv encoding --- http_config eval "$::HttpConfig" @@ -202,6 +200,7 @@ GET /t --- response_body chomp Ã0 + === TEST 10: Validate IPv4/IPv6 IPs --- http_config eval "$::HttpConfig" @@ -227,6 +226,7 @@ GET /t --- response_body eval ["OK","OK"] + === TEST 11: Fail on bad IPs --- http_config eval "$::HttpConfig" @@ -251,3 +251,81 @@ GET /t [error] --- response_body eval ["OK","OK"] + + +=== TEST 12: Test geoip_lookup +--- http_config eval +"$::HttpConfig" +--- config + charset utf8; + location /t { + default_type text/plain; + charset utf-8; + content_by_lua_block { + local geoip_lookup = require("geojs.utils").geoip_lookup + local args = ngx.req.get_uri_args() + local ip = args.ip + local cjson = require("cjson") + + ngx.print(cjson.encode(geoip_lookup(ip))) + } + } +--- request eval +["GET /t?ip=8.8.8.9", +"GET /t?ip=2001:4860:4860::8888"] +--- no_error_log +[error] +--- response_body eval +['{"subdivisions":[{"names":{}}],"autonomous_system_number":15169,"registered_country":{"geoname_id":6252001,"names":{"en":"United States","ru":"США","fr":"États-Unis","pt-BR":"Estados Unidos","zh-CN":"美国","es":"Estados Unidos","de":"USA","ja":"アメリカ合衆国"},"iso_code":"US"},"continent":{"geoname_id":6255149,"names":{"en":"North America","ru":"Северная Америка","fr":"Amérique du Nord","pt-BR":"América do Norte","zh-CN":"北美洲","es":"Norteamérica","de":"Nordamerika","ja":"北アメリカ"},"code":"NA"},"postal":{},"city":{"names":{}},"country":{"geoname_id":6252001,"iso_code3":"USA","names":{"en":"United States","ru":"США","fr":"États-Unis","pt-BR":"Estados Unidos","zh-CN":"美国","es":"Estados Unidos","de":"USA","ja":"アメリカ合衆国"},"iso_code":"US"},"location":{"latitude":37.751,"accuracy_radius":1000,"longitude":-97.822},"autonomous_system_organization":"Google LLC"}','{"subdivisions":[{"names":{}}],"autonomous_system_number":15169,"registered_country":{"geoname_id":6252001,"names":{"en":"United States","ru":"США","fr":"États-Unis","pt-BR":"Estados Unidos","zh-CN":"美国","es":"Estados Unidos","de":"USA","ja":"アメリカ合衆国"},"iso_code":"US"},"continent":{"geoname_id":6255149,"names":{"en":"North America","ru":"Северная Америка","fr":"Amérique du Nord","pt-BR":"América do Norte","zh-CN":"北美洲","es":"Norteamérica","de":"Nordamerika","ja":"北アメリカ"},"code":"NA"},"postal":{},"city":{"names":{}},"country":{"geoname_id":6252001,"iso_code3":"USA","names":{"en":"United States","ru":"США","fr":"États-Unis","pt-BR":"Estados Unidos","zh-CN":"美国","es":"Estados Unidos","de":"USA","ja":"アメリカ合衆国"},"iso_code":"US"},"location":{"latitude":37.751,"accuracy_radius":100,"longitude":-97.822},"autonomous_system_organization":"Google LLC"}'] + + +=== TEST 13: Test geo_lookup +--- http_config eval +"$::HttpConfig" +--- config + charset utf8; + location /t { + default_type text/plain; + charset utf-8; + content_by_lua_block { + local geo_lookup = require("geojs.utils").geo_lookup + local args = ngx.req.get_uri_args() + local ip = args.ip + local cjson = require("cjson") + + ngx.print(cjson.encode(geo_lookup(ip))) + } + } +--- request eval +["GET /t?ip=8.8.8.8", +"GET /t?ip=2001:4860:4860::8888"] +--- no_error_log +[error] +--- response_body eval +['{"organization_name":"Google LLC","accuracy":1000,"asn":15169,"organization":"AS15169 Google LLC","longitude":"-97.822","country_code3":"USA","area_code":"0","ip":"8.8.8.8","country":"United States","continent_code":"NA","country_code":"US","latitude":"37.751"}','{"organization_name":"Google LLC","accuracy":100,"asn":15169,"organization":"AS15169 Google LLC","longitude":"-97.822","country_code3":"USA","area_code":"0","ip":"2001:4860:4860::8888","country":"United States","continent_code":"NA","country_code":"US","latitude":"37.751"}'] + + +=== TEST 14: Test country_lookup +--- http_config eval +"$::HttpConfig" +--- config + charset utf8; + location /t { + default_type text/plain; + charset utf-8; + content_by_lua_block { + local country_lookup = require("geojs.utils").country_lookup + local args = ngx.req.get_uri_args() + local ip = args.ip + local cjson = require("cjson") + + ngx.print(cjson.encode(country_lookup(ip))) + } + } +--- request eval +["GET /t?ip=8.8.8.8", +"GET /t?ip=2001:4860:4860::8888"] +--- no_error_log +[error] +--- response_body eval +['{"country":"US","country_3":"USA","ip":"8.8.8.8","name":"United States"}', '{"country":"US","country_3":"USA","ip":"2001:4860:4860::8888","name":"United States"}'] diff --git a/t/v1/hooks/01-sanity.t b/t/v1/hooks/01-sanity.t index 694e79d..5f04c28 100644 --- a/t/v1/hooks/01-sanity.t +++ b/t/v1/hooks/01-sanity.t @@ -11,9 +11,6 @@ our $HttpConfig = qq{ require("luacov.runner").init() end } - geoip_country "$pwd/download-cache/maxmind/GeoIPv6.dat"; - geoip_city "$pwd/download-cache/maxmind/GeoLiteCityv6.dat"; - geoip_org "$pwd/download-cache/maxmind/GeoIPASNumv6.dat"; lua_package_path "$pwd/lib/?.lua;;"; set_real_ip_from 127.0.0.1/32; }; diff --git a/t/v1/hooks/02-hipchat.t b/t/v1/hooks/02-hipchat.t index 6263f16..e769346 100644 --- a/t/v1/hooks/02-hipchat.t +++ b/t/v1/hooks/02-hipchat.t @@ -11,10 +11,7 @@ our $HttpConfig = qq{ require("luacov.runner").init() end } - geoip_country "$pwd/download-cache/maxmind/GeoIPv6.dat"; - geoip_city "$pwd/download-cache/maxmind/GeoLiteCityv6.dat"; - geoip_org "$pwd/download-cache/maxmind/GeoIPASNumv6.dat"; - lua_package_path "$pwd/lib/?.lua;$pwd/repos/lua-resty-dns/lib/?.lua;$pwd/repos/lua-resty-http/lib/?.lua;$pwd/repos/lua-resty-iconv/lualib/?.lua;$pwd/repos/lua-resty-reqargs/lib/?.lua;$pwd/repos/lua-resty-upload/lib/?.lua;;"; + lua_package_path "$pwd/lib/?.lua;;"; real_ip_header X-Real-IP; set_real_ip_from 127.0.0.1/32; }; @@ -127,7 +124,7 @@ $::JSONPayload" --- no_error_log [error] --- response_body -{"notify":"False","message_format":"html","message":"Results for 8.8.8.8<\/b>

PTR: google-public-dns-a.google.com
Country: United States
Organization: AS15169 Google LLC

Powered by GeoJS<\/a>","card":{"icon":{"url":"https:\/\/static.jloh.co\/geojs\/flags\/v1\/us.png","url@2x":"https:\/\/static.jloh.co\/geojs\/flags\/v1\/2x\/us.png"},"format":"medium","title":"GeoIP results for 8.8.8.8","id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","attributes":[{"label":"Powered by","value":{"label":"GeoJS","url":"https:\/\/geojs.io"}}],"activity":{"html":"8.8.8.8<\/strong> is a United States IP belonging to AS15169 Google LLC"},"style":"application","description":{"value":"PTR:<\/strong> google-public-dns-a.google.com","format":"html"}}} +{"notify":"False","message_format":"html","card":{"icon":{"url":"https:\/\/static.jloh.co\/geojs\/flags\/v1\/us.png","url@2x":"https:\/\/static.jloh.co\/geojs\/flags\/v1\/2x\/us.png"},"title":"GeoIP results for 8.8.8.8","activity":{"html":"8.8.8.8<\/strong> is a United States IP belonging to Google LLC"},"id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","description":{"value":"PTR:<\/strong> google-public-dns-a.google.com","format":"html"},"attributes":[{"label":"Powered by","value":{"label":"GeoJS","url":"https:\/\/geojs.io"}}],"style":"application","format":"medium"},"message":"Results for 8.8.8.8<\/b>

PTR: google-public-dns-a.google.com
Country: United States
Organization: Google LLC

Powered by
GeoJS<\/a>"} === TEST 2: Webhook without a valid IP --- http_config eval @@ -146,4 +143,4 @@ $::BadJSONPayload" --- no_error_log [error] --- response_body -{"color":"red","message_format":"html","message":"Hmmm. Looks like you've given us a bad IP (google.com<\/code>). This command only accepts IPs (IPv6 or IPv4) for now, sorry!","notify":"False"} +{"message":"Hmmm. Looks like you've given us a bad IP (google.com<\/code>). This command only accepts IPs (IPv6 or IPv4) for now, sorry!","message_format":"html","color":"red","notify":"False"} diff --git a/t/v1/hooks/03-slack.t b/t/v1/hooks/03-slack.t index b22be45..9cd6dc6 100644 --- a/t/v1/hooks/03-slack.t +++ b/t/v1/hooks/03-slack.t @@ -11,10 +11,7 @@ our $HttpConfig = qq{ require("luacov.runner").init() end } - geoip_country "$pwd/download-cache/maxmind/GeoIPv6.dat"; - geoip_city "$pwd/download-cache/maxmind/GeoLiteCityv6.dat"; - geoip_org "$pwd/download-cache/maxmind/GeoIPASNumv6.dat"; - lua_package_path "$pwd/lib/?.lua;$pwd/repos/lua-resty-dns/lib/?.lua;$pwd/repos/lua-resty-http/lib/?.lua;$pwd/repos/lua-resty-iconv/lualib/?.lua;$pwd/repos/lua-resty-reqargs/lib/?.lua;$pwd/repos/lua-resty-upload/lib/?.lua;;"; + lua_package_path "$pwd/lib/?.lua;;"; real_ip_header X-Real-IP; set_real_ip_from 127.0.0.1/32; }; @@ -83,7 +80,7 @@ Content-type: application/x-www-form-urlencoded --- no_error_log [error] --- response_body -{"attachments":[{"text":"Hmmm. Looks like you've given us a bad IP (`google.com`). This command only accepts IPs (IPv6 or IPv4) for now, sorry!","mrkdwn_in":["text"],"fallback":"Hmmm. Looks like you've given us a bad IP. This command only accepts IPs (IPv6 or IPv4) for now, sorry!","color":"danger"}]} +{"attachments":[{"text":"Hmmm. Looks like you've given us a bad IP (`google.com`). This command only accepts IPs (IPv6 or IPv4) for now, sorry!","fallback":"Hmmm. Looks like you've given us a bad IP. This command only accepts IPs (IPv6 or IPv4) for now, sorry!","color":"danger","mrkdwn_in":["text"]}]} === TEST 4: Help command --- http_config eval diff --git a/t/v1/hooks/04-twist.t b/t/v1/hooks/04-twist.t index d04ecd8..ff79574 100644 --- a/t/v1/hooks/04-twist.t +++ b/t/v1/hooks/04-twist.t @@ -11,10 +11,7 @@ our $HttpConfig = qq{ require("luacov.runner").init() end } - geoip_country "$pwd/download-cache/maxmind/GeoIPv6.dat"; - geoip_city "$pwd/download-cache/maxmind/GeoLiteCityv6.dat"; - geoip_org "$pwd/download-cache/maxmind/GeoIPASNumv6.dat"; - lua_package_path "$pwd/lib/?.lua;$pwd/repos/lua-resty-dns/lib/?.lua;$pwd/repos/lua-resty-http/lib/?.lua;$pwd/repos/lua-resty-iconv/lualib/?.lua;$pwd/repos/lua-resty-reqargs/lib/?.lua;$pwd/repos/lua-resty-upload/lib/?.lua;;"; + lua_package_path "$pwd/lib/?.lua;;"; real_ip_header X-Real-IP; set_real_ip_from 127.0.0.1/32; }; @@ -50,7 +47,7 @@ Content-type: application/x-www-form-urlencoded --- response_headers Content-Type: application/json --- response_body -{"content":"### IP information for **8.8.8.8**\nPTR: `google-public-dns-a.google.com`\nCountry: United States\nOrganization: AS15169 Google LLC\nPowered by [GeoJS](https:\/\/geojs.io)"} +{"content":"### IP information for **8.8.8.8**\nPTR: `google-public-dns-a.google.com`\nCountry: United States\nOrganization: Google LLC\nPowered by [GeoJS](https:\/\/geojs.io)"} === TEST 2: Bad token --- http_config eval diff --git a/t/v1/ip/01-sanity.t b/t/v1/ip/01-sanity.t index 849475b..970a7b7 100644 --- a/t/v1/ip/01-sanity.t +++ b/t/v1/ip/01-sanity.t @@ -4,9 +4,6 @@ use Cwd qw(cwd); my $pwd = cwd(); our $HttpConfig = qq{ - geoip_country "$pwd/download-cache/maxmind/GeoIPv6.dat"; - geoip_city "$pwd/download-cache/maxmind/GeoLiteCityv6.dat"; - geoip_org "$pwd/download-cache/maxmind/GeoIPASNumv6.dat"; lua_package_path "$pwd/lib/?.lua;;"; real_ip_header X-IP; set_real_ip_from 127.0.0.1/32; diff --git a/t/v1/ip/02-ip.t b/t/v1/ip/02-ip.t index 6342424..ec39106 100644 --- a/t/v1/ip/02-ip.t +++ b/t/v1/ip/02-ip.t @@ -11,9 +11,6 @@ our $HttpConfig = qq{ require("luacov.runner").init() end } - geoip_country "$pwd/download-cache/maxmind/GeoIPv6.dat"; - geoip_city "$pwd/download-cache/maxmind/GeoLiteCityv6.dat"; - geoip_org "$pwd/download-cache/maxmind/GeoIPASNumv6.dat"; lua_package_path "$pwd/lib/?.lua;;"; set_real_ip_from 127.0.0.1/32; }; diff --git a/t/v1/ip/03-country.t b/t/v1/ip/03-country.t index cf33094..af69baf 100644 --- a/t/v1/ip/03-country.t +++ b/t/v1/ip/03-country.t @@ -11,9 +11,6 @@ our $HttpConfig = qq{ require("luacov.runner").init() end } - geoip_country "$pwd/download-cache/maxmind/GeoIPv6.dat"; - geoip_city "$pwd/download-cache/maxmind/GeoLiteCityv6.dat"; - geoip_org "$pwd/download-cache/maxmind/GeoIPASNumv6.dat"; lua_package_path "$pwd/lib/?.lua;;"; real_ip_header X-IP; set_real_ip_from 127.0.0.1/32; @@ -22,7 +19,7 @@ our $HttpConfig = qq{ run_tests(); __DATA__ -=== TEST 1: Plain text endpoint +=== TEST 1.a: Plain text endpoint --- http_config eval "$::HttpConfig" --- config @@ -39,7 +36,22 @@ Content-Type: text/plain US -=== TEST 2: JSON Endpoint +=== TEST 1.b: Plain text endpoint with specific IP +--- http_config eval +"$::HttpConfig" +--- config + include "../../../conf/v1/ip.conf"; +--- request +GET /v1/ip/country/8.8.8.8 +--- no_error_log +[error] +--- response_headers +Content-Type: text/plain +--- response_body +US + + +=== TEST 2.a: JSON Endpoint --- http_config eval "$::HttpConfig" --- config @@ -53,10 +65,25 @@ X-IP: 8.8.8.8 --- response_headers Content-Type: application/json --- response_body -{"country":"US","ip":"8.8.8.8","name":"United States","country_3":"USA"} +{"country":"US","country_3":"USA","ip":"8.8.8.8","name":"United States"} + + +=== TEST 2.b: JSON Endpoint with specific IP +--- http_config eval +"$::HttpConfig" +--- config + include "../../../conf/v1/ip.conf"; +--- request +GET /v1/ip/country/8.8.8.8.json +--- no_error_log +[error] +--- response_headers +Content-Type: application/json +--- response_body +{"country":"US","country_3":"USA","ip":"8.8.8.8","name":"United States"} -=== TEST 3: JS Endpoint +=== TEST 3.a: JS Endpoint --- http_config eval "$::HttpConfig" --- config @@ -70,10 +97,25 @@ GET /v1/ip/country.js --- response_headers Content-Type: application/javascript --- response_body -countryip({"country":"US","ip":"8.8.8.8","name":"United States","country_3":"USA"}) +countryip({"country":"US","country_3":"USA","ip":"8.8.8.8","name":"United States"}) + + +=== TEST 3.b: JS Endpoint with specific IP +--- http_config eval +"$::HttpConfig" +--- config + include "../../../conf/v1/ip.conf"; +--- request +GET /v1/ip/country/8.8.8.8.js +--- no_error_log +[error] +--- response_headers +Content-Type: application/javascript +--- response_body +countryip({"country":"US","country_3":"USA","ip":"8.8.8.8","name":"United States"}) -=== TEST 4: JS Endpoint with custom callback +=== TEST 4.a: JS Endpoint with custom callback --- http_config eval "$::HttpConfig" --- config @@ -87,10 +129,25 @@ X-IP: 8.8.8.8 --- response_headers Content-Type: application/javascript --- response_body -tests({"country":"US","ip":"8.8.8.8","name":"United States","country_3":"USA"}) +tests({"country":"US","country_3":"USA","ip":"8.8.8.8","name":"United States"}) -=== TEST 5: JS Endpoint sanitise user input +=== TEST 4.b: JS Endpoint with custom callback specific IP +--- http_config eval +"$::HttpConfig" +--- config + include "../../../conf/v1/ip.conf"; +--- request +GET /v1/ip/country/8.8.8.8.js?callback=tests +--- no_error_log +[error] +--- response_headers +Content-Type: application/javascript +--- response_body +tests({"country":"US","country_3":"USA","ip":"8.8.8.8","name":"United States"}) + + +=== TEST 5.a: JS Endpoint sanitise user input --- http_config eval "$::HttpConfig" --- config @@ -104,4 +161,51 @@ X-IP: 8.8.8.8 --- response_headers Content-Type: application/javascript --- response_body -%3Cscript%3E({"country":"US","ip":"8.8.8.8","name":"United States","country_3":"USA"}) +%3Cscript%3E({"country":"US","country_3":"USA","ip":"8.8.8.8","name":"United States"}) + + +=== TEST 5.b: JS Endpoint sanitise user input specific IP +--- http_config eval +"$::HttpConfig" +--- config + include "../../../conf/v1/ip.conf"; +--- request +GET /v1/ip/country/8.8.8.8.js?callback=