-- ************************************************************************ -- -- 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 . -- -- 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