-- ************************************************************************
--
-- Gateway Interface utility routines.
-- Copyright 2019 by Sean Conner. All Rights Reserved.
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see .
--
-- Comments, questions and criticisms can be sent to: sean@conman.org
--
-- ************************************************************************
-- luacheck: globals parse_headers get_instance isset merge
-- luacheck: globals breakdown setup_env handle_output
-- luacheck: ignore 611
-- RFC-3875
local syslog = require "org.conman.syslog"
local abnf = require "org.conman.parsers.abnf"
local fsys = require "org.conman.fsys"
local lpeg = require "lpeg"
local math = require "math"
local os = require "os"
local uurl = require "GLV-1.url-util"
local pairs = pairs
local select = select
local tonumber = tonumber
local tostring = tostring
_ENV = {}
-- ************************************************************************
do
local Cc = lpeg.Cc
local Cf = lpeg.Cf
local Cg = lpeg.Cg
local Cs = lpeg.Cs
local Ct = lpeg.Ct
local C = lpeg.C
local P = lpeg.P
local R = lpeg.R
local S = lpeg.S
local H do
local text = R("AZ","az") / function(c) return P(c:lower()) + P(c:upper()) end
+ P(1) / function(c) return P(c) end
H = function(s)
local pattern = Cf(text^1,function(acc,pat) return acc * pat end)
return pattern:match(s) / s
end
end
local LWSP = (abnf.WSP + abnf.CRLF * abnf.WSP)
local text = LWSP^1 / " "
+ abnf.VCHAR
local ignore = LWSP + abnf.VCHAR
local code = (R"16" * R"09" * #(P(1) - R"09")) / tonumber
+ (R"09" * R"09" * #(P(1) - R"09")) * Cc(50)
-- ------------------------------------------
-- Most common web status codes, translated
-- ------------------------------------------
+ P"200" * Cc(20)
+ P"301" * Cc(31)
+ P"302" * Cc(30)
+ P"400" * Cc(59)
+ P"403" * Cc(60)
+ P"404" * Cc(51)
+ P"405" * Cc(59)
+ P"500" * Cc(40)
+ P"501" * Cc(40)
-- ----------------
-- Web catch all
-- ----------------
+ P"2" * R"09" * R"09" * Cc(20)
+ P"3" * R"09" * R"09" * Cc(30)
+ P"4" * R"09" * R"09" * Cc(50)
+ P"5" * R"09" * R"09" * Cc(40)
+ R"09" * R"09" * R"09" * Cc(50)
local separator = S'()<>@,;:\\"/[]?={}\t '
local token = (abnf.VCHAR - separator)^1
local status = H"Status" * P":" * LWSP * code * ignore^0 * abnf.CRLF
local content_type = H"Content-Type" * P":" * LWSP * Cs(text^1) * abnf.CRLF
local location = H"Location" * P":" * LWSP * C(abnf.VCHAR^1) * abnf.CRLF
local generic = C(token) * P":" * LWSP * C(text^0) * abnf.CRLF
local headers = status + content_type + location + generic
parse_headers = Cf(Ct"" * Cg(headers)^1,function(acc,name,value)
acc[name] = value
return acc
end)
* abnf.CRLF
end
-- ************************************************************************
function get_instance(location,list)
if list then
for name,info in pairs(list) do
if location.path:match(name) then
return info
end
end
end
return {}
end
-- ************************************************************************
function isset(...)
for i = 1 , select('#',...) do
local v = select(i,...)
if v ~= nil then return v end
end
end
-- ************************************************************************
function merge(...)
local accenv = {}
for i = 1 , select('#',...) do
local env = select(i,...)
if env then
for var,val in pairs(env) do
accenv[var] = val
end
end
end
return accenv
end
-- ************************************************************************
function breakdown(env,base,fields)
for name,value in pairs(fields) do
env[base .. name] = value
end
end
-- ************************************************************************
function setup_env(auth,program,base,location,directory,di,hconf,gconf)
gconf = gconf or {} -- server wide config
hconf = hconf or {} -- host config
local dconf = directory[di] or {} -- directory config
if program:match "^/" then
program = uurl.rm_dot_segs:match(program)
else
program = uurl.rm_dot_segs:match(fsys.getcwd() .. "/" .. program)
end
local gconfi = get_instance(location,gconf.instance)
local hconfi = get_instance(location,hconf.instance)
local dconfi = get_instance(location,dconf.instance)
local env = merge(
gconf.env,
gconfi.env,
hconf.env,
hconfi.env,
dconf.env,
dconfi.env
)
env.GEMINI_DOCUMENT_ROOT = directory.directory
env.GEMINI_SCRIPT_FILENAME = program
env.GEMINI_URL_PATH = location.path
env.GEMINI_URL = uurl.toa(location)
env.GATEWAY_INTERFACE = "CGI/1.1"
env.QUERY_STRING = location.query or ""
env.REMOTE_ADDR = auth._remote
env.REMOTE_HOST = auth._remote
env.SCRIPT_NAME = base
env.SERVER_NAME = location.host
env.SERVER_PORT = tostring(location.port)
env.SERVER_SOFTWARE = "GLV-1.12556/1"
local _,e = location.path:find(fsys.basename(program),1,true)
local pathinfo = e and location.path:sub(e+1,-1) or location.path
if pathinfo ~= "" then
env.PATH_INFO = pathinfo
env.PATH_TRANSLATED = directory.directory .. pathinfo
end
local http = isset(dconfi.http, dconf.http, hconfi.http, hconf.http, gconfi.http, gconf.http)
local apache = isset(dconfi.apache,dconf.apache,hconfi.apache,hconf.apache,gconfi.apache,gconf.apache)
local envtls = isset(dconfi.envtls,dconf.envtls,hconfi.envtls,hconf.envtls,gconfi.envtls,gconf.envtls)
if http then
env.REQUEST_METHOD = "GET"
env.SERVER_PROTOCOL = "HTTP/1.0"
env.HTTP_ACCEPT = "*/*"
env.HTTP_ACCEPT_LANGUAGE = "*"
env.HTTP_CONNECTION = "close"
env.HTTP_REFERER = ""
env.HTTP_USER_AGENT = ""
else
env.REQUEST_METHOD = ""
env.SERVER_PROTOCOL = "GEMINI"
end
if auth._provided then
env.AUTH_TYPE = "Certificate"
env.REMOTE_USER = auth.subject.CN or ""
if envtls then
local remain = tostring(math.floor(os.difftime(auth.notafter,auth.now) / 86400))
if not apache then
env.TLS_CIPHER = auth._ctx:conn_cipher()
env.TLS_VERSION = auth._ctx:conn_version()
env.TLS_CLIENT_HASH = auth._ctx:peer_cert_hash()
env.TLS_CLIENT_ISSUER = auth.I
env.TLS_CLIENT_SUBJECT = auth.S
env.TLS_CLIENT_NOT_BEFORE = os.date("%Y-%m-%dT%H:%M:%SZ",auth.notbefore)
env.TLS_CLIENT_NOT_AFTER = os.date("%Y-%m-%dT%H:%M:%SZ",auth.notafter)
env.TLS_CLIENT_REMAIN = remain
env.TLS_SNI = location.host
breakdown(env,"TLS_CLIENT_ISSUER_", auth.issuer)
breakdown(env,"TLS_CLIENT_SUBJECT_",auth.subject)
else
env.SSL_CIPHER = auth._ctx:conn_cipher()
env.SSL_PROTOCOL = auth._ctx:conn_version()
env.SSL_CLIENT_I_DN = auth.I
env.SSL_CLIENT_S_DN = auth.S
env.SSL_CLIENT_V_START = os.date("%b %d %H:%M:%S %Y GMT",auth.notbefore)
env.SSL_CLIENT_V_END = os.date("%b %d %H:%M:%S %Y GMT",auth.notafter)
env.SSL_CLIENT_V_REMAIN = remain
env.SSL_TLS_SNI = location.host
breakdown(env,"SSL_CLIENT_I_DN_",auth.issuer)
breakdown(env,"SSL_CLIENT_S_DN_",auth.subject)
end
end
end
if apache then
env.DOCUMENT_ROOT = directory.directory
env.CONTEXT_DOCUMENT_ROOT = directory.directory
env.CONTENT_PREFIX = ""
env.SCRIPT_FILENAME = program
end
return env
end
-- ************************************************************************
function handle_output(ios,inp,program)
local headers = inp:read("h")
if not headers then
syslog('error',"%s: is this a *GatewayInterface program?",program)
ios:write("40 \r\n")
return 40
end
local pheaders = parse_headers:match(headers)
if not pheaders then
syslog('error',"%s: Bad headers? %q",program,headers or "")
ios:write("40 \r\n")
return 40
end
headers = pheaders
local status = headers['Status'] or 20
if headers['Location'] then
ios:write(status," ",headers['Location'],"\r\n")
else
ios:write(status," ",headers['Content-Type'] or "text/plain","\r\n")
repeat
local data = inp:read(1024)
if data then ios:write(data) end
until not data
end
return headers['Status'] or 20
end
-- ************************************************************************
return _ENV