feat: initial commit
This commit is contained in:
commit
6179dea246
19 changed files with 693 additions and 0 deletions
9
src/Dockerfile
Normal file
9
src/Dockerfile
Normal file
|
@ -0,0 +1,9 @@
|
|||
FROM openresty/openresty:alpine-fat
|
||||
|
||||
RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-http
|
||||
RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-cookie
|
||||
RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-iputils
|
||||
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY env.main /etc/nginx/conf.d/env.main
|
||||
COPY lua/* /opt/sitelen-tu/
|
42
src/default.conf
Normal file
42
src/default.conf
Normal file
|
@ -0,0 +1,42 @@
|
|||
error_log /dev/stdout;
|
||||
|
||||
lua_shared_dict jasima_cache 20m;
|
||||
lua_package_path "/opt/sitelen-tu/?.lua;;";
|
||||
resolver 127.0.0.11 valid=60 ipv6=off;
|
||||
|
||||
upstream origin {
|
||||
server 127.0.0.1;
|
||||
|
||||
balancer_by_lua_file /opt/sitelen-tu/balancer.lua;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost default;
|
||||
|
||||
location / {
|
||||
# These variables are set in the access_by_lua stage
|
||||
# TODO: These might be better to set with a set_by_lua_block
|
||||
set $jasima_host "fallback.invalid";
|
||||
set $jasima_host_header "fallback.invalid";
|
||||
set $jasima_host_ssl "fallback.invalid";
|
||||
|
||||
access_by_lua_file /opt/sitelen-tu/access.lua;
|
||||
|
||||
proxy_pass https://origin;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name $jasima_host_ssl;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_set_header Host $jasima_host_header;
|
||||
|
||||
sub_filter_once off;
|
||||
sub_filter_types text/html text/css text/xml application/javascript application/rss+xml application/atom+xml application/vnd.mpegurl application/x-mpegurl;
|
||||
sub_filter 'http://$jasima_host' '/';
|
||||
sub_filter 'https://$jasima_host' '/';
|
||||
sub_filter '//$jasima_host' '/';
|
||||
sub_filter 'REWRITE_JASIMA_HOST_PLACEHOLDER' $jasima_host;
|
||||
|
||||
header_filter_by_lua_file /opt/sitelen-tu/header_filter.lua;
|
||||
body_filter_by_lua_file /opt/sitelen-tu/body_filter.lua;
|
||||
}
|
||||
}
|
1
src/env.main
Normal file
1
src/env.main
Normal file
|
@ -0,0 +1 @@
|
|||
env JASIMA_MATOMO_HOST;
|
72
src/lua/access.lua
Normal file
72
src/lua/access.lua
Normal file
|
@ -0,0 +1,72 @@
|
|||
local http = require "resty.http"
|
||||
|
||||
local config = require "config"
|
||||
local geo = require "geo"
|
||||
local utils = require "utils"
|
||||
|
||||
local jasima_host = config.get_jasima_host()
|
||||
ngx.ctx.jasima_host = jasima_host
|
||||
if not jasima_host then
|
||||
return ngx.exit(400)
|
||||
end
|
||||
|
||||
local err
|
||||
ngx.ctx.jasima_config, err = config.load_config(jasima_host)
|
||||
if err then
|
||||
ngx.status = 500
|
||||
ngx.log(ngx.ERR, "Could not load config: " .. err)
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
if not ngx.ctx.jasima_config then
|
||||
ngx.status = 403
|
||||
ngx.log(ngx.ERR, "Requested a canonical host that has no configuration specified: " .. jasima_host)
|
||||
ngx.exit(403)
|
||||
end
|
||||
|
||||
local country = geo.viewer_country()
|
||||
if not ngx.ctx.jasima_config.geo_redirect_disable and not geo.needs_mirror(country) then
|
||||
local request_uri = ngx.var.request_uri
|
||||
local new_url = "https://" .. jasima_host .. request_uri
|
||||
return ngx.redirect(new_url, ngx.HTTP_MOVED_TEMPORARILY)
|
||||
end
|
||||
|
||||
-- Get jasima_pool (not critical)
|
||||
local jasima_pool = config.get_jasima_pool()
|
||||
ngx.ctx.jasima_pool_map, err = config.load_pool_mapping(jasima_pool)
|
||||
if err then
|
||||
ngx.status = 500
|
||||
ngx.log(ngx.WARN, "Could not load pool mapping: " .. err)
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
local headers = ngx.req.get_headers()
|
||||
|
||||
-- Remove the headers that should not be proxied to the origin
|
||||
for k, v in pairs(headers) do
|
||||
if k:lower():match("^jasima%-") then
|
||||
ngx.req.clear_header(k)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add additional headers that have been specified in the configuration
|
||||
if ngx.ctx.jasima_config.headers then
|
||||
for k, v in pairs(ngx.ctx.jasima_config.headers) do
|
||||
ngx.req.set_header(k, v)
|
||||
end
|
||||
end
|
||||
|
||||
-- Look up the IP to connect to the origin
|
||||
local host_connect = ngx.ctx.jasima_config.host_connect or jasima_host
|
||||
local upstream_ips = utils.resolve_origin(host_connect)
|
||||
ngx.ctx.upstream_ips = utils.filter_bogons(upstream_ips)
|
||||
|
||||
if #ngx.ctx.upstream_ips == 0 then
|
||||
ngx.log(ngx.ERR, "no A records found")
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
-- Set the nginx host variables
|
||||
ngx.var.jasima_host = jasima_host
|
||||
ngx.var.jasima_host_header = ngx.ctx.jasima_config.host_header or jasima_host
|
||||
ngx.var.jasima_host_ssl = ngx.ctx.jasima_config.host_ssl or jasima_host
|
24
src/lua/balancer.lua
Normal file
24
src/lua/balancer.lua
Normal file
|
@ -0,0 +1,24 @@
|
|||
local balancer = require "ngx.balancer"
|
||||
|
||||
local upstream_ips = ngx.ctx.upstream_ips
|
||||
if not upstream_ips then
|
||||
ngx.log(ngx.ERR, "No upstream IPs in context")
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
ngx.ctx.balancer_try = (ngx.ctx.balancer_try or 0) + 1
|
||||
local try_index = ngx.ctx.balancer_try
|
||||
|
||||
if try_index > #upstream_ips then
|
||||
ngx.log(ngx.ERR, "All upstream IPs tried, none succeeded")
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
local ip = upstream_ips[try_index]
|
||||
ngx.log(ngx.INFO, "Trying upstream IP: ", ip)
|
||||
|
||||
local ok, err = balancer.set_current_peer(ip, 443)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "failed to set peer: ", err)
|
||||
return ngx.exit(500)
|
||||
end
|
63
src/lua/body_filter.lua
Normal file
63
src/lua/body_filter.lua
Normal file
|
@ -0,0 +1,63 @@
|
|||
local function matomo_tracking_code(site_id)
|
||||
return [=[
|
||||
<!-- Matomo -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
var p = "https://REWRITE_JASIMA_HOST_PLACEHOLDER" + window.location.pathname;
|
||||
_paq.push(["setCustomUrl", p]);
|
||||
_paq.push(["setExcludedQueryParams", ["roomName","account","accountnum","address","address1","address2","address3","addressline1","addressline2","adres","adresse","age","alter","auth","authpw","bic","billingaddress","billingaddress1","billingaddress2","calle","cardnumber","cc","ccc","cccsc","cccvc","cccvv","ccexpiry","ccexpmonth","ccexpyear","ccname","ccnumber","cctype","cell","cellphone","city","clientid","clientsecret","company","consumerkey","consumersecret","contrasenya","contrase\u00f1a","creditcard","creditcardnumber","cvc","cvv","dateofbirth","debitcard","direcci\u00f3n","dob","domain","ebost","email","emailaddress","emailadresse","epos","epost","eposta","exp","familyname","firma","firstname","formlogin","fullname","gender","geschlecht","gst","gstnumber","handynummer","has\u0142o","heslo","iban","ibanaccountnum","ibanaccountnumber","id","identifier","indirizzo","kartakredytowa","kennwort","keyconsumerkey","keyconsumersecret","konto","kontonr","kontonummer","kredietkaart","kreditkarte","kreditkort","lastname","login","mail","mobiili","mobile","mobilne","nachname","name","nickname","false","osoite","parole","pass","passord","password","passwort","pasword","paswort","paword","phone","pin","plz","postalcode","postcode","postleitzahl","privatekey","publickey","pw","pwd","pword","pwrd","rue","secret","secretq","secretquestion","shippingaddress","shippingaddress1","shippingaddress2","socialsec","socialsecuritynumber","socsec","sokak","ssn","steuernummer","strasse","street","surname","swift","tax","taxnumber","tel","telefon","telefonnr","telefonnummer","telefono","telephone","token","token_auth","tokenauth","t\u00e9l\u00e9phone","ulica","user","username","vat","vatnumber","via","vorname","wachtwoord","wagwoord","webhooksecret","website","zip","zipcode"]]);
|
||||
_paq.push(["trackPageView", p]);
|
||||
_paq.push(["enableLinkTracking"]);
|
||||
(function() {
|
||||
var u="//]=] .. os.getenv("JASIMA_MATOMO_HOST") .. [=[/";
|
||||
_paq.push(["setTrackerUrl", u+"matomo.php"]);
|
||||
_paq.push(["setSiteId", "]=] .. tostring(ngx.ctx.jasima_config.matomo_site_id) .. [=["]);
|
||||
var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0];
|
||||
g.async=true; g.src=u+"matomo.js"; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Matomo Code -->
|
||||
</body>
|
||||
]=]
|
||||
end
|
||||
|
||||
local function rewrite_body(body, eof)
|
||||
if not ngx.ctx.jasima_pool_map or ngx.ctx.jasima_config.rewrite_disable then
|
||||
return body
|
||||
end
|
||||
for from, to in pairs(ngx.ctx.jasima_pool_map) do
|
||||
if ngx.ctx.jasima_config.rewrite_case_insensitive then
|
||||
local pattern = ngx.re.escape(from)
|
||||
body = ngx.re.gsub(body, pattern, to, "ijo")
|
||||
else
|
||||
-- We expect that str:match("^[%w%-%.]+$") ~= nil
|
||||
local pattern = from:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") -- escape Lua patterns
|
||||
body = body:gsub(pattern, to)
|
||||
end
|
||||
end
|
||||
if eof and ngx.ctx.jasima_config.matomo_site_id then
|
||||
body = body:gsub("</body>", matomo_tracking_code(ngx.ctx.jasima_config.matomo_site_id))
|
||||
-- TODO: Ensure that tracking code was added when it's HTML, but only for HTML
|
||||
end
|
||||
return body
|
||||
end
|
||||
|
||||
if ngx.ctx.rewriting then
|
||||
local chunk = ngx.arg[1]
|
||||
local eof = ngx.arg[2]
|
||||
|
||||
ngx.ctx.buffered = (ngx.ctx.buffered or "") .. (chunk or "")
|
||||
|
||||
if #ngx.ctx.buffered > 5 * 1024 * 1024 and not eof then
|
||||
-- Don't just consume memory forever
|
||||
ngx.arg[1] = rewrite_body(ngx.ctx.buffered, eof) -- We still do our best
|
||||
ngx.ctx.rewriting = false
|
||||
return
|
||||
end
|
||||
|
||||
if eof then
|
||||
ngx.arg[1] = rewrite_body(ngx.ctx.buffered, eof)
|
||||
else
|
||||
ngx.arg[1] = nil
|
||||
end
|
||||
end
|
95
src/lua/config.lua
Normal file
95
src/lua/config.lua
Normal file
|
@ -0,0 +1,95 @@
|
|||
local ck = require "resty.cookie"
|
||||
local cjson = require "cjson.safe"
|
||||
local redis = require "resty.redis"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.get_jasima_host()
|
||||
local headers = ngx.req.get_headers()
|
||||
if headers["Jasima-Host"] then
|
||||
return headers["Jasima-Host"]
|
||||
end
|
||||
|
||||
local cookie, err = ck:new()
|
||||
if not cookie then
|
||||
ngx.log(ngx.ERR, "failed to get cookie: ", err)
|
||||
return nil
|
||||
end
|
||||
|
||||
local jasima_cookie, err = cookie:get("jasima_host")
|
||||
if jasima_cookie then
|
||||
return jasima_cookie
|
||||
elseif err then
|
||||
ngx.log(ngx.ERR, "failed to get jasima_host cookie: ", err)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function _M.get_jasima_pool()
|
||||
local headers = ngx.req.get_headers()
|
||||
if headers["Jasima-Pool"] then
|
||||
return headers["Jasima-Pool"]
|
||||
end
|
||||
|
||||
local cookie, err = ck:new()
|
||||
if not cookie then
|
||||
ngx.log(ngx.ERR, "failed to get cookie: ", err)
|
||||
return nil
|
||||
end
|
||||
|
||||
local jasima_cookie, err = cookie:get("jasima_pool")
|
||||
if jasima_cookie then
|
||||
return jasima_cookie
|
||||
elseif err then
|
||||
ngx.log(ngx.ERR, "failed to get jasima_pool cookie: ", err)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function _M.load_pool_mapping(pool_name)
|
||||
if not pool_name then pool_name = "public" end
|
||||
|
||||
local cache = ngx.shared.jasima_cache
|
||||
local cache_key = "poolmap:" .. pool_name
|
||||
local cached = cache:get(cache_key)
|
||||
if cached then return cjson.decode(cached) end
|
||||
|
||||
local red = redis:new()
|
||||
red:set_timeout(1000)
|
||||
local ok, err = red:connect("redis", 6379)
|
||||
if not ok then return nil, "Redis connect failed: " .. err end
|
||||
|
||||
local key = "jasima:poolmap:" .. pool_name
|
||||
local res, err = red:get(key)
|
||||
if not res or res == ngx.null then return nil, "No pool mapping found" end
|
||||
|
||||
red:set_keepalive(10000, 100)
|
||||
cache:set(cache_key, res, 60)
|
||||
|
||||
return cjson.decode(res)
|
||||
end
|
||||
|
||||
function _M.load_config(jasima_host)
|
||||
local cache = ngx.shared.jasima_cache
|
||||
local cache_key = "config:" .. jasima_host
|
||||
local cached = cache:get(cache_key)
|
||||
if cached then return cjson.decode(cached) end
|
||||
|
||||
local red = redis:new()
|
||||
red:set_timeout(1000)
|
||||
local ok, err = red:connect("redis", 6379)
|
||||
if not ok then return nil, "Redis connect failed: " .. err end
|
||||
|
||||
local key = "jasima:config:" .. jasima_host
|
||||
local res, err = red:get(key)
|
||||
if not res or res == ngx.null then return nil, "No config in Redis" end
|
||||
|
||||
red:set_keepalive(10000, 100)
|
||||
|
||||
cache:set(cache_key, res, 60)
|
||||
return cjson.decode(res)
|
||||
end
|
||||
|
||||
return _M
|
49
src/lua/geo.lua
Normal file
49
src/lua/geo.lua
Normal file
|
@ -0,0 +1,49 @@
|
|||
local _M = {}
|
||||
|
||||
function _M.viewer_country()
|
||||
-- Maybe the CDN was nice and gave this to us
|
||||
local country = ngx.var.http_cloudfront_viewer_country or -- AWS CloudFront
|
||||
ngx.var.http_fastly_client_country or -- Fastly
|
||||
ngx.var.http_cf_ipcountry -- CloudFlare
|
||||
if not country then return nil end
|
||||
return country:upper()
|
||||
|
||||
-- TODO: Fallback to GeoIP lookup
|
||||
end
|
||||
|
||||
function _M.needs_mirror(country)
|
||||
if not country then return true end
|
||||
|
||||
-- TODO: Allow override of safe countries in host config
|
||||
|
||||
local safe_countries = {
|
||||
US = true, -- United States
|
||||
GB = true, -- United Kingdom
|
||||
IE = true, -- Ireland
|
||||
FR = true, -- France
|
||||
DE = true, -- Germany
|
||||
NL = true, -- Netherlands
|
||||
BE = true, -- Belgium
|
||||
CH = true, -- Switzerland
|
||||
AT = true, -- Austria
|
||||
LU = true, -- Luxembourg
|
||||
LI = true, -- Liechtenstein
|
||||
MC = true, -- Monaco
|
||||
AD = true, -- Andorra
|
||||
ES = true, -- Spain
|
||||
PT = true, -- Portugal
|
||||
IT = true, -- Italy
|
||||
SM = true, -- San Marino
|
||||
VA = true, -- Vatican City
|
||||
MT = true, -- Malta
|
||||
NO = true, -- Norway
|
||||
SE = true, -- Sweden
|
||||
DK = true, -- Denmark
|
||||
FI = true, -- Finland
|
||||
IS = true -- Iceland
|
||||
}
|
||||
if safe_countries[country] then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
return _M
|
14
src/lua/header_filter.lua
Normal file
14
src/lua/header_filter.lua
Normal file
|
@ -0,0 +1,14 @@
|
|||
if ngx.header["Content-Type"] then
|
||||
local content_type = ngx.header["Content-Type"]
|
||||
if content_type:find("text/html") or
|
||||
content_type:find("text/css") or
|
||||
content_type:find("text/xml") or
|
||||
content_type:find("application/javascript") or
|
||||
content_type:find("application/rss+xml") or
|
||||
content_type:find("application/atom+xml") or
|
||||
content_type:find("application/vnd.mpegurl") or
|
||||
content_type:find("application/x-mpegurl") then
|
||||
ngx.ctx.rewriting = true
|
||||
ngx.header["Content-Length"] = nil
|
||||
end
|
||||
end
|
75
src/lua/utils.lua
Normal file
75
src/lua/utils.lua
Normal file
|
@ -0,0 +1,75 @@
|
|||
local resolver = require "resty.dns.resolver"
|
||||
local iputils = require "resty.iputils"
|
||||
|
||||
iputils.enable_lrucache()
|
||||
|
||||
local bogon_ips = iputils.parse_cidrs({
|
||||
"0.0.0.0/8",
|
||||
"10.0.0.0/8",
|
||||
"100.64.0.0/10",
|
||||
"127.0.0.0/8",
|
||||
"169.254.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"192.0.0.0/24",
|
||||
"192.0.2.0/24",
|
||||
"192.168.0.0/16",
|
||||
"198.18.0.0/15",
|
||||
"198.51.100.0/24",
|
||||
"203.0.113.0/24",
|
||||
"224.0.0.0/4",
|
||||
"240.0.0.0/4"
|
||||
})
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.resolve_origin(origin_host)
|
||||
local cache = ngx.shared.jasima_cache
|
||||
local cache_key = "upstream_ips:" .. origin_host
|
||||
local cached = cache:get(cache_key)
|
||||
if cached then return cached end
|
||||
|
||||
local r, err = resolver:new{
|
||||
nameservers = {"8.8.8.8", "8.8.4.4"},
|
||||
retrans = 5,
|
||||
timeout = 2000,
|
||||
}
|
||||
|
||||
if not r then
|
||||
ngx.log(ngx.ERR, "failed to instantiate resolver: ", err)
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
local answers, err = r:query(origin_host, {qtype = r.TYPE_A})
|
||||
if not answers then
|
||||
ngx.log(ngx.ERR, "failed to query: ", err)
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
if answers.errcode then
|
||||
ngx.log(ngx.ERR, "DNS error code: ", answers.errcode, ": ", answers.errstr)
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
local origin_ips = {}
|
||||
for _, ans in pairs(answers) do
|
||||
if ans.address then
|
||||
table.insert(origin_ips, ans.address)
|
||||
end
|
||||
end
|
||||
|
||||
cache:set(cache_key, origin_ips, 60)
|
||||
|
||||
return origin_ips
|
||||
end
|
||||
|
||||
function _M.filter_bogons(ip_list)
|
||||
local filtered = {}
|
||||
for _, ip in ipairs(ip_list) do
|
||||
if not iputils.ip_in_cidrs(ip, bogon_ips) then
|
||||
table.insert(filtered, ip)
|
||||
end
|
||||
end
|
||||
return filtered
|
||||
end
|
||||
|
||||
return _M
|
Loading…
Add table
Add a link
Reference in a new issue