💾 Archived View for gemini.conman.org › extensions › GLV-1 › handlers › gemmention.lua captured on 2023-07-10 at 15:15:18.
⬅️ Previous capture (2023-01-29)
-=-=-=-=-=-=-
-- ************************************************************************ -- -- Gemini mentions, a proof-of-concept -- Copyright 2022 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 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" local pairs = pairs _ENV = {} -- ************************************************************************ function init(conf) if not conf.pattern then return false,"Missing pattern rule" end if not conf.file then return false,"Missing required Gemini text file" end return true end -- ************************************************************************ local parse_list_expr do local function doset(dest,name,value) if not dest[name] then dest[name] = value end return dest end local xdigit = lpeg.locale().xdigit local char = lpeg.P"%" * lpeg.C(xdigit * xdigit) / function(c) return string.char(tonumber(c,16)) end + lpeg.P(1) local name = lpeg.Cs((char - (lpeg.P"=" + lpeg.P"&"))^1) local value = lpeg.Cs((char - lpeg.P"&")^1) + lpeg.Cc"" local pair = name * (lpeg.P"=" * value)^-1 * lpeg.P"&"^-1 parse_list_expr = lpeg.Cf(lpeg.Ct"" * (lpeg.Cg(pair))^0,doset) 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,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,doctitle) end if not line:match"^=>" then return checktarget(ios,target,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,doctitle) end if target.scheme ~= loc.scheme then return checktarget(ios,target,doctitle) end if target.host ~= loc.host then return checktarget(ios,target,doctitle) end if target.port ~= loc.port then return checktarget(ios,target,doctitle) end if target.path ~= loc.path then return checktarget(ios,target,doctitle) end return doctitle or title end -- ************************************************************************ local function gemini_fetch(u,location,target,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 = checktarget(ios,target) ios:close() return title 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 == "" or loc.query == 'gemini-mention' then local f,err = io.open(conf.file,"r") if not f then syslog('error',"%s: %s",conf.file,err) ios:write("40\r\n") return 40 end ios:write("20 text/gemini\r\n") repeat local data = f:read(1024) if data then ios:write(data) end until not data f:close() return 20 end local args = parse_list_expr:match(loc.query) if not args.source or not args.target then ios:write("59 Bad request\r\n") return 59 end local su = url:match(args.source) local tu = url:match(args.target) if not su or not tu then ios:write("59 Bad request\r\n") return 59 end if tu.host ~= loc.host or tu.port ~= loc.port then ios:write("59 Bad request\r\n") return 59 end local year,month,day,part = tu.path:match(conf.pattern) if not year then ios:write("59 Bad request\r\n") return 59 end local title = gemini_fetch(args.source,su,tu,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, sir!\n", "\n", string.format("From: %s\n",ios.__remote.addr), "Expected Values:\n", string.format("\tsource: %s\n",args.source), string.format("\ttarget: %s\n",args.target), "\n", "All values:\n" ) for name,value in pairs(args) do mail:write(string.format("%s: %s\n",name,value)) end mail:close() end end return 20 end -- ************************************************************************ return _ENV