-- ************************************************************************

--

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

Proxy Information
Original URL
gemini://gemini.conman.org/extensions/GLV-1/handlers/mention.lua
Status Code
Success (20)
Meta
text/plain; charset=us-ascii
Capsule Response Time
555.520266 milliseconds
Gemini-to-HTML Time
4.751761 milliseconds

This content has been proxied by September (ba2dc).