Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
GLV-1.12556/Lua/GLV-1.12556.lua
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
537 lines (441 sloc)
15.8 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- #!/usr/bin/env lua | |
| -- ************************************************************************ | |
| -- | |
| -- Server program | |
| -- 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 <http://www.gnu.org/licenses/>. | |
| -- | |
| -- Comments, questions and criticisms can be sent to: sean@conman.org | |
| -- | |
| -- ************************************************************************ | |
| -- luacheck: ignore 611 | |
| local signal = require "org.conman.signal" | |
| local exit = require "org.conman.const.exit" | |
| local syslog = require "org.conman.syslog" | |
| local fsys = require "org.conman.fsys" | |
| local magic = require "org.conman.fsys.magic" | |
| local nfl = require "org.conman.nfl" | |
| local tls = require "org.conman.nfl.tls" | |
| local ip = require "org.conman.parsers.ip-text" | |
| local lpeg = require "lpeg" | |
| local url = require "org.conman.parsers.url" * lpeg.P(-1) | |
| local MSG = require "GLV-1.MSG" | |
| local CONF = {} | |
| magic:flags("mime") | |
| -- ************************************************************************ | |
| local parse_address do | |
| local C = lpeg.C | |
| local P = lpeg.P | |
| local R = lpeg.R | |
| local host = ip.IPv4 | |
| + P"[" * ip.IPv6 * P"]" | |
| + C(R("!9",";~")^1) | |
| local port = P":" * (R"09"^1 / tonumber) | |
| parse_address = host * port | |
| end | |
| -- ************************************************************************ | |
| if #arg == 0 then | |
| io.stderr:write(string.format("usage: %s config\n",arg[0])) | |
| os.exit(exit.USAGE,true) | |
| end | |
| do | |
| local conffile,err = loadfile(arg[1],"t",CONF) | |
| if not conffile then | |
| syslog('critical',"%s: %s",arg[1],err) | |
| io.stderr:write(string.format("%s: %s\n",arg[1],err)) | |
| os.exit(exit.CONFIG,true) | |
| end | |
| conffile() | |
| if not CONF.syslog then | |
| CONF.syslog = { ident = "gemini" , facility = "daemon" } | |
| else | |
| CONF.syslog.ident = CONF.syslog.ident or "gemini" | |
| CONF.syslog.facility = CONF.syslog.facility or "daemon" | |
| end | |
| syslog.open(CONF.syslog.ident,CONF.syslog.facility) | |
| if not CONF.address then | |
| CONF.address = "[::]:1965" | |
| CONF._host = "[::]" | |
| CONF._port = 1965 | |
| else | |
| CONF._host,CONF._port = parse_address:match(CONF.address) | |
| if not CONF._host or not CONF._port then | |
| syslog('critical',"%s: syntax error with address",arg[1]) | |
| io.stderr:write(string.format("%s: syntax error with address\n",arg[1])) | |
| os.exit(exit.CONFIG,true) | |
| end | |
| end | |
| if not CONF.hosts then | |
| syslog('critical',"%s: at least one host needs to be defined",arg[1]) | |
| io.stderr:write(string.format("%s: at least one host needs to be defined\n",arg[1])) | |
| os.exit(exit.CONFIG,true) | |
| end | |
| -- ---------------------------------------------------------------------- | |
| -- This expression will canonicalize the address field. If the host is | |
| -- missing, it will be replaced with the "all" address. If the port is | |
| -- missing, it will be replaced with the default port 1965. If the host | |
| -- is '@', it will be replaced by the name of the host. | |
| -- ---------------------------------------------------------------------- | |
| local canon_address do | |
| local Carg = lpeg.Carg | |
| local Cc = lpeg.Cc | |
| local Cs = lpeg.Cs | |
| local P = lpeg.P | |
| local R = lpeg.R | |
| local host = ip.IPv4 | |
| + P"[" * ip.IPv6 * P"]" | |
| + P"@" / "" * Carg(1) | |
| + R("!9",";~")^1 | |
| + Cc(CONF._host) | |
| local port = P":" * R"09"^1 | |
| + Cc(":" .. CONF._port) | |
| canon_address = Cs(host * port) | |
| end | |
| CONF._interfaces = {} | |
| -- ------------------- | |
| -- Process each host. | |
| -- ------------------- | |
| for host,conf in pairs(CONF.hosts) do | |
| if not conf.certificate then | |
| syslog('error',"%s: host %q missing certifiate---can't configure host",arg[1],host) | |
| end | |
| if not conf.keyfile then | |
| syslog('error',"%s: host %q missing keyfile---can't configure host",arg[1],host) | |
| end | |
| local addr = conf.address and canon_address:match(conf.address,1,host) | |
| or CONF.address | |
| if conf.certificate and conf.keyfile then | |
| local info | |
| if not CONF._interfaces[addr] then | |
| info = {} | |
| CONF._interfaces[addr] = info | |
| else | |
| info = CONF._interfaces[addr] | |
| end | |
| table.insert(info,{ | |
| cert = conf.certificate , | |
| key = conf.keyfile , | |
| hostinfo = conf , | |
| }) | |
| end | |
| conf.language = conf.language or CONF.language | |
| conf.charset = conf.charset or CONF.charset | |
| if not conf.authorization then | |
| conf.authorization = {} | |
| end | |
| -- -------------------------------------------- | |
| -- Make sure the redirect tables always exist. | |
| -- -------------------------------------------- | |
| if not conf.redirect then | |
| conf.redirect = { temporary = {} , permanent = {} , gone = {} } | |
| else | |
| conf.redirect.temporary = conf.redirect.temporary or {} | |
| conf.redirect.permanent = conf.redirect.permanent or {} | |
| conf.redirect.gone = conf.redirect.gone or {} | |
| end | |
| -- -------------------------------------------------------------------- | |
| -- If we don't have any handlers, make sure they now exist. | |
| -- If we do have handlers, load them up and initialize them. | |
| -- -------------------------------------------------------------------- | |
| if not conf.handlers then | |
| syslog('warning',"%s: host %q has no handlers",arg[1],host) | |
| conf.handlers = {} | |
| else | |
| local function notfound() | |
| return 51,MSG[51],"" | |
| end | |
| local function loadmod(info) | |
| if not info.path then | |
| syslog('error',"missing path field in handler") | |
| info.path = "" | |
| info.code = { handler = notfound } | |
| return | |
| end | |
| if not info.module then | |
| syslog('error',"%s: missing module field",info.path) | |
| info.code = { handler = notfound } | |
| return | |
| end | |
| local okay,mod = pcall(require,info.module) | |
| if not okay then | |
| syslog('error',"%s: %s",info.module,mod) | |
| info.code = { handler = notfound } | |
| return | |
| end | |
| if type(mod) ~= 'table' then | |
| syslog('error',"%s: module not supported",info.module) | |
| info.code = { handler = notfound } | |
| return | |
| end | |
| info.code = mod | |
| if not mod.handler then | |
| syslog('error',"%s: missing handler()",info.module) | |
| mod.handler = notfound | |
| return | |
| end | |
| info.language = info.language or conf.language | |
| info.charset = info.charset or conf.charset | |
| if mod.init then | |
| okay,err = mod.init(info,conf,CONF) | |
| if not okay then | |
| syslog('error',"%s: %s",info.module,err) | |
| mod.handler = notfound | |
| return | |
| end | |
| end | |
| end | |
| for _,info in ipairs(conf.handlers) do | |
| loadmod(info) | |
| end | |
| end | |
| syslog('info',"host %q configured",host) | |
| end | |
| if not next(CONF._interfaces) then | |
| syslog('critical',"%s: at least one host needs to be configured",arg[1]) | |
| io.stderr:write(string.format("%s: at least one host needs to be configured\n",arg[1])) | |
| os.exit(exit.CONFIG,true) | |
| end | |
| package.loaded.CONF = CONF | |
| end | |
| -- ************************************************************************ | |
| local redirect_subst do | |
| local replace = lpeg.C(lpeg.P"$" * lpeg.R"09") * lpeg.Carg(1) | |
| / function(c,t) | |
| c = tonumber(c:sub(2,-1)) | |
| return t[c] | |
| end | |
| local char = replace + lpeg.P(1) | |
| redirect_subst = lpeg.Cs(char^1) | |
| end | |
| -- ************************************************************************ | |
| local cert_parse do | |
| local Cf = lpeg.Cf | |
| local Cg = lpeg.Cg | |
| local Ct = lpeg.Ct | |
| local C = lpeg.C | |
| local P = lpeg.P | |
| local R = lpeg.R | |
| local name = R("AZ","az")^1 | |
| local value = R(" .","0\255")^0 | |
| local record = Cg(P"/" * C(name) * P"=" * C(value)) | |
| cert_parse = Cf(Ct"" * record^1,function(acc,n,v) acc[n] = v return acc end) | |
| + Ct"" | |
| end | |
| -- ************************************************************************ | |
| local function main(ios) | |
| local request | |
| local auth = | |
| { | |
| _remote = ios.__remote.addr, | |
| _port = ios.__remote.port | |
| } | |
| local function handler() | |
| ios:_handshake() | |
| request = ios:read("*l") | |
| if not request then | |
| ios:write("50 ",MSG[59],"\r\n") | |
| return 59 | |
| end | |
| -- ------------------------------------------------- | |
| -- Current Gemini spec lists URLS max limit as 1024. | |
| -- ------------------------------------------------- | |
| if #request > 1024 then | |
| ios:write("59 ",MSG[59],"\r\n") | |
| return 59 | |
| end | |
| local loc = url:match(request) | |
| if not loc then | |
| ios:write("59 ",MSG[59],"\r\n") | |
| return 59 | |
| end | |
| if not loc.scheme then | |
| ios:write("59 ",MSG[59],"\r\n") | |
| return 59 | |
| end | |
| if not loc.host then | |
| ios:write("59 ",MSG[59],"\r\n") | |
| return 59 | |
| end | |
| if loc.scheme ~= 'gemini' | |
| or not CONF.hosts[loc.host] | |
| or loc.port ~= CONF.hosts[loc.host].port then | |
| ios:write("53 ",MSG[53],"\r\n") | |
| return 53 | |
| end | |
| -- --------------------------------------------------------------- | |
| -- user portion of a URL is invalid. | |
| -- --------------------------------------------------------------- | |
| if loc.user then | |
| ios:write("59 ",MSG[59],"\r\n") | |
| return 59 | |
| end | |
| -- --------------------------------------------------------------- | |
| -- Relative path resolution is the domain of the client, not the | |
| -- server. So reject any requests with relative path elements. | |
| -- Also check for multiple '//' in a path, which I'm treating | |
| -- as invalid. | |
| -- --------------------------------------------------------------- | |
| if loc.path:match "/%.%./" or loc.path:match "/%./" or loc.path:match "//+" then | |
| ios:write("50 ",MSG[59],"\r\n") | |
| return 59 | |
| end | |
| -- -------------------------------------------------------------- | |
| -- Do our authorization checks. This way, we can get consistent | |
| -- authorization checks across handlers. We do this before anything else | |
| -- (even redirects) to prevent unintended leakage of data (resources that | |
| -- might be available under authorization) | |
| -- -------------------------------------------------------------- | |
| for _,rule in ipairs(CONF.hosts[loc.host].authorization) do | |
| if loc.path:match(rule.path) then | |
| if not ios.__ctx:peer_cert_provided() then | |
| ios:write("60 ",MSG[60],"\r\n") | |
| return 60 | |
| end | |
| auth._provided = true | |
| auth._ctx = ios.__ctx | |
| auth.I = ios.__ctx:peer_cert_issuer() | |
| auth.S = ios.__ctx:peer_cert_subject() | |
| auth.issuer = cert_parse:match(auth.I) | |
| auth.subject = cert_parse:match(auth.S) | |
| auth.notbefore = ios.__ctx:peer_cert_notbefore() | |
| auth.notafter = ios.__ctx:peer_cert_notafter() | |
| auth.now = os.time() | |
| if auth.now < auth.notbefore then | |
| ios:write("62 ",MSG[62],"\r\n") | |
| return 62 | |
| end | |
| if auth.now > auth.notafter then | |
| ios:write("62 ",MSG[62],"\r\n") | |
| return 62 | |
| end | |
| local okay,allowed = pcall(rule.check,auth.issuer,auth.subject,loc) | |
| if not okay then | |
| syslog('error',"%s: %s",rule.path,allowed) | |
| ios:write("40 ",MSG[40],"\r\n") | |
| return 40 | |
| end | |
| if not allowed then | |
| ios:write("61 ",MSG[61],"\r\n") | |
| return 61 | |
| end | |
| break | |
| end | |
| end | |
| -- ------------------------------------------------------------- | |
| -- We handle the various redirections here, the temporary ones, | |
| -- the permanent ones, and those that are gone gone gone ... | |
| -- I'm still unsure of the order I want these in ... | |
| -- ------------------------------------------------------------- | |
| for _,rule in ipairs(CONF.hosts[loc.host].redirect.temporary) do | |
| local match = table.pack(loc.path:match(rule[1])) | |
| if #match > 0 then | |
| local new = redirect_subst:match(rule[2],1,match) | |
| ios:write("30 ",new,"\r\n") | |
| return 30 | |
| end | |
| end | |
| for _,rule in ipairs(CONF.hosts[loc.host].redirect.permanent) do | |
| local match = table.pack(loc.path:match(rule[1])) | |
| if #match > 0 then | |
| local new = redirect_subst:match(rule[2],1,match) | |
| ios:write("31 ",new,"\r\n") | |
| return 31 | |
| end | |
| end | |
| for _,pattern in ipairs(CONF.hosts[loc.host].redirect.gone) do | |
| if loc.path:match(pattern) then | |
| ios:write("52 ",MSG[52],"\r\n") | |
| return 52 | |
| end | |
| end | |
| -- ------------------------------------- | |
| -- Run through our installed handlers | |
| -- ------------------------------------- | |
| local found = false | |
| local okay | |
| local status | |
| for _,info in ipairs(CONF.hosts[loc.host].handlers) do | |
| local match = table.pack(loc.path:match(info.path)) | |
| if #match > 0 then | |
| found = true | |
| okay,status = pcall(info.code.handler,info,auth,loc,match,ios) | |
| if not okay then | |
| syslog('error',"request=%q error=%q",request,status) | |
| status = 41 | |
| end | |
| break | |
| end | |
| end | |
| if not found then | |
| syslog('error',"no handlers for %q found---possible configuration error?",request) | |
| ios:write("41 ",MSG[41],"\r\n") | |
| status = 41 | |
| end | |
| return status | |
| end | |
| local status = handler() | |
| syslog( | |
| 'info', | |
| "remote=%s status=%d request=%q bytes=%d subject=%q issuer=%q", | |
| ios.__remote.addr, | |
| status, | |
| request, | |
| ios.__wbytes, | |
| auth and auth.S or "", | |
| auth and auth.I or "" | |
| ) | |
| ios:close() | |
| end | |
| -- ************************************************************************ | |
| local function init_interface(interface,info) | |
| local addr,port = parse_address:match(interface) | |
| local okay,err = tls.listen(addr,port,main,function(conf) | |
| conf:verify_client_optional() | |
| conf:insecure_no_verify_cert() | |
| info[1].hostinfo.port = port | |
| if not conf:keypair_file(info[1].cert,info[1].key) then return false end | |
| for i = 2 , #info do | |
| info[i].hostinfo.port = port | |
| if not conf:add_keypair_file(info[i].cert,info[i].key) then | |
| return false | |
| end | |
| end | |
| return conf:protocols "tlsv1.2,tlsv1.3" | |
| end) | |
| if not okay then | |
| syslog('critical',"%s: %s\n",arg[1],err) | |
| io.stderr:write(string.format("%s: %s\n",arg[1],err)) | |
| os.exit(exit.OSERR,true) | |
| end | |
| end | |
| -- ************************************************************************ | |
| for interface,info in pairs(CONF._interfaces) do | |
| init_interface(interface,info) | |
| end | |
| signal.catch('int') | |
| signal.catch('term') | |
| syslog('info',"entering service @%s",fsys.getcwd()) | |
| nfl.server_eventloop(function() return signal.caught() end) | |
| for host,conf in pairs(CONF.hosts) do | |
| for _,info in ipairs(conf.handlers) do | |
| if info.code and info.code.fini then | |
| local ok,status = pcall(info.code.fini,info) | |
| if not ok then | |
| syslog('error',"%s %s: %s",host,info.module,status) | |
| end | |
| end | |
| end | |
| end | |
| os.exit(exit.OK,true) |