-- ************************************************************************
--
-- Another Gemini mention thingy
-- Copyright 2023 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: globals init handler
-- luacheck: ignore 611
-- gemini://gmi.bacardi55.io/gemlog/2022/02/27/my-take-on-gemlog-replies/
local process = require "org.conman.process"
local syslog = require "org.conman.syslog"
local errno = require "org.conman.errno"
local nfl = require "org.conman.nfl"
local fsys = require "org.conman.fsys"
local tls = require "org.conman.nfl.tls"
local url = require "org.conman.parsers.url"
local mime = require "org.conman.parsers.mimetype"
local blog = require "org.conman.app.mod_blog"
local uurl = require "GLV-1.url-util"
local lpeg = require "lpeg"
local string = require "string"
local io = require "io"
_ENV = {}
-- ************************************************************************
function init(conf)
if not conf.pattern then
return false,"Missing pattern rule"
end
if not conf.url then
return false,"Missing require URL under mention control"
end
local loc = url:match(conf.url)
if not loc then
return false,"Bad URL"
end
conf.url = loc
return true
end
-- ************************************************************************
local statparse do
local Cc = lpeg.Cc
local C = lpeg.C
local P = lpeg.P
local R = lpeg.R
local S = lpeg.S
local status = P"1" * R"09" * Cc'input' * Cc'required' * Cc(true)
+ P"2" * R"09" * Cc'okay' * Cc'content' * Cc(true)
+ P"3" * R"09" * Cc'redirect' * Cc'temporary' * Cc(true)
+ P"4" * R"09" * Cc'error' * Cc'temporary' * Cc(true)
+ P"5" * R"09" * Cc'error' * Cc'permanent' * Cc(true)
+ P"6" * R"09" * Cc'auth' * Cc'required' * Cc(true)
local infotype = S" \t"^1 * C(R" \255"^0)
+ Cc"type/text; charset=utf-8"
statparse = status * infotype
end
-- ************************************************************************
local function checktarget(ios,target,pattern,doctitle)
local line = ios:read("*l")
if not line then
return false
end
if not doctitle and line:match "^#%s*[^#]" then
doctitle = line:match("^#%s*(.*)")
return checktarget(ios,target,pattern,doctitle)
end
if not line:match"^=>" then return checktarget(ios,target,pattern,doctitle) end
local u,title = line:match("^=>%s*(%S+)%s+(.*)")
if not u then
u = line:match9("^=>%s*(%S+)")
title = ""
end
local loc = url:match(u)
if not loc then return checktarget(ios,target,pattern,doctitle) end
if target.scheme ~= loc.scheme then return checktarget(ios,target,pattern,doctitle) end
if target.host ~= loc.host then return checktarget(ios,target,pattern,doctitle) end
if target.port ~= loc.port then return checktarget(ios,target,pattern,doctitle) end
local year,month,day,part = loc.path:match(pattern)
if not year then
return checktarget(ios,target,pattern,doctitle)
end
return doctitle or title,year,month,day,part
end
-- ************************************************************************
local function gemini_fetch(u,location,target,pattern,rcount)
local ios = tls.connect(location.host,location.port,nil,function(conf)
conf:insecure_no_verify_name()
conf:insecure_no_verify_time()
conf:insecure_no_verify_cert()
return conf:protocols "all"
end)
if not ios then
return
end
if not ios:write(u,"\r\n") then
ios:close()
return
end
local statline = ios:read("*l")
if not statline then
ios:close()
return
end
local system,,,info = statparse:match(statline)
if not system then
ios:close()
return
end
if system == 'auth' then
ios:close()
return
elseif system == 'redirect' then
ios:close()
if rcount == 5 then
return
end
local where = url:match(info)
local new = uurl.merge(location,where)
local newloc = uurl.toa(new)
return gemini_fetch(ios,newloc,target,rcount + 1)
elseif system == 'okay' then
info = mime:match(info)
if not info or info.type ~= 'text/gemini' then
ios:close()
return
end
local title,year,month,day,part = checktarget(ios,target,pattern)
ios:close()
return title,year,month,day,part
else
ios:close()
return
end
end
-- ************************************************************************
function handler(conf,,loc,,ios)
local function append(file,location,title)
for line in file:lines() do
local u = line:match("^(%S+)")
if u == location then
return
end
end
file:write(location,"\t",title,"\n")
end
if not loc.query or loc.query == "" then
ios:write("10 URL to send\r\n")
return 10
end
loc.query = loc.query:gsub("%%%x%x",
function(c)
return string.char(tonumber(c:sub(2),16))
end)
local su = url:match(loc.query)
local tu = conf.url
if not su then
ios:write("59 Bad request\r\n")
return 59
end
local title,year,month,day,part = gemini_fetch(loc.query,su,tu,conf.pattern,1)
if not title then
ios:write("59 Bad request\r\n")
return 59
end
local file = blog.filename {
start = { year = year , month = month , day = day } ,
filename = string.format("%d.webmention",part)
}
local f,err = io.open(file,"a+")
if not f then
syslog('error',"%s: %s",file,err)
ios:write("59 bad request\r\n")
return 59
end
-- In order to avoid blocking the entire process, we need to call
-- fsys._lock() with the non-blocking option, and if we would lock, just
-- yield our coroutine and try again. Yes, this may burn some CPU cycles,
-- but it shouldn't be that bad. I can always think on this if it does
-- become an issue.
local function lock()
local okay,err1 = fsys._lock(f,'write',true)
if okay then return okay end
if err1 == errno.EACCESS or err1 == errno.EAGAIN then
nfl.schedule(coroutine.running())
coroutine.yield()
return lock()
else
return okay,err1
end
end
local okay,err2 = lock()
if not okay then
syslog('error',"%s: %s",file,errno[err2])
ios:write("40 Temporary failure\r\n")
return 40
end
append(f,uurl.toa(su),title)
fsys._lock(f,'release',true)
f:close()
ios:write("20 text/plain\r\nIt's been accepted. Thank you.\n")
if conf.update then
local child,err1 = process.fork()
if not child then
syslog('error',"fork() = %s",errno[err1])
elseif child == 0 then
process.exec(conf.update.cmd,conf.update.args,conf.update.env)
process.exit(1)
else
local info,err3 = process.wait(child)
if not info then
syslog('error',"wait() = %s",errno[err3])
else
if info.status ~= 'normal' or info.rc ~= 0 then
syslog('error',"%s: status=%q description=%q rc=%d",info.status,info.description,info.rc)
end
end
end
end
if conf.email then
local mail = io.popen("/usr/sbin/sendmail " .. conf.email,"w")
if mail then
mail:write(
string.format("From: <%s>\n",conf.email),
string.format("To: <%s>\n",conf.email),
"Subject: Gemini mention 2, sir!\n",
"\n",
string.format("From: %s\n",ios.__remote.addr),
string.format("%s\n",loc.query),
"\n",
"Info: gemini://gmi.bacardi55.io/gemlog/2022/02/27/my-take-on-gemlog-replies/\n"
)
mail:close()
end
end
return 20
end
-- ************************************************************************
return _ENV
text/plain; charset=us-ascii
This content has been proxied by September (ba2dc).